I need help with timeouts signaling in a different state then my current one

Godot Version

Godot-4.5

Question

For a bit of context, I am trying to make a small souls-like game, and if you are unfamiliar with these games, when clicking the sprint button you roll and when holding it after a little bit you sprint, I have been using a state machine and when I’m in the Idle state and attempt to hold sprint so that the player will start the sprint timer (as to see if I hold the button long enough to sprint) after it times out it’s supposed to check whether or not you are still holding sprint, if yes, change player speed, but, when I do this, it instead triggers the timeout, for the same timer, in the walking state and without instancing the player, so it ends up saying that the “player” is null and crashes. I’m sorry that this is so long, I’ve been struggling with this for about a month.

Idle state

extends PlayerState
@export var sprint : bool
func _enter_state(player_node):
	super(player_node)
	player.velocity.z = 0
	player.velocity.x = 0
	player.animplayer.play("Idle")
	
	
func _handle_inputs(_delta):
	if Input.is_action_just_pressed("Jump") and player.is_on_floor() and player.jumpbuffer.time_left == 0:
		player.Change_state("Jumpstate")
		player.last_state = "Idle"
		
	elif Input.get_vector("Left", "Right", "forward", "backwards"):
		if sprint == false:
			player.Change_state("walkingstate")
			player.last_state = "Idle"
		elif sprint == true:
			player.Change_state("Runningstate")
			player.last_state = "Idle"
			
	elif Input.is_action_just_released("Sprint"):
		if  player.rollingtimer.time_left > 0:
			player.rollingtimer.stop()
			player.Change_state("Rollingstate")
			player.last_state = "Idle"
			
		else:
			sprint = false
		
	elif Input.is_action_just_pressed("Sprint"):
		player.rollingtimer.start()


func _on_rollingtimer_timeout() -> void:
	if Input.is_action_pressed("Sprint"):
		sprint = true
	else:
		sprint = false







walking state


extends PlayerState


func _enter_state(player_node):
	super(player_node)
	player.SPEED = player.Walk_SPEED
	player.animplayer.speed_scale = 1


func _handle_inputs(delta):
	
	var input_dir = Input.get_vector("Left", "Right", "forward", "backwards")
	var direction = (player.spring_arm.transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
	
	if direction:
		player.animplayer.play("Walk")
		player.velocity.x = direction.x * player.SPEED
		player.velocity.z = direction.z * player.SPEED
		player.theta = wrapf(atan2(-direction.z, direction.x) - player.visuals.rotation.y - PI/2, -PI, PI)
		player.visuals.rotation.y += clamp(player.rotation_speed * delta, 0, abs(player.theta)) * sign(player.theta)
		player.last_faced_direction.x = direction.x
		player.last_faced_direction.z = direction.z
	else:
		player.Change_state("Idlestate")
		player.last_state = "Walk"
	if Input.is_action_just_pressed("Jump") and player.is_on_floor() and player.jumpbuffer.time_left == 0:
		player.Change_state("Jumpstate")
		player.last_state = "Walk"
		
	elif Input.is_action_just_pressed("Sprint"):
		player.rollingtimer.start()
		
	elif Input.is_action_just_released("Sprint") and player.rollingtimer.time_left > 0:
		player.rollingtimer.stop()
		player.Change_state("Rollingstate")
		player.last_state = "Walk"
		
	player.move_and_slide()
	


func _on_rollingtimer_timeout() -> void:
	if Input.is_action_pressed("Sprint"):
		player.Change_state("Runningstate")
		player.last_state = "Walk"

Gonna need to see the code for PlayerState and if that extends something non-standard, that too. Based on your description I have an idea of where your problem is, but it’s not in this code. It’s farther up.

I’m guessing you are calling _handle_inputs() in _process() or _physics_process(), and you’re not turning those off and on appropriately when you enter/exit states.

This is the playerstate code, and an odd issue with it is that it works when I transition from Idle-Walking then back to Idle again, then it starts working, but I can’t off the bat.

extends Node

class_name PlayerState

var player

func _enter_state(player_node):

player = player_node

func _exit_state():
pass

func _handle_inputs(_delta):
pass

Please format your code correctly when posting it. Press Ctrl + E in the forum editor and paste the code between the ``` marks above and below the code.

Ok, so based on the code you are showing me, your states shouldn’t work at all. So I’m guessing there’s a State Machine that has code that is also involved. Gonna need to see that too.

Sorry, I’m new to asking questions because until now I’ve always managed to figure it out, anyways, here’s the state machine controller.

extends CharacterBody3D

var current_state
@export var SPEED :float
@export var JUMP_VELOCITY :float = 4
@onready var spring_arm: SpringArm3D = $SpringArm
var theta : float
@export var rotation_speed : float = TAU * 2
@onready var visuals : Node3D = $Visuals
@onready var animplayer : AnimationPlayer = $Visuals/BaseCharacter/AnimationPlayer
var last_faced_direction : Vector3
@export var Walk_SPEED : float = 2
@export var Run_SPEED : float = 3
@onready var jumpbuffer: Timer = $Timers/Jumpbuffer
@export var last_state : String
@onready var rollingtimer: Timer = $Timers/Rollingtimer
@export var rolling_time : float = 1.5
@onready var rollduration: Timer = $Timers/Rollduration
@onready var comboroll: Timer = $Timers/comboroll


func _ready() -> void:
	SPEED = Walk_SPEED
	Change_state("Idlestate")
func Change_state(new_state_name: String):
	if current_state:
		current_state._exit_state()
	current_state = get_node(new_state_name)
	
	if current_state:
		current_state._enter_state(self)
func _physics_process(delta: float) -> void:
	rollduration.wait_time = rolling_time
	if not is_on_floor():
		velocity += get_gravity() * delta
		
	if current_state:
		current_state._handle_inputs(delta)
	move_and_slide()

Ok, I see the problem. _physics_process() can actually run before _ready() is finished running.

Do something like this:

extends CharacterBody3D

@export var speed: float
@export var JUMP_VELOCITY: float = 4
@export var rotation_speed: float = TAU * 2
@export var walk_speed: float = 2
@export var run_speed: float = 3
@export var last_state: String
@export var rolling_time: float = 1.5

var current_state
var theta: float
var last_faced_direction: Vector3
var ready := false

@onready var spring_arm: SpringArm3D = $SpringArm
@onready var visuals : Node3D = $Visuals
@onready var animplayer : AnimationPlayer = $Visuals/BaseCharacter/AnimationPlayer
@onready var jumpbuffer: Timer = $Timers/Jumpbuffer
@onready var rollingtimer: Timer = $Timers/Rollingtimer
@onready var rollduration: Timer = $Timers/Rollduration
@onready var comboroll: Timer = $Timers/comboroll


func _ready() -> void:
	speed = walk_speed
	change_state("Idlestate")
	ready.connect(func(): ready = true)


func change_state(new_state_name: String):
	if current_state:
		current_state._exit_state()
	current_state = get_node(new_state_name)
	
	if current_state:
		current_state._enter_state(self)


func _physics_process(delta: float) -> void:
	if not ready:
		return

	rollduration.wait_time = rolling_time
	if not is_on_floor():
		velocity += get_gravity() * delta
		
	if current_state:
		current_state._handle_inputs(delta)
	move_and_slide()

I also reorganized your variables for you to make them a little clearer. It’s also not recommended to use capital letters in function names. And ALL_CAPS is reserved for constans. You can see all the coding suggestions in the GDScript Style Guide.

I’m sorry, but this didn’t do anything I’m still getting the same error from this part of my walkingstate code that says “invalid call, nonexistant function change_state in base nil


func _on_rollingtimer_timeout() -> void:
	if Input.is_action_pressed("Sprint"):
		player.change_state("Runningstate")
		player.last_state = "Walk"

Try this:

extends CharacterBody3D

@export var speed: float
@export var JUMP_VELOCITY: float = 4
@export var rotation_speed: float = TAU * 2
@export var walk_speed: float = 2
@export var run_speed: float = 3
@export var last_state: String
@export var rolling_time: float = 1.5

var current_state
var theta: float
var last_faced_direction: Vector3
var ready := false

@onready var spring_arm: SpringArm3D = $SpringArm
@onready var visuals : Node3D = $Visuals
@onready var animplayer : AnimationPlayer = $Visuals/BaseCharacter/AnimationPlayer
@onready var jumpbuffer: Timer = $Timers/Jumpbuffer
@onready var rollingtimer: Timer = $Timers/Rollingtimer
@onready var rollduration: Timer = $Timers/Rollduration
@onready var comboroll: Timer = $Timers/comboroll


func _ready() -> void:
	speed = walk_speed
	ready.connect(_on_ready)

func _on_ready() -> void:
	ready = true
	change_state("Idlestate")

func change_state(new_state_name: String) -> void:
	if current_state:
		current_state._exit_state()
	current_state = get_node(new_state_name)
	
	if current_state:
		current_state._enter_state(self)


func _physics_process(delta: float) -> void:
	if not ready:
		return

	rollduration.wait_time = rolling_time
	if not is_on_floor():
		velocity += get_gravity() * delta
		
	if current_state:
		current_state._handle_inputs(delta)
	move_and_slide()

If that doesn’t work then your WalkingState is somehow entering with a null reference to Player.

Check to make sure when you switch state it’s actually finding the node by printing out the node name.

the issue is I’m not even triggering entering the walking state, I’m holding sprint which is supposed to change my player speed so when I start moving I enter the sprint state, and I’m doing that by having a timer count down, so after a while if the button is still being held it changes my speed but instead it seems to be triggering the timeout function in the walkingstate instead of the idlestate code

and since I haven’t entered the walking state yet, it doesn’t have a reference for player

Ok, I was under the impression this was happening as soon as you started the project. So this is really an architectural issue. Your states require the passing of a reference to the player every time they start or they clear it out. I recommend starting it upon initialization.

Your specific problem is that your _on_rollingtimer_timeout() function executes whenever that timer finished - it doesn’t matter if it’s in that state or not. If you want it to stop, you can set_process_input(false) when you initialize the state and exit it, and set_process_input(true) when you enter it.

If you want to look at an alternate ways to handle state machines, you can also check out my State Machine Plugin. It’s free and comes with a comprehensive Readme. It handles a number of issues you are likely to encounter with state machines.

Thank you so much for your help, you’ve been a life saver I was afraid I needed to scrap the entire code, I’m happy you helped me fix it.

1 Like