Making an Endless Runner in Godot, Part 2 — Collisions & Controls

Matt Lim
8 min readApr 25, 2020

Welcome back! This is part of an ongoing series of tutorials where I write about how to make an entire endless runner game in Godot. If you haven’t read the first part, you can check it out here. If you want to download the game, head over here.

We previously created a scrolling background for our game. In this tutorial, we’re going to add the penguin. We’ll make him flap, add some basic controls, and add collisions to keep him within the game’s boundaries. Let’s get started!

The source code for this tutorial can be found here.

Hey! I’m making a YouTube video to accompany each of these posts. Check them out if you’d rather watch than read.

Flapping and User Input

It’s time for the star of the show to make his appearance! In this section we’ll add the penguin; we’ll make it so that the user can make him flap and he collides with stuff.

First, we need to make a new scene for our penguin. Here’s what the node tree should eventually look like:

Penguin node tree, named for convenient copying.

Let’s walk through the node tree.

  • RigidBody2D - The documentation on this is pretty good: “This node implements simulated 2D physics. You do not control a RigidBody2D directly. Instead you apply forces to it (gravity, impulses, etc.) and the physics simulation calculates the resulting movement based on its mass, friction, and other physical properties.” Here’s a simplified version: if you want something to collide with other stuff, and you want it to move based on gravity or some other external force (as opposed to code, for example), use one of these things. In our case, we want the penguin to collide with boundaries and obstacles, and we want it to be affected by gravity, so using this makes perfect sense.
  • AnimationPlayer - We’ll use this to animate our penguin, specifically to create a flapping animation.
  • Sprite - We’ll use this to render our penguin textures.
  • CollisionPolygon2D1 - We’ll use this to create a collision shape that conforms to our penguin texture. Without this, our penguin won’t collide with anything. A RigidBody2D should usually be combined with some kind of collision shape (Godot will warn you if it’s not).

First, we’ll add the flapping animation. To start, create a RigidBody2D as the root node of a new scene. Then, add a Sprite and an AnimationPlayer as children. Drag Penguin.png into the Texture field of the Sprite, and set Hframes = 3. Note that Penguin.png is a sprite sheet; we’re going to be animating the Frame field of the Sprite to create the flapping animation. Now, let’s go to our AnimationPlayer and create a new animation called “Flap”. We’ll add a Property Track that animates the Frame field of the Sprite node. Since we have an AnimationPlayer in the scene, we can see these little keys next to fields in the Inspector. We can use these keys to add frames to our animation. Our “Flap” animation will have two frames, one at 0 seconds and one at 0.4 seconds. The first frame will have Frame = 1 and the second frame will have Frame = 0.

We’re animating the “Frame” field.
The “Flap” animation should look like this.

Now to test out our animation, we’re going to attach a script to the root node of our penguin scene called Penguin.gd. The script will contain this code:

extends RigidBody2Dconst UP_IMPULSE: float = -55.0func _ready() -> void:
pass

func _input(event: InputEvent) -> void:
if event is InputEventKey:
if event.is_action_pressed("ui_select"):
_penguin_jump()

func _penguin_jump() -> void:
set_linear_velocity(Vector2(0, 0))
apply_central_impulse(Vector2(0, UP_IMPULSE))
$FlapAnimationPlayer.stop()
$FlapAnimationPlayer.play("Flap", -1, 1)

This code makes it so that when spacebar is pressed, the penguin flaps! Try this out using the Cmd R keyboard shortcut, which plays the scene you’re currently editing (this should be the penguin scene). You should see a penguin slowly falling, and when you press spacebar, you should see the penguin flap. In order to make the physics a little more reasonable, you can modify the fields of the RigidBody2D. I set Weight = 1 and Gravity Scale = 16.

Finally, let’s add our penguin to the main scene. The easiest way to do this is to drag Penguin.tscn from the “FileSystem” explorer directly into the scene. You can also use the little chain link button in the “Scene” UI section. Once the penguin is in Main.tscn's node tree, you can run Cmd B — you should see something similar to the video at the beginning of this section. Note that the penguin can flap off the top of the screen or fall off the bottom since there are currently no collisions.

Collisions

Collisions, with “Visible Collision Shapes” enabled.

Now that our penguin is flapping away, it’s time to add collisions. First, we need to add a CollisionPolygon2D to our penguin scene. When I first discovered this type of node, I spent a long time meticulously shaping the polygon to exactly cover my textures. Then, I realized Godot can do this automatically. Here’s what we gotta do. First, note that this process is a little messy for spritesheets (it doesn’t work unless Hframes == 1 && Vframes == 1), so we need to reset Hframes = 1. We can change this back later. Then, while the penguin Sprite is selected, go up to the top, click “Sprite” and then select “Create CollisionPolygon2D Sibling.” This is going to create three separate CollisionPolygon2D nodes, one for each Hframe. We’re going to take the lazy route out here and just use one of these, even though our penguin has an animation. The animation doesn’t affect the collision area by much though, so I don’t think it matters in terms of playability. After we delete the second and third CollisionPolygon2D nodes, we can switch Hframes back to three, and we should have a nice, automatically generated CollisionPolygon2D that fits the penguin! Feel free to tweak it if you want it to be more exact.

How to make Godot do all the work for you.

Make sure to keep the Centered field of the penguin Sprite checked. If the Sprite is not centered, then the CollisionPolygon2D will not be centered with respect to the RigidBody2D, and you’ll get weird bugs like this once you start adding collisions:

What I call the “center of gravity” bug.

Now, let’s actually put this CollisionPolygon2D to good use by adding some stuff for our penguin to collide with. We could do this by adding some StaticBody2D nodes to Main.tscn, but I think it’s more fun to do it programmatically. Let’s make a new script called Main.gd, attach it to the root node of Main.tscn, and dump this code into it.

extends Node2D# Called when the node enters the scene tree for the first time.
func _ready() -> void:
# Top wall
_add_wall(Vector2(0, -10), Vector2(1600, 10))
# Bottom, covers grass
_add_wall(Vector2(0, 900), Vector2(1600, 50))
func _add_wall(position: Vector2, size: Vector2) -> void:
var rect := RectangleShape2D.new()
rect.set_extents(size)

var collision_shape := CollisionShape2D.new()
collision_shape.shape = rect

var collision_object := StaticBody2D.new()
collision_object.position = position
collision_object.add_child(collision_shape)

add_child(collision_object)

Here’s a quick explanation of this code. _add_wall is a function that adds a wall to the scene, which is represented as a StaticBody2D. The CollisionShape2D tells it where the collision boundaries are, which gets its shape/size from the RectangleShape2D. Then, in _ready(), we add two walls, one for the top and one for the bottom.

In order to test this code out, I recommend enabling Debug > Visible Collision Shapes. This will make it so that you can see the collision shapes when you run the code. Now you can run the code with Cmd B, and the penguin should be able to collide with the top and bottom walls!

Collision Layers

Before we finish up, we’re going to make our collisions more robust by utilizing collision layers and masks. These properties are used to control what collides with what. For example, I could use these properties to make the penguin collide with the top wall but not the bottom wall. Here’s a definition of each property, taken from Godot’s documentation:

  • collision_layer - This describes the layers that the object appears in. By default, all bodies are on layer 1.
  • collision_mask - This describes what layers the body will scan for collisions. If an object isn’t in one of the mask layers, the body will ignore it. By default, all bodies scan layer 1.

It’s not really necessary to introduce collision layers and masks right now, since the only two things that can collide are the penguin and a couple of walls. However, it’ll make things easier as we continue to introduce collidable objects.

There are two main ways to use these properties. The first way is to use the Inspector, and set collision_layer and collision_mask values in the UI. If you do it this way, I recommend going to Project > Project Settings > Layer Names > 2d Physics and naming the layers; it’ll make things much easier.

Using the UI to set collision properties.

The second way, which is what we’ll do for this project, is to set the collision properties in code. I prefer this approach because oftentimes, one script is attached to multiple objects, and it’s easier to ensure consistency if the properties are controlled via the script. I think it also makes things more readable and easier to refactor.

In order to keep track of collision layers in code, we’re going to create a new script called CollisionLayers.gd, which will contain the following code:

extends Nodeenum Layers {
WALL = 0,
PENGUIN = 1,
}

We need to make CollisionPlayers a singleton because many scripts will need access to this enum, and GDScript does not support global variables. This page in the docs has more info on singletons in GDScript; the simple explanation is that you go to Project > Project Settings > AutoLoad and add your script.

Creating a CollisionLayers singleton.

Then, we can write stuff like CollisionLayers.Layers.WALL in any other script. Neat! Now all we need to do is use the Layers enum to set collision properties for our penguin and the walls. Here’s what the _ready() function in Penguin.gd should look like:

func _ready() -> void:
collision_layer = 0
set_collision_layer_bit(CollisionLayers.Layers.PENGUIN, true)
collision_mask = 0
set_collision_mask_bit(CollisionLayers.Layers.WALL, true)

And we can update the _add_wall() function in Main.gd as follows:

func _add_wall(position: Vector2, size: Vector2) -> void:
var rect := RectangleShape2D.new()
rect.set_extents(size)

var collision_shape := CollisionShape2D.new()
collision_shape.shape = rect

var collision_object := StaticBody2D.new()
collision_object.position = position
collision_object.add_child(collision_shape)

collision_object.collision_layer = 0
collision_object.set_collision_layer_bit(
CollisionLayers.Layers.WALL, true)
collision_object.collision_mask = 0

add_child(collision_object)

Now, all collidable objects are on a distinct layer, and the penguin only collides with walls.

Wrapping Up

Whew, that was a lot. But we also got a lot done! By this point, you should be able to make the penguin flap, have a flapping animation, and have collisions for the top and bottom of the screen. In the next tutorial, we’ll add an object pool so that characters start walking across the bottom of the screen. As usual, feel free to ask questions or leave feedback here or @pencilflip. Till next time!

--

--

Matt Lim

Software Engineer. Tweeting @pencilflip. Mediocre boulderer, amateur tennis player, terrible at Avalon. https://www.mattlim.me/