Making an Endless Runner in Godot, Part 2 — Collisions & Controls
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:
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 aRigidBody2D
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. ARigidBody2D
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
.
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
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.
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:
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.
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.
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!