Thomas Winged

hero image

Wheel of Fortune - Puzzle Board


Overview

It's time to look at the board displaying a phrase that needs to be guessed. There will be three actors I will cover in this post - the WordPuzzle board, which is constructed of PuzzleBlock's, which instances are pooled inside ActorsPool. I decided to introduce such a distinction to separate the visuals and additional mechanics of a single piece of the puzzle from the logic of arranging them into a specific shape. On top of that, when one round ends and another starts, the board must present a new phrase, usually made from the different count of blocks. So I use pooling to recycle spawned blocks and make the game run smoothly.

In this post, I will follow along with created diagrams explaining the flow of logic related to this actor.

puzzle-0_overview.webp

General overview of logic regarding puzzle board

Pick a phrase

I decided to make GameMode the actor that keeps records of all phrases and broadcasts them on round change, which ultimately is received by WordPuzzle. Each round has a different pass and associated hint. For a quick review, this is how the selection of the phrase looks like:

StandardMode.cpp - a snippet of logic handling selection of round's phrase
void AStandardMode::NextRound()
{
    ...

    // Set round passphrase
    FRoundPhrase RoundPassphrase;
    if (RoundPassphrases.IsValidIndex(CurrentRoundID))
    {
        RoundPassphrase = RoundPassphrases[CurrentRoundID];
    }
    ...

    const FRoundInfo Info = FRoundInfo(CurrentRoundID, TopDollarValue, RoundPassphrase);

    UE_LOG(LogTemp, Warning, TEXT("%s >> Round #%d started, round's top dollar value: %d"), ANSI_TO_TCHAR(__FUNCTION__), CurrentRoundID, TopDollarValue);

    RoundChanged.Broadcast(Info);
    ...
}

221212072749-nOuMwRyKEP.webp

Phrases can be specified directly in the editor.

For more details, please check out my post about GameMode object #todo.

Construct a board

Since we got the phrase we want to display on the board, let's construct it using this information. Let's take a look at the diagram below:

puzzle-2_construct_a_board.webp

General overview of logic behind constructing the board

Calculate the board size

We will need to correlate the maximum length of a single passphrase line with the count of horizontal blocks and the count of lines with the vertical height of the wall. In addition, I will add control over padding around the board with empty blocks.

221212075921-PGHnCg8ADu.webp

Calculating the board size

Note: You will notice that many blueprints could be moved to C++, for example, the above method. But in this case, I neglected it. I implemented only the most generic functions and moved to the process of prototyping in Blueprints to speed up the creation and to make it easier to modify by non-programming game designers.

221212075111-R75p2ay46m.webp 221212075139-X8fsOrb3BJ.webp
On the left - no extra padding, on the right - two blocks of extra padding

Calculate location of blocks

Next, using basic XY for loop using Final Blocks Count I calculated locations for all of the blocks. I added some randomization in position and rotation to make it look less sterile.

221212080327-bAx4OadWkM.webp

Calculating transform of a block
221212081227-apvsvbE77e.webp 221212081312-rC0NW9059E.webp
On the left - no randomization; on the right - introduced randomization of blocks placement

Construct letter blocks

puzzle-3_construct_letter_blocks.webp

Logic behind constructing puzzle blocks

The actual construction of a letter block is a more extensive. In the first version, I used Instanced Static Mesh to generate all blocks, similarly to what I did with debug wedges in the case of the wheel pawn. But soon, I decided I wanted to add some physics to it (more about it in the last part of this post) and animation, and I was unaware of how to do it using instanced on GPU meshes. So I switched to spawning detached PuzzleBlock actors. The prototype was working, but soon I encountered a problem of a micro lag when rounds switched, and the board generated new blocks. It was because destroying old blocks and spawning new actors required some computation and allocating new computing resources. It was not THAT bad, but it was a visible stutter. I decided to work on that, and I chose to use:

Object Pooling

For those who are not familiar with Object Pooling, I will quote Robert Nystrom's book as I appreciate his contribution to the topic of programming patterns used in game designs:

"Improve performance and memory use by reusing objects from a fixed pool instead of allocating and freeing them individually."

For this task, I had to create two new classes. The first one, ActorsPool handles fetching required and returning unused objects to the pool. The second one, PooledActor held just a simple boolean flag indicating if the resource is currently used. The latter became a parent class for PuzzleBlock, allowing it to be pooled.

Let's start by taking a look at ActorsPool. Its first primary job is to initialize the pool by instantiating a fixed count of instances. For this, I need an int PoolSize variable that will be high enough to ensure that no additional blocks will ever need to spawn. I could make it grow dynamically, but I assumed it an overkill. I also need to specify the class of actor that inherits the PooledActor class that will be spawned and an array that will store references to instances.

ActorsPool.h - variables associated with pool initialization
public:
    int PoolSize = 100;

    UPROPERTY(EditAnywhere, Category="Object Pooling")
    TSubclassOf<APooledActor> PooledActorType;

private:
    void InitializePool();

    UPROPERTY()
    TArray<APooledActor*> Pool;

Next, I iterate over a count of required instances and spawn them. I make the pooled actor ready to use by setting its flag to false:

ActorsPool.cpp - initializing the pool
void AActorsPool::InitializePool()
{
  ...
    if (PooledActorType)
    {
        if (UWorld* const World = GetWorld())
        {
            for (int i = 0; i < PoolSize; i++)
            {
                APooledActor* PooledActor = World->SpawnActor<APooledActor>(PooledActorType, GetActorLocation(), FRotator::ZeroRotator);
                if (ensureMsgf(PooledActor, TEXT("Could not create pooled actor during pool initialization!")))
                {
                    PooledActor->AttachToActor(this, FAttachmentTransformRules::KeepRelativeTransform);
                    PooledActor->SetActive(false);
                    Pool.Add(PooledActor);
                }
            }

            UE_LOG(LogTemp, Warning, TEXT("Created actors pool"));
        }
    }
}

221212084856-GBRkOCJcn2.webp

To make these settings accessible to non-programming game designers, I exposed them to Blueprints

And since I want this initialization happens every time the value changes, I trigger it inside PostEditChangeProperty:

ActorsPool.cpp - initializing the pool on value change
#if WITH_EDITOR
void AActorsPool::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
    Super::PostEditChangeProperty(PropertyChangedEvent);
    InitializePool();
}
#endif

Now let's take a small peak inside PooledActor to how this activation flag is coded and what happens when we turn it off.

PooledActor.h - declaration of attributes
public:
    UFUNCTION(BlueprintCallable, Category="Object Pooling")
    void SetActive(bool bInActive);

    UFUNCTION(BlueprintCallable, Category="Object Pooling")
    bool IsActive();

    UFUNCTION(BlueprintCallable, Category="Object Pooling")
    void ReturnToPool();

private:
    UPROPERTY()
    bool bActive;

And for the logic - it is simply hiding the actor when it is not used and turning off its physics.

PooledActor.cpp
void APooledActor::SetActive(bool bInActive)
{
    bActive = bInActive;
    SetActorEnableCollision(bInActive);
    SetActorHiddenInGame(!bActive);
}

bool APooledActor::IsActive()
{
    return bActive;
}

void APooledActor::ReturnToPool()
{
    SetActive(false);
}

This knowledge will help us understand how ActorsPool manage pooled objects. I decided that these two methods were enough to control the pool as I needed to fetch an object from the collection during the board's construction and return all actors after solving the puzzle.

ActorsPool.h - two main public functions
public:
    UFUNCTION(BlueprintCallable, Category="Object Pooling")
    bool GetActorFromPool(APooledActor*& Actor);

    UFUNCTION(BlueprintCallable, Category="Object Pooling")
    void ReturnAllActorsToPool();

Getting the actor from the pool is as simple as iterating over all spawned instances and yielding an instance that is not active - not visible.

ActorsPool.cpp getting an actor from the pool
bool AActorsPool::GetActorFromPool(APooledActor*& Actor)
{
    for (const auto PooledActor : Pool)
    {
        if (!PooledActor->IsActive())
        {
            Actor = PooledActor;
            return true;
        }
    }

    return false;
}

And what happens when we want to return all blocks to the pool? We hide them and, just in case, reset their transform property.

ActorsPool.cpp - returning all actors to the pool
void AActorsPool::ReturnAllActorsToPool()
{
    for (const auto PooledActor : Pool)
    {
        PooledActor->SetActive(false);
        PooledActor->SetActorTransform(FTransform(GetActorLocation()));
    }
}

This mechanism allows us to spawn as many instances as our PC can handle during runtime. And we only pay the price of creation once during the game's startup when we initialize the pool. Having object pooling covered, let's get back to WordPuzzle actor and see how it utilizes this idea. Immediately after calculating block's transform, I fetch the instance from the pool and assign a proper location and rotation to it:

221212091335-d8U5HBlHhE.webp

Placing a new block on the map

The FetchBlockFromPool method requests one actor from the pool and casts it to our actual PuzzleBlock implementation. It resets its properties which I will describe later, and, finalizing, adds it to BlockEntries array, which holds all cubes used in the current puzzle board representation.

AWordPuzzle.cpp - preparing a block fetched from the pool
bool AWordPuzzle::FetchBlockFromPool(APuzzleBlock*& Block)
{
    if (PuzzleBlocksPool != nullptr)
    {
        APooledActor* PooledActor;
        if (PuzzleBlocksPool->GetActorFromPool(PooledActor))
        {
            PooledActor->SetActive(true);
            APuzzleBlock* PooledBlock = CastChecked<APuzzleBlock>(PooledActor);
            PooledBlock->ResetBlock();

            Block = PooledBlock;

            BlockEntries.Add(PooledBlock);

            return true;
        }
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("No PuzzleBlocksPool selected!"))
    }

    return false;
}

Removing all cubes from the map simply utilizes the APooledActors::ReturnAllActorsToPool method so I won't go into details here. Finally having a block ready, let's go forward to the process of setting up the block's virtual representation.

Puzzle Block

Before diving into the visual part, I would like to mark that the backend of PuzzleBlock is just a subtle extension of the APooledActor. What's essential for us to know right now is that it manages three variables - FString Letter, bool bIsEmpty, and bool bIsShown. The first one informs which letter this block should represent. The second is an extension of the first one which helps me a little in the logic, as it signalizes if the block is a letter block or a surrounding block. The last one states whether the block has been guessed and is rotated towards the player.

PuzzleBlock.h - a heart of the backend
private:
    bool bIsEmpty = true;
    bool bIsShown = false;
    FString Letter = " ";

Three public functions are exposed to Blueprints vital to the concrete implementation of the block - initialization, displaying mechanism, and resetting. All of them are meant to be implemented in the editor and be strictly connected to logic coupled to visual representation:

PuzzleBlock.h - the thee vital methods
public:
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Puzzle Block")
    void InitializeBlock(bool bIsBlockEmpty, const FString& BlockLetter);

    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Puzzle Block")
    void ShowLetter(bool bUseTransition = true);

    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Puzzle Block")
    void ResetBlock();

And the last significant bit of the puzzle block is its ability to convert its letter to ASCII code. I will show its usage later down the road.

PuzzleBlock.cpp - converting a single character to ASCII
int APuzzleBlock::GetLetterAsciiCode()
{
    return int(*TCHAR_TO_ANSI(*Letter));
}

So now that we got the whole C++ code for the block covered, let's head to the editor and look at how the visual side of the block works.

221212103004-lrfN7IIDy3.webp

The puzzle block's visual representation - a wooden toy block

Let's think for a moment about displaying a letter on that tiny piece of the puzzle. My first idea was to create 26 different textures depicting another symbol. Then I could create a TMap<FString, UTexture> somewhere in the PuzzleBlock and pass an appropriate letter texture to the material. It could work, but storing 26 separate textures is not very efficient, and... it's boring. What's an alternative? Store all letters inside one big texture atlas.

T_LettersArray.webp

The texture atlas containing all letters

But how to select one letter from one image containing a grid of many letters? By using a 2dArrayLookupByIndex node inside Materials. I enclosed this approach inside a MF_ChooseLetter material function, in which I had to scale and translate the texture to snug nicely onto the UV of the cube and apply some color. Notice the LetterIndex variable - it controls which letter is visible on the block. I will use control it later inside BP_PuzzleBlock_Toy to set the symbol on the cube via Material Instance Dynamic.

MF_ChooseLetter.webp

A material function that selects a portion of a texture atlas

Then I used this material function inside the toy's block primary material where I blended it with albedo texture:

221212102820-zMB7Q575qn.webp

Usage of MF_ChooseLetter material function

Given the fully working block's material, it's time to finally initialize it. I will repeat the diagram from above to refresh the logic flow.

puzzle-3_construct_letter_blocks.webp

Logic behind constructing puzzle blocks

Starting with creating a Dynamic Material Instance, I apply subtle randomization to its luminance. Then if the block contains a letter, I tint it, so it's visible when it's rotated back to the player. Then I apply random color to the letter, and finally, I select the letter from the texture atlas. Here you can see the usage of the GetLetterAsciiCode method I mentioned earlier.

InitializeBlock.webp

Initialization of the puzzle block

All of this above gives us beautifully constructed pooled blocks whose appearance is controlled via material parameters. Isn't this neat?

221212104730-O4CeeoS81h.webp

Result so far

Hint text

Let's take a look at the more general diagram again. We have constructed the puzzle blocks; let's move ahead.

puzzle-2_construct_a_board.webp

General overview of logic behind constructing the board

Without the hint, nobody would be able to guess the phrase (or maybe there is already such a version of this game show?). So let's add it quickly. I will create TextRenderComponent inside WordPuzzle, to which I will assign the text when a SetPuzzle method is called (more about it later).

221212105530-9YRwK4AJO6.webp

BP_WordPuzzle - setting the text of a hint

I will also make the position of the hint controllable by a sexy 3D widget, as I want to place it in front of a horizontal block that is a part of a level design.

221212110122-fByIVFVUwG.webp

The location of the hint is easily controlled by 3D Widget

Showing solved puzzle + animating it

Since displaying the solved version of the puzzle for validation is just one step before implementing the animation of blocks, I will mention both topics in this section. I spent considerable time researching the most intuitive approach to animating such simple rigid objects as these small toy cubes, and I wrote a separate post about this topic. That's why I will describe the animation part only briefly. The approach I took here is to use a timeline component in combination with separate curves for the progress of rotation, the opacity of the letter, and the glowing block effect. Below you can see the product I was after:

block_animation.webp

The animation of blocks

I created a linear timeline that interpolated over three curves and modified the block's properties. This timeline was then triggered whenever the ShowLetter method was called. Notice that the method has the bUseTransition attribute, which controls whether the animation should be visible or skip to the last frame demonstrating the letter to the player during the validation step.

221212111125-5TxmRN1Vd3.webp

The logic behind animating puzzle blocks

221212111738-lkwd5O6X3i.webp

Curves I used for animation of blocks

Prepare for the game

Now that we got the board constructed let's prepare it for the first contestant. Before first letter guess, this is the list of actions we need to execute:

puzzle-4_prepare_for_the_game.webp

Logic behind preparing the board for the actual game

I created a method for this task that shows and hides all blocks using scale-in-out animation utilizing a timer. Every 1/50 of a second, it executes a function that interpolates the scale of each block. When pieces become either fully displayed or hidden, it invalidates the handle, thereby stopping scaling interpolation.

221213090917-pveDktWQTI.webp

Preparing blocks for the game

Guess a letter

The board is standing in its glory. Contestants are on the stage. It's time to start the game! The first player is spinning the wheel and is about to guess a letter. But wait! We still need to implement the logic behind the AWordPuzzle::GuessConsonant method that the PlayerController will soon call! Without further ado, let's dive in!

puzzle-5_guess_a_consonant.webp

A flow if guessing a letter

To guess a consonant, first, I need to make sure that the requested letter is, in fact, a consonant. Only then do I iterate over all blocks to count the number of hits. If the contestant is lucky, blocks are rotated toward the stage, and the guessed letters counter is increased. The result is broadcasted to interested listeners. If all letters are uncovered on the board, the contestant is allowed to put his/her hands in the air and scream "" because the round is over.

WordPuzzle.cpp - the method for handling the consonant letter guess
int AWordPuzzle::GuessConsonant(const FString& Consonant)
{
    if (Consonant.Equals("A") || Consonant.Equals("E") || Consonant.Equals("I") || Consonant.Equals("O") || Consonant.Equals("U") || Consonant.Equals("Y"))
    {
        UE_LOG(LogTemp, Warning, TEXT("%s >> Letter is not consonant!"), ANSI_TO_TCHAR(__FUNCTION__));
        return 0;
    }

    const int ConsonantsGuessed = GuessLetter(Consonant);

    ConsonantGuessed.Broadcast(ConsonantsGuessed);

    CharactersLeftCount -= ConsonantsGuessed;
    if (CharactersLeftCount <= 0)
    {
        PuzzleSolved.Broadcast();
    }

    return ConsonantsGuessed;
}

The GuessConsonant and the GuessVowel methods are very similar as they use a more general GuessLetter function. The difference is only in the check of whether the requested letter is a consonant or not.

WordPuzzle.cpp - the method handling the letter guess request
int AWordPuzzle::GuessLetter(const FString& Letter)
{
    int LettersGuessed = 0;

    for (const auto Block : BlockEntries)
    {
        if (IsValid(Block))
        {
            if (!Block->IsBlockEmpty() && !Block->IsBlockShown())
            {
                if (Block->GetLetter().Equals(Letter))
                {
                    Block->ShowLetter(true);
                    LettersGuessed++;
                }
            }
        }
    }

    return LettersGuessed;
}

Solve the puzzle

And there is the last part - my favorite because it contains EXPLOSION! A little one. Ok, there is no fire or flames, but it's still cool. So let's see what is left on the TODO list:

puzzle-6_solve_the_puzzle.webp

Logic behind solving the puzzle

The first step is easy - animate all the remaining letters in case the contestant shouts the answer during the show without guessing it letter by letter.

WordPuzzle.cpp - uncovering all remaining puzzles
void AWordPuzzle::SolvePuzzle(bool bUseTransition)
{
    for (const auto Block : BlockEntries)
    {
        if (IsValid(Block))
        {
            if (!Block->IsBlockEmpty() && !Block->IsBlockShown())
            {
                Block->ShowLetter(bUseTransition);
            }
        }
    }

    CharactersLeftCount = 0;

    PuzzleSolved.Broadcast();
}

And finally, let's explode it! I used AddImpulseAtLocation method to achieve this amazing result:

221213095552-j5eYUvTpzb.webp

My highly advanced pyrotechnical secret

And... wait for it...

explosion.webp

Explosion time!

Conclusion

Gameplay programming is fun! Especially if the mechanics are well-planned. Even though I could somehow construct this whole board on GPU instances, I was happy to use the pooling pattern here for practice.

If you enjoyed this article, check out the remaining parts of the entire project entry. Click >> here<< to go to the Table of Content of the project. Thanks!