Godot 101 - Part 6: Area-based Collisions
Thu, Feb 23, 2017This is part 6 of “Godot 101”. In this installment, we’ll learn how to detect when two collision areas overlap, so we can make our player run around and pick up gems. If you haven’t already read through the previous parts, please start with Part 1.
About this series
Godot 101 is an introduction to the Godot game engine and how it works. If you’ve never used a game engine before, or if you’re just new to Godot, this is the place to start. If you’re new here, a quick note about this website: we’re called KidsCanCode because we teach programming and game development to kids, but if you’re an adult you’re welcome here, too. We don’t believe in dumbing-down the material for kids, and game development is hard - so it will probably be challenging no matter what your age.
You can watch a video version of this lesson here:
Gem Scene
First, let’s create a new scene for our gem objects. The tree will look like this:
-
gem
-
sprite
-
collision
-
We’ll use the gem image from the art folder, and a basic RectangleShape2D
for the collision. Resize it to cover the sprite texture, and remember: don’t scale the collision shape! Use the handles to adjust the extents of your rectangle - see the previous lesson if you need help.
Main Scene
Now we can start building our main scene. The root will just be a plain Node
named “main”. Click the “Instance” button: and choose the player scene.
We don’t want to have to add a bunch of gem instances manually, so we’re going to do it in code. We’ll be spawning lots of gems, and we will want to keep track of how many are left, so let’s make a node to hold them all. Add another Node
as a child of main
and call it gem_container
. Then add a script to the main
node with the following code:
extends Node
onready var gem = preload("res://gem.tscn")
onready var gem_container = get_node("gem_container")
var screensize
var score = 0
func _ready():
randomize()
screensize.get_viewport().get_rect().size
set_process(true)
spawn_gems(10)
func spawn_gems(num):
for i in range(num):
var g = gem.instance()
gem_container.add_child(g)
g.set_pos(Vector2(rand_range(40, screensize.width-40),
rand_range(40, screensize.height-40)))
In this script, we use preload
to load the gem scene into memory so we can use it to create instances. We also assign a variable to the gem_container
node so we can easily refer to it. Our spawn_gems
function just takes a number and creates that many gem instances, assigning them as children of gem_container
and setting pos
to a random location. Run the scene, and you should see this:
Area collisions - using signals
We want to have the gem disappear (be picked up) when the player touches it. Click on the gem
node and then click “Node” in the Inspector:
This is a list of all the signals that an Area2D
node can produce. The ones labeled area_*
are for detecting other area nodes, while the body*
ones are for detecting PhysicsBody2D
nodes. The one we want to use is area_enter
which will return the area that entered - in our case the player.
Click on the area_enter
signal and click the “Connect” button. You will then see the following dialog:
This lets us set what node we want the signal to be sent to. gem
is highlighted in red because it’s the current node. That’s fine, and at the bottom we’ll leave the “Method in Node” name as it is. Godot will now create that function for us. To start, let’s fill it out like this:
func _on_gem_area_enter(area):
print(area.get_name())
Now when you move the player over a gem, you will see “player” printed in the output window. However, if you run it a couple of times, you might see something like this as well:
@gem@4
@gem@7
What’s happening here? Well, this signal is generated when any area enters the gem’s area. This includes other gems! If we just deleted a gem when this signal happens, then two touching gems would delete each other, and we don’t want that. So we’re going to make sure and check the name of the entering area:
func _on_gem_area_enter(area):
if area.get_name() == 'player':
queue_free()
Spawning more gems
Back in our main
scene, we want to detect when the player has collected all the gems, so we can spawn more. We do that by looking at the gem_container
and counting its children:
var level = 1
func _process(delta):
if gem_container.get_child_count() == 0:
level += 1
spawn_gems(level * 10)
We add a variable called level
and whenever the gem_container
is empty, we increment it and spawn more gems. Collect away!
Wrapping Up
The next step will be to increment the player’s score when they collect a gem. However, there’s a catch: the code that detects collision and deletes the gem is running on the gem node, while the code that holds the score (and will display it) is on the main
node. So we need a way to have an instanced node communicate with another node that is farther up the tree. This is a common occurrence, but is often done incorrectly by beginners. In the next lesson, we’ll look at the wrong (but obvious) way to do this, and then show how to do it properly.