Need help with adding a wall jump state within a Finite State Machine

Godot Version

4.2.2 stable

Question

Hello, I have once again come to seek your help.

I made a FSM by following this great tutorial https://www.youtube.com/watch?v=hqys6_S2WQs by GeMakesGames. But now, I am lost. I want the player to be able to walljump only against a specific layer in my game. My main problem is adding a way to stop the player from jumping against the wall it is currently climbing against (either by just pressing “jump” or by pressing the direction against it and pressing “jump”.

Here are the scripts that I think are revelant:

fsm.gf:

extends Node
class_name FSM

var states = {}
var current_state
var current_state_node
var previous_state

func _ready():
	var object = get_parent()
	for child in get_children():
		if child is State:
			states[child.name.to_lower()] = child
			child.fsm = self
			child.object = object

func physics_update(delta):
	if not current_state: return
	current_state_node.update(delta)
	
	if current_state: current_state_node.update(delta)

func _physics_process(delta):
	if not current_state: return
	current_state_node.physics_update(delta)

func change_state(next_state):
	if current_state:
		current_state_node.exit()
		
	previous_state = current_state
	current_state = next_state
	current_state_node = states[next_state]
	current_state_node.enter()

input_handler.gd:

extends Node

@onready var jump_buffer_timer = $JumpBufferTimer

var x = 0
var jump_pressed = false

var jump_just_pressed = false:
	get:
		return jump_just_pressed
	set(value):
		jump_just_pressed = value
		if value: jump_buffer = true

var jump_buffer:
	get:
		return not jump_buffer_timer.is_stopped()
	set(value):
		if value: jump_buffer_timer.start()
		else: jump_buffer_timer.stop()

func update():
	x = Input.get_axis("btn_left", "btn_right")
	jump_just_pressed = Input.is_action_just_pressed("btn_jump")
	jump_pressed = Input.is_action_pressed("btn_jump")

player.gd:

extends CharacterBody2D
class_name Player

@onready var fsm = $FSM
@onready var sprite = $AnimatedSprite2D
@onready var input = $InputHandler

const AIR_MULTIPLIER = 0.7
const MAX_SPEED = 90.0
const ACCELERATION = 900.0

const JUMP_GRAVITY = 900.0
const FALL_GRAVITY = 500.0
const TERMINAL_VELOCITY = 600.0

# direction = the direction that the player is facing
var direction :
	get: return direction
	set(value):
		if value == 0 or value == direction: return
		direction = value
		sprite.flip_h = value == -1

func _ready():
	fsm.change_state("idle")

func _physics_process(delta):
	input.update()
	fsm.physics_update(delta)

	if is_on_floor():
		# Disable collision with objects on layer 9
		set_collision_mask_value(9, false)
	else:
		# Enable collision with objects on layer 9
		set_collision_mask_value(9, true)

player_base_state.gd:

extends State
class_name PlayerBaseState

var input:
	get: return object.input

func play(animation):
	object.sprite.play(animation)

func accelerate(delta, direction = input.x):
	var mult = Player.AIR_MULTIPLIER if not object.is_on_floor() else 1.0 
	object.velocity.x = move_toward(object.velocity.x, Player.MAX_SPEED * direction, Player.ACCELERATION * mult * delta)

func apply_gravity(delta):
		var g = Player.JUMP_GRAVITY if fsm.current_state == "jump" else Player.FALL_GRAVITY
		object.velocity.y = move_toward(object.velocity.y, Player.TERMINAL_VELOCITY, g * delta)

func move(delta, apply_g, update_direction = true, direction = input.x):
		accelerate(delta, direction)
		if apply_g: apply_gravity(delta)
		if update_direction: object.direction = direction
		object.move_and_slide()

player_fall_state.gd:

extends PlayerBaseState

@onready var coyote_timer = $CoyoteTimer

func enter():
	play("fall")
	if fsm.previous_state != "jump":
		coyote_timer.start()
	
func physics_update(delta):
	move(delta, true)
	
	if not coyote_timer.is_stopped() && input.jump_just_pressed:
		change_state("jump")	
	elif object.is_on_floor():
		if input.jump_buffer:
			change_state("jump")
		elif input.x == 0:
			change_state("idle")
		else:
			change_state("run")
	elif object.is_on_wall():
		change_state("climb")

player_jump_state:

extends PlayerBaseState

var variable_jump_height

func enter():
	play("jump")
	object.velocity.y = -300
	object.velocity.x += input.x * Player.MAX_SPEED
	variable_jump_height = false
	input.jump_buffer = false
	
func physics_update(delta):
	move(delta, true)


	if not variable_jump_height and not input.jump_pressed:
		variable_jump_height = true
		if object.velocity.y <= 0:
			object.velocity.y /= 2
	if object.velocity.y >= 0:
		change_state("fall")
	elif object.is_on_wall():
		change_state("climb")

player_climb_state.gd:

extends PlayerBaseState

func enter():
	play("climb")

func physics_update(delta):
	if input.jump_pressed:
		change_state("jump")

image

And here is a video showing what I don’t want to occur:

I also want to be able to change how the jump is handled after jumping from the wall, but like I said my main issue is just that I am not managing to tell Godot that: if the player is pressing the direction against the wall or no direction at all, I don’t want it to be able to jump. I just want the player to be able to jump while pressing the opposite direction of the wall and then jumping. The player needs to be stuck against the wall until it jumps.

I tried finding answers in the documentation or here but I am way too lost. I checked multiple tutorials but there is always something that “blocks” me. I long for the day I can just stop checking tutorials and mainly use the documentation because I tried this learning code thing for a few weeks now and I have nothing to show for it, very discouraging!

Thanks in advance everyone.

I managed to do it with the help of ChatGPT… That’s the second time I ask for help after wasting a whole day on something that is probably very simple, only to find the answer a bit later.

Oh well. Now I am trying to understand why it works. I tried this solution earlier and couldn’t get it to work by myself…

Updated player_climb_state.gd =

extends PlayerBaseState

func enter():
	play("climb")

func physics_update(delta):
	# Ensure the player is pressing jump and moving away from the wall
	if input.jump_just_pressed and object.input.x != 0 and object.input.x != object.direction:
		change_state("jump")

ChatGPT also gave me extra lines of code and variables that were useless or, I think, serving the same purpose as what was already there. Anyway, it worked without them so I removed them.

From what I understand:

object.input.x != 0 means that if my player is not moving, it cannot jump, which removes the possibility of jumping “up”.

object.input.x != object.direction is the one that makes my brain hurts more. If x, which represents -1 left, 0 not moving, 1 right, is not the same as direction, which is… err… I don’t know. All I know is that it makes it so the player cannot jump against the wall, because the previous direction it was on is “disabled”.

I think it would help if I understand this code better:

var direction :
	get: return direction
	set(value):
		if value == 0 or value == direction: return
		direction = value
		sprite.flip_h = value == -1

Thanks if anyone sees that and can help! Sorry for my stream of consciousness like sentences.

To enter the “jump” state, there are three conditions that have to evaluate to true. First, the player needs to have started pressing the jump button just this frame, secondly the player has to supply some input on the x-axis (i.e. moving either to the left or right but not standing still) and lastly, that input on the x-axis must be different from the current move direction (which only leaves one of the previous two options: “left” if the player is currently moving right, or “right” if the player is currently moving left).

It’s a variable with both a getter and setter function which will be called when the variable is read or written to. The getter is actually pointless here and could be stripped away, since it just describes the default behavior. The setter will return early if the provided value is zero, which means that running direction = 0 won’t actually change the value of the variable. It will also return early if the provided value is identical to the current one, which is not strictly necessary in this case, but also not wrong (since nothing would change when setting this variable to the same value again). In all other cases, the variable’s value is updated. If the new value is equal to -1, then the flip_h of your sprite will be set to true, or false otherwise. That’s all. :slight_smile:

1 Like

Thank you for taking the time to explain it thoroughly to me! It made sense even if I will need practice to fully understand it and find other ways to use it. I will try messing with it and remove the useless part too.

Do you think the solution to this issue I have (The player can move to the opposite direction for one frame when wall climbing to a wall - #4 by denkisutando) can be fixed by adding a condition to this code since it deals with the sprite flipping? In any case, I will try… again… later today.