Help with creating a State Machine for a grid-based first-person controller

Godot Version

Godot 4.5.1 Stable

Question

I’ve run into a snag in attempting to create a state machine for use in my practice Dungeon Crawler. It appears to be a conflict between the requirements of the movement code I’m using and the State Machine itself.

First, the State Machine I’m working with is derived from this video: https://www.youtube.com/watch?v=VtJXqRsFezY. It is a node-based system that uses a basic Node as the PlayerStateMachine and several child nodes that correspond to movement. In my case, the states I want are for movement, rotation and idle.

The movement code I’m using uses tweening, and uses the transform method that requires a CharacterBody3D/Node3D.

Movement Code

func _physics_process(_delta: float) → void:

if tween is Tween:
    if tween.is_running():
       return

if Input.is_action_pressed("Forward") and !forward.is_colliding():
	tween = create_tween().set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
	tween.tween_property(self, "transform", transform.translated_local(Vector3.FORWARD * 2), SPEED)
	
if Input.is_action_pressed("Backwards"):
		tween = create_tween().set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
		tween.tween_property(self, "rotation:y", rotation.y + deg_to_rad (int(180)), 0.7)

if Input.is_action_pressed("Left"):
	tween = create_tween().set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
	tween.tween_property(self, "transform:basis", transform.basis.rotated(Vector3.UP, PI / 2), SPEED)
	
if Input.is_action_pressed("Right"):
	tween = create_tween().set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
	tween.tween_property(self, "transform:basis", transform.basis.rotated(Vector3.UP, -PI / 2), SPEED)

if Input.is_action_pressed("Strafe L") and not left.is_colliding():
	tween = create_tween().set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
	tween.tween_property(self, "transform", transform.translated_local(Vector3.LEFT * 2), SPEED)
	
if Input.is_action_pressed("Strafe R") and not right.is_colliding():
	tween = create_tween().set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
	tween.tween_property(self, "transform", transform.translated_local(Vector3.RIGHT * 2), SPEED)

So the problem I’m facing is that transform is not compatible with the Node that the state machine scripts are attaching to, and changing these nodes to a Node3D or CharacterBody3D breaks it entirely (I receive ‘null instance’ errors). I’m attempting to troubleshoot that part on my own, but my question is really regarding my current movement code and if this is the right approach I should be taking.

The current state.gd looks like this, for reference

State.gd

```

class_name State

extends Node3D

signal transitioned(new_state_name: StringName)

#Enter a state
func enter() → void:
pass

#Exit a state
func exit() → void:
pass

#update process
func update(_delta: float) → void:
pass

#update physics process
func physics_update(_delta: float) → void:
pass

And here is IdleState.GD

```

class_name IdleState

extends State

func update(_delta: float) → void:

if Input.is_action_pressed(“Forward”):
transitioned.emit(“MoveState”)


elif Input.is_action_pressed("Left") or Input.is_action_pressed("Right"):
		transitioned.emit("RotateState")

```

If the script is in a plain state node then it doesn’t make sense to tween transform properties of that node. Tween the actual character body node.

Thank you for the response! This was a great help.

I’ve been trying to go about tweening the character body, but so far I’m still being met with errors:

class_name MoveState

extends State

var player = preload(“res://src/player/player.gd”)

func _physics_process(delta: float) → void:if Input.is_action_pressed(“Forward”):player.tween = create_tween().set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)player.tween.tween_property(player, “transform”, player.translated_local(Vector3.FORWARD * 2), player.SPEED)

if Input.is_action_pressed("Backwards"):
		player.tween = create_tween().set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
		player.tween.tween_property(player, "rotation:y", player.rotation.y + deg_to_rad (int(180)), 0.7)

“E 0:00:02:143 MoveState._physics_process: Invalid assignment of property or key ‘tween’ with value of type ‘Tween’ on a base object of type ‘GDScript’.
move_state.gd:8 @ MoveState._physics_process()
move_state.gd:8 @ _physics_process()”

What is the proper protocol here?

player should be a node reference. Currently you’re assigning a script resource to it. You can assign this in several ways depending on your scene hierarchy. The tutorial you linked does it using an autoload named Global that holds a reference to the player node.

So make a script like this and in project settings set it up as an autoload named Global:

extends Node

var player = null

Then let the player’s script initialize this in its _ready() with a reference to self:

func _ready():
    Global.player = self

Once this is done you can access the player node from any other script using Global.player:

Global.player.tween.tween_property(Global.player, "rotation:y", Global.player.rotation.y + deg_to_rad (int(180)), 0.7)

Thank you so much! That did it :slight_smile:

1 Like