
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.
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:
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);
...
}
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:
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.
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.


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.


Construct letter 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.
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
:
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"));
}
}
}
And since I want this initialization happens every time the value changes, I trigger it inside PostEditChangeProperty
:
#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.
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.
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.
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.
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.
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:
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.
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.
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:
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.
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.
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.
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.
Then I used this material function inside the toy's block primary material where I blended it with albedo texture:
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.
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.
All of this above gives us beautifully constructed pooled blocks whose appearance is controlled via material parameters. Isn't this neat?
Hint text
Let's take a look at the more general diagram again. We have constructed the puzzle blocks; let's move ahead.
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).
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.
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:
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.
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:
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.
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!
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 "
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.
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:
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.
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:
And... wait for it...
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!