Create a State Machine for Character Logic in Typescript

Simplify code for creating complex character logic & actions

by on 9 minute read


Complex character logic can get out of hand quickly without a system to manage it.

For example, a platforming hero needs to at least stand idle, move left and right, and jump.

But those actions alone don't often make a fun game. We'll want to add melee attacks, ranged attacks, double jump, ducking, crawling, and more.

One tried and true solution to handle this is to use a Finite State Machine or just State Machine. We'll show you how to create and use one in this article!

Check out this article for a review of the basic State Pattern.

Example Overview

The examples in this article assumes a project environment like the one in phaser3-typescript-parcel-template.

If you are not familiar with Phaser 3 and TypeScript then we have a free book to help you get started 👇

Learn to make an Infinite Runner in Phaser 3 with TypeScript!

Drop your email into the box below to get this free 90+ page book and join our newsletter.

Learn more about the book here.

You can find the source code for this state machine implementation in our Sidescrolling Platformer Starter Template.

There's also a video version going over the implementation of this state machine here. And then a video of it being used for character logic here.

How does a State Machine Work?

A State Machine is simply a collection of States with a defined system for switching between each State.

There are traditionally 3 hooks or phases in each State: onEnter, onUpdate, and onExit.

You start by defining all possible States that the State Machine can be in. For simple character logic it could be something like:

  • Idle
  • Move
  • Jump

Each bullet above is a State. Let's say you start by going to the Idle State. Idle would have code for each of the 3 hooks as needed and they would get called like this:

  1. Call onEnter if it is defined
  2. Then on each update tick call onUpdate if it is defined
  3. When switching to a new State, call onExit if it is defined on the current State

A State Machine can only be in 1 State at a time and you cannot switch to the same State that you are already in.

Now that you have an overview of how it works, let's get started with the implemention!

Creating a State Machine

First, let's lay out the basic skeleton of our StateMachine class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
interface IState
{
	name: string
	onEnter?: () => void
	onUpdate?: (dt: number) => void
	onExit?: () => void
}

export default class StateMachine
{
	private states = new Map<string, IState>()
	private currentState?: IState

	addState(name: string, config?: { onEnter?: () => void, onUpdate?: (dt: number) => void, onExit?: () => void })
	{
		// add a new State
	}

	setState(name: string)
	{

		// switch to State called `name`
	}

	update(dt: number)
	{
		// update current state if exists
	}
}

We start by defining an IState interface with a required name and then optional hooks for onEnter, onUpdate, and onExit as we discussed in the previous section.

Then in the StateMachine class we use a Map to store each State by their name. There's a currentState property to hold a reference to the active State if there is one. This will be used in update(dt: number) to call the onUpdate hook in the game loop.

The addState method is used to add and define a State like Idle, Move, or Jump. It takes a name and then an object that defines what should happen in each of the 3 hooks.

The setState method just takes a name of a State and will handle logic for switching from one State to another.

Before we implement each of these methods, there's some helpful things we can do to help us debug and make it easier to use in a Phaser game.

Identification and Context

It is helpful to name each StateMachine when you are using more than one to better understand who is doing what.

For example, you can have enemies that each use a StateMachine. Debugging any problems will be much easier if you can easily see which enemy is changing states.

Then for convenience, we can define a class level context property to automatically bind method references used as hooks in each State. This is nice for the case where your character class defines all the logic for each State as methods.

Let's add 2 properties and a constructor to our StateMachine class 👇

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// previous code...

// simple global id counter
let idCount = 0

export default class StateMachine
{
	private id = (++idCount).toString()
	private context?: object

	// previous code...

	constructor(context?: object, id?: string)
	{
		this.id = id ?? this.id
		this.context = context
	}

	// previous code...
}

The id and context given to the constructor are both optional. We use the idCount variable that is global to all StateMachine instances to default a simple id value for each StateMachine if none is given.

There is a default value set in the property declaration and then the constructor will use the passed in id value if it exists.

Adding a State

To add a state we just need to add an entry to our states map. Every state is assumed to have a unique name and is based on the IState interface.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
addState(name: string, config?: { onEnter?: () => void, onUpdate?: (dt: number) => void, onExit?: () => void })
{
	const context = this.context
	
	this.states.set(name, {
		name,
		onEnter: config?.onEnter?.bind(context),
		onUpdate: config?.onUpdate?.bind(context),
		onExit: config?.onExit?.bind(context)
	})

	return this
}

The addState() method takes a name and then an optional config object. That config object is where you'll pass in handlers for onEnter, onUpdate, and onExit as necessary.

The config object is optional because sometimes a state won't have any associated logic.

Inside the method we create an IState object by combining the name and defined state hooks. That object is then stored in the states map.

Notice that we are also using the class context property to bind the context of each passed in handler. This assumes we'll be defining the handlers within the same class or context. It'll make more sense when we get to an example of this in action.

Switching States

Some state machines switch states by defining transitions. Transitions can help make things clearer and easier to debug as they limit and define which state can be transitioned to from another state.

In this example, we're going to be a little reckless and allow switching to any state at any time. 😈

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
setState(name: string)
{
	if (!this.states.has(name))
	{
		console.warn(`Tried to change to unknown state: ${name}`)
		return
	}

	if (this.isCurrentState(name))
	{
		return
	}

	if (this.isChangingState)
	{
		this.changeStateQueue.push(name)
		return
	}

	this.isChangingState = true

	console.log(`[StateMachine (${this.id})] change from ${this.currentState?.name ?? 'none'} to ${name}`)

	if (this.currentState && this.currentState.onExit)
	{
		this.currentState.onExit()
	}

	this.currentState = this.states.get(name)!

	if (this.currentState.onEnter)
	{
		this.currentState.onEnter()
	}

	this.isChangingState = false
}

First thing to notice is that we are using a method and property that we have not yet defined: isCurrentState() and isChangingState. We'll create these shortly.

The idea of setState() is to take a name of the state you'd like to go to and only do it if that state exists and we are not already in that state. A state machine will not go into a state that it is already in.

To perform the transition we call onExit on the current state and then onEnter on the new state. We flag that we are in the process of changing state to avoid excuting another state change while one is already in progress. Any requests to change state from an onExit or onEnter during a state change will be queued.

Next, let's make sure we define the isCurrentState() method and isChangingState property 👇

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// imports...

export default class StateMachine
{
	// other properties...

	private isChangingState = false

	// other code...

	isCurrentState(name: string)
	{
		if (!this.currentState)
		{
			return false
		}

		return this.currentState.name === name
	}
}

Updating on Each Tick

The last thing we need to do is handle updating the current state on each tick. We do this in the update() method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
update(dt: number)
{
	if (this.changeStateQueue.length > 0)
	{
		this.setState(this.changeStateQueue.shift()!)
		return
	}

	if (this.currentState && this.currentState.onUpdate)
	{
		this.currentState.onUpdate(dt)
	}
}

We start by processing any queued state changes one at a time and only call the onUpdate() handler once there are no more queued state changes.

Once all the state changes are settled we will call onUpdate() if it exists on the current state.

And now you have a fully functioning state machine! 🎉

Usage Example

Here's a simple example of how you can use this StateMachine for a typical hero character 👇

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// NOTE: your import may be different depending on project structure
import StateMachine from './StateMachine'

class Hero
{
	private stateMachine: StateMachine

	constructor()
	{
		this.stateMachine = new StateMachine(this, 'hero')

		this.stateMachine.addState('idle')
			.addState('move', {
				onEnter: this.onMoveEnter,
				onUpdate: this.onMoveUpdate
			})
			.addState('attack', {
				onEnter: this.onAttackEnter,
				onExit: this.onAttackExit
			})

		this.stateMachine.setState('idle')
	}

	update(dt: number)
	{
		this.stateMachine.update(dt)
	}

	private onMoveEnter()
	{
		// logic for entering move state
	}

	private onMoveUpdate(dt: number)
	{
		// logic for moving on each update tick
	}

	private onAttackEnter()
	{
		// logic for attacking
	}

	private onAttackExit()
	{
		// logic for when leaving the attack state
	}

	// ...
}

Next Steps

This StateMachine is easy to get started with but could benefit from things like defined transitions or accepting State objects with handlers that use different contexts.

Having different State classes can help better organize logic and make things easier to manage for complex state machines. You can do this by changing addState() to take in an IState reference as well as an ad-hoc config.

Check out this video on YouTube for a more detailed usage example.

Be sure to sign up for our newsletter so you don't miss any future game development tips and techniques!

Drop your email into the box below.

Don't miss any future game development content

Subscribers get exclusive tips & techniques not found on the blog!

Join our newsletter. It's free. We don't spam. Spamming is for jerks.

statemachine typescript design pattern pattern

Want tips and techniques more suited for you?


You may also like...


Video Guides


Beginner Guides


Articles Recommended For You

State Pattern for Character Movement in Phaser 3

by on

Writing clean and well-organized code is something all game developers aspire to. We want code to be reusable and easy …

7 minute read

Point & Click Movement with Pathfinding using Tilemaps in Phaser 3

by on

Are you are making an action-RPG, RTS, dungeon crawler, or similar type of game using tilemaps in Phaser 3 and want …

11 minute read

Moving Platforms with Matter Physics in Phaser 3

by on

Are you working on a side-scrolling platformer in Phaser 3 using Matter Physics and having trouble with moving …

5 minute read

Firebase Leaderboard with Rex Plugins in Phaser 3

by on

Are you looking to add more replayability to your game? Leaderboards are a long-standing feature of single-player games …

9 minute read

Didn't find what you were looking for?


comments powered by Disqus