Search Unity

Let us share our ECS patterns!

Discussion in 'Data Oriented Technology Stack' started by ComteJaner, Sep 13, 2018.

  1. ComteJaner

    ComteJaner

    Joined:
    Jun 9, 2013
    Posts:
    9
    Hello everyone!

    I think one neat thing about ECS is that we can have something that requires far less work than an actual sample/demo and still gives us useful insights about patterns and good usage of ECS : we can just share our list of Components, Systems and Archetypes (or other often used Entity "signatures") in our games, projects, or part of it (restricted to a "World" for exemple). Or aven a very simplified version of it.

    I believe that just with this list, you can learn a lot about the logic and design of a given ECS-driven game system. For exemple, how one managed his input/control, his event-driven mechanics, his specific Physics Simulation (boids was a neat exemple, maybe fire spreading? or fluids?), his crafting, his inventory, his procedural terrain or even higher level, his small STR, his turn-based tactics game, his AI etc...

    It is difficult to find good ressources on robust patterns for specific mechanics, or type of mechanics in ECS, so I think it could be something nice! Also I believe it would be quite cost-effective in term of effort required vs insight offered...

    The bonus would be to explain how you approached the problem, and reframed your mechanics in term of Data and Systems that operates on it.

    I do not know, maybe I am wrong, but I believe this would be useful to many!
     
    PhilSA and Spy-Shifty like this.
  2. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    5,875
    You are not wrong. And idea is neat. Only the problem is ECS itself at current stage.
    It evolving dynamically. And what is working today, tomorrow appears not working.

    However, I do work on ECS project, but since things I got scattered and I am continuously learning, only thing solid to share atm, I have 3 samples, which I hope still working.

    ECS with 3 very basics Hybrid, Pure and Job System examples
    https://github.com/Antypodish/Unity-ECS-basics-Hybrid-Pure-Job-Systems-examples
     
  3. Micz84

    Micz84

    Joined:
    Jul 21, 2012
    Posts:
    242
    I am not experienced DOD and ECS, but I come up with this pattern.
    When I need to have some components to be added and then removed after some time passes, for example, a player is invulnerable for 1 second after being hit or can't shoot because is reloading, and other buffs and debuffs.
    First I have CountDown with a float containing time left and CountDownEnded which is a tag component. CountDownSystem decreases countdown and when it is 0 or less adds CountDownEnded.
    An entity also has LinkedEntity component which has EntityID linked with it and LinkedComponent which has a field with ComponentType. There is a system that marks those entities with RemoveLinkedComponent tag when CountDownEnded is added to them. Then I have a RemoveComponentFromLinkedSystem which works on entities that have LinkedEntity, LinkedComponent, and RemoveLinkedComponent that removes the component from the linked entity and destroys this entity.
    I have created the mark for remove and remove system this way I can have a not only timer based linked components in future. I hope you can understand how it works from what I have written :) Any suggestions and critics are welcome :)
     
  4. ComteJaner

    ComteJaner

    Joined:
    Jun 9, 2013
    Posts:
    9
    Ok maybe my idea was a bit early! But I think the implementatio is evolving, but the general principles will be the same!

    In my project I only use basic ECS, like for movement and economic simulation (very simple stuff, very few interaction).
    For UI I use old style but I want to switch to ECS eventually...
     
  5. Spy-Shifty

    Spy-Shifty

    Joined:
    May 5, 2011
    Posts:
    529
    For UI, I use my own framework -> MVC pattern with binding concerns to systems which acts as controller. This gives me the most flexible solution. If anybody is interested in more details just ask me.

    Event System Pattern:
    Basically an event is an entity with a specific event component which holds the necessary information about the event. This could be sender and receiver Entities or other data. To fire an event just create a new Entity and attach the specific event component to it.

    My recommendation:
    An event should by definition be a rarely used message. The creator of this should also be responsible for the deletion.

    Simple example:
    Code (CSharp):
    1. struct SomeEvent : IComponentData {
    2.      public Entity Sender;
    3.      public int SomeData;
    4. }
    5.  
    6. public SomeEventTrigger : ComponentSystem {
    7.    struct EventData {
    8.        public ComponentDataArray<SomeEvent> Events;
    9.        public EntityArray Entities;
    10.        public readonly int Length;
    11.    }
    12.    [Inject] EventData eventData:
    13.    void Update(){
    14.         for(int i = 0; i< eventData.Length; i++) {
    15.             PostCommandUpdate.Destroy(eventData.Entities[i]);
    16.         }
    17.  
    18.  
    19.         // Some conditions...
    20.         PostCommandUpdate.CreateEntity();
    21.         PostCommandUpdate.AddComponent(new SomeEvent { Sender = sender, Data = data });
    22.   }
    23.  
    24.  
    25. }
    But sometimes this isn't possible (events created by MonoBeahviour)
    Case 1 - You have only one system that work with the event:
    Than this system can delete the event entity.
    (Attention: Error error prone for extending behaviour)

    Case 2 - You have multiple systems that work with the event:
    Than you should use barriers for creation and deletion to garantie every system can see and work on the event.
    (more work load but save in execution and extensibility)

    Maybe I can write more later ...
     
  6. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    7,135
    Good point I think once you start to make generic ECS systems then you can have a smaller number of systems do more work.

    For a possible bad example the Twin Stick Shooter Sample has multiple overlapping systems for each entity...

    3 Removal systems
    EnemyRemovalSystem.cs
    RemoveDeadSystem.cs
    ShotDestroySystem.cs​

    2 Shoot systems
    EnemyShootSystem.cs
    PlayerMoveSystem.cs //shoots and moves​

    2 Spawn systems
    EnemySpawnSystem.cs
    ShotSpawnSystem.cs​

    MoveForwardSystem.cs
    PlayerInputSystem.cs
    ShotDamageSystem.cs
    ComponentTypes.cs
    TwoStickBootstrap.cs
    TwoStickSettings.cs
    UpdatePlayerHud.cs

    Could these 14 files be reduced to...

    ComponentTypes.cs

    RemovalSystem.cs
    ShootSystem.cs
    MoveSystem.cs
    SpawnSystem.cs

    PlayerInputSystem.cs
    DamageSystem.cs
    TwoStickBootsrap.cs
    TwoStickSettings.cs
    UpdateHud.cs​

    10 files with more generic and potentially reusable systems.
     
  7. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    5,875
    I maybe misunderstand @Arowx your intention, then correct me please.
    But yes you can shrink the code into fewer files.
    however, wouldn't that defeat the Single Responsibility Principle?
     
  8. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    7,135
    Not if the Single Responsibility Principle is regard to the functional processing applied to data and not the domain models or games style of objects.

    A MovementSystem could as easily move bullets, enemies and players just by taking the delta time, current position, rotation and move data. Why write separate functions just for different game elements types when the functional processing for all of them is the same.

    If you look at the data in and data out then this totally fits SRP if you look at the entity types or game elements it looks like it breaks SRP but isn't that one of the OOP scalability issues that ECS helps resolve.
     
  9. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,086
    Not sure if it's really a "pattern", but I'd like to say this: Don't be afraid to maintain your own arrays of structs instead of making everything be ECS entities and components.

    Whenever you have a situation where the order of things in an array could be important (tree structures are a good example), don't just use entities and components to handle it. Maintain that data manually in your own NativeArrays instead.

    For example, if need an octree, I won't spawn a bunch of entities with "OctreeNode" component and try to find some kind of way to link them together in a hierarchy. Instead I will maintain my own NativeArray<OctreeNode> so I can order it however I want for improved efficiency, and no part of that octree will actually be represented by entities.
     
    Last edited: Sep 17, 2018
    jlreymendez, eterlan, ajaxlex and 6 others like this.
  10. Spy-Shifty

    Spy-Shifty

    Joined:
    May 5, 2011
    Posts:
    529
    Here is one more pattern that I use:

    I've multiple systems there I need to fill up an internal cache. This cache e.g. stores an entity at the index based of its GridPosition.

    So I've an 8x8 tall gamefiled (64 cells). The index in the array is calculated by GridPosition.y * 8 + GridPosition.x.
    To be able to use CachedFigureJobComponentSystem in multiple systems, you have to make it generic. The generic type must be unique for each system. This way the "InCache" component will also be unique for each system.

    E.g.
    -> CachedFigureJobComponentSystem<MovementSystem>.InCache
    -> CachedFigureJobComponentSystem<PathFindingSystem>.InCache


    Example:
    Code (CSharp):
    1. public abstract class CachedFigureJobComponentSystem<TSystem> : JobComponentSystem {
    2.  
    3.     protected struct InCache : ISystemStateComponentData { public int Index; }
    4.  
    5.     protected struct AddToCacheData {
    6.         [ReadOnly] public ComponentDataArray<GridPosition> GridPosition;
    7.         [ReadOnly] public SubtractiveComponent<InCache> InCache;
    8.         public EntityArray Entity;
    9.         public readonly int Length;
    10.     }
    11.  
    12.     struct AddToCacheJob : IJobParallelFor {
    13.         public EntityCommandBuffer.Concurrent CommandBuffer;
    14.         public EntityArray Entity;
    15.         [ReadOnly] public ComponentDataArray<GridPosition> GridPosition;
    16.         [NativeDisableParallelForRestriction] [WriteOnly] public NativeArray<Entity> Cache;
    17.  
    18.         public void Execute(int index) {
    19.             var type = typeof(TSystem);
    20.             var test = new InCache();
    21.  
    22.             Entity entity = Entity[index];
    23.             GridPosition gridPosition = GridPosition[index];
    24.  
    25.             int cacheIndex = GameField.GetIndex(gridPosition.Value);
    26.             Cache[cacheIndex] = entity;
    27.             CommandBuffer.AddComponent(index, entity, new InCache { Index = cacheIndex });
    28.         }
    29.     }
    30.  
    31.     struct UpdateCacheJob : IJobProcessComponentDataWithEntity<GridPosition, InCache> {
    32.         [NativeDisableParallelForRestriction] [WriteOnly] public NativeArray<Entity> Cache;
    33.  
    34.         public void Execute(Entity entity, int index, [ReadOnly, ChangedFilter] ref GridPosition data, ref InCache inCache) {
    35.             int newCacheIndex = GameField.GetIndex(data.Value);
    36.             Cache[inCache.Index] = Entity.Null;
    37.             Cache[newCacheIndex] = entity;
    38.             inCache.Index = newCacheIndex;
    39.         }
    40.     }
    41.  
    42.     [RequireSubtractiveComponent(typeof(GridPosition))]
    43.     struct RemoveFromCacheJob : IJobProcessComponentDataWithEntity<InCache> {
    44.         public EntityCommandBuffer.Concurrent CommandBuffer;
    45.         [NativeDisableParallelForRestriction] public NativeArray<Entity> Cache;
    46.  
    47.         public void Execute(Entity entity, int index, ref InCache data) {
    48.             int cacheIndex = data.Index;
    49.             if (Cache[cacheIndex] == entity) {
    50.                 Cache[cacheIndex] = Entity.Null;
    51.             }
    52.             CommandBuffer.RemoveComponent<InCache>(index, entity);
    53.         }
    54.     }
    55.  
    56.     protected NativeArray<Entity> cachedFigures;
    57.  
    58.  
    59.     protected override void OnCreateManager() {
    60.         base.OnCreateManager();
    61.         if (!cachedFigures.IsCreated) {
    62.             cachedFigures = new NativeArray<Entity>(64, Allocator.Persistent);
    63.         }
    64.     }
    65.  
    66.     protected override void OnDestroyManager() {
    67.         base.OnDestroyManager();
    68.         if (cachedFigures.IsCreated) {
    69.             cachedFigures.Dispose();
    70.         }
    71.     }
    72.  
    73.     [Inject] protected AddToCacheData addToCacheData;
    74.     [Inject] protected EndFrameBarrier endFrameBarrier;
    75.  
    76.     protected override JobHandle OnUpdate(JobHandle inputDeps) {
    77.         var addToCacheJob = new AddToCacheJob { Entity = addToCacheData.Entity, GridPosition = addToCacheData.GridPosition,  Cache = cachedFigures, CommandBuffer = endFrameBarrier.CreateCommandBuffer().ToConcurrent() };
    78.         var removeFromCacheJob = new RemoveFromCacheJob { Cache = cachedFigures, CommandBuffer = endFrameBarrier.CreateCommandBuffer().ToConcurrent() };
    79.         var updateCacheJob = new UpdateCacheJob { Cache = cachedFigures };
    80.  
    81.         inputDeps = addToCacheJob.Schedule(addToCacheData.Length, 64, inputDeps);
    82.         inputDeps = updateCacheJob.Schedule(this, inputDeps);
    83.         inputDeps = removeFromCacheJob.Schedule(this, inputDeps);
    84.  
    85.         return inputDeps;
    86.     }
    87. }
    Usage:
    Code (CSharp):
    1. public class MovementSystem : CachedFigureJobComponentSystem<MovementSystem> {
    2.  
    3.     struct MotionJob : IJobParallelFor {
    4.          //...
    5.     }
    6.     //...
    7.  
    8.     protected override JobHandle OnUpdate(JobHandle inputDeps) {
    9.         inputDeps = base.OnUpdate(inputDeps);
    10.    
    11.         var job = new MotionJob() {
    12.             dt = Time.deltaTime,
    13.             //...
    14.         };
    15.         return job.Schedule(figureData.Length, 64, inputDeps);
    16.     }
    17.  
    18. }
    19. public class PathFindingSystem : CachedFigureJobComponentSystem<PathFindingSystem> {
    20.  
    21.     struct CalculatePathJob : IJobProcessComponentDataWithEntity<GridPosition, MoveTo> {
    22.           //...
    23.     }
    24.  
    25.     protected override JobHandle OnUpdate(JobHandle inputDeps) {
    26.         inputDeps = base.OnUpdate(inputDeps);
    27.         var calculatePathJob = new CalculatePathJob() {
    28.              //...
    29.         };
    30.  
    31.         return calculatePathJob.Schedule(this, inputDeps);
    32.     }
    33. }
    34.  
    35.  
    EDIT:
    [Inject] AddToCacheData addToCacheData;
    must be protected to get injected correctly!
     
    Last edited: Sep 26, 2018
    Enzi and Micz84 like this.
  11. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    609
    Micz84, how do you store type of component in another component? Type isn't blittable so.. Dictionary<SomethingBlittable, Type>()? What's your approach?

    I added this, but how the hell is it going to multithread?

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3.  
    4. public struct TypeCode
    5. {
    6.     static private Dictionary<Type, int> _codes = new Dictionary<Type, int>(1000);
    7.     static private List<Type> _types = new List<Type>(100);
    8.  
    9.     private int _code;
    10.  
    11.     public TypeCode(Type type)
    12.     {
    13.         int code;
    14.         if (_codes.TryGetValue(type, out code))
    15.         {
    16.             _code = code;
    17.         }
    18.         else
    19.         {
    20.             _codes.Add(type, _types.Count);
    21.             _code = _types.Count;
    22.             _types.Add(type);
    23.         }
    24.     }
    25.  
    26.     public Type ToType()
    27.     {
    28.         return _types[_code];
    29.     }
    30. }
    Will it work if I replace generics with native containers?
     
    Last edited: Sep 29, 2018
  12. pcysl5edgo

    pcysl5edgo

    Joined:
    Jun 3, 2018
    Posts:
    45
    Use TypeManager.GetTypeIndex<T>() / GetTypeIndex(Type type).
    They return int-type value.
    You can also get Type object by TypeManger.GetType(int typeIndex).
     
  13. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    609
    Sorry, where can I find that TypeManager? Is it part of ECS?

    Nvm, found it. They say it can't be Burst comiled... :(
     
  14. Spy-Shifty

    Spy-Shifty

    Joined:
    May 5, 2011
    Posts:
    529
    Last edited: Sep 29, 2018
  15. Micz84

    Micz84

    Joined:
    Jul 21, 2012
    Posts:
    242
    There is ComponentType struct. It only works for struct components implementing IComponentData, but in my hybrid approach I almost do not have any MonoBehaviour based components.

    Code (CSharp):
    1.  public struct LinkedComponentType : IComponentData
    2.     {
    3.         public ComponentType Type;
    4.     }
    ComponentType has a static generic method Create<T>() that returns ComponentType for given component.
     
    illinar likes this.
  16. Micz84

    Micz84

    Joined:
    Jul 21, 2012
    Posts:
    242
    I do not understand what is a purpose of line 19. This type is never used. And why as a Generic system is used not a component? Wouldn't it be more useful if it would look like this:

    Code (CSharp):
    1. public abstract class CachedFigureJobComponentSystem<TComponent> : JobComponentSystem  where T:IComponentData{
    2.     protected struct InCache : ISystemStateComponentData { public int Index; }
    3.     protected struct AddToCacheData {
    4.         [ReadOnly] public ComponentDataArray<GridPosition> GridPosition;
    5. [ReadOnly] public ComponentDataArray<TComponent> CachedComponent;
    6.  
    7.         [ReadOnly] public SubtractiveComponent<InCache> InCache;
    8.         public EntityArray Entity;
    9.         public readonly int Length;
    10.     }
    11.  
    12. // ...
    Or I am missing something?
     
  17. Spy-Shifty

    Spy-Shifty

    Joined:
    May 5, 2011
    Posts:
    529
    line 19 and 20 was just for testing. I forgot to delete it!

    Why as generic system?
    As I sad befor:
    To be able to use CachedFigureJobComponentSystem in multiple systems, you have to make it generic. The generic type must be unique for each system. This way the "InCache" component will also be unique for each system.

    If you won't use thegeneric you would end up with the error that the component can only be attached once per entity...
    With the generic system InCache isn't the same!

    Hope this will answer your question.


    This is the same pattern with a generic component:
    Code (CSharp):
    1. public abstract class CachedFigureJobComponentSystem<TSystem, TComponent> : JobComponentSystem where TComponent : struct, IComponentData {
    2.  
    3.     protected struct InCache : ISystemStateComponentData { public int Index; }
    4.  
    5.     protected struct AddToCacheData {
    6.         [ReadOnly] public ComponentDataArray<GridPosition> GridPosition;
    7.         [ReadOnly] public ComponentDataArray<TComponent> Component;
    8.         [ReadOnly] public SubtractiveComponent<InCache> InCache;
    9.         public EntityArray Entity;
    10.         public readonly int Length;
    11.     }
    12.  
    13.     struct AddToCacheJob : IJobParallelFor {
    14.         public EntityCommandBuffer.Concurrent CommandBuffer;
    15.         [ReadOnly] public ComponentDataArray<TComponent> Component;
    16.         [ReadOnly] public ComponentDataArray<GridPosition> GridPosition;
    17.         public EntityArray Entity;
    18.      
    19.         [NativeDisableParallelForRestriction] public NativeArray<Entity> CacheEntity;
    20.         [NativeDisableParallelForRestriction] public NativeArray<TComponent> CacheComponent;
    21.  
    22.         public void Execute(int index) {
    23.             Entity entity = Entity[index];
    24.             GridPosition gridPosition = GridPosition[index];
    25.  
    26.             int cacheIndex = Chessboard.GetIndex(gridPosition.Value);
    27.             CacheEntity[cacheIndex] = entity;
    28.             CacheComponent[cacheIndex] = Component[index];
    29.             CommandBuffer.AddComponent(index, entity, new InCache { Index = cacheIndex });
    30.         }
    31.     }
    32.  
    33.     struct UpdateCacheJob : IJobProcessComponentDataWithEntity<GridPosition, TComponent, InCache> {
    34.         [NativeDisableParallelForRestriction] public NativeArray<Entity> CacheEntity;
    35.         [NativeDisableParallelForRestriction] public NativeArray<TComponent> CacheComponent;
    36.  
    37.         public void Execute(Entity entity, int index, [ReadOnly]/*[ChangedFilter]*/ ref GridPosition data, [ReadOnly] ref TComponent component, ref InCache inCache) {
    38.             int newCacheIndex = Chessboard.GetIndex(data.Value);
    39.             if (newCacheIndex != inCache.Index) {
    40.  
    41.                 CacheEntity[inCache.Index] = Entity.Null;
    42.                 CacheEntity[newCacheIndex] = entity;
    43.  
    44.                 CacheComponent[inCache.Index] = default(TComponent);
    45.                 CacheComponent[newCacheIndex] = component;
    46.                 inCache.Index = newCacheIndex;
    47.             }
    48.         }
    49.  
    50.     }
    51.  
    52.     [RequireSubtractiveComponent(typeof(GridPosition))]
    53.     struct RemoveFromCacheJob : IJobProcessComponentDataWithEntity<InCache> {
    54.         public EntityCommandBuffer.Concurrent CommandBuffer;
    55.         [NativeDisableParallelForRestriction] public NativeArray<Entity> CacheEntity;
    56.         [NativeDisableParallelForRestriction] public NativeArray<TComponent> CacheComponent;
    57.  
    58.         public void Execute(Entity entity, int index, ref InCache data) {
    59.             int cacheIndex = data.Index;
    60.             if (CacheEntity[cacheIndex] == entity) {
    61.                 CacheEntity[cacheIndex] = Entity.Null;
    62.                 CacheComponent[cacheIndex] = default(TComponent);
    63.             }
    64.             CommandBuffer.RemoveComponent<InCache>(index, entity);
    65.         }
    66.     }
    67.  
    68.     protected NativeArray<Entity> cachedFigures;
    69.     protected NativeArray<TComponent> cachedComponents;
    70.  
    71.  
    72.     protected override void OnCreateManager() {
    73.         base.OnCreateManager();
    74.         if (!cachedFigures.IsCreated) {
    75.             cachedFigures = new NativeArray<Entity>(64, Allocator.Persistent);
    76.         }
    77.         if (!cachedComponents.IsCreated) {
    78.             cachedComponents = new NativeArray<TComponent>(64, Allocator.Persistent);
    79.         }
    80.     }
    81.  
    82.     protected override void OnDestroyManager() {
    83.         base.OnDestroyManager();
    84.         if (cachedFigures.IsCreated) {
    85.             cachedFigures.Dispose();
    86.         }
    87.         if (cachedComponents.IsCreated) {
    88.             cachedComponents.Dispose();
    89.         }
    90.     }
    91.  
    92.     [Inject] protected AddToCacheData addToCacheData;
    93.     [Inject] protected EndFrameBarrier endFrameBarrier;
    94.  
    95.     protected override JobHandle OnUpdate(JobHandle inputDeps) {
    96.         var addToCacheJob = new AddToCacheJob { Entity = addToCacheData.Entity, GridPosition = addToCacheData.GridPosition, CacheComponent = cachedComponents, CacheEntity = cachedFigures, Component = addToCacheData.Component,  CommandBuffer = endFrameBarrier.CreateCommandBuffer().ToConcurrent() };
    97.         var removeFromCacheJob = new RemoveFromCacheJob { CacheComponent = cachedComponents, CacheEntity = cachedFigures, CommandBuffer = endFrameBarrier.CreateCommandBuffer().ToConcurrent() };
    98.         var updateCacheJob = new UpdateCacheJob { CacheComponent = cachedComponents, CacheEntity = cachedFigures };
    99.  
    100.         inputDeps = addToCacheJob.Schedule(addToCacheData.Length, 64, inputDeps);
    101.         inputDeps = updateCacheJob.Schedule(this, inputDeps);
    102.         inputDeps = removeFromCacheJob.Schedule(this, inputDeps);
    103.  
    104.         return inputDeps;
    105.     }
    106. }
     
  18. Micz84

    Micz84

    Joined:
    Jul 21, 2012
    Posts:
    242
    I understood why you have used generic system. I thought that using genetic component would be the same, but you are right. I would need two systems with same component it would not work. Thanks for clarification.