The title itself should give a brief idea but a much more elaborate explanation would be this:
I was working on an Object-Based Finite State Machine to handle my player states since there’s three of them: Ground, Air, and Wall states. The only Node that the player accesses these states is the StateMachine node itself. Again, I’m using an Object-Based FSM.
I tried implementing a Wall Latch / Wall Slide mechanism that should obviously switch to the Falling state if we’re not sliding on a wall. In my Wall states, I had this line “if input_x == 0 or not player.is_on_wall_only(): blah blah”. It should just switch the Wall state to the Falling state when we’re either a) not holding any directional input or b) if we’re not on a wall. This is to prevent the player from sliding on empty space.
But that’s the issue, it works just fine. Sliding on an empty space will transition it to the Falling state. But for some reason, my states keep flickering? It would’ve been fine if it wasn’t for the fact I have specific functionality in the Wall Latch and Wall Slide states.
Removing that “or not player.is_on_wall_only()” removes the issue, but it also prevents the “empty space wall sliding” checker.
I already tried using a cache / grace timer to give the “is_on_wall_only()” check some breathing room. But that didn’t help the problem. It just delayed the “state switching spam”.
Anyone have any clue? I’m happy to provide as much of my script setup as possible, but that would get too cluttered so other than this explanation, this video demonstrates the bug: https://youtu.be/I5I5Knyid0Q?si=FFzQikRJHdmeBIGG
Edit: I probably should’ve added at least the important stuff. Thank you to “wchc” for letting me know.
StateMachine.gd
class_name StateMachine
extends Node
var player: Nox
@export var initial_state: String = "ground_idle"
var current_state: States
var states: Dictionary
func _ready():
player = get_parent()
# Instantation of all states (looks a bit messy but this is how it's done on Object-Based / Script-Based FSMs
states["ground_idle"] = GroundIdle.new(self, player)
states["ground_walk"] = GroundWalk.new(self, player)
states["ground_sprint"] = GroundSprint.new(self, player)
states["ground_stalk"] = GroundStalk.new(self, player)
states["air_jump"] = AirJump.new(self, player)
states["air_super_jump"] = AirSuperJump.new(self, player)
states["air_fall"] = AirFall.new(self, player)
states["wall_latch"] = WallLatch.new(self, player)
states["wall_slide"] = WallSlide.new(self, player)
states["wall_jump"] = WallJump.new(self, player)
# Just a simple check to see if our initial player state actually exists
if initial_state and states.has(initial_state.to_lower()):
change_state(initial_state.to_lower())
else:
push_error("Initial player state not found or set!")
func _process(delta):
if current_state:
current_state.update(delta)
func _physics_process(delta):
if current_state:
current_state.physics_update(delta)
func change_state(new_state_name: String) -> void:
if current_state:
current_state.exit()
current_state = states.get(new_state_name.to_lower())
if current_state:
current_state.enter()
States.gd
class_name States
extends RefCounted
var player: Nox
var state_machine: StateMachine
var input_x: float = 0.0
# Constructor
func _init(_state_machine: StateMachine, _player: Nox) -> void:
state_machine = _state_machine
player = _player
# Virtual methods -- overriden later by the individual states
func enter() -> void: pass
func exit() -> void: pass
func update(_delta: float) -> void: pass
func physics_update(_delta: float) -> void: pass
func handle_input(_event: InputEvent) -> void: pass
Air.gd
class_name Air
extends States
func physics_update(_delta) -> void:
input_x = Input.get_axis("move_left", "move_right")
# Coyote Time Monitor
if player.is_on_floor():
player.CoyoteTime.stop()
player.is_coyote_active = false
else:
if player.CoyoteTime.is_stopped():
player.CoyoteTime.start()
player.is_coyote_active = true
# Gravity Application -- should apply after we check for Coyote Time
var gravity: float = player.jump_gravity if player.velocity.y > 0 else player.fall_gravity
var gravity_scaling: float = 1.4 if player.is_using_superjump else 1.0
player.velocity.y -= gravity * gravity_scaling * _delta
var target_speed: float = input_x * player.desired_max_speed
player.velocity.x = lerp(
player.velocity.x,
target_speed,
player.speed_change_rate * _delta
)
# Wall Transition Checkers
var wall_normal = player.get_wall_normal()
var moving_into_wall = player.velocity.dot(-wall_normal) > 0.1
# State Transitions (Wall has first priority)
if player.can_wall_latch and moving_into_wall:
state_machine.change_state("wall_latch")
return
if player.is_on_floor():
player.can_wall_latch = true
state_machine.change_state("ground_idle")
return
Wall.gd
class_name Wall
extends States
func physics_update(_delta) -> void:
input_x = Input.get_axis("move_left", "move_right")
# Hard check for if the player tries to latch onto a wall while jumping from the groun
# Earlier bug had the player fly up instead of sliding down, this just prevents that
# Can be improved
if player.velocity.y > 0:
player.velocity.y = 0
if Input.is_action_just_pressed("jump"):
state_machine.change_state("wall_jump")
return
if input_x == 0 or not player.is_on_wall_only():
if player.WallCountdown.is_stopped():
player.can_wall_latch = false
state_machine.change_state("air_fall")
return
^^^ All of this should be relevant to the problem. ^^^
Thanks again to whoever can help me with this!