Section 3: State Machine

In this section, we talked about the concept of a state machine and how to set one up for the player to add support for various states.

Section Intro - State Machine

There are no notes for this lecture.

What is a State machine?

In this lecture, we discussed what a state machine is and why it's useful. We defined two terms.

  • State - Refers to the current action being performed by the player, such as walking, attacking or dashing.

  • State Machine - Refers to a program responsible for switching between states and managing them.

Creating an Idle and Move State

In this lecture, we created classes to represent each state for the player. One class per state. For the idle state, we wrote the following code:

using Godot;
using System;

public partial class PlayerIdleState : Node
{
    public override void _Ready()
    {
        Player characterNode = GetOwner<Player>();
        characterNode.animPlayerNode.Play(GameConstants.ANIM_IDLE);
    }
}

When the state is initialized, we're grabbing the Player class since this class is attached to the root node. The root node is considered to be the owner of all the other nodes in the hierarchy for a scene. Once we grab the Player class, we're accessing the AnimationPlayer node to play the idle animation.

We had to update our references to the AnimationPlayer node by changing their access modifiers to public instead of private.

[Export] public AnimationPlayer animPlayerNode;
[Export] public Sprite3D spriteNode;

Building the State Machine

In this lecture, we worked on a custom state machine to select an initial state and disable all other states. First, we created a node with an attached script that looks something like this:

using Godot;
using System;

public partial class StateMachine : Node
{
    [Export] private Node currentState;
    [Export] private Node[] states;

    public override void _Ready()
    {
        foreach (var state in states)
        {
            state.Notification(5001);
        }

        currentState.Notification(5002);
    }
}

In the above example, we're doing the following:

  1. Exporting a field for storing the current state, which will be a node.

  2. Exporting another field for storing an array of valid states.

  3. In the _Ready method, we're sending a notification to each node that it should be disabled.

  4. Lastly, we're enabling the current state with a notification as well.

Godot uses the notification system itself, so make sure that you're using a number that isn't taken by Godot. You can always refer to the documentation for a list of officially taken notification numbers.

After sending a notification, nodes can receive a notification by overriding the _Notification method like we did in the PlayerIdleState class.

public override void _Notification(int what)
{
    base._Notification(what);

    if (what == 5001)
    {
        GD.Print("Exiting Idle state");
    }
    else if (what == 5002)
    {
        Player characterNode = GetOwner<Player>();
        characterNode.animPlayerNode.Play(GameConstants.ANIM_IDLE);
    }
}

In this example, we're calling the base _Notification method as we don't want to completely override Godot's default behavior for handling notifications. If Godot doesn't have a response to a specific notification, we handled it ourselves. The number 5001 means we should disable a node. The number 5002 means we should enable a node. When enabling a node, we're just playing the state's animation.

Resources

Transitioning States

In this lecture, we updated our state machine to allow states to perform transitions. In the StateMachine.cs file, we defined a SwitchState method that has a generic called T to help it search for a state to switch to.

public void SwitchState<T>()
{
    Node newState = null;

    foreach (Node state in states)
    {
        if (state is T)
        {
            newState = state;
        }
    }

    if (newState == null) { return; }

    currentState = newState;
    currentState.Notification(5001);
}

During this method, we're doing a few things.

  1. Looping through the states stored in the states field.

  2. On each iteration, we're checking if the current item in the iteration has the T class. If it does, we're updating the newState variable to this state.

  3. After the loop, we're checking if a state was found. If it wasn't, we're stopping the rest of method from running.

  4. Otherwise, we're updating the currentState variable with the newState variable and then sending a notification to enable the state.

To transition between states, we can call this method with the state we'd like to switch to. For example, in the PlayerIdleState class, we're switching to the PlayerMoveState if the player's direction is not a vector zero.

public override void _PhysicsProcess(double delta)
{
    if (characterNode.direction != Vector2.Zero)
    {
        characterNode.stateMachineNode.SwitchState<PlayerMoveState>();
    }
}

Disabling Nodes

In this lecture, we disabled nodes by calling the SetPhysicsProcess method from the Node class. This method instructs Godot whether it should bother calling the _PhysicsProcess method on every frame. It accepts a boolean value. For example, in our _Ready method for both states, we're passing in false to disable this behavior.

public override void _Ready()
{
    characterNode = GetOwner<Player>();
    SetPhysicsProcess(false);
}

To enable the physics process, we're sending out a new notification from the SwitchState method.

currentState.Notification(5002);
currentState = newState;
currentState.Notification(5001);

We're using 5002 as the notification number for when a node should be disabled. It's important that we disable the current state before switching to a new one.

Lastly, in either states, we're just checking for the notification number to determine whether the state should be enabled or disabled.

if (what == 5001)
{
    SetPhysicsProcess(true);
    characterNode.animPlayerNode.Play(GameConstants.ANIM_IDLE);
}
else if (what == 5002)
{
    SetPhysicsProcess(false);
}

Setting up the Dash State

In this lecture, we took the time to set up a dash state by adding a dash animation, adding a DashState node, and then playing the animation. The only thing we did unique in this lecture was added the state nodes as child nodes to the StateMachine node so that the nodes are ready before the state machine is ready. This way, we have any references that we need before entering or exiting a state.

Transitioning to the Dash State

In this lecture, we transitioned to the dash state from the idle and move state. To perform this task, we had to query the Input class for the action pressed from the _Input method in both states like so:

if (Input.IsActionJustPressed(GameConstants.INPUT_DASH))
{
    characterNode.stateMachineNode.SwitchState<PlayerDashState>();
}

Godot provides two methods from the Input class to check if an action was pressed.

  • IsActionPressed - Can be used whenever you need to check if a button is currently pressed.

  • IsActionJustPressed - Can be used whenever you need to check if an action was just pressed. Doesn't bother checking if a button is continuously being held down.

In this case, we're using the IsActionJustPressed since we're not interesting in checking if the action is continuously being pressed.

Lastly, we called a method called SetProcessInput to disable/enable any state that has the _Input method. Passing in false prevents this method from being called. Here's an example.

SetProcessInput(false);

Adding a Dash Timer

In this lecture, we added a Timer node. During this process, we had to subscribe to a signal from the Timer node. Signals are a feature for storing and emitting events. The Timer node has an event for when the timer runs out called timeout. If you're using C#, the signal is pascal cased, so the name is Timeout.

dashTimerNode.Timeout += HandleDashTimeout;

To subscribe to a signal, we must use the += operator. The value for a signal is the name of a method that you want to run when the event is emitted. In this example, we're setting the method to HandleDashTimeout. The method we wrote looks like this where we're switching to the idle state.

private void HandleDashTimeout()
{
    characterNode.stateMachineNode.SwitchState<PlayerIdleState>();
}

Lastly, we started the timer by calling the Start method on the variable holding a reference to the timer.

dashTimerNode.Start();

Resources

Finalizing the Dash State

In this lecture, we finalized the state by moving the player while they're in the dash state. During this process, we had to check if the player was transitioning from the idle state.

if (characterNode.Velocity == Vector3.Zero)
{
    characterNode.Velocity = characterNode.spriteNode.FlipH ?
        Vector3.Left :
        Vector3.Right;
}

By checking if the Velocity property is a vector zero, we can check to make sure the player is moving. If they aren't, we're setting the Velocity property to either the Vector3.Left or Vector3.Right property.

In this example, we're using a ternary operator. A ternary operator has the condition to the left of the ? character. If the condition evaluates to true, the value to the right of the same character is used. Otherwise, the value to the right of the : character is used.

Reusing States With Inheritance

In this lecture, we outsourced some of the logic for a state in a separate class. This class is called PlayerState and it looks like the following:

using Godot;

public partial class PlayerState : Node
{
    protected Player characterNode;

    public override void _Ready()
    {
        characterNode = GetOwner<Player>();
        SetPhysicsProcess(false);
        SetProcessInput(false);
    }

    public override void _Notification(int what)
    {
        base._Notification(what);

        if (what == 5001)
        {
            SetPhysicsProcess(true);
            SetProcessInput(true);
            EnterState();
        }
        else if (what == 5002)
        {
            SetPhysicsProcess(false);
            SetProcessInput(false);
        }
    }

    protected virtual void EnterState() { }

}

In this example, we're doing a few things.

  1. We're inheriting from the Node class since only one class can be inherited and all states will need access to the node.

  2. The characterNode field is set to protected so that child classes have access to the field.

  3. We're defining a method called EnterState(), which can be overriden since it's virtual. In addition, it's called after the SetProcessInput(true) method when we're entering the state. This way, child classes will be able to perform additional setup if they need to.

After defining this class, we inherited it from all of our states. As an example, we did the following for the idle state:

public partial class PlayerIdleState : PlayerState
{
    protected override void EnterState()
    {
        characterNode.AnimPlayerNode.Play(GameConstants.ANIM_IDLE);
    }
}

In this example, we're inheriting from the PlayerState class and overriding the EnterState() method so that we can play the idle animation.

In the PlayerDashState class, we also had to override the _Ready() method even though this method was defined in the PlayerState class. In that case, we can use the base keyword to access the original _Ready() method so that we can use the parent and child methods.

public override void _Ready()
{
    base._Ready();

    dashTimerNode.Timeout += HandleDashTimeout;
}

Using Abstract Classes

In this lecture, we added the abstract class to the PlayerState class. By adding this keyword, the class cannot be instantiated. This prevents us from accidentally applying this class to a node in Godot as our game won't work when trying to attach a abstract class.

public abstract partial class PlayerState : Node
{

}

Dealing with Magic Numbers

In this lecture, we talked about magic numbers. A magic number is a number that holds meaning and is not just some ordinary number. Magic numbers can cause confusion because their meaning will not be universally understood from developer to developer. Whenever you have a magic number, it's considered good practice to define a variable for storing the magic number to provide a description.

We had two magic numbers in our codebase, which were 5001 and 5002. So, we decided to add these as constants to the GameConstants class.

public class GameConstants
{
    // Notifications
    public const int NOTIFICATION_ENTER_STATE = 5001;
    public const int NOTIFICATION_EXIT_STATE = 5002;
}

We then used these constants like so:

currentState.Notification(GameConstants.NOTIFICATION_ENTER_STATE);

Export Property Hints

In this lecture, we learned how to customize a field by adding controls for modifying a value. For example, we can add a slider to a field for numeric fields by adding the PropertyHint.Range to an exported variable. In the PlayerDashState class, we modifed the speed field like so:

[Export(PropertyHint.Range, "0,20,0.1")] private float speed = 10f;

We updated the Export variable by passing in two values. First, we must provide the type of property that is being modified. By setting it to PropertyHint.Range, Godot will understand that we have a number and will add a slider.

The second argument to this attribute are settings. For range sliders, we're setting a minimum threshold, maximum threshold, and the value to increment the number when moving the slider.

Resources

Auto Properties

In this lecture, we talked about how auto-properties can be helpful in C#. So far, we've been using public for fields that should be accessible outside of an existing class. However, that also allows them to be modified outside the class. To prevent a field from being modified but still allow it to be read, we can change it to a property.

A property allows us to refine how a variable is exposed. Here's an example with the AnimPlayerNode.

[Export] public AnimationPlayer AnimPlayerNode { get; private set; }

By adding the get and set keywords, we can change the access modifiers for each of them. For example, the set has the private modifier. This means that external classes will not be able to change the field. The property can only be read from other classes.

In addition, we changed the name to pascal case since it's a common naming convention for properties to be pascal cased.

Last updated