Thomas Winged

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.

Source: docs.unrealengine.com

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:

actions.webp

Available actions

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:

AWheel.h
public:
    ...
    UFUNCTION(BlueprintCallable, Category="Wheel")
    void Spin();

AWordPuzzle.h
public:
    ...
    UFUNCTION(BlueprintCallable, Category="Word Puzzle")
    int GuessConsonant(const FString& Consonant);

AStandardMode.h
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.

MainPlayerController.cpp - fetching actors in BeginPlay
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?

MainPlayerController.cpp - asking about the requested action
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.

StandardMode.cpp - using flags, game mode decides if specific action is allowed
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.

StandardMode.cpp - allowing to spin the wheel
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.

StandardMode.cpp - setting flags
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__));
    }
}

finite_state_machine.webp

Tangled flow of a game that I wanted to solve using booleans

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:

AMainPlayerController.h - defining current state object
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:

ControllerState.cpp - a slice of a base class for 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.

ControllerState_Spin.h - overrides only allowed and compatible actions
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.

MainPlayerController.cpp - requesting a wheel spin
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.

MainPlayerController.h - deffered declaration of the ControllerState class
#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.

ControllerState_SuccessfulAnswer.cpp - a slice of
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.

MainPlayerController.cpp - connecting a wheel delegate to a method that changes state
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.

MainPlayerController.cpp - resolving the wedge or changing the state after the wheel stops
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.

MainPlayerController.cpp - connecting to GameMode delegates
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.

MainPlayerController.cpp - switching to the initial state after the contestant bankrupt
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.

player_controller.webp

Diagram of PlayerController interactions

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.

touchosc_board.webp

TouchOSC Editor, contestant panel

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.

touchosc_board-presenter.webp

TouchOSC Editor, presenter panel

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.

touchosc_board_rl.webp

TouchOSC control board on phone's screen

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.

221129085500-rfJydZo5Wk.webp

Creating server and connecting event listener

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):

221129085701-Yje58lVKcD.webp

Splitting signal into command and parameter

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++.

221125091317-BlHDnKV4Hy.webp

Handling unary commands

In case the command is constructed out of two-part, I consider the possibility of guessing the consonant, vowel, or action of the presenter.

221129090145-mTEuN4Xn8X.webp

Handling the two-part command

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 <<<