The player can move to the opposite direction for one frame when wall climbing to a wall

Godot Version:

4.2.2 stable

Question:

Hello everyone, nice to meet you. This is my first post here and hopefully not my last.

I am trying to recreate the game mechanics of “Ninja Gaiden” as a way to learn and train.
I am running into a “bug” that I would need help fixing. Coding is unfortunately proving extremely difficult for me and I am at my wits’ end with this one!

Bug:

Player’s “climb” animation can face the wrong direction by jumping against a wall from a wall jump when colliding with the wall while pressing the opposite direction.

It’s probably horrendous so I am sorry about that, but first here is my player’s script:

extends CharacterBody2D

@onready var sprite = $AnimatedSprite2D
@onready var wall_checker = $WallChecker

const SPEED = 100.0
const JUMP_VELOCITY = -310.0
var last_direction = 1
var direction = 1
var prev_velocity = Vector2.ZERO
var gravity: int = 900
var is_sticking = false
var is_crouching: bool = false
var is_attacking: bool
var is_wall_jumping: bool = false

func _ready():
	is_attacking = false

func _physics_process(delta):
	print(is_near_wall())
	
	if direction != 0:
		last_direction = direction
	
	# ADD THE GRAVITY.
	if not is_on_floor():
		velocity.y += gravity * delta
		prev_velocity = velocity

	# HANDLE JUMP.
	if Input.is_action_just_pressed("jump") and is_on_floor() and not is_attacking:
		velocity.y = JUMP_VELOCITY
		is_wall_jumping = false

	# HANDLE CROUCH.
	if not is_attacking:
		if Input.is_action_pressed("crouch") and is_on_floor():
			is_crouching = true
		else:
			is_crouching = false

	# Get the input direction and handle the movement/deceleration.
	var direction = Input.get_axis("move_left", "move_right")
	
	if is_attacking and is_on_floor():
		velocity.x = 0  # Stop movement during attack
	else:
		if direction:
			velocity.x = direction * SPEED
		else:
			velocity.x = move_toward(velocity.x, 0, SPEED)

	if not is_on_floor() and not is_wall_jumping:
		velocity.x = lerp(prev_velocity.x, velocity.x, 0.090 / 2)

	if not is_attacking and !is_sticking:  # Condition to check if not sticking to a wall
		if Input.is_action_just_pressed("attack"):  # Check attack input only when not sticking to a wall
			is_attacking = true
			handle_attack_animation(delta)

	set_direction()
	handle_sticking()
	move_and_slide()

	# Update animations
	handle_movement_animation(direction)
		
	toggle_flip_sprite(direction)

func set_direction():
	direction = 1 if not sprite.flip_h else -1
	wall_checker.rotation_degrees = 90 * -direction

func is_near_wall():
	return wall_checker.is_colliding()

func handle_sticking():
	var direction = Input.get_axis("move_left", "move_right")
	if !is_on_floor() and is_on_wall() and wall_checker.is_colliding():
		is_sticking = true
		velocity.x = 0
		velocity.y = 0
		sprite.play("climb")
		
	elif !is_on_floor() and is_on_wall() and !wall_checker.is_colliding():
		sprite.flip_h = direction < 0
		is_sticking = true
		velocity.x = 0
		velocity.y = 0
		sprite.play("climb")

	if Input.is_action_just_pressed("jump") and is_on_wall() and wall_checker.is_colliding():
		if direction != 0 and direction != last_direction:
			sprite.flip_h = direction < 0
			is_sticking = false
			velocity.y = JUMP_VELOCITY / 1.5
			velocity.x = direction * SPEED / 1
			is_wall_jumping = true

func handle_movement_animation(dir):
	if is_on_floor() and not is_attacking and !is_on_wall():
		if velocity.length() == 0:
			if is_crouching:
				sprite.play("crouch")
			else:
				sprite.play("stand")
		else:
			sprite.play("run")
	elif not is_on_floor() and not is_attacking and !is_on_wall():
		sprite.play("jump")
		
func toggle_flip_sprite(direction):
	if direction != 0 and is_on_floor() and !is_on_wall():
		sprite.flip_h = direction < 0

func handle_attack_animation(delta):
	if is_attacking and !is_on_wall():
		if is_crouching:
			sprite.play("crouch_attack")
		else:
			sprite.play("stand_attack")
	if is_attacking and !is_on_floor() and !is_on_wall():
		velocity.y += gravity * delta + 11 # Counteract gravity to prevent gaining height when attacking while jumping

func _on_animated_sprite_2d_animation_finished():
	is_attacking = false`Preformatted text`

Here is a video in which you can see the issue occuring, which might help you understand more what the issue is:

https://www.youtube.com/watch?v=I5tYyUUE1ig

Steps to reproduce:

  1. Jump against a wall
  2. Wall jump from the same wall
  3. Go against the same wall or another one
  4. Press the direction opposite to the wall while colliding against it
  5. Notice the “climb” sprite is in the wrong direction

Notes:

  1. Issue doesn’t seem to occur when the player is jumping from the floor to the wall
  2. Player can still turn toward the wall when facing the wrong direction in the “climb” state
  3. Player cannot go back to the wrong direction once they turned back to correctly face the wall

I hope it’s enough information to help me fix it. Hopefully the code is not so bad that it needs a total overhaul haha… Also, even with help, it’s possible I don’t understand what you are trying to explain to me, so I might ask some questions again. Sorry.

Thank you!

this block of code might be the reason
you might want to make the player no longer stick once the “wall checker” isn’t colliding anymore

1 Like

Thank you for answering me so fast! I managed to fix it earlier while my post was still pending. You were right, this part was indeed the issue. I removed

velocity.x = 0
velocity.y = 0

from it and it suddenly worked like I wanted. I am not entirely sure why though.

On to the next issue I have… Which I will make a post about once I am sure I don’t find the answer myself or somewhere. I have been trying to a few hours though so I will likely do it sooner than later.

I have the very same problem with another project. And once again I am stuck…

Issue: If the player pressed the opposite direction of the wall right before colliding with it, the “climb” animation is on the wrong side. It does not occur if the player jump and holds the direction in the same direction of the wall. It happens in both the fall and jump state.

Here are the revelant scripts:

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

var last_direction = 1
var current_direction = 1

# 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)

 # Update last_direction based on current_direction
	last_direction = current_direction if input.x == 0 else input.x

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_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")

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.gd =

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")

Video of the issue: https://youtu.be/rJqrBqgez3s

Thank you in advance! I am sure the answer is obvious but I tried and tried…