Skip to content

t1k:unity:dots-core:bridges

FieldValue
Moduledots-core
Version2.3.2
Effortmedium
Tools

Keywords: AnalyticsBridge, AudioBridge, bridge, camera shake, CameraTrauma, DOTS bridge, dots-bridges, ECS to managed, FloatingText, IScreenShakeBridge, ManagedEventQueue, screen shake, ScreenShake, SignalBus, VFXBridge

/t1k:unity:dots-core:bridges

DOTS Bridges — universal ECS↔managed bridges

Section titled “DOTS Bridges — universal ECS↔managed bridges”

com.the1studio.dots-bridges (v1.0.0, renamed from com.the1studio.dots-ui-bridge) ships six production-ready bridges built on one shared primitive — ManagedEventQueue<T>. Each bridge follows the (Read, Write, Lifetime) triple so the hand-off from Burst-compiled ISystem to Unity-managed MonoBehaviour is leak-free, frame-accurate, and Play-mode-exit safe.

LayerSub-asmdefPurpose
CoreDOTSBridges.CoreManagedEventQueue<T>, DOTSBridgeMonoBehaviour<T>, IManagedRequestProcessor<T>
VContainerDOTSBridges.VContainerSignalBusDOTSBridgeMonoBehaviour<TEvent,TSignal>, registration extensions
AudioDOTSBridges.AudioIAudioBridge + AudioBridge + AudioBankSO — layers on DOTSCore.AudioEvent
VFXDOTSBridges.VFXIVFXBridge + VFXBridge + VFXBankSO + VFXEventType (13 categories)
AnalyticsDOTSBridges.AnalyticsIAnalyticsBridge + AnalyticsBridge + NoOpAnalyticsBridge fallback
UI/FloatingTextDOTSBridges.FloatingTextFloatingTextBridge + FloatingTextStyleBank + aggregator (damage / heal / gold / XP)
ScreenShakeDOTSBridges.ScreenShakeIScreenShakeBridge + CameraShakeDOTSBridgeMonoBehaviour (drain) + NoOpScreenShakeBridge + CameraShakeEvent DTO — drains DOTSCore.CameraTrauma
  • Burst ISystem needs to talk to managed UI / audio / VFX / analytics → use the matching bridge.
  • VContainer scope needs SignalBus.Fire(...) from ECS events → SignalBusDOTSBridgeMonoBehaviour.
  • UI controllers must enqueue requests back into ECS (claim reward, sell item) → IManagedRequestProcessor<TRequest> (the Write side).
  • Do NOT put a bridge in a Burst job. Bridges are managed by design — see Gotchas.
BridgeRead side (ECS → managed)Write side / Configuration
AudioAudioEnqueueSystem drains DynamicBuffer<DOTSCore.AudioEvent>ManagedEventQueue<AudioEvent>.InstanceAudioBridge MonoBehaviour + AudioBankSO (AudioEventTypeAudioClip)
VFXVFXEnqueueSystem drains DynamicBuffer<VFXSpawnRequest> → queueVFXBridge + VFXBankSO (VFXEventType → prefab)
AnalyticsAnalyticsEnqueueSystem drains DynamicBuffer<AnalyticsEvent> → queueAnalyticsBridge + IAnalyticsBridge backend (Firebase, AppsFlyer, custom)
FloatingTextFloatingTextEnqueueSystem + FloatingTextAggregatorSystem (coalesces damage by target)FloatingTextBridge + FloatingTextStyleBank (color → style SO) + IObjectPoolManager
SignalBus(inherits from DOTSBridgeMonoBehaviour<TEvent>)SignalBusDOTSBridgeMonoBehaviour<TEvent,TSignal> — override Map(in TEvent) → TSignal, calls signalBus.Fire(in signal)
ScreenShakeCameraShakeDOTSBridgeMonoBehaviour polls the DOTSCore.CameraTrauma decaying scalar in LateUpdate, edge-detects upward threshold crossings → IScreenShakeBridge.Shake(pos, intensity)RegisterBridge(IScreenShakeBridge) from a bootstrapper; defaults to NoOpScreenShakeBridge

→ See managed-event-queue-pattern.md for the (Read, Write, Lifetime) triple + bootstrap order.

ScreenShake seam (camera-shake third-party decoupling)

Section titled “ScreenShake seam (camera-shake third-party decoupling)”

The ScreenShake bridge is the library’s Recipe B managed-effect seam (see the library-decoupling skill): it decouples ECS combat trauma from any concrete shake backend (Cinemachine impulse, Feel MMF_Player, custom). Unlike the queue-based bridges, it drains the DOTSCore.CameraTrauma decaying scalar directly — no ManagedEventQueue, no DynamicBuffer.

  • IScreenShakeBridge — library-owned interface: void Shake(float3 position, float intensity). Carries only Unity.Mathematics types — no vendor type in the signature (decoupling rule objective test #4). Impls just play a shake; they MUST NOT re-apply edge-detection or ReduceMotion scaling.
  • CameraShakeDOTSBridgeMonoBehaviour — the drain. A [DisallowMultipleComponent] singleton (Instance) placed in the main scene (not SubScene). All policy lives HERE so every impl stays dumb: it polls CameraTrauma.Current, fires Shake only on an upward crossing of FireThreshold (default 0.05), applies ReducedMotionScale when DOTSCore.CameraAccessibility.ReduceMotion is set (falls back to PlayerPrefs["ReduceMotion"] pre-world), and resolves a spatial origin from the CameraTarget singleton (float3.zero if absent).
  • NoOpScreenShakeBridge — null-object default (precedent: NoOpAnalyticsBridge). Returned by the Bridge property when nothing is registered; Verbose=true logs each event for emission verification without dragging in any SDK. This is the graceful-degrade contract — never throw when no backend is wired.
  • CameraShakeEvent — a readonly struct managed DTO (Position, Intensity), stack-created in the drain and passed by value. NOT an IComponentData / IBufferElementData — do not bake it.

Registration (VContainer / bootstrapper):

// Place CameraShakeDOTSBridgeMonoBehaviour on a main-scene GameObject, then:
var drain = CameraShakeDOTSBridgeMonoBehaviour.Instance;
drain.RegisterBridge(new FeelScreenShakeBridge(myMMFPlayer)); // consumer-side impl
// or Cinemachine-only consumers self-register from CinemachineCameraBridge.
// No registration → drain falls back to NoOpScreenShakeBridge (logs only).

Double-consumer guard: when CameraShakeDOTSBridgeMonoBehaviour is present, DOTSCore.Camera.CinemachineCameraBridge detects it on its first LateUpdate (one-time Instance != null check) and suppresses its own ReadTraumaShake poll to prevent double-shake. Scenes without the drain MonoBehaviour (e.g. BattleDemo*) keep the legacy self-poll unchanged. The single registered impl IS the natural one-consumer guard — it replaces the old runtime FindFirstObjectByType double-consumer guard from the removed FeelCombatBridge.

// === Audio: emit a hit sound from a Burst combat system ===
[UpdateInGroup(typeof(CoreSystemGroup))]
public partial struct OnHitAudioSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
foreach (var (hit, audio) in SystemAPI
.Query<RefRO<HitEvent>, DynamicBuffer<AudioEvent>>())
{
audio.Add(AudioEvent.Create(
AudioEventType.Hit, hit.ValueRO.Position, volume: 1f, pitch: 1f));
}
}
}
// AudioEnqueueSystem drains the buffer this frame; AudioBridge plays in LateUpdate.
// === VFX: spawn a blood splat ===
vfxBuffer.Add(new VFXSpawnRequest {
Type = VFXEventType.Hit, Position = hitPos, Rotation = quaternion.identity,
Scale = new float3(1,1,1), Duration = 0.6f });
// === FloatingText: aggregated damage number ===
floatingBuffer.Add(new FloatingTextRequest {
Position = targetPos, Color = FloatingTextColor.Damage,
AggregationValue = damage, AggregationTarget = targetEntity,
Lifetime = 0f /* style default */ });
// === Analytics: level_complete event ===
analyticsBuffer.Add(AnalyticsEvent.Create(
new FixedString64Bytes("level_complete"),
new FixedString512Bytes("{\"level\":5,\"timeSec\":42.3}"),
timestampSec: (float)SystemAPI.Time.ElapsedTime));
// === SignalBus: bridge a custom XP event ===
public sealed class XPBridge : SignalBusDOTSBridgeMonoBehaviour<XPEvent, XPSignal>
{
protected override XPSignal Map(in XPEvent evt) => new XPSignal { Amount = evt.Amount };
}
// VContainer: builder.RegisterDOTSBridge<XPEvent>();

→ See audio-uses-dots-core-audioevent.md, vfx-bridge.md, analytics-bridge.md, floating-text-hoist.md for per-bridge wiring, bank authoring, override seams, and pool integration.

  1. DOTSCore.AudioEvent is the canonical audio event — do NOT duplicate it. The Audio bridge LAYERS on top: AudioEnqueueSystem reads the existing DynamicBuffer<DOTSCore.AudioEvent> (per-entity, cleaned by DOTSCore.CoreEventCleanupSystem). The 3-pass library search caught this — dots-core/Runtime/Core/Components/AudioEvent.cs already exists. NEVER introduce DOTSBridges.AudioEvent as a new type. → audio-uses-dots-core-audioevent.md.

  2. file: Unity packages cannot carry semver deps on other file: packages. Adding "com.the1studio.dots-bridges": "1.0.0" to the legacy dots-ui-bridge shim’s package.json triggered Package cannot be found from Unity’s package manager. Fix: drop the explicit version dep — asmdef-level assemblyReferences: on DOTSBridges.Core is sufficient for compile-time linkage. → file-package-deps-gotcha.md.

  3. Bridges are managed by design — never [BurstCompile] an enqueue system. ManagedEventQueue<T> wraps System.Collections.Concurrent.ConcurrentQueue<T>; Burst refuses to compile it. The enqueue ISystem runs un-Bursted, but the cost is negligible — it executes one foreach over a small per-frame buffer.

  4. Bridges target the managed↔ECS GAP, not the hot ECS path. Putting a bridge between two ISystems defeats the point. Use IEnableableComponent for stateless signals or DynamicBuffer<T> per-entity for payloaded ECS-internal events. Bridges are ONLY for ECS → MonoBehaviour / VContainer / SignalBus.

  5. 33 EditMode tests cover all 5 queue bridges (10 ManagedEventQueue + 6 Audio + 6 VFX + 6 Analytics + 5 FloatingText). Test runner discovery is currently blocked by pre-existing ChaosForge phantom namespaces (DOTSCore.UI.Bridge, DOTSProgression.Realm) — branch-isolation issue, NOT a bridge defect.

  6. Two ownership modes for ManagedEventQueue<T> — static Instance (sample / headless test fallback) vs DI-registered (production). Mixing both for the same T is anti-pattern #6 in the architecture research doc. Production projects ALWAYS register via builder.RegisterDOTSBridge<TEvent>().

  7. SignalBus.Fire(in T), NOT Publish<T>. GameFoundation SignalBus has no Publish method. Subscribe with NAMED methods, not lambdas — lambdas have no reference equality, so Unsubscribe silently leaks.

  8. OnDisable MUST nullify the World reference. DOTSBridgeMonoBehaviour<T> already handles this; consumer overrides MUST call base.OnDisable(). Forgetting → first scene reload after Play-mode exit throws ObjectDisposedException.

  9. Singleton-event drain systems must use an explicit EntityQuery, NOT SystemAPI, under wrapper delegation. ManagedSingletonEventDrainSystem (and any drain system that reads a singleton event buffer) threw a per-frame NullReferenceException when invoked through the bridge wrapper delegation path — surfaced via FloatingTextDrainSystem in ChaosForge Play mode. The SystemAPI.GetSingletonBuffer<T>() form resolves against the wrong system-state handle when the system runs as a delegated wrapper rather than a directly-scheduled ISystem. Fix: cache an explicit EntityQuery for the singleton in OnCreate and drain via query.GetSingletonBuffer<T>() / query.TryGetSingletonEntity<T>() instead of the SystemAPI accessor. Fixed in unity-dots-library PR #32 (5f33dff). Symptom to watch for: a drain system that compiles and runs fine standalone but NREs only when wrapped/delegated.

  10. ScreenShake drains CameraTrauma (a decaying scalar), NOT a buffer/queue — and is the ONLY non-queue bridge. CameraShakeDOTSBridgeMonoBehaviour edge-detects an upward FireThreshold crossing on DOTSCore.CameraTrauma.Current in LateUpdate; there is no ManagedEventQueue<CameraShakeEvent> and no DynamicBuffer. Consequences: (a) place the drain in the main scene, not a SubScene — it needs World.DefaultGameObjectInjectionWorld; (b) only ONE CameraTrauma consumer may run — when the drain MonoBehaviour exists, CinemachineCameraBridge suppresses its own ReadTraumaShake poll (one-time Instance != null check on first LateUpdate). If you add the drain to a scene that already had CinemachineCameraBridge doing trauma polling, you get the delegation automatically — do NOT also keep a FeelCombatBridge-style FindFirstObjectByType guard (that pattern is removed). (c) CameraShakeEvent is a managed readonly struct DTO passed by value — never bake it as IComponentData. (d) No backend registered → NoOpScreenShakeBridge (logs-only, Verbose opt-in); never throws. This is a Recipe-B decoupling seam — the vendor impl (Cinemachine impulse / Feel MMF_Player) is consumer-side, never in the library. → library-decoupling skill.

Bridges marshal ECS data to managed Unity APIs (AudioSource.Play, Object.Instantiate, Debug.Log, SignalBus.Fire). Standard guard: never enqueue user-controlled strings without length-clamping — AnalyticsEvent already enforces this via FixedString64Bytes/FixedString512Bytes.

  • dots-vcontainer-integration (legacy alias of t1k:unity:dots-core:vcontainer-integration) — the 12-anti-pattern playbook this package supersedes.
  • t1k:unity:dots-core:ecsDynamicBuffer<T> patterns the bridges drain.
  • t1k:unity:dots-core:enableable-components — alternative for stateless intra-ECS signals.
  • t1k:unity:audio — managed-side audio (FMOD/Wwise) the AudioBridge routes to.
  • library-decoupling — the engine-agnostic interface+provider seam pattern; IScreenShakeBridge is its Unity Recipe-B reference impl (core event boundary, consumer-side vendor backend).