Timer, goal, and pause
The level's rules as an asset
Each level has a time limit, a number of coins that wins it and a level that follows it. Those belong to the level rather than to any object in it, so they go in an asset of their own: one .level file per level.
- Create a component named
LevelDefinition, as before, and replace its two files. It is not a component once you have: it derives fromDataAsset, the base class for assets made of fields.
#pragma once
#include "Shalltear.h"
// rules of one level, stored in an asset (not in the scene). Each level
// gets a .level file (made from the Asset Browser's Create menu)
class LevelDefinition : public DataAsset {
public:
SLT_REFLECT_CLASS(LevelDefinition)
SLT_REFLECT_PARENT(DataAsset)
SLT_CREATE_ASSET_MENU("Level Definition", ".level")
SLT_REFLECT_FIELD(timeLimit)
SLT_REFLECT_FIELD(coinsToWin)
SLT_REFLECT_FIELD(nextLevel)
SLT_END_REFLECT()
// seconds the player has to finish
f32 timeLimit = 30.0f;
i32 coinsToWin = 5;
// the scene following a win. a win should return to menu if empty
Handle<SceneAsset> nextLevel;
};#include "LevelDefinition.h"
// registering the type is all a data asset needs. Create menu entry/Inspector/
// saving come from reflected fields
SLT_REGISTER_TYPE(LevelDefinition)Save both files.
SLT_CREATE_ASSET_MENUadds Level Definition to the Asset Browser's Create menu.In the Asset Browser, select + and choose Create ▸ Level Definition. Rename it
Level1.level.
In the Inspector, timeLimit is set to
30and coinsToWin is set to5. Leave nextLevel empty for now.
The HUD
The HUD and the pause menu are one UI document, Hud.slxml. A UI document is a file you can write in a code editor, or build in the editor's UI Builder: in the Asset Browser, select + and choose Create ▸ UI Document, then double-click the new file. This page gives you the text, so the ids the code looks for match exactly.
- In your code editor, create the file
Assets/UI/Hud.slxmlin the project folder with this text, and save it. The editor finds the new file and adds it to the Asset Browser. To change it later, double-click it to open the UI Builder.
<?xml version="1.0" encoding="UTF-8"?>
<ui version="1">
<panel id="screen" style="position: absolute; left: 0; top: 0; width: 100%; height: 100%">
<!-- LevelController writes the coins + the time left into these two, by their ids -->
<panel id="hud" style="position: absolute; left: 24px; top: 24px; flex-direction: column">
<panel id="coins">Coins: 0 / 0</panel>
<panel id="timer">Time: 0</panel>
</panel>
<!-- covers the screen, starts hidden with display: none. LevelController shows it on pause + wires its two buttons -->
<panel id="pause-menu" style="display: none; position: absolute; left: 0; top: 0; width: 100%; height: 100%; flex-direction: column; align-items: center; justify-content: center; background-color: #00000099">
<panel id="paused" style="margin-bottom: 24px">Paused</panel>
<button id="resume" style="width: 240px; margin-bottom: 12px" text="Resume"/>
<button id="menu" style="width: 240px" text="Main Menu"/>
</panel>
</panel>
</ui>
In the Asset Browser, select + and choose Create ▸ Panel Settings. Rename it
Game.slpanel, set fontSizePx to28and select Save. Panel settings apply to a whole panel; this one makes its text large enough to read.
The LevelController component
- Create a component named
LevelControllerand replace its two files:
#pragma once
#include "Shalltear.h"
#include "LevelDefinition.h"
// FD
namespace Shalltear {
class UIPanelRenderer;
}
// runs one level: counts down its time, shows the coins + the clock, pauses
// the game, moves on when the level is won (or the time runs out)
//
// Sits on the same object as the UIPanelRenderer that draws the HUD (below)
// A component is enabled after the ones above -> the HUD is built by
// the time OnEnable looks for its buttons
class LevelController : public Behavior {
public:
LevelController();
SLT_REFLECT_COMPONENT(LevelController, Behavior)
SLT_REFLECT_FIELD(level)
SLT_REFLECT_FIELD(player)
SLT_REFLECT_FIELD(menuScene)
SLT_END_REFLECT()
// this level's rules
Handle<LevelDefinition> level;
// the object with the PlayerController
Handle<SceneObject> player;
// where a lost level (or the pause menu's Main Menu button) goes
Handle<SceneAsset> menuScene;
protected:
void OnEnable() override;
void OnDisable() override;
void OnUpdate() override;
private:
UIPanelRenderer *GetHud() const;
void SetPaused(bool paused);
void GoTo(Handle<SceneAsset> scene);
InputActionMap _controls;
ActionId _pause;
f32 _timeLeft = 0.0f;
bool _paused = false;
bool _finished = false;
};#include "LevelController.h"
#include "PlayerController.h"
SLT_REGISTER_TYPE(LevelController)
LevelController::LevelController() {
_pause = _controls.RegisterAction("Pause"_hash, ActionType::Button);
_controls.Bind(_pause, {.code = u16(Key::Escape)});
_controls.Bind(_pause, {.source = InputBinding::Source::GamepadButton, .code = u16(GamepadButton::Start)});
}
void LevelController::OnEnable() {
LevelDefinition *rules = level.Get();
_timeLeft = rules ? rules->timeLimit : 0.0f;
_controls.Enable();
// the pause menu's two buttons (by the id they have in Hud.slxml). The
// handler holds a Handle, not `this` -> a click arriving after
// this component is dead does nothing
if (UIPanelRenderer *hud = GetHud()) {
UI::Context *ui = hud->GetContext();
Handle<LevelController> self(this);
ui->Events(hud->FindElement("resume")).onClick = [self] {
if (LevelController *controller = self.Get()) {
controller->SetPaused(false);
}
};
ui->Events(hud->FindElement("menu")).onClick = [self] {
if (LevelController *controller = self.Get()) {
controller->GoTo(controller->menuScene);
}
};
}
SetPaused(false);
}
void LevelController::OnDisable() {
_controls.Disable();
Time::SetTimeScale(1.0f);
if (UIPanelRenderer *hud = GetHud()) {
UI::Context *ui = hud->GetContext();
ui->Events(hud->FindElement("resume")).onClick = nullptr;
ui->Events(hud->FindElement("menu")).onClick = nullptr;
}
}
// the HUD on this object (or null when it is missing/not built)
UIPanelRenderer *LevelController::GetHud() const {
UIPanelRenderer *hud = GetOwner()->GetComponent<UIPanelRenderer>().Get();
return hud && hud->GetContext() ? hud : nullptr;
}
void LevelController::OnUpdate() {
_controls.Evaluate();
if (_controls.WasPerformed(_pause) && !_finished) {
SetPaused(!_paused);
}
LevelDefinition *rules = level.Get();
SceneObject *playerObject = player.Get();
PlayerController *hero = playerObject ? playerObject->GetComponent<PlayerController>().Get() : nullptr;
if (!rules || !hero || _finished) {
return;
}
// GetDeltaTime() is scaled time. While the game is paused it is 0
_timeLeft = Math::Max(0.0f, _timeLeft - Time::GetDeltaTime());
if (UIPanelRenderer *hud = GetHud()) {
UI::Context *ui = hud->GetContext();
ui->SetTextString(hud->FindElement("coins"), std::format("Coins: {} / {}", hero->GetCoins(), rules->coinsToWin));
ui->SetTextString(hud->FindElement("timer"), std::format("Time: {}", static_cast<i32>(std::ceil(_timeLeft))));
}
if (hero->GetCoins() >= rules->coinsToWin) {
// won: to the next level (or back to the menu if last)
GoTo(rules->nextLevel.Get() ? rules->nextLevel : menuScene);
} else if (_timeLeft <= 0.0f) {
GoTo(menuScene);
}
}
void LevelController::SetPaused(bool paused) {
_paused = paused;
// time scale 0 stops physics/everything that reads GetDeltaTime().
// OnUpdate still runs (pause key can unpause)
Time::SetTimeScale(paused ? 0.0f : 1.0f);
// sound runs on own clock (paused separately). The
// player's AudioSource plays every sound in this game (pausing it
// holds its one-shots too). Game with many sources routes them to a mixer
// group whose clock is Gameplay instead -> the 0 time scale holds them
SceneObject *playerObject = player.Get();
if (AudioSource *audio = playerObject ? playerObject->GetComponent<AudioSource>().Get() : nullptr) {
if (paused) {
audio->Pause();
} else {
audio->UnPause();
}
}
// the document hides the pause menu with `display: none` -> it stays out
// of the way while you edit. This shows/hides it again
if (UIPanelRenderer *hud = GetHud()) {
UI::Context *ui = hud->GetContext();
UI::ElementHandle pauseMenu = hud->FindElement("pause-menu");
UI::Style style = ui->GetStyle(pauseMenu);
style.display = paused ? UI::Display::Flex : UI::Display::None;
ui->SetStyle(pauseMenu, style);
}
}
void LevelController::GoTo(Handle<SceneAsset> scene) {
SceneAsset *target = scene.Get();
if (!target || _finished) {
return;
}
_finished = true;
// every loaded scene is replaced by this one at the end of the frame
// (safe to ask from anywhere -> a button click included)
SceneSystem::ReplaceScenes(target->assetId);
}Save both files.
In the Hierarchy, select + and choose Create Empty. Rename the object
Level.
Select Add Component, open UI and select UIPanelRenderer. Drag
Hud.slxmlfromAssets/UIonto its document field andGame.slpanelonto panelSettings.
Add LevelController, so that it is below the UIPanelRenderer. Drag
Level1.levelonto its level field, thePlayerrow from the Hierarchy onto player, andMain.scenefromAssets/Scenesonto menuScene.
Press Ctrl+P. The HUD shows the coins and the time. Press Escape to pause, and Resume to carry on. Collecting all five coins, or running out of time, loads Main.scene, which still holds only a camera and a light; the next page makes it the menu.


Save the scene. Next, add a menu and a second level.