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