Repository navigation
Scene Management
The SceneManager handles loading, saving, and manipulating scenes at runtime. Scenes are stored as JSON files with a hierarchical node structure. The SceneManager is the single point of truth for the live scene graph.
stable-v1 support boundary:
stable-v1is blocked and uncertified; its exact host is Windows 11 x64. This page describes source-level scene behavior, including paths that are outside that profile, and does not certify a release or platform.
Source: SparkEngine/Source/SceneManager/SceneManager.h
The SceneManager owns:
- A flat list of
SceneNodeobjects (m_sceneNodes) that define the hierarchy - The corresponding instantiated
GameObjectobjects used at runtime - Global scene settings via
SceneMetadata
| Responsibility | Description |
|---|---|
| Serialization | JSON and legacy binary round-trip via LoadJSON/SaveJSON/LoadCustom
|
| Hierarchy management | Add, remove, reparent nodes; maintain index invariants |
| Prefab system | Save/load subtrees as reusable prefab assets |
| Async loading | Background scene transitions via LoadSceneAsync
|
| Console integration | Runtime inspection and manipulation from the debug console |
| Dirty tracking | Tracks unsaved changes for editor "Save changes?" prompts |
Scenes use a JSON format (.scene or .json):
{
"metadata": {
"sceneName": "Level01",
"author": "Developer",
"version": "1.0",
"description": "First level of the game",
"ambientLightR": 0.1,
"ambientLightG": 0.1,
"ambientLightB": 0.15,
"gravityX": 0.0,
"gravityY": -9.81,
"gravityZ": 0.0
},
"nodes": [
{
"name": "Ground",
"type": "plane",
"position": [0, 0, 0],
"rotation": [0, 0, 0],
"scale": [10, 1, 10],
"parentIndex": -1
},
{
"name": "Box",
"type": "cube",
"position": [0, 1, 0],
"rotation": [0, 45, 0],
"scale": [1, 1, 1],
"parentIndex": 0
}
]
}The SceneMetadata struct holds global scene settings:
struct SceneMetadata
{
std::string sceneName; // Display name ("Level01")
std::string author; // Author (informational)
std::string version; // Format version ("1.0")
std::string description; // Human-readable description
float ambientLightR = 0.1f; // Ambient light red [0,1]
float ambientLightG = 0.1f; // Ambient light green [0,1]
float ambientLightB = 0.1f; // Ambient light blue [0,1]
float gravityX = 0.0f; // Gravity X (m/s^2)
float gravityY = -9.81f; // Gravity Y (m/s^2), default Earth
float gravityZ = 0.0f; // Gravity Z (m/s^2)
};The ambient light fields control the global ambient pass in the renderer. The gravity fields are forwarded to the Jolt physics world on scene load.
Each object in a scene is represented as a SceneNode:
struct SceneNode
{
std::string name; // Unique name within the scene
std::string type; // Node type (see table below)
XMFLOAT3 position; // World-space position
XMFLOAT3 rotation; // Euler rotation (degrees)
XMFLOAT3 scale; // Scale multiplier (default 1,1,1)
std::string modelPath; // Mesh asset path (for "model" type)
std::string materialPath; // Material override path
int parentIndex; // Parent index (-1 = root)
std::vector<int> childIndices; // Direct children
std::unordered_map<std::string, std::string> properties; // Custom key-value pairs
};| Type | Description | Instantiation |
|---|---|---|
"cube" |
Unit cube mesh with optional material | Primitives::CreateCube() |
"sphere" |
Unit sphere mesh with optional material | Primitives::CreateSphere() |
"plane" |
Flat plane mesh (XZ-aligned) | Primitives::CreatePlane() |
"model" |
Mesh requested from modelPath
|
.obj through Mesh/tinyobjloader; unsupported, missing, or failed paths use a placeholder mesh |
"light" |
Light source (point, directional, spot) | Creates LightComponent
|
"trigger" |
Invisible trigger volume | Creates ColliderComponent (trigger) |
Unrecognized types are silently skipped during InstantiateNodes() for forward compatibility.
Nodes can carry arbitrary key-value properties:
node.properties["health"] = "100";
node.properties["faction"] = "enemy";
node.properties["lightType"] = "directional";
node.properties["intensity"] = "1.5";
node.properties["spawnDelay"] = "3.0";Properties are round-tripped through JSON unchanged. Game code reads them to configure gameplay behavior.
SceneManager sceneMgr(&graphicsEngine, &inputManager);
if (!sceneMgr.LoadScene(L"Assets/Scenes/Level01.scene")) {
LOG_ERROR("Failed to load scene");
}File format is determined by extension:
-
.scene-- legacy binary format (LoadCustom) -
.jsonor anything else -- JSON format (LoadJSON)
Load scenes on a background thread to avoid hitches during transitions:
sceneMgr.LoadSceneAsync(L"Assets/Scenes/Level02.scene",
[](bool success, const std::string& sceneName) {
if (success) {
LOG("Loaded: " + sceneName);
} else {
LOG_ERROR("Failed to load: " + sceneName);
}
});Warning: Do not call other SceneManager methods while an async load is in flight, as node data may be partially overwritten. The transition to the new scene (clearing the old one and instantiating nodes) happens on the main thread at the start of the next frame after loading is done.
LoadScene(filepath)
│
├── Clear existing scene (destroy GameObjects, clear nodes)
│
├── Determine format from extension
│ ├── .scene → LoadCustom(filepath) [legacy binary]
│ └── .json → LoadJSON(filepath) [JSON]
│
├── Parse file into m_metadata + m_sceneNodes
│
├── InstantiateNodes()
│ └── For each node: create GameObject based on type
│ ├── cube/sphere/plane → Primitives factory
│ ├── model → LoadOrPlaceholderMesh(modelPath): OBJ or placeholder
│ ├── light → Create LightComponent
│ └── trigger → Create ColliderComponent (isTrigger=true)
│
├── Clear dirty flag
│
└── Publish SceneLoadedEvent via EventBus
Nodes form a parent-child hierarchy using integer indices:
-
parentIndex == -1denotes a root node - Each node maintains
childIndicesfor efficient tree traversal - Parent-child relationships are encoded by index rather than pointers so the node vector can be freely resized
// Get all root nodes
for (int root : sceneMgr.GetRootNodes()) {
const SceneNode* node = sceneMgr.GetNode(root);
LOG("Root: " + node->name);
}
// Navigate children recursively
void PrintHierarchy(const SceneManager& mgr, int index, int depth = 0)
{
const SceneNode* node = mgr.GetNode(index);
std::string indent(depth * 2, ' ');
LOG(indent + node->name + " [" + node->type + "]");
for (int childIdx : node->childIndices)
{
PrintHierarchy(mgr, childIdx, depth + 1);
}
}
for (int root : sceneMgr.GetRootNodes())
PrintHierarchy(sceneMgr, root);SceneNode node;
node.name = "NewCube";
node.type = "cube";
node.position = {5, 0, 0};
int index = sceneMgr.AddNode(node);The node is pushed to the back of the node list. If node.parentIndex >= 0, the new node is added to the parent's childIndices. The dirty flag is set automatically.
sceneMgr.RemoveNode(index);Recursively removes the node and all descendants. Indices of remaining nodes are updated to preserve consistency. Warning: Previously captured node indices may become stale after removal.
// Make node a child of another node
sceneMgr.SetParent(childIndex, parentIndex);
// Detach node (make it a root)
sceneMgr.SetParent(childIndex, -1);sceneMgr.SaveScene(L"Assets/Scenes/Modified.scene");| Method | Description |
|---|---|
SceneManager(GraphicsEngine*, InputManager*) |
Construct with engine subsystems |
~SceneManager() |
Join async thread, destroy GameObjects |
void NewScene(const string& name = "Untitled") |
Clear and start a new empty scene |
void Clear() |
Destroy all objects without resetting metadata |
| Method | Description |
|---|---|
bool LoadScene(const wstring& filepath) |
Load scene synchronously (returns false on error) |
bool SaveScene(const wstring& filepath) const |
Save scene to JSON (returns false on I/O error) |
void LoadSceneAsync(const wstring& filepath, SceneLoadCallback) |
Load on background thread |
| Method | Description |
|---|---|
int AddNode(const SceneNode& node) |
Append a node, return its index |
void RemoveNode(int index) |
Remove node and all descendants |
void SetParent(int childIndex, int parentIndex) |
Reparent a node (-1 = root) |
vector<int> GetRootNodes() const |
Get indices of all root nodes |
const SceneNode* GetNode(int index) const |
Get node by index (const) |
SceneNode* GetNode(int index) |
Get node by index (mutable) |
int FindNode(const string& name) const |
Find node index by name (-1 if not found) |
int GetNodeCount() const |
Total number of nodes |
| Method | Description |
|---|---|
bool SavePrefab(int nodeIndex, const wstring& filepath) const |
Save subtree as prefab |
int LoadPrefab(const wstring& filepath, const XMFLOAT3& pos) |
Instantiate prefab at position |
| Method | Description |
|---|---|
bool IsDirty() const |
Check for unsaved changes |
void MarkDirty() |
Explicitly mark as modified |
const SceneMetadata& GetMetadata() const |
Read-only metadata access |
SceneMetadata& GetMetadata() |
Mutable metadata access |
const wstring& GetCurrentFilePath() const |
Last loaded/saved file path |
| Method | Description |
|---|---|
string Console_ListNodes() const |
Formatted hierarchy tree |
string Console_GetNodeInfo(int index) const |
Detailed node information |
bool Console_MoveNode(int index, float x, y, z) |
Move node to position |
bool Console_RenameNode(int index, const string& newName) |
Rename a node |
SceneManager sceneMgr(&graphicsEngine, &inputManager);
sceneMgr.NewScene();
// Set scene metadata
SceneMetadata& meta = sceneMgr.GetMetadata();
meta.sceneName = "TestArena";
meta.author = "Developer";
meta.gravityY = -9.81f;
meta.ambientLightR = 0.2f;
meta.ambientLightG = 0.2f;
meta.ambientLightB = 0.3f;
// Add a ground plane
SceneNode ground;
ground.name = "Ground";
ground.type = "plane";
ground.position = {0.0f, 0.0f, 0.0f};
ground.scale = {50.0f, 1.0f, 50.0f};
int groundIdx = sceneMgr.AddNode(ground);
// Add a directional light
SceneNode light;
light.name = "SunLight";
light.type = "light";
light.position = {0.0f, 20.0f, 0.0f};
light.properties["lightType"] = "directional";
light.properties["intensity"] = "1.5";
light.properties["color"] = "1.0,0.95,0.9";
sceneMgr.AddNode(light);
// Add a model as a child of the ground
SceneNode enemy;
enemy.name = "EnemySpawn";
enemy.type = "model";
enemy.modelPath = "Assets/Models/Enemy.obj";
enemy.position = {10.0f, 0.0f, 5.0f};
int enemyIdx = sceneMgr.AddNode(enemy);
sceneMgr.SetParent(enemyIdx, groundIdx); // Parent to ground
// Add a trigger volume for area detection
SceneNode trigger;
trigger.name = "ExitTrigger";
trigger.type = "trigger";
trigger.position = {0.0f, 1.0f, 25.0f};
trigger.scale = {5.0f, 3.0f, 1.0f};
trigger.properties["onEnter"] = "ExitLevel";
sceneMgr.AddNode(trigger);
// Save the scene
sceneMgr.SaveScene(L"Assets/Scenes/TestArena.scene");// Find a node by name (linear search, returns index or -1)
int sunIdx = sceneMgr.FindNode("SunLight");
if (sunIdx >= 0) {
const SceneNode* sun = sceneMgr.GetNode(sunIdx);
LOG("Sun position: " + std::to_string(sun->position.y));
}
// Get total node count
int count = sceneMgr.GetNodeCount();
// List available scene files in a directory
auto scenes = sceneMgr.GetAvailableScenes(L"Assets/Scenes");
for (const auto& path : scenes) {
LOG("Scene: " + path);
}Save and load reusable prefab templates:
// Save a node subtree as a prefab
sceneMgr.SavePrefab(nodeIndex, L"Assets/Prefabs/Enemy.prefab");
// Instantiate a prefab into the scene at a specific position
int newNodeIndex = sceneMgr.LoadPrefab(
L"Assets/Prefabs/Enemy.prefab",
{10.0f, 0.0f, 5.0f} // World-space offset
);
if (newNodeIndex < 0) {
LOG_ERROR("Failed to load prefab");
}Prefab files use the same JSON format as scene files but contain only the exported subtree. Node positions are stored relative to the prefab's internal origin and offset by the position parameter during instantiation.
The scene manager tracks unsaved changes:
sceneMgr.MarkDirty(); // Mark scene as modified
bool unsaved = sceneMgr.IsDirty(); // Check for unsaved changesMethods that automatically set the dirty flag:
-
AddNode(),RemoveNode(),SetParent() -
Console_MoveNode(),Console_RenameNode()
Methods that clear the dirty flag:
-
LoadScene(),SaveScene(),NewScene()
The editor uses IsDirty() to prompt "Save changes?" before closing or loading a new scene.
Scene history supports undo/redo for editor operations. The undo stack captures snapshots of the node list before each modifying operation.
A legacy binary format is supported via LoadCustom() for older .scene files. The binary format is engine-version-specific. New scenes should always use the JSON format.
scene_info # Show current scene metadata (name, author, node count)
scene_list # List all nodes in hierarchy with tree indentation
scene_load <path> # Load a scene file (synchronous)
scene_save <path> # Save current scene to file
scene_clear # Clear the scene (destroy all nodes)
scene_node <index> # Show detailed info for a specific node
scene_move <index> <x> <y> <z> # Move a node to a new position
scene_rename <index> <name> # Rename a node
scene_parent <child> <parent> # Reparent a node (-1 = root)
Scene nodes are converted to ECS entities during InstantiateNodes(). Each node creates an entity with:
-
NameComponent(fromnode.name) -
Transform(fromnode.position,rotation,scale) - Type-specific components (
MeshRenderer,LightComponent,ColliderComponent)
- Gravity settings from
SceneMetadataare applied to the Jolt physics world - Trigger nodes create
ColliderComponentwithisTrigger = true - Nodes with physics properties create
RigidBodyComponent
The scene manager publishes events through the Event System:
-
SceneLoadedEventwhen a scene finishes loading -
SceneUnloadedEventwhen a scene is cleared
- Entity Component System -- ECS entities created from scene nodes
- SparkEditor -- Visual scene hierarchy editor
- Asset Pipeline -- Model and asset loading
- Rendering and Graphics -- Scene rendering pipeline
- Physics -- Physics bodies from scene nodes
- Gameplay Systems -- Gravity and interactive objects
- Save System -- Saving and restoring scene state
- Event System -- Scene lifecycle events
- Networking -- Networked scene synchronization
Published from 2b03dc797148. Edit the canonical source in wiki/.
- Documentation
- Docs route
- Wiki index
- Guides
- Tutorials
- Samples
- Examples
- API Reference
- API route
- Reference
- Build Guide
- Dependencies
- FAQ
- Changelog
- Roadmap
- Contributing
- Code of Conduct
- Home
- FAQ
- Getting Started
- Quick-Start Tutorial
- Making Your First Game
- Making Your First Multiplayer Game
- Artist Workflow Guide
- Editor Walkthrough
- Migration Guide
- How SparkEngine Works
- Architecture Overview
- Engine Architecture Flowchart
- Creating a Game Module
- Game Modules (catalog)
- Entity Component System
- Rendering and Graphics
- Physics
- Cloth Simulation
- Audio
- Input System
- Camera System
- Scripting with AngelScript
- Visual Scripting
- AI and Navigation
- Animation
- 2D Systems
- Networking
- Dedicated Server
- Multiplayer Quick Start
- Area Server Architecture
- Scene Management
- Large World Support
- Collaborative Editing
- Coroutine System
- Event System
- Event Response System
- Job System
- UI System
- UI Layout Extensions
- Localization
- Dialogue System
- Destruction System
- Replay System
- Achievement System
- Loading System
- Mod System
- Content Delivery
- Tween System
- Memory Integrity
- Gameplay Systems
- Terrain and Procedural Generation
- Save System
- Persistence System
- Day Night Cycle and Weather
- Cinematic Sequencer
- Runtime Prefabs
- SparkEditor
- Editor Tutorials
- SparkConsole
- SparkDaemon
- Shader Pipeline
- Asset Pipeline
- Asset Validation
- Asset Migration
- Game Packaging
- Online Services
- DataTable System
- Loot and Crafting System
- CSG System
- Font System
- Timer Manager
- Movie Render Pipeline
- HLOD and World Partition
- Remote Debug System
- Selection Manager
- Asset Dependency Graph
- Editor Automation
- File Watcher
- Project Templates
- System Requirements
- VR Support
- Mobile Platform
- Accessibility
- Platform Input
- Platform Certification
- Cross-Compilation: Wine Testing
- RHI Abstraction Layer
- D3D11 Backend
- D3D12 Backend
- Vulkan Backend
- OpenGL Backend
- Metal Backend
- DXR Raytracing
- Hybrid Ray Tracing
- Upscaling (DLSS/FSR)
- Render Graph
- Shader Graph
- GPU Particles
- GPU-Driven Rendering
- Volumetric Fog
- Volumetric Clouds
- Global Illumination
- Virtual Texturing
- Water Rendering
- Clustered Lighting
- Material System
- Post-Processing
- Shadow System
- Particle System
- Decal System
- Sky and Atmosphere
- Foliage System
- Mesh Shaders
- Neural Rendering
- Configuration Reference
- Performance Tips
- Benchmark Framework
- Threading Model
- Fuzz Policy and Parser Security
- Memory Safety
- Memory Management Patterns
- Build System and CMake Modules
- Profiler and Debugging
- Performance Profiling Guide
- Telemetry System
- Crash Reporting
- Golden Image Testing
- Utilities
- Testing
- Fuzz Policy and Parser Security
- Codebase Statistics
- Codebase Health
- Error Handling Patterns
- Hot Reload Overview
- Troubleshooting
- Contributing
- Workflow Patterns
- Build Optimizations
- CI Reproducible Builds
- GitHub API and PR Checks
- Git Rebase Conflicts
- Clang-Format
- Code Quality Violations
- AI Bloat Pattern
- MinGW + Wine Cross-Compilation
- Live Editor Testing
- Engine & Renderer Landscape
- DuetOS Portability Catalog
- Five-Engine Analysis
- Eleven-Engine Analysis
- ThorVG / Unity Graphics Analysis
- Advanced Techniques Catalog
- Third-Party Library Evaluation
- Engine Viability Evaluation
- Engine Feature Recommendations
- Project Recommendations
- Mac Compatibility Analysis
- Codebase Observations
- Codebase Bloat Audit
- Test Suite Audit
- Documentation Coverage Audit
- ThirdParty Dependencies Audit
- Load Test Baseline
- Gameplay Systems Status
- SparkGame Module Status
- Stub and Abandoned Features
- Memory Integrity System
- Memory Safety Evaluation
- Hardware Acceleration Systems
- Jolt Physics Integration
- GPU/CPU Separation Plan
- Daemon Services Architecture
- Reflection & Polymorphism Refactoring Plan
- SparkBuild In-Tree
- Wine No-JobSystem Breakthrough
- Wine Role and Fallback Tiers