Scene Management Guide¶
Learn how to organize your game into scenes using the Citrus Engine Scene Management system.
Table of Contents¶
- Overview
- Quick Start
- Core Concepts
- Creating Scenes
- Managing Entities
- Scene Hierarchies
- Scene Lifecycle
- Multi-Scene Workflows
- Saving and Loading
- Prefabs
- Best Practices
Overview¶
The Scene Management system provides a way to organize your game entities into logical groups (scenes). Each scene can represent a level, menu, game state, or any other logical grouping you need.
Key Features: - 🌳 Hierarchical organization - Parent-child entity relationships - 💾 Scene serialization - Save/load complete scenes to JSON - 🧩 Prefab system - Reusable entity templates with inheritance - 🎨 Multi-scene support - Run multiple scenes simultaneously (e.g., game + UI) - ⚡ ECS-native - Built on Flecs, seamlessly integrates with your ECS code - 🔄 Lifecycle callbacks - Initialize, Update, Render, Shutdown hooks
Quick Start¶
#include <engine/scene/scene.cppm>
#include <engine/scene/scene_manager.cpp>
using namespace engine::scene;
// Get the scene manager (singleton)
auto& mgr = GetSceneManager();
// Create a new scene
SceneId scene_id = mgr.CreateScene("Level1");
Scene& scene = mgr.GetScene(scene_id);
// Configure scene settings
scene.SetBackgroundColor({0.2f, 0.3f, 0.4f, 1.0f});
scene.SetAmbientLight({0.1f, 0.1f, 0.1f, 1.0f});
// Create entities
auto player = scene.CreateEntity("Player");
player.set<Transform>({.position = {0.0f, 0.0f, 0.0f}});
player.set<Renderable>({/* ... */});
// Activate the scene
mgr.SetActiveScene(scene_id);
// In your game loop
mgr.Update(delta_time);
mgr.Render();
Core Concepts¶
Scene Root Pattern¶
Every scene has a root entity. All entities in the scene are descendants of this root via Flecs' ChildOf relationship.
Scene& scene = mgr.GetScene(scene_id);
auto root = scene.GetSceneRoot(); // Returns the root entity
// All scene entities are descendants
auto player = scene.CreateEntity("Player");
// player has ChildOf relationship to scene root
Scenes Organize, Don't Own¶
Important: Scenes don't "own" entities. Entities belong to the ECS world. Scenes provide logical grouping through parent-child relationships.
This means: - ✅ You can create entities directly in the ECS world without a scene - ✅ Entities from different scenes can interact - ✅ ECS systems run globally across all scenes - ⚠️ Destroying a scene destroys all its entities
Multi-Scene Architecture¶
You can have one active scene and multiple additional scenes:
// Primary scene (game world)
mgr.SetActiveScene(game_scene_id);
// Overlay scenes (UI, debug tools)
mgr.ActivateAdditionalScene(ui_scene_id);
mgr.ActivateAdditionalScene(debug_scene_id);
// All scenes receive Update() and Render()
Use cases: - Game + HUD - Keep UI separate from game world - Game + Pause Menu - Overlay pause menu without unloading game - Game + Debug Console - Persistent debug overlay
Creating Scenes¶
Method 1: Programmatically¶
auto& mgr = GetSceneManager();
// Create an empty scene
SceneId id = mgr.CreateScene("MyScene");
Scene& scene = mgr.GetScene(id);
// Configure settings
scene.SetBackgroundColor({0.5f, 0.7f, 1.0f, 1.0f}); // Sky blue
scene.SetAmbientLight({0.2f, 0.2f, 0.2f, 1.0f}); // Dark gray
scene.SetPhysicsBackend("jolt"); // Physics engine
// Set lifecycle callbacks
scene.SetInitializeCallback([]() {
CE_LOG_INFO("Scene initialized!");
});
scene.SetUpdateCallback([](float delta_time) {
// Per-frame logic
});
scene.SetRenderCallback([]() {
// Custom rendering
});
scene.SetShutdownCallback([]() {
CE_LOG_INFO("Scene shutting down!");
});
Method 2: Load from File¶
// Load a scene from JSON
SceneId id = mgr.LoadSceneFromFile("assets/scenes/level1.scene.json");
Scene& scene = mgr.GetScene(id);
// Activate it
mgr.SetActiveScene(id);
Scene Metadata¶
scene.SetName("Forest Level");
scene.SetDescription("The player explores a dark forest");
scene.SetAuthor("Your Name");
Managing Entities¶
Creating Entities¶
Scene& scene = mgr.GetScene(scene_id);
// Simple entity
auto entity = scene.CreateEntity("Player");
// With components
auto enemy = scene.CreateEntity("Enemy");
enemy.set<Transform>({.position = {10.0f, 0.0f, 0.0f}});
enemy.set<Health>({.current = 100, .max = 100});
enemy.set<Renderable>({/* ... */});
// With parent (creates hierarchy)
auto weapon = scene.CreateEntity("Weapon", enemy);
weapon.set<Transform>({.position = {0.0f, 1.5f, 0.5f}}); // Relative to enemy
Querying Entities¶
// Get all entities in the scene
auto all_entities = scene.GetAllEntities();
// Get entities by name
auto player = scene.GetEntityByName("Player");
// Spatial queries
auto at_point = scene.QueryPoint(glm::vec3(5.0f, 0.0f, 5.0f));
auto in_sphere = scene.QuerySphere(
glm::vec3(0.0f, 0.0f, 0.0f), // Center
10.0f, // Radius
LayerMask::Default // Layer filter
);
Destroying Entities¶
// Destroy a single entity
scene.DestroyEntity(entity);
// Destroy all entities in the scene
scene.Clear();
Scene Hierarchies¶
Hierarchies use Flecs' native ChildOf relationship. Child transforms are relative to their parent.
Creating Hierarchies¶
// Create parent
auto tank = scene.CreateEntity("Tank");
tank.set<Transform>({.position = {10.0f, 0.0f, 0.0f}});
// Create child (Method 1: pass parent to CreateEntity)
auto turret = scene.CreateEntity("Turret", tank);
turret.set<Transform>({.position = {0.0f, 2.0f, 0.0f}}); // Relative to tank
// Create child (Method 2: SetParent)
auto barrel = scene.CreateEntity("Barrel");
barrel.set<Transform>({.position = {0.0f, 0.5f, 1.0f}});
scene.SetParent(barrel, turret);
// Hierarchy:
// Tank (world position: 10, 0, 0)
// └─ Turret (world position: 10, 2, 0)
// └─ Barrel (world position: 10, 2.5, 1)
Navigating Hierarchies¶
// Get immediate children
auto children = scene.GetChildren(tank);
for (auto child : children) {
CE_LOG_INFO("Child: {}", child.name().c_str());
}
// Get all descendants (recursive)
auto descendants = scene.GetDescendants(tank);
// Returns: [turret, barrel]
// Get parent
auto parent = scene.GetParent(barrel);
// Returns: turret
Transform Spaces¶
// Local transform (relative to parent)
auto local = turret.get<Transform>();
// local->position = {0, 2, 0}
// World transform (absolute position)
auto world = turret.get<WorldTransform>();
// world->position = {10, 2, 0}
// Transform system automatically updates WorldTransform
// when Transform or parent changes
Scene Lifecycle¶
Scenes follow this lifecycle:
State Transitions¶
// 1. Create
SceneId id = mgr.CreateScene("Level1");
// State: Created
// 2. Load assets
mgr.LoadScene(id);
// State: Loaded
// → Shaders compiled, textures loaded
// 3. Activate
mgr.SetActiveScene(id);
// State: Active
// → Initialize() callback invoked
// → Receives Update() and Render() calls
// 4. Deactivate
mgr.DeactivateScene(id);
// State: Inactive
// → Shutdown() callback invoked
// → No longer receives updates
// 5. Unload
mgr.UnloadScene(id);
// State: Unloaded
// → GPU resources freed
// 6. Destroy
mgr.DestroyScene(id);
// State: Destroyed
// → All entities deleted
// → Scene removed from manager
Lifecycle Callbacks¶
scene.SetInitializeCallback([]() {
// Called once when scene becomes active
// Setup initial state, spawn entities, etc.
CE_LOG_INFO("Scene starting");
});
scene.SetUpdateCallback([](float delta_time) {
// Called every frame while scene is active
// Game logic, AI, physics, etc.
});
scene.SetRenderCallback([]() {
// Called every frame while scene is active
// Custom rendering (most rendering is automatic)
});
scene.SetShutdownCallback([]() {
// Called once when scene is deactivated
// Cleanup, save state, etc.
CE_LOG_INFO("Scene ending");
});
Multi-Scene Workflows¶
Example: Game + HUD¶
Keep your UI separate from the game world:
// Create game world scene
SceneId game_id = mgr.CreateScene("GameWorld");
Scene& game = mgr.GetScene(game_id);
// ... populate with 3D entities
// Create UI scene
SceneId ui_id = mgr.CreateScene("HUD");
Scene& ui = mgr.GetScene(ui_id);
// ... populate with UI elements
// Activate both
mgr.SetActiveScene(game_id); // Primary scene
mgr.ActivateAdditionalScene(ui_id); // Overlay
// Both receive updates
mgr.Update(delta_time); // Updates game AND ui
mgr.Render(); // Renders game, then ui
Example: Pause Menu¶
Overlay a pause menu without unloading the game:
// Game is running
mgr.SetActiveScene(game_id);
// Player presses pause
SceneId pause_id = mgr.CreateScene("PauseMenu");
// ... create pause menu UI
mgr.ActivateAdditionalScene(pause_id);
// Game keeps rendering (frozen), pause menu renders on top
// Player unpauses
mgr.DeactivateScene(pause_id);
mgr.DestroyScene(pause_id);
Example: Level Transitions¶
Smoothly transition between levels:
// Fade out current level
StartFadeOut();
// Wait for fade to complete (async or in update loop)
WaitForFade();
// Switch scenes
mgr.DeactivateScene(current_level_id);
mgr.UnloadScene(current_level_id);
mgr.DestroyScene(current_level_id);
SceneId next_level_id = mgr.LoadSceneFromFile("scenes/level2.scene.json");
mgr.SetActiveScene(next_level_id);
// Fade in new level
StartFadeIn();
Saving and Loading¶
Save Scene to JSON¶
Scene& scene = mgr.GetScene(scene_id);
// Save the entire scene
bool success = mgr.SaveScene(scene_id, "assets/scenes/my_level.scene.json");
if (!success) {
CE_LOG_ERROR("Failed to save scene");
}
Load Scene from JSON¶
// Load scene from file
SceneId id = mgr.LoadSceneFromFile("assets/scenes/my_level.scene.json");
if (id == InvalidSceneId) {
CE_LOG_ERROR("Failed to load scene");
return;
}
// Activate it
mgr.SetActiveScene(id);
What Gets Saved?¶
- ✅ Scene metadata (name, description, author)
- ✅ Scene settings (background color, ambient light, physics backend)
- ✅ Asset definitions (shaders, meshes, textures)
- ✅ All entities and their components
- ✅ Entity hierarchies (parent-child relationships)
- ✅ Active camera reference
- ✅ World singletons (physics configuration)
Scene JSON Format¶
{
"version": 1,
"name": "Forest Level",
"description": "Dark forest exploration",
"author": "Your Name",
"settings": {
"background_color": [0.2, 0.3, 0.4, 1.0],
"ambient_light": [0.1, 0.1, 0.1, 1.0],
"physics_backend": "jolt"
},
"assets": [
{
"type": "shader",
"name": "basic_shader",
"vertex_path": "shaders/basic.vert",
"fragment_path": "shaders/basic.frag"
},
{
"type": "mesh",
"name": "ground",
"mesh_type": "quad",
"params": [100.0, 100.0, 1.0]
}
],
"flecs_data": "[{\"path\": \"::Scene_Root::Player\", ...}]",
"active_camera": "::Scene_Root::MainCamera"
}
Snapshot/Restore (Editor Play Mode)¶
Use snapshots to save and restore scene state without file I/O:
#include <engine/scene/scene_serializer.cppm>
// Capture current state
std::string snapshot = SceneSerializer::SnapshotEntities(scene, ecs_world);
// ... modify scene (e.g., play mode in editor)
// Restore original state
SceneSerializer::RestoreEntities(snapshot, scene, ecs_world);
Prefabs¶
Prefabs are reusable entity templates using Flecs' native prefab system with is_a() inheritance.
Creating Prefabs¶
#include <engine/scene/prefab.cppm>
// Create a template entity
auto enemy_template = ecs_world.CreateEntity("EnemyTemplate");
enemy_template.set<Transform>({.position = {0, 0, 0}});
enemy_template.set<Health>({.current = 100, .max = 100});
enemy_template.set<AI>({.type = AIType::Aggressive});
// Save as prefab
auto prefab = PrefabUtility::SaveAsPrefab(
enemy_template,
ecs_world,
"assets/prefabs/enemy.prefab.json"
);
// Template entity is now converted to an instance!
Instantiating Prefabs¶
// Load and instantiate
auto enemy1 = PrefabUtility::InstantiatePrefab(
"assets/prefabs/enemy.prefab.json",
&scene,
ecs_world,
{} // No parent
);
// Override components
enemy1.set<Transform>({.position = {10, 0, 0}});
// Instance inherits other components from prefab
auto health = enemy1.get<Health>(); // Still 100/100
Multiple Instances¶
// Spawn 10 enemies
for (int i = 0; i < 10; i++) {
auto enemy = PrefabUtility::InstantiatePrefab(
"assets/prefabs/enemy.prefab.json",
&scene,
ecs_world,
{}
);
// Position each uniquely
enemy.set<Transform>({.position = {i * 5.0f, 0, 0}});
}
Prefab Inheritance¶
Instances use Flecs' is_a() relationship for component inheritance:
// Instance inherits from prefab
auto enemy = PrefabUtility::InstantiatePrefab(...);
// Component queries automatically resolve inheritance
auto health = enemy.get<Health>();
// Returns prefab's Health if not overridden, or instance's Health if overridden
// Override a component
enemy.set<Health>({.current = 50, .max = 100});
// Now this instance has its own Health component
Apply Changes to Prefab Source¶
// Modify an instance
auto enemy = PrefabUtility::InstantiatePrefab(...);
enemy.set<Health>({.current = 150, .max = 150}); // Buff enemies!
// Apply changes back to prefab
PrefabUtility::ApplyToSource(enemy, ecs_world);
// All future instances will have 150 health
Prefab JSON Format¶
Best Practices¶
✅ Do¶
- Use scenes for logical grouping - Levels, menus, game states
- Keep hierarchies shallow - Deep hierarchies are expensive to update
- Use multi-scene for UI - Separate game world from UI overlays
- Use prefabs for repeated entities - Memory efficient, easier to maintain
- Save scenes often - Version control your scene files
- Use lifecycle callbacks - Clean initialization and shutdown
- Name entities clearly - Makes debugging and serialization easier
❌ Don't¶
- Don't create scenes for temporary entities - Use ECS directly for bullets, particles
- Don't nest scenes - Scenes are top-level, not nestable
- Don't store gameplay logic in scenes - Use ECS systems instead
- Don't assume scene isolation - Entities from different scenes can interact
- Don't forget to unload - Free GPU resources when done with scenes
Performance Tips¶
- Spatial queries - Use
QuerySphere()instead of iterating all entities - Transform updates - Only modified transforms are recomputed (dirty flag)
- Batch entity creation - Create all entities at once, then set components
- Prefab instances - Share component data across many instances
- Scene-scoped queries - Filter ECS queries by scene membership
Organization Patterns¶
Simple Game:
Complex Game:
- MainMenu (scene)
- InGameWorld (scene)
- InGameUI (scene, overlay)
- PauseMenu (scene, conditional overlay)
- Settings (scene, conditional overlay)
Editor:
Complete Example¶
Putting it all together:
#include <engine/scene/scene.cppm>
#include <engine/scene/scene_manager.cpp>
#include <engine/scene/prefab.cppm>
using namespace engine::scene;
void SetupGame() {
auto& mgr = GetSceneManager();
// Create main game scene
SceneId game_id = mgr.CreateScene("ForestLevel");
Scene& game = mgr.GetScene(game_id);
// Configure scene
game.SetBackgroundColor({0.5f, 0.7f, 1.0f, 1.0f}); // Sky blue
game.SetAmbientLight({0.2f, 0.2f, 0.2f, 1.0f});
// Add assets
ShaderAsset shader;
shader.name = "basic";
shader.vertex_path = "shaders/basic.vert";
shader.fragment_path = "shaders/basic.frag";
game.AddAsset(shader);
// Create player
auto player = game.CreateEntity("Player");
player.set<Transform>({.position = {0, 0, 0}});
player.set<Renderable>({/* ... */});
player.set<PlayerController>({/* ... */});
// Create enemies from prefab
for (int i = 0; i < 5; i++) {
auto enemy = PrefabUtility::InstantiatePrefab(
"prefabs/enemy.prefab.json",
&game,
ecs_world,
{}
);
enemy.set<Transform>({.position = {i * 10.0f, 0, 20.0f}});
}
// Create camera
auto camera = game.CreateEntity("MainCamera");
camera.set<Transform>({.position = {0, 5, -10}});
camera.set<Camera>({/* ... */});
// Create UI overlay
SceneId ui_id = mgr.CreateScene("HUD");
Scene& ui = mgr.GetScene(ui_id);
auto health_bar = ui.CreateEntity("HealthBar");
health_bar.set<UIElement>({/* ... */});
// Activate both scenes
mgr.SetActiveScene(game_id);
mgr.ActivateAdditionalScene(ui_id);
// Save for later
mgr.SaveScene(game_id, "scenes/forest_level.scene.json");
}
void GameLoop(float delta_time) {
auto& mgr = GetSceneManager();
// Update all active scenes
mgr.Update(delta_time);
// Render all active scenes
mgr.Render();
}
void LoadLevel(const std::string& path) {
auto& mgr = GetSceneManager();
// Unload current level
if (current_level_id != InvalidSceneId) {
mgr.DeactivateScene(current_level_id);
mgr.UnloadScene(current_level_id);
mgr.DestroyScene(current_level_id);
}
// Load new level
current_level_id = mgr.LoadSceneFromFile(path);
mgr.SetActiveScene(current_level_id);
}
Next Steps¶
- Read the Architecture Overview to understand the ECS architecture
- Check Asset System for loading shaders, textures, and resources
- Explore Physics API to add physics simulation to your scenes
- See Audio API for adding sound effects and music
- Browse the API Reference for detailed class documentation
For detailed API reference, see the generated Doxygen documentation.