t1k:unity:dots-core:vcontainer-integration
| Field | Value |
|---|---|
| Module | dots-core |
| Version | 2.3.2 |
| Effort | high |
| Tools | — |
Keywords: BattlePass bridge, DOTS bridge, ECS UI integration, IAsyncStartable, ManagedEventQueue, SignalBus, VContainer
How to invoke
Section titled “How to invoke”/t1k:unity:dots-core:vcontainer-integrationDOTS ↔ VContainer Integration
Section titled “DOTS ↔ VContainer Integration”How DOTS (unmanaged, World-scoped, Burst-compiled) and VContainer + SignalBus (managed, LifetimeScope-scoped, GC) coexist without leaking lifetime, fighting over data, or breaking Burst.
Covers:
- DOTS systems publishing events to VContainer-managed UI controllers (Read).
- VContainer-managed UI sending requests into DOTS (Write).
- World ↔ LifetimeScope lifetime ownership and shutdown order.
- Bootstrap order for
TheOneFeaturemodules (TimeService → Inventory → Feature). - Test isolation across the boundary.
Does NOT cover:
- DOTS rendering (
dots-graphics), navigation (agents-navigation), audio (unity-audio). - General MonoBehaviour DI (
unity-monobehaviour) — load that for non-DOTS DI. - Save/load serialization (
memorypack,unity-save-system).
The (Read, Write, Lifetime) Triple
Section titled “The (Read, Write, Lifetime) Triple”Every DOTS↔managed boundary has three orthogonal concerns. Solve them together or you will rediscover footgun #1 or #6 at runtime.
ECS WORLD │ MANAGED / VContainer (Burst, struct) │ (GC, class)───────────────────────────────────────── │ ───────────────────────────────── DynamicBuffer<XPEvent> singleton │ SignalBus.Fire(new XPEvent(...)) │ │ ▲ [Read] BridgeSystem (OrderFirst) │ │ LateUpdate poll ▼ │ │ ManagedEventQueue<T> ◄────── shared static ref ───┤ ▲ │ │ [Write] ClaimSystem queries │ │ EntityManager.Add ClaimRewardRequest │ ▼ │ │ UI controller (IStartable) ◄── EntityManager.Create ────┘ │ [Lifetime] World.IsCreated guard ─────► nullable World ref + OnDisable nullify| Direction | Recommended | Anti-pattern |
|---|---|---|
| Read ECS → MB | Bridge ISystem (OrderFirst) drains buffer → enqueues into static ManagedEventQueue<T>; MB polls in LateUpdate and fires to SignalBus via signalBus.Fire(...). | Direct EntityManager.GetBuffer poll in MB Update — wrong frame timing, GC alloc, no Burst. |
| Write MB → ECS | UI calls world.EntityManager.AddComponentData(new ClaimRewardRequest{...}). Poll-system in OrderFirst consumes + destroys the request entity. | BeginSimulationECB from a managed system — boilerplate without benefit for one-off requests. |
| Lifetime | Bridge MB stores World ref (NOT EntityManager); guards every use with world != null && world.IsCreated; nullifies in OnDisable. | Capturing EntityManager in a closure or MB field — struct copy goes stale on chunk move. |
→ See bootstrap-order.md for the World-vs-LifetimeScope timeline.
Bootstrap Order
Section titled “Bootstrap Order”TheOneFeature services have a hard DAG. Register out-of-order and stamina
recharge breaks, BattlePass tier unlocks miss XP, daily streak resets every
launch. The required order:
UserDataManager— every feature reads LocalData via this; must be ready first.TimeService— must beIsSyncedbefore any time-driven feature (Lives, BattlePass, DailyReward, Booster).InventoryService— every reward-granting service callsinventoryService.AddRewards(...); register before BattlePass / DailyReward / Gacha / ClaimReward.- Gameplay features —
RegisterLives(),RegisterBattlePass(),RegisterDailyReward(). - Monetization / engagement —
RegisterFeatureIAP(),RegisterBooster(),RegisterGacha(),RegisterNotification(). - DOTS bridge MonoBehaviours — instantiate AFTER step 5; they
[Inject]the services from steps 3–5 and resolveWorld.DefaultGameObjectInjectionWorldinOnEnable.
→ See bootstrap-order.md for the full registration template and shutdown sequence.
ManagedEventQueue<T>
Section titled “ManagedEventQueue<T>”A tiny non-allocating wrapper around System.Collections.Generic.Queue<T> that
serves as the DOTS→managed marshalling buffer. It lives on the bridge MB, is
referenced by the bridge ISystem via a static field, and is cleared on scope
teardown.
public sealed class ManagedEventQueue<T> where T : unmanaged{ private readonly Queue<T> queue = new(256); public void Enqueue(in T item) => this.queue.Enqueue(item); public bool TryDequeue(out T item) => this.queue.TryDequeue(out item); public void Clear() => this.queue.Clear(); public int Count => this.queue.Count;}Once the com.the1studio.dots-ui-bridge library package ships, prefer the
shared DOTSUIBridge.ManagedEventQueue<T> over a per-demo copy.
→ See managed-event-queue.md for the full bridge-system + bridge-MB pair, including the static field bootstrap pattern and a NativeQueue<T> variant for jobs.
SignalBus Integration
Section titled “SignalBus Integration”The project’s SignalBus ships with GameFoundation
(Packages/com.gdk.core/Scripts/Signals/SignalBus.cs, namespace
GameFoundation.Signals). Publish API is Fire(in T), NOT
Publish<T>. Three non-negotiable rules:
- Container plumbing order —
builder.RegisterDependencyContainer()MUST run beforebuilder.RegisterSignalBus()(its ctor injectsIDependencyContainer). See signalbus-integration.md § “Required Bootstrap Order.”
Then the handler-lifecycle rules from code-conventions-unity.md:
- Subscribe with named methods, not lambdas. Lambdas have no reference
equality —
Unsubscribesilently fails and you leak callbacks across scene reloads. - Always pair
SubscribeinIStartable.Start()(orOnEnable) withUnsubscribeinIDisposable.Dispose()(orOnDisable). Track theIDisposableif your DI container returns one.
public sealed class BattlePassUIController : IStartable, IDisposable{ private readonly SignalBus signalBus; private readonly BattlePassUIPanel panel;
public BattlePassUIController(SignalBus signalBus, BattlePassUIPanel panel) { this.signalBus = signalBus; this.panel = panel; }
public void Start() => this.signalBus.Subscribe<BattlePassXPEvent>(this.OnXPGained); public void Dispose() => this.signalBus.Unsubscribe<BattlePassXPEvent>(this.OnXPGained); private void OnXPGained(BattlePassXPEvent e) => this.panel.AddXP(e.Amount);}
// Bridge MB publishes via Fire — NOT Publish.this.signalBus.Fire(new BattlePassXPEvent { Amount = 10 });→ See signalbus-integration.md for IAsyncStartable, signal-event payload conventions, and Burst-compatibility constraints (signal payloads must be unmanaged to round-trip through ManagedEventQueue<T>).
Anti-patterns
Section titled “Anti-patterns”Twelve footguns from Phase C / Phase D delivery (BackpackCrawler 2026-05). Each is expanded in gotchas.md.
- Captured
EntityManagerin a closure. Struct copy goes stale after chunk move. StoreWorld, callworld.EntityManagerfresh per use. CreateEntityQueryevery frame. Cache inStart(); dispose inOnDestroy().- Cached
DynamicBuffer<T>reference across frames. Re-fetch each frame. - No
World.IsCreatedguard on Play-mode exit. Guard every access. - Bridge
ISysteminLateSimulationSystemGroup. Use[UpdateInGroup(typeof(SimulationSystemGroup), OrderFirst = true)]. - Multiple LifetimeScopes each instantiating their own bridge. One
app-level bridge (
DontDestroyOnLoad) injected into every scene scope. - Lambda
SignalBus.Subscribe(...). No reference equality →Unsubscribesilently does nothing. Use named instance methods. - Calling
signalBus.Publish<T>(...). UsesignalBus.Fire(in T)— GameFoundation’sSignalBushas noPublishmethod. EntityQuery.Dispose()withoutWorld.IsCreatedguard. A Tier-2 bridge caching anEntityQuerymust guardworld.IsCreatedAND checkquery != defaultBEFORE disposing inOnDestroy/OnDisable, else Play-mode exit throwsObjectDisposedExceptionper bridge. Same rule for every cachedNative*collection.RegisterSignalBus()beforeRegisterDependencyContainer().SignalBus’s ctor injectsIDependencyContainer; missing it produces an opaqueVContainerExceptionon first signal resolve.- Calling
Register{Feature}()without registering its ctor deps first. Extensions are NOT self-contained — e.g.BattlePassServiceneeds 5 globally pre-registered deps. - Typo’ing
TheOne.Features.Timein usings.TimeServicelives atTheOne.Feature.Time.Core.Services— singularFeature.
→ See gotchas.md for the full failure mode + fix per item.
Reference Files
Section titled “Reference Files”| File | Topic |
|---|---|
| bootstrap-order.md | Frame-by-frame timeline, registration template, shutdown order, time-sync gate |
| managed-event-queue.md | ManagedEventQueue<T> full source, bridge-system + bridge-MB pair, NativeQueue<T> variant |
| signalbus-integration.md | Named-method subscribe/unsubscribe, IAsyncStartable, payload conventions, leak detection |
| gotchas.md | The 12 anti-patterns above with full failure modes, repro symptoms, and fixes |
Related Skills
Section titled “Related Skills”t1k-unity-dots-core-ecs-core— IComponentData, ISystem, EntityManager, query patterns.t1k-unity-dots-core-enableable-components—IEnableableComponentpatterns;GetSingletonEntityrejects enableable singletons (useIgnoreComponentEnabledState).t1k-unity-dots-core-entity-command-buffer— when to prefer ECB over direct EntityManager calls.t1k-unity-base-monobehaviour— non-DOTS MonoBehaviour DI, lifecycle, lambdas vs named handlers.t1k-unity-testing-dots-unit-testing—DOTSTestBasefixture for the ECS-side unit tests in this skill.