Hey guys, I know I was silent for a long time. So happy to return from my 5-weeks rewind at sunny Crete.
This weekend’s work focused on a complete redesign of the prefab system, making it composable, extensible, and suitable for future modding capabilities. The implementation now supports both patching and derivation of prefabs, as well as proper argument mapping and validation. Circular dependency handling has also been added.
How it was before this weekend
From the very beginning, patching of existing prefabs was always in my mind. The engine always provided a capability to register a new prefab factory under the same ID as an existing prefab. In that case, the new factory didn’t overwrite the pre-existing one, but got appended to the chain of factories.
For example, there is a prefab Prisoner. As a modder, you’d like to add a new component to all Prisoner entities, say RunSpeed (to make it variable for guards to catch different escaping prisoners). How could you achieve that?
world.Prefabs.Register(new PrefabDef {
Id = PrefabIds.Prisoner,
Factory = (world, prisoner, args) => {
// Base components are already added by the implicitly
// called pre-existing factories.
// Just need to add our custom one.
prisoner.Add(new RunSpeed((float)args["RunSpeed"]));
return prisoner;
}
});
As this Factory() is just being added to the tail of the chain of pre-existing factories, its prisoner parameter is already prefilled entity. Thanks to that architectural decision, you don’t need to recreate the full prefab from scratch. Just enrich it with one more component and return. It should perfectly survive further updates of the game.
Problem 1: Too Much of C#
As a C# coder, I had an unconscious bias that everyone likes coding, or at least doesn’t vomit on a thought about it. That’s why the idea of chained factories seemed such appealing to me.
“You need to create a derived prefab like SuperGuard? Just register a new prefab, and in its factory code just explicitly call the engine prefab for Guard. This is what the world parameter is passed for.” This was my initial concept.
But hey, how many of you guys really love coding for modding through your free time? Let me count your raised hands…
Obviously, forcing you to run up an entire IDE like Visual Studio for such a trivial task as adding a new entity type to the game makes my work nothing close to the proclaimed goal of Modders First.
Problem 2: Flexible Mixins
Furthermore, it didn’t allow to make a complex hierarchy with passing arguments through multiple layers.
Imagine a hierarchy: Prisoner -> Human -> Animal -> Renderable.
- The
RenderablerequiresSpriteandPositioncomponents. - But neither
AnimalnorHumandoes so for themselves. Prisoneronly needsPositionas an input parameter, but providesSpritedown toRenderableon its own.
With the system existed prior to this weekend, all such intermediate layers of prefabs would have to explicitly declare and pass these arguments. Totally inflexible. And ruins the idea of ECS as lightweight mixins, instead locking us up with a pre-hardcoded hierarchy - some OOP relic in its worst form.
Solution
I decided to spend this weekend on a complete retouchment of how the prefab system works. My final goal was to allow to register new prefabs with no explicitly defined factories, i.e. with no C# code at all, just in purely declarative way.
Prefab Patching (Preserved & Enhanced)
As before, a prefab can still be patched by defining another prefab with the same ID. For you, it doesn’t differ much from the past behavior. But trust me, a lot of stuff had to be reshuffled in order to eliminate the need of Factory() in as many use cases as possible.
Just to formalize:
- The engine preserves the original prefab factory and chains the new one after it.
- On
PrefabRegistry.Create("prefabId"), all factories in the chain are invoked sequentially from oldest to newest. - This allows mods to modify or extend existing prefabs without redefining them.
- Each factory in the chain can add or remove components created by earlier factories. This is what I call a layer of prefab.
Example use case: adding new duties or attributes to all entities instantiated from an existing prefab (Guard, Door, etc.).
Prefab Derivation (New)
This is the achievement I’m proud of.
A prefab can declare one or more base prefabs, forming a directed acyclic graph of dependencies.
- When a prefab is instantiated, all of its base prefabs are resolved and invoked recursively before its own factory.
- A prefab may optionally omit its own factory and rely entirely on its base prefabs’ logic.
- Circular dependencies between base prefabs are detected and skipped with a warning.
- Argument mapping is fully supported between caller and base prefabs.
- The engine carefully tracks arguments usage by base prefabs. Should none of the factories use any of passed arguments, it emits warning. Good for spotting cases when you missed some base prefab in declaration.
Example of declaration:
world.Prefabs.Register(new PrefabDef {
Id = PrefabIds.Prisoner,
FactoryArgs = [],
BasePrefabs = new() {
{ Base.PrefabIds.Renderable, new() {
{ "Sprite", _ => SpriteIds.PrisonerIdle },
} },
{ Base.PrefabIds.Human, new() }
},
});
(Yeah, it’s still C# so far, but it’s declarative at least. JSON is on the roadmap, no worries!)
Here:
Prisonerderives fromRenderable.- It provides a constant value for
SpritetoRenderablevia an argument mapper. - Other arguments required by
Renderableare automatically propagated from the caller. Prisoneralso derives fromHuman. That factory adds all relevant components (likeSleepNeed) on its own.Prisonerdefines no factory of its own.
Example of entity spawn:
var prisoner = _world.Prefabs.Create(PrefabIds.Prisoner, new() {
{ "Position", new Position { X = 128, Y = 128, Floor = 0 } }
});
Argument Handling
- Only named arguments are supported (no positional parameters).
- Argument mappers are lambdas that accept the full argument dictionary provided by the caller.
- Validation covers:
- Missing required arguments
- Unused arguments
- Type mismatches
Multiple Inheritance (Diamond Problem)
Absolutely permitted, and totally under safeguards.
Circular Dependency Detection
Implemented lightweight cycle tracking to prevent infinite recursion:
- Each prefab instantiation maintains a visited set of prefab IDs.
- When a prefab is encountered more than once, subsequent invocations are ignored, and a warning is logged with the chain trace.
Next Steps
- Acquire additional textures for the upcoming demo scene.
- Build a static test map showcasing prefab composition (Guards, Prisoners, Workers, environment objects).
Hopefully this closes the prefab system’s architectural phase. The system is now ready for use in constructing initial game content and will serve as a foundation for modding and entity behaviors.