Up until this point, we have programmed an FSM and it is functional, but not really useful. What gives an FSM it's usefulness is how integrated, and yet isolated it is. We want to interact with the state machine in a way that doesn't couple it to what is calling it or responding to it. No, not everything can be abstract. It has be linked at some point, but lets keep it as minimal and consistent aspossible.
Responding To Changes
We already know from some debug messages that we are changing the state appropriately. Now lets respond to the changes in a meaningful way. To accomplish this, I'm going to use a delegate (event).
Raising the Event
In the FarmplotFSM header file, add the delegate.
#pragma once
...
struct Plowed : public FBasicFarmplotState { Plowed() : FBasicFarmplotState() {}; };
struct Planted : public FBasicFarmplotState { Planted() : FBasicFarmplotState() {}; };
DECLARE_DYNAMIC_DELEGATE_OneParam(FFarmPlotStateChangedDelegate, FBasicFarmplotState, newState);
...
private:
struct Work
{
...
public:
Work() { m_state.<Placed>(Placed()); };
void RaiseOnPlotStateChanged(FBasicFarmplotState newState)
{
_onFarmplotStateChanged->Execute(newState);
}
Now in each operator we need to call this new function. So to accomplish this, add this line, be sure to change the get to match the state that is being changed.
RaiseOnPlotStateChanged(m_state.Get<[StateName]>());1
So, in the operator that takes Placed as the param would look like this:
State operator()(const Placed&)
{
m_state.Set<Tilled>(Tilled());
RaiseOnPlotStateChanged(m_state.Get<Tilled>());
return m_state;
}
and the operator that takes Tilled as the param would look like this:
State operator()(const Tilled&)
{
m_state.Set<Fertilized>(Fertilized());
RaiseOnPlotStateChanged(m_state.Get<Fertilized>());
return m_state;
}
Follow this pattern for the remainder of the operators. Now we need to have some data in our state structs that can be used to easily determine the state we are in.
In our base struct FBasicFarmplotState add and FString property named PlotStateName along with a new constructor, in addition to the existing empty default constructor. This new constructor will take the name of the state. We also need to update the derived states to pass in the state name. The new sections should looks something like this:
//FarmplotFSM.h
#pragma once
#include "CoreMinimal.h"
#include "FarmplotFSM.generated.h"
USTRUCT(BlueprintType)
struct FBasicFarmplotState
{
GENERATED_BODY()
FBasicFarmplotState() {}
FBasicFarmplotState(FString stateName) {PlotStateName = stateName;}
UPROPERTY(BlueprintReadOnly)
FString PlotStateName;
};
struct Placed : public FBasicFarmplotState { Placed() : FBasicFarmplotState("Placed") {}; };
struct Tilled : public FBasicFarmplotState { Tilled() : FBasicFarmplotState("Tilled") {}; };
struct Fertilized : public FBasicFarmplotState { Fertilized() : FBasicFarmplotState("Fertilized") {}; };
struct Plowed : public FBasicFarmplotState { Plowed() : FBasicFarmplotState("Plowed") {}; };
struct Planted : public FBasicFarmplotState { Planted() : FBasicFarmplotState("Planted") {}; };
Responding to the Event
In our farmplot class, create the method that'll be invoked.
//farmplot.h
#pragma once
...
class FSM_BLOG_API AFarmplot
{
...
public:
...
void LookAway_Implementation() override;
void Interact_Implementation() override;
UFUNCTION(BlueprintCallable)
void FarmplotWorked(FBasicFarmplotState newState);
};
//farmplot.cpp
...
void AFarmplot::FarmplotWorked(FBasicFarmplotState newState)
{
groundMesh->SetMaterial(0, plotMats->FindRow<FFarmplotMeshMaterialTableRow>((FName)newState.PlotStateName, "")->Material);
}
All that's left now is to listen for the event. in the constructor of the farm plot object, we bind to the event.
//farmplot.cpp
#include "Farmplot.h"
...
AFarmplot::AFarmplot()
{
...
fsm= new FarmplotFSM();
auto farmplotWorkedFunctionName = GET_FUNCTION_NAME_CHECKED(AFarmplot, FarmplotWorked);
fsm->onFarmplotStateChanged.BindUFunction(this, farmplotWorkedFunctionName);
Running the game and interacting with the farmplot will now change the material.
Gating the FSM Transitions
Now lets suppose that we only want the state to change if we have the correct tool selected. Well that's simple enough to implement. Start by making an enum that'll define all the tools that apply. I'll create a new header file for this to host all the enums in the game in the public folder.
//GameEnums.h
#pragma once
#include "CoreMinimal.h"
UENUM(BlueprintType)
enum class EToolTypes : uint8
{
VE_None UMETA(DisplayName="None"),
VE_Hoe UMETA(DisplayName="Hoe"),
VE_Bag UMETA(DisplayName="Bag")
};
Next, we update our states to set what tool is needed to get pass the gate.
//farmplotFSM.h
#pragma once
#include "CoreMinimal.h"
#include "GameEnums.h"
#include "FarmplotFSM.generated.h"
USTRUCT(BlueprintType)
struct FBasicFarmplotState
{
GENERATED_BODY()
FBasicFarmplotState() {}
FBasicFarmplotState(FString stateName, EToolTypes gatedTool) {PlotStateName = stateName; _gatedTool = gatedTool;}
UPROPERTY(BlueprintReadOnly)
FString PlotStateName;
EToolTypes _gatedTool;
};
struct Placed : public FBasicFarmplotState { Placed() : FBasicFarmplotState("Placed", EToolTypes::VE_Hoe) {}; };
struct Tilled : public FBasicFarmplotState { Tilled() : FBasicFarmplotState("Tilled", EToolTypes::VE_Bag) {}; };
struct Fertilized : public FBasicFarmplotState { Fertilized() : FBasicFarmplotState("Fertilized", EToolTypes::VE_Hoe) {}; };
struct Plowed : public FBasicFarmplotState { Plowed() : FBasicFarmplotState("Plowed", EToolTypes::VE_Bag) {}; };
struct Planted : public FBasicFarmplotState { Planted() : FBasicFarmplotState("Planted", EToolTypes::VE_Hoe) {}; };
While we're in this header file for the FSM, lets add one more inline function to help facilitate retrieving what the current tool required is to get pass the gate.
//FarmplotFSM.h
#include "FarmplotFSM.generated.h"
...
class FarmplotFSM
{
public:
...
EToolTypes GetGatedTool(){return Visit([](auto arg)->EToolTypes{return arg._gatedTool;}, _currentState);}
...
};
What this inline function does is it passes our class variable _currentState (naming it arg) to the Visit method and returns the underlying structs' _gatedTool value. This allows us to not need to track the gate and what the next gate needs to be. Just let the FSM (and the underlying structs) tell us what it is.
Now, we need to update our interface to pass in the equiped tool that was used to interact with the farm plot. If we were adding this to an already existing code base, it may be wise to just overload the function.
//Interactable.h
#pragma once
...
#include "GameEnums.h"
#include "Interactable.generated.h"
class IInteractable
{
GENERATED_BODY()
....
void Interact(EToolTypes equipedTool);
};
...
We need to now also update our farmplot files to consume this new change
//farmplot.h
#pragma once
...
#include Farmplot.generated.h"
UCLASS(BlueprintType)
class FSM_BLOG_API AFarmplot : public AActor, public IInteractable
{
GENERATED_BODY()
...
public:
...
void Interact_Implementation(EToolTypes equipedTool) override;
};
//farmplot.cpp
...
void AFarmplot::Interact_Implementation(EToolTypes equipedTool)
{
if(equipedTool == fsm->GetGatedTool())
fsm->BeginWork();
}
...
With our state machine, and our farm plot updated to use gates, we now need to update our player character blueprint to be able to switch between equipped tools and pass the currently equipped tool to the interact function when called. This is all updated in the blueprint class for our character.
Set up the input (I'm using enhanced input and using the scroll wheel). In the player blueprint create a variable for storing the equipped tool. Have your input change the equipped tool.
Don't forget to have your interact pass the equipped tool as well.
And now we have gated transitions.
Conclusion
There it is. A finite state machine where each instance tracks its state and gates its own transition. This is all written in C++ with the blueprint items only needed to integrate the player object's blueprint with interacting with the farm plot. We successfully abstracted the interaction of player to the object, and the object from the underlying state machine. This concept can be extrapolated easily into other game objects. See if you can add an FSM to trees or add a UI on top of the selected tool Maybe you can add a few animations and delay the call to work the plot until mid-animation.
I hope you have enjoyed this adventure. I spent months focusing on how to do an FSM in Unreal Engine leveraging C++ 11. I used a lot of guides for how to build an FSM in native C++ 11. Then Days sifting through Unreal docs to find the data types. Weeks scavenging the forums to untangle build errors. I then recreated all this in a fresh project distill the knowledge into something presentable. I hope someone finds this guide helpful in their adventures.
Komentar