Weird bug with Wall Sliding / Wall Latching interaction and is_on_wall_only() check [State Machine Help]

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!

We need to see the code snippets of at least the StateMachine class, States class, FallingState class, WallLatchState class.
Paste your code as preformatted text between the tripple backticks ``` on both beginning and end of the snippet.

2 Likes

Just updated it now. Please have a look! Thanks!

1 Like