Learn Simulant
Everything you need to know to build games with Simulant
Scene Management
The SceneManager is the engine subsystem responsible for creating, loading, activating, deactivating, and unloading scenes. It handles the entire lifecycle of scene transitions, including passing arguments between scenes, preloading in the background, and managing activation behaviour.
Related documentation:
- Scenes -- Creating scenes, lifecycle methods, and stage node management
- Application -- Application entry point and accessing the SceneManager
- Render Pipelines -- Setting up layers, multi-camera rendering, and transitions
- Signals -- Using scene activation/deactivation signals
1. SceneManager Overview
The SceneManager is a property of the Application class, accessible from anywhere in your code via the scenes shortcut. Its job is to:
- Store scene factories -- recipes for creating scenes on demand
- Instantiate scenes the first time they are requested
- Load and activate scenes with configurable behaviour
- Pass arguments to scenes at load time
- Preload scenes synchronously or in the background
- Provide signals when scenes activate or deactivate
At a high level, the SceneManager works with two internal maps:
| Map | Purpose |
|---|---|
scene_factories_ |
Stores the factory functions registered by register_scene<T>() |
routes_ |
Stores already-instantiated Scene objects, keyed by their string name |
When you call activate("game"), the manager looks up "game" in routes_. If it is not there, it uses the factory from scene_factories_ to create it. This means scenes are created lazily -- they are not instantiated until they are first needed.
Application
|
+-- SceneManager
|
+-- scene_factories_ : { "menu" => MenuScene factory,
| "game" => GameScene factory, ... }
|
+-- routes_ : { "menu" => MenuScene instance (active),
| "game" => GameScene instance (loaded but not active) }
|
+-- current_scene_ => MenuScene (the active scene)
Accessing the SceneManager
From anywhere in your Application or a Scene, the SceneManager is available via the scenes property:
// From within a Scene
scenes->activate("menu");
scenes->preload("loading");
// From within the Application
scenes->register_scene<MenuScene>("menu");
scenes->activate("menu");
// From anywhere using the global app pointer
smlt::get_app()->scenes->activate("game");
2. Scene Registration
Before a scene can be activated, it must be registered with the SceneManager. Registration is typically done in your Application's init() method.
Basic Registration
The simplest form registers a scene type with no extra arguments:
class MyGame : public Application {
public:
MyGame(const AppConfig& config)
: Application(config) {}
bool init() override {
scenes->register_scene<MenuScene>("menu");
scenes->register_scene<GameScene>("game");
scenes->register_scene<GameOverScene>("game_over");
return true;
}
};
The string "main" is special: it is the first scene the engine attempts to activate when the application starts. Make sure one scene is registered with this name, or manually activate a different scene yourself.
Registration with Constructor Arguments
If your Scene constructor takes additional parameters beyond Window*, you can pass them at registration time:
// Scene definition
class GameScene : public Scene {
public:
GameScene(Window* window, int level_number, bool hard_mode)
: Scene(window),
level_number_(level_number),
hard_mode_(hard_mode) {}
private:
int level_number_;
bool hard_mode_;
void on_load() override {
S_INFO("Loading level {} (hard_mode={})", level_number_, hard_mode_);
}
};
// Registration -- the extra args are forwarded to the constructor
scenes->register_scene<GameScene>("game", 3, true);
You can pass any number of arguments. They are captured and stored, then used each time the scene factory creates an instance.
Registering Multiple Factory Variants
Because the factory is keyed by a string name, you can register the same Scene type under different names with different parameters:
scenes->register_scene<GameScene>("level_1", 1);
scenes->register_scene<GameScene>("level_2", 2);
scenes->register_scene<GameScene>("level_3", 3);
Unregistering a Scene
You can remove a scene factory at any time with unregister_scene(). This also unloads and destroys any existing instance of that scene:
scenes->unregister_scene("level_1");
After unregistering, calling activate("level_1") will throw an error because no factory exists for that name.
3. Scene Lifecycle
Every Scene goes through a well-defined lifecycle managed by the SceneManager. Understanding this lifecycle is essential to writing correct scene code.
Lifecycle States and Transitions
register_scene()
|
v
[Registered] <-- Factory stored, scene not yet created
|
activate() / preload()
|
v
[Loading] <-- on_pre_load() -> on_load()
|
v
[Activated] <-- on_activate()
|
| (every frame)
v
[Updating] <-- on_update(dt), on_fixed_update(step), on_late_update(dt)
|
activate(other_scene)
|
v
[Deactivated] <-- on_deactivate()
|
(depends on behaviour)
/ \
v v
[Unloaded] [Still Loaded]
on_unload() (ready to re-activate)
on_post_unload()
Lifecycle Methods
These are the methods you can override in your Scene subclass, in the order they are called:
| Method | When Called |
|---|---|
on_pre_load() |
Before on_load() -- optional pre-load hook |
on_load() |
Pure virtual -- create nodes, load assets, set up pipelines |
on_activate() |
When the scene becomes the active scene |
on_update(float dt) |
Every frame while active |
on_fixed_update(float step) |
At a fixed timestep (60 Hz) while active |
on_late_update(float dt) |
After all on_update calls, while active |
on_deactivate() |
When the scene stops being active |
on_unload() |
When the scene is unloaded |
on_post_unload() |
After on_unload() -- optional post-unload hook |
For detailed information on each method, see the Scenes documentation.
Manual Load and Unload
You can also control the load state independently of activation:
// Check if a scene is loaded
if (scenes->is_loaded("game")) {
S_INFO("Game scene is already loaded");
}
// Manually unload a scene
scenes->unload("game");
// Unload and destroy all scenes
scenes->destroy_all();
scenes->clean_destroyed_scenes();
// Reset -- unloads and clears everything
scenes->reset();
Calling unload() on a scene will also call deactivate() first if the scene is currently active.
4. Scene Activation Behaviours
When switching from one scene to another, the SceneManager supports two activation behaviours that control the order of unload and load operations.
ACTIVATE_BEHAVIOUR_UNLOAD_FIRST (Default)
This is the default behaviour. When you call activate():
- The current scene is deactivated (
on_deactivate()called) - The current scene is unloaded (if
unload_on_deactivateis true) - The new scene is loaded (if not already loaded,
on_load()called) - The new scene is activated (
on_activate()called)
Current Scene New Scene
| |
deactivate() |
unload() |
load()
activate()
Use this when you want to free memory from the current scene before loading the next one. This is the safest choice for memory-constrained platforms.
scenes->activate("menu", ACTIVATE_BEHAVIOUR_UNLOAD_FIRST);
// Or simply (this is the default):
scenes->activate("menu");
ACTIVATE_BEHAVIOUR_UNLOAD_AFTER
This behaviour loads the new scene before unloading the old one:
- The new scene is loaded (if not already loaded)
- The current scene is deactivated
- The scenes are swapped (new scene becomes active)
- The new scene is activated
- The current scene is unloaded (if
unload_on_deactivateis true)
Current Scene New Scene
| |
load()
deactivate() |
<--- swap ---> |
activate()
unload() (old scene)
Use this when you need the new scene to be fully loaded before the old one is torn down. This is useful for:
- Transition effects -- keep the old scene's render pipeline alive while fading in the new scene
- Seamless transitions -- have both scenes coexist briefly during the transition
scenes->activate("game", ACTIVATE_BEHAVIOUR_UNLOAD_AFTER);
Controlling Unload on Deactivate
By default, scenes are set to unload_on_deactivate = true, meaning they are automatically unloaded when deactivated. You can change this per scene:
class PersistentHUDScene : public Scene {
public:
PersistentHUDScene(Window* window)
: Scene(window) {
// This scene should stay loaded across transitions
set_unload_on_deactivate(false);
}
void on_load() override {
// Set up HUD elements that persist
}
};
When unload_on_deactivate is false, the scene stays loaded in memory and can be re-activated instantly without going through on_load() again. Only on_activate() will be called.
5. Preloading Scenes
Preloading allows you to load a scene's assets ahead of time so that activation is instant. There are two ways to preload: synchronous and asynchronous (background).
Synchronous Preload: preload()
Loads the scene immediately on the calling thread:
class MyGame : public Application {
public:
bool init() override {
scenes->register_scene<GameScene>("game");
// Preload the game scene synchronously
scenes->preload("game", 1); // Passes level 1 as a constructor arg
// Now activate it -- no loading delay
scenes->activate("game");
return true;
}
};
If the scene is already loaded, preload() returns immediately without doing anything.
Asynchronous Preload: preload_in_background()
Loads the scene on a background thread, returning a Promise<void> that you can use to know when loading is complete:
class MyGame : public Application {
public:
bool init() override {
scenes->register_scene<LoadingScene>("loading");
scenes->register_scene<GameScene>("game");
// Show loading screen immediately
scenes->activate("loading");
// Start loading the game scene in the background
scenes->preload_in_background("game", 1).then([this]() {
// This callback runs when loading is complete
scenes->activate("game");
});
return true;
}
};
The .then() method starts a coroutine that waits until the preload finishes, then executes your callback. This is the recommended pattern for any scene that takes noticeable time to load.
Checking Load State
You can check if a scene is loaded at any time:
if (scenes->is_loaded("game")) {
scenes->activate("game");
} else {
// Still loading -- show a progress indicator
loading_bar_->set_progress(0.5f);
}
Checking if Activation is Pending
You can also check if a scene activation has been queued but not yet executed:
if (scenes->scene_queued_for_activation()) {
S_INFO("A scene activation is pending");
}
This is useful in the Application's init() to avoid double-activating:
bool init() override {
scenes->register_scene<MenuScene>("menu");
// Only activate if no activation is already queued
if (!scenes->scene_queued_for_activation()) {
scenes->activate("menu");
}
return true;
}
6. Passing Arguments to Scenes
There are two ways to pass data to scenes: constructor arguments (at registration time) and load-time arguments (at activation time).
Constructor Arguments (Registration Time)
As shown earlier, constructor arguments are passed when the scene factory creates the scene instance. These are fixed at registration time and cannot change between activations:
// Registered once with a fixed level number
scenes->register_scene<GameScene>("level1", 1);
// Every time level1 is activated, it uses level_number = 1
scenes->activate("level1");
scenes->activate("menu");
scenes->activate("level1"); // Same level_number = 1
Load-Time Arguments (Activation Time)
Any arguments passed to activate(), preload(), or preload_in_background() beyond the scene name are stored in the scene's load_args vector. The scene can access them via get_load_arg<T>(index):
class GameScene : public Scene {
public:
GameScene(Window* window)
: Scene(window) {}
void on_load() override {
// Read load-time arguments
if (load_arg_count() >= 2) {
int level = get_load_arg<int>(0);
std::string difficulty = get_load_arg<std::string>(1);
S_INFO("Starting level {} on {} difficulty", level, difficulty);
}
}
};
Pass the arguments when activating or preloading:
// Activate with load-time arguments
scenes->activate("game", 3, std::string("hard"));
// Preload with load-time arguments
scenes->preload("game", 5, std::string("easy"));
// Background preload with arguments
scenes->preload_in_background("game", 2, std::string("normal")).then([this]() {
scenes->activate("game");
});
When to Use Each Approach
| Use Case | Approach |
|---|---|
| Fixed properties that never change (e.g. base level number at registration) | Constructor arguments |
| Dynamic values that vary per activation (e.g. player score carried over) | Load-time arguments |
| A single scene type reused for many levels with different params | Constructor arguments per registration name |
| Runtime state passed between scenes (e.g. "coming from level 3") | Load-time arguments |
7. Scene Transitions and Transition Effects
Scene transitions are the moment the player moves from one scene to another. The SceneManager provides signals and activation behaviours that make it possible to implement smooth, visually pleasing transitions.
Using Activation Signals
The SceneManager fires two signals during every scene change:
// Connect in your Application or a dedicated transition manager
scenes->signal_scene_activated().connect(
[this](const std::string& route, Scene* scene) {
S_INFO("Scene activated: {}", route);
on_scene_entered(route);
}
);
scenes->signal_scene_deactivated().connect(
[this](const std::string& route, Scene* scene) {
S_INFO("Scene deactivated: {}", route);
on_scene_exited(route);
}
);
Implementing Fade Transitions
A common transition effect is a fade-to-black. This is typically done using a full-screen black UI element whose opacity is animated. The recommended approach is to use the ACTIVATE_BEHAVIOUR_UNLOAD_AFTER behaviour so that both the old and new scene pipelines remain active during the transition.
class TransitionManager {
public:
TransitionManager(Application* app)
: app_(app) {
app->scenes->signal_scene_deactivated().connect(
[this](const std::string&, Scene*) {
start_fade_out();
}
);
app->scenes->signal_scene_activated().connect(
[this](const std::string&, Scene*) {
start_fade_in();
}
);
}
private:
Application* app_;
ui::Image* fade_overlay_ = nullptr;
void start_fade_out() {
// Animate a black overlay to opacity 1.0 over 0.5 seconds
// Implementation depends on your animation system
}
void start_fade_in() {
// Animate the black overlay from opacity 1.0 to 0.0 over 0.5 seconds
}
};
Using a Loading Scene as a Transition
A common pattern is to activate a loading scene, preload the target in the background, and then activate the target when ready. The built-in Splash scene demonstrates this pattern:
bool init() override {
scenes->register_scene<Splash>("splash", std::string("menu"));
scenes->register_scene<MenuScene>("menu");
scenes->register_scene<GameScene>("game");
// Splash is the "main" scene -- it will auto-transition to "menu"
// No explicit activate needed, the engine activates "main" automatically
return true;
}
Cross-Fade Between Scenes
For a cross-fade (old scene fades out while new scene fades in behind it):
void crossfade_to(const std::string& target) {
cr_async([this, target]() {
// 1. Start fading out the current scene's overlay alpha
fade_overlay(1.0f, 0.5f);
cr_yield_for(0.5f);
// 2. Activate with UNLOAD_AFTER so both scenes are briefly active
app->scenes->activate(target, ACTIVATE_BEHAVIOUR_UNLOAD_AFTER);
// 3. Fade in the new scene
fade_overlay(0.0f, 0.5f);
cr_yield_for(0.5f);
});
}
8. Overlay Scenes (Global HUD, Pause Menus)
The Application provides a special overlay scene that is always rendered with the highest priority over all other scenes. This is designed for persistent UI elements like:
- FPS counters and debug information
- Performance graphs
- Game version text
- Global HUD elements that must appear on top of every scene
How the Overlay Works
The overlay scene is created automatically by the Application when the render context is established. It is an internal OverlayScene subclass of Scene that is always initialized and always rendered.
// The overlay scene is accessible via the `overlay` property
auto overlay = app->overlay;
Adding Elements to the Overlay
Because the overlay is a regular Scene, you can create StageNodes inside it:
class MyGame : public Application {
public:
MyGame(const AppConfig& config)
: Application(config) {}
bool init() override {
scenes->register_scene<MenuScene>("menu");
scenes->register_scene<GameScene>("game");
// Add a persistent FPS counter to the overlay
auto fps_label = overlay->create_node<ui::Label>("FPS: 60");
fps_label->set_anchor_point(1.0f, 1.0f);
fps_label->set_position_2d(
window->coordinate_from_normalized(0.98f, 0.98f)
);
// Connect to the frame signal to update it
signal_frame_finished().connect([this, fps_label]() {
float fps = time_keeper->fps();
fps_label->set_text("FPS: " + std::to_string((int)fps));
});
return true;
}
};
Using the Overlay for a Pause Menu
A pause menu can be implemented by adding UI nodes to the overlay scene and toggling their visibility:
class PauseMenu {
public:
PauseMenu(Application* app) {
auto overlay = app->overlay;
resume_btn_ = overlay->create_node<ui::Button>("Resume");
quit_btn_ = overlay->create_node<ui::Button>("Quit");
// Initially hidden
set_visible(false);
}
void show() {
resume_btn_->set_visible(true);
quit_btn_->set_visible(true);
paused_ = true;
}
void hide() {
resume_btn_->set_visible(false);
quit_btn_->set_visible(false);
paused_ = false;
}
private:
ui::Button* resume_btn_ = nullptr;
ui::Button* quit_btn_ = nullptr;
bool paused_ = false;
};
Overlay Rendering Priority
The overlay scene is always rendered last (highest priority), so anything you add to it will appear on top of all scene content. This makes it ideal for HUDs that must always be visible regardless of which scene is active.
For more on render priority, see Render Pipelines.
9. Background Loading Scenes
Background loading is the pattern where you show a loading screen while the next scene loads in the background. This prevents the player from staring at a frozen screen.
Basic Background Loading Pattern
class MyGame : public Application {
public:
bool init() override {
scenes->register_scene<LoadingScene>("loading");
scenes->register_scene<MenuScene>("menu");
// Show loading screen immediately
scenes->activate("loading");
// Load menu in the background
scenes->preload_in_background("menu").then([this]() {
scenes->activate("menu");
});
return true;
}
};
Loading Scene with Progress
A loading scene can monitor the load state of the target scene and display progress:
class LoadingScene : public Scene {
public:
LoadingScene(Window* window, std::string target)
: Scene(window), target_(std::move(target)) {}
private:
std::string target_;
ui::ProgressBar* progress_bar_ = nullptr;
ui::Label* status_label_ = nullptr;
void on_load() override {
auto camera = create_node<Camera2D>();
camera->set_orthographic_projection(
0, window->width(), 0, window->height()
);
pipeline_ = compositor->create_layer(this, camera);
progress_bar_ = create_node<ui::ProgressBar>(0.0f);
progress_bar_->set_anchor_point(0.5f, 0.5f);
progress_bar_->set_position_2d(
window->coordinate_from_normalized(0.5f, 0.5f)
);
status_label_ = create_node<ui::Label>("Loading...");
status_label_->set_anchor_point(0.5f, 0.4f);
status_label_->set_position_2d(
window->coordinate_from_normalized(0.5f, 0.45f)
);
}
void on_update(float dt) override {
Scene::on_update(dt);
// Check if the target scene has finished loading
if (scenes->is_loaded(target_)) {
status_label_->set_text("Ready!");
progress_bar_->set_progress(1.0f);
// Activate the target scene
scenes->activate(target_);
return;
}
// Update progress (simulated -- you'd need a way to track actual progress)
progress_bar_->set_progress(std::min(progress_bar_->get_progress() + dt * 0.3f, 0.95f));
}
void on_unload() override {
if (pipeline_) {
pipeline_->destroy();
}
}
LayerPtr pipeline_;
};
Using the Promise .then() Chain
The preload_in_background() method returns a Promise<void>, which can be chained:
scenes->preload_in_background("level_2").then([this]() {
S_INFO("Level 2 is loaded");
scenes->activate("level_2");
});
Multiple preloads can be queued:
scenes->preload_in_background("level_1").then([this]() {
scenes->preload_in_background("level_2");
scenes->preload_in_background("level_3");
scenes->activate("level_1");
});
10. Scene Persistence Across Transitions
By default, scenes are unloaded when they are deactivated. However, you may want certain scenes to persist across transitions.
unload_on_deactivate Property
Every scene has an unload_on_deactivate flag (default: true). When set to false, the scene remains loaded after deactivation:
class GlobalMusicScene : public Scene {
public:
GlobalMusicScene(Window* window)
: Scene(window) {
// Keep this scene loaded even when deactivated
set_unload_on_deactivate(false);
}
void on_load() override {
// Load music assets and set up audio
music_ = assets->load_sound("background_music.ogg");
source_ = create_node<AudioSource>();
}
void on_activate() override {
source_->play_sound(music_, AUDIO_REPEAT_LOOP, DISTANCE_MODEL_AMBIENT);
}
void on_deactivate() override {
// Don't stop the music -- let it keep playing
// The scene stays loaded in memory
}
private:
SoundPtr music_;
AudioSource* source_ = nullptr;
};
Re-activating a Persistent Scene
When you re-activate a scene that was not unloaded, only on_activate() is called -- on_load() is skipped because the scene is already loaded:
// First activation: on_load() -> on_activate()
scenes->activate("music");
// Deactivating (e.g., by activating another scene):
// on_deactivate() is called, but NOT on_unload()
// Re-activating: only on_activate() is called
scenes->activate("music"); // Fast -- no loading needed
Manually Controlling Persistence
You can also manually control persistence by toggling the flag at runtime:
// Make the current scene persist
scenes->active_scene()->set_unload_on_deactivate(true);
// Later, when you want to free it:
scenes->active_scene()->set_unload_on_deactivate(false);
11. Managing Scenes with Pipelines
Scenes and render pipelines are tightly coupled. Each scene typically creates one or more Layer objects through its compositor. Understanding how pipelines interact with scene transitions is important for advanced rendering.
Pipeline Lifecycle
When a scene is unloaded, you are responsible for destroying its pipelines:
void on_unload() override {
if (pipeline_) {
pipeline_->destroy();
pipeline_ = nullptr;
}
}
Pipelines are not destroyed automatically because they may need to outlive the scene (e.g., during transitions).
Multiple Pipelines Per Scene
A single scene can have multiple pipelines for different rendering purposes:
void on_load() override {
// Main 3D camera
camera3d_ = create_node<Camera3D>();
// Main 3D pipeline
main_pipeline_ = compositor->create_layer(this, camera3d_);
main_pipeline_->set_background_color(Color(0.1f, 0.1f, 0.2f, 1.0f));
// 2D UI camera
camera2d_ = create_node<Camera2D>();
camera2d_->set_orthographic_projection(0, window->width(), 0, window->height());
// UI pipeline with higher priority (draws on top)
ui_pipeline_ = compositor->create_layer(this, camera2d_);
ui_pipeline_->set_priority(RENDER_PRIORITY_FOREGROUND);
}
Keeping Pipelines Active During Transitions
When using ACTIVATE_BEHAVIOUR_UNLOAD_AFTER, the old scene's pipelines remain active while the new scene loads and activates. This allows you to implement effects like:
void on_activate() override {
// Use UNLOAD_AFTER to keep the previous scene's pipeline briefly active
scenes->activate("game", ACTIVATE_BEHAVIOUR_UNLOAD_AFTER);
// At this point, both old and new scenes may have active pipelines
// The compositor will render both, sorted by priority
}
Off-Screen Render Targets
Scenes can render to off-screen textures for effects like mirrors, security cameras, or picture-in-picture:
void on_load() override {
// Create a texture to render into
mirror_texture_ = window->create_texture(256, 256);
// Create a pipeline that renders to the texture instead of the screen
auto mirror_pipeline = compositor->create_layer(
this,
mirror_camera_,
Viewport(0, 0, 1.0f, 1.0f), // Full viewport
mirror_texture_ // Off-screen target
);
}
For a complete guide to pipelines and layers, see Render Pipelines.
12. Complete Examples
Example 1: Menu to Game Transition
A complete flow from main menu to gameplay with proper loading:
// --- MenuScene ---
class MenuScene : public Scene {
public:
MenuScene(Window* window)
: Scene(window) {}
private:
LayerPtr pipeline_;
CameraPtr camera_;
ui::Button* play_button_ = nullptr;
void on_load() override {
camera_ = create_node<Camera2D>();
camera_->set_orthographic_projection(
0, window->width(), 0, window->height()
);
pipeline_ = compositor->create_layer(this, camera_);
pipeline_->set_background_color(Color(0.0f, 0.0f, 0.0f, 1.0f));
// Title
auto title = create_node<ui::Label>("My Awesome Game");
title->set_anchor_point(0.5f, 0.8f);
title->set_position_2d(
window->coordinate_from_normalized(0.5f, 0.8f)
);
// Play button
play_button_ = create_node<ui::Button>("Play");
play_button_->set_anchor_point(0.5f, 0.5f);
play_button_->set_position_2d(
window->coordinate_from_normalized(0.5f, 0.5f)
);
}
void on_update(float dt) override {
Scene::on_update(dt);
if (play_button_->is_pressed()) {
// Start loading the game scene in background
scenes->preload_in_background("game", 1).then([this]() {
scenes->activate("game");
});
}
}
void on_unload() override {
if (pipeline_) {
pipeline_->destroy();
}
}
};
// --- GameScene ---
class GameScene : public Scene {
public:
GameScene(Window* window, int level)
: Scene(window), level_(level) {}
private:
int level_;
LayerPtr pipeline_;
CameraPtr camera_;
void on_load() override {
S_INFO("Loading level {}", level_);
// Load level mesh
std::string mesh_file = "levels/level_" + std::to_string(level_) + ".glb";
auto mesh = assets->load_mesh(mesh_file);
create_node<Actor>(mesh);
camera_ = create_node<Camera3D>();
camera_->transform->set_position(0, 10, -15);
pipeline_ = compositor->create_layer(this, camera_);
}
void on_activate() override {
S_INFO("Level {} activated", level_);
}
void on_deactivate() override {
S_INFO("Level {} deactivated", level_);
}
void on_update(float dt) override {
Scene::on_update(dt);
// Press Escape to return to menu
if (input->key_just_pressed(KEY_ESCAPE)) {
scenes->activate("menu");
}
}
void on_unload() override {
if (pipeline_) {
pipeline_->destroy();
}
}
};
// --- Application ---
class MyGame : public Application {
public:
MyGame(const AppConfig& config)
: Application(config) {}
bool init() override {
scenes->register_scene<MenuScene>("menu");
scenes->register_scene<GameScene>("game", 1);
// Menu is "main", so it activates automatically
return true;
}
};
Example 2: Level Loading with Arguments
Demonstrates passing level number and difficulty at activation time:
class GameScene : public Scene {
public:
GameScene(Window* window)
: Scene(window) {}
private:
int current_level_ = 0;
std::string difficulty_ = "normal";
LayerPtr pipeline_;
CameraPtr camera_;
void on_load() override {
// Read the arguments passed at activation time
if (load_arg_count() >= 2) {
current_level_ = get_load_arg<int>(0);
difficulty_ = get_load_arg<std::string>(1);
}
S_INFO("Loading level {} on {} difficulty", current_level_, difficulty_);
std::string mesh_file = "levels/level_" + std::to_string(current_level_) + ".glb";
auto mesh = assets->load_mesh(mesh_file);
create_node<Actor>(mesh);
camera_ = create_node<Camera3D>();
camera_->transform->set_position(0, 10, -15);
pipeline_ = compositor->create_layer(this, camera_);
}
void on_activate() override {
// Start preloading the next level
int next_level = current_level_ + 1;
scenes->preload_in_background("game", next_level, difficulty_)
.then([this, next_level]() {
S_INFO("Level {} is preloaded", next_level);
});
}
void on_update(float dt) override {
Scene::on_update(dt);
// Right arrow goes to next level
if (input->key_just_pressed(KEY_RIGHT)) {
int next = current_level_ + 1;
scenes->activate("game", next, difficulty_);
}
// Left arrow goes to previous level
if (input->key_just_pressed(KEY_LEFT) && current_level_ > 1) {
int prev = current_level_ - 1;
scenes->activate("game", prev, difficulty_);
}
// Escape returns to menu
if (input->key_just_pressed(KEY_ESCAPE)) {
scenes->activate("menu");
}
}
void on_unload() override {
if (pipeline_) {
pipeline_->destroy();
}
}
};
Example 3: Background Loading with Loading Screen
A complete background loading pattern with progress display:
// --- LoadingScene ---
class LoadingScene : public Scene {
public:
LoadingScene(Window* window, std::string target)
: Scene(window), target_(std::move(target)) {}
private:
std::string target_;
LayerPtr pipeline_;
CameraPtr camera_;
ui::Label* status_label_ = nullptr;
void on_load() override {
camera_ = create_node<Camera2D>();
camera_->set_orthographic_projection(
0, window->width(), 0, window->height()
);
pipeline_ = compositor->create_layer(this, camera_);
pipeline_->set_background_color(Color(0.05f, 0.05f, 0.1f, 1.0f));
status_label_ = create_node<ui::Label>("Loading...");
status_label_->set_anchor_point(0.5f, 0.5f);
status_label_->set_position_2d(
window->coordinate_from_normalized(0.5f, 0.5f)
);
}
void on_activate() override {
status_label_->set_text("Loading " + target_ + "...");
}
void on_update(float dt) override {
Scene::on_update(dt);
if (scenes->is_loaded(target_)) {
status_label_->set_text("Ready!");
scenes->activate(target_);
}
}
void on_unload() override {
if (pipeline_) {
pipeline_->destroy();
}
}
};
// --- Application ---
class MyGame : public Application {
public:
MyGame(const AppConfig& config)
: Application(config) {}
bool init() override {
scenes->register_scene<LoadingScene>("loading", std::string("game"));
scenes->register_scene<GameScene>("game");
scenes->register_scene<MenuScene>("menu");
// The engine will activate "main" automatically.
// If "main" is not registered, activate "loading" manually:
// scenes->activate("loading");
return true;
}
};
Example 4: Physics Sample Pattern (from samples/boxes.cpp)
A common pattern used in the engine's own samples:
class PhysicsDemo : public Application {
public:
PhysicsDemo(const AppConfig& config)
: Application(config) {}
private:
bool init() override {
scenes->register_scene<GameScene>("main");
// Show a loading screen while the main scene loads in background
scenes->activate("_loading");
scenes->preload_in_background("main").then([this]() {
// Loading complete -- switch to main
scenes->activate("main");
});
return true;
}
};
Quick Reference
| Task | Code |
|---|---|
| Register a scene | scenes->register_scene<MyScene>("name") |
| Register with constructor args | scenes->register_scene<GameScene>("game", 3, true) |
| Activate (default behaviour) | scenes->activate("name") |
| Activate with UNLOAD_FIRST | scenes->activate("name", ACTIVATE_BEHAVIOUR_UNLOAD_FIRST) |
| Activate with UNLOAD_AFTER | scenes->activate("name", ACTIVATE_BEHAVIOUR_UNLOAD_AFTER) |
| Activate with load-time args | scenes->activate("name", arg1, arg2) |
| Preload synchronously | scenes->preload("name") |
| Preload in background | scenes->preload_in_background("name").then([&]() { ... }) |
| Check if loaded | scenes->is_loaded("name") |
| Check if activation pending | scenes->scene_queued_for_activation() |
| Unload a scene | scenes->unload("name") |
| Unregister a scene | scenes->unregister_scene("name") |
| Destroy all scenes | scenes->destroy_all() |
| Reset everything | scenes->reset() |
| Get active scene | scenes->active_scene() |
| Set unload on deactivate | scene->set_unload_on_deactivate(false) |
| Scene activation signal | scenes->signal_scene_activated().connect(...) |
| Scene deactivation signal | scenes->signal_scene_deactivated().connect(...) |
| Access overlay | app->overlay |
| Read load arg in scene | get_load_arg<int>(0) |
| Count load args | load_arg_count() |
See Also
- Scenes -- Scene lifecycle methods, properties, and creating stage nodes
- Application -- Application entry point, AppConfig, and accessing the SceneManager
- Render Pipelines -- Layers, compositor, render priority, and multi-camera setups
- Signals -- Scene transition signals and event-driven programming
- Coroutines -- Using
cr_async()andPromise::then()for background loading