ECS Architecture with Unity // by Example Entitas // @entitas_csharp // github.com/sschmid/Entitas-CSharp Maxim Zaks // @icex33 // github.com/mzaks Simon Schmid // @s_schmid // github.com/sschmid
ECS Architecturewith Unity// by Example
Entitas // @entitas_csharp // github.com/sschmid/Entitas-CSharp
Maxim Zaks // @icex33 // github.com/mzaks
Simon Schmid // @s_schmid // github.com/sschmid
Unity pain points• Testability• Code sharing• Co-dependent logic• Querying• Deleting code
Entitas isopen sourcegithub.com/sschmid/Entitas-CSharp
Agenda:• Introduction to Entitas• User Input• Integration with Unity Collision System• Replayable games• Integration with Unity UI System• Quizz
Unity Components:Data + Behaviour
Entitas ComponentsData only
// Data only - no behaviour
public class PositionComponent : IComponent { public Vector3 value;}
public class MovableComponent : IComponent {}
+------------------+ | Pool | |------------------| | e e | +-----------+ | e e---|----> | Entity | | e e | |-----------| | e e e | | Component | | e e | | | +-----------+ | e e | | Component-|----> | Component | | e e e | | | |-----------| | e e e | | Component | | Data | +------------------+ +-----------+ +-----------+ | | | +-------------+ | | e | Groups | | e e | +---> | +------------+ | e | | | | e | e | e | +--------|----+ e | | e | | e e | +------------+
Logic?
public class VelocitySystem : IExecuteSystem {
public void Execute() {
var movables = Pools.pool.GetGroup( Matcher.AllOf( Matcher.Velocity, Matcher.Position ));
foreach (var e in movables.GetEntities()) { var pos = e.position.value; e.ReplacePosition(pos + e.velocity.value); } }}
SystemsChain of Responsibility
+------------+ +------------+ +------------+ +------------+| | | | | | | || System | +---> | System | +---> | System | +---> | System || | | | | | | |+------------+ +------------+ +------------+ +------------+
Systems in Entitas• Initialize System• Execute System• Reactive System
• only processes changed entities
Agenda:• Introduction to Entitas ✅• User Input• Integration with Unity Collision System• Replayable games• Integration with Unity UI System• Quizz
Demo ShmupShoot ’em up• Entitas Visual Debugging• Blueprints
public class InputController : MonoBehaviour {
void Update() { var inputPool = Pools.input;
// Move var moveX = Input.GetAxisRaw("Horizontal"); var moveY = Input.GetAxisRaw("Vertical"); inputPool.CreateEntity() .AddMoveInput(new Vector3(moveX, moveY)) .AddInputOwner("Player1");
// Shoot var fire = Input.GetAxisRaw("Fire1"); if (fire != 0) { inputPool.CreateEntity() .IsShootInput(true) .AddInputOwner("Player1"); } }}
public class CollisionEmitter : MonoBehaviour {
public string targetTag;
void OnCollisionEnter(Collision collision) { if (collision.gameObject.CompareTag(targetTag)) { var link = gameObject.GetEntityLink(); var targetLink = collision.gameObject.GetEntityLink();
Pools.input .CreateEntity() .AddCollision(link.entity, targetLink.entity); } }}
Great, butPerformance?
Unity vs Entitas1000 objects with 2 components
• Memory: 9x (2.9 MB vs 0.32 MB)• CPU: 17x (105ms vs 6ms)
Entitas...• reuses Entities• reuses Components• caches Groups• can index Components
Entitas...• reuses Entities• reuses Components• caches Groups• can index Components
so nice
Agenda:• Introduction to Entitas ✅
• User Input ✅
• Integration with Unity Collision System ✅• Replayable games• Integration with Unity UI System• Quizz
Demo Reactive UI
Show me your tables, and I won’t usually need your flowcharts; they’ll be obvious.— Fred Brooks
[SingleEntity]public class TickComponent : IComponent{ public long currentTick;}
[SingleEntity]public class ElixirComponent : IComponent{ public float amount;}
public class ConsumeElixirComponent : IComponent{ public int amount;}
[SingleEntity]public class PauseComponent : IComponent {}
public class TickUpdateSystem : IInitializeSystem, IExecuteSystem, ISetPool{ Pool _pool;
public void SetPool(Pool pool) { _pool = pool; }
public void Initialize() { _pool.ReplaceTick(0); }
public void Execute() { if (!_pool.isPause) { _pool.ReplaceTick(_pool.tick.currentTick + 1); } }}
public class ProduceElixirSystem : IInitializeSystem, IReactiveSystem, ISetPool{
public TriggerOnEvent trigger { get { return Matcher.Tick.OnEntityAdded(); }}
public void Initialize() { _pool.ReplaceElixir(0); }
public void Execute(List<Entity> entities) { var newAmount = _pool.elixir.amount + ProductionStep; newAmount = Math.Min(ElixirCapacity, newAmount); _pool.ReplaceElixir(newAmount); }}
public class ConsumeButtonBehaviour : MonoBehaviour, IPauseListener, IElixirListener {
//...
public void ButtonPressed() { if(Pools.pool.isPause) return; Pools.pool.CreateEntity().AddConsumeElixir(consumptionAmount); }}
[SingleEntity]public class ConsumptionHistoryComponent : IComponent{ public List<ConsumptionEntry> entries;}
public class ConsumptionEntry{ public readonly long tick; public readonly int amount;
public ConsumptionEntry(long tick, int amount) { this.tick = tick; this.amount = amount; }}
public class JumpInTimeComponent : IComponent{ public long targetTick;}
public class TimePickerBehaviour : MonoBehaviour, IPauseListener{
// ...
public void ChangedValue() { Pools.pool.ReplaceJumpInTime((long)GetComponent<Slider>().value); }}
ReplaySystemfor tick: 0..< targetTick
How do we get a reference to Logic Systems?
Dependency managment with Entitas[SingleEntity]public class LogicSystemsComponent : IComponent{ public Systems systems;}
pool.SetLogicSystems(new Systems() .Add(pool.CreateSystem<TickUpdateSystem>()) .Add(pool.CreateSystem<ProduceElixirSystem>()) .Add(pool.CreateSystem<ConsumeElixirSystem>()) .Add(pool.CreateSystem<PersistConsumeElixirSystem>()) .Add(pool.CreateSystem<ConsumeElixirCleanupSystem>()));
UI notification systems• NotifyTickListenersSystem• NotifyPauseListenersSystem• NotifyElixirListenersSystem
public class NotifyTickListenersSystem : IReactiveSystem, ISetPool{ Group listeners;
public TriggerOnEvent trigger { get { return Matcher.Tick.OnEntityAddedOrRemoved(); } }
public void SetPool(Pool pool) { listeners = pool.GetGroup(Matcher.TickListener); }
public void Execute(List<Entity> entities) { var e = entities[0]; foreach (var entity in listeners.GetEntities()) { entity.tickListener.value.TickChanged(e.tick.currentTick); } }}
public interface ITickListener { void TickChanged(long currentTick);}
public interface IPauseListener { void PauseStateChanged(bool isPaused);}
public interface IElixirListener { void ElixirAmountChanged(float amount);}
Quiz!
Tick Listener?
Tick Listener!
Elixir Listener?
Elixir Listener!
Pause Listener?
Pause Listener!
public class ConsumeButtonBehaviour : MonoBehaviour, IPauseListener, IElixirListener{ void Awake() { Pools.pool.CreateEntity() .AddPauseListener(this) .AddElixirListener(this); }
public void PauseStateChanged (bool isPaused) { GetComponent<Button>().enabled = !isPaused; }
public void ElixirAmountChanged (float amount) { var ratio = 1 - Mathf.Min(1f, (amount / (float)consumptionAmount)); progressBox.fillAmount = ratio; GetComponent<Button>().enabled = (System.Math.Abs (ratio - 0) < Mathf.Epsilon); }}
Recap• Control your input, even if it is time• Input is a state change• Simulation is state change over time• State change drives UI updates• Persist only relevant parts of state for replay• Use Entitas to manage dependencies
Agenda:• Introduction to Entitas ✅
• User Input ✅
• Integration with Unity Collision System ✅
• Replayable games ✅
• Integration with Unity UI System ✅
• Quizz ✅
Bonus ThoughtHow hard is it to implement:• Multiplayer game• BackEnd validation• Tutorial and Quest systems• Any kind of Logging
Questions?#Entitas
@entitas_csharp
github.com/sschmid/Entitas-CSharp