Making an Endless Runner in Godot, Parts 3 & 4 — Object Pool

Matt Lim
6 min readMay 2, 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 previous part, you can check it out here. If you want to download the game, head over here.

Previously, we added the penguin to our game, along with basic support for user input, a flapping animation, and collisions. In this tutorial, we’re going to add the characters that the penguin will eventually poop on. The characters will continually walk across the screen, and will be spawned and controlled by an object pool. 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.

Here’s Part 3. Part 4 is coming out soon!

Character Scenes

The first thing we need to do is drag in all the character assets (TODO: link).

Your assets folder should look like this.

Then, we’ll make a new subfolder under the scenes directory called characters, and make a scene for each character.

Your scenes folder should look like this.

The node tree for each of these character scenes should look like this.

The node tree for CaptainHook.tscn.

The root node is a StaticBody2D, since in the future we’ll want these characters to collide with the penguin’s poop. Then, there is a Sprite to display a sprite sheet, and an AnimationPlayer to animate that sprite sheet.

Similarly to what we did with the penguin, we’ll set Hframes = 3, since the characters’ sprite sheets have 3 frames. Then, we’ll create an animation called “Walk” which animates the Frame field of the Sprite. It should look like this.

For the first frame, time is 0 and value is 0. For the second frame, time is 0.5 and value is 1. I.e. the animation toggles between the 0th and 1st frames of the sprite sheet.

It will be a bit repetitive, but we need to do that for each character. After that’s done, and all our characters have animations, it’s time to move on to the main event — creating an object pool to spawn and control these characters!

Object Pool

Let’s jump ahead for a moment. When we’re done with this section, our game should look like this.

Background

Now, let’s back up. Before we start talking about object pools and why we want one, let’s review the problem at hand.

  1. We want to make our set of characters walk across the screen.
  2. It should be possible for multiple of the same character to be on-screen at the same time, e.g. it should be possible for two Voldemorts to be on-screen at the same time.
  3. We don’t want to continually instance our scenes in order to do this, e.g. we don’t want to keep on calling load(resource) over and over again. This is expensive!

Without the last constraint, we could solve this in a very simple but expensive manner. If we wanted to spawn a new character, we could just pick a random character scene, instance it, and send it walking across the screen. The main problem here, as mentioned above, is that instancing scenes is slow (it’s usually done in _ready() when everything is being initialized). Further, without some manual cleanup, the scene would become bloated with more and more characters, which would also slow things down.

Given the last constraint, a reasonable solution is to instance a bunch of characters at initialization time, and then keep on re-using them. With this approach, if we want to spawn a new character, we need to pick one of the existing instances that is not already on the screen, move it to some starting position, and send it walking across the screen.

This solution is exactly what an object pool provides! Specifically, our object pool will simply be a node that does the following:

  • On initialization, instances each character scene (multiple copies of each scene, if desired) and stores them in a “pool” (we’ll use a list) of objects.
  • Controls how and when the objects spawn, e.g. how and when the characters start walking across the screen.

Note that what we’ve described is not actually specific to characters. Object pools are a generic concept, and we’ll design ours so that we can re-use it for other objects in our game later on. Anyhow, now that we’ve covered all this background info, let’s move onto our implementation!

Implementation

We’re going to go over the implementation in 3 steps. First, we’ll look at the _ready() function. Then, we’ll look at the _process() function. Finally, we’ll look at the entire ObjectPool.gd script.

  1. The _ready() function
func _ready() -> void:
var paths: Array = _get_full_paths(g_path)
for path in paths:
var resource = load(path)
for _i in g_copies_of_each:
var object: Node2D = resource.instance()
object.global_position = _get_random_global_position(object)
g_object_pool.append(object)
g_object_pool_available.append(object)
get_parent().call_deferred('add_child_below_node', self, object)

g_max_available_objects = paths.size() * g_copies_of_each

Clearly, we haven’t defined many of these functions yet. However, we should still be able to read through this code and understand it.

At a high level, the _ready() function grabs every scene that is a direct child of g_path, instances it, sets its position, and adds it to a pool of objects. That’s it!

For example, if g_path = /home/person/scenes/, and that folder contains scene1.tscn and scene2.tscn, then ready() will instance both of those scenes g_copies_of_each times and put those instances in a list.

2. The _process() function

func _process(_delta: float) -> void:
var time_diff = OS.get_system_time_msecs() - g_last_spawn_time_ms
if time_diff > g_rand_spawn_wait_ms:
var available_object = _find_and_remove_available_object()
if available_object:
available_object.global_position = _get_random_global_position(available_object)
available_object.start(g_object_velocity)
g_last_spawn_time_ms = OS.get_system_time_msecs()
g_rand_spawn_wait_ms = rand_range(g_min_spawn_wait_ms, g_max_spawn_wait_ms)
_add_to_available_objects()

Similarly to above, we also haven’t defined many of these functions. But again, we should still be able to understand it!

At a high level, the _process() function first checks to see if enough time has elapsed for it to spawn the next object. If enough time has elapsed, then it finds and removes an object from the pool of available objects. If this operation succeeds, then it sets its position, “starts” it, and resets the time variables. Finally, it calls _add_to_available_objects, which recomputes the pool of available objects. Note that “start” means different things for different objects. Calling a character’s start() function should make it start walking across the screen.

3. Putting it all together

The two most important functions are _ready() and _process(). However, if you want to see all the helper functions and global variables and whatnot, here you go! You can also check out the source code (TODO: link).

Using the Object Pool

Now that we have our script, all that’s left is to put it to use. Doing this is simple; we just need to add a Node2D to our main scene and attach this script to it. You should end up with something that looks like this.

The CharacterPool has ObjectPool.gd attached as a script.

Adjusting Some Scales

Finally, in order to get things exactly the same as the GitHub repo, you need to adjust some scales. You should just use the GitHub repo as a reference; adding screenshots would be too tedious. In general, you want to adjust the scale of:

  • The Penguin Sprite and CollisionPolygon2D (don’t scale the root RigidBody2D node, it won’t work).
  • All the character StaticBody2Ds. You also need to adjust the Sprite scale for GenericCharacter1 and GenericCharacter2, because those assets were made at a different scale.

If you’ve reached this point, running the game should look exactly like the video above. As usual, feel free to ask questions or leave feedback here or @pencilflip. Till next time!

--

--

Matt Lim
Matt Lim

Written by Matt Lim

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

Responses (1)