Updated Code For Godot 4.1 Users

MilkDrinker

So with Godot 4.1, we dont have to worry about the cyclic dependency issue anymore, which means that the code becomes simpler as we don't need to do is_in_group() checks anymore and we can also fully take advantage of the new way properties work to avoid having boiler plate getters and setters methods all over the place. This works perfectly for me, but I am by no means an expert on GD script so if there are some suggestions on how to further improve the code then by all means please let me know and adapt it.

I'll be doing this for the rest of the course as the need arises so yeah :)

StateMachine Class

@icon("res://Assets/2D/Icons/state_machine.svg")
extends Node
class_name StateMachine

signal transitioned(state_path)

@export var initial_state:= NodePath()

@onready var state: State = get_node(initial_state):
get:
return state
set(value):
state = value
_state_name = state.name

var _state_name:= ""

func _ready():
await owner.ready
state.enter()

func _unhandled_input(event):
state.unhandled_input(event)

func _process(delta):
state.process(delta)

func _physics_process(delta):
state.physics_process(delta)

func transition_to(target_state_path: String, message:= {}):
if not has_node(target_state_path):
return

var target_state:= get_node(target_state_path)
state.exit()
self.state = target_state
state.enter(message)
emit_signal("transitioned",target_state_path)

Note that the yield has been changed to await with owner now having a ready enum instead of a string, and we can do the setting of the _state_name and state object itself through the getter and setter.

State Class

@icon("res://Assets/2D/Icons/state.svg")
extends Node
class_name State

var _state_machine: StateMachine:
get:
var node: Node = self
while node != null:
if node is StateMachine:
return node
node = node.get_parent()
return null

var _parent:State = null

func _ready():
await owner.ready
var parent = get_parent()
if not parent is StateMachine:
_parent = parent

func unhandled_input(event):
return

func process(delta):
return

func physics_process(delta):
return

func enter(message:= {}):
return

func exit():
return

In the State class, theres no need to annotate the _state_machine as @onready because it's not necessary when you're using a property with a custom getter. The custom getter defines what value you get whenever you access _state_machine, making @onready redundant.

I also used a while loop instead of recursion to travel up the tree and find the state machine. This does essentially the same thing as the recursive _get_state_machine(), but directly within the property getter.

Note that i can now also just do

if node is StateMachine:

because Godot 4.1 has fixed the cyclic dependency issue now.

Last thing is that I found is that all I needed for the built in extended scripts is to go

func unhandled_input(event):
    if event.is_action_pressed("ui_accept"):
        _state_machine.transition_to("Run")

Adding that additional line about the input being handled is not needed.

1 love
Reply
  • Nathan Lovato replied

    Thank you so much for taking the time to do this!

    Note that you can directly export a node type as well to the inspector. You don't need to go through a NodePath anymore. You can remove the initial_state variable and just do:

    @export var state: State = null ...

    And it'll display a slot to select a node that extends State in the Inspector.

    2 loves