# 6. Bridging ECS with GameObjects, UI and VFX Part of [[DOTS]]. Previous: [[5. Spatial Hashing Separation and Damage]]. Next: [[7. DOTS Profiling Quest and When Not to Use]]. Real projects are **hybrid**. The camera, player controller, UI, audio and animation stay on GameObjects, and only the horde lives in ECS. The bridge goes through `World.DefaultGameObjectInjectionWorld.EntityManager`. > [!note] Verification status > `World.DefaultGameObjectInjectionWorld`, `EntityManager.CreateEntity/SetComponentData/Exists/DestroyEntity`, `EntityManager.CreateEntityQuery`, `EntityQuery.CalculateEntityCount` and `EntityQuery.ToComponentDataArray<T>(Allocator)` are long-standing Entities 1.x APIs, kept in 6.5 as far as the docs show. `EntityManager.SetName` is used only inside `#if UNITY_EDITOR`. **Code not compiled.** ## 6.1 Player → ECS (a singleton written every frame) ```csharp // Assets/Scripts/DOTS/Bridge/PlayerEcsBridge.cs using Unity.Entities; using UnityEngine; public class PlayerEcsBridge : MonoBehaviour { public float auraRadius = 3f; public float auraDps = 4f; EntityManager em; Entity playerEntity; bool ready; void Start() { World world = World.DefaultGameObjectInjectionWorld; if (world == null || !world.IsCreated) { Debug.LogWarning("No default ECS world."); return; } em = world.EntityManager; playerEntity = em.CreateEntity(typeof(PlayerTarget)); #if UNITY_EDITOR em.SetName(playerEntity, "PlayerTarget"); #endif ready = true; } void Update() { if (!ready || !em.World.IsCreated) return; em.SetComponentData(playerEntity, new PlayerTarget { Position = transform.position, AuraRadius = auraRadius, AuraDps = auraDps, }); } void OnDestroy() { // The world may already be gone when the app quits. if (ready && em.World.IsCreated && em.Exists(playerEntity)) em.DestroyEntity(playerEntity); } } ``` `Update()` runs before the ECS `SimulationSystemGroup`? **Not guaranteed.** The default world's groups are injected into the player loop, and the exact order relative to MonoBehaviour `Update` is **unverified** here. A one-frame lag is invisible for a target position. If you need strict ordering, write the singleton from a managed `SystemBase` in `InitializationSystemGroup` that reads a static reference to the player. ## 6.2 GameObject input → ECS event (a telegraphed blast) **Telegraphing** ([[Level Up! (Review)]]): the blast shows a ring for 0.6 s before it fires, so players learn its reach. ```csharp // Assets/Scripts/DOTS/Bridge/BlastAbility.cs using System.Collections; using Unity.Entities; using UnityEngine; using UnityEngine.InputSystem; public class BlastAbility : MonoBehaviour { public InputActionReference blastAction; // e.g. Gameplay/Blast (Space / South button) public float radius = 8f; public float damage = 10f; public float telegraphSeconds = 0.6f; public float cooldown = 3f; public GameObject telegraphRing; // a flat ring mesh scaled to radius float nextReady; void OnEnable() => blastAction.action.Enable(); void Update() { if (Time.time < nextReady || !blastAction.action.WasPressedThisFrame()) return; nextReady = Time.time + cooldown; StartCoroutine(Blast()); } IEnumerator Blast() { Vector3 center = transform.position; if (telegraphRing) { telegraphRing.transform.SetPositionAndRotation(center, Quaternion.identity); telegraphRing.transform.localScale = Vector3.one * radius * 2f; telegraphRing.SetActive(true); } yield return new WaitForSeconds(telegraphSeconds); if (telegraphRing) telegraphRing.SetActive(false); World world = World.DefaultGameObjectInjectionWorld; if (world == null || !world.IsCreated) yield break; EntityManager em = world.EntityManager; Entity request = em.CreateEntity(typeof(BlastRequest)); em.SetComponentData(request, new BlastRequest { Center = center, Radius = radius, Damage = damage }); // BlastSystem (part 5) consumes it next simulation tick. } } ``` See [[Input System]] for setting up the action asset. Pair the blast with camera shake and a VFX burst ([[3. Worked Effect - Impact Sparks with One Graph]]) for **juice**. ## 6.3 ECS → UI Toolkit HUD ```csharp // Assets/Scripts/DOTS/Bridge/SwarmHud.cs using Unity.Entities; using UnityEngine; using UnityEngine.UIElements; public class SwarmHud : MonoBehaviour { public UIDocument document; // UXML has Labels named "alive" and "kills" public int spawnedTotal = 10000; Label alive, kills; EntityQuery enemies; bool ready; float nextRefresh; void Start() { var root = document.rootVisualElement; alive = root.Q<Label>("alive"); kills = root.Q<Label>("kills"); World world = World.DefaultGameObjectInjectionWorld; if (world == null) return; enemies = world.EntityManager.CreateEntityQuery(typeof(EnemyTag)); // prefab entities are excluded automatically ready = true; } void Update() { if (!ready || Time.unscaledTime < nextRefresh) return; nextRefresh = Time.unscaledTime + 0.1f; // 10 Hz is plenty for a counter (Dirty Flag spirit) int count = enemies.CalculateEntityCount(); alive.text = count.ToString("N0"); kills.text = (spawnedTotal - count).ToString("N0"); } } ``` For a localized HUD, bind these labels to Smart strings with `{count}` variables instead of setting `.text` ([[3. Smart Strings Plurals and Variables]]). ## 6.4 ECS → VFX Graph (render the swarm as particles) Entities Graphics renders each enemy as a mesh. As an alternative, or for distant LODs, stream positions into the `SwarmToVfx` buffer from [[5. VFX GraphicsBuffers and Audio-Reactive Effects]]: ```csharp // Assets/Scripts/DOTS/Bridge/SwarmVfxFeed.cs using Unity.Collections; using Unity.Entities; using Unity.Mathematics; using Unity.Transforms; using UnityEngine; public class SwarmVfxFeed : MonoBehaviour { public SwarmToVfx target; public Transform player; public float heatRadius = 6f; EntityQuery query; NativeArray<SwarmPoint> points; bool ready; void Start() { World world = World.DefaultGameObjectInjectionWorld; if (world == null || target == null) return; query = world.EntityManager.CreateEntityQuery( ComponentType.ReadOnly<EnemyTag>(), ComponentType.ReadOnly<LocalTransform>()); points = new NativeArray<SwarmPoint>(target.maxPoints, Allocator.Persistent); ready = true; } void LateUpdate() // after simulation has moved everyone this frame { if (!ready) return; // Completes jobs that write LocalTransform (a sync point), then copies. using NativeArray<LocalTransform> transforms = query.ToComponentDataArray<LocalTransform>(Allocator.Temp); int n = math.min(transforms.Length, points.Length); float3 p = player ? (float3)player.position : float3.zero; for (int i = 0; i < n; i++) { float3 pos = transforms[i].Position; points[i] = new SwarmPoint { position = pos, heat = 1f - math.saturate(math.distance(pos, p) / heatRadius), }; } target.Push(points, n); } void OnDestroy() { if (points.IsCreated) points.Dispose(); } } ``` **Explanation** - `ToComponentDataArray` creates a sync point: the main thread waits for the steering job. That's fine at this scale. To optimize, move the copy into a Burst `IJobEntity` that writes `SwarmPoint`s, and complete it in LateUpdate. - If VFX renders the swarm, remove the MeshRenderer from the enemy prefab so Entities Graphics doesn't draw a second copy. - VFX Graph needs compute shaders. Gate it with `VfxCapabilityGate` ([[1. VFX Graph Setup and Mental Model]]). ## 6.5 Patterns - **Observer / Event Queue**: `BlastRequest` entities are events. ECS systems are the listeners. - **Service Locator**: `World.DefaultGameObjectInjectionWorld` is a global locator. Keep its use in a few bridge scripts, not everywhere. - **Dirty Flag**: the HUD refreshes at 10 Hz, not every frame. ## Sources - World and EntityManager: https://docs.unity3d.com/Packages/[email protected]/manual/concepts-worlds.html - Entity queries: https://docs.unity3d.com/Packages/[email protected]/manual/systems-entityquery.html - `GraphicsBuffer.SetData`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/GraphicsBuffer.SetData.html