Making Level Checkpoints in Unity

Goals

Hey there, thanks for dropping by! For today’s post I’m going to share my process for making a checkpoint system for a 2D platformer (and probably many other types of games as well). Even if it’s not exactly what you’re looking for, I think the design is basic enough that it could serve as a good starting point to work from. I’ll include code samples throughout, and sample project that you can import into unity to see the code in action!

I used Media Molecule’s LittleBig Planet games as a model for these checkpoints. These games use a checkpoint gate or object that activates when the character approaches it, and when they die or reload the level the character appears at the last activated checkpoint. Theres a lot to like in this basic premise – it can work for both linear games (super mario) or games that allow backtracking (super metroid). I also wanted a system that would allow for simple duplication of checkpoints, so that you can easily drag and drop checkpoint prefabs into your level. Finally, taking another page from LBP, you can use your checkpoints for playtesting purposes – simply drag and drop the checkpoint closest to the area you need to test, or make a temporary checkpoint to test from and just delete it when you’re done! No muss, no fuss.

Set up our scene

To start, let’s make some of the game objects we’ll need for our checkpointing system. The first thing we should do is make an empty game object and name it SceneMaster, then make a new script for our SceneMaster. This is where I like to keep code that will manage the initialization and timing of events that are local to the current level or scene. Our SceneMaster code should contain a public reference to a Checkpoint script that we will call “currentCheckpoint”. This provides us a field in the inspector that we can use to drag and drop any checkpoint object into, and when we start our scene for the first time, our player will appear at the location of this game

using System.Collections;
using System.Collections.Generic;
using UnityEngine; 
using UnityEngine.SceneManagement; 

public class SceneMaster : MonoBehaviour {

  // Variables
  public static SceneMaster active; 

  // Reference Variables
  public Checkpoint currentCheckpoint; 

  private void Awake(){
    active = this; 
  }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour {

  void Start(){
      transform.position 
          = SceneMaster.active.currentCheckpoint.transform.position;         
      Debug.Log("Box Position: " + transform.position);
  }
}

Before we do anything else we will want to make a Checkpoint script – it’s ok to leave it empty for now. Let’s make an empty gameObject to attach this checkpoint script to (you may want to add a sprite or a gizmo to this object so we can see it in our scene). We will also need a container object to organize all of our checkpoints, I’m going to make another empty game object and call it “LevelCheckpoints.” Once we add our checkpoint to Level Checkpoints, let’s drag it into the currentCheckpoint variable on our SceneMaster script.

Now we also want to add some code to the start up process of our player game object that will set our player position to the currentCheckpoint position.

If everything’s set up right, when we start our scene, our player should appear at the location of our checkpoint! so far, so good.

Level Persistence

And now for an interlude where we look at the lifecycle of a scene in Unity. When a scene is first loaded, each object in the scene’s hierarchy is added to memory and that object is initialized – this is done by calling its OnEnable() and Awake() functions, and finally the Start() function is called, before beginning the cycle of calling Update() once per frame. Now when we change scenes, each of the objects local to the scene is removed from memory, and this is done by first disabling each object (calling OnDisable()) and then using the Destroy() function to, well, destroy the object.

If we had code in our project that would reload our level, say, if our player fell on some spikes, Unity would delete all the objects in our scene, and then create a new instance of that scene, and a new instance of all of our objects – the player, the SceneMaster, the spikes.

Our goal is to be able to set our current checkpoint dynamically during the scene, but as soon as our scene ends, sceneMaster is destroyed and replaced with a new sceneMaster that has its default settings – including its original currentCheckpoint reference. This is no bueno – in order for this system to work we need a type of object that won’t be destroyed along with the scene. We also need to make sure that when the new scene’s copy of scenemaster shows up, it doesn’t cause confusion with the older, properly-bound scenemaster.

To that end, let’s turn our SceneMaster into a singleton using the following code. You can learn more about the singleton pattern HERE, and I encourage you to check it out when you have time.

public class SceneMaster : MonoBehaviour {

  // Variables
  public static SceneMaster active; 
  public Checkpoint currentCheckpoint; 
  private string levelID; 

  private void Awake(){
        Scene scene = SceneManager.GetActiveScene();
        levelID = scene.name; 

        // if SceneMaster exists from a previous level, delete it
        if( SceneMaster.active != null ){
            if( SceneMaster.active.LevelID != scene.name ){
                Destroy( SceneMaster.active.gameObject ); 
                SceneMaster.active = null; 
                Debug.Log("Active SceneMaster not for this level, deleted");

            // if SceneMaster exists for this level, delete this instance
            }else{
                Debug.Log("SceneMaster already exists for this level, deleting");
                Destroy( gameObject ); 
            }
        }

        // if no SceneMaster is active, make this the active manager
        if( SceneMaster.active == null ){
            SceneMaster.active = this; 
            Debug.Log("No active SceneMaster found, making this active");
            DontDestroyOnLoad(this); 
        }
  }
}

A pure singleton in Unity is a game object that refuses to die on a scene change, and they are commonly used to store game states that need to persist across multiple levels. OUR singleton is slightly different, because we needs objects that will persist as long as we are reloading the current level, but then will get replaced when the next level is loaded (presumably with its own set of checkpoints).

So our singletons will set themselves up with a string that holds their original scene’s name, and if ever they find themselves on a different scene, they will exit stage right.

Once we have set this code up in our SceneMaster, we will also want to make a CheckpointMaster script that we will put on our “Level Checkpoints” game object. CheckpointMaster is going to use pretty much the same logic, posted here.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement; 

public class CheckpointManager : MonoBehaviour {

    // Variables
    public static CheckpointManager active; 
    private string levelID;

    // Properties
    public string LevelID { get{return levelID;} }

    // [[ ----- AWAKE ----- ]]
    private void Awake(){
        Scene scene = SceneManager.GetActiveScene();
        levelID = scene.name; 

        // if last checkpoint was on different level, delete it
        if( CheckpointManager.active != null ){
            if( CheckpointManager.active.LevelID != scene.name ){
                Destroy( CheckpointManager.active.gameObject ); 
                CheckpointManager.active = null; 
                Debug.Log("Active checkpoint not for this level, deleted");

            // if checkpoint manager exists for this level, delete this instance
            }else{
                Debug.Log("Checkpoint already exists for this level, deleting");
                Destroy( gameObject ); 
            }
        }

        // if no checkpoint manager is active, make this the active manager
        if( CheckpointManager.active == null ){
            CheckpointManager.active = this; 
            DontDestroyOnLoad(this); 
            Debug.Log("No active checkpoint found, making this active");
        }
    }
}

Why don’t we put this code into our Checkpoint script? Well the main reason is efficiency – because we have organized our checkpoints as children of “Level Checkpoints” they will persist or be removed as an extension of that game object. Also, the DontDestroyOnLoad() function only works on root-level objects in the hierarchy, which makes sense because otherwise you would have orphaned child objects on scene load, and that’s just too sad.

Bonus – Awake() for Woke Objects

An interesting quirk of using singletons in Unity is in the initialization routine on subsequent scene loads. As mentioned, enable, awake and start events are called the first time an object appears on the scene, which means that immortal objects are not getting a new initialization sequence when the next scene loads up.

If you have code that you want to run at the beginning of EACH scene that an immortal game object appears in, you can do so by piggybacking on Unity’s SceneManager.sceneLoaded event, as shown in the following code.

public class SceneMaster : MonoBehaviour {

  // Variables
  public static SceneMaster active; 
  public Checkpoint currentCheckpoint; 
  private string levelID; 

  private void OnEnable(){
      SceneManager.sceneLoaded += Initialize; 
  }

  private void OnDisable(){
      SceneManager.sceneLoaded -= Initialize;
  }

  private void Awake(){
        Scene scene = SceneManager.GetActiveScene();
        levelID = scene.name; 

        // if SceneMaster exists from a previous level, delete it
        if( SceneMaster.active != null ){
            if( SceneMaster.active.LevelID != scene.name ){
                Destroy( SceneMaster.active.gameObject ); 
                SceneMaster.active = null; 
                Debug.Log("Active SceneMaster not for this level, deleted");

            // if SceneMaster exists for this level, delete this instance
            }else{
                Debug.Log("SceneMaster already exists for this level, deleting");
                Destroy( gameObject ); 
            }
        }

        // if no SceneMaster is active, make this the active manager
        if( SceneMaster.active == null ){
            SceneMaster.active = this; 
            Debug.Log("No active SceneMaster found, making this active");
            DontDestroyOnLoad(this); 
        }
  }

  // Logic that should be called at the load of the level 
  private void Initialize( Scene scene, LoadSceneMode mode ){

      // catch missing checkpoint error
      if( currentCheckpoint == null ){ 
          Debug.LogError("Checkpoint not provided"); 
      }
  }
}

We’re going to be using events in the next part of our script, so don’t worry if it doesn’t make sense now.

Making our Checkpoint Objects

Right now our checkpoints are basically just objects with an empty script on them, but we would like them to be a little more than that. Our checkpoints MUST have a trigger collider applied to them so that when our player touches one it becomes active. They optionally may have sprites attached to them as well, if you’d like your checkpoints to be visible.

In my example I’ve made a rather crappy dashed circle icon to indicate where my checkpoints are, and also which one is the currently active checkpoint. I’ve done this by attaching two sprites to my checkpoint object and turning them on and off via code, but you could add animations or forgo sprites altogether, your call.

Once you have set up your checkpoint object with the visuals you’d like, let’s create a prefab out of it by dragging the object down into one of our assets folder. This way if we want to update our sprites later we just have to edit our prefab, and all of our placed checkpoints will update automatically.

Here’s my checkpoint’s OnTrigger code, which should work so long as your player character has its tag set to “Player”

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class Checkpoint : MonoBehaviour{ 

  // Variables
    private bool isCurrCheckpoint; 

  private void OnTriggerEnter2D( Collider2D _c ){
        if( !isCurrCheckpoint ){
            if( _c.gameObject.tag == "Player" ){
                Debug.Log("Checkpoint Hit: " + gameObject.name);
            }
        }
    }


}

I have also added some logic that prevents a player from triggering an already active checkpoint, simply because once a checkpoint is set there’s no reason to trigger further logic if that checkpoint is hit again immediately. Right now that’s just the “isCurrCheckpoint” variable, but we’ll be using that in the next block of code.

Now let’s talk about events. Events allow our game objects to “subscribe” to a function that may be called on some far-off object, in order to trigger their own logic whenever the event takes place. You can read more about events in unity HERE.

The event we specifically want to create will trigger whenever a player touches a checkpoint trigger. When that happens, we’ll want every checkpoint in our scene to decide if they were the checkpoint that was hit, if so then they will add themselves as the new currentCheckpoint in our scene master. If they aren’t, then they will turn off their active sprite so we know that they have been deactivated.

Check out the following script to see how I handle this in code.

public class Checkpoint : MonoBehaviour{ 

    // Variables
    private bool isCurrCheckpoint; 

    // Reference Variables
    private GameObject activeSprite; 
    private GameObject inactiveSprite; 

    // Event setup
    public delegate void UpdateCheckpointDel( string _cpName ); 
    public static event UpdateCheckpointDel OnCheckpointHit; 


    // [[ ----- ON ENABLE ----- ]]
    private void OnEnable(){
        OnCheckpointHit += UpdateCheckpoint; 
    }

    // [[ ----- ON DISABLE ----- ]]
    private void OnDisable(){
        OnCheckpointHit -= UpdateCheckpoint; 
    }

    // [[ ----- START ----- ]]
    private void Start(){
        // set up sprite references
        activeSprite = transform.Find("activeSprite").gameObject; 
        inactiveSprite = transform.Find("inactiveSprite").gameObject; 
        if( SceneMaster.active.currentCheckpoint == this ){SetSpriteActive(true);}
        else{ SetSpriteActive(false); }
    }

    // [[ ----- UPDATE CHECKPOINT ----- ]]
    // called by OnCheckpointHit event
    public void UpdateCheckpoint( string _cpName ){
        if( gameObject.name == _cpName ){
            SceneMaster.active.currentCheckpoint = this; 
            SetSpriteActive(true); 
        }else{
            SetSpriteActive(false); 
        }
    }

    // [[ ----- ON TRIGGER ENTER 2D ----- ]]
    private void OnTriggerEnter2D( Collider2D _c ){
        if( !isCurrCheckpoint ){
            if( _c.gameObject.tag == "Player" ){
                Debug.Log("Checkpoint Hit: " + gameObject.name);
                // trigger checkpoint event to deactivate all 
                Checkpoint.OnCheckpointHit( gameObject.name ); 
            }
        }
    }

    // [[ ----- SET SPRITE ACTIVE ----- ]]
    private void SetSpriteActive( bool _true ){
        if( _true ){
            activeSprite.SetActive(true);
            inactiveSprite.SetActive(false);
        }else{
            activeSprite.SetActive(false);
            inactiveSprite.SetActive(true); 
        }
    }

Now there are more straitforward ways to do this, such as creating an array of Checkpoints in our CHeckpointMaster script that we manually add our checkpoints to in the Unity inspector. Then we could call a function on CheckpointMaster that would iterate through the array and turn our checkpoint sprites on and off as necessary.

BUT I want my checkpoints to be drag-and-drop-simple. By setting up this event, all of my checkpoints can operate autonomously – no need to change an array length and add a new reference. No, with this system adding a new checkpoint is as easy as ctrl+c, ctrl+v.

Done

At this point your code should be working as described! You will need some code that will reload your scene and code that will load into a new scene to fully test how this technique works (or you could import the sample project found at the end of this post). Whichever checkpoint has been set to currentCheckpoint in our SceneMaster will be the first place your player will spawn when you hit play in your editor. If you touch another checkpoint before transitioning the scene, that’s the next spawn point you should appear at when the level is reloaded. Of course if you transition to a new scene and then back to the original, you will begin again at the default checkpoint.

It should be clear now that this system has some limitations, specifically when it comes to persistent game states. If you don’t want to erase the player’s progress through the level on death (enemies defeated, coins collected, etc), then reloading the entire level may be counter-productive. As with everything in programming there are a thousand ways to design your checkpoints, and it’s up to you to decide what’s right for your project.

Sample Project Files

You can download a custom asset package that contains the final versions of all of these scripts here: CheckpointDemo-UnityPackage

You can import asset packages into a blank unity project by using Assets > Import Package > Custom Package

If you are using the imported project files, remember that before transitioning between scenes in-game you will have to add both scenes to your “build”, you can do this by opening each scene in turn and using File > BuildSettings > Click on the “Add Open Scenes” Button. You should see both scenes in the “Scenes in Build” window when you’re finished.

I can be reached @NinjaBoots88 on twitter, let me know if you found this post helpful or if you get stuck! I’d like to post more tutorials like this if people find them helpful, so any feedback is appreciated. Keep working at it, and I will too.