Wheel of Fortune - Player Controller
This post is a part of a more extensive project entry. If you are interested in reading more of my insights, make sure to check it out and click >> here<< !
In this post, I would like to look at how a player can interact with this virtual world. As Unreal Engine standards suggest, PlayerController
is the place for implementation of this kind of logic:
The PlayerController implements functionality for taking the input data from the player and translating that into actions, such as movement, using items, firing weapons, etc.
So there are two topics to cover here - input data and actions. For now, let's focus on the latter one. In the simplest version of this game show, I enlisted the following possible movements:
I can distinguish three subjects these actions are affecting. The first one is a wheel pawn, as I need to make it spin by clicking on some button. The next one is a puzzle pawn. It should receive information about the guessed letter. And the last one is game mode or some intermediate object, which should handle the contestant's requests like buying a vowel or passing a turn. And so I started by making sure all enlisted entities above provide public methods triggering these actions. So there are, for example:
public:
...
UFUNCTION(BlueprintCallable, Category="Wheel")
void Spin();
public:
...
UFUNCTION(BlueprintCallable, Category="Word Puzzle")
int GuessConsonant(const FString& Consonant);
public:
...
UFUNCTION(BlueprintCallable, Category="Wheel Of Fortune")
void HandleContestantPass();
After fetching wheel, pawn, and GameMode actors, I can call these actions from within PlayerController. That's a good start.
void AMainPlayerController::ConnectToPawnsAndGameMode()
{
Wheel = Cast<AWheel>(UGameplayStatics::GetActorOfClass(GetWorld(), AWheel::StaticClass()));
WordPuzzle = Cast<AWordPuzzle>(UGameplayStatics::GetActorOfClass(GetWorld(), AWordPuzzle::StaticClass()));
GameMode = Cast<AStandardMode>(UGameplayStatics::GetActorOfClass(GetWorld(), AStandardMode::StaticClass()));
}
And now it's time to think about a flow of triggering these actions. Let's take an example of spinning the wheel - the contestant can perform it only in specified cases - when the turn begins or after saying a correct consonant. Similarly, as we advance, the player can only buy a vowel once guessing a consonant. In addition, purchasing a vowel is an alternative move to turning the wheel. What was my first idea for managing this flow? Using booleans and implementing improvised Finite State Machine. At first, this idea seemed sufficient. However, the number of rules soon grew, and things started to become chaotic.
Finite State Machine approach
To demonstrate how uncontrollable the idea of using booleans became, I will start from PlayerController
's point of view. Because the GameMode
is the referee and keeps track of all ongoing rules, the PlayerController
needs to ask it every time if the requested action is allowed now, right?
void AMainPlayerController::BuyVowel()
{
if (GameMode->IsVowelBuyEnabled())
{
GameMode->HandleVowelBuyRequest();
}
}
void AMainPlayerController::SpinWheel()
{
if (GameMode->IsWheelSpinEnabled())
{
Wheel->Spin();
}
}
The game mode bases its judgment on flags indicating allowed actions.
bool AStandardMode::IsVowelBuyEnabled() const
{
return CurrentRoundID >= 0 && bEnableVowelBuy && !bVowelGuessEnabled;
}
bool AStandardMode::IsWheelSpinEnabled() const
{
return CurrentRoundID >= 0 && bWheelSpinEnabled;
}
And so, the contestant could spin the wheel only when the previous contestant failed or passed or after successfully guessing consonants.
void AStandardMode::NextContestant()
{
CurrentContestantID = FMath::Fmod(CurrentContestantID + 1, ContestantsCount);
AllowWheelSpin();
TurnChanged.Broadcast(CurrentContestantID);
}
void AStandardMode::HandleGuessedConsonant()
{
ContestantSucceeded.Broadcast(CurrentContestantID);
SetWheelSpinState(false);
}
"For now, it looks good enough. What's the problem?" Let's zoom inside and see how game mode assigns allowed-action flag values.
void AStandardMode::AllowWheelSpin(bool bFirstTime)
{
bWheelSpinEnabled = true;
bConsonantGuessEnabled = false;
bVowelGuessEnabled = false;
bExpressGuessMode = false;
bWaitingForMysteryResolve = false;
bExpressGuessMode = false;
if (!bFirstTime)
{
bEnableVowelBuy = true;
bEnableWildCardUse = true;
UE_LOG(LogTemp, Warning, TEXT("%s >> Interaction options: [SPIN] or [BUY VOWEL] or [WILD CARD] or [PASS]"), ANSI_TO_TCHAR(__FUNCTION__));
}
else
{
bEnableVowelBuy = false;
bEnableWildCardUse = false;
UE_LOG(LogTemp, Warning, TEXT("%s >> Interaction options: [SPIN] or [PASS]"), ANSI_TO_TCHAR(__FUNCTION__));
}
}
Yack! That looks horrible! I bet you can imagine a similar mechanism for AStandardMode::EnableVowelBuying()
. In the end, this approach resulted in tons of booleans sneaking around the GameMode
class. The more rules, the harder it was to control the chaos. For this reason, I decided to refactor PlayerController
and use State Pattern.
State Pattern approach
For those who are not familiar with State Pattern, I will quote Robert Nystrom's book:
"Allow an object to alter it's behaviour when it's internal state changes. The object will appear to change its class"
Decoding this message, in this case, one of the ways to implement it is to create an object that encapsulates the logic for handling actions in a given moment of the game. Or I should say bunch of objects, as in a different moment a different action is allowed, and we want to adjust the logic after performing the given action.
And so, the PlayerController
should pass received player input directly to the current state object and let it handle it. And the state, after resolving the action, should decide which state comes next. In the end it means that almost all logic of handling actions shifts from PlayerController
into states objects, freeing the first one from burden of managing it. And because states decides by themselves which state comes next, the flow of allowed actions can be coded inside these small objects. Without further ado, let's start from defining new member of the controller, which will hold the current state:
private:
UPROPERTY()
UControllerState* State;
Now, what shall be inside this state object? We know that it should be able to handle players' input, like BuyVowel
, SpinWheel
, UseWildCard
, and so on. So let's create a base class for derived states:
UCLASS(Abstract)
class WHEELOFFORTUNE_API UControllerState : public UObject
{
GENERATED_BODY()
public:
virtual void Enter() {}
...
virtual UControllerState* BuyVowel(AMainPlayerController* Controller)
{
UE_LOG(LogTemp, Warning, TEXT("%s >> Not available in current state!"), ANSI_TO_TCHAR(__FUNCTION__));
return nullptr;
}
virtual UControllerState* SpinWheel(AMainPlayerController* Controller)
{
UE_LOG(LogTemp, Warning, TEXT("%s >> Not available in current state!"), ANSI_TO_TCHAR(__FUNCTION__));
return nullptr;
}
};
Note that if we derive a new state and do not override a method like SpinWheel
, if the player request to spin the wheel while this state is active, only a warning string will be printed. And so every state that inherits from the base class should override only actions allowed at its purposed moment. Otherwise, the request will be ignored. Let's look at a concrete implementation of a state describing a player's range of possibilities after guessing a letter.
UCLASS()
class WHEELOFFORTUNE_API UControllerState_SuccessfulAswer : public UControllerState
{
GENERATED_BODY()
public:
virtual void Enter() override;
virtual UControllerState* SpinWheel(AMainPlayerController* Controller) override;
virtual UControllerState* BuyVowel(AMainPlayerController* Controller) override;
virtual UControllerState* UseWildCard(AMainPlayerController* Controller) override;
virtual UControllerState* Pass(AMainPlayerController* Controller) override;
};
Now let's imagine that the contestant wants to spin the wheel at some point. He or she clicks a button on the controller, and PlayerController
dispatches the received request. First, it ensures that any state is assigned. If it is, it does not matter if the player is actually in an appropriate moment to ask for this action. If the contestant is currently guessing the consonant, the SpinWheel
method in the current GuessingConsonant
state is not implemented. Hence only the "Not available in current state!"
the message will be printed in the console. Otherwise, the appropriate logic will be triggered.
void AMainPlayerController::SpinWheel()
{
if (IsValid(State))
{
const auto NewState = State->SpinWheel(this);
if (NewState)
{
State = NewState;
State->Enter();
}
}
}
Remember when I wrote that the "PlayerController
should pass received player input directly to the current state object" and that we need to "switch them after performing given action accordingly"? Now it should make much more sense : )
Note: To avoid cyclic reference between PlayerController
and State
, I used deferred declaration in Controller's header.
#include "MainPlayerController.generated.h"
class UControllerState;
UCLASS()
class WHEELOFFORTUNE_API AMainPlayerController : public APlayerController
{
...
Below you can see the actual implementation of the SuccessfulAnswer
state. I use the Enter()
method to inform the player about possible actions. In this case, I display a humble string inside the console. A technique below, the SpinWheel
first ensures that PlayerController
has been properly assigned (can be skipped?) and then calls the Spin
method of the Wheel
class. After spinning, the contestant needs to wait until the wheel finishes rotating. Hence the state returns a new state, called Wait
, which has no action overridden = no input will trigger any effect until the state changes.
void UControllerState_SuccessfulAnswer::Enter()
{
if (bFirstTimeInTurn)
{
UE_LOG(LogTemp, Warning, TEXT("%s >> Input available: [SPIN]"), ANSI_TO_TCHAR(__FUNCTION__));
}
else
{
UE_LOG(LogTemp, Warning, TEXT("%s >> Input available: [SPIN] or [BUY VOWEL] or [USE WILD CARD] or [PASS]"), ANSI_TO_TCHAR(__FUNCTION__));
}
}
UControllerState* UControllerState_SuccessfulAnswer::SpinWheel(AMainPlayerController* Controller)
{
if (IsValid(Controller))
{
Controller->Wheel->Spin();
return NewObject<UControllerState_Wait>(Controller);
}
return nullptr;
}
UControllerState* UControllerState_SuccessfulAnswer::BuyVowel(AMainPlayerController* Controller)
{
if (IsValid(Controller) && !bFirstTimeInTurn)
{
if (Controller->GameMode->HandleVowelBuyRequest())
{
return NewObject<UControllerState_VowelGuess>(Controller);
}
}
return nullptr;
}
...
While the player is waiting, the PlayerController
waits for Wheel
to broadcast a delegate informing that wheel landed on a specific wedge.
void AMainPlayerController::BeginPlay()
{
Super::BeginPlay();
ConnectToPawnsAndGameMode();
}
void AMainPlayerController::ConnectToPawnsAndGameMode()
{
Wheel = Cast<AWheel>(UGameplayStatics::GetActorOfClass(GetWorld(), AWheel::StaticClass()));
if (Wheel)
{
Wheel->WheelLanded.AddDynamic(this, &AMainPlayerController::OnWheelLanded);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("%s >> Wheel pawn is not on the map!"), ANSI_TO_TCHAR(__FUNCTION__));
}
...
When it does, if the wedge should not be resolved immediately (e.g., cash wedge), the PlayerController
moves to a guessing consonant state, and the game keeps moving on. Otherwise, if the player draws, for example, a 'Bankrupt' wedge, the consequences are immediate.
void AMainPlayerController::OnWheelLanded(TSubclassOf<UWedge> LandedOn)
{
CurrentWedge = NewObject<UWedge>(GetTransientPackage(), LandedOn);
if (CurrentWedge->bInstantResolve)
{
ResolveCurrentWedge(0);
}
else
{
State = NewObject<UControllerState_GuessConsonant>(this);
State->Enter();
}
}
On top of that, PlayerController
during initialization becomes a listener to GameMode
delegates allowing the adoption of state when game flow requires that.
GameMode = Cast<AStandardMode>(UGameplayStatics::GetActorOfClass(GetWorld(), AStandardMode::StaticClass()));
if (GameMode)
{
GameMode->TurnChanged.AddDynamic(this, &AMainPlayerController::OnTurnChanged);
GameMode->RoundChanged.AddDynamic(this, &AMainPlayerController::OnRoundChanged);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("%s >> Wrong game mode!"), ANSI_TO_TCHAR(__FUNCTION__));
}
For example, when at some point GameMode decides that the player needs to switch, PlayerController
changes the state to the initial UControllerState_StartTurn
state, ready to accept the following contestant's spin.
void AMainPlayerController::OnTurnChanged(int EventIndex)
{
State = NewObject<UControllerState_StartTurn>(this);
State->Enter();
}
So, as you can see, properly planning the structure of the PlayerController
using State Pattern allows you to manage crisscrossing rules and actions transparently. There is also nothing stopping you from adding new mechanics. To decrease coupling even further, I could create an interface for each action, e.g., IAction_WheelSpin
, and inherit them in the PlayerController
. This would make creating unique rules even more straightforward in the cost of increasing the count of classes. But to keep it simple, I will leave it like this.
For readers who are visual learners, I have created a diagram of interactions between the PlayerController and other game elements with which it directly and indirectly, exchanges information.
Having understood the mechanics of the abstract PlayerController
class's mechanics, let's look at its concrete implementation. Since I have used the OSC protocol to control visual devices very often in my professional life in the events industry, and it's a very flexible solution, I also decided to use it here. But before I get to the description of the mechanics in Unreal Engine, I will briefly describe the process of creating a control board, which I display on the phone to control the gameplay. In the simplest version, one participant can pass the phone to the next one after a fail.
Creating a control panel
The best way I know of to create a control board using the OSC protocol is to use the TouchOSC application. It has an intuitive interface and excellent documentation, making it the ideal tool for this task.
Knowing what actions the player can perform, I created a board for the player, shown below on the left. Each button has an OSC output address assigned to it, which sends information to the server launched in Unreal Engine. For example, by clicking the letter K, the participant sends a request at /consonant/k
, and by clicking the SPIN button, the information comes in at /spin
. Addressing should be straightforward for ease of later decoding in the player controller.
To play the role of the presenter, I need a separate board control with the ability to inform the game, for example, about the right or wrong answer given. For this purpose, I created a second page on TouchOSC, where, among other things, clicking the SOLVE PUZZLE button sends a signal at /admin/solve_puzzle
. I also put a control for controlling the camera vision and my favorite button for firing fireworks.
Such a layout is then loaded into the phone. Next, connection settings need to be configured so that it connects over WiFi to the computer on which the Wheel of Fortune game is running. I won't go into details here because, as I mentioned, TouchOSC has great documentation.
Concrete PlayerController OSC implementation
Since we have a working control board, it is time to set up an OSC server in Unreal Engine and connect to it. At the very beginning, after BeginPlay
I create a server and connect to it a listener waiting for signals from the On Osc Message Received
delegate.
Inside the Handle OSC Message Received
function, all interpretation of the OSC signal happens. First, I need to determine if the received signal is constructed of one part (e.g. /spin
), or two parts (e.g. /consonant/k
):
If it's a unary command, the simplest thing in the world is to call the desired action. Thanks to self-managing states, I can do this without unnecessary checks and let all the magic happen under the hood in C++.
In case the command is constructed out of two-part, I consider the possibility of guessing the consonant, vowel, or action of the presenter.
What can be impoved?
Returning to the ControllerState
abstraction, I informed the player about possible actions by printing a string to the console when the state changed. The Enter
method could be refactored to accept some logger. By default, it could simply print text to the console, as it's happening now. However, when using the OSC version of PlayerController
, the Enter
method could receive OscLogger
that could turn the buttons on and off and change their colors on the control board by sending OSC signals to our phones. I will leave this idea just for future consideration.
Conclusion
And that's it when it comes to the PlayerController
I created for this game. By hiding all the mechanics under the hood using C++, I could easily create an OSC controller by prototyping in Blueprints. Of course, nothing prevents you from coding this entire implementation in C++. I, however, stopped with Blueprints in this case.
Make sure to check out the remaining parts of the entire project entry. Click >> here<< to go to the Table of Content of the project.
Links and files
TouchOSC layout: >>> LINK <<<