Godot 101 - Part 6: Area-based Collisions

Tags: godot tutorial gamedev

This 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:

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:

Click to enlarge

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.

Full code for this part

Godot 101 - Part 7

Comments