Skip to content

t1k:unity:dots-core:vcontainer-integration

FieldValue
Moduledots-core
Version2.3.2
Efforthigh
Tools

Keywords: BattlePass bridge, DOTS bridge, ECS UI integration, IAsyncStartable, ManagedEventQueue, SignalBus, VContainer

/t1k:unity:dots-core: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 TheOneFeature modules (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).

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
DirectionRecommendedAnti-pattern
Read ECS → MBBridge 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 → ECSUI 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.
LifetimeBridge 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.

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:

  1. UserDataManager — every feature reads LocalData via this; must be ready first.
  2. TimeService — must be IsSynced before any time-driven feature (Lives, BattlePass, DailyReward, Booster).
  3. InventoryService — every reward-granting service calls inventoryService.AddRewards(...); register before BattlePass / DailyReward / Gacha / ClaimReward.
  4. Gameplay featuresRegisterLives(), RegisterBattlePass(), RegisterDailyReward().
  5. Monetization / engagementRegisterFeatureIAP(), RegisterBooster(), RegisterGacha(), RegisterNotification().
  6. DOTS bridge MonoBehaviours — instantiate AFTER step 5; they [Inject] the services from steps 3–5 and resolve World.DefaultGameObjectInjectionWorld in OnEnable.

→ See bootstrap-order.md for the full registration template and shutdown sequence.

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.

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:

  1. Container plumbing orderbuilder.RegisterDependencyContainer() MUST run before builder.RegisterSignalBus() (its ctor injects IDependencyContainer). See signalbus-integration.md § “Required Bootstrap Order.”

Then the handler-lifecycle rules from code-conventions-unity.md:

  1. Subscribe with named methods, not lambdas. Lambdas have no reference equality — Unsubscribe silently fails and you leak callbacks across scene reloads.
  2. Always pair Subscribe in IStartable.Start() (or OnEnable) with Unsubscribe in IDisposable.Dispose() (or OnDisable). Track the IDisposable if 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>).

Twelve footguns from Phase C / Phase D delivery (BackpackCrawler 2026-05). Each is expanded in gotchas.md.

  1. Captured EntityManager in a closure. Struct copy goes stale after chunk move. Store World, call world.EntityManager fresh per use.
  2. CreateEntityQuery every frame. Cache in Start(); dispose in OnDestroy().
  3. Cached DynamicBuffer<T> reference across frames. Re-fetch each frame.
  4. No World.IsCreated guard on Play-mode exit. Guard every access.
  5. Bridge ISystem in LateSimulationSystemGroup. Use [UpdateInGroup(typeof(SimulationSystemGroup), OrderFirst = true)].
  6. Multiple LifetimeScopes each instantiating their own bridge. One app-level bridge (DontDestroyOnLoad) injected into every scene scope.
  7. Lambda SignalBus.Subscribe(...). No reference equality → Unsubscribe silently does nothing. Use named instance methods.
  8. Calling signalBus.Publish<T>(...). Use signalBus.Fire(in T) — GameFoundation’s SignalBus has no Publish method.
  9. EntityQuery.Dispose() without World.IsCreated guard. A Tier-2 bridge caching an EntityQuery must guard world.IsCreated AND check query != default BEFORE disposing in OnDestroy / OnDisable, else Play-mode exit throws ObjectDisposedException per bridge. Same rule for every cached Native* collection.
  10. RegisterSignalBus() before RegisterDependencyContainer(). SignalBus’s ctor injects IDependencyContainer; missing it produces an opaque VContainerException on first signal resolve.
  11. Calling Register{Feature}() without registering its ctor deps first. Extensions are NOT self-contained — e.g. BattlePassService needs 5 globally pre-registered deps.
  12. Typo’ing TheOne.Features.Time in usings. TimeService lives at TheOne.Feature.Time.Core.Servicessingular Feature.

→ See gotchas.md for the full failure mode + fix per item.

FileTopic
bootstrap-order.mdFrame-by-frame timeline, registration template, shutdown order, time-sync gate
managed-event-queue.mdManagedEventQueue<T> full source, bridge-system + bridge-MB pair, NativeQueue<T> variant
signalbus-integration.mdNamed-method subscribe/unsubscribe, IAsyncStartable, payload conventions, leak detection
gotchas.mdThe 12 anti-patterns above with full failure modes, repro symptoms, and fixes
  • t1k-unity-dots-core-ecs-core — IComponentData, ISystem, EntityManager, query patterns.
  • t1k-unity-dots-core-enableable-componentsIEnableableComponent patterns; GetSingletonEntity rejects enableable singletons (use IgnoreComponentEnabledState).
  • 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-testingDOTSTestBase fixture for the ECS-side unit tests in this skill.