Question about State Machine

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By amaou310

Hello community. I was reading Finite State Machine in Godot by Nathan Lovato from GDQuest.

Below the Player script, there’s a line of unfinished code in the clean up the previous state section. What is it for? and What do I need to add to complete the script?

var ground_speed := 500.0
var air_speed := 300.0

var _speed := 500.0


func change_state(new_state: int) -> void:
	var previous_state := _state
	_state = new_state

	# Initialize the new state.
	match _state:
		States.IN_AIR:
			_speed = air_speed
		States.ON_GROUND:
			_speed = ground_speed

	# Clean up the previous state.
	match previous_state:
		States.IN_AIR:
			#...
:bust_in_silhouette: Reply From: USBashka

It can be used to turn off effects that used for IN_AIR state (and maybe play landing effect, not sure). If you have nothing to clean, just write pass.

Thank you USBashika. I really appreciate your help. I have another problem that Id like to ask you. When I played the scene, the error occurs saying “Attempt to call function ‘enter’ in base ‘null instance’ on a null instance.” Do you have any idea why and how to fix it?

I simply copied and pasted script from the tutorial, so have no idea why this problem occurs.

# Generic state machine. Initializes states and delegates engine callbacks
# (_physics_process, _unhandled_input) to the active state.
class_name StateMachine
extends Node

# Emitted when transitioning to a new state.
signal transitioned(state_name)

# Path to the initial active state. We export it to be able to pick the initial state in the inspector.
export var initial_state := NodePath()

# The current active state. At the start of the game, we get the `initial_state`.
onready var state: State = get_node(initial_state)


func _ready() -> void:
	yield(owner, "ready")
	# The state machine assigns itself to the State objects' state_machine property.
	for child in get_children():
		child.state_machine = self
	state.enter()


# The state machine subscribes to node callbacks and delegates them to the state objects.
func _unhandled_input(event: InputEvent) -> void:
	state.handle_input(event)


func _process(delta: float) -> void:
	state.update(delta)


func _physics_process(delta: float) -> void:
	state.physics_update(delta)


# This function calls the current state's exit() function, then changes the active state,
# and calls its enter function.
# It optionally takes a `msg` dictionary to pass to the next state's enter() function.
func transition_to(target_state_name: String, msg: Dictionary = {}) -> void:
	# Safety check, you could use an assert() here to report an error if the state name is incorrect.
	# We don't use an assert here to help with code reuse. If you reuse a state in different state machines
	# but you don't want them all, they won't be able to transition to states that aren't in the scene tree.
	if not has_node(target_state_name):
		return

	state.exit()
	state = get_node(target_state_name)
	state.enter(msg)
	emit_signal("transitioned", state.name)

amaou310 | 2022-07-14 13:11

You forgot to pick the initial_state (Initial State) in the inspector tab. It is needed in _ready function to enter to initial state.

USBashka | 2022-07-14 14:14

Thank you again. Understood that I need to assign the “Idle” in the inspector tab.
Another problem is that “Invalid set index ‘velocity’ (on base: ‘KinematicBody2D (player.gd)’) with value of type ‘Vector2’.” I have “player.velocity = Vector2.ZERO” in the PlayerIdle script, and “_velocity = Vector2.ZERO” in the Player script.

# Idle.gd
extends PlayerState

# Upon entering the state, we set the Player node's velocity to zero.
func enter(_msg := {}) -> void:
	# We must declare all the properties we access through `owner` in the `Player.gd` script.
	player.velocity = Vector2.ZERO

func physics_update(_delta: float) -> void:
	# If you have platforms that break when standing on them, you need that check for 
	# the character to fall.
	if not player.is_on_floor():
		state_machine.transition_to("Air")
		return
	
	if Input.is_action_pressed("move_left") or Input.is_action_pressed("move_right"):
		state_machine.transition_to("Move")

Player script:

# Character that moves and jumps.
class_name Player
extends KinematicBody2D

# An enum allows us to keep track of valid states.
enum States {ON_GROUND, IN_AIR}

var _velocity = Vector2.ZERO

export var speed := 200.0
export var gravity := 1200.0

var ground_speed := 500.0
var air_speed := 300.0
var _speed := 500.0

# With a variable that keeps track of the current state, we don't need to add more booleans.
var _state : int = States.ON_GROUND


func _physics_process(delta: float) -> void:
	
	var input_direction_x: float = (
		Input.get_action_strength("move_right")
		- Input.get_action_strength("move_left")
	)
	
	_velocity.x = input_direction_x * speed
	_velocity.y = gravity * delta
	_velocity = move_and_slide(_velocity, Vector2.UP)
	
	
	var is_jumping: bool = _state == States.ON_GROUND and Input.is_action_just_pressed("move_up")
	
	if is_on_floor():
		_state = States.ON_GROUND


func change_state(new_state: int) -> void:
	var previous_state := _state
	_state = new_state
	
	match _state:
		States.IN_AIR:
			_speed = air_speed
		States.ON_GROUND:
			_speed = ground_speed
	
	match previous_state:
		States.IN_AIR:
			pass
		

amaou310 | 2022-07-15 04:20

Sorry for delay. I think it’s obvious: velocity is not _velocity

USBashka | 2022-07-16 04:45

Thank you for the help USBashka. Tbh, this node based state machine was too difficult to implement for me. I wish I could do this, but I just gave up. Instead, I followed this tutorial and implemented much simpler state machine. Anyway, thank you for the help.

amaou310 | 2022-07-18 14:18