
Wheel of Fortune - Wheel
This post is a part of a more extensive project entry. If you are interested in it, make sure to check it out and click >> here<< !
What would be the game Wheel of Fortune without the actual wheel? For sure, it would be, (un)fortunately, awkward to take part in such a show. In this entry, I will describe my approach to creating this pawn, representing most of the steps I took and the problems I encountered.
Base construction
The most important parts of the wheel pawn are wedges. Without them, there would be no fun. They differ in size, color, and reward/punishment. Some wedges are more expansive, while others are thinner. Usually, they come in the size of 1 to 3 smaller zones. It's best visible in the photo below.
To let the game designer construct the wheel using appropriate wedges, I created a structure describing each section. Primary properties are count of zones that this wedge occupies and action that needs to be resolved when the wheel lands on that wedge. I also included radial span inside the structure below, expressed in the degree unit. I bundled it within this structure despite making it editable only in C++ because I thought it would be nice to debug radial values directly in the editor. Besides, this span will be essential for us to determine the result of wheel rotation, and I will calculate inside the AWheel::OnConstruction
method taking into account all other specified sections.
USTRUCT(BlueprintType)
struct FWheelSection
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Wheel")
int ZonesCount = 3;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Wheel")
FVector2D RadialSpan;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Wheel")
TSubclassOf<UWedge> WedgeType;
};
Adding an editable array property to the AWheel
actor allows me to specify wheel elements inside the editor and read desired content of the wheel. So let's create a child Blueprint class of the wheel and fill in the data.
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Wheel")
TArray<FWheelSection> Wedges;
So now, after picking some wedges, I can calculate the radial span of each of them. Using this information, I will construct the actual wheel visualization. First, let's count the number of zones by iterating over each FWheelSection
element of the array, getting TotalZonesCount
, and then using this value to calculate in/out radial span.
void AWheel::UpdateWedgesRadialSpan(int TotalZonesCount)
{
int WedgeStartZoneIndex = 0;
int WedgeEndZoneIndex = 0;
for (int i = 0; i < Wedges.Num(); i++)
{
WedgeEndZoneIndex += Wedges[i].ZonesCount;
Wedges[i].RadialSpan = FVector2D(
WedgeStartZoneIndex * 360.f / TotalZonesCount,
WedgeEndZoneIndex * 360.f / TotalZonesCount
);
WedgeStartZoneIndex = WedgeEndZoneIndex;
}
}
Now we got all information needed to construct a prototype wheel. Let's dive into the editor for now.
Visual prototype
My idea of debug representation of the wheel is to construct it out of instanced mesh wedges. Each wedge can be a simple plane with applied material that masks it into a wedge-like shape.
First, we need a procedurally generated radial slice using shaders. For this, I created a material function called MF_RadialSlice
. In its heart lies a native VectorToRadialValue
node which creates a circular gradient. You can read more about it here.
The next step is to take this gradient, add a desired radial out value, and cap it using Floor
to get a nice solid black and white mask. We receive a nice radial slice by doing the same with radial in value and then subtracting results from each other.
Let's use it as a mask in our prototype wedge material. Because I want this material to be controllable per mesh instance, I use PerInstanceCustomData
in combination with VertexInterpolator
to pass values. I also added control for wedge emissiveness, as I would like the wedge to glow when the wheel stops on it. And, of course, each wedge needs to have a unique color. All parameters can be controlled per material or mesh instance basis.
Now we can move on to creating actual instances of wedges. I will use InstancedStaticMeshComponent
for this task. So add it to the actor and assign a plane mesh and our prototype wedge material.
Important part: make sure to set count of custom data floats that we will pass to the material's PerInstanceCustomData
via the SetCustomDataValue
node. I often forget about this step, and later I spend a long time figuring out what has gone wrong.
In the construction script, let's iterate over the Wedges
array and construct instances. On the top, I saved some local variables for later use, as I prefer not to drag long cables across the node graph.
For each instance pass the radial span and give it a touch of gorgeous random color. As you remember, this data will be read by PerInstanceCustomData
nodes inside our debug wedge material.
Any color by itself will not let me differentiate wedges, so I will also add the names of their actions using TextRenderComponent
s. I did some simple math translating the text in 3D relatively to the wedge radial span and centered it to the wedge surface.
As a cherry on top of a pie, I placed an arrow to visualize the current rotation of the wheel. And now, wait for it...
API
The wheel object can be perceived purely as a pawn. Disregarding all its visual parts, as this game could be a text game too, there are two things we are interested in. The first one is controlling its action - making it a spin. The second one is reading its output - on which wedge it stopped. These will be our sockets for connecting its mechanism to a larger ecosystem. As usual, in the case of pawns, the player controller will execute its action and read its output. And so, I implemented spinning the wheel as a public AWheel::Spin
method, while sending the output through dynamic multicast delegate AWheel::WheelLanded
as I don't want to couple this object with any concrete implementation of player controller, which would result in cyclic dependency error.
public:
AWheel();
...
UFUNCTION(BlueprintCallable, Category="Wheel")
void Spin();
UPROPERTY(BlueprintAssignable)
FWheelLanded WheelLanded;
// Informs about at which section the wheel stopped and about radial coordinates of section start and end for visual purposes
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FWheelLanded, TSubclassOf<UWedge>, WheelLandedOn, FVector2D, SectionRadialSpan);
Let's give it a spin!
To visually define the style of wheel rotation, I decided to use timeline (UTimelineComponent
) in combination with interpolation along a curve (UCurveFloat
). In the editor, I created a couple of movement variations. Then I chose the most dynamic and pleasing to the eye, assigning it to the editable protected property SpinSpeedCurve
. For the record, I could use all of them simultaneously, either randomizing the curve from the list or substituting it after landing some wedge, just for fun. Additional parameters to control the animation of the wheel are animation duration, randomization, and maximum angle.
public:
UPROPERTY(EditAnywhere, Category="Wheel")
float SpinStrength = 720.0;
UPROPERTY(EditAnywhere, Category="Wheel")
float SpinStrengthVariance = 180;
UPROPERTY(EditAnywhere, Category="Wheel")
float SpinDuration = 12;
UPROPERTY(EditAnywhere, Category="Wheel")
UCurveFloat* SpinSpeedCurve;
SpinTimeline = CreateDefaultSubobject<UTimelineComponent>(TEXT("SpinTimeline"));
Since the timeline component reports the progress of the animation, I also need three global variables to store and control the current state of the rotation and use them later during rotation interpolation.
private:
float InitialSpinRotation = 0;
float CurrentSpinRotation = 0;
float TargetSpinRotation = 0;
Then in AWheel::BeginPlay
, I bind timeline delegates, attaching the curve to the interpolation callback and linking the timeline termination signal.
if (IsValid(SpinSpeedCurve))
{
FOnTimelineFloat SpinTimelineCallback;
SpinTimelineCallback.BindUFunction(this, FName("UpdateWheelRotation"));
SpinTimeline->AddInterpFloat(SpinSpeedCurve, SpinTimelineCallback);
FOnTimelineEvent SpinTimelineOnFinish;
SpinTimelineOnFinish.BindUFunction(this, FName("FinishWheelRotation"));
SpinTimeline->SetTimelineFinishedFunc(SpinTimelineOnFinish);
}
The first one - delegate SpinTimelineCallback
calls theUpdateWheelRotation
method in which I modify the rotation of the wheel. I also broadcast the result of this operation for further use - it will let me make the wheel's arrow tick simulating a collision with protruding tubes defining the boundaries of each zone of the wheel.
void AWheel::UpdateWheelRotation(float Progress)
{
const float PreviousSpinRotation = CurrentSpinRotation;
CurrentSpinRotation = InitialSpinRotation + (TargetSpinRotation - InitialSpinRotation) * Progress;
const FRotator TickRotation = FRotator(0, -(CurrentSpinRotation - PreviousSpinRotation), 0);
Wheel->AddRelativeRotation(TickRotation);
WheelRotated.Broadcast(CurrentSpinRotation);
}
The second delegate, which notifies the end of the timeline, calls the FinishWheelRotation
method. Without going into details, which you can preview in the (not yet) provided source code, I determine the wedge on which the wheel stopped and broadcast this final information out into the world using the delegate created at the beginning.
void AWheel::FinishWheelRotation()
{
const FWheelSection Wedge = GetWedgeFromRotation(CurrentSpinRotation);
UE_LOG(LogTemp, Warning, TEXT("%s >> Wheel landed on [%s]"), ANSI_TO_TCHAR(__FUNCTION__), *Wedge.WedgeType->GetName());
WheelLanded.Broadcast(Wedge.WedgeType, Wedge.RadialSpan);
}
Having the timeline part covered, let's finally give it a spin! In the most essential AWheel::Spin
method that the player controller will use, I randomize the rotation on which the wheel will stop, set the rotation speed and play the timeline.
Note: If you are either a bad guy or a businessman, this is a place to implement some logic of faking the result ^ ^
void AWheel::Spin()
{
if (!SpinTimeline->IsPlaying())
{
InitialSpinRotation = TargetSpinRotation;
const float SpinStrengthRandomization = FMath::RandRange(-SpinStrengthVariance, +SpinStrengthVariance);
TargetSpinRotation = InitialSpinRotation + SpinStrength + SpinStrengthRandomization;
SpinTimeline->SetPlayRate((SpinStrength - SpinStrengthRandomization) / SpinStrength / SpinDuration);
SpinTimeline->PlayFromStart();
const FString PredictedWedge = GetWedgeFromRotation(TargetSpinRotation).WedgeType->GetName();
UE_LOG(LogTemp, Warning, TEXT("%s >> Succesfully spinned the wheel, it will land on [%s] wedge"), ANSI_TO_TCHAR(__FUNCTION__), *PredictedWedge);
}
}
One last thing - as you can see in the video above, when the wheel stops, the wedge below the arrow glows for a moment. To achieve this effect, I used another timeline component. Again, without going into details, when the wheel stops, I determine the wedge index that the arrow points to. This index is also the same index of a mesh instance of this wedge. With this setup, I can pass the strength value of the glow effect via PerInstanceCustomData
.
Wedges
I already mentioned a couple of times how vital wedges are in the overall mechanism of the wheel pawn, but I still need to explain how they work under the hood.
Starting with some examples, each wedge has a more or less unique action that needs to be resolved. Some of them require to be resolved instantly, e.g., Bankrupt wedge, which interrupts the contestant's turn and clears out his pending reward. On the other hand, wedges like $1000 cash prize require guessing a consonant before gratifying the player with the juicy tip.
In the beginning, I decided to differentiate the action of each kind of wedge using Enum
. I chose three distinct wedges and implemented their resolve mechanism inside Game Mode. So there was one significant function with a switch
statement, executing various actions and checking some stuff. Then I expanded it many times until my class was filled with enormous switch statements, and I lost track of what I was doing. It took me a lot of time to debug after making small changes. It had to stop.
For the rescue came Command Pattern. To explain it briefly for those who are not familiar what it, I will quote a definition from Robert Nystrom's book:
"Encapsulate a request as an object, thereby letting users parameterize clients with different requests, queue or log requests, and support undoable operation."
In this case, it means encapsulating mechanism of processing each type of wedge action in its own smaller object. This way, handling the mechanism is easier, as I can pass it around in my code. And also, it's easier for our brain to assimilate and understand the outcome of that action. No more switch
statements as each case
is encapsulated in its own object. Let's take a look at the base implementation of the UWedge
object:
...
#include "UObject/Object.h"
#include "WheelOfFortune/Core/Mode/StandardMode.h"
UCLASS(Abstract, Blueprintable, BlueprintType)
class WHEELOFFORTUNE_API UWedge : public UObject
{
GENERATED_BODY()
public:
virtual void Resolve(AStandardMode* Mode, int GuessedLetters);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Wheel Of Fortune|Wedge")
bool bInstantResolve = false;
};
And so the abstract UWedge
class has only two members that are common across all wedges - method for executing action of the effect, and boolean informing about whether the action should happen instantaneously, or should it be postponed until contestant guessed the letter.
Now let's look at the concrete implementation of Bankrupt action. Remember when I wrote about piles of cases inside the switch
statement? Now we got this:
...
#include "Wedge.h"
UCLASS()
class WHEELOFFORTUNE_API UWedge_Bankrupt : public UWedge
{
GENERATED_BODY()
public:
UWedge_Bankrupt();
virtual void Resolve(AStandardMode* Mode, int GuessedLetters) override;
};
#include "Wedge_Bankrupt.h"
UWedge_Bankrupt::UWedge_Bankrupt()
{
bInstantResolve = true;
}
void UWedge_Bankrupt::Resolve(AStandardMode* Mode, int GuessedLetters)
{
Mode->HandleBankrupt();
}
As simple as that - when the action is resolved, all it does is call the AStandardMode::HandleBankrupt
method. And who resolves these actions? PlayerController
receives information about the wedge when the wheel lands on it. Notice that some wedges require delayed resolving of action (waiting for an answer) while others should be resolved instantaneously (bankrupt), hence the bInstanceResolve
bool.
void AMainPlayerController::OnWheelLanded(TSubclassOf<UWedge> WheelLandedOn, FVector2D SectionRadialSpan)
{
CurrentWedge = NewObject<UWedge>(GetTransientPackage(), WheelLandedOn);
if (CurrentWedge->bInstantResolve)
{
ResolveCurrentWedge(0);
}
else
{
State = NewObject<UControllerState_GuessConsonant>(this);
State->Enter();
}
}
void AMainPlayerController::ResolveCurrentWedge(int LettersGuessed)
{
if (IsValid(CurrentWedge))
{
CurrentWedge->Resolve(GameMode, LettersGuessed);
CurrentWedge = nullptr;
}
}
Let's take a look at another example of implementation of UWedge
- this time cash prize wedge:
...
#include "Wedge.h"
UCLASS(Abstract)
class WHEELOFFORTUNE_API UWedge_Cash : public UWedge
{
GENERATED_BODY()
public:
virtual void Resolve(AStandardMode* Mode, int GuessedLetters) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Wheel Of Fortune|Wedge")
int Money = 0;
};
#include "Wedge_Cash.h"
void UWedge_Cash::Resolve(AStandardMode* Mode, int GuessedLetters)
{
Mode->HandleCashWin(Money * GuessedLetters);
}
This way, I created a library that's easy to use, expand, or modify without the risk of breaking other wedge mechanics. New actions can be easily added when fresh wedges are introduced. By making these objects inherit from UObject
, and because UWedge
is BlueprintType
, they can be easily assigned to an array that describes each section of the wheel, as I showed you earlier. What's even more remarkable is that a non-programming game designer can create new derivative actions inside the editor, creating new blueprints based on the UWedge
parent class because it's Blueprintable
.
There is just one thing that unsettles my nerves. It's the fact that the UWedge
is coupled with AStandardMode
. I could create an interface for each wedge action type for complete decoupling and communicate with GameMode through them. But for now, I will leave it like this.
Improving visual look
The wheel is constructed, spinning, and giving correct results. In brief - it's working just fine. It's time to give it a new, better face. I am not a shader expert, so I followed the wheel layout currently visible on the wheel prototype and recreated it in Photoshop. I used Materialize software to generate PBR maps like normal, metalness, height.
With this set of textures, the next step was to create a PBR material, which will be applied to a flat plane and completely replace the prototype's instanced wedge meshes.
You may notice I used decreased MipBias for almost all textures. This is my simple trick for making textures of important objects placed far away sharper. I could not use it with the height map because it messes up with tesselation. Let me know if you know a better way to ensure the correct sharpness of textures.
Next step - adding protruding tubes, defining each zone of the wheel, and animating the arrow, so it ticks like it's colliding with the pipes. I could use some collision for this task, but meh, I'm not sure if it would work with a fast-spinning wheel, and it would cost a lot of calculations. So without further ado, let's create the tubes! I will again use InstancedStaticMeshComponent
with a simple cylinder as a mesh and some lovely metal material.
Using information about the count of wheel zones (from the Wedges
array of the wheel, which the designer populated in the first place), I calculate the angle between them and interpolate arrow rotation based on modulo between wheel's current rotation and unit zone angle. I will provide a screenshot of my node graph without a mouthful explanation of the procedure. In case you wonder where input WheelRotation
comes from - the execution of the TickArrow
method is bound to the AWheel::WheelRotated
delegate in the Event Graph.
So now, when we spin with the wheel, this is what we get with arrow animation. It's not perfect, but it does not need to be. It's just a game, after all.
After tuning the look of the wheel by adding a fancy cube in the middle, this is the final comparison between the prototype and refreshed version of the wheel. Is it not beautiful?


Conclusion
Creating this pawn was fun. Especially the backend part was a messy pile of enums at the beginning, and later, after implementing Command Pattern became an elegant solution. Anyway, this wheel is just one element of the whole game show. 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
Raw .psd
project file of the wheel board: >>> LINK <<<