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:
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
.
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:
In the above example, we're doing the following:
Exporting a field for storing the current state, which will be a node.
Exporting another field for storing an array of valid states.
In the
_Ready
method, we're sending a notification to each node that it should be disabled.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.
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.
During this method, we're doing a few things.
Looping through the states stored in the
states
field.On each iteration, we're checking if the current item in the iteration has the
T
class. If it does, we're updating thenewState
variable to this state.After the loop, we're checking if a state was found. If it wasn't, we're stopping the rest of method from running.
Otherwise, we're updating the
currentState
variable with thenewState
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.
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.
To enable the physics process, we're sending out a new notification from the SwitchState
method.
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.
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:
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.
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
.
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.
Lastly, we started the timer by calling the Start
method on the variable holding a reference to the timer.
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.
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:
In this example, we're doing a few things.
We're inheriting from the
Node
class since only one class can be inherited and all states will need access to the node.The
characterNode
field is set toprotected
so that child classes have access to the field.We're defining a method called
EnterState()
, which can be overriden since it'svirtual
. In addition, it's called after theSetProcessInput(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:
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.
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.
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.
We then used these constants like so:
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:
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
C# Exported Properties - https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_exports.html
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.
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