Search Unity

Feature Request: CustomBarrierSystem

Discussion in 'Entity Component System' started by PublicEnumE, Feb 3, 2019.

  1. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    I often find myself wanting to do something like this in my ECS code:

    From inside a ComponentSystem or Job, I want to create a group of entities which need some special setup. Either they need components with linkages to each other, or they need default data set after they're created, or something else custom. Then after this group of entities is setup, I want to alter their initial states with some custom data (for example a custom position where the cluster of enemies needs to spawn).

    EntityCommandBuffers and BarrierSystems are designed to meet this need. And they work great - letting me defer the creation of Entities until a later System can do it safely on the main thread. And crucially, they also let me schedule commands to alter that Entity after it's made:

    Code (CSharp):
    1. Entity temp = entityCommandBuffer.InstantiateEntity(templateEntity);
    2. entityCommandBuffer.SetComponent(temp, new Position(/*...*/));
    But this pattern has a few weaknesses of note:
    1. You can only create one Entity per call.
    2. You must manually set an Entity's default component data every time you create it.
    3. If you create multiple Entities, you cannot easily setup references between them.
    4. If you're using Instantiate(), you must store a reference to each of the template entities in the System or Job you are calling from.
    However, there may be a solution that would avoid all of these weaknesses:

    Currently, the EntityCommandBuffer API is designed around scheduling pretty low-level tasks: individual entity creation, or single ComponentData assignment, etc.

    But what if you could use an EntityCommandBuffer to schedule more high-level tasks? For example:

    Your game has a snake-like enemy character, which is made up of several body segments that are each government by a different Entity. Those segment Entities have component data which holds references to the segments before and after it. Also, these snake enemies always spawn in a specific pose. So the snake's segments need to start with some default positional data, relative to each other.

    It would be nice if we could just do this following, from a ComponentSystem or Job:

    Code (CSharp):
    1. var buffer = snakeEnemyCreationBarrierSystem.CreateBuffer();
    2. Entity snakeEnemyRoot = buffer.CreateSnakeEnemy();
    3. buffer.SetComponentData(snakeEnemyRoot, new Position(/*...*/));
    My "SnakeEnemyCreationBarrierSystem" would store a reference to all of the Entities it needs to spawn the Snake Enemy, and all of it's segments (these don't need to be stored in any other Job or System).

    When it gets a chance to Update(), it would execute my custom code, which would instantiate a 'root' Entity for the enemy, instantiate Entities for all of its segments, set the proper Entity references between all of the segments, and assign the starting position data that forms the Snake's initial pose. Then, I would return a reference to the Snake's 'root' Entity, which could then be used to set it's spawn position.

    <>

    I've been wondering how something like this might be implemented. I've tried a few approaches on my own, but some of the Typed needed to closely replicate EntityCommandBuffer are currently internal.

    A blue-sky, ideal version of this feature would:
    1. Let you define custom APIs for EntityCommandBuffer, like
      CreateSnakeEnemy()
      .
    2. Or, maybe you could just write your own EntityCommandBuffer types, defined by an interface, like
      ICommandBuffer
      , which would be paired with
      public class BarrierSystem<T> : ComponentSystem where T : ICommandBuffer
      .
    Reading the Unity source code, it's pretty clear this would require a pretty major refactoring of EntityCommandBuffer, and related Types.

    Maybe a simpler, more doable version of the feature would be the "CustomBarrierSystem" class:


    Code (CSharp):
    1. public class CustomizableBarrierSystem : BarrierSystem
    2. {
    3.     public virtual void OnDestroyEntity(Entity entity)
    4.     {
    5.         EntityManager.DestroyEntity(entity);
    6.     }
    7.  
    8.     public virtual void OnRemoveComponent(Entity entity, ComponentType componentType)
    9.     {
    10.         EntityManager.RemoveComponent(entity, componentType);
    11.     }
    12.  
    13.     public virtual void OnCreateEntity(EntityArchetype archetype)
    14.     {
    15.         EntityManager.CreateEntity(archetype);
    16.     }
    17.  
    18.     public virtual void OnInstantiateEntity(Entity srcEntity)
    19.     {
    20.         EntityManager.Instantiate(srcEntity);
    21.     }
    22.  
    23.     public virtual void OnAddComponent<T>(Entity entity, T componentData) where T : struct, IComponentData
    24.     {
    25.         EntityManager.AddComponentData(entity, componentData);
    26.     }
    27.  
    28.     public virtual void OnSetComponent<T>(Entity entity, T componentData) where T : struct, IComponentData
    29.     {
    30.         EntityManager.SetComponentData(entity, componentData);
    31.     }
    32.  
    33.     public virtual void OnAddBuffer<T>(Entity entity) where T : struct, IBufferElementData
    34.     {
    35.         EntityManager.AddBuffer<T>(entity);
    36.     }
    37.  
    38.     public virtual void OnSetBuffer<T>(Entity entity, DynamicBuffer<T> buffer) where T : struct, IBufferElementData
    39.     {
    40.         EntityManager.SetBuffer(entity, buffer);
    41.     }
    42.  
    43.     public virtual void OnAddSharedComponent<T>(Entity entity, T sharedComponentData) where T : struct, ISharedComponentData
    44.     {
    45.         EntityManager.AddSharedComponentData(entity, sharedComponentData);
    46.     }
    47.  
    48.     public virtual void OnSetSharedComponent<T>(Entity entity, T sharedComponentData) where T : struct, ISharedComponentData
    49.     {
    50.         EntityManager.SetSharedComponentData(entity, sharedComponentData);
    51.     }
    52. }
    Usage in an derived System class:

    Code (CSharp):
    1. public class SnakeEnemyCreationBarrierSystem : CustomizableBarrierSystem
    2. {
    3.     private Entity snakeRootTemplate;
    4.     private Entity snakeSegmentTemplate;
    5.  
    6.     private Entity lastCreatedRoot = Entity.Null;
    7.  
    8.     protected override void OnCreateManager()
    9.     {
    10.         // assign snakeRootTemplateEntity
    11.         // assign snakeSegmentTemplateEntity
    12.     }
    13.  
    14.     public override void OnCreateEntity(EntityArchetype archetype)
    15.     {
    16.         Entity snakeRoot = EntityManager.Instantiate(snakeRootTemplate);
    17.         Entity snakeSegment01 = EntityManager.Instantiate(snakeSegmentTemplate);
    18.         Entity snakeSegment02 = EntityManager.Instantiate(snakeSegmentTemplate);
    19.         Entity snakeSegment03 = EntityManager.Instantiate(snakeSegmentTemplate);
    20.         Entity snakeSegment04 = EntityManager.Instantiate(snakeSegmentTemplate);
    21.         Entity snakeSegment05 = EntityManager.Instantiate(snakeSegmentTemplate);
    22.  
    23.         /*
    24.             Setup Entity references between the root and different segments
    25.         */
    26.  
    27.         lastCreatedRoot = snakeRoot;
    28.     }
    29.  
    30.     public override void OnSetComponent<T>(Entity entity, T componentData)
    31.     {
    32.         if(lastCreatedRoot == Entity.Null)
    33.         {
    34.             // error handling
    35.         }
    36.  
    37.         // set the component data on the last-created Snake root Entity
    38.         EntityManager.SetComponentData(lastCreatedRoot, componentData);
    39.     }
    40. }
    And usage from inside a Job or ComponentSystem:

    Code (CSharp):
    1. EntityCommandBuffer snakeCreationBuffer = World.Active.GetOrCreateManager<SnakeEnemyCreationBarrierSystem>().CreateCommandBuffer();
    2.  
    3. snakeCreationBuffer.CreateEntity();
    4. snakeCreationBuffer.SetComponent(Entity.None, new Position(/* spawn position data */));
    <>

    In general, it would be great to be able to schedule high-level tasks from inside a System or Job, and then define the execution of those tasks from inside the BarrierSystem that executes them.

    If anyone has any thoughts on this idea, or better ways to implement it, please please sound off below. I would love to know other people's takes on this.

    Thank you for reading. :)
     
  2. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    I suppose another way this could be done is with an event Entity:

    1. The System or Job creates a single new entity using their PostCommandBuffer.
    2. That Entity is then given a "CreateSnakeEnemyCommand" Component.
    3. That Entity is then optionally given a "Position" Component, with the spawn position data.
    4. Later, a "SnakeEnemyCreationSystem" iterates over all Entities with a "CreateSnakeEnemyCommand" Component, and does all of the neccessary creation and setup work.
     
    superpig likes this.