t1k:unity:dots-core:bridges
| Field | Value |
|---|---|
| Module | dots-core |
| Version | 2.3.2 |
| Effort | medium |
| Tools | — |
Keywords: AnalyticsBridge, AudioBridge, bridge, camera shake, CameraTrauma, DOTS bridge, dots-bridges, ECS to managed, FloatingText, IScreenShakeBridge, ManagedEventQueue, screen shake, ScreenShake, SignalBus, VFXBridge
How to invoke
Section titled “How to invoke”/t1k:unity:dots-core:bridgesDOTS Bridges — universal ECS↔managed bridges
Section titled “DOTS Bridges — universal ECS↔managed bridges”Overview
Section titled “Overview”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.
| Layer | Sub-asmdef | Purpose |
|---|---|---|
| Core | DOTSBridges.Core | ManagedEventQueue<T>, DOTSBridgeMonoBehaviour<T>, IManagedRequestProcessor<T> |
| VContainer | DOTSBridges.VContainer | SignalBusDOTSBridgeMonoBehaviour<TEvent,TSignal>, registration extensions |
| Audio | DOTSBridges.Audio | IAudioBridge + AudioBridge + AudioBankSO — layers on DOTSCore.AudioEvent |
| VFX | DOTSBridges.VFX | IVFXBridge + VFXBridge + VFXBankSO + VFXEventType (13 categories) |
| Analytics | DOTSBridges.Analytics | IAnalyticsBridge + AnalyticsBridge + NoOpAnalyticsBridge fallback |
| UI/FloatingText | DOTSBridges.FloatingText | FloatingTextBridge + FloatingTextStyleBank + aggregator (damage / heal / gold / XP) |
| ScreenShake | DOTSBridges.ScreenShake | IScreenShakeBridge + CameraShakeDOTSBridgeMonoBehaviour (drain) + NoOpScreenShakeBridge + CameraShakeEvent DTO — drains DOTSCore.CameraTrauma |
When to use
Section titled “When to use”- Burst
ISystemneeds 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.
Core API (6 bridges)
Section titled “Core API (6 bridges)”| Bridge | Read side (ECS → managed) | Write side / Configuration |
|---|---|---|
| Audio | AudioEnqueueSystem drains DynamicBuffer<DOTSCore.AudioEvent> → ManagedEventQueue<AudioEvent>.Instance | AudioBridge MonoBehaviour + AudioBankSO (AudioEventType → AudioClip) |
| VFX | VFXEnqueueSystem drains DynamicBuffer<VFXSpawnRequest> → queue | VFXBridge + VFXBankSO (VFXEventType → prefab) |
| Analytics | AnalyticsEnqueueSystem drains DynamicBuffer<AnalyticsEvent> → queue | AnalyticsBridge + IAnalyticsBridge backend (Firebase, AppsFlyer, custom) |
| FloatingText | FloatingTextEnqueueSystem + 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) |
| ScreenShake | CameraShakeDOTSBridgeMonoBehaviour 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 onlyUnity.Mathematicstypes — 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 pollsCameraTrauma.Current, firesShakeonly on an upward crossing ofFireThreshold(default 0.05), appliesReducedMotionScalewhenDOTSCore.CameraAccessibility.ReduceMotionis set (falls back toPlayerPrefs["ReduceMotion"]pre-world), and resolves a spatial origin from theCameraTargetsingleton (float3.zeroif absent).NoOpScreenShakeBridge— null-object default (precedent:NoOpAnalyticsBridge). Returned by theBridgeproperty when nothing is registered;Verbose=truelogs each event for emission verification without dragging in any SDK. This is the graceful-degrade contract — never throw when no backend is wired.CameraShakeEvent— areadonly structmanaged DTO (Position,Intensity), stack-created in the drain and passed by value. NOT anIComponentData/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.
Examples
Section titled “Examples”// === 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.
Gotchas
Section titled “Gotchas”-
DOTSCore.AudioEventis the canonical audio event — do NOT duplicate it. The Audio bridge LAYERS on top:AudioEnqueueSystemreads the existingDynamicBuffer<DOTSCore.AudioEvent>(per-entity, cleaned byDOTSCore.CoreEventCleanupSystem). The 3-pass library search caught this —dots-core/Runtime/Core/Components/AudioEvent.csalready exists. NEVER introduceDOTSBridges.AudioEventas a new type. → audio-uses-dots-core-audioevent.md. -
file:Unity packages cannot carry semver deps on otherfile:packages. Adding"com.the1studio.dots-bridges": "1.0.0"to the legacydots-ui-bridgeshim’spackage.jsontriggeredPackage cannot be foundfrom Unity’s package manager. Fix: drop the explicit version dep — asmdef-levelassemblyReferences:onDOTSBridges.Coreis sufficient for compile-time linkage. → file-package-deps-gotcha.md. -
Bridges are managed by design — never
[BurstCompile]an enqueue system.ManagedEventQueue<T>wrapsSystem.Collections.Concurrent.ConcurrentQueue<T>; Burst refuses to compile it. The enqueueISystemruns un-Bursted, but the cost is negligible — it executes oneforeachover a small per-frame buffer. -
Bridges target the managed↔ECS GAP, not the hot ECS path. Putting a bridge between two
ISystems defeats the point. UseIEnableableComponentfor stateless signals orDynamicBuffer<T>per-entity for payloaded ECS-internal events. Bridges are ONLY for ECS → MonoBehaviour / VContainer / SignalBus. -
33 EditMode tests cover all 5 queue bridges (10
ManagedEventQueue+ 6Audio+ 6VFX+ 6Analytics+ 5FloatingText). Test runner discovery is currently blocked by pre-existing ChaosForge phantom namespaces (DOTSCore.UI.Bridge,DOTSProgression.Realm) — branch-isolation issue, NOT a bridge defect. -
Two ownership modes for
ManagedEventQueue<T>— staticInstance(sample / headless test fallback) vs DI-registered (production). Mixing both for the sameTis anti-pattern #6 in the architecture research doc. Production projects ALWAYS register viabuilder.RegisterDOTSBridge<TEvent>(). -
SignalBus.Fire(in T), NOTPublish<T>. GameFoundationSignalBushas noPublishmethod. Subscribe with NAMED methods, not lambdas — lambdas have no reference equality, soUnsubscribesilently leaks. -
OnDisableMUST nullify the World reference.DOTSBridgeMonoBehaviour<T>already handles this; consumer overrides MUST callbase.OnDisable(). Forgetting → first scene reload after Play-mode exit throwsObjectDisposedException. -
Singleton-event drain systems must use an explicit
EntityQuery, NOTSystemAPI, under wrapper delegation.ManagedSingletonEventDrainSystem(and any drain system that reads a singleton event buffer) threw a per-frameNullReferenceExceptionwhen invoked through the bridge wrapper delegation path — surfaced viaFloatingTextDrainSystemin ChaosForge Play mode. TheSystemAPI.GetSingletonBuffer<T>()form resolves against the wrong system-state handle when the system runs as a delegated wrapper rather than a directly-scheduledISystem. Fix: cache an explicitEntityQueryfor the singleton inOnCreateand drain viaquery.GetSingletonBuffer<T>()/query.TryGetSingletonEntity<T>()instead of theSystemAPIaccessor. 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. -
ScreenShake drains
CameraTrauma(a decaying scalar), NOT a buffer/queue — and is the ONLY non-queue bridge.CameraShakeDOTSBridgeMonoBehaviouredge-detects an upwardFireThresholdcrossing onDOTSCore.CameraTrauma.CurrentinLateUpdate; there is noManagedEventQueue<CameraShakeEvent>and noDynamicBuffer. Consequences: (a) place the drain in the main scene, not a SubScene — it needsWorld.DefaultGameObjectInjectionWorld; (b) only ONECameraTraumaconsumer may run — when the drain MonoBehaviour exists,CinemachineCameraBridgesuppresses its ownReadTraumaShakepoll (one-timeInstance != nullcheck on firstLateUpdate). If you add the drain to a scene that already hadCinemachineCameraBridgedoing trauma polling, you get the delegation automatically — do NOT also keep aFeelCombatBridge-styleFindFirstObjectByTypeguard (that pattern is removed). (c)CameraShakeEventis a managedreadonly structDTO passed by value — never bake it asIComponentData. (d) No backend registered →NoOpScreenShakeBridge(logs-only,Verboseopt-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-decouplingskill.
Security
Section titled “Security”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.
Related skills
Section titled “Related skills”dots-vcontainer-integration(legacy alias oft1k:unity:dots-core:vcontainer-integration) — the 12-anti-pattern playbook this package supersedes.t1k:unity:dots-core:ecs—DynamicBuffer<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) theAudioBridgeroutes to.library-decoupling— the engine-agnostic interface+provider seam pattern;IScreenShakeBridgeis its Unity Recipe-B reference impl (core event boundary, consumer-side vendor backend).