Search Unity

Question Reactive System - Generic way ?

Discussion in 'Entity Component System' started by genaray, Jun 25, 2020.

  1. genaray

    genaray

    Joined:
    Feb 8, 2017
    Posts:
    191
    I have been looking into SystemStateComponent for creating a "Reactive System" in order to listen to structural entity changes ( Component was added, was removed... )

    The main problem here is that i cannot find a generic approach for this... i managed to create a small reactive system using those SystemStateComponents, which accepts generic input for a target component and several components getting added once that target component is getting added/removed... The problem here, everytime we wanna "listen" for a new component, it requires a new reactive system for that special component...

    That means, if i wanna listen for "Health" - Component getting added or removed, im forced to create a special system for those health structural changes... it already reduced the amount of work but is still pretty bad... i actually wanna achieve a reactive system where you can put in every component you want without creating a special system for each component taking care of this.

    My current attempt looks like this :

    Code (CSharp):
    1. /// <summary>
    2.     /// There no callbacks or listeners for added/removed components on <see cref="Entity"/>'s
    3.     /// Thats where this system comes in using <see cref="ISystemStateComponentData"/> for simulating those callbacks inside the ecs.
    4.     /// </summary>
    5.     public class ReactiveSystem<Component, Managed, Added, Removed> : SystemBase {
    6.         private EntityQuery newEntities;
    7.         private EntityQuery activeNewEntities;
    8.         private EntityQuery currentRemovedEntities;
    9.         private EntityQuery removedEntities;
    10.  
    11.         private EndInitializationEntityCommandBufferSystem beginBuffer;
    12.         private EndSimulationEntityCommandBufferSystem endBuffer;
    13.         protected override void OnCreate() {
    14.             base.OnCreate();
    15.             beginBuffer = World.GetOrCreateSystem<EndInitializationEntityCommandBufferSystem>();
    16.             endBuffer = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    17.  
    18.             // Query to get all newly created entities, without being marked as added
    19.             newEntities = GetEntityQuery(new EntityQueryDesc{
    20.                 All = new[]{ComponentType.ReadOnly<Component>()},
    21.                 None = new[]{ComponentType.ReadWrite<Added>(), ComponentType.ReadWrite<Removed>() },
    22.             });
    23.  
    24.             // Query of all entities which where added this frame
    25.             activeNewEntities =  GetEntityQuery(new EntityQueryDesc{
    26.                 All = new[]{ComponentType.ReadWrite<Component>(),ComponentType.ReadWrite<Managed>(), ComponentType.ReadWrite<Added>()},
    27.                 None = new[]{ComponentType.ReadWrite<Removed>() }
    28.             });
    29.             // Query to get all destroyed entities
    30.             removedEntities = GetEntityQuery(new EntityQueryDesc{
    31.                 All = new[]{ComponentType.ReadWrite<Managed>()},
    32.                 None = new[]{ComponentType.ReadOnly<Component>(), ComponentType.ReadWrite<Added>(), ComponentType.ReadWrite<Removed>() },
    33.             });
    34.         }
    35.         protected override void OnUpdate() {
    36.             var beginCommandBuffer = beginBuffer.CreateCommandBuffer();
    37.             var endCommandBuffer = endBuffer.CreateCommandBuffer();
    38.  
    39.             // Create active new entities at the start of the frame
    40.             beginCommandBuffer.AddComponent<Managed>(newEntities);
    41.             beginCommandBuffer.AddComponent<Added>(newEntities);
    42.             beginCommandBuffer.AddComponent<Removed>(removedEntities);
    43.  
    44.             // Remove "added" from active new entities at the end of the frame
    45.             endCommandBuffer.RemoveComponent<Added>(activeNewEntities);
    46.  
    47.             // Remove "removed" from active destroyed entities at the end of the frame
    48.             endCommandBuffer.RemoveComponent<Managed>(removedEntities);
    49.             endCommandBuffer.RemoveComponent<Removed>(removedEntities);
    50.             endCommandBuffer.DestroyEntity(removedEntities);
    51.         }
    52.     }
    I wondered if theres a way to make such a ReactiveSystem generic and centralized...
    Code (CSharp):
    1.  
    2.  
    3. So the goal is to reshape this reactive system to...
    4.  
    5. world.OnCreate<MobComponent>(() => {});
    6.  
    7. or
    8.  
    9. Entities.ForEach((ref MobComponent mc, ref MobComponentAdded mca) => {});
    10.  
    11. or
    12.  
    13. var group = GetGroup(typeof(MobComponent));
    14. group.OnCreate((MobComponent mc) => {});
    15.  
    16. instead of
    17.  
    18. struct HealthManaged{}
    19. struct HealthAdded{}
    20. struct HealthRemoved{}
    21.  
    22. public HealthReactiveSystem : ReactiveSystem<Health, HealthManaged, HealthAdded, HealthRemoved>{}
    23.  
    24. Entities.ForEach((ref Health h, ref HealthAdded ha) => {});
    25.  
    26. I hope you know what i mean :)
    27.  
    any ideas ?
     
    Last edited: Jun 25, 2020
  2. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    What would be the goal of that reactive system ?
    What do you do when adding/removing a component ?
    What problem are you trying to solve ?
     
  3. genaray

    genaray

    Joined:
    Feb 8, 2017
    Posts:
    191
    Thats actually explained in the post above... i corrected it many times and made sure that its understandable... just look at the last example, the goal is to make this reactive system generic and flexible without hardcoding one reactive system for every component we wanna watch
     
  4. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    Yes I understand that but what do you want to do once you detect that a component has been added ?

    Does it not depend on the component? I think it does so you will have to make one system for each type of component you want to detect the addition.


    Now you can abstract your system (the one you posted) so that you only declare the system state component for each IComponentData and a derived system

    So for you Heath example, you will have:

    - HealthComponent (actual IComponentData)

    - HealthSystemComponentAdded (when HealthComponent is added to an entity)

    - HealthSystemComponentRemoved (when HealthComponent is removed from an entity)

    - HealthSystemComponentManaged (when ?)


    Then you can add an additional struct that implement an interface like IComponnentChangeManager that provide 3 methods :

    HeatthComonentChangeManager : IComponnentChangeManager

    - OnCreate(ref HealthComponent)

    - OnRemove(ref HealthComponent)

    - OnManage(ref HealthComponent)


    Then you just have to declare the reactive system for that specific component.


    HealthComponentReactiveSystem : ReactiveSystem<HealthComponent , HealthSystemComponentManaged , HealthSystemComponentAdded , HealthSystemComponentRemoved , HeatthComonentChangeManager >.


    For an example of how the behavior injection works with a struct you can look at my skill package (the context writer works taht way :

    Abstract class: https://github.com/WAYNGROUP/MGM-Skill/blob/master/Runtime/Systems/EffectTriggerSystems.cs

    Derived concrete implementation: https://github.com/WAYNGROUP/MGM-Skill/blob/2b1bfe238f3c1ec1e70bd642d515cc94f77a39bc/Samples/Simple Skill/Effect2.cs#L38
     
    AlexanderKallin and genaray like this.
  5. genaray

    genaray

    Joined:
    Feb 8, 2017
    Posts:
    191
    Thanks for your help ! That already helps :) But how could we reshape that "ReactiveSystem" in order to automate the creation of those "HealthSystemComponentAdded", "HealthSystemComponentRemoved" structs aswell as the reactive system for them ?

    This way we could use a (extension) method like...
    world.Listen<HealthComponent>();

    Which automaticly creates the fitting components and its reactive system for us ( what you posted above )... that could save tons of boilerplate code... thats the reason why im searching for some generic solution ;)
     
  6. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    Well you still have to code the behavior...

    Actullay, I'm not sure why you need all 3 manager component.

    From my understanding, your reactive system should have only 1 system state component (HealthSystemComponent)

    If an entity has the HealthComponent but not the HealthSystemComponent then the reactive system will infer that HealthComponent just got added then you can call the HeatthComonentChangeManager.OnCreate(ref HealthComponent) (probably should be named OnComponentAdded)

    If an entity has HealthSystemComponent but not HealthComponent then the reactive system will infer that HealthComponent just got removed then you can call the HeatthComonentChangeManager.OnRemove(ref HealthComponent) (probably should be named OnComponentRemoved)

    Then for managed I don't know what you mean but you could add a copy of the HealthComponent in the HealthSystemComponent and if the entity has both HealthSystemComponent and HealthComponent you compare the 2 values to detect a change then you can call the HeatthComonentChangeManager.OnComponentChanged(ref HealthComponent) (don't forget to save the new value in the copy of the component in the HealthSystemComponent)

    For my package I used the ProjectWindowUtil.CreateScriptAssetFromTemplateFile to make all the boilet plate code generated in bya click in hte editor menu, all the user has to do is define his game specific behavior.
    see :
    https://github.com/WAYNGROUP/MGM-Skill/blob/master/Editor/ScriptTemplates/ScriptTemplates.cs
    https://github.com/WAYNGROUP/MGM-Skill/blob/master/Editor/ScriptTemplates/EffectType.txt
     
  7. genaray

    genaray

    Joined:
    Feb 8, 2017
    Posts:
    191
    Thanks im gonna give that a try ! Nevertheless you still require a "HealthSystemStateComponent"... and i hoped this could get "automated"/generalized too :) Any idea on this ?
     
  8. brunocoimbra

    brunocoimbra

    Joined:
    Sep 2, 2015
    Posts:
    679
    Never tried that (will try this weekend, very interesting that idea), but you could give it a go:
    Code (CSharp):
    1. public class ReactiveSystem<T> : SystemBase
    2. {
    3.     public struct State : ISystemStateCoponentData { }
    4.  
    5.     // use your T and State inside the system
    6. }
    You will still need to create a system for each component, but at least the State component creation will be automated.

    I know that won't work with Entities.ForEach, but will work just fine with IJobChunk.
     
  9. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    Wanted to test is before posting it but if anyone want to give it a try, here it goes :
    Code (CSharp):
    1.  
    2. using Unity.Burst;
    3. using Unity.Collections;
    4. using Unity.Entities;
    5. using Unity.Jobs;
    6.  
    7. public interface IComponentReactor<COMPONENT>
    8. {
    9.     void ComponentAdded(ref COMPONENT newComponent);
    10.     void ComponentRemoved(in COMPONENT oldComponent);
    11.     void ComponentValueChanged(ref COMPONENT newComponent, in COMPONENT oldComponent);
    12. }
    13.  
    14. public abstract class ReactiveComponentSystem<COMPONENT, COMPONENT_REACTOR> : SystemBase
    15.     where COMPONENT : struct, IComponentData
    16.     where COMPONENT_REACTOR : struct, IComponentReactor<COMPONENT>
    17. {
    18.     /// <summary>
    19.     /// Struct implementing IComponentReactor<COMPONENT> that implements the behavior when COMPONENT is added, removed or changed value.
    20.     /// </summary>
    21.     private COMPONENT_REACTOR _reactor;
    22.  
    23.     /// <summary>
    24.     /// Query to detect the addition of COMPONENT to an entity.
    25.     /// </summary>
    26.     private EntityQuery _componentAddedQuery;
    27.     /// <summary>
    28.     /// Query to detect the removal of COMPONENT from an entity.
    29.     /// </summary>
    30.     private EntityQuery _componentRemovedQuery;
    31.     /// <summary>
    32.     /// Query to gateher all entity that need to check for change in value.
    33.     /// </summary>
    34.     private EntityQuery _componentValueChangedQuery;
    35.  
    36.     /// <summary>
    37.     /// EnityCommandBufferSystem used to add and remove the StateComponent.
    38.     /// </summary>
    39.     private EntityCommandBufferSystem _entityCommandBufferSystem;
    40.  
    41.     /// <summary>
    42.     /// The state component for this reactive system.
    43.     /// It contains a copy of the COMPONENT data.
    44.     /// </summary>
    45.     private struct StateComponent : ISystemStateComponentData
    46.     {
    47.         public COMPONENT Value;
    48.     }
    49.  
    50.     /// <inheritdoc/>
    51.     protected override void OnCreate()
    52.     {
    53.         base.OnCreate();
    54.         _reactor = CreateComponentRactor();
    55.  
    56.         _componentAddedQuery = GetEntityQuery(new EntityQueryDesc()
    57.         {
    58.             All = new ComponentType[] { ComponentType.ReadWrite(typeof(COMPONENT)) },
    59.             None = new ComponentType[] { ComponentType.ReadOnly(typeof(COMPONENT_REACTOR)) }
    60.         });
    61.  
    62.         _componentRemovedQuery = GetEntityQuery(new EntityQueryDesc()
    63.         {
    64.             All = new ComponentType[] { ComponentType.ReadOnly(typeof(COMPONENT_REACTOR)) },
    65.             None = new ComponentType[] { ComponentType.ReadOnly(typeof(COMPONENT)) }
    66.         });
    67.  
    68.         _componentValueChangedQuery = GetEntityQuery(new EntityQueryDesc()
    69.         {
    70.             All = new ComponentType[] { ComponentType.ReadWrite(typeof(COMPONENT)), ComponentType.ReadWrite(typeof(COMPONENT_REACTOR)) }
    71.         });
    72.  
    73.         _entityCommandBufferSystem = GetCommandBufferSystem();
    74.     }
    75.  
    76.     /// <summary>
    77.     /// Create the reactor struct that implements the behavior when COMPONENT is added, removed or changed value.
    78.     /// </summary>
    79.     /// <returns>COMPONENT_REACTOR</returns>
    80.     protected abstract COMPONENT_REACTOR CreateComponentRactor();
    81.  
    82.     /// <summary>
    83.     /// Get the EntityCommandBufferSystem buffer system to use to add and remove the StateComponent.
    84.     /// </summary>
    85.     /// <returns>EntityCommandBufferSystem</returns>
    86.     protected EntityCommandBufferSystem GetCommandBufferSystem()
    87.     {
    88.         return World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    89.     }
    90.  
    91.     /// <summary>
    92.     /// This system call the COMPONENT_REACTOR.ComponentAdded method on all enttiy that have a new COMPONENT.
    93.     /// </summary>
    94.     [BurstCompile]
    95.     private struct ManageComponentAdditionJob : IJobChunk
    96.     {
    97.         public EntityCommandBuffer.Concurrent EntityCommandBuffer;
    98.         public ArchetypeChunkComponentType<COMPONENT> ComponentChunk;
    99.         [ReadOnly] public ArchetypeChunkEntityType EntityChunk;
    100.         [ReadOnly] public COMPONENT_REACTOR Reactor;
    101.  
    102.         public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    103.         {
    104.             NativeArray<COMPONENT> components = chunk.GetNativeArray(ComponentChunk);
    105.             NativeArray<Entity> entities = chunk.GetNativeArray(EntityChunk);
    106.  
    107.             for (int i = 0; i < chunk.Count; ++i)
    108.             {
    109.                 // Calls the mathod and reassign the COMPONENT to take into account any modification that may have accured during the method call.
    110.                 COMPONENT component = components[i];
    111.                 Reactor.ComponentAdded(ref component);
    112.                 components[i] = component;
    113.  
    114.                 // Add the system state component and set it's value that on the next frame, the ManageComponentValueChangeJob can handle any change in the COMPONENT value.
    115.                 EntityCommandBuffer.AddComponent<StateComponent>(chunkIndex, entities[i]);
    116.                 EntityCommandBuffer.SetComponent(chunkIndex, entities[i], new StateComponent() { Value = component });
    117.             }
    118.  
    119.         }
    120.     }
    121.     /// <summary>
    122.     /// This system call the COMPONENT_REACTOR.ComponentRemoved method on all enttiy that were strip down of their COMPONENT.
    123.     /// </summary>
    124.     [BurstCompile]
    125.     private struct ManageComponentRemovalJob : IJobChunk
    126.     {
    127.  
    128.         public EntityCommandBuffer.Concurrent EntityCommandBuffer;
    129.         [ReadOnly] public ArchetypeChunkComponentType<StateComponent> StateComponentChunk;
    130.         [ReadOnly] public ArchetypeChunkEntityType EntityChunk;
    131.         [ReadOnly] public COMPONENT_REACTOR Reactor;
    132.  
    133.         public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    134.         {
    135.             NativeArray<StateComponent> stateComponents = chunk.GetNativeArray(StateComponentChunk);
    136.             NativeArray<Entity> entities = chunk.GetNativeArray(EntityChunk);
    137.  
    138.             for (int i = 0; i < chunk.Count; ++i)
    139.             {
    140.                 // Calls the mathod with the last know copy of the component, this copy is read only has the component will be remove by hte end of the frame.
    141.                 StateComponent stateComponent = stateComponents[i];
    142.                 Reactor.ComponentRemoved(in stateComponent.Value);
    143.  
    144.                 // Remove the state component so that the entiyt can be destroyed or listen again for COMPONENT addition.
    145.                 EntityCommandBuffer.RemoveComponent<StateComponent>(chunkIndex, entities[i]);
    146.             }
    147.  
    148.         }
    149.     }
    150.  
    151.     /// <summary>
    152.     /// This system call the COMPONENT_REACTOR.ComponentValueChanged method on all entity that had their COMPONENT value changed.
    153.     /// </summary>
    154.     [BurstCompile]
    155.     private struct ManageComponentValueChangeJob : IJobChunk
    156.     {
    157.         public ArchetypeChunkComponentType<COMPONENT> ComponentChunk;
    158.         public ArchetypeChunkComponentType<StateComponent> StateComponentChunk;
    159.         [ReadOnly] public COMPONENT_REACTOR Reactor;
    160.  
    161.         public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    162.         {
    163.             NativeArray<COMPONENT> components = chunk.GetNativeArray(ComponentChunk);
    164.             NativeArray<StateComponent> stateComponents = chunk.GetNativeArray(StateComponentChunk);
    165.  
    166.             for (int i = 0; i < chunk.Count; ++i)
    167.             {
    168.                 // Chaeck if the value changed since last frame.
    169.                 StateComponent stateComponent = stateComponents[i];
    170.                 COMPONENT component = components[i];
    171.  
    172.                 // If it did not change, move to the next entity in chunk.
    173.                 if (stateComponent.Value.Equals(component)) continue;
    174.  
    175.                 // If it did change, call the method with the new value and the old value (from the last know copy of the COMPONENT)
    176.                 Reactor.ComponentValueChanged(ref component, in stateComponent.Value);
    177.  
    178.                 // Ressign the COMPONENT to take into account any modification that may have accured during the method call.
    179.                 components[i] = component;
    180.  
    181.                 // Update the copy of the COMPONENT.
    182.                 stateComponent.Value = component;
    183.                 stateComponents[i] = stateComponent;
    184.             }
    185.  
    186.         }
    187.     }
    188.  
    189.     protected override void OnUpdate()
    190.     {
    191.         JobHandle systemDeps = Dependency;
    192.  
    193.         systemDeps = new ManageComponentValueChangeJob()
    194.         {
    195.             ComponentChunk = GetArchetypeChunkComponentType<COMPONENT>(false),
    196.             StateComponentChunk = GetArchetypeChunkComponentType<StateComponent>(false),
    197.             Reactor = _reactor
    198.         }.ScheduleParallel(_componentValueChangedQuery, systemDeps);
    199.  
    200.         systemDeps = new ManageComponentAdditionJob()
    201.         {
    202.             ComponentChunk = GetArchetypeChunkComponentType<COMPONENT>(false),
    203.             EntityChunk = GetArchetypeChunkEntityType(),
    204.             EntityCommandBuffer = _entityCommandBufferSystem.CreateCommandBuffer().ToConcurrent(),
    205.             Reactor = _reactor
    206.         }.ScheduleParallel(_componentAddedQuery, systemDeps);
    207.  
    208.         _entityCommandBufferSystem.AddJobHandleForProducer(systemDeps);
    209.  
    210.         systemDeps = new ManageComponentRemovalJob()
    211.         {
    212.             StateComponentChunk = GetArchetypeChunkComponentType<StateComponent>(false),
    213.             EntityChunk = GetArchetypeChunkEntityType(),
    214.             EntityCommandBuffer = _entityCommandBufferSystem.CreateCommandBuffer().ToConcurrent(),
    215.             Reactor = _reactor
    216.         }.ScheduleParallel(_componentRemovedQuery, systemDeps);
    217.  
    218.         _entityCommandBufferSystem.AddJobHandleForProducer(systemDeps);
    219.  
    220.         Dependency = systemDeps;
    221.     }
    222. }
    223.  
    Edit : feel free to suggest cahnges, not sure about the order or if each job really need to be sequenced. Aslo put read write constraint as I feel they should be but depending on what you want to do you may even remove the write capability to the COMPONENT.
     
    davenirline and brunocoimbra like this.
  10. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    Several issues with the previous post.

    1 ) the entitiy query desription should reference StateComponent instead of COMPONENT_REACTOR
    2 ) the state component is considered generic so it need to be declared with
    [assembly: RegisterGenericComponentType(typeof(ReactiveComponentSystem<Health, HealthComponentReactor>.StateComponent))] in each derived reactive system.
    3 ) the ManageComponentValueChangeJob can't be bursted due to the use of the .Equals method.
     
    brunocoimbra likes this.
  11. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    Corrected version with an exemple of a derived reactvie system declaration.
    The change value is know bursted and the struct equality is peformed by converting the component and it's copy into naitvearray of byte that are used for comparison (had to change COMPONENT constraint from struct to unmanaged which should not be an issue in this context).

    Abstract reactive system :
    Code (CSharp):
    1.  
    2. using Unity.Burst;
    3. using Unity.Collections;
    4. using Unity.Collections.LowLevel.Unsafe;
    5. using Unity.Entities;
    6. using Unity.Jobs;
    7. public interface IComponentReactor<COMPONENT>
    8. {
    9.     void ComponentAdded(ref COMPONENT newComponent);
    10.     void ComponentRemoved(in COMPONENT oldComponent);
    11.     void ComponentValueChanged(ref COMPONENT newComponent, in COMPONENT oldComponent);
    12. }
    13. public abstract class ReactiveComponentSystem<COMPONENT, COMPONENT_REACTOR> : SystemBase
    14.     where COMPONENT : unmanaged, IComponentData
    15.     where COMPONENT_REACTOR : struct, IComponentReactor<COMPONENT>
    16. {
    17.     /// <summary>
    18.     /// Struct implmenting IComponentReactor<COMPONENT> that implements the behavior when COMPONENT is added, removed or changed value.
    19.     /// </summary>
    20.     private COMPONENT_REACTOR _reactor;
    21.     /// <summary>
    22.     /// Query to detect the addition of COMPONENT to an entity.
    23.     /// </summary>
    24.     private EntityQuery _componentAddedQuery;
    25.     /// <summary>
    26.     /// Query to detect the removal of COMPONENT from an entity.
    27.     /// </summary>
    28.     private EntityQuery _componentRemovedQuery;
    29.     /// <summary>
    30.     /// Query to gateher all entity that need to check for change in value.
    31.     /// </summary>
    32.     private EntityQuery _componentValueChangedQuery;
    33.     /// <summary>
    34.     /// EnityCommandBufferSystem used to add and remove the StateComponent.
    35.     /// </summary>
    36.     private EntityCommandBufferSystem _entityCommandBufferSystem;
    37.     /// <summary>
    38.     /// The state component for this reactive system.
    39.     /// It contains a copy of the COMPONENT data.
    40.     /// </summary>
    41.     public struct StateComponent : ISystemStateComponentData
    42.     {
    43.         public COMPONENT Value;
    44.     }
    45.     /// <inheritdoc/>
    46.     protected override void OnCreate()
    47.     {
    48.         base.OnCreate();
    49.         _reactor = CreateComponentRactor();
    50.         _componentAddedQuery = GetEntityQuery(new EntityQueryDesc()
    51.         {
    52.             All = new ComponentType[] { ComponentType.ReadWrite(typeof(COMPONENT)) },
    53.             None = new ComponentType[] { ComponentType.ReadOnly(typeof(StateComponent)) }
    54.         });
    55.         _componentRemovedQuery = GetEntityQuery(new EntityQueryDesc()
    56.         {
    57.             All = new ComponentType[] { ComponentType.ReadOnly(typeof(StateComponent)) },
    58.             None = new ComponentType[] { ComponentType.ReadOnly(typeof(COMPONENT)) }
    59.         });
    60.         _componentValueChangedQuery = GetEntityQuery(new EntityQueryDesc()
    61.         {
    62.             All = new ComponentType[] { ComponentType.ReadWrite(typeof(COMPONENT)), ComponentType.ReadWrite(typeof(StateComponent)) }
    63.         });
    64.         _entityCommandBufferSystem = GetCommandBufferSystem();
    65.     }
    66.     /// <summary>
    67.     /// Create the reactor struct that implements the behavior when COMPONENT is added, removed or changed value.
    68.     /// </summary>
    69.     /// <returns>COMPONENT_REACTOR</returns>
    70.     protected abstract COMPONENT_REACTOR CreateComponentRactor();
    71.     /// <summary>
    72.     /// Get the EntityCommandBufferSystem buffer system to use to add and remove the StateComponent.
    73.     /// </summary>
    74.     /// <returns>EntityCommandBufferSystem</returns>
    75.     protected EntityCommandBufferSystem GetCommandBufferSystem()
    76.     {
    77.         return World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    78.     }
    79.     /// <summary>
    80.     /// This system call the COMPONENT_REACTOR.ComponentAdded method on all enttiy that have a new COMPONENT.
    81.     /// </summary>
    82.     [BurstCompile]
    83.     private struct ManageComponentAdditionJob : IJobChunk
    84.     {
    85.         public EntityCommandBuffer.Concurrent EntityCommandBuffer;
    86.         public ArchetypeChunkComponentType<COMPONENT> ComponentChunk;
    87.         [ReadOnly] public ArchetypeChunkEntityType EntityChunk;
    88.         [ReadOnly] public COMPONENT_REACTOR Reactor;
    89.         public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    90.         {
    91.             NativeArray<COMPONENT> components = chunk.GetNativeArray(ComponentChunk);
    92.             NativeArray<Entity> entities = chunk.GetNativeArray(EntityChunk);
    93.             for (int i = 0; i < chunk.Count; ++i)
    94.             {
    95.                 // Calls the mathod and reassign the COMPONENT to take into account any modification that may have accured during the method call.
    96.                 COMPONENT component = components[i];
    97.                 Reactor.ComponentAdded(ref component);
    98.                 components[i] = component;
    99.                 // Add the system state component and set it's value that on the next frame, the ManageComponentValueChangeJob can handle any change in the COMPONENT value.
    100.                 EntityCommandBuffer.AddComponent<StateComponent>(chunkIndex, entities[i]);
    101.                 EntityCommandBuffer.SetComponent(chunkIndex, entities[i], new StateComponent() { Value = component });
    102.             }
    103.         }
    104.     }
    105.     /// <summary>
    106.     /// This system call the COMPONENT_REACTOR.ComponentRemoved method on all enttiy that were strip down of their COMPONENT.
    107.     /// </summary>
    108.     [BurstCompile]
    109.     private struct ManageComponentRemovalJob : IJobChunk
    110.     {
    111.         public EntityCommandBuffer.Concurrent EntityCommandBuffer;
    112.         [ReadOnly] public ArchetypeChunkComponentType<StateComponent> StateComponentChunk;
    113.         [ReadOnly] public ArchetypeChunkEntityType EntityChunk;
    114.         [ReadOnly] public COMPONENT_REACTOR Reactor;
    115.         public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    116.         {
    117.             NativeArray<StateComponent> stateComponents = chunk.GetNativeArray(StateComponentChunk);
    118.             NativeArray<Entity> entities = chunk.GetNativeArray(EntityChunk);
    119.             for (int i = 0; i < chunk.Count; ++i)
    120.             {
    121.                 // Calls the mathod with the last know copy of the component, this copy is read only has the component will be remove by hte end of the frame.
    122.                 StateComponent stateComponent = stateComponents[i];
    123.                 Reactor.ComponentRemoved(in stateComponent.Value);
    124.                 // Remove the state component so that the entiyt can be destroyed or listen again for COMPONENT addition.
    125.                 EntityCommandBuffer.RemoveComponent<StateComponent>(chunkIndex, entities[i]);
    126.             }
    127.         }
    128.     }
    129.     /// <summary>
    130.     /// This system call the COMPONENT_REACTOR.ComponentValueChanged method on all entity that had their COMPONENT value changed.
    131.     /// </summary>
    132.     [BurstCompile]
    133.     private struct ManageComponentValueChangeJob : IJobChunk
    134.     {
    135.         public ArchetypeChunkComponentType<COMPONENT> ComponentChunk;
    136.         public ArchetypeChunkComponentType<StateComponent> StateComponentChunk;
    137.         [ReadOnly] public COMPONENT_REACTOR Reactor;
    138.         public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    139.         {
    140.             NativeArray<COMPONENT> components = chunk.GetNativeArray(ComponentChunk);
    141.             NativeArray<StateComponent> stateComponents = chunk.GetNativeArray(StateComponentChunk);
    142.             for (int i = 0; i < chunk.Count; ++i)
    143.             {
    144.                 // Chaeck if the value changed since last frame.
    145.                 StateComponent stateComponent = stateComponents[i];
    146.                 COMPONENT component = components[i];
    147.                 // If it did not change, move to the next entity in chunk.
    148.                 if (ByteBufferUtility.AreEqualStruct(stateComponent.Value, component)) continue;
    149.                 // If it did change, call the method with the new value and the old value (from the last know copy of the COMPONENT)
    150.                 Reactor.ComponentValueChanged(ref component, in stateComponent.Value);
    151.                 // Ressign the COMPONENT to take into account any modification that may have accured during the method call.
    152.                 components[i] = component;
    153.                 // Update the copy of the COMPONENT.
    154.                 stateComponent.Value = component;
    155.                 stateComponents[i] = stateComponent;
    156.             }
    157.         }
    158.     }
    159.     protected override void OnUpdate()
    160.     {
    161.         JobHandle systemDeps = Dependency;
    162.         systemDeps = new ManageComponentValueChangeJob()
    163.         {
    164.             ComponentChunk = GetArchetypeChunkComponentType<COMPONENT>(false),
    165.             StateComponentChunk = GetArchetypeChunkComponentType<StateComponent>(false),
    166.             Reactor = _reactor
    167.         }.ScheduleParallel(_componentValueChangedQuery, systemDeps);
    168.         systemDeps = new ManageComponentAdditionJob()
    169.         {
    170.             ComponentChunk = GetArchetypeChunkComponentType<COMPONENT>(false),
    171.             EntityChunk = GetArchetypeChunkEntityType(),
    172.             EntityCommandBuffer = _entityCommandBufferSystem.CreateCommandBuffer().ToConcurrent(),
    173.             Reactor = _reactor
    174.         }.ScheduleParallel(_componentAddedQuery, systemDeps);
    175.         _entityCommandBufferSystem.AddJobHandleForProducer(systemDeps);
    176.         systemDeps = new ManageComponentRemovalJob()
    177.         {
    178.             StateComponentChunk = GetArchetypeChunkComponentType<StateComponent>(false),
    179.             EntityChunk = GetArchetypeChunkEntityType(),
    180.             EntityCommandBuffer = _entityCommandBufferSystem.CreateCommandBuffer().ToConcurrent(),
    181.             Reactor = _reactor
    182.         }.ScheduleParallel(_componentRemovedQuery, systemDeps);
    183.         _entityCommandBufferSystem.AddJobHandleForProducer(systemDeps);
    184.         Dependency = systemDeps;
    185.     }
    186.     public static class ByteBufferUtility
    187.     {
    188.         public static bool AreEqualStruct<T>(T frist, T second) where T : unmanaged
    189.         {
    190.             NativeArray<byte> firstArray = ConvertToNativeBytes<T>(frist, Allocator.Temp);
    191.             NativeArray<byte> secondArray = ConvertToNativeBytes<T>(second, Allocator.Temp);
    192.             if (firstArray.Length != secondArray.Length) return false;
    193.             for (int i = 0; i < firstArray.Length; ++i)
    194.             {
    195.                 if (firstArray[i] != secondArray[i]) return false;
    196.             }
    197.             return true;
    198.         }
    199.         private static NativeArray<byte> ConvertToNativeBytes<T>(T value, Allocator allocator) where T : unmanaged
    200.         {
    201.             int size = UnsafeUtility.SizeOf<T>();
    202.             NativeArray<byte> ret = new NativeArray<byte>(size, allocator);
    203.             unsafe
    204.             {
    205.                 UnsafeUtility.CopyStructureToPtr(ref value, ret.GetUnsafePtr());
    206.             }
    207.             return ret;
    208.         }
    209.        
    210.     }
    211. }
    212.  
    213.  
    Derived System :

    Code (CSharp):
    1.  
    2. using Unity.Entities;
    3.  
    4. using UnityEngine;
    5.  
    6.  
    7. [assembly: RegisterGenericComponentType(typeof(ReactiveComponentSystem<Health, HealthComponentReactor>.StateComponent))]
    8.  
    9. public struct Health : IComponentData
    10. {
    11.     public float Value;
    12. }
    13.  
    14. public struct HealthComponentReactor : IComponentReactor<Health>
    15. {
    16.     public void ComponentAdded(ref Health newComponent)
    17.     {
    18.         Debug.Log("Added");
    19.     }
    20.  
    21.     public void ComponentRemoved(in Health oldComponent)
    22.     {
    23.         Debug.Log("Removed");
    24.     }
    25.  
    26.     public void ComponentValueChanged(ref Health newComponent, in Health oldComponent)
    27.     {
    28.         Debug.Log("Changed");
    29.     }
    30. }
    31. public class HealtReactiveSystem : ReactiveComponentSystem<Health, HealthComponentReactor>
    32. {
    33.     protected override HealthComponentReactor CreateComponentRactor()
    34.     {
    35.         return new HealthComponentReactor();
    36.     }
    37. }
    38.  
    Implemented it in my skill/ability system for quick testing and I do get the log for ADD and CHANGE (not delete because I don't ever delete the component in my use case, but it should work as well)

    EDIT : regarding the [assembly: RegisterGenericComponentType(typeof(ReactiveComponentSystem<Health, HealthComponentReactor>.StateComponent))] , making the user define the statecomponent himself in the derived class does not seem to be much more work and it would have 2 advantage :
    1 ) it should get rid of the necessity to declare the RegisterGenericComponentType
    2 ) It would allow the user to define it's own state data wich may not be just a copy of the component.

    Again, I did it for the exemple but I have actually no use case in mind where I would use this system o_O

    EDIT 2 : the copy struct to nativearray of byte I got from here.
     
    Last edited: Jun 26, 2020
    tarahugger and genaray like this.
  12. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    array equality with == returns false because it compares pointer, not values apparently.
    I'll keep looking nad learning ;)

    EDIT : worked around by comparing each byte one at a time if one is not equal, they are different. I updated the implementation in hte previous post.
     
    Last edited: Jun 26, 2020
    genaray likes this.
  13. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    Use case just poped into head.

    Using this sort of system on the transform component, we could probably use it to populate an octree when a transform is added, update if the tranform changed and remove when the transform is removed.

    No idea how to create a DOTS compatible octree that would support efficiently parrallel writes...
     
    brunocoimbra and genaray like this.
  14. genaray

    genaray

    Joined:
    Feb 8, 2017
    Posts:
    191
    Damn ! Thanks a lot that looks promising :D Im actually really happy that you show such a engagement, it helps me a lot... i was pretty busy today but im gonna test your implementation tommorow, it looks really good ^^
     
  15. genaray

    genaray

    Joined:
    Feb 8, 2017
    Posts:
    191
    Oh one more question... As far as i see is that your system is running multithreaded... Is it possible to add function callbacks or multiple interfaces ? This way we multiple listeners could get attached... For example a one listener taking care of updating the visuals, one listener playing some animation, another one debugging e.g. ^^
     
  16. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    Yes it is multithreaded.

    For multiple behavior on the same component addition/removal I would acctually suggest that you make another reactor system.
    That way you can order them with the update before/after and disable them separatly if needed.

    Morevoer the fact that we use interface to inject the behavior make it incompatible with the native container so we can't pass in the job a nativearray of behavior.
    And using a normal list to schedule multiple job is a bad idea in respect to the state component.
     
    genaray likes this.
  17. genaray

    genaray

    Joined:
    Feb 8, 2017
    Posts:
    191
    I had some time and tested the system, it is truly awesome ! Good job :D The only issue left is the chaining of actions... so i continued the work on the reactive system in order to use actions/delegates instead of predefined methods inside the interface..
    It kinda works, but it still throws the following exception
    "ArgumentException: The specified Type must not be a generic type definition."

    this probably happens during the construction of the function pointer... looks like marshal does not like generics at all... the exception is getting thrown everywhere we try to obtain the function pointer...
    new FunctionPointer<OnRemoved<COMPONENT>>(Marshal.GetFunctionPointerForDelegate(_reactor.OnRemoved))

    nevertheless its running and working, even if the exception is getting thrown :eek:
    any idea ?

    Code (CSharp):
    1. namespace Script.Client.Environment.Entitys.Systems {
    2.  
    3.     public delegate void OnAdded<T>(ref T item);
    4.     public delegate void OnRemoved<T>(in T item);
    5.     public delegate void OnChanged<T>(ref T item, in T oldItem);
    6.  
    7.     /// <summary>
    8.     /// A interface for the component which controlls the reaction to addition, removal and change
    9.     /// </summary>
    10.     /// <typeparam name="COMPONENT"></typeparam>
    11.     public interface IComponentReactor<COMPONENT> {
    12.         OnAdded<COMPONENT> OnAdded { get; }
    13.         OnRemoved<COMPONENT> OnRemoved { get; }
    14.         OnChanged<COMPONENT> OnChanged { get; }
    15.     }
    16.    
    17.     public abstract class MultiThreadedReactiveSystem<COMPONENT, COMPONENT_REACTOR> : SystemBase where COMPONENT : unmanaged, IComponentData where COMPONENT_REACTOR : struct, IComponentReactor<COMPONENT> {
    18.  
    19.         private COMPONENT_REACTOR _reactor;
    20.  
    21.         private EntityQuery _componentAddedQuery;
    22.         private EntityQuery _componentRemovedQuery;
    23.         private EntityQuery _componentValueChangedQuery;
    24.         private EntityCommandBufferSystem _entityCommandBufferSystem;
    25.    
    26.         /// <summary>
    27.         /// The state component for this reactive system.
    28.         /// It contains a copy of the COMPONENT data.
    29.         /// </summary>
    30.         public struct StateComponent : ISystemStateComponentData { public COMPONENT Value; }
    31.    
    32.         /// <inheritdoc/>
    33.         protected override void OnCreate() {
    34.             base.OnCreate();
    35.        
    36.             _reactor = CreateComponentRactor();
    37.        
    38.             // Query for receving all added components
    39.             _componentAddedQuery = GetEntityQuery(new EntityQueryDesc {
    40.                 All = new ComponentType[] { ComponentType.ReadWrite<COMPONENT>() },
    41.                 None = new ComponentType[] { ComponentType.ReadOnly(typeof(StateComponent)) }
    42.             });
    43.        
    44.             // Query for receiving all removed components
    45.             _componentRemovedQuery = GetEntityQuery(new EntityQueryDesc {
    46.                 All = new ComponentType[] { ComponentType.ReadOnly(typeof(StateComponent)) },
    47.                 None = new ComponentType[] { ComponentType.ReadOnly(typeof(COMPONENT)) }
    48.             });
    49.        
    50.             // Query for receiving all changed components
    51.             _componentValueChangedQuery = GetEntityQuery(new EntityQueryDesc {
    52.                 All = new ComponentType[] { ComponentType.ReadWrite<COMPONENT>(), ComponentType.ReadWrite<StateComponent>() }
    53.             });
    54.        
    55.             _entityCommandBufferSystem = GetCommandBufferSystem();
    56.         }
    57.    
    58.         /// <summary>
    59.         /// Create the reactor struct that implements the behavior when COMPONENT is added, removed or changed value.
    60.         /// </summary>
    61.         /// <returns>COMPONENT_REACTOR</returns>
    62.         protected abstract COMPONENT_REACTOR CreateComponentRactor();
    63.    
    64.         /// <summary>
    65.         /// Get the EntityCommandBufferSystem buffer system to use to add and remove the StateComponent.
    66.         /// </summary>
    67.         /// <returns>EntityCommandBufferSystem</returns>
    68.         protected EntityCommandBufferSystem GetCommandBufferSystem() { return World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>(); }
    69.    
    70.         /// <summary>
    71.         /// This system call the COMPONENT_REACTOR.ComponentAdded method on all enttiy that have a new COMPONENT.
    72.         /// </summary>
    73.         [BurstCompile]
    74.         private struct ManageComponentAdditionJob : IJobChunk {
    75.        
    76.             public EntityCommandBuffer.Concurrent EntityCommandBuffer;
    77.             public ArchetypeChunkComponentType<COMPONENT> ComponentChunk;
    78.        
    79.             [ReadOnly] public ArchetypeChunkEntityType EntityChunk;
    80.             [ReadOnly] public FunctionPointer<OnAdded<COMPONENT>> Reactor;
    81.        
    82.             public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
    83.            
    84.                 var components = chunk.GetNativeArray(ComponentChunk);
    85.                 var entities = chunk.GetNativeArray(EntityChunk);
    86.            
    87.                 for (var i = 0; i < chunk.Count; ++i) {
    88.                
    89.                     // Calls the mathod and reassign the COMPONENT to take into account any modification that may have accured during the method call.
    90.                     var component = components[i];
    91.                     Reactor.Invoke(ref component);
    92.                     components[i] = component;
    93.                
    94.                     // Add the system state component and set it's value that on the next frame, the ManageComponentValueChangeJob can handle any change in the COMPONENT value.
    95.                     EntityCommandBuffer.AddComponent<StateComponent>(chunkIndex, entities[i]);
    96.                     EntityCommandBuffer.SetComponent(chunkIndex, entities[i], new StateComponent() { Value = component });
    97.                 }
    98.             }
    99.         }
    100.    
    101.    
    102.         /// <summary>
    103.         /// This system call the COMPONENT_REACTOR.ComponentRemoved method on all enttiy that were strip down of their COMPONENT.
    104.         /// </summary>
    105.         [BurstCompile]
    106.         private struct ManageComponentRemovalJob : IJobChunk {
    107.        
    108.             public EntityCommandBuffer.Concurrent EntityCommandBuffer;
    109.             [ReadOnly] public ArchetypeChunkComponentType<StateComponent> StateComponentChunk;
    110.             [ReadOnly] public ArchetypeChunkEntityType EntityChunk;
    111.             [ReadOnly] public FunctionPointer<OnRemoved<COMPONENT>> Reactor;
    112.        
    113.             public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
    114.            
    115.                 var stateComponents = chunk.GetNativeArray(StateComponentChunk);
    116.                 var entities = chunk.GetNativeArray(EntityChunk);
    117.                 for (var i = 0; i < chunk.Count; ++i) {
    118.                
    119.                     // Calls the mathod with the last know copy of the component, this copy is read only has the component will be remove by hte end of the frame.
    120.                     var stateComponent = stateComponents[i];
    121.                     Reactor.Invoke(in stateComponent.Value);
    122.                
    123.                     // Remove the state component so that the entiyt can be destroyed or listen again for COMPONENT addition.
    124.                     EntityCommandBuffer.RemoveComponent<StateComponent>(chunkIndex, entities[i]);
    125.                 }
    126.             }
    127.         }
    128.    
    129.    
    130.         /// <summary>
    131.         /// This system call the COMPONENT_REACTOR.ComponentValueChanged method on all entity that had their COMPONENT value changed.
    132.         /// </summary>
    133.         [BurstCompile]
    134.         private struct ManageComponentValueChangeJob : IJobChunk {
    135.        
    136.             public ArchetypeChunkComponentType<COMPONENT> ComponentChunk;
    137.             public ArchetypeChunkComponentType<StateComponent> StateComponentChunk;
    138.             [ReadOnly] public FunctionPointer<OnChanged<COMPONENT>> Reactor;
    139.        
    140.             public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
    141.            
    142.                 var components = chunk.GetNativeArray(ComponentChunk);
    143.                 var stateComponents = chunk.GetNativeArray(StateComponentChunk);
    144.                 for (var i = 0; i < chunk.Count; ++i) {
    145.                
    146.                     // Chaeck if the value changed since last frame.
    147.                     var stateComponent = stateComponents[i];
    148.                     var component = components[i];
    149.                
    150.                     // If it did not change, move to the next entity in chunk.
    151.                     if (ByteBufferUtility.AreEqualStruct(stateComponent.Value, component)) continue;
    152.                
    153.                     // If it did change, call the method with the new value and the old value (from the last know copy of the COMPONENT)
    154.                     Reactor.Invoke(ref component, in stateComponent.Value);
    155.                
    156.                     // Ressign the COMPONENT to take into account any modification that may have accured during the method call.
    157.                     components[i] = component;
    158.                     // Update the copy of the COMPONENT.
    159.                     stateComponent.Value = component;
    160.                     stateComponents[i] = stateComponent;
    161.                 }
    162.             }
    163.         }
    164.    
    165.    
    166.         protected override void OnUpdate() {
    167.        
    168.             // Running job for calling changed values
    169.             var systemDeps = Dependency;
    170.             systemDeps = new ManageComponentValueChangeJob{
    171.                 ComponentChunk = GetArchetypeChunkComponentType<COMPONENT>(false),
    172.                 StateComponentChunk = GetArchetypeChunkComponentType<StateComponent>(false),
    173.                 Reactor = new FunctionPointer<OnChanged<COMPONENT>>(Marshal.GetFunctionPointerForDelegate(_reactor.OnChanged))
    174.             }.ScheduleParallel(_componentValueChangedQuery, systemDeps);
    175.        
    176.             // Running job for adding components
    177.             systemDeps = new ManageComponentAdditionJob{
    178.                 ComponentChunk = GetArchetypeChunkComponentType<COMPONENT>(false),
    179.                 EntityChunk = GetArchetypeChunkEntityType(),
    180.                 EntityCommandBuffer = _entityCommandBufferSystem.CreateCommandBuffer().ToConcurrent(),
    181.                 Reactor = new FunctionPointer<OnAdded<COMPONENT>>(Marshal.GetFunctionPointerForDelegate(_reactor.OnAdded))
    182.             }.ScheduleParallel(_componentAddedQuery, systemDeps);
    183.             _entityCommandBufferSystem.AddJobHandleForProducer(systemDeps);
    184.        
    185.             // Running job for component removal
    186.             systemDeps = new ManageComponentRemovalJob{
    187.                 StateComponentChunk = GetArchetypeChunkComponentType<StateComponent>(false),
    188.                 EntityChunk = GetArchetypeChunkEntityType(),
    189.                 EntityCommandBuffer = _entityCommandBufferSystem.CreateCommandBuffer().ToConcurrent(),
    190.                 Reactor = new FunctionPointer<OnRemoved<COMPONENT>>(Marshal.GetFunctionPointerForDelegate(_reactor.OnRemoved))
    191.             }.ScheduleParallel(_componentRemovedQuery, systemDeps);
    192.             _entityCommandBufferSystem.AddJobHandleForProducer(systemDeps);
    193.        
    194.             Dependency = systemDeps;
    195.         }
    196.     }
    197.  
    198.     /// <summary>
    199.     /// A class that contains multiple methods for comparing structs to each other and converting them to bytes.
    200.     /// </summary>
    201.     public static class ByteBufferUtility {
    202.        
    203.         /// <summary>
    204.         /// Checks if two structs are equal.
    205.         /// </summary>
    206.         /// <param name="frist"></param>
    207.         /// <param name="second"></param>
    208.         /// <typeparam name="T"></typeparam>
    209.         /// <returns></returns>
    210.         public static bool AreEqualStruct<T>(T frist, T second) where T : unmanaged {
    211.            
    212.             var firstArray = ConvertToNativeBytes<T>(frist, Allocator.Temp);
    213.             var secondArray = ConvertToNativeBytes<T>(second, Allocator.Temp);
    214.             if (firstArray.Length != secondArray.Length) return false;
    215.            
    216.             for (var i = 0; i < firstArray.Length; ++i)
    217.                 if (firstArray[i] != secondArray[i]) return false;
    218.            
    219.             return true;
    220.         }
    221.    
    222.         /// <summary>
    223.         /// Converts a struct to a native array of bytes.
    224.         /// </summary>
    225.         /// <param name="value"></param>
    226.         /// <param name="allocator"></param>
    227.         /// <typeparam name="T"></typeparam>
    228.         /// <returns></returns>
    229.         private static NativeArray<byte> ConvertToNativeBytes<T>(T value, Allocator allocator) where T : unmanaged {
    230.            
    231.             var size = UnsafeUtility.SizeOf<T>();
    232.             var ret = new NativeArray<byte>(size, allocator);
    233.            
    234.             unsafe { UnsafeUtility.CopyStructureToPtr(ref value, ret.GetUnsafePtr()); }
    235.             return ret;
    236.         }
    237.     }
    238. }
     
  18. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    It may be runing but did you check if it was still burst compiled ?
     
  19. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
  20. ElliotB

    ElliotB

    Joined:
    Aug 11, 2013
    Posts:
    285
    I'm a bit late but I hope this helps. I used a similar approach to the original proposal to implement a generic system that would listen for equipment being added/removed or enabled/disabled, and react accordingly. It used three components - `TParent`, `TEquipment` and `TAggregator` (a strategy used to modify TParent component values according to those of the TEquipment).

    https://github.com/ElliotB256/ECSCo...uipment/Aggregate/AggregateEquipmentSystem.cs

    The generic system was used like so (this example being the code that would modify a spaceship's speed when an engine was enabled or disabled)
    Code (CSharp):
    1. namespace Battle.Equipment
    2. {
    3.     [AlwaysUpdateSystem]
    4.     [UpdateInGroup(typeof(EquipmentUpdateGroup))]
    5.     public class EngineSystem : AggregateEquipmentSystem<Speed, Engine, EngineAggregator>
    6.     {
    7.         protected override AggregationScenario Scenario => AggregationScenario.OnEnableAndDisable;
    8.     }
    9.  
    10.     public struct EngineAggregator : IAggregator<Speed, Engine>
    11.     {
    12.         public Speed Combine(Speed original, Engine component)
    13.         {
    14.             original.Value += component.Thrust;
    15.             return original;
    16.         }
    17.  
    18.         public Speed Remove(Speed original, Engine component)
    19.         {
    20.             original.Value -= component.Thrust;
    21.             return original;
    22.         }
    23.     }
    24. }
    If you check the most recent version of the repository, you'll see I moved away from this pattern. I liked it, but I believe (and would love to be otherwise corrected) that Burst only supports generic classes with one generic argument - so the system would never be Burst compiled. I regret to say I don't have a source for that claim, I believe it was in the output log when trying to compile the package.
     
  21. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    I don't think it is.
    Maybe you were missing the [RegisterGenericComponentType] part ?

    [assembly: RegisterGenericComponentType(typeof(ReactiveComponentSystem<Health, HealthComponentReactor>.StateComponent))]
     
  22. DreamersINC

    DreamersINC

    Joined:
    Mar 4, 2015
    Posts:
    131
    FYI for any looking to use, This system does not function properly with empty Components/ComponentTags.
     
    Last edited: Jun 29, 2020
  23. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
  24. genaray

    genaray

    Joined:
    Feb 8, 2017
    Posts:
    191
    I gave up on the "Action/Func" callbacks in the multi threaded system... so i went with a burst and jobs compatible system instead :/

    So theres a "ReactiveSystem" ( should be burst/job compatible ) and a "CallbackReactiveSystem" which main purpose is to deliver callbacks to the main thread ( UI, Server/Client communication etc. )

    Code (CSharp):
    1.     /// <summary>
    2.     /// There no callbacks or listeners for added/removed components on <see cref="Entity"/>'s
    3.     /// Thats where this system comes in using <see cref="ISystemStateComponentData"/> for simulating those callbacks inside the ecs.
    4.     /// <typeparam name="Component">The component we wanna listen to</typeparam>
    5.     /// <typeparam name="Added">The component which indicates that our component has been added, gets attached for one frame to the entity</typeparam>
    6.     /// <typeparam name="Removed">The component which indicates that our component was removed, gets attached for one frame to the entity</typeparam>
    7.     /// </summary>
    8.     public class ReactiveSystem<Component, Added, Removed> : SystemBase where Added : IComponentData
    9.                                                                         where Removed : IComponentData {
    10.  
    11.         protected EntityQuery newEntities;
    12.         protected EntityQuery activeNewEntities;
    13.         protected EntityQuery activeRemovedEntities;
    14.         protected EntityQuery removedEntities;
    15.  
    16.         private EndInitializationEntityCommandBufferSystem atFrameStartBuffer;
    17.         private EndSimulationEntityCommandBufferSystem atFrameEndBuffer;
    18.  
    19.         [BurstCompile] public struct State : ISystemStateComponentData { public short state; }
    20.  
    21.         protected override void OnCreate() {
    22.             base.OnCreate();
    23.  
    24.             atFrameStartBuffer = World.GetOrCreateSystem<EndInitializationEntityCommandBufferSystem>();
    25.             atFrameEndBuffer = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    26.    
    27.             // Query to get all newly created entities, without being marked as added
    28.             newEntities = GetEntityQuery(new EntityQueryDesc{
    29.                 All = new[]{ComponentType.ReadOnly<Component>()},
    30.                 None = new[]{ComponentType.ReadOnly<State>()}
    31.             });
    32.    
    33.             // Query of all entities which where added this frame
    34.             activeNewEntities =  GetEntityQuery(new EntityQueryDesc{
    35.                 All = new[]{ComponentType.ReadOnly<State>(), ComponentType.ReadOnly<Added>()},
    36.                 None = new[]{ComponentType.ReadOnly<Removed>() }
    37.             });
    38.  
    39.             // Query of all entities which where removed this frame
    40.             activeRemovedEntities = GetEntityQuery(new EntityQueryDesc() {
    41.                 All = new[]{ComponentType.ReadOnly<State>(), ComponentType.ReadOnly<Removed>()},
    42.                 None = new[]{ComponentType.ReadOnly<Added>() }
    43.             });
    44.  
    45.             // Query to get all destroyed entities
    46.             removedEntities = GetEntityQuery(new EntityQueryDesc{
    47.                 All = new[]{ComponentType.ReadOnly<State>()},
    48.                 None = new[]{ComponentType.ReadOnly<Component>(), ComponentType.ReadOnly<Added>()}
    49.             });
    50.         }
    51.  
    52.         protected override void OnUpdate() {
    53.  
    54.             // The order matters.
    55.             // The query changes each time a component was added
    56.             // Added gets attached -> newEntities may result in different entities
    57.             // Thats why you need to make sure that the order of the components and querys make sense !
    58.  
    59.             var startCommandBuffer = atFrameStartBuffer.CreateCommandBuffer();
    60.             var endCommandBuffer = atFrameEndBuffer.CreateCommandBuffer();
    61.    
    62.             BeforeAdded?.Invoke();
    63.             startCommandBuffer.AddComponent<Added>(newEntities);
    64.  
    65.             startCommandBuffer.AddComponent<State>(newEntities);
    66.  
    67.             BeforeRemoved?.Invoke();
    68.             startCommandBuffer.AddComponent<Removed>(removedEntities);
    69.  
    70.             // Remove "added" from active new entities at the end of the frame
    71.             endCommandBuffer.RemoveComponent<Added>(activeNewEntities);
    72.    
    73.             // Remove "removed" from active destroyed entities at the end of the frame
    74.             endCommandBuffer.RemoveComponent<Removed>(removedEntities);
    75.             endCommandBuffer.RemoveComponent<State>(removedEntities);
    76.             endCommandBuffer.DestroyEntity(removedEntities);
    77.         }
    78.  
    79.         /// <summary>
    80.         /// A callback getting executed before the Added component was attached to the entities
    81.         /// </summary>
    82.         public Action BeforeAdded {
    83.             get;
    84.             set;
    85.         }
    86.  
    87.         /// <summary>
    88.         /// A callback getting executed before the removed component was attached to the entities
    89.         /// </summary>
    90.         public Action BeforeRemoved {
    91.             get;
    92.             set;
    93.         }
    94.     }
    Code (CSharp):
    1.  
    2.  
    3. public delegate void OnAddedEntity<T>(Entity entity, ref T item);
    4. public delegate void OnRemovedEntity<T>(Entity entity, in T item);
    5.  
    6. /// <summary>
    7.     /// A <see cref="ReactiveSystem{Component,Added,Removed}"/> with the ability to trigger callbacks once the desired component was added or removed.
    8.     /// </summary>
    9.     /// <typeparam name="Component">The component we wanna listen to</typeparam>
    10.     /// <typeparam name="Added">The component which indicates that our component has been added, gets attached for one frame to the entity</typeparam>
    11.     /// <typeparam name="Removed">The component which indicates that our component was removed, gets attached for one frame to the entity</typeparam>
    12.     public abstract class CallbackReactiveSystem<Component, Added, Removed> : ReactiveSystem<Component, Added, Removed> where Added : IComponentData
    13.                                                                                                                where Removed : IComponentData {
    14.  
    15.         private OnAddedEntity<Component> onAdded = (Entity entity, ref Component item) => { };
    16.         private OnRemovedEntity<Component> onRemoved = (Entity entity, in Component item) => { };
    17.  
    18.         private IList<Tuple<IList<Type>, OnAddedEntity<Component>>> onAddedType = new List<Tuple<IList<Type>, OnAddedEntity<Component>>>();
    19.         private IList<Tuple<IList<Type>, OnRemovedEntity<Component>>> onRemovedType = new List<Tuple<IList<Type>, OnRemovedEntity<Component>>>();
    20.  
    21.         protected override void OnCreate() {
    22.             base.OnCreate();
    23.    
    24.             BeforeAdded += AddedListener;
    25.             BeforeRemoved += RemovedListener;
    26.         }
    27.  
    28.         /// <summary>
    29.         /// Registers a listener for newly added componentents which only gets triggered if all given types are also attached to the entity
    30.         /// </summary>
    31.         /// <param name="onAdded">The callback getting executed</param>
    32.         /// <param name="types">The types that are required for the callback to get executed</param>
    33.         public void OnAddedType(OnAddedEntity<Component> onAdded, params Type[] types) {
    34.             onAddedType.Add(new Tuple<IList<Type>, OnAddedEntity<Component>>(types.ToList(), onAdded));
    35.         }
    36.  
    37.         /// <summary>
    38.         /// Registers a listener for newly removed componentents which only gets triggered if all given types are also attached to the entity
    39.         /// </summary>
    40.         /// <param name="onAdded">The callback getting executed</param>
    41.         /// <param name="types">The types that are required for the callback to get executed</param>
    42.         public void OnRemovedType(OnRemovedEntity<Component> onRemoved, params Type[] types) {
    43.             onRemovedType.Add(new Tuple<IList<Type>, OnRemovedEntity<Component>>(types.ToList(), onRemoved));
    44.         }
    45.  
    46.         /// <summary>
    47.         /// This method is used to iterate over all entities with the added component in order to fire a callback for each of them
    48.         /// </summary>
    49.         /// <param name="buffer"></param>
    50.         /// <param name="added"></param>
    51.         protected void AddedListener() {
    52.  
    53.             if (activeNewEntities.CalculateEntityCount() <= 0) return;
    54.    
    55.             var entities = activeNewEntities.ToEntityArray(Allocator.TempJob);
    56.             foreach (var entity in entities) {
    57.        
    58.                 var component = GetComponent(typeof(Component), entity);
    59.                 OnAdded?.Invoke(entity, ref component);
    60.        
    61.                 // Callbacks for those which require a list of other components attached
    62.                 foreach (var callback in onAddedType) {
    63.  
    64.                     var allValid = true;
    65.                     foreach(var type in callback.Item1)
    66.                         if (!EntityManager.HasComponent(entity, type))
    67.                             allValid = false;
    68.            
    69.                     if(allValid) callback.Item2.Invoke(entity, ref component);
    70.                 }
    71.             }
    72.             entities.Dispose();
    73.         }
    74.  
    75.         /// <summary>
    76.         /// This method is used to iterate over all entities with the removed component in order to fire a callback for each of them
    77.         /// </summary>
    78.         /// <param name="buffer"></param>
    79.         /// <param name="added"></param>
    80.         protected void RemovedListener() {
    81.  
    82.             if (activeRemovedEntities.CalculateEntityCount() <= 0) return;
    83.    
    84.             var entities = activeRemovedEntities.ToEntityArray(Allocator.TempJob);
    85.             foreach (var entity in entities) {
    86.  
    87.                 var component = GetComponent(typeof(Component), entity);
    88.                 OnRemoved?.Invoke(entity, component);
    89.        
    90.                 // Callbacks for those which require a list of other components attached
    91.                 foreach (var callback in onRemovedType) {
    92.  
    93.                     var allValid = true;
    94.                     foreach(var type in callback.Item1)
    95.                         if (!EntityManager.HasComponent(entity, type))
    96.                             allValid = false;
    97.            
    98.                     if(allValid) callback.Item2.Invoke(entity, in component);
    99.                 }
    100.             }
    101.             entities.Dispose();
    102.         }
    103.  
    104.         /// <summary>
    105.         /// A callback which should decide how we require the component of the entity.
    106.         /// This is nessecary because there several ways of getting them and several different component types ( Shared, IComponent, Object... )
    107.         /// </summary>
    108.         public abstract Component GetComponent(in Type type, in Entity entity);
    109.  
    110.         /// <summary>
    111.         /// A callback which gets called once a new instance of the component was added.
    112.         /// </summary>
    113.         public OnAddedEntity<Component> OnAdded {
    114.             get => onAdded;
    115.             set => onAdded = value;
    116.         }
    117.  
    118.         /// <summary>
    119.         /// A callback which gets called once a new instance of the component was removed.
    120.         /// </summary>
    121.         public OnRemovedEntity<Component> OnRemoved {
    122.             get => onRemoved;
    123.             set => onRemoved = value;
    124.         }
    125.     }
    The CallbackReactiveSystem isnt that clean and probably needs a little rework... i dont like the seperation of "callbacks" and "callback if type is attached to entity"...
    The useage is pretty straight... and works for IComponent's, SharedComponents and even ObjectComponents...

    Heres an example of how i listen to gameObjects which are getting added async. to my entities.
    First you need to create the system...
    Code (CSharp):
    1.  
    2.  
    3. [assembly: RegisterGenericComponentType(typeof(GameObjectReactiveSystem.State))]
    4. [BurstCompile] public struct GameObjectCreated : IComponentData{}
    5.     [BurstCompile] public struct GameObjectDestroyed : IComponentData{}
    6.  
    7.     /// <summary>
    8.     /// This system listens for <see cref="GameObject"/> getting attached to entities in order to trigger callbacks.
    9.     /// </summary>
    10.     public class GameObjectReactiveSystem : CallbackReactiveSystem<GameObject, GameObjectCreated, GameObjectDestroyed> {
    11.         public override GameObject GetComponent(in Type type, in Entity entity) { return EntityManager.GetComponentObject<GameObject>(entity); }
    12.     }
    13.  
    Then you either listen in a system like...
    Code (CSharp):
    1. Entities.ForEach((GameObject go, ref GameObjectCreated goa) => {
    2.   // My Code
    3. }.Schedule();
    Or you listen using the callbacks...
    Code (CSharp):
    1. var gorc = world.GetOrCreateSystem<GameObjectReactiveSystem>();
    2.  
    3. // Direct action chaining, getting called everytime a g.o. getting assigned
    4. gorc.OnAdded += (Entity entity, ref GameObject item){
    5.   Debug.Log("Added");
    6. }
    7.  
    8. // Action only getting executed if the entity has a "Player" component attached ( Params, multiple types possible at this point ) and a gameobject was just assigned to that entity
    9. gorc.OnAddedType((...) => {}, typeof(Player));
    Hope this helps anyone... this system cant listen for value changes, i wasnt sure how you add this mechanic in a clear and efficient way, probably someone else is able to add this to my reactive system ^^
     
    Last edited: Jun 30, 2020
  25. DreamersINC

    DreamersINC

    Joined:
    Mar 4, 2015
    Posts:
    131
    ReactiveComponentSystem<COMPONENT, COMPONENT_REACTOR>.
    When using an empty component, I get the follow error message.
    InvalidOperationException: The previously scheduled job TRSToLocalToWorldSystem:TRSToLocalToWorld reads from the NativeArray TRSToLocalToWorld.safety. You are trying to schedule a new job ReactiveComponentSystem`3:ManageComponentValueChangeJob, which writes to the same NativeArray (via ManageComponentValueChangeJob.JobData.ComponentChunk). To guarantee safety, you must include TRSToLocalToWorldSystem:TRSToLocalToWorld as a dependency of the newly scheduled job.
    Unity.Entities.JobChunkExtensions.ScheduleInternal[T] (T& jobData, Unity.Entities.EntityQuery query, Unity.Jobs.JobHandle dependsOn, Unity.Jobs.LowLevel.Unsafe.ScheduleMode mode, System.Boolean isParallel) (at Library/PackageCache/com.unity.entities@0.11.1-preview.4/Unity.Entities/IJobChunk.cs:216)
    Unity.Entities.JobChunkExtensions.ScheduleParallel[T] (T jobData, Unity.Entities.EntityQuery query, Unity.Jobs.JobHandle dependsOn) (at Library/PackageCache/com.unity.entities@0.11.1-preview.4/Unity.Entities/IJobChunk.cs:128)
    Utilities.ReactiveSystem.ReactiveComponentSystem`3[COMPONENT,AICOMPONENT,COMPONENT_REACTOR].OnUpdate () (at Assets/Scripts/IAUS/ECS Take2/Reactive System/ReactiveSystemBase.cs:187)

    By adding a single variable to the component, everything works smoothly
     
  26. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    Well that's unexpected, I don't see why the TRSToLocalToWorldSystem get involved here...
     
  27. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    So your action happening on adding or removing a component is executed non bursted on the main thread, correct ?
    If so, (probably even if not) I still prefer to make a new derived system for the every new behavior.
    1 ) it's in a job
    2 ) it's bursted
    3 ) there is a clear separation for each behavior (can enable/disable)
    4 ) this make much smaller files if you have several behavior helping in maintaining hte code and avoiding potential regression is case of change to one of the behavior.
    5 ) If several behavior don't need the same state data, or even some additionnal component or condition (e.g. : one behavior should apply only if the entity also has some component data), then there is no risk to apply a behavior by mistake as the system only run on the targeted entities.
     
  28. DreamersINC

    DreamersINC

    Joined:
    Mar 4, 2015
    Posts:
    131
    The action adding/removing components is burst compiled jobs. The jobs systems causing the errors aren't even related to the components being added on removed. I think it is a DOTS quirk as adding a simple int to the IComponentdata Struct solves as all this issues.
     
  29. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    I retested quickly the empty component thing and I don't have the issue.
    I do have another issue about trying to get zero sized component but I'm working on a fix for that.
     
  30. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    This version works with empty component. (note: with empty component we don't check for change in value as tehre is none). There was no change to the interface. you can drop in the new abstract system in place of the old one and all should work.

    Code (CSharp):
    1.  
    2. using Unity.Burst;
    3. using Unity.Collections;
    4. using Unity.Collections.LowLevel.Unsafe;
    5. using Unity.Entities;
    6. using Unity.Jobs;
    7.  
    8. public interface IComponentReactor<COMPONENT>
    9. {
    10.     void ComponentAdded(ref COMPONENT newComponent);
    11.     void ComponentRemoved(in COMPONENT oldComponent);
    12.     void ComponentValueChanged(ref COMPONENT newComponent, in COMPONENT oldComponent);
    13. }
    14.  
    15.  
    16. public abstract class ReactiveComponentSystem<COMPONENT, COMPONENT_REACTOR> : SystemBase
    17.     where COMPONENT : unmanaged, IComponentData
    18.     where COMPONENT_REACTOR : struct, IComponentReactor<COMPONENT>
    19. {
    20.     /// <summary>
    21.     /// Struct implmenting IComponentReactor<COMPONENT> that implements the behavior when COMPONENT is added, removed or changed value.
    22.     /// </summary>
    23.     private COMPONENT_REACTOR _reactor;
    24.  
    25.     /// <summary>
    26.     /// Query to detect the addition of COMPONENT to an entity.
    27.     /// </summary>
    28.     private EntityQuery _componentAddedQuery;
    29.     /// <summary>
    30.     /// Query to detect the removal of COMPONENT from an entity.
    31.     /// </summary>
    32.     private EntityQuery _componentRemovedQuery;
    33.     /// <summary>
    34.     /// Query to gateher all entity that need to check for change in value.
    35.     /// </summary>
    36.     private EntityQuery _componentValueChangedQuery;
    37.  
    38.     /// <summary>
    39.     /// EnityCommandBufferSystem used to add and remove the StateComponent.
    40.     /// </summary>
    41.     private EntityCommandBufferSystem _entityCommandBufferSystem;
    42.  
    43.     private bool _isZeroSized;
    44.  
    45.     /// <summary>
    46.     /// The state component for this reactive system.
    47.     /// It contains a copy of the COMPONENT data.
    48.     /// </summary>
    49.     public struct StateComponent : ISystemStateComponentData
    50.     {
    51.         public COMPONENT Value;
    52.     }
    53.  
    54.     /// <inheritdoc/>
    55.     protected override void OnCreate()
    56.     {
    57.         base.OnCreate();
    58.         _reactor = CreateComponentRactor();
    59.  
    60.         int m_TypeIndex = TypeManager.GetTypeIndex<COMPONENT>();
    61.         _isZeroSized = TypeManager.GetTypeInfo(m_TypeIndex).IsZeroSized;
    62.  
    63.         _componentAddedQuery = GetEntityQuery(new EntityQueryDesc()
    64.         {
    65.             All = new ComponentType[] { ComponentType.ReadWrite(typeof(COMPONENT)) },
    66.             None = new ComponentType[] { ComponentType.ReadOnly(typeof(StateComponent)) }
    67.         });
    68.  
    69.         _componentRemovedQuery = GetEntityQuery(new EntityQueryDesc()
    70.         {
    71.             All = new ComponentType[] { ComponentType.ReadOnly(typeof(StateComponent)) },
    72.             None = new ComponentType[] { ComponentType.ReadOnly(typeof(COMPONENT)) }
    73.         });
    74.  
    75.         _componentValueChangedQuery = GetEntityQuery(new EntityQueryDesc()
    76.         {
    77.             All = new ComponentType[] { ComponentType.ReadWrite(typeof(COMPONENT)), ComponentType.ReadWrite(typeof(StateComponent)) }
    78.         });
    79.  
    80.         _entityCommandBufferSystem = GetCommandBufferSystem();
    81.     }
    82.  
    83.     /// <summary>
    84.     /// Create the reactor struct that implements the behavior when COMPONENT is added, removed or changed value.
    85.     /// </summary>
    86.     /// <returns>COMPONENT_REACTOR</returns>
    87.     protected abstract COMPONENT_REACTOR CreateComponentRactor();
    88.  
    89.     /// <summary>
    90.     /// Get the EntityCommandBufferSystem buffer system to use to add and remove the StateComponent.
    91.     /// </summary>
    92.     /// <returns>EntityCommandBufferSystem</returns>
    93.     protected EntityCommandBufferSystem GetCommandBufferSystem()
    94.     {
    95.         return World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    96.     }
    97.  
    98.     /// <summary>
    99.     /// This system call the COMPONENT_REACTOR.ComponentAdded method on all enttiy that have a new COMPONENT.
    100.     /// </summary>
    101.     [BurstCompile]
    102.     private struct ManageComponentAdditionJob : IJobChunk
    103.     {
    104.         public EntityCommandBuffer.Concurrent EntityCommandBuffer;
    105.         public ArchetypeChunkComponentType<COMPONENT> ComponentChunk;
    106.         public bool IsZeroSized;
    107.         [ReadOnly] public ArchetypeChunkEntityType EntityChunk;
    108.         [ReadOnly] public COMPONENT_REACTOR Reactor;
    109.  
    110.         public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    111.         {
    112.             NativeArray<COMPONENT> components = IsZeroSized ? default : chunk.GetNativeArray(ComponentChunk);
    113.             NativeArray<Entity> entities = chunk.GetNativeArray(EntityChunk);
    114.  
    115.             for (int i = 0; i < chunk.Count; ++i)
    116.             {
    117.                 // Calls the mathod and reassign the COMPONENT to take into account any modification that may have accured during the method call.
    118.                 COMPONENT component = IsZeroSized ? default : components[i];
    119.                 Reactor.ComponentAdded(ref component);
    120.                 if (!IsZeroSized)
    121.                 {
    122.                     components[i] = component;
    123.  
    124.                 }
    125.                 // Add the system state component and set it's value that on the next frame, the ManageComponentValueChangeJob can handle any change in the COMPONENT value.
    126.                 EntityCommandBuffer.AddComponent<StateComponent>(chunkIndex, entities[i]);
    127.                 EntityCommandBuffer.SetComponent(chunkIndex, entities[i], new StateComponent() { Value = component });
    128.             }
    129.  
    130.         }
    131.     }
    132.     /// <summary>
    133.     /// This system call the COMPONENT_REACTOR.ComponentRemoved method on all enttiy that were strip down of their COMPONENT.
    134.     /// </summary>
    135.     [BurstCompile]
    136.     private struct ManageComponentRemovalJob : IJobChunk
    137.     {
    138.  
    139.         public EntityCommandBuffer.Concurrent EntityCommandBuffer;
    140.         public bool IsZeroSized;
    141.         [ReadOnly] public ArchetypeChunkComponentType<StateComponent> StateComponentChunk;
    142.         [ReadOnly] public ArchetypeChunkEntityType EntityChunk;
    143.         [ReadOnly] public COMPONENT_REACTOR Reactor;
    144.  
    145.         public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    146.         {
    147.             NativeArray<StateComponent> stateComponents = IsZeroSized ? default : chunk.GetNativeArray(StateComponentChunk);
    148.             NativeArray<Entity> entities = chunk.GetNativeArray(EntityChunk);
    149.  
    150.             for (int i = 0; i < chunk.Count; ++i)
    151.             {
    152.                 // Calls the mathod with the last know copy of the component, this copy is read only has the component will be remove by hte end of the frame.
    153.                 COMPONENT stateComponent = IsZeroSized ? default : stateComponents[i].Value;
    154.                 Reactor.ComponentRemoved(in stateComponent);
    155.  
    156.                 EntityCommandBuffer.RemoveComponent<StateComponent>(chunkIndex, entities[i]);
    157.  
    158.             }
    159.  
    160.         }
    161.     }
    162.  
    163.     /// <summary>
    164.     /// This system call the COMPONENT_REACTOR.ComponentValueChanged method on all entity that had their COMPONENT value changed.
    165.     /// </summary>
    166.     [BurstCompile]
    167.     private struct ManageComponentValueChangeJob : IJobChunk
    168.     {
    169.         public ArchetypeChunkComponentType<COMPONENT> ComponentChunk;
    170.         public ArchetypeChunkComponentType<StateComponent> StateComponentChunk;
    171.         [ReadOnly] public COMPONENT_REACTOR Reactor;
    172.  
    173.         public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    174.         {
    175.             NativeArray<COMPONENT> components = chunk.GetNativeArray(ComponentChunk);
    176.             NativeArray<StateComponent> stateComponents = chunk.GetNativeArray(StateComponentChunk);
    177.  
    178.             for (int i = 0; i < chunk.Count; ++i)
    179.             {
    180.                 // Chaeck if the value changed since last frame.
    181.                 StateComponent stateComponent = stateComponents[i];
    182.                 COMPONENT component = components[i];
    183.  
    184.                 // If it did not change, move to the next entity in chunk.
    185.                 if (ByteBufferUtility.AreEqualStruct(stateComponent.Value, component)) continue;
    186.                 // If it did change, call the method with the new value and the old value (from the last know copy of the COMPONENT)
    187.                 Reactor.ComponentValueChanged(ref component, in stateComponent.Value);
    188.  
    189.                 // Ressign the COMPONENT to take into account any modification that may have accured during the method call.
    190.                 components[i] = component;
    191.  
    192.                 // Update the copy of the COMPONENT.
    193.                 stateComponent.Value = component;
    194.                 stateComponents[i] = stateComponent;
    195.             }
    196.  
    197.         }
    198.     }
    199.  
    200.  
    201.  
    202.     protected override void OnUpdate()
    203.     {
    204.         JobHandle systemDeps = Dependency;
    205.         // There is no point in looking for change in a component that has no data.
    206.         if (!_isZeroSized)
    207.         {
    208.             systemDeps = new ManageComponentValueChangeJob()
    209.             {
    210.                 ComponentChunk = GetArchetypeChunkComponentType<COMPONENT>(false),
    211.                 StateComponentChunk = GetArchetypeChunkComponentType<StateComponent>(false),
    212.                 Reactor = _reactor
    213.  
    214.             }.ScheduleParallel(_componentValueChangedQuery, systemDeps);
    215.         }
    216.  
    217.         systemDeps = new ManageComponentAdditionJob()
    218.         {
    219.             ComponentChunk = GetArchetypeChunkComponentType<COMPONENT>(false),
    220.             EntityChunk = GetArchetypeChunkEntityType(),
    221.             EntityCommandBuffer = _entityCommandBufferSystem.CreateCommandBuffer().ToConcurrent(),
    222.             Reactor = _reactor,
    223.             IsZeroSized = _isZeroSized
    224.         }.ScheduleParallel(_componentAddedQuery, systemDeps);
    225.  
    226.         _entityCommandBufferSystem.AddJobHandleForProducer(systemDeps);
    227.  
    228.  
    229.         systemDeps = new ManageComponentRemovalJob()
    230.         {
    231.             StateComponentChunk = GetArchetypeChunkComponentType<StateComponent>(false),
    232.             EntityChunk = GetArchetypeChunkEntityType(),
    233.             EntityCommandBuffer = _entityCommandBufferSystem.CreateCommandBuffer().ToConcurrent(),
    234.             Reactor = _reactor,
    235.             IsZeroSized = _isZeroSized
    236.         }.ScheduleParallel(_componentRemovedQuery, systemDeps);
    237.  
    238.         _entityCommandBufferSystem.AddJobHandleForProducer(systemDeps);
    239.  
    240.         Dependency = systemDeps;
    241.     }
    242.  
    243.  
    244.     public static class ByteBufferUtility
    245.     {
    246.         public static bool AreEqualStruct<T>(T frist, T second) where T : unmanaged
    247.         {
    248.             NativeArray<byte> firstArray = ConvertToNativeBytes<T>(frist, Allocator.Temp);
    249.             NativeArray<byte> secondArray = ConvertToNativeBytes<T>(second, Allocator.Temp);
    250.  
    251.             if (firstArray.Length != secondArray.Length) return false;
    252.  
    253.             for (int i = 0; i < firstArray.Length; ++i)
    254.             {
    255.                 if (firstArray[i] != secondArray[i]) return false;
    256.             }
    257.  
    258.             return true;
    259.  
    260.         }
    261.  
    262.         private static NativeArray<byte> ConvertToNativeBytes<T>(T value, Allocator allocator) where T : unmanaged
    263.         {
    264.             int size = UnsafeUtility.SizeOf<T>();
    265.             NativeArray<byte> ret = new NativeArray<byte>(size, allocator);
    266.  
    267.             unsafe
    268.             {
    269.                 UnsafeUtility.CopyStructureToPtr(ref value, ret.GetUnsafePtr());
    270.             }
    271.  
    272.             return ret;
    273.         }
    274.  
    275.     }
    276.  
    277. }
    278.  
    Code (CSharp):
    1. using Unity.Entities;
    2.  
    3. using UnityEngine;
    4.  
    5.  
    6. [assembly: RegisterGenericComponentType(typeof(ReactiveComponentSystem<EmptyComponent, HealthComponentReactor>.StateComponent))]
    7.  
    8. [GenerateAuthoringComponent]
    9. public struct EmptyComponent : IComponentData
    10. {
    11. }
    12.  
    13. public struct HealthComponentReactor : IComponentReactor<EmptyComponent>
    14. {
    15.     public void ComponentAdded(ref EmptyComponent newComponent)
    16.     {
    17.         Debug.Log("Added");
    18.     }
    19.  
    20.     public void ComponentRemoved(in EmptyComponent oldComponent)
    21.     {
    22.         Debug.Log("Removed");
    23.     }
    24.  
    25.     public void ComponentValueChanged(ref EmptyComponent newComponent, in EmptyComponent oldComponent)
    26.     {
    27.         Debug.LogError("That should never be called !");
    28.     }
    29. }
    30. public class HealtReactiveSystem : ReactiveComponentSystem<EmptyComponent, HealthComponentReactor>
    31. {
    32.     protected override HealthComponentReactor CreateComponentRactor()
    33.     {
    34.         return new HealthComponentReactor();
    35.     }
    36. }
    37.  
    EDIT : Consider this code under MIT license.
     
    Last edited: Sep 25, 2020
  31. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    I think one system could do all the job let me try it. will update soon.
     
  32. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    Last edited: Sep 27, 2020
    Timboc likes this.
  33. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    Hi,
    Nice work. I'll allow myself some remarks in comparison with my system.

    The usage code is simple but the base system itself seems much more involved.

    I don't understand exactly what info you get from your tracking system appart from the add/exists/remove and I'm not sure how to react to a tracked component change when you are tracking several.

    An other difference with my system is that you seem your exists will trigger even when the component value did not change as I don't see any checks for it.

    You say you have one system to track all components but you would still need to make a system to react to each component change specifically. So I see little change here.

    I'm curious how both solutions do performance wise (CPU,memory cost), because having the tracking component at all time will avoid the add/remove cost of state component, but the bit mask seems to need some memory. On my system side state component should cost no very little memory except for the change component wich track the previous value (but if your system does/did, it will cost the same)
    Also I could probably use the change filter on the component to improve.performance on the value change tracking.

    All in all it feels like both systems do different things.and don't meet the same set of requirements.
     
  34. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    Basically, one ComponentExist component can track all components on an entity. 2bit for each tracked component.
    You just need to feed ComponentExist with ComponentExistHandle(which defines one type) then it will give you that component type's State.

    TrackID is a condensed TypeIndex(without mask), that skips all untrack indexes.
    With TrackID, I can make sure that the storage for all components is Minimum. And can be stored linearly in a list, instead of HashMap, so getting TrackID and updating existence state is fast. all operation is manually vectorized.
    So It's boosted by SIMD. It should be the fastest solution. But could be some minor optimization.

    The test system repeating prints ExistState Just because ComponentExist is updated every frame.
     
    Last edited: Sep 27, 2020
  35. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    Hi,
    I imported your scripts in my project to test it a bit and try to understand better how it works.
    I had to do some changes because of compilation errors so I submitted issues in your repository.
    Also ExistState does not seem to work (may be due to the changes I had to make).
     
  36. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    Okay look like I had better make a Unity Project with AsmReference Define.
    So those Unity.Entities internal will be available.
     
  37. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    That way this system works:

    0. Add ComponentExist on Entities you want to track

    Then in your system

    1. In OnCreate() call ComponentExistInfoSystem.RegisterTypeForTracking<T>() tell tracking system I want to track this type's change (That will apply to all Entity with ComponentExist as it Register a type not a entity), you can Register multiple Type, and Register the same Type multiple times is fine.

    2.in OnUpdate() call ComponentExistInfoSystem.GetExistHandle<T>() to get any number of TrackHandle you need. You will need them in job.

    3. add ComponentExist component to your Query or ForEach

    4. In the job, Call ComponentExist.GetTrackState(ComponentExistHandle) to get that component's State. Or WasAdded/WasRemoved/WasExist(ComponentExistHandle).
    All Types's State is in that single ComponentExist
     
  38. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    Shouldn't the change filter avoid that ?
     
  39. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    well, those "WithChangeFilter" can be removed.
    ComponentExist is ReadWrite accessed by the tracking system every frame. Just leaving it there to see if I can hack it. But It turns out to be not very easy.
     
  40. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    Still Exists don't track change, it track that the component was neither added nor removed this frame.
    It does not give any additional information than the existance of a component which can be assessed by the entity query or the HasComponent.
     
  41. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    Okay, I got what you mean.My goal is to cacehe wasAdded and wasRemoved transient state. Not planing for data change.
    As WithChangeFilter<T>() is good enough. But Write access can be track with a little change on my source. If a full old vs new data comparison is needed. then 2bits each component is apparently not enough.
    I do have some other systems doing this type of work, they are on my private Gitlab. I can make a full repo later when my game project is finished. As they are just too much. Including a full 2D Dots Physic Engine...
     
    Last edited: Sep 27, 2020
  42. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    The project is set up and fixed a couple of bugs. Now the lib can be tested.
     
  43. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    ComponentExist version change only when some type is added or removed now.
    So WithChangeFilter<ComponentExist> + WithChangeFilter<ComponentDisable> is working as expected now.
     
  44. genaray

    genaray

    Joined:
    Feb 8, 2017
    Posts:
    191
    Hey there ! Saw this because you replied to my reactive system.
    Yours look great ! And the interface is a great idea, i just wonder if theres any chance to combine our both systems to have one reactive-system which is capable of registering such interface-callbacks aswell as iterating over added/removed components.

    Such a combined system would truly solve all issues at once ^^
     
  45. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    I'm not sure I understand what your mean.
    This system will iteratare over all added / removed component and execute the proper method from the reactor component.
    If you mean to have several reactor you can either create several derived system of adapt this one to allow you to register a list of reactor and run them all at once.

    Last thought would be if you want to behave differently for some archetype when the component is added then we could adapt the base system to accept a base query and make a derived system for each archetype that should behave differently.

    Can you clarify what you mean ?
     
  46. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,116
    @WAYN_Games is the MGM-Ability project abandoned ?
    very soon, I will start creating a DOTS package to recreate Unreal's Gameplay Ability System.
     
  47. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    Hello,

    Thanks you for your interest.
    No it is not abandoned. But to be honest I have not add time to work much on it lately.
    I wanted to move forward with integration to animation / vfx / audios but the dots package are not in a really user friendly state at the moment. And since we did not have upgrade for a long time I suspect it's not worth diving into it know.

    Also I started integrating the new input system but being stuck in 2020.3 I'm plagued with a bug when using entities work and new input system that don't allow me to use subscenes.

    I should have a bit more time starting next year to work on that. There is still a lot to do and I hope we will have some unity dots update in the near future to renew my motivation ;).

    I'm not familiar with unreal's ability system but I'll look at it for inspiration. Until now I was trying to make something that I find performant and practical (without much frame of reference...). I also have ideas on the authoring side but it will be even further down the line.

    Anyways if you want to bounce idea back and forth in your own implementation feel free to contact me. ;).
     
    Opeth001 likes this.
  48. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,116
    Sadly Dots is still too unstable, but I think after their new official post things will start to change very soon.


    Gameplay Ability System is used in games like Fortnite ..., to create abilities ranging from a simple jump to a fireball eg:
    .


    Thanks !!!