Need help fixing my boss script in GDscript

Hey everyone, sorry, first time being in this forum so im not sure if im doing it right,

I’ve been following multiple class on godot and familiarised myself with gdscript, so when finished the Build a 2D Action-Adventure Game, i felt comfortable enough to expand it a bit and create a boss enemy.

Buuuuuut, i think i overestimate my skills, because i’ve been stuck for the past few days trying so many things, i feel like im lost in the sauce. So… Help please?

The goal, is to have a big boss that has two attack one on the right, one on the left, when the player enters the attack range, the boss should “charge” the right or left area depending on where the character is, so that the player can try to quickly leave that area before the boss attacks.

However, right now, i have two big problems, if the player enters both the right and left attack range, it attacks twice (which is unfortunate - i also wanted to randomised that with randf() but it didnt seem to work so ive deleted that) and also, it only attacks once the player enters the zone, and not everytime the player is within that zone.

Ive tried so many things, and right now i think the attack don’t even work anymore, so please, what am i doing wrong? (im gonna put the code that doesnt seem to work and exclude what does work so its easier on the eyes)

thanks in advance cause im getting crazy :sob:

extends CharacterBody2D

var target: Node2D
@export var speed: int = 30
@export var acceleration: float = 5
@export var hp: int = 30


@onready var damage_sfx: AudioStreamPlayer2D = $DamageSFX
@onready var animated_sprite_2d: AnimatedSprite2D = $AnimatedSprite2D
@onready var left_attack_range_area_2d: Area2D = $LeftAttackRangeArea2D
@onready var right_attack_range_area_2d: Area2D = $RightAttackRangeArea2D
@onready var attack_cool_down: Timer = $AttackCoolDown
@onready var sword_slash: AudioStreamPlayer2D = $sword_slash

var can_attack: bool = true
var is_in_attack_range: bool = false
var is_attacking: bool = false
var is_charging: bool = false


func _physics_process(delta: float) -> void:
	if hp <= 0:
		return

	if not is_in_attack_range:
		move_and_slide()
		chase_target()
	else:
		velocity = Vector2.ZERO

	if is_attacking == false and is_charging == false:
		animate_enemy()


func _on_attack_range_area_2d_body_entered(body: Node2D) -> void:
	if body is Player:
		is_in_attack_range = true
		print("Player entered attack range")
		charge_attack(body)


func _on_attack_range_area_2d_body_exited(body: Node2D) -> void:
	if body is Player:
		is_in_attack_range = false
		print("Player left attack range")

func charge_attack(body: Node2D) -> void:
	if is_attacking or can_attack == false:
		return
		print("can't attack")

	print("charge")
	is_charging = true
	var in_left_range = left_attack_range_area_2d.get_overlapping_bodies().has(body)
	var in_right_range = right_attack_range_area_2d.get_overlapping_bodies().has(body)
	
	if in_left_range and in_right_range:
		print("is in both range")
		animated_sprite_2d.play("charge_left")
		await animated_sprite_2d.animation_finished
		attack_left_player(body)
	elif in_left_range:
		print("is in left range")
		animated_sprite_2d.play("charge_left")
		await animated_sprite_2d.animation_finished
		attack_left_player(body)
	elif in_right_range:
		print("is in right range")
		animated_sprite_2d.play("charge_right")
		await animated_sprite_2d.animation_finished
		attack_right_player(body)
		
	is_charging = false

func start_attack(direction: String, body: Node2D) -> void:
	if direction == "left":
		animated_sprite_2d.play("charge_left")
		await animated_sprite_2d.animation_finished
		attack_left_player(body)
	elif direction == "right":
		animated_sprite_2d.play("charge_right")
		await animated_sprite_2d.animation_finished
		attack_left_player(body)

func attack_right_player(body: Node2D) -> void:
	attack_cool_down.start()
	is_attacking = true
	print("Right Attack!")
	can_attack = false
	sword_slash.play()
	animated_sprite_2d.play("right_attack")
	body.take_damage()
	await animated_sprite_2d.animation_finished
	is_attacking = false


func attack_left_player(body: Node2D) -> void:
	attack_cool_down.start()
	is_attacking = true
	print("Left Attack!")
	can_attack = false
	animated_sprite_2d.play("left_attack")
	sword_slash.play()
	body.take_damage()
	await animated_sprite_2d.animation_finished
	is_attacking = false


func _on_attack_cool_down_timeout() -> void:
	print("cooldown is over")
	can_attack = true

Cool idea =)

Ok, I see two problems at the moment, and there may be more:

  • In attack_left_player() and attack_right_player(), it looks like you never actually call start_attack() anywhere, which is probably why your attacks don’t work at all anymore. This may not be the case, since you mentioned you omitted some code from this post, but you can verify by placing a print statement somewhere in start_attack().
  • get_overlapping_bodies() doesn’t work reliably, if at all. This is a known engine bug, and there isn’t a clean fix for it. You’re better off using the body_entered and body_exited signals of the left and right areas respectively and using these to toggle in_left_range and in_right_range as each callback fires. To facilitate that, I’d move these variable declarations outside of charge_attack(). The added benefit of this approach is that if an attack completes and the Player hasn’t exited the area, you can conclude that the Player is still there and just repeat the attack again and again (it’s a boss; the user should be mindful enough not to get cornered!).

Additionally, you may want to disable the charge attack entirely for now (with return in the first line of the function), since only a tiny portion of the charge area is actually outside of the left and right areas. Either way, you might as well make sure each attack works properly individually first, and then it’ll be easier to see if they’re actively conflicting with each other.

Ahhh i see Thanks!
I followed what you said, and went into an earlier code + i changed the code to this

extends CharacterBody2D

var target: Node2D
@export var speed: int = 30
@export var acceleration: float = 5
@export var hp: int = 30


@onready var damage_sfx: AudioStreamPlayer2D = $DamageSFX
@onready var animated_sprite_2d: AnimatedSprite2D = $AnimatedSprite2D
@onready var left_attack_range_area_2d: Area2D = $LeftAttackRangeArea2D
@onready var right_attack_range_area_2d: Area2D = $RightAttackRangeArea2D
@onready var attack_cool_down: Timer = $AttackCoolDown
@onready var sword_slash: AudioStreamPlayer2D = $sword_slash

var can_attack: bool = true
var is_in_attack_range: bool = false
var is_attacking: bool = false
var is_charging: bool = false
var in_left_range: bool = false
var in_right_range: bool = false

func _physics_process(delta: float) -> void:
	if hp <= 0:
		return

	if is_in_attack_range == true or is_charging == true or is_attacking == true:
		velocity = Vector2.ZERO
	else:
		move_and_slide()
		chase_target()


	if is_attacking == false and is_charging == false:
		animate_enemy()



func charge_attack(body: Node2D) -> void:
	if is_attacking or can_attack == false:
		return
	
	is_charging = true
	if in_left_range == true and in_right_range == true:
		return
	elif in_left_range == true:
		animated_sprite_2d.play("charge_left")
		await animated_sprite_2d.animation_finished
		attack_left_player(body)
	elif in_right_range == true:
		animated_sprite_2d.play("charge_right")
		await animated_sprite_2d.animation_finished
		attack_right_player(body)
		
	is_charging = false


func attack_right_player(body: Node2D) -> void:
	in_right_range = true
	charge_attack(body)
	attack_cool_down.start()
	is_attacking = true
	can_attack = false
	sword_slash.play()
	animated_sprite_2d.play("right_attack")
	if is_in_attack_range == true:
		body.take_damage()
	await animated_sprite_2d.animation_finished
	is_attacking = false


func attack_left_player(body: Node2D) -> void:
	attack_cool_down.start()
	is_attacking = true
	can_attack = false
	animated_sprite_2d.play("left_attack")
	sword_slash.play()
	if is_in_attack_range == true:
		body.take_damage()
	await animated_sprite_2d.animation_finished
	is_attacking = false


func _on_attack_cool_down_timeout() -> void:
	print("cooldown is over")
	can_attack = true


func _on_player_detection_area_2d_body_shape_entered(body_rid: RID, body: Node2D, body_shape_index: int, local_shape_index: int) -> void:
	if body is Player:
		target = body


func chase_target():
	if target and is_in_attack_range == false:
		var direction_to_player: Vector2 = (target.global_position - global_position).normalized()
		velocity = velocity.move_toward(direction_to_player * speed, acceleration)


func animate_enemy():
	if velocity.length() > 0.707:
		animated_sprite_2d.play("move")
	else:
		animated_sprite_2d.play("Idle")

func take_damage():
	damage_sfx.play()

	if is_instance_valid(self):
		var flash_red_color: Color = Color(20, 0, 0)
		var original_color: Color = Color(1, 1, 1)

		modulate = flash_red_color
		await get_tree().create_timer(0.1).timeout
		if is_instance_valid(self):
			modulate = original_color

	if is_instance_valid(self):
		hp -= 5
		if hp <= 0:
			if is_instance_valid(self):
				die()

func die():
	$GPUParticles2D.emitting = true
	animated_sprite_2d.visible = false
	await get_tree().create_timer(1).timeout
	queue_free()


func _on_right_attack_range_area_2d_body_exited(body: Node2D) -> void:
	if body is Player:
		in_right_range = false



func _on_left_attack_range_area_2d_body_exited(body: Node2D) -> void:
	if body is Player:
		in_left_range = false



func _on_left_attack_range_area_2d_body_entered(body: Node2D) -> void:
	if body is Player:
		in_left_range = true
		charge_attack(body)


func _on_right_attack_range_area_2d_body_entered(body: Node2D) -> void:
	if body is Player:
		in_right_range = true
		charge_attack(body)


func _on_is_in_range_body_entered(body: Node2D) -> void:
	if body is Player:
		print("is in range")
		is_in_attack_range = true


func _on_is_in_range_body_exited(body: Node2D) -> void:
	if body is Player:
		print("left the range")
		is_in_attack_range = false

essentially i rechanged the left and right area so that they dont trigger the attack instantly. It mostly works right now, the whole code, although theres a few bugs here and there i cant quite figure out (like the enemy will stay stuck for a bit for some reason) but these bugs dont trigger all the time, so thats a problem for future me lol

I still have the problem where the attack only triggers if the player enters the area and not, if its within this area, i wanted to add it in the process function, an if statement with within_range but can’t quite put the charge_attack(body) function in there since the body is not declared in the current scope. any idea?

That’s fantastic!

If I’m understanding correctly, the attack only happens once, when the Player first enters an area, and not continuously while the Player remains in that area, right?

If that’s the case, this is because you’re only calling charge_attack() a single time, in the left and right body_entered callbacks. What you would want to do is call it once, as you’re doing, and then start a timer that calls it again and again whenever its timeout() fires. In your left and right body_exited callbacks, disable this timer again.

What happens if Left is entered, Right is entered, and Left is exited? With the solution above, the boss will stop attacking, so you need an integer to keep track of how many areas the Player is currently in. That should be no problem for you, because exactly the same approach was used for the puzzle buttons in the course =)

If that’s the case, this is because you’re only calling charge_attack() a single time, in the left and right body_entered callbacks. What you would want to do is call it once, as you’re doing, and then start a timer that calls it again and again whenever its timeout() fires. In your left and right body_exited callbacks, disable this timer again.

oh, i didnt thought about timers, thats smart, thanks!

Although? wouldnt that pose the same problem? with charge_attack needing an argument i dont have in that function?

Cause i tried putting a variable at the beginning with like; target_body: Node2D = null, and having the entered signal do the body = target_body,
but then while it does loop, it breaks at the player receives damage part. cause the body becomes null again :thinking: any cleaner way to do this or, am i missing something?

What happens if Left is entered, Right is entered, and Left is exited? With the solution above, the boss will stop attacking, so you need an integer to keep track of how many areas the Player is currently in. That should be no problem for you, because exactly the same approach was used for the puzzle buttons in the course =)

I hear you and i think thats quite a good logic but what i dont get is, why, my current code, wouldnt work? since i have 3 booleans, with “is in range” as a whole and is in “right range” or “left range”
Soooo, if i enter in the middle let’s say; it would become

is_in_range = true
is_in_right = true
is_in_left = true

but if i leave left,
wouldnt that just become ??

is_in_range = true
is_in_right = true
is_in_left = false

Im sorry i feel stupid but i dont understand why it wouldnt work?
especially since my charge is this

	if in_left_range == true and in_right_range == true:
		return
	elif in_left_range == true and in_right_range == false:
		animated_sprite_2d.play("charge_left")
		await animated_sprite_2d.animation_finished
		attack_left_player(body)
	elif in_right_range == true and in_left_range == false:
		animated_sprite_2d.play("charge_right")
		await animated_sprite_2d.animation_finished
		attack_right_player(body)
	else:
		return

Sorry, maybe its because ive been on this code for a while and nothing makes sense anymore :sob:

Operating on body directly is best left to code within the callback, and that’s because the value of body is ephemeral, as you discovered. When going outside the callback (timeout() in this case), it’s better to pass body to some other variable so that you can maintain a reference to it. Then, instead of calling body.take_damage() (or whatever you’ve called the relevant function), you can directly call target_player.take_damage(). That reference will survive until you overwrite it at the next body_entered signal.

I’d have to have a closer look to determine that, but the variable names are a little confusing since you also have the circular attack range area. If you’ve been staring at it this long, that might be a source of false beliefs that keeps you from seeing the real problem.

Also, print statements + divide-and-conquer testing are your friends =)

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