2d platformer: Enemy movement problems (hangs in air, etc.)

Godot Version

v4.2.2.stable.official [15073afe3]

Question

I cannot seem to get this enemy to move well. The intended behavior of the enemy is that it should rest until its raycasts detect the player, then it jumps at the player, then it rests, then it gets back up and walks around until its cooldown timer runs out, after which it is free to jump at the player again. Instead it hangs in air when it should fall to the ground (collision shape not colliding with anything as far as I can tell). This is pronounced when the enemy is walking or jumping off a ledge, or sometimes when the enemy has been attacked. There may be other problems with the movement, but until the thing lands on the ground like it should, they’ll be hard to diagnose. Thanks!

extends CharacterBody2D

#CONSTANTS
const LEFT = 1
const RIGHT = 0
const REST = 0
const WALK = 1
const ATTACK = 2
const DIE = 3

#VARIABLES
var speed = 50
var enemy_direction = LEFT
var facing_right = false
var direction = -1
var maxhp = 5
var currenthp = maxhp
var dead : bool = false
var knocked_back : bool = false
var movement_suspended : bool = false
var invincible : bool = false
var is_dying = false

var move_type = REST

var target = null
var cooldown = false
var new_bone_pos = null
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")

var flipped = false

#REFERENCE THINGS
@onready var game_manager = %GameManager
@onready var ray_cast_right = $RayCastRight
@onready var ray_cast_left = $RayCastLeft
@onready var animated_sprite_2d = $AnimatedSprite2D
@onready var player = $"../../Player"
@onready var knockback_timer = $EnemyHitbox/KnockbackTimer
@onready var enemy_hit_sound = $EnemyHitbox/EnemyHitSound
@onready var after_death_timer = $AfterDeathTimer
@onready var enemy_death_sound = $EnemyDeathSound
@onready var cooldown_timer = $CooldownTimer
@onready var attack_timer = $AttackTimer
@onready var enemy_hitbox = $EnemyHitbox
@onready var flip_timer = $FlipTimer

@export var bone_scene:PackedScene

#DEATH
func death(fell=false):
	#print("Panther died!")	
	dead = true	
	if fell:
		queue_free()
	else:
		move_type = DIE
		print("Play panther death animation.")
		after_death_timer.start()
	
func _on_after_death_timer_timeout():
	enemy_death_sound.play()
	visible = false
	queue_free()

#GETTING ATTACKED
func _on_enemy_hitbox_area_entered(area):
	if area.is_in_group("PlayerWeapon"):
		if invincible == false:
			currenthp = currenthp - 3 
			enemyknockback()
			print("Panther takes damage.  HP:" + str(currenthp))		

func enemyknockback():
	#print ("Panther knockback...")
	knocked_back = true
	invincible = true
	enemy_hit_sound.play()
	if knocked_back:
		move_type = ATTACK
		print ("enemy_direction is " + str(enemy_direction))
		print ("enenmy knockback - player direction: " + str(player.player_direction))
		velocity.y = -120
		velocity.x = -64
		if player.player_direction == player.RIGHT:
			velocity.x *= -1
	knockback_timer.start()
	
func _on_knockback_timer_timeout():
	print("Panther knockback ended.")
	knocked_back = false
	invincible = false
	visible = true
	if currenthp <= 0 and !dead:
		invincible = true
		print ("Check panther death.")
		death()
	
#ATTACKING
func attack_input():
	print ("Panther attacks...")
	move_type = ATTACK
	attack_timer.start()
	target = null
	velocity.y = -240
	velocity.x = 240
	if player.player_direction == player.RIGHT:
		velocity.x *= -1
	cooldown = true
	print ("Cooldown switched to:" + str(cooldown))
	cooldown_timer.start()
	
func _on_attack_timer_timeout():
	print("Attack timer end.")
	move_type = REST
	movement_suspended = true
	
func _on_cooldown_timer_timeout():
	print ("Cooldown timer end.")
	movement_suspended = false
	cooldown = false
	move_type = WALK	
	print("Cooldown switched to:" + str(cooldown))


#PROCESS & PHYSICS
func _physics_process(delta):
	# Add the gravity.
	if not is_on_floor():
		velocity.y += gravity * delta
	if movement_suspended == false:
		#if is_dying:
		#	return
	
		if !knocked_back and move_type == REST:
			pass
		else:	
			velocity.x = direction * speed
		
	 
			
		#handle attack	
		if is_on_floor() and !cooldown and !is_dying and move_type != ATTACK:
			if $RayCastAttackFront.is_colliding():
				target = $RayCastAttackFront.get_collider().get_parent()
			elif $RayCastAttackUp.is_colliding():
				target = $RayCastAttackUp.get_collider().get_parent()
			elif $RayCastAttackDown.is_colliding():
				target = $RayCastAttackDown.get_collider().get_parent()
			
		# Check for target not being null first
			if target and target.is_in_group("Player"):
				attack_input()
		
		#handle flip
		if is_on_floor():
			if ray_cast_left.is_colliding() or ray_cast_right.is_colliding(): 
				flip()
				#print ("Casting flip ray.  Direction: " + str(direction) + "Enemy direction:" + str(enemy_direction) + "Facing Right: " + str(facing_right) + "Movement Suspended: " + str(movement_suspended))
				
				
		if knocked_back and velocity.y < 10:
			velocity.y += 4
		
		if !knocked_back and is_dying:
			movement_suspended = true
		

		
		move_and_slide()
		
	
		
func flip():
	if !flipped:
		facing_right = !facing_right
		flipped = true
		flip_timer.start()
		scale.x = abs(scale.x) * -1
		if facing_right:
			enemy_direction = RIGHT
			direction = 1

		else:
			enemy_direction = LEFT
			direction = -1

func _on_flip_timer_timeout():
	flipped = false
	

				
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):		
	if currenthp <= 0:
		is_dying = true
	#if invincible == true:
	#	visible = !visible
	if move_type == WALK: #and !is_dying:
		animated_sprite_2d.play("Walk")
	elif move_type == ATTACK:
		animated_sprite_2d.play("Attack")
	elif move_type == REST:
		animated_sprite_2d.play("Rest")
	elif move_type == DIE:
		animated_sprite_2d.play("Death")
1 Like

Your code seems quite extensive. I suggest implementing a State Machine or using Behavior Trees for managing the enemy’s AI behavior.