Godot Version
4.4
Question
Fairly inexperienced in Godot, but I like to think I’m getting a decent handle on problem-solving, but I’ve got a bug that I can’t figure out:
Making a 2d side-scrolling shooter.
My player controller has an all-in-one-code state machine that works perfectly 98% of the time, as well as a simple knockback system that also provides some kickback when the guns are fired. However, I can reliably create a bug where if the player is firing from midair and then lands, the falling state takes a second or two to transition back to one of the grounded states, and occasionally can get stuck for several seconds as long as the button is held. It’s not game-breaking, but I like to understand what’s happening with these things and work out how to solve them early so they don’t become big problems later on.
I thought possibly the y vector of the knockback was causing the character to float off the ground, but I checked the velocity.y and it doesn’t seem to be stalling in that way, and I can reproduce the bug even when the knockback variable of the gun in use is set to 0.
Any ideas as to what’s going on? Including the player’s state machine, movement controls:
extends CharacterBody2D
class_name Player
# This enum lists all the possible states the character can be in.
enum States {IDLE, CROUCHING, RUNNING, CRAWLING, JUMPING, FALLING, WALLGRABBING, WALLJUMPING}
# This variable keeps track of the character's current state.
var state: States = States.IDLE
#state index
var state_names = {
States.IDLE:"IDLE",
States.CROUCHING:"CROUCHING",
States.RUNNING:"RUNNING",
States.CRAWLING:"CRAWLING",
States.JUMPING:"JUMPING",
States.FALLING:"FALLING",
States.WALLGRABBING:"WALLGRABBING",
States.WALLJUMPING:"WALLJUMPING"
}
#movement
@export var crawl_speed = 800.0
@export var speed = 1600.0
@export var jump_height = 650
@export var jump_time_to_peak = .4
@export var jump_time_to_descent = .3
@export var walljump_force: Vector2 = Vector2(2000,3500)
#acceleration must remain above 250, or state machine fidgets between run and idle against walls--cannot tell why
var acceleration = 250
var deceleration = 0.15
#jump mechanics
@onready var jump_velocity = ((2.0 * jump_height) / jump_time_to_peak) * -1.0
@onready var jump_gravity = ((-2.0 * jump_height) / (jump_time_to_peak * jump_time_to_peak)) * -1.0
@onready var fall_gravity = ((-2.0 * jump_height) / (jump_time_to_descent * jump_time_to_descent)) * -1.0
var has_double_jumped = false
var can_jump = true
var can_wall_grab = false
var is_wall_grabbing_left = false
var is_wall_grabbing_right = false
#component measures
var burn_health = 15
var knockback = Vector2.ZERO
var look_rotation: float = 0.0
@onready var burn_health_component: BurnHealthComponent = $Components/BurnHealthComponent
#gun mechanics
@onready var gun = %Gun
#timers
@onready var jump_timer: Timer = $MovementTimers/JumpTimer
@onready var wall_grab_timer: Timer = $MovementTimers/WallGrabTimer
@onready var wall_jump_timer: Timer = $MovementTimers/WallJumpTimer
#sprites
@onready var sprite_controller: Node2D = $SpriteController
@onready var body_sprite: AnimatedSprite2D = $SpriteController/BodySprite
@onready var head_sprite: AnimatedSprite2D = $SpriteController/HeadSprite
#checkers and collisions
@onready var standing_collision: CollisionShape2D = $StandingCollision
@onready var crouching_collision: CollisionShape2D = $CrouchingCollision
@onready var crouch_check_raycasts: Node2D = $CheckRaycasts/CrouchCheckRaycasts
@onready var crouch_check_left: RayCast2D = $CheckRaycasts/CrouchCheckRaycasts/CrouchCheckLeft
@onready var crouch_check_right: RayCast2D = $CheckRaycasts/CrouchCheckRaycasts/CrouchCheckRight
@onready var wall_check_raycasts: Node2D = $CheckRaycasts/WallCheckRaycasts
@onready var raycast_top_left: RayCast2D = $CheckRaycasts/WallCheckRaycasts/RaycastTopLeft
@onready var raycast_bottom_left: RayCast2D = $CheckRaycasts/WallCheckRaycasts/RaycastBottomLeft
@onready var raycast_top_right: RayCast2D = $CheckRaycasts/WallCheckRaycasts/RaycastTopRight
@onready var raycast_bottom_right: RayCast2D = $CheckRaycasts/WallCheckRaycasts/RaycastBottomRight
@onready var floor_check_raycasts: Node2D = $CheckRaycasts/FloorCheckRaycasts
@onready var floorcheck_right: RayCast2D = $CheckRaycasts/FloorCheckRaycasts/floorcheck_right
@onready var floorcheck_left: RayCast2D = $CheckRaycasts/FloorCheckRaycasts/floorcheck_left
#particles
@onready var burn_particles: CPUParticles2D = $Components/BurningStateComponent/BurnParticles
@onready var run_particles: CPUParticles2D = $ParticleEffects/RunParticles
@onready var jump_particles_left: CPUParticles2D = $ParticleEffects/JumpParticles_left
@onready var jump_particles_right: CPUParticles2D = $ParticleEffects/JumpParticles_right
var jump_particle_emitted = false
#sounds
@onready var jump_sound: AudioStreamPlayer2D = $JumpSound
const DEATH = preload("res://scenes/scenes_enemies/fly_dead.tscn")
func _ready() -> void:
burn_particles.emitting = false
head_sprite.visible = true
gun.visible = true
func _process(delta: float) -> void:
Global.player_position.emit(self.global_position)
func _physics_process(delta):
knockback = lerp(knockback, Vector2.DOWN, 0.5)
if is_on_floor():
knockback.y = 0
#State machine decision tree (no sprite management)
if is_on_floor():
if velocity.x == 0 or (Input.is_action_just_released("input_left") or Input.is_action_just_released("input_right")):
if Input.is_action_pressed("input_down") or (crouch_check_left.is_colliding() or crouch_check_right.is_colliding()):
state = States.CROUCHING
else:
state = States.IDLE
if velocity.x != 0 and (Input.is_action_pressed("input_left") or Input.is_action_pressed("input_right")):
if Input.is_action_pressed("input_down") or (crouch_check_left.is_colliding() or crouch_check_right.is_colliding()):
state = States.CRAWLING
else:
state = States.RUNNING
if is_on_wall_only() and (can_wall_grab == true):
if (raycast_bottom_left.is_colliding() and raycast_top_left.is_colliding()) and Input.is_action_pressed("input_left"):
state = States.WALLGRABBING
is_wall_grabbing_left = true
#gun.wallgrab_left = true
elif (raycast_bottom_right.is_colliding() and raycast_top_right.is_colliding()) and Input.is_action_pressed("input_right"):
state = States.WALLGRABBING
is_wall_grabbing_right = true
#gun.wallgrab_right = true
elif (!is_on_floor() and velocity.y < 0) and state != States.WALLJUMPING:
state = States.JUMPING
elif !is_on_floor() and velocity.y > 0.0 and state != States.WALLJUMPING:
state = States.FALLING
# Set Gravity
velocity.y += find_gravity() * delta + (knockback.y * .3)
#Horizontal Control
if state in [States.IDLE, States.CROUCHING, States.RUNNING, States.CRAWLING, States.JUMPING, States.FALLING]:
if get_input_velocity() == 1:
if state == States.CROUCHING or state == States.CRAWLING:
velocity.x = min(velocity.x + acceleration, crawl_speed) + knockback.x
else:
velocity.x = min(velocity.x + acceleration, speed) + knockback.x
elif get_input_velocity() == -1:
if state == States.CROUCHING or state == States.CRAWLING:
velocity.x = max(velocity.x - acceleration, -crawl_speed) + knockback.x
else:
velocity.x = max(velocity.x - acceleration, -speed) + knockback.x
else:
velocity.x = lerp(velocity.x, 0.0, deceleration) + knockback.x
can_wall_grab = false
And here is the gun code, feeding the knockback:
extends CharacterBody2D
# This enum lists all the possible states the character can be in. LENGTH is used to restart cycle
enum States {SHOTGUN, FIRE, ICE, SPORE, LENGTH}
# This variable keeps track of the character's current state.
var state: States = States.SHOTGUN
var state_names = {
States.SHOTGUN:"SHOTGUN",
States.FIRE:"FIRE",
States.ICE:"ICE",
States.SPORE:"SPORE",
States.LENGTH:"LENGTH"
}
const sporebulletPath = preload("res://scenes/scenes_bullets/spore_bullet.tscn")
const shotgunbulletPath = preload("res://scenes/scenes_bullets/shotgun_bullet.tscn")
const firebulletPath = preload("res://scenes/scenes_bullets/fire_bullet.tscn")
const icebulletPath = preload("res://scenes/scenes_bullets/ice_bullet.tscn")
var can_shoot = true
var wallgrab_left = false
var wallgrab_right = false
var gun_rotation: float = 0.0
var gun_available = true
@onready var shoot_delay: Timer = $ShootDelay
@onready var gun_sprite: AnimatedSprite2D = $GunSprite
@onready var gun_state_label: Label = $GunStateLabel
@onready var player: CharacterBody2D = $".."
@onready var shotgun_shoot_sound: AudioStreamPlayer2D = $ShotgunShootSound
@onready var spore_shoot_sound: AudioStreamPlayer2D = $SporeShootSound
func _physics_process(_delta):
move_and_slide()
gun_state_label.text = str(state_names[state])
# and ((rotation_degrees > -90) and (rotation_degrees < 90))
func _process(_delta):
if Input.is_action_pressed("input_shoot"):
shoot()
$".".look_at(get_global_mouse_position())
if wallgrab_left == true:
if get_rotation_degrees() < -70:
set_rotation_degrees(-70)
if get_rotation_degrees() > 70:
set_rotation_degrees(70)
if wallgrab_right == true:
if (get_rotation_degrees() > -110) and (get_rotation_degrees()< 0):
set_rotation_degrees(-110)
if (get_rotation_degrees() > 0) and (get_rotation_degrees() < 110):
set_rotation_degrees(110)
gun_rotation = get_rotation_degrees()
func _input(_event):
#weapon picking via state machine
if Input.is_action_just_pressed("cycle_weapon_backward"):
@warning_ignore("int_as_enum_without_cast")
state = wrapi(state - 1, States.SHOTGUN, States.LENGTH)
if Input.is_action_just_pressed("cycle_weapon_forward"):
@warning_ignore("int_as_enum_without_cast")
state = wrapi(state + 1, States.SHOTGUN, States.LENGTH)
func shoot():
if state == States.SPORE:
if can_shoot == true:
var sporebullet = sporebulletPath.instantiate()
get_tree().get_root().add_child(sporebullet)
sporebullet.position = $Marker2D.global_position
sporebullet.velocity = Vector2.RIGHT.rotated(deg_to_rad(rotation_degrees))
sporebullet.rotation = %Gun.rotation
shoot_delay.set_wait_time(0.1)
shoot_delay.start()
can_shoot = false
spore_shoot_sound.play()
gun_sprite.play("blast")
player.knockback = Vector2.LEFT.rotated(deg_to_rad(rotation_degrees)) * 100
if state == States.SHOTGUN:
if gun_available == true && can_shoot == true:
var shotgunbullet = shotgunbulletPath.instantiate()
get_tree().get_root().add_child(shotgunbullet)
shotgunbullet.position = $Marker2D.global_position
shotgunbullet.velocity = Vector2.RIGHT.rotated(deg_to_rad(rotation_degrees))
shotgunbullet.rotation = %Gun.rotation
shoot_delay.set_wait_time(0.7)
shoot_delay.start()
can_shoot = false
shotgun_shoot_sound.play()
gun_sprite.play("blast")
player.knockback = Vector2.LEFT.rotated(deg_to_rad(rotation_degrees)) * 600
if state == States.FIRE:
if gun_available == true && can_shoot == true:
var firebullet = firebulletPath.instantiate()
get_tree().get_root().add_child(firebullet)
firebullet.position = $Marker2D.global_position
firebullet.velocity = firebullet.velocity.rotated(deg_to_rad(rotation_degrees))
firebullet.rotation = %Gun.rotation
shoot_delay.set_wait_time(0.1)
shoot_delay.start()
can_shoot = false
spore_shoot_sound.play()
gun_sprite.play("blast")
player.knockback = Vector2.LEFT.rotated(deg_to_rad(rotation_degrees)) * 100
if state == States.ICE:
if gun_available == true && can_shoot == true:
var icebullet = icebulletPath.instantiate()
get_tree().get_root().add_child(icebullet)
icebullet.position = $Marker2D.global_position
icebullet.velocity = Vector2.RIGHT.rotated(deg_to_rad(rotation_degrees))
icebullet.rotation = %Gun.rotation
shoot_delay.set_wait_time(1)
shoot_delay.start()
can_shoot = false
spore_shoot_sound.play()
gun_sprite.play("blast")
player.knockback = Vector2.LEFT.rotated(deg_to_rad(rotation_degrees)) * 700
func _on_shoot_delay_timeout() -> void:
can_shoot = true
gun_sprite.play("rest")