Now that the holidays are over, and 2025 is already off to the races, I wanted to take some time to dig into Artificial Intelligence. Because of the A.I. landscape today, I have to preface this by saying I am referring to game A.I., not machine learning topics such as LLMs. This will be a series of posts due to the breadth of the subject, but to start I will talk about state machines. I will be using Unreal Engine for my examples but the the A.I. portions of the code will be mostly engine agnostic. (I say mostly because I will also have asides about tools Unreal provides)
If you'd like to check out the code, it can be found here.
Let's see how wikipedia defines a state machine.
A finite state machine is an abstract machine that can be in exactly one of a finite number of states at any given time. A state is a description of the status of a system that is waiting to execute a transition. A transition is a set of actions to be executed when a condition is fulfilled or when an event is received.

While not a bad definition, it's easier to grasp with an example. You have an NPC that you want to antagonize the player. The NPC has three states: chasing player, finding cover, and shooting at the player. The NPC moves from one state to the next when some criteria is met (is the player within range, did the npc find cover, is the player out of range).

As you can see, a state machine can be simple and are easy for non programmers to grasp. This is fantastic for collaboration with your team! State machine usage isn't limited to A.I. either, one really popular usage is for animation states. And they can grow to be as complicated as you want:

Side note: If you find your state machine starting to look like this, you can try to simplify things by using state machines within state machines. Using the above example, the crouching states get moved into a crouched state machine and the standing states get moved into a standing state machine. Then you set up transition rules between standing and crouched. I'll go into more detail below about this towards the end of this post.


Now that we know what a state machine is, let's go over a simple example: Lumberjack Simulator. There is a Lumberjack who lives in a village. He chops wood when he can, buys and eats food when he is hungry, sells wood when his inventory is full, and sleeps when he is tired. So to graph it out:

Now let's write some code! We'll start by creating a simple State Machine class. Since we are keeping this simple, the State Machine only has two attributes, an Owner and the CurrentState. In this case the Owner will be the Lumberjack and the CurrentState will be whatever state the Lumberjack is in. The State Machine also has three member functions:
- Update which is triggered periodically (when the A.I. ticks)
- ChangeState, which transitions the Lumberjack from one state to the next
- SetCurrentState, a setter we use to set the initial state of the lumberjack
class StateMachine { public: StateMachine(ALumberjackController* MachineOwner) : Owner(MachineOwner) , CurrentState(nullptr) {} void SetCurrentState(State* S) { CurrentState = S; } void Update() const; // Change to a new state, calling exit and enter functionality along the way void ChangeState(State* NewState); private: ALumberjackController* Owner; State* CurrentState; }
And the implementation of Update and ChangeState:
void StateMachine::Update() { if (CurrentState) { CurrentState->Execute(Owner); } } void StateMachine::ChangeState(State* NewState) { if (!NewState) { return; } CurrentState->Exit(Owner); CurrentState = NewState; CurrentState->Enter(Owner); }
With the State Machine out of the way, let's create a base State class that other states inherit from. Along side triggering the state itself, it's popular to define functions for handling entering the state and exiting the state. This allows you to also have logic related to transitioning into the state and transitioning out of the state. We'll see a benefit of this with our very first state.
class State { public: virtual void Enter(ALumberjackController* Lumberjack) = 0; virtual void Execute(ALumberjackController* Lumberjack) = 0; virtual void Exit(ALumberjackController* Lumberjack) = 0; };
As you can see, we pass in the entity into each state, this allows the state to perform actions such as adding wood to the Lumberjack's inventory. Now let's implement the ChopWood state. First, the ChopWood declaration:
class ChopWood : public State { public: void Enter(ALumberjackController* Lumberjack) override; void Execute(ALumberjackController* Lumberjack) override; void Exit(ALumberjackController* Lumberjack) override; };
When the Lumberjack enters the ChopWood state, he sets off to the woods:
void ChopWood::Enter(ALumberjackController* Lumberjack) { if (Lumberjack) { Lumberjack->MoveToWoods(); } }
Now that the Lumberjack is in the ChopWood state, when the A.I. ticks, Execute is triggered:
void ChopWood::Execute(ALumberjackController* Lumberjack) { if (!Lumberjack) { return; } if (!Lumberjack->IsNearTree()) { // Not at the woods, too far away return; } Lumberjack->AddWood(1); Lumberjack->AddHunger(1); Lumberjack->AddFatigue(1); if (Lumberjack->IsInventoryFull()) { StateMachine* SM = Lumberjack->GetStateMachine(); if (SM) { SM->ChangeState(Lumberjack->GetSellWoodState()); } } if (Lumberjack->IsHungry()) { StateMachine* SM = Lumberjack->GetStateMachine(); if (SM) { SM->ChangeState(Lumberjack->GetEatFoodState()); } } }
And when the lumberjack's inventory is full or he is hungry, ChangeState will call the Exit function. Now that he's finished chopping wood, the lumberjack can put away his axe:
void ChopWood::Exit(ALumberjackController* Lumberjack) { if (Lumberjack) { Lumberjack->Emote(ELumberjackEmotes::ELE_SheatheAxe); } }
The other states (SellWood, EatFood, Sleep) are straightforward following the same structure as Chopwood:
- Enter sends the Lumberjack to the required location
- Execute performs the action (increasing the Lumberjack's money, decreasing hunger, decreasing fatigue) and then checks if the Lumberjack is ready to transition to another state.
- Exit performs some emote to signify the Lumberjack is finished with the action.
Here's a small demo showing it in action:
This section is Unreal Engine specific. Unreal provides a State Machine implementation via plugins called StateTree. While the above Finite State Machine code is easy to implement and grasp, one downside that becomes apparent during implementation is as you add more states, the State Machine can become more difficult to manage and modify.
Using the above Lumberjack example, if I add three new states "VisitFriend", "GoToBar", and "SharpenAxe", we go from 4 states to 7. Now we have
to verify that each state transitions correctly into other states and that it's impossible to get stuck in a particular state. (For example, if I messed up the IsInventoryFull()
logic, the Lumberjack will never go and sell the wood). You also end up with duplicated code (Writing Lumberjack->IsTired; ChangeState(Sleep);
in every state).

I alluded to this problem and a potential solution earlier with the example of crouch and standing animation states. Instead of only supporting two levels (State Machine and State), States themselves can be their own state machines. That's the hierarchical part, you define a hierarchy of state machines by grouping similar states. To continue the above example, we could group SharpenAxe, ChoppingWood, and SellWood into a Work state machine. Then we could group Sleep, VisitFriend, GoToBar into a Relax state machine. The top level state machine becomes:

Or using Unreal's StateTree:

A fair bit simpler to reason about! That's not the only way state machines can be expanded. Another improvement is you track the PreviousState along with the CurrentState, now you can have the concept of interrupting and resuming states. For example: when the Lumberjack is attacked by a bandit, he stops whatever he is doing and defends himself. Then after fending off the attack, he resumes his previous task (could be chopping wood or eating or sleeping). I hope this shows the power (and simplicity) state machines can provide! For the next part in this series, an A.I. topic I want to dig into is boids.