Finite State Machines are a key part of software development. State machines are used to compartmentalize the logic of an objects state, along with when and how these objects change states. Unreal has support for state machines, but only where it applies animations for animation states. State machines usefulness extends far beyond animations, and we'll show you how to build your own state machine.
This is a multi-part blog that'll go through not only showing how to setup a FSM in C++, but also how to interact with it and connect it to blueprints so that game designers can interact with it in UE5's blueprint system.
The Game
The game we'll be using is a simple farming game. We first need to collect the assets to use. Roughly, this'll include some textures to use. We will have some planes that are already placed on in the world in our UE5 editor and the player will look at the plot and interact with them. The finite machine will focus on interacting with the farm plots. Any building or other mechanics will not be covered in this blog. First, a peek at the end result.
Setting the Stage
Before we begin, we need to create our project and import some assets. We'll start by creating a C++ 3rd person game with starter content. This example will be creating farm plots for a colony sim.
For the state machine, we're going to employ concepts and techniques that became available in the C++11 standard. Though we can't use the STL directly due to UE essentially recreating the library, we can still use it as a base. The thing that makes this all work is the TVariant type. UE has their own implementation so standard docs and guides won't work on their own. However, I've put together this guide that translates the C++11 standard guides to work in UE5. Also note that UE5 uses empty constructors for a lot of it's behind the scenes work. Due to this, a lot of tasks that would typically be set in the constructor has been moved to an Init() method.
Lets start by defining a new C++ class and call it Farmplot. be sure to decorate the class with UCLASS(BlueprintType). This class derives from AActor. Give it a public UStaticMeshComponent member pointer and call it groundMesh. Decorate it with the following: UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category=Resources, meta=(AllowPrivateAccess="true")) This is going to be our visual representation of our state. When we interact with this farm plot, we will update the state and change the texture drawn. Again, we aren't going to tell the plot what texture to draw, just to transition to the next state. What that means is handled by the FSM and that'll take care of the rest. So far this is what your class should look like
//Farmplot.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Farmplot.generated.h"
UCLASS(BlueprintType)
class FSM_BLOG_API AFarmplot : public AActor
{
GENERATED_BODY()
private:
FarmplotFSM* fsm; //this'll error until we create the type below
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category=Resources, meta=(AllowPrivateAccess="true"))
UStaticMeshComponent* groundMesh;
// Called every frame
virtual void Tick(float DeltaTime) override;
// Sets default values for this actor's properties
AFarmplot();
};
Setting up the FSM
From here we'll start setting up our FSM. Part of this is to determine the names of the states we wish to use. For this example, I determined that I wanted 5 states, Placed, Tilled, Fertilized, Plowed, and Planted. To use these states in our FSM, we need to define them as structs. Before we do that, they must all derive from the same base type, or else UE5 type matching will error. To accomplish this, create inside a new file, a struct called FBasicFarmplotState and decorate it like so: USTRUCT(BlueprintType). Lastly give the structure a default constructor.
Following the basic struct, create a struct for each desired state. Make sure they all derive from the basic farmplot struct, and have an empty default constructor. When finished, you farmplot FSM file should look like this:
#pragma once
#include "CoreMinimal.h"
#include "FarmplotFSM.generated.h"
USTRUCT(BlueprintType)
struct FBasicFarmplotState
{
GENERATED_BODY()
FBasicFarmplotState() {}
};
struct Placed : public FBasicFarmplotState { Placed() : FBasicFarmplotState() {}; };
struct Tilled : public FBasicFarmplotState { Tilled() : FBasicFarmplotState() {}; };
struct Fertilized : public FBasicFarmplotState { Fertilized() : FBasicFarmplotState() {}; };
struct Plowed : public FBasicFarmplotState { Plowed() : FBasicFarmplotState() {}; };
struct Planted : public FBasicFarmplotState { Planted() : FBasicFarmplotState() {}; };
With the Structs in place, lets create the FSM to use these. Yes, this is more boilerplate code. This should be done in the same FarmplotFSM.h file as the structs created above. Pay attention, the FSM goes into a wrapper struct. This is necessary when it comes to the use of the FSM and will be explained later.
class FarmplotFSM
{
public:
using State = TVariant<Placed, Tilled, Fertilized, Plowed, Planted>;
void BeginWork() { _currentState = Visit(_work, _currentState); };
struct Work{
private:
State m_state;
public:
Work()
{
m_state.Set<Placed>(Placed());
};
State operator()(const Placed&)
{
m_state.Set<Tilled>(Tilled());
return m_state;
}
State operator()(const Tilled&)
{
m_state.Set<Fertilized>(Fertilized());
return m_state;
}
State operator()(const Fertilized&)
{
m_state.Set<Plowed>(Plowed());
return m_state;
}
State operator()(const Plowed&)
{
m_state.Set<Planted>(Planted());
return m_state;
}
State operator()(const Planted&)
{
m_state.Set<Placed>(Placed());
return m_state;
}
};
};
If you noticed, the operators take one state, set the current state to the next state, and return this new state. This is our transition from one state to the next.
Here is where we see that we need to wrap our state machine in a struct. Due to some nuance in the Variant type along with Visit Wrapping the operators, and ultimately the FSM, in this work struct has been the only way I've been able to successfully create an FSM utilizing the TVariant type.
This is a bit of boilerplate, and not a lot of functionality. Believe it or not, this is the bulk of the FSM. There is very little left to add functionality to the FSM. What's left is to interact with and react to the changes.
My next post we will setup the player controller to be able to interact with the farm plot (and consequently the FSM). We'll also setup the textures of the farm plot to show changes. The work accomplished today lays foundations for additional features. From here, our changes will be more scalable.
Homework
To prepare for the next post, go ahead and search quixel bridge (megascans) and find 5 surface textures you would like to use that correspond to our defined states.
Comments