Skip to content

Feature: Checkpoints - #145

Merged
JorxPi merged 31 commits into
mainfrom
feature/checkpoints
Aug 7, 2026
Merged

Feature: Checkpoints#145
JorxPi merged 31 commits into
mainfrom
feature/checkpoints

Conversation

@javiercasman

@javiercasman javiercasman commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Description

We all know what a checkpoint is. However, there are several ways to define them: how to activate them, what the level will be like upon respawning (whether enemies will be alive or dead, whether pickups will be collected or not, etc.).

This implies the need to somehow save the game state when activating a checkpoint for a possible future respawn. Below, I will explain in detail how a checkpoint works in this game, and what the flow is from when characters die until they respawn at a checkpoint.

1. How a Checkpoint Works

First, players must activate a checkpoint (currently, it's a TriggerArea, activated automatically upon entering. Depending on design decisions, this could change in the future, for example, by adding an object to activate).
When a checkpoint is reached, a "state" is saved, defined by the following elements:

  • Enemies defeated up to the checkpoint
  • Breakables broken up to the checkpoint
  • Puzzles solved
  • Triggered events
  • Current HP
  • Current Reaper Gauge amount
  • Current Power-ups unlocked
  • Respawn points

This means that if players are defeated and choose to respawn, they will respawn at the last checkpoint reached, taking into account all the elements saved in the "state": defeated enemies will remain defeated, puzzles will remain solved, etc.

NOTE: If at any point during development it becomes necessary to save other game elements in this state system, it can be done. I have only added the crucial elements and those that have occurred to me so far during testing. It is subject to change.

2. Implementation Details

From here on, I will explain how this system is implemented: its structure, how states are stored and persisted, etc.

2.1. PersistingCheckpointState

PersistingCheckpointState is, as its name indicates, a class that persists throughout the game. This means that it is NOT a script (replicated behavior of PersistingPowerupState). Therefore, it is not contained in a GameObject, but rather is a class (specifically, a Singleton) that persists during gameplay. Consequently, it is not affected, on its own, by changes between levels or scenes. This is key and essential if we want to maintain saved information between respawns.

PersistingCheckpointState acts as a class that stores and updates the elements that will be restored upon respawning, and other classes will access this information to know what to do when players respawn. For example, CharacterDamageable will access this Singleton when, at respawn time, it needs to know how much health Lyriel (or Death) should have when reappearing at the checkpoint.

To do this, the information is stored in different data types.

  • std::vector<UID> for dead enemies, broken breakables, and triggered events.

  • floats for current HP and reaper gauge amount.

  • array<bool> for solved puzzles and another for unlocked power-ups.

Respawning is, ultimately, restarting the scene from scratch. Therefore, certain scripts will look in PersistingCheckpointState for the information they need to know what to do in their Start() function. You can guess which scripts will do this: BreakableDamageable, EnemyDamageable, PuzzleManager, etc.

For std::vector<UID>, a simple search will suffice to find the UID of the GameObject it's contained in, and if it's included, its GameObject will disappear from the scene (I won't go into specific details about which scripts follow this pattern).

You can see that there are two different vectors for each type: the normal vector and the "persistent" vector.

std::vector<UID> m_deadEnemies;
std::vector<UID> m_deadEnemiesPersistent;

The first acts as a kind of "cache": it stores the defeated enemies (or whatever corresponds to the vector) until the next checkpoint is reached.

The second serves to "confirm" those dead enemies (or whatever corresponds) once the checkpoint is reached, and this information is saved persistently until the level ends.

Upon reaching a checkpoint, the first checkpoint will save its elements to the second, and the first will reset, continuing to accumulate new UIDs. The purpose of this is to avoid storing duplicate UIDs.

Why not use std::set<> then? Simply put, vector searches are faster for elements like a UID.

2.1.1. m_triggeredEvents

This is where events that have already been triggered and should not be triggered again will be stored.

For an event to be stored here upon being triggered, two conditions must be met:

  • TriggerOnlyOnce == true
  • isPersistent == true

isPersistent is a new bool to differentiate between Events that have TriggerOnlyOnce = true and should be stored in this array, and those that don't. For example, MusicStateEvent is TriggerOnlyOnce, but we don't want to store it in this array, as otherwise the music won't be able to be reactivated upon respawning.

Other events like CameraTransitionEvent, CombatAreaEvent, or PopUpEvent will have isPersistent = true.

2.2. CheckpointEvent

CheckpointEvent is a script that inherits from GameplayEventAction, and it's the script responsible for activating the checkpoint so that players respawn at it.

When the event is triggered with ExecuteEvent(), it will save Lyriel's and Death's current lives, the amount of Reaper Gauge, and any unlocked power-ups. It will also call PersistingCheckpointState::SetCheckpoint(checkpointId), which will set the ID of the newly activated checkpoint, as well as update the persistent vectors and reset the others.

IMPORTANT CheckpointEvent is intended to be a base class to simplify things. I'll go into more detail about this when I explain the checkpoint prefab.

2.3. PersistingManager

PersistingManager is a Manager script whose function is to manage persistent information when loading a scene.

Each time a scene loads, PersistingManager will reset the PersistingCheckpointState temporal vectors to refresh the stored UIDs. It will also update any activated power-ups (PersistingPowerupState::setUnlockedPowerupState()).

To ensure the proper functionality of these persistent classes, a new script function has been added to the engine: OnGameStop(), an event that will be called when the game stops (when the stop button is pressed). This function ensures that PersistingCheckpointState and PersistingPowerupState are reset when the game ends/the stop button is pressed.

IMPORTANT For a future pull request, my intention is to ensure the correct functioning of checkpoints and the persistence of information between level changes, but I considered this slightly out of scope, as it requires changing the GameObjects responsible for this (using GameplayEventTrigger instead of TriggerArea).

2.4. Checkpoint Prefab

To simplify the implementation of checkpoints in the scene, I've created a prefab for quickly adding a checkpoint. The only changes needed are to modify the added Event script (by default, it's Checkpoint1_Level1_Event, just create a new one with the correct naming) and change the CheckpointId defined in Start(). Since it's an enum, it will need to be added to the CheckpointId enum of PersistingCheckpointState, if it doesn't already exist.

In addition, there's also a GameplayEventTrigger that will define the checkpoint's activation area (a particle or something similar will need to be added in the future to indicate the area to the player; currently, it's invisible).

Finally, there are two GameObjects: LyrielRespawn and DeathRespawn. These GameObjects are used to define the respawn positions of the characters at that checkpoint; simply change the Transform to the desired position of each one.

Pending changes & observations

Here I'll mention some pending changes and observations that should be considered for the correct use of checkpoints.

  • For the other levels, replicate the behavior of PuzzleManagerLVL1, since the classes don't exist and therefore I haven't been able to apply any changes.
  • The position of Checkpoint 1 (the only one currently present) is entirely provisional and its purpose is purely illustrative. I leave it to the Level Designers to modify it as they see fit and add Checkpoints for all levels.
  • Please, if you have any questions about how Checkpoints work or anything else to implement, don't hesitate to ask.

TODO

Resolve conflicts: add again new elements on Level 1 scene

Since we don't have a general LevelManager script, I am adding it for now in DefeatConditionManager
Needed to the map<UID, GameObject*> in the manager
# Conflicts:
#	ScriptsProject/Damageable.h
#	ScriptsProject/EnemyDamageable.cpp
#	ScriptsProject/EnemyDamageable.h
#	ScriptsProject/ScriptsProject.vcxproj
#	ScriptsProject/ScriptsProject.vcxproj.filters
# Conflicts:
#	ScriptsProject/ScriptsProject.vcxproj.filters
@javiercasman
javiercasman marked this pull request as ready for review July 31, 2026 18:12
@javiercasman javiercasman self-assigned this Jul 31, 2026
# Conflicts:
#	Engine/Assets/NavMeshes/Level1.navmesh
#	Engine/Assets/NavMeshes/Level1.navmesh.metadata
#	Engine/Assets/Scenes/Level1.scene
#	Engine/Assets/Scenes/Level1.scene.metadata
#	ScriptsProject/EnemyDamageable.cpp
@JorxPi
JorxPi merged commit 112c36b into main Aug 7, 2026
@JorxPi
JorxPi deleted the feature/checkpoints branch August 7, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants