Notifications
Article
Unity ECS: Some system samples
Published 6 months ago
405
0
What we can do in systems, some fun with experiments
I'll play around an API and make some stuff, it doesn't need to be useful but can. Experiment and have fun with me. Let's write something finally.

Singleton System - hated and loved antipattern

A Usage Example

public struct Player : IComponentData { public int SomeFloat; } public class PlayerSystem : SingletonComponentSystem<Player> { protected override void OnUpdate() { // do your stuff var comp = Component; comp.SomeFloat = Time.deltaTime; Component = comp; } }
[UpdateAfter(typeof(PlayerSystem))] // we need to be sure PlayerSystem is initialized before accessing it public class SomeSystem : ComponentSystem { protected override void OnUpdate() { var compInstance = PlayerSystem.Instance.Component; compInstance.SomeFloat = Time.deltaTime + 1024; // edit player's float PlayerSystem.Instance.Component = compInstance; } }

What's inside SingletonComponentSystem<T> ?

public abstract class SingletonComponentSystem<T> : ComponentSystem where T : struct, IComponentData { public static SingletonComponentSystem<T> Instance { get; private set; } [Inject] protected FilterGroup group; protected struct FilterGroup { public readonly int Length; [ReadOnly] public EntityArray Entities; public ComponentDataArray<T> Component; } public T Component { get => group.Component[0]; set => group.Component[0] = value; } protected override void OnStartRunning() { if (Instance == null) { Instance = this; } else if (group.Length > 1) { for (int i = 1; i < group.Length; i++) { PostUpdateCommands.DestroyEntity(group.Entities[i]); } Debug.LogWarning($"There were {group.Length - 1} more entities with instance of {typeof(T)}!"); } } }


Short Component System - let's write less code

For now you would write system something like this:
public class SomeComponentSystem : ComponentSystem { [Inject] FilterGroup group; struct FilterGroup { public readonly int Length; public ComponentDataArray<SomeComponent> SomeComponents; } protected override void OnUpdate() { float dt = Time.deltaTime; for (int i = 0; i < group.Length; i++) { var some = group.SomeComponents[i]; some.FloatingNum = dt; group.SomeComponents[i] = some; } } }
But with this, you can make a little change and write just:
public class SomeComponentSystem : ComponentSystem<SomeComponent> { protected override void SetUpdate() { float dt = Time.deltaTime; Execute = (ref SomeComponent some) => { some.FloatingNum = dt; }; } }

What's inside ComponentSystem<T> ?

public abstract class ComponentSystem<T0> : ComponentSystem where T0 : struct, IComponentData { [Inject] protected FilterGroup Group; protected delegate void UpdateHandler(ref T0 data0); protected UpdateHandler Execute = delegate { Debug.LogWarning("You didn't initialized 'Execute'"); }; protected struct FilterGroup { public readonly int Length; public ComponentDataArray<T0> data0; } protected abstract void SetUpdate(); protected override void OnUpdate() { SetUpdate(); for (int i = 0; i < Group.Length; i++) { T0 data = Group.data0[i]; Execute(ref data); Group.data0[i] = data; } } }

Can we do it even more friendly? Sure, but with this you miss some optimization:

public abstract class ComponentSystem<T0> : ComponentSystem where T0 : struct, IComponentData { [Inject] protected FilterGroup Group; protected struct FilterGroup { public readonly int Length; public ComponentDataArray<T0> data0; } protected abstract void Execute(ref T0 data0); protected override void OnUpdate() { for (int i = 0; i < Group.Length; i++) { T0 data = Group.data0[i]; Execute(ref data); Group.data0[i] = data; } } }
public class SomeComponentSystem : ComponentSystem<SomeComponent> { protected override void Execute(ref SomeComponent some) { var dt = Time.deltaTime; // miss little optimization - recreating dt for each entity some.FloatingNum = dt; } }
But do you really need to miss it?
public abstract class ComponentSystem<T0> : ComponentSystem where T0 : struct, IComponentData { [Inject] protected FilterGroup Group; protected struct FilterGroup { public readonly int Length; public ComponentDataArray<T0> data0; } protected abstract void Execute(ref T0 data0); protected virtual void OnEarlyUpdate() { } protected virtual void OnLateUpdate() { } protected override void OnUpdate() { OnEarlyUpdate(); for (int i = 0; i < Group.Length; i++) { T0 data = Group.data0[i]; Execute(ref data); Group.data0[i] = data; } OnLateUpdate(); } }
public class SomeComponentSystem : ComponentSystem<SomeComponent> { private float dt; protected override void OnEarlyUpdate() { dt = Time.deltaTime; } protected override void Execute(ref SomeComponent some) { some.FloatingNum = dt; } }

Single Fire System - system that "shoots" once

Cameron MacFarland has an article about this pattern, I recommend you to read it THERE to get a more in-depth explanation about it. I modified it a bit to fit my convenience.

A Usage Example:

We will check if our entity left our map borders if so, deal damage until it returns to map.
public struct Damage : IComponentData { public int Value; } public class DamageBarrier : BarrierSystem { } public class DamageSystem : SingleFireConcurrentSystem<Damage, DamageBarrier> { // [BurstCompile] // buffer doesn't work with burst properly for now (preview 20) struct Job : IJobProcessComponentData<Damage, Health> { public void Execute([ReadOnly] ref Damage damage, ref Health health) { health.Value -= damage.Value; } } protected override JobHandle CreateJobHandle(JobHandle inputDeps) { return new Job().Schedule(this, inputDeps); } }
public class OutOfMapCheckBarrier : BarrierSystem { } public class OutOfMapCheckSystem : JobComponentSystem { [Inject] FilterGroup group; [Inject] OutOfMapCheckBarrier barrier; struct FilterGroup { public readonly int Length; [ReadOnly] public EntityArray Entities; [ReadOnly] public ComponentDataArray<Position2D> Positions; [ReadOnly] public SubtractiveComponent<Damage> Damage; } // [BurstCompile] // buffer doesn't work with burst properly for now (preview 20) struct Job : IJobParallelFor { [ReadOnly] public EntityArray entities; [ReadOnly] public ComponentDataArray<Position2D> positions; public EntityCommandBuffer.Concurrent buffer; public void Execute(int index) { if (IsOutOfBorder(positions[index].Value)) { buffer.AddComponent(entities[index], new Damage { Value = 1 }); } } private bool IsOutOfBorder(float2 value) { // Some placeholder size of map return value.x > 100 || value.x < -100 || value.y > 100 || value.y < -100; } } protected override JobHandle OnUpdate(JobHandle inputDeps) { return new Job { buffer = barrier.CreateCommandBuffer(), entities = group.Entities, positions = group.Positions }.Schedule(group.Length, 16, inputDeps); } }

What's inside SingleFireConcurrentSystem<TComponent, TBarrier> ?

public abstract class SingleFireConcurrentSystem<TComponent, TBarrier> : JobComponentSystem where TComponent : struct, IComponentData where TBarrier : BarrierSystem { [Inject] protected FilterGroup Group; [Inject] protected TBarrier Barrier; protected struct FilterGroup { public readonly int Length; [ReadOnly] public EntityArray Entities; [ReadOnly] public ComponentDataArray<TComponent> Component; } // [BurstCompile] // buffer doesn't work with burst properly for now (preview 20) private struct Job : IJobParallelFor { [ReadOnly] public EntityArray entities; public EntityCommandBuffer.Concurrent buffer; public void Execute(int index) => buffer.RemoveComponent<TComponent>(entities[index]); } protected override JobHandle OnUpdate(JobHandle inputDeps) { return new Job { entities = Group.Entities, buffer = Barrier.CreateCommandBuffer() }.Schedule(Group.Length, 16, CreateJobHandle(inputDeps)); } protected abstract JobHandle CreateJobHandle(JobHandle inputDeps); }

Single Fire Cooldown System - fire system after delay


A Usage Example

We will make simple teleportation. We don't want to teleport our entity instantly, so we'll make a bit delay
public struct Teleportation2D : IComponentData { public float2 NextPosition; } public class DelayedTeleportBarrier : BarrierSystem { } public class DelayedTeleportSystem : SingleFireConcurrentCooldownSystem<Teleportation2D, DelayedTeleportBarrier> { // [BurstCompile] // buffer doesn't work with burst properly for now (preview 20) struct Job : IJobProcessComponentData<Position2D, Teleportation2D> { public void Execute(ref Position2D pos, [ReadOnly] ref Teleportation2D teleport) { pos.Value = teleport.NextPosition; } } protected override JobHandle CreateJobHandle(JobHandle inputDeps) { return new Job().Schedule(this, inputDeps); } }
public class PortalBarrier : BarrierSystem { } // This probably isn't a correct way to have portals but system would be too big to be just a small example, so it's very simplified public class PortalSystem : JobComponentSystem { [Inject] FilterGroup group; [Inject] PortalBarrier barrier; struct FilterGroup { public readonly int Length; [ReadOnly] public EntityArray Entities; [ReadOnly] public ComponentDataArray<Position2D> Positions; [ReadOnly] public SubtractiveComponent<Teleportation2D> Teleportation; } // [BurstCompile] // buffer doesn't work with burst properly for now (preview 20) struct Job : IJobParallelFor { public EntityCommandBuffer.Concurrent buffer; [ReadOnly] public EntityArray entities; [ReadOnly] public ComponentDataArray<Position2D> positions; public void Execute(int index) { if (IsInPortal(positions[index].Value)) { float2 nextPos = new float2(0); // simplified next positon getter buffer.AddComponent(entities[index], new Cooldown { Value = 2 }); // make 2s cooldown buffer.AddComponent(entities[index], new Teleportation2D { NextPosition = nextPos }); // Teleport after cooldown } } private bool IsInPortal(float2 value) { float2 portalPosition = new float2(100, 100); // simplified portal position getter return math.distance(value, portalPosition) <= 1; } } protected override JobHandle OnUpdate(JobHandle inputDeps) { return new Job { buffer = barrier.CreateCommandBuffer(), entities = group.Entities, positions = group.Positions }.Schedule(group.Length, 16, inputDeps); } }

What's inside SingleFireConcurrentCooldownSystem<TComponent, TBarrier> ?

public struct Cooldown : IComponentData { public float Value; } public abstract class SingleFireConcurrentCooldownSystem<TComponent, TBarrier> : JobComponentSystem where TComponent : struct, IComponentData where TBarrier : BarrierSystem { [Inject] protected FilterGroup Group; [Inject] protected TBarrier Barrier; protected struct FilterGroup { public readonly int Length; [ReadOnly] public EntityArray Entities; [ReadOnly] public ComponentDataArray<TComponent> Component; public ComponentDataArray<Cooldown> Cooldown; } // [BurstCompile] // buffer doesn't work with burst properly for now (preview 20) private struct CooldownJob : IJobParallelFor { [ReadOnly] public EntityArray entities; public float dt; public ComponentDataArray<Cooldown> cooldown; public EntityCommandBuffer.Concurrent buffer; [WriteOnly] public NativeArray<int> shouldFire; public void Execute(int index) { var cd = cooldown[index]; cd.Value -= dt; cooldown[index] = cd; if (cd.Value <= 0.0f) { shouldFire[0] = 1; buffer.RemoveComponent<Cooldown>(entities[index]); } } } // [BurstCompile] // buffer doesn't work with burst properly for now (preview 20) private struct SingleFireJob : IJobParallelFor { [ReadOnly] public EntityArray entities; public EntityCommandBuffer.Concurrent buffer; public void Execute(int index) => buffer.RemoveComponent<TComponent>(entities[index]); } protected override JobHandle OnUpdate(JobHandle inputDeps) { // NativeContainer allows us to get result from job NativeArray<int> shouldFireInt = new NativeArray<int>(1, Allocator.Temp); var buffer = Barrier.CreateCommandBuffer(); var cooldownHandle = new CooldownJob { entities = Group.Entities, dt = Time.deltaTime, cooldown = Group.Cooldown, buffer = buffer, shouldFire = shouldFireInt }.Schedule(Group.Length, 16, inputDeps); cooldownHandle.Complete(); bool shouldFire = shouldFireInt[0] > 0; shouldFireInt.Dispose(); if (shouldFire) { return new SingleFireJob { entities = Group.Entities, buffer = buffer }.Schedule(Group.Length, 16, CreateJobHandle(inputDeps)); } else { return inputDeps; } } protected abstract JobHandle CreateJobHandle(JobHandle inputDeps); }

Unique Death System - simple, unique system to kill entity


public struct Death : IComponentData { } public class DeathBarrier : BarrierSystem { } public class DeathSystem : JobComponentSystem { [Inject] FilterGroup group; [Inject] DeathBarrier barrier; struct FilterGroup { public readonly int Length; [ReadOnly] public EntityArray Entities; [ReadOnly] public ComponentDataArray<Death> DeathTag; } [BurstCompile] struct Job : IJobParallelFor { public EntityCommandBuffer.Concurrent buffer; [ReadOnly] public EntityArray entities; public void Execute(int index) => buffer.DestroyEntity(entities[index]); } protected override JobHandle OnUpdate(JobHandle inputDeps) { return new Job { buffer = barrier.CreateCommandBuffer(), entities = group.Entities }.Schedule(group.Length, 16, inputDeps); } }
So we can use easily kill entity by adding Death Component to it
public struct Health : IComponentData { public float Value; } public class HealthBarrier : BarrierSystem { } public class HealthSystem : JobComponentSystem { [Inject] HealthBarrier barrier; [Inject] FilterGroup group; struct FilterGroup { public readonly int Length; [ReadOnly] public EntityArray Entities; [ReadOnly] public ComponentDataArray<Health> Health; [ReadOnly] public SubtractiveComponent<Death> Death; // we don't want to add multiple deaths } // [BurstCompile] // buffer doesn't work with burst properly for now (preview 20) struct Job : IJobParallelFor { [ReadOnly] public EntityArray entities; [ReadOnly] public ComponentDataArray<Health> Health; public EntityCommandBuffer.Concurrent buffer; public void Execute(int index) { if (Health[index].Value <= 0) { buffer.AddComponent(entities[index], new Death()); } } } protected override JobHandle OnUpdate(JobHandle inputDeps) { return new Job { buffer = barrier.CreateCommandBuffer(), entities = group.Entities, Health = group.Health }.Schedule(group.Length, 16, inputDeps); } }
Give me some feedback in the comment section, don't worry I won't hate you if you show me some "anomaly" in my article. If you enjoy my article - like it and follow me. It'll motivate me to write more articles. See you.

Tomasz Piowczyk
Student Game Developer - Programmer
8
Comments