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 editor packages its selected default scene as Startup.sparkscene beside
the game host. SparkGameFPS consumes that file through SceneManager::LoadScene
before initializing its camera and player. Selection uses the executable directory,
not the working directory. Only a genuinely absent startup file permits the legacy
Assets/Scenes/level1.scene path; an unreadable or invalid selected file fails module
initialization. The host's separate -scene preview mode is unchanged.
This is a deliberately bounded adapter, not complete ECS-to-FPS scene support. It accepts root entities with unique nonempty names and Transform plus exactly one OBJ MeshRenderer, the main Camera, or a supported SpawnPointComponent. A main Camera and at least one mesh are required even when spawn points are present. Position/rotation/scale, camera FOV and clipping are transferred to gameplay state. Camera scale must be one, pitch must be within [-89,89] degrees, FOV within [10,170], near within [0.01,10], and far within [100,10000] with near below far. Hierarchy, other components, explicit materials, nondefault visibility/shadow/emissive settings, missing meshes and unsupported camera values fail rather than silently lose authored state. Unicode and quoted names are accepted; the legacy node lookup requires unique names. Mesh references must resolve beneath the scene's project root and load as actual OBJ geometry, without placeholder fallback.
SpawnPointComponent support is limited to enabled neutral player points with
spawnTag="default", teamID=0, spawnRadius=0, respawnDelay=0, and
maxConcurrent=-1. The adapter preserves priority, position and facing as an
existing FPS SpawnPoint node. Spawn transforms must have unit scale and pitch in
[-89,89]; hierarchy remains unsupported. More than 32 points rejects the whole
scene instead of silently truncating the FPS table. Mixed mesh/camera/spawn
components on one entity also reject the scene. The strict reflected schema
requires all authored fields and rejects unknown or runtime-only fields.
The existing FPS selector uses highest priority and first-stored ties for F11
match start and respawn. The component's reuse cooldown is distinct from the
player's independent death timer, which remains unchanged. The component defaults
spawnRadius=1 and respawnDelay=5 must be changed explicitly for this exact-point
subset. Disabled/team-specific/radius/cooldown/concurrency-limited points and
wave_spawn are rejected. WaveSpawner's separate jittered placement does not
implement these component semantics. No SpawnPoint-aware cook transformation is
needed: the existing cooker preserves the selected scene bytes.
The direct reflected SceneManager family now contains ten source cases (six existing plus four spawn regressions). Its real RespawnSystem/event test covers selection and the death timer, not physical F11 input or a rendered match. The installed two-entity camera/mesh lineage fixture remains separate and does not prove packaged spawn consumption; that requires an explicit fixture/validator extension and fresh native evidence.
The primary reflected document is read with the existing strict deserializer and
size bound, without automatic .bak recovery. The existing LoadScene transaction
retains the old nodes, object ownership, path and dirty state on failure. Successful
windowed FPS startup exposes committed node, actual mesh, camera and player records.
Data-only SceneManager loading cannot establish GPU mesh consumption, and the separate
headless FPS arena does not exercise this startup adapter. Native regression and
installed windowed WARP results are required for the new source; prior Release
evidence does not cover it or certify a final Shipping playthrough.
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 |
| 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 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);
}SceneManager has no prefab API: its line-based SavePrefab/LoadPrefab had no caller and were removed. Editor prefabs are .sparkprefab assets owned by SparkEditor::PrefabManager.
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 614bca17b41e. 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
- CI Controlled-Failure Rehearsal
- Release Publication Stages
- Session Handoff 2026-09-27
- 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
- Online Service Boundary
- Server Operations Runbook
- Reflection & Polymorphism Refactoring Plan
- SparkBuild In-Tree
- Wine No-JobSystem Breakthrough
- Wine Role and Fallback Tiers