Reactive System - Generic way ?

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.

using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Entities;
using Unity.Jobs;

public interface IComponentReactor<COMPONENT>
{
    void ComponentAdded(ref COMPONENT newComponent);
    void ComponentRemoved(in COMPONENT oldComponent);
    void ComponentValueChanged(ref COMPONENT newComponent, in COMPONENT oldComponent);
}


public abstract class ReactiveComponentSystem<COMPONENT, COMPONENT_REACTOR> : SystemBase
    where COMPONENT : unmanaged, IComponentData
    where COMPONENT_REACTOR : struct, IComponentReactor<COMPONENT>
{
    /// <summary>
    /// Struct implmenting IComponentReactor<COMPONENT> that implements the behavior when COMPONENT is added, removed or changed value.
    /// </summary>
    private COMPONENT_REACTOR _reactor;

    /// <summary>
    /// Query to detect the addition of COMPONENT to an entity.
    /// </summary>
    private EntityQuery _componentAddedQuery;
    /// <summary>
    /// Query to detect the removal of COMPONENT from an entity.
    /// </summary>
    private EntityQuery _componentRemovedQuery;
    /// <summary>
    /// Query to gateher all entity that need to check for change in value.
    /// </summary>
    private EntityQuery _componentValueChangedQuery;

    /// <summary>
    /// EnityCommandBufferSystem used to add and remove the StateComponent.
    /// </summary>
    private EntityCommandBufferSystem _entityCommandBufferSystem;

    private bool _isZeroSized;

    /// <summary>
    /// The state component for this reactive system.
    /// It contains a copy of the COMPONENT data.
    /// </summary>
    public struct StateComponent : ISystemStateComponentData
    {
        public COMPONENT Value;
    }

    /// <inheritdoc/>
    protected override void OnCreate()
    {
        base.OnCreate();
        _reactor = CreateComponentRactor();

        int m_TypeIndex = TypeManager.GetTypeIndex<COMPONENT>();
        _isZeroSized = TypeManager.GetTypeInfo(m_TypeIndex).IsZeroSized;

        _componentAddedQuery = GetEntityQuery(new EntityQueryDesc()
        {
            All = new ComponentType[] { ComponentType.ReadWrite(typeof(COMPONENT)) },
            None = new ComponentType[] { ComponentType.ReadOnly(typeof(StateComponent)) }
        });

        _componentRemovedQuery = GetEntityQuery(new EntityQueryDesc()
        {
            All = new ComponentType[] { ComponentType.ReadOnly(typeof(StateComponent)) },
            None = new ComponentType[] { ComponentType.ReadOnly(typeof(COMPONENT)) }
        });

        _componentValueChangedQuery = GetEntityQuery(new EntityQueryDesc()
        {
            All = new ComponentType[] { ComponentType.ReadWrite(typeof(COMPONENT)), ComponentType.ReadWrite(typeof(StateComponent)) }
        });

        _entityCommandBufferSystem = GetCommandBufferSystem();
    }

    /// <summary>
    /// Create the reactor struct that implements the behavior when COMPONENT is added, removed or changed value.
    /// </summary>
    /// <returns>COMPONENT_REACTOR</returns>
    protected abstract COMPONENT_REACTOR CreateComponentRactor();

    /// <summary>
    /// Get the EntityCommandBufferSystem buffer system to use to add and remove the StateComponent.
    /// </summary>
    /// <returns>EntityCommandBufferSystem</returns>
    protected EntityCommandBufferSystem GetCommandBufferSystem()
    {
        return World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }

    /// <summary>
    /// This system call the COMPONENT_REACTOR.ComponentAdded method on all enttiy that have a new COMPONENT.
    /// </summary>
    [BurstCompile]
    private struct ManageComponentAdditionJob : IJobChunk
    {
        public EntityCommandBuffer.Concurrent EntityCommandBuffer;
        public ArchetypeChunkComponentType<COMPONENT> ComponentChunk;
        public bool IsZeroSized;
        [ReadOnly] public ArchetypeChunkEntityType EntityChunk;
        [ReadOnly] public COMPONENT_REACTOR Reactor;

        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
        {
            NativeArray<COMPONENT> components = IsZeroSized ? default : chunk.GetNativeArray(ComponentChunk);
            NativeArray<Entity> entities = chunk.GetNativeArray(EntityChunk);

            for (int i = 0; i < chunk.Count; ++i)
            {
                // Calls the mathod and reassign the COMPONENT to take into account any modification that may have accured during the method call.
                COMPONENT component = IsZeroSized ? default : components[i];
                Reactor.ComponentAdded(ref component);
                if (!IsZeroSized)
                {
                    components[i] = component;

                }
                // 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.
                EntityCommandBuffer.AddComponent<StateComponent>(chunkIndex, entities[i]);
                EntityCommandBuffer.SetComponent(chunkIndex, entities[i], new StateComponent() { Value = component });
            }

        }
    }
    /// <summary>
    /// This system call the COMPONENT_REACTOR.ComponentRemoved method on all enttiy that were strip down of their COMPONENT.
    /// </summary>
    [BurstCompile]
    private struct ManageComponentRemovalJob : IJobChunk
    {

        public EntityCommandBuffer.Concurrent EntityCommandBuffer;
        public bool IsZeroSized;
        [ReadOnly] public ArchetypeChunkComponentType<StateComponent> StateComponentChunk;
        [ReadOnly] public ArchetypeChunkEntityType EntityChunk;
        [ReadOnly] public COMPONENT_REACTOR Reactor;

        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
        {
            NativeArray<StateComponent> stateComponents = IsZeroSized ? default : chunk.GetNativeArray(StateComponentChunk);
            NativeArray<Entity> entities = chunk.GetNativeArray(EntityChunk);

            for (int i = 0; i < chunk.Count; ++i)
            {
                // 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.
                COMPONENT stateComponent = IsZeroSized ? default : stateComponents[i].Value;
                Reactor.ComponentRemoved(in stateComponent);

                EntityCommandBuffer.RemoveComponent<StateComponent>(chunkIndex, entities[i]);

            }

        }
    }

    /// <summary>
    /// This system call the COMPONENT_REACTOR.ComponentValueChanged method on all entity that had their COMPONENT value changed.
    /// </summary>
    [BurstCompile]
    private struct ManageComponentValueChangeJob : IJobChunk
    {
        public ArchetypeChunkComponentType<COMPONENT> ComponentChunk;
        public ArchetypeChunkComponentType<StateComponent> StateComponentChunk;
        [ReadOnly] public COMPONENT_REACTOR Reactor;

        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
        {
            NativeArray<COMPONENT> components = chunk.GetNativeArray(ComponentChunk);
            NativeArray<StateComponent> stateComponents = chunk.GetNativeArray(StateComponentChunk);

            for (int i = 0; i < chunk.Count; ++i)
            {
                // Chaeck if the value changed since last frame.
                StateComponent stateComponent = stateComponents[i];
                COMPONENT component = components[i];

                // If it did not change, move to the next entity in chunk.
                if (ByteBufferUtility.AreEqualStruct(stateComponent.Value, component)) continue;
                // If it did change, call the method with the new value and the old value (from the last know copy of the COMPONENT)
                Reactor.ComponentValueChanged(ref component, in stateComponent.Value);

                // Ressign the COMPONENT to take into account any modification that may have accured during the method call.
                components[i] = component;

                // Update the copy of the COMPONENT.
                stateComponent.Value = component;
                stateComponents[i] = stateComponent;
            }

        }
    }



    protected override void OnUpdate()
    {
        JobHandle systemDeps = Dependency;
        // There is no point in looking for change in a component that has no data.
        if (!_isZeroSized)
        {
            systemDeps = new ManageComponentValueChangeJob()
            {
                ComponentChunk = GetArchetypeChunkComponentType<COMPONENT>(false),
                StateComponentChunk = GetArchetypeChunkComponentType<StateComponent>(false),
                Reactor = _reactor

            }.ScheduleParallel(_componentValueChangedQuery, systemDeps);
        }

        systemDeps = new ManageComponentAdditionJob()
        {
            ComponentChunk = GetArchetypeChunkComponentType<COMPONENT>(false),
            EntityChunk = GetArchetypeChunkEntityType(),
            EntityCommandBuffer = _entityCommandBufferSystem.CreateCommandBuffer().ToConcurrent(),
            Reactor = _reactor,
            IsZeroSized = _isZeroSized
        }.ScheduleParallel(_componentAddedQuery, systemDeps);

        _entityCommandBufferSystem.AddJobHandleForProducer(systemDeps);


        systemDeps = new ManageComponentRemovalJob()
        {
            StateComponentChunk = GetArchetypeChunkComponentType<StateComponent>(false),
            EntityChunk = GetArchetypeChunkEntityType(),
            EntityCommandBuffer = _entityCommandBufferSystem.CreateCommandBuffer().ToConcurrent(),
            Reactor = _reactor,
            IsZeroSized = _isZeroSized
        }.ScheduleParallel(_componentRemovedQuery, systemDeps);

        _entityCommandBufferSystem.AddJobHandleForProducer(systemDeps);

        Dependency = systemDeps;
    }


    public static class ByteBufferUtility
    {
        public static bool AreEqualStruct<T>(T frist, T second) where T : unmanaged
        {
            NativeArray<byte> firstArray = ConvertToNativeBytes<T>(frist, Allocator.Temp);
            NativeArray<byte> secondArray = ConvertToNativeBytes<T>(second, Allocator.Temp);

            if (firstArray.Length != secondArray.Length) return false;

            for (int i = 0; i < firstArray.Length; ++i)
            {
                if (firstArray[i] != secondArray[i]) return false;
            }

            return true;

        }

        private static NativeArray<byte> ConvertToNativeBytes<T>(T value, Allocator allocator) where T : unmanaged
        {
            int size = UnsafeUtility.SizeOf<T>();
            NativeArray<byte> ret = new NativeArray<byte>(size, allocator);

            unsafe
            {
                UnsafeUtility.CopyStructureToPtr(ref value, ret.GetUnsafePtr());
            }

            return ret;
        }

    }

}
using Unity.Entities;

using UnityEngine;


[assembly: RegisterGenericComponentType(typeof(ReactiveComponentSystem<EmptyComponent, HealthComponentReactor>.StateComponent))]

[GenerateAuthoringComponent]
public struct EmptyComponent : IComponentData
{
}

public struct HealthComponentReactor : IComponentReactor<EmptyComponent>
{
    public void ComponentAdded(ref EmptyComponent newComponent)
    {
        Debug.Log("Added");
    }

    public void ComponentRemoved(in EmptyComponent oldComponent)
    {
        Debug.Log("Removed");
    }

    public void ComponentValueChanged(ref EmptyComponent newComponent, in EmptyComponent oldComponent)
    {
        Debug.LogError("That should never be called !");
    }
}
public class HealtReactiveSystem : ReactiveComponentSystem<EmptyComponent, HealthComponentReactor>
{
    protected override HealthComponentReactor CreateComponentRactor()
    {
        return new HealthComponentReactor();
    }
}

EDIT : Consider this code under MIT license.

8 Likes