Designing a Checkpoint System

As mentioned over on No Robots, I was at a game jam the weekend just gone. It was an amazing experience and it felt great to stretch my development muscles. It was also a powerful learning experience working in a team. I couldn’t be lazy, I needed to design my code so it was easily used by the designers who were laying out the levels and applying my scripts in Unity.

Since our game is a 3D platformer, we decided to insert checkpoints. I felt strongly that these should be tuneable, even though we had a group discussion about how many there should be, and where. Things happen, minds change and sometimes what seems like a perfect idea just doesn’t hold up to play testing, so the checkpoint system I eventually came up with was easily scaled from one to as many checkpoints as you like.

The first step was designing the relationships between the different objects in the scene. The player was tagged Player, the checkpoints were tagged Checkpoint and the enemies and hazards were labelled Enemy. This is important, because the scripts need to be able to make sense of this kind of logic flow:

Checkpoint System - Logical Flow

The controller contains pointers to all of the checkpoints in the scene, in the order that they will be triggered. This is a limitation of the system, but one that doesn’t impact our game as it’s a linear experience, are most checkpointed games. When the player enters a checkpoint, the checkpoint makes a record of the location of the player as a Vector3 struct, sets its triggered flag to true and calls the GetLatestCheckpoint() method on the controller. This method iterates through the array of checkpoints starting from the end and stops as soon as it reaches a triggered checkpoint. It remembers this as an integer.

This process repeats any time the player enters a checkpoint, but when the player dies – that is to say, when the player collides with an Enemy object, the respawn method on the Player calls the GetRespawnLocation() method on the controller. This returns a Vector3 from the latest-triggered checkpoint, which the player’s respawn method uses to relocate the model.

Example

In the below example, we have a list of checkpoints, ordered top to bottom in the list and left to right on the map.

List of Checkpoints

Checkpoints on Map

These checkpoints are tagged Checkpoint and have the Checkpoint.cs script attached as a component.

Checkpoint with script

You’ll notice I’ve also included an object using the CheckpointController script. This is to make it a little more fool-proof. You can totally use searching functions to make that stuff implicit, but I don’t like the idea of the overhead, or the scope for weird bugs to creep in, so I like to keep it explicit.

The object with the CheckpointController script applied is an empty game object with a bunch of different controller scripts applied, it just makes it much easier to find when you’re laying out the hierarchy of the scene.

screen_shot_2014-04-15_at_11.03.19

Incidentally, one of the nicest things about working in Unity has to be how it handles circular references – like the above, including pointers to the controller in the checkpoints, and pointers to the checkpoints in the controller. One of the most annoying aspects of C++ is planning out your code to avoid interdependencies, which thankfully isn’t necessary here.

You’ll notice that I’ve put the checkpoints into the array in the order that they will be hit, this is because the member functions will be iterating through the array in reverse to find the latest-triggered checkpoint, so the sequence is important.

That last field, the Latest Checkpoint one, is really just for debugging, to make sure the system is working properly, or to skip the player ahead to a later part of the level.

The code is quite straightforward. The main methods in the controller are:

// This method should get called when the player collides
with a checkpoint, this is in the checkpoint script.

public int GetLatestCheckpoint() { for (int i = createdCheckpoints.Length – 1; i >= 0; i–) { if (createdCheckpoints[i].triggered) { latestCheckpoint = i; break; } } return latestCheckpoint; }

and

public Vector3 GetLatestCheckpoint()
{
return createdCheckpoints[GetLatestCheckpoint()].respawnLocation;
}

The GetLatestCheckpoint() is called by the respawn function of the Player object to get the location it needs to respawn to, and as you probably guessed, createdCheckpoints[] is the array of checkpoint objects.