# 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