Godot 4: Detect a collision before adding node in a 2D game (part 1).
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:
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:
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
:
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:
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:
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:
Access the Project Settings and incorporate two additional 2D Physics layers: "Planets" and "SpawnBoundaries."
Assign the Planet node to Collision Layer 1 and Collision Mask 1.
Assign the SpawnBoundary node to Collision Layer 2 and Collision Mask 2.
Attach a new script to the Planet node and name it
Planet.gd
.Select the SpawnBoundary node, and incorporate the
area_entered(area: Area2D)
signal into the Planet script.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:
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.
Navigate to the Debug menu and activate "Visible Collision Shapes." Afterward, run the scene:
- 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:
- It's evident that the SpawnBoundary nodes are overlapping, thereby prohibiting the spawning of the Planet. To address this, let's enhance the
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)
- So this makes sense right? Before adding it to the scene, I will check the variable
planet_nearby
. In my case, this should betrue
, because thearea_entered
signal is triggered. So it should skip the if statement and don't add the planet to the scene. Let's run it:
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 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.
- 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
- 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:
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)
:
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!
Subscribe to my newsletter
Read articles from Joeri Damme directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by