Godot 4: Detect a collision before adding node in a 2D game (part 1).

Joeri DammeJoeri Damme
7 min read

Introduction

Over the past month, I've been immersing myself in the world of Godot 4, and it's truly an exceptional game engine. For quite some time, I've had an idea brewing for a 2D exploration game set in space. I'm currently engaged in procedurally generating a map using the TileMap node and the FastNoiseLite library.

Based on the noise at specific coordinates, I place a visible tile (which will eventually become invisible) with a value ranging from 0 to 7. When the value reaches 6 or higher, I aim to position a planet. However, here lies the challenge: I want to avoid spawning planets too closely together. Below is a screenshot of the tile map:

Procedural generated map with the FastNoiseLite library

As you can see, there are clusters of tiles and I don't want to generate on each tile with the number 6 a planet. So we need to detect if there is another planet close by. I decided to setup a test project and see how I can detect a planet near by.

Setting up the test project

I've initiated a new project in Godot 4. Within this project, I've crafted a Game scene and introduced a specialized Planet scene. The Planet scene is straightforward, featuring a CharacterBody2D node as the root, accompanied by two child nodes—a CollisionShape2D and a Sprite2D. The Sprite2D node showcases a circular pink image, symbolizing a planet:

CharacterBody2D node basic setup with a Sprite2D and CollisionShape2D

In the Game scene, I incorporated a Node2D as the root node. I then connected the Planet node once and adjusted its position within the viewport to x: 550, y: 300:

The Planet scene inside the new Game scene

I've attached a script to the Game node. In the _ready() function, I aim to simulate the addition of a planet on top of another. I prefer doing this programmatically, as it mirrors the functionality within my game. The script now includes the following code to ensure the planets overlap each other:

extends Node2D

var planet: PackedScene = preload("res://scenes/Planet.tscn")

func _ready() -> void:
    # Programmatically add planet
    var second_planet = planet.instantiate()
    second_planet.position = Vector2(600, 300)
    add_child(second_planet)

And the result will be 2 overlapping planets:

Example of two Planet scenes colliding

The next objective is to implement a check before adding the Planet to the Game scene. I aim to determine whether it is permissible to spawn a planet within a certain radius from another planet.

The not so good solution 1: Area2D collision detection

The initial concept that crossed my mind involves incorporating an Area2D with a CollisionShape2D to the planet. By adding a signal when a different Area2D is in collision, we can trigger a function that sets a boolean. Prior to introducing the Planet to the Game scene, we can evaluate this boolean to determine whether the action is permissible:

  1. Open the Planet scene and include an Area2D and CollisionShape2D. Rename the Area2D node to "SpawnBoundary." Configure the CollisionShape2D as a CircleShape and set the radius to 150px:

    Adding the spawn boundary around the planet

  2. Access the Project Settings and incorporate two additional 2D Physics layers: "Planets" and "SpawnBoundaries."

    Setting the 2D Physics layers

  3. Assign the Planet node to Collision Layer 1 and Collision Mask 1.

    Setting the collision layers and masks for the Planet scene

  4. Assign the SpawnBoundary node to Collision Layer 2 and Collision Mask 2.

    Setting the collision layer and mask for the SpawnBoundary node

  5. Attach a new script to the Planet node and name it Planet.gd .

  6. Select the SpawnBoundary node, and incorporate the area_entered(area: Area2D) signal into the Planet script.

  7. Introduce a boolean at the top of the script, such as var planet_nearby: bool = false, and set it to true within the signal function. Additionally, include a print statement for debugging. Here's an example:

extends CharacterBody2D

var planet_nearby: bool = false

func _on_spawn_boundary_area_entered(area):
    print('Planet nearby detected')
    planet_nearby = true

The initial phase is complete. Now, let's return to the Game scene and put our solution to the test:

  1. In the Game scene, adjust the position of the Planet node (the one that is already linked in the scene) slightly to the left to prevent too much overlap with the other planet. I recommend moving it to x: 350px and y: 300px. After making this adjustment, save the scene.

  2. Navigate to the Debug menu and activate "Visible Collision Shapes." Afterward, run the scene:

    Colliding spawnboundaries example

    1. It's evident that the SpawnBoundary nodes are overlapping, thereby prohibiting the spawning of the Planet. To address this, let's enhance the Game.gd script with the following updates:
    extends Node2D

    var planet: PackedScene = preload("res://scenes/Planet.tscn")

    func _ready() -> void:
        # Programmatically add planet
        var second_planet = planet.instantiate() as CharacterBody2D
        second_planet.position = Vector2(600, 300)

        # Checking if planet is nearby before adding to scene
        if !second_planet.planet_nearby:
            print('No planet nearby')
            add_child(second_planet)
  1. So this makes sense right? Before adding it to the scene, I will check the variable planet_nearby. In my case, this should be true, because the area_entered signal is triggered. So it should skip the if statement and don't add the planet to the scene. Let's run it:

Output messages are in wrong order

Okay, that's not what I expected. As you can see in the output below, the 'No planet nearby' message appeared before the print statement in the _on_spawn_boundary_area_entered function in the Planet script. But why?

The reason is simple: Since the Planet is instantiated and not in the scene yet, we cannot check if the collision has happened yet. So the only option now is to add the planet to the scene, but before that, make it invisible. If a planet is not detected nearby, just make it visible:

extends Node2D

var planet: PackedScene = preload("res://scenes/Planet.tscn")

func _ready() -> void:
    # Programmatically add planet
    var second_planet = planet.instantiate() as CharacterBody2D
    second_planet.position = Vector2(600, 300)

    # Make the planet invisible and add to the tree
    second_planet.visible = 0
    add_child(second_planet)

    # Checking if planet is nearby before adding to scene
    if !second_planet.planet_nearby:
        print('No planet nearby')
        second_planet.visible = 1
    else:
        # Delete the planet if not needs to be rendered
        second_planet.queue_free()

Now let's run it again:

Still no success

Still not working. The debug messages are still in the wrong order. After some research, there is also a different problem: The physics engine must render at least one frame before any collision can be detected. So, how can we wait for one frame before checking the planet_nearby variable? We can do that with a signal and coroutines.

  1. Let's open the Planet.gd script again, and make sure that we add a signal wait_first_frame and a boolean variable at the top of the script. Also, we want to check when the _process() function has rendered the first frame. If that is done, we emit the new signal and set the boolean to true:
extends CharacterBody2D

var planet_nearby: bool = false

signal wait_first_frame

var first_frame_rendered: bool = false

func _on_spawn_boundary_area_entered(area):
    print('Planet nearby detected')
    planet_nearby = true

func _process(delta):
    if !first_frame_rendered:
        print('First frame rendered...')
        wait_first_frame.emit()
        first_frame_rendered = true
  1. In the Game.gd script, we gonna wait for the signal until is has emitted:
extends Node2D

var planet: PackedScene = preload("res://scenes/Planet.tscn")

func _ready() -> void:
    # Programmatically add planet
    var second_planet = planet.instantiate() as CharacterBody2D
    second_planet.position = Vector2(600, 300)

    # Make the planet invisible and add to the tree
    second_planet.visible = 0
    add_child(second_planet)

    # Now wait for the first frame to be rendered
    await second_planet.wait_first_frame

    # Checking if planet is nearby before adding to scene
    if !second_planet.planet_nearby:
        print('No planet nearby')
        second_planet.visible = 1
    else:
        # Delete the planet if not needs to be rendered
        second_planet.queue_free()

Now let's run the game again:

Planet not generated because of the await and emit functionality

That's looking good! Let's move the instantiated planet a 100 pixels to the right, by changing the line second_planet.position = Vector2(600, 300) to second_planet.position = Vector2(700, 300) :

Second planet generated because no collision

This seems to work! But do I like it? Not really. I'm adding a node, make it invisible, adding it to the scene, we need to hook into the process() function of the planet, adding a signal...it seems hard to maintain. Is there a different solution? Let's find out in part 2!

0
Subscribe to my newsletter

Read articles from Joeri Damme directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Joeri Damme
Joeri Damme