16 — Multiple Worlds¶
Two independent Trecs World instances running side-by-side in one Unity scene. Worlds are isolated entity stores with their own systems, RNG, and lifecycle, while sharing the process-wide type ID registry.
Source: com.trecs.core/Samples~/Tutorials/16_MultipleWorlds/
What it does¶
Two worlds tick every frame:
- World A -- spawns red spheres around
x = -3, every 0.5 s, lifetime 3 s. - World B -- spawns blue cubes around
x = +3, every 0.7 s, lifetime 3 s.
Each world has its own WorldBuilder, World, system instances, and entity store. They share a Construct() call but otherwise know nothing about each other.
Schema¶
A globals template and a critter template, both registered with both worlds:
public static class SampleTags
{
public struct Critter : ITag { }
}
[Unwrap]
public partial struct Lifetime : IEntityComponent
{
public float Value;
}
public partial class SampleGlobals : ITemplate, IExtends<TrecsTemplates.Globals>
{
SpawnSystem.State SpawnState = default;
}
public partial class CritterEntity
: ITemplate,
IExtends<CommonTemplates.RenderableGameObject>,
ITagged<SampleTags.Critter>
{
Position Position = default;
Lifetime Lifetime;
PrefabId PrefabId = new(MultipleWorldsPrefabs.Critter);
}
Position comes from Common/; PrefabId / GameObjectId come in via the RenderableGameObject base.
Composition root¶
Building two worlds in one Construct():
var worldA = new WorldBuilder()
.SetDebugName("World A — Red Spheres")
.AddTemplates(new[]
{
SampleTemplates.SampleGlobals.Template,
SampleTemplates.CritterEntity.Template,
})
.Build();
var goManagerA = new RenderableGameObjectManager(worldA);
worldA.AddSystems(new ISystem[]
{
new SpawnSystem(SpawnIntervalA, Lifetime, SpawnRadius,
new Vector3(-WorldSeparation, 0, 0)),
new LifetimeSystem(),
new PrimitivePresenter(goManagerA),
});
var worldB = new WorldBuilder()
.SetDebugName("World B — Blue Cubes")
.AddTemplates(new[]
{
SampleTemplates.SampleGlobals.Template,
SampleTemplates.CritterEntity.Template,
})
.Build();
var goManagerB = new RenderableGameObjectManager(worldB);
worldB.AddSystems(new ISystem[]
{
new SpawnSystem(SpawnIntervalB, Lifetime, SpawnRadius,
new Vector3(WorldSeparation, 0, 0)),
new LifetimeSystem(),
new PrimitivePresenter(goManagerB),
});
Each world gets its own SpawnSystem/LifetimeSystem/PrimitivePresenter instance — system instances are not shared across worlds. SetDebugName labels each world for editor tooling (e.g. the World dropdown in TrecsPlayerWindow).
Both worlds register the same template type (CritterEntity) — allowed and common. Templates describe a shape; each world independently allocates its own component storage for that shape. An entity created in World A is invisible to queries in World B.
Lifecycle wiring¶
Two worlds means two of every lifecycle hook:
initializables = new()
{
worldA.Initialize,
worldB.Initialize,
sceneInitA.Initialize,
sceneInitB.Initialize,
};
tickables = new() { worldA.Tick, worldB.Tick };
lateTickables = new() { worldA.LateTick, worldB.LateTick };
disposables = new()
{
goManagerA.Dispose,
goManagerB.Dispose,
worldA.Dispose,
worldB.Dispose,
};
Each world ticks independently. If you wanted to pause one world while the other runs, you would simply skip its Tick() call. Worlds maintain independent fixed-update accumulators, so a paused world resumes where it left off -- it doesn't try to catch up missed simulation time.
Per-world GameObject managers¶
Each world gets its own RenderableGameObjectManager. The manager subscribes to OnAdded / OnRemoved on its world's accessor, so its lifetime — and the GameObjectId counter it allocates from the world's heap — is 1:1 with a single World. GameObjectId values are world-local; either world can be snapshotted independently and its GameObjects will be rebuilt deterministically from its entity set.
What's process-global vs per-world¶
- Per-world — entity store, component arrays, system instances, accessors, RNG, blob cache, event manager, interpolation. Everything holding simulation state.
- Process-global — component / tag / set type IDs (
ComponentTypeId<T>,Tag<T>,EntitySet<T>), plus theWorldRegistrylisting active worlds. Stable identifiers, not state.
This split is what lets the same template type register in multiple worlds without conflict.
Concepts introduced¶
- Multiple
Worldinstances in one process — supported and isolated. WorldBuilder.SetDebugNamefor editor disambiguation.WorldRegistry.ActiveWorldsfor discovering all live worlds (useful for editor tooling).- Per-world pause — the application chooses when to call
Tick(); skipping the call is all "pause" means. - Per-world application services (here:
RenderableGameObjectManager) — when a service holds per-world state (an id counter on the world's heap, in this case), instantiate one per world rather than sharing.