Adding a delay when exiting a state

Godot Version

4.3.stable

Question

I have the below GDScript in the Characterbody2D

extends CharacterBody2D

const SPEED = 150.0
const JUMP_VELOCITY = -300.0
const COYOTE_TIME = 0.1 # Coyote time allows jump slightly after leaving a platform
var coyote_time_left = 0.0 # Timer to track how much coyote time remains
var collision_shapes_freed = false # Flag to track if collision shapes are freed
var is_crouching = false # Tracks if the player is crouching

@onready var animated_sprite: AnimatedSprite2D = $AnimatedSprite2D
@onready var collision_shape_standing: CollisionShape2D = $CollisionShapeStanding
@onready var collision_shape_crouching: CollisionShape2D = $CollisionShapeCrouching
@onready var raycast_up: RayCast2D = $RayCastUp # Add a RayCast2D pointing upwards

func _ready() → void:
raycast_up.enabled = true # Ensure the raycast is enabled

func _physics_process(delta: float) → void:
# Only interact with collision shapes if they haven’t been freed
if not collision_shapes_freed:
# Check if “move_down” action is pressed
if Input.is_action_pressed(“move_down”):
enter_crouch_state()

	# Check if the "move_down" action is released and there's no obstacle above
	elif is_crouching and not raycast_up.is_colliding():
		exit_crouch_state()

# Add gravity if not on the floor
if not is_on_floor():
	velocity += get_gravity() * delta
	coyote_time_left -= delta  # Countdown coyote time when not on the floor
else:
	coyote_time_left = COYOTE_TIME  # Reset coyote time when on the ground

# Handle movement and animations
var direction := Input.get_axis("move_left", "move_right")

if direction > 0:
	animated_sprite.flip_h = false
elif direction < 0:
	animated_sprite.flip_h = true

# Handle jump with coyote time
if Input.is_action_just_pressed("jump") and (is_on_floor() or coyote_time_left > 0):
	velocity.y = JUMP_VELOCITY
	coyote_time_left = 0.0  # Reset coyote time after jumping

# Play animations based on movement state
if is_on_floor():
	if is_crouching:
		animated_sprite.play("down")
		velocity.x = 0  # Stop horizontal movement while in crouch state
	elif direction == 0:
		animated_sprite.play("idle")
	else:
		animated_sprite.play("run")
else:
	animated_sprite.play("jump")

# Apply movement
if not is_crouching:  # Prevent moving while crouched
	if direction:
		velocity.x = direction * SPEED
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)

# Move the player
move_and_slide()

Function to enter the crouch state

func enter_crouch_state() → void:
collision_shape_standing.disabled = true
collision_shape_crouching.disabled = false
set_collision_mask_value(3, false) # Disable collision layer 3
is_crouching = true

Function to exit the crouch state (stand up)

func exit_crouch_state() → void:
collision_shape_standing.disabled = false
collision_shape_crouching.disabled = true
set_collision_mask_value(3, true) # Re-enable collision layer 3
is_crouching = false

A function to be called from the other node to free collision shapes

func free_collision_shapes():
if not collision_shapes_freed:
collision_shape_standing.queue_free()
collision_shape_crouching.queue_free()
collision_shapes_freed = true # Set the flag to true to stop further interaction with freed nodes

It does as I intend, except when exiting the ‘crouch’ due to raycasting no longer detecting collision as it causes the player to fall through the platform. This only happens with raycasting, not when ‘move_down’ is released successfully by the player.

I tried this:

extends CharacterBody2D

const SPEED = 150.0
const JUMP_VELOCITY = -300.0
const COYOTE_TIME = 0.1 # Coyote time allows jump slightly after leaving a platform
var coyote_time_left = 0.0 # Timer to track how much coyote time remains
var collision_shapes_freed = false # Flag to track if collision shapes are freed
var is_crouching = false # Tracks if the player is crouching

@onready var animated_sprite: AnimatedSprite2D = $AnimatedSprite2D
@onready var collision_shape_standing: CollisionShape2D = $CollisionShapeStanding
@onready var collision_shape_crouching: CollisionShape2D = $CollisionShapeCrouching
@onready var raycast_up: RayCast2D = $RayCastUp # Add a RayCast2D pointing upwards

var is_exiting_via_raycast = false # Tracks if we’re exiting crouch via raycast

func _ready() → void:
raycast_up.enabled = true # Ensure the raycast is enabled

func _physics_process(delta: float) → void:
# Only interact with collision shapes if they haven’t been freed
if not collision_shapes_freed:
# Check if “move_down” action is pressed
if Input.is_action_pressed(“move_down”):
enter_crouch_state()

	# Check if the "move_down" action is released and there's no obstacle above
	elif is_crouching and not raycast_up.is_colliding() and not is_exiting_via_raycast:
		exit_crouch_with_delay()

# Add gravity if not on the floor
if not is_on_floor():
	velocity += get_gravity() * delta
	coyote_time_left -= delta  # Countdown coyote time when not on the floor
else:
	coyote_time_left = COYOTE_TIME  # Reset coyote time when on the ground

# Handle movement and animations
var direction := Input.get_axis("move_left", "move_right")

if direction > 0:
	animated_sprite.flip_h = false
elif direction < 0:
	animated_sprite.flip_h = true

# Handle jump with coyote time
if Input.is_action_just_pressed("jump") and (is_on_floor() or coyote_time_left > 0):
	velocity.y = JUMP_VELOCITY
	coyote_time_left = 0.0  # Reset coyote time after jumping

# Play animations based on movement state
if is_on_floor():
	if is_crouching:
		animated_sprite.play("down")
		velocity.x = 0  # Stop horizontal movement while in crouch state
	elif direction == 0:
		animated_sprite.play("idle")
	else:
		animated_sprite.play("run")
else:
	animated_sprite.play("jump")

# Apply movement
if not is_crouching:  # Prevent moving while crouched
	if direction:
		velocity.x = direction * SPEED
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)

# Move the player
move_and_slide()

Function to enter the crouch state

func enter_crouch_state() → void:
collision_shape_standing.disabled = true
collision_shape_crouching.disabled = false
set_collision_mask_value(3, false) # Disable collision layer 3
is_crouching = true

Function to exit the crouch state (stand up)

func exit_crouch_state() → void:
collision_shape_standing.disabled = false
collision_shape_crouching.disabled = true
set_collision_mask_value(3, true) # Re-enable collision layer 3
is_crouching = false
is_exiting_via_raycast = false # Reset the raycast exit flag

Function to exit the crouch state after a delay via raycast

func exit_crouch_with_delay() → void:
is_exiting_via_raycast = true # Set flag to prevent multiple calls
await get_tree().create_timer(0.2).timeout # Add a 0.2 second delay (adjust as needed)
exit_crouch_state()

Optional: A function to be called from the other node to free collision shapes

func free_collision_shapes():
if not collision_shapes_freed:
collision_shape_standing.queue_free()
collision_shape_crouching.queue_free()
collision_shapes_freed = true # Set the flag to true to stop further interaction with freed nodes

Which adds a delay to the exit crouch and fixes the issue… only now there’s a delay after every time ‘move_down’ is exited, regardless of the reason. I’ve tried everything at my skill level, I really have, to make this work. Either I separate or add flags and the delay doesn’t even work or it continues to delay the exit all the time.

Some insight would be greatly appreciated

What is collision layer 3?

The documentation says collisionshape2d.disabled should be set trough object.set_deferred()

Maybe you can try this:


func enter_crouch_state() → void:
  collision_shape_crouching.call_deferred(set_disabled(false))
  collision_shape_standing.call_deferred(set_disabled(true))
  set_collision_mask_value(3, false) # Disable collision layer 3
  is_crouching = true

exitiing:

func exit_crouch_state() → void:
  collision_shape_crouching.call_deferred(set_disabled(true))
  collision_shape_standing.call_deferred(set_disabled(false))
  set_collision_mask_value(3, true) # Re-enable collision layer 3
  is_crouching = false

I reversed the disabled(true) and disabled(false) because there might be a frame where no shape is enabled, causing the player to fall.

Collision layer 3 contains one-way collision tilesets the player can pass through from above when ‘crouching’. So, they work as usual one-way collision tilesets until the player ‘crouches’ on one and they can fall through.

I’ll try that other fix in a bit, thank you so much

Your fix didn’t work; but I did hack together a fix here:

extends CharacterBody2D

const SPEED = 150.0
const JUMP_VELOCITY = -300.0
const COYOTE_TIME = 0.1  # Coyote time allows jump slightly after leaving a platform
var coyote_time_left = 0.0  # Timer to track how much coyote time remains
var collision_shapes_freed = false  # Flag to track if collision shapes are freed
var is_crouching = false  # Tracks if the player is crouching

@onready var animated_sprite: AnimatedSprite2D = $AnimatedSprite2D
@onready var collision_shape_standing: CollisionShape2D = $CollisionShapeStanding
@onready var collision_shape_crouching: CollisionShape2D = $CollisionShapeCrouching
@onready var raycast_up: RayCast2D = $RayCastUp  # RayCast2D pointing upwards

func _ready() -> void:
	raycast_up.enabled = true  # Ensure the raycast is enabled

func _physics_process(delta: float) -> void:
	# Only interact with collision shapes if they haven't been freed
	if not collision_shapes_freed:
		# Enter crouch state when "move_down" is pressed
		if Input.is_action_pressed("move_down"):
			enter_crouch_state()

		# Attempt to exit crouch state when "move_down" is just released
		elif Input.is_action_just_released("move_down") and is_crouching:
			if not raycast_up.is_colliding():
				exit_crouch_state()
			else:
				# Start monitoring for obstacle clearance
				check_and_exit_crouch_when_clear()
	
	# Add gravity if not on the floor
	if not is_on_floor():
		velocity += get_gravity() * delta
		coyote_time_left -= delta  # Countdown coyote time when not on the floor
	else:
		coyote_time_left = COYOTE_TIME  # Reset coyote time when on the ground

	# Handle movement and animations
	var direction := Input.get_axis("move_left", "move_right")

	if direction > 0:
		animated_sprite.flip_h = false
	elif direction < 0:
		animated_sprite.flip_h = true

	# Handle jump with coyote time
	if Input.is_action_just_pressed("jump") and (is_on_floor() or coyote_time_left > 0):
		velocity.y = JUMP_VELOCITY
		coyote_time_left = 0.0  # Reset coyote time after jumping

	# Play animations based on movement state
	if is_on_floor():
		if is_crouching:
			animated_sprite.play("down")
			velocity.x = 0  # Stop horizontal movement while in crouch state
		elif direction == 0:
			animated_sprite.play("idle")
		else:
			animated_sprite.play("run")
	else:
		animated_sprite.play("jump")

	# Apply movement
	if not is_crouching:  # Prevent moving while crouched
		if direction:
			velocity.x = direction * SPEED
		else:
			velocity.x = move_toward(velocity.x, 0, SPEED)
	else:
		velocity.x = 0  # Ensure player doesn't move horizontally while crouching

	# Move the player
	move_and_slide()

# Function to enter the crouch state
func enter_crouch_state() -> void:
	collision_shape_standing.disabled = true  # Disables standing collision shape
	collision_shape_crouching.disabled = false  # Enables crouching collision shape
	set_collision_mask_value(3, false)  # Disable collision layer 3
	is_crouching = true

# Function to check and exit crouch when the obstacle clears
func check_and_exit_crouch_when_clear() -> void:
	# Start a timer to check periodically
	monitor_obstacle_clearance()

# Coroutine to monitor obstacle clearance
func monitor_obstacle_clearance() -> void:
	while is_crouching and raycast_up.is_colliding():
		await get_tree().process_frame  # Wait for the next frame
	if is_crouching:
		exit_crouch_state_delay()

# Function to exit the crouch state after a 0.2-second delay
func exit_crouch_state_delay() -> void:
	await get_tree().create_timer(0.2).timeout  # Add a 0.2-second delay. The only thing stopping the player from clipping after raycast clears. Don't change.
	collision_shape_standing.disabled = false  # Enables standing collision shape. Don't reverse. Breaks func free_collision_shapes()
	collision_shape_crouching.disabled = true  # Disables crouching collision shape
	set_collision_mask_value(3, true)  # Re-enable collision layer 3
	is_crouching = false

# Function to exit the crouch state (stand up)
func exit_crouch_state() -> void:
	collision_shape_standing.disabled = false  # Enables standing collision shape
	collision_shape_crouching.disabled = true  # Disables crouching collision shape
	set_collision_mask_value(3, true)  # Re-enable collision layer 3
	is_crouching = false

# A function to be called from the other node to free collision shapes: specifically, killzone node.
func free_collision_shapes():
	if not collision_shapes_freed:
		collision_shape_standing.queue_free()
		collision_shape_crouching.queue_free()
		collision_shapes_freed = true  # Set the flag to true to stop further interaction with freed nodes
1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.