Asteroids Clone: instantiating smaller asteroids on destruction

Good morning, afternoon or evening. I’m working on an Asteroids clone for the sake of practice and learning, trying to actually start the “just make games” phase.

Recently finished all the player systems, with the most recent being collision damage and respawning, then moved on to destroying asteroids. While destroying them is easy, I can’t figure out the whole logic and algorithm behind an asteroid instantiating another two on being destroyed.

I’ve been using composition (or, at least, a bare-bones, 100% inefficient version of it), I suppose, and it has been working so far, but now it feels like my brain just went jelly for the whole week and even though it sounds simple in practice, I just can’t get my head to work and solve this at all. There are three asteroid scenes, “asteroid_big”, “asteroid_medium” and “asteroid_small”. They are all CharacterBody2D and share the same script that makes it move forward.

extends CharacterBody2D # asteroid.gd

@export var SPEED : float = 200;

func _process(_delta: float) -> void:
  velocity += transform.x * SPEED;
  if velocity.length() >= SPEED:
    velocity = velocity.limit_length(SPEED);
    move_and_slide();

An Area2D called “component_damage” detects collisions, handles health and emits a signal for either collision or health depletion.

extends Area2D #component_damage.gd

@export var parent = Node2D;
@export var health : int;

signal has_collided
signal health_depleted

  func contact_damage(_area: Area2D) -> void:
    #print(str(parent.name) + " has collided with " + str(_area.name));
    has_collided.emit();
    deplete_health();

func deplete_health():
  health -= 1;
  #print(str(parent.name) + "'s HEALTH has been reduced to " +  str(get_health_value()));
  health_depleted.emit();
  if health <= 0:
    health = 0;
    #print(str(parent.name) + "'s HEALTH is zero and cannot be depleted any   further.");

func get_health_value():
  return health;

I’ve had a handful of failed approaches that, at this point, makes me believe I might have to completely start over with the implementation of asteroids in general, and just the thought of having to do that deeply demotivates me and makes it harder and harder to even feel like giving another shot.

The closest I got to achieving even the most basic functionality of this feature was by loosely following a tutorial, which led to me trying a component_asteroid another time, setting an enum that stores different asteroid sizes and using a match statement to set the “next_asteroid” according to the asteroid’s size.

Since this was a component, since I knew I couldn’t reference/preload the path of the asteroid’s scene within its own script, I’d have to add a health variable to the asteroid script and set it to the same value as the health variable under component_damage every frame. With that, I managed to detect damage and instantiate the proper “next_asteroid”, but I had no idea how to proceed with either freeing the asteroid from the queue while maintaining the instantiated asteroids in the scene. With how different my implementation is compared to the tutorial, just following it would make my project even more confusing.

I apologize for this wall of text of a post, but I’m at a total loss here. This is the first time I actually managed to get anything beyond a somewhat functional character movement and I really want to finish this project before attempting a game of my own.

Here’s the remote repository of the project, in case the information I provided within the post isn’t enough. Please point me toward the right direction! I would prefer not to be given code, but receive detailed suggestions to how I could approach this feature. Once again, sorry for the wall of text and thank you in advance!

1 Like

I think you have the right idea as far as approach: when an asteroid is destroyed, call a function that spawns additional, smaller asteroids. Theres a number of ways to approach this so don’t take the following as being the best. Hopefully I can provide a perspective that helps you find the best solution for your code.

The idea is:

func _on_asteroid_explode(asteroid: AsteroidNode):
    if asteroid.size > asteroid.min_size:
        asteroid.spawn_more(4)
    asteroid.queue_free()

That sums up what we want to happen. The details really are up to you here, but what this tells us is we need to know:

  1. How small can the asteriod get?
  2. How many smaller asteroids should spawn (this can even be from a random range)?
  3. When should it not spawn additional asteroids?

I’m assuming the definition of AsteroidNode includes behaviors that handle things like velocity, speed, gravity – which can help you easily calculate the trajectory and force with which they move when spawned.

I hope this makes sense, but I’d be interested know how your final solution.

edit: clarity

1 Like

A couple of different ways. Let’s say you have a scene for each level in the game, where all the action takes place, we’ll call it level_one.tscn

What do you need to actually do in that scene, for it to actually be a game? Well, you’d first need a player. You create your player.tscn and add it to the level. This player scene can be however simple or complex you want to make it. It’s most likely where you will take care of movement, animations, stats and abilities, etc. When implementing those, you might choose to have multiple other scenes (components that you will be able to reuse in other parts of the game), or just add everything in the player.tscn

You then want to have some enemies (or obstacles), in this case asteroids. We start simple, we know we’ll have multiple instances, so we will most like need a “manager” for them (spawner). So we create a new scene called asteroid_spawner.tscn (we could just add everything inside the level scene, but we will most likely want to reuse this spawner in other levels, so we plan ahead and make it its own scene).

So far our level_one.tscn looks like this:

  • LevelOne
  • Player
  • AsteroidSpawner

Will we need other spawners in the future? Should we create a base Spawner class? Maybe, maybe not. In general it’s good to just do what you have in mind now, and refactor later. So we will continue working on this one AsteroidSpawner.
What will it need to do? Well, spawn asteroid.tscn, which we will create right now:

What do we want here? Well, three kinds of asteroids, that break into smaller ones when destroyed. Do we need to create three different scenes? We could. We could also create an enum that tracks which size this asteroid is. When we instantiate the asteroid, we can have the Spawner decide its properties (this part will really benefit from some code):

# spawner.gd

func create_asteroid() -> Asteroid:
    var asteroid: Asteroid = Asteroid.new()
    var asteroid_type: Asteroid.Type = Asteroid.Type.keys().pick_random()
    asteroid.set_type(asteroid_type)

Other logic should be handled here in the spawner, such as checking and adjusting the spawn rate based on difficulty, or if there are too many or too few of one kind of asteroid, etc.

Everything else is probably handled in the Asteroid class. There we will need the enum, a function like the set_type(Asteroid.Type) with which we will create that certain asteroid by adding the texture based on the size, adding the CollisionShape2D based on the size, etc.

Likewise, the asteroid knows when it dies or when it goes off screen (it knows where it is at all times), and as such it should handle those events and queue_free() itself. It can also signal so the Spawner knows in case it needs to.

Breaking into smaller asteroids on death can be handled either in the Asteroid or in the Spawner.

EDIT: Below is the initial idea, which is obviously wrong now that I’ve actually read my own post. When writing a lot, my brain goes into hibernation and the first thing that comes to mind gets the spotlight.

One way you can do it, is by creating the smaller asteroids when we create the big one. In the above example, that would be in the set_type() function.
The Asteroid class would have a property that looks something like this:

var smaller_instances: Array[Asteroid]

The function would load and fill this array with however many smaller asteroids are needed, if the one currently being created is not already the smallest possible size.
This might or might not be ideal depending on how many nodes you want to have loaded at once, but the rule of thumb is you worry about performance when you need to.

When the asteroid dies, it can signal to the Spawner the array of smaller instances, which the Spawner will take care to add in the level, probably in a on_asteroid_death() function.

… or something like that, idk.

EDIT: It’s obviously bad to load and instantiate multiple nodes that you might never need, especially when they grow exponentially. Load them as needed or make a pool and reuse them.

I did this in a game not too long ago. If I recall correctly, I too had three scenes for my asteroids that shared an asteroid class so only the size specific stuff was kept in the individual scenes. (Each scene, like small_asteroid, had several options for the texture used so there was a variety of asteroid types at each size).

When an asteroid was destroyed it emitted a signal that said where it was, it’s velocity and how big it was. The asteroid manager listened for the signal and spawned for a large asteroid dying, two or three medium asteroids in its place, for a medium asteroid dying, two or three small asteroids in its place, and for a small asteroid dying, nothing at all.

I also inherited the parent velocity adding some deviation. So if a large asteroid was moving, say, straight up, its child medium asteroids continued moving up but one with some left component and one with some right component added and some random modifiers to keep it looking interesting.

I am sure there are many ways to do this but doing it in the asteroid spawner seemed the most simple to me.

3 Likes