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"
// the rules of one level, kept in an asset rather than in the scene: each
// level gets its own .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;
// coins that win the level
i32 coinsToWin = 5;
// the scene that follows a win. Left empty, a win returns to the menu
Handle<SceneAsset> nextLevel;
};#include "LevelDefinition.h"
// registering the type is all a data asset needs: the Create menu entry, the
// Inspector and saving come from its 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, set timeLimit to
30and coinsToWin to5, and select Save. Leave nextLevel empty for now.
The HUD
The HUD and the pause menu are one UI document, Hud.slxml.
- 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 and 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, and starts hidden with display: none. LevelController shows it on pause and 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 and the clock, pauses
// the game, and moves on when the level is won or the time runs out.
//
// It sits on the same object as the UIPanelRenderer that draws the HUD, below
// it: a component is enabled after the ones above it, so 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 carrying 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"
#include "Shalltear/Core/UI/Context.h"
#include "Shalltear/Core/UI/UIPanelRenderer.h"
#include <cmath>
#include <format>
SLT_REGISTER_TYPE(LevelController)
LevelController::LevelController() : Behavior(BehaviorTick::Update) {
_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, found by the id they have in Hud.slxml. The
// handler holds a Handle rather than `this`, so a click that arrives after
// this component is gone 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();
// a game paused when it left the level must not arrive paused elsewhere
Time::SetTimeScale(1.0f);
// the handlers are this project's code, which a reload replaces, so they
// go with the component that set them
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 or 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 zero, so
// the clock stops with everything else
_timeLeft = std::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: on to the next level, or back to the menu after the last one
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 and everything that reads GetDeltaTime().
// OnUpdate still runs, which is how the pause key can unpause
Time::SetTimeScale(paused ? 0.0f : 1.0f);
// the document hides the pause menu with `display: none`, so it stays out
// of the way while you edit; this shows it and 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,
// which makes it 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.slxmlonto 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.