I implemented a feature messily and I want to make it better

Godot Version

4.6

Question

Hello! I’m pretty new to Godot and currently I’ve been working on a recreation of the Tanks! mini-game from Wii Play just as a

way to try and familiarize myself with the engine. I was able to implement a feature where the player can drop a bomb underneath their tank, and after a few seconds it explodes. While this mechanic technically works, the way I implemented it is quite messy, there are three different timers to try and handle the bomb’s functionality and animations, and in the “_on_body_entered(body)” function I had to call body.queue_free because without it for some reason the explosion function doesn’t kill you if you drive over the bomb, but it does kill you if you wait for it to go off. For more context, I wanted the bomb to spawn under the tank, but once you drive out of it’s hitbox then it can kill you if you drive over it again. And the explosion collision has the killzone.gd script attached to it, which just frees the player upon contact. Any help would be greatly appreciated!

#Bomb.gd
extends Area2D

@onready var bomb_sprite: AnimatedSprite2D = $BombSprite
@onready var explosion_scene = load("res://scenes/bomb_explosion.tscn")

var within_bomb = true


func _on_body_entered(body):
	if within_bomb == false:
		explode()
		body.queue_free()
		
func _on_body_exited(body):
	within_bomb = false

func _on_countdown_timer_timeout():
	bomb_sprite.play("ticking")
	$ExplosionTimer.start()
	
func _on_explosion_timer_timeout():
	explode()
	
func explode():
	$BlastRadius/ExplosionColision.disabled = false
	$CountdownTimer.queue_free()
	$ExplosionTimer.queue_free()
	bomb_sprite.queue_free()
	$BlastRadius/DamageTimer.start()
	var explosion = explosion_scene.instantiate()
	add_child(explosion)
	explosion.position = $BombColision.position

func _on_damage_timer_timeout():
	$".".queue_free()

Love Tanks!
Seems good! What is with the BlastRadius though? Shouldn’t damage be handled by the explosion_scene?

Using queue_free() to destroy targets could be changed to a function to handle damage, and potentially a inherited class or component-style system. Changing this gives the actors who are to be destroyed more control of how they go out, including visual effects or tying into a scoring system.

# checking by type, possibly inherited
func _on_body_entered(body: Node2D) -> void:
    if body is Destructable: # requires inheritance through `class_name Destructable`
        body.take_damage()

# checking by component
func _on_body_entered(body: Node2D) -> void:
    if body.has_meta("health_component"):
        var health_component: HealthComponent = body.get_meta("health_component")
        health_component.take_damage()

Godot better-supports types and inheritance but component-style nodes can be used more freely

Thanks for the feedback! The explosion_anim scene actually doesn’t have any kind of collision to it, it’s just plays a unique animation that also has an AnimationPlayer attached to it which makes the explosion sprite rotate around. I definitely could give it it’s own hitbox though.

1 Like

Sounds good too, especially if the hitbox is instant anyways via _on_body_entered. Is there anything in specific about this script you found messy?

You probably do not need to delete the timers, especially if they are set to “one shot”

I didn’t like all of the timers, and the explode function seemed cluttered to me. I talk in past tense because I actually updated it to be a little sleeker haha

extends Area2D

@onready var collision_shape_2d: CollisionShape2D = $ExplosionArea/CollisionShape2D


func _process(_delta):
	if $Countdown.time_left < 3:
		$AnimatedSprite2D.play("ticking")
	if $Countdown.time_left < 0.5:
		$AnimatedSprite2D.hide()
		explode()

func _on_countdown_timeout():
	queue_free()
	
func explode():
	collision_shape_2d.set_deferred("disabled", false)

func _on_body_entered(_body):
	$Countdown.start(0.5)

Weirdly enough though now I’m having an issue where bullets aren’t triggering the explosion function even though they’re a CharacterBody2D. When the player runs into the mine it explodes no problem, so I’m not sure what the issue is. It might have something to do with my collision layers but I think they’re set up right. 1 is walls, 2 is the player/enemies, 3 is bullets, 4 is for the bullets hitbox, and 5 is for mines. When a shoot a bullet at the mine the bullet disappears but the mine doesn’t explode.

It probably is the layers/masks, what masks are enabled for the bullets and mine? Does the bullet delete itself when detecting an area?

Since timers are basically making a sequence, you can cram all that sequencing into a tween.

A few things.

  1. Always use @onready variables for node refernces. This prevents you from having to update multiple references in complex code later on. Especially annoying when you move or rename a node.
  2. Timers are not guaranteed to be accurate at less than half a second. So I’d recommend not using one for this since you’re right on the edge.
  3. You should use a variable to store the amount of countdown time to avoid magic numbers. Making it an @export variable allows you to play with the value in the Inspector and while the game is running (for testing).
  4. Alternately, if you have a Timer node, it’s appropriate to set the value in the node itself.

With Timer

extends Area2D

@export countdown_time: float = 0.5

@onready var collision_shape_2d: CollisionShape2D = $ExplosionArea/CollisionShape2D
@onready var countdown: Timer = $Countdown
@onready var animated_sprite_2d: AnimatedSprite2D = $AnimatedSprite2D


func _process(_delta):
	if countdown.time_left < 3:
		animated_sprite_2d.play("ticking")
	if countdown.time_left < 0.5:
		animated_sprite_2d.hide()
		explode()


func _on_countdown_timeout():
	queue_free()

	
func explode():
	collision_shape_2d.set_deferred("disabled", false)


func _on_body_entered(_body):
	countdown.start(countdown_time)

Without Timer

extends Area2D

@export countdown_time: float = 0.5

@onready var collision_shape_2d: CollisionShape2D = $ExplosionArea/CollisionShape2D
@onready var animated_sprite_2d: AnimatedSprite2D = $AnimatedSprite2D


func _ready() -> void:
	set_process(false)


func _process(delta):
	countdown -= delta
	if countdown < 3.0:
		animated_sprite_2d.play("ticking")
	if countdown < 0.5:
		animated_sprite_2d.hide()
		explode()
	if countdown <= 0.0:
		queue_free()


func explode():
	collision_shape_2d.set_deferred("disabled", false)


func _on_body_entered(_body):
	set_process(true)

Yea you were right, I didn’t set the mask collisions right for the mine, whoops