Repository navigation
Runtime Prefabs
The Runtime Prefab system provides reusable, data-driven entity templates that can be registered, cloned, serialized, and instantiated at runtime. Each prefab holds a list of component descriptors with string key-value properties, and a global PrefabRegistry manages lookup by name or numeric ID and handles spawning via the ECS.
Source: SparkEngine/Source/Engine/ECS/RuntimePrefab.h
| Class | Responsibility |
|---|---|
RuntimePrefab |
A reusable entity template holding ordered component descriptors with property overrides |
PrefabComponentData |
Serialized descriptor for a single component (type name + key-value properties) |
PrefabRegistry |
Singleton registry that stores, retrieves, and spawns entities from registered prefabs |
PrefabFileHeader |
Binary file format constants (magic number and version) for prefab serialization |
Describes a single component within a prefab. Properties are stored as string key-value pairs that the spawning code interprets when creating real ECS components:
struct PrefabComponentData
{
std::string typeName; // Component type (e.g. "Transform", "MeshRenderer")
std::unordered_map<std::string, std::string> properties; // Key-value pairs
};Constants for the binary serialization format:
struct PrefabFileHeader
{
static constexpr uint32_t kMagic = 0x50524642; // "PRFB"
static constexpr uint32_t kVersion = 1;
};#include "Engine/ECS/RuntimePrefab.h"
// Create a prefab for an enemy guard entity
auto prefab = std::make_unique<Spark::ECS::RuntimePrefab>("EnemyGuard");
// Add component descriptors with properties
prefab->AddComponent("Transform", {
{"posX", "0"}, {"posY", "0"}, {"posZ", "0"},
{"rotY", "180"}
});
prefab->AddComponent("MeshRenderer", {
{"mesh", "models/guard.fbx"},
{"material", "materials/guard_pbr.mat"}
});
prefab->AddComponent("Health", {
{"maxHealth", "100"},
{"currentHealth", "100"}
});
prefab->AddComponent("AIController", {
{"behaviorTree", "ai/guard_patrol.bt"},
{"detectionRange", "15.0"}
});auto& registry = Spark::ECS::PrefabRegistry::GetInstance();
// RegisterPrefab takes ownership and returns an auto-assigned ID
uint32_t guardId = registry.RegisterPrefab(std::move(prefab));
// Register more prefabs
auto crate = std::make_unique<Spark::ECS::RuntimePrefab>("WoodenCrate");
crate->AddComponent("Transform", {{"posX", "0"}, {"posY", "0"}});
crate->AddComponent("MeshRenderer", {{"mesh", "models/crate.fbx"}});
crate->AddComponent("Physics", {{"mass", "10.0"}, {"shape", "box"}});
uint32_t crateId = registry.RegisterPrefab(std::move(crate));// Spawn by ID
uint32_t entity1 = registry.SpawnEntity(guardId);
// Spawn by name
uint32_t entity2 = registry.SpawnEntity("WoodenCrate");
// Spawn multiple instances
for (int i = 0; i < 10; ++i)
{
uint32_t entity = registry.SpawnEntity("EnemyGuard");
// Each call creates a new ECS entity with the prefab's components
}// Check how many prefabs are registered
uint32_t count = registry.GetRegisteredCount();
// Get all registered prefab names
auto names = registry.GetAllPrefabNames();
for (auto name : names)
{
std::println("Registered prefab: {}", name);
}
// Look up a specific prefab
const auto* guardPrefab = registry.GetPrefab("EnemyGuard");
if (guardPrefab)
{
std::println("Guard has {} components", guardPrefab->GetComponents().size());
}
// Look up by ID
const auto* prefabById = registry.GetPrefab(guardId);const auto* prefab = registry.GetPrefab("EnemyGuard");
// Check if a component type exists
if (prefab->HasComponent("AIController"))
{
std::println("This prefab has AI capabilities");
}
// Iterate over all component descriptors
for (const auto& comp : prefab->GetComponents())
{
std::println("Component: {}", comp.typeName);
for (const auto& [key, value] : comp.properties)
{
std::println(" {} = {}", key, value);
}
}Prefabs support inheritance through a parent pointer. When spawning, the parent's components are applied first, then the child's components override or extend them:
// Base enemy prefab
auto baseEnemy = std::make_unique<Spark::ECS::RuntimePrefab>("BaseEnemy");
baseEnemy->AddComponent("Transform", {{"posX", "0"}, {"posY", "0"}});
baseEnemy->AddComponent("Health", {{"maxHealth", "100"}});
uint32_t baseId = registry.RegisterPrefab(std::move(baseEnemy));
// Specialized prefab inheriting from base
auto boss = std::make_unique<Spark::ECS::RuntimePrefab>("BossEnemy");
boss->SetParent(registry.GetPrefab(baseId));
boss->AddComponent("Health", {{"maxHealth", "500"}}); // Override health
boss->AddComponent("BossAI", {{"phase", "1"}}); // Add new component
uint32_t bossId = registry.RegisterPrefab(std::move(boss));
// Query parent
const auto* bossPrefab = registry.GetPrefab(bossId);
if (bossPrefab->GetParent() != nullptr)
{
std::println("Boss inherits from: {}", bossPrefab->GetParent()->GetName());
}Create deep copies of existing prefabs for modification:
const auto* original = registry.GetPrefab("EnemyGuard");
auto variant = original->Clone();
// Modify the clone
variant->RemoveComponent("AIController");
variant->AddComponent("AIController", {
{"behaviorTree", "ai/guard_aggressive.bt"},
{"detectionRange", "25.0"}
});
uint32_t variantId = registry.RegisterPrefab(std::move(variant));Prefabs serialize to a binary format with a PrefabFileHeader (magic 0x50524642 / "PRFB", version 1), followed by the prefab name, component count, and each component's type name and properties:
// Serialize a single prefab
Spark::BinaryWriter writer("prefabs/guard.prefab");
prefab->Serialize(writer);
// Deserialize
Spark::BinaryReader reader("prefabs/guard.prefab");
auto loaded = std::make_unique<Spark::ECS::RuntimePrefab>("");
loaded->Deserialize(reader);Save and load all registered prefabs at once:
// Save all registered prefabs to a binary file
registry.SaveToFile("prefabs/all_prefabs.bin");
// Load prefabs from a binary file and register them
registry.LoadFromFile("prefabs/all_prefabs.bin");Since properties are string key-value pairs, you can clone a prefab, modify properties, and spawn without registering the variant:
const auto* basePrefab = registry.GetPrefab("EnemyGuard");
auto instance = basePrefab->Clone();
// Override a specific property before spawning
// (Requires accessing the component data directly)
for (auto& comp : /* mutable access to components */)
{
if (comp.typeName == "Transform")
{
comp.properties["posX"] = "50.0";
comp.properties["posZ"] = "30.0";
}
}The PrefabRegistry provides a console status method:
std::string status = registry.Console_GetStatus();
// Output:
// [PrefabRegistry] prefabs=3
// [1] EnemyGuard (4 components)
// [2] WoodenCrate (3 components)
// [3] BossEnemy (2 components)Prefab IDs are auto-incremented starting from 1. The registry uses two indices:
-
m_prefabs:uint32_tID tounique_ptr<RuntimePrefab>mapping -
m_nameIndex:stringname touint32_tID for fast name-based lookup
Entity IDs from SpawnEntity() start at 1000 (placeholder counter; production uses the EnTT registry).
// Remove by ID
bool removed = registry.RemovePrefab(guardId);
// Note: RemovePrefab cleans up both the ID mapping and name indexThe spawn path creates EnTT entities and attaches components based on the prefab's PrefabComponentData list. Parent prefab components are applied first when GetParent() returns non-null:
// The internal SpawnFromPrefab() method:
// 1. Creates an EnTT entity
// 2. Walks prefab.GetComponents() and adds matching components
// 3. Applies parent prefab components first if GetParent() != nullptrThe editor can display registered prefabs in an asset browser, allowing drag-and-drop instantiation:
// Editor lists all available prefabs
auto names = registry.GetAllPrefabNames();
for (auto name : names)
{
if (ImGui::Selectable(std::string(name).c_str()))
{
registry.SpawnEntity(name);
}
}RuntimePrefab uses BinaryWriter and BinaryReader for serialization. The format is versioned via PrefabFileHeader::kVersion for forward compatibility.
Prefabs can be loaded on demand when streaming new areas:
// When an area streams in, load its prefab definitions
registry.LoadFromFile("Areas/Forest/prefabs.bin");
// Spawn entities defined in the area
registry.SpawnEntity("ForestTree");
registry.SpawnEntity("ForestRock");| Method | Description |
|---|---|
RuntimePrefab(string name) |
Construct with a unique prefab name |
AddComponent(string typeName, unordered_map<string, string>) |
Add a component descriptor |
RemoveComponent(const string& typeName) -> bool |
Remove first matching component |
HasComponent(const string& typeName) -> bool |
Check if component type exists |
GetComponents() -> const vector<PrefabComponentData>& |
Get all component descriptors |
GetName() -> string_view |
Get the prefab name |
SetParent(const RuntimePrefab*) |
Set parent for inheritance (non-owning) |
GetParent() -> const RuntimePrefab* |
Get parent prefab or nullptr |
Clone() -> unique_ptr<RuntimePrefab> |
Deep copy with same name, components, parent |
Serialize(BinaryWriter&) |
Write to binary stream |
Deserialize(BinaryReader&) |
Read from binary stream |
| Method | Description |
|---|---|
GetInstance() -> PrefabRegistry& |
Access the singleton |
RegisterPrefab(unique_ptr<RuntimePrefab>) -> uint32_t |
Register and take ownership, returns auto-assigned ID |
GetPrefab(uint32_t id) -> const RuntimePrefab* |
Look up by numeric ID |
GetPrefab(string_view name) -> const RuntimePrefab* |
Look up by name |
RemovePrefab(uint32_t id) -> bool |
Remove a prefab by ID |
SpawnEntity(uint32_t prefabId) -> uint32_t |
Spawn entity from prefab ID |
SpawnEntity(string_view name) -> uint32_t |
Spawn entity from prefab name |
GetRegisteredCount() -> uint32_t |
Number of registered prefabs |
GetAllPrefabNames() -> vector<string_view> |
Names of all registered prefabs |
SaveToFile(string_view path) |
Save all prefabs to binary file |
LoadFromFile(string_view path) |
Load and register prefabs from file |
Console_GetStatus() -> string |
Human-readable status for console |
-
PrefabRegistryis a singleton with no internal synchronization. All registration, lookup, and spawn calls must happen on a single thread (typically the main thread). -
RuntimePrefabinstances are value-like objects. A prefab obtained viaGetPrefab()returns aconstpointer and is safe to read from multiple threads as long as the registry is not being modified concurrently. -
GetInstance()uses a function-local static and is safe for concurrent first-access under C++11 magic-statics guarantees.
- ECS-Overview -- Entity Component System architecture
- Components -- Available ECS component types
- Serializer -- Binary serialization utilities (BinaryWriter / BinaryReader)
- Scene-Management -- Scene loading and entity lifecycle
- Streaming-System -- Seamless area streaming and prefab loading
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