If you ever wanted to REACT on added or removed components, then this is for you !
With the power of an reactive system, you will be listening to added or removed components in no time ! And the best… its easy to use and saves a lot of boilerplate code.
Features ?
- Listen for added Components via mainthread callbacks
- Listen for removed Components via mainthread callbacks
- Iterate over added Components via component markers and Entities.ForEach
- Iterate over removed Components via component markers and Entities.ForEach
- Uses IStateComponents to copy the component each frame, great for disposals
Whats still missing ?
- Listen for changes via callbacks
Why isnt it working as expected ?
- Make sure the created entities and added components were created by the
BeginInitializationEntityCommandBufferSystem. Entities and their components need to exist since the start of the frame. When they appear somewhere in between, the reactive system is not capable of tracking those changes from the start which might cause problems.
[assembly: RegisterGenericComponentType(typeof(ReactiveSystem<Inventory, InventoryAdded, InventoryRemoved>.State))]
[assembly: RegisterGenericComponentType(typeof(ManagedReactiveSystem<GameObject, GameObjectAdded, GameObjectRemoved>.State))]
namespace Script.Client.ECS.Systems.Reactive {
// Fast, multithreaded for burst compatible components
// This is added to the entity if the desired component was added to mark it
[BurstCompile]
public struct InventoryAdded : IComponentData { }
// This is added to the entity if the desired component was removed
[BurstCompile]
public struct InventoryRemoved: IComponentData { }
// Creating an system which attaches "InventoryAdded" once the Inventory component was added and "InventoryRemoved" once it was removed.
public class InventoryReactiveSystem : ReactiveSystem<Inventory, InventoryAdded, InventoryRemoved> {}
// Also works for managed objects, a bit slower
[BurstCompile]
public struct GameObjectAdded : IComponentData { }
[BurstCompile]
public struct GameObjectRemoved : IComponentData { }
public class GameObjectReactiveSystem : ManagedReactiveSystem<GameObject, GameObjectAdded, GameObjectRemoved> {}
}
And then you are ready to either hook up callbacks ( running on the mainthread ).
Or you can use the component markers to loop over the entities which works great with schedule or scheduleParallel.
myReactiveSystemReference.OnAdded += (ref Entity en, ref Inventory ia) => {
// Execute logic, inventory was just added
};
myReactiveSystemReference.OnRemoved += (ref Entity, in Inventory ia) => {
// Execute logic, inventory was just removed
};
Entities.ForEach((ref Inventory inv, ref InventoryAdded iva) => {
// Runs once, some cool logic to trigger something
}).Schedule();
Entities.ForEach((ref Entity en, ref InventoryRemoved iva) => {
// Runs once, great for triggering logic or cleaning up stuff.
var state = GetComponent<InventoryReactiveSystem.State>(en);
state.component.items.Dispose();
}).Schedule();
And here is the reactive system
If you find any improvements in useability, performance or whatever… Just tell me.
But i hope this helps people.
/// <summary>
/// A delegate being invoked once the <see cref="ReactiveSystem{TComponent,TAdded,TRemoved}"/> added an component
/// </summary>
/// <typeparam name="T"></typeparam>
public delegate void OnAdded<T>(ref Entity entity, ref T addedComponent) where T : struct, IComponentData;
/// <summary>
/// A delegate being invoked once the <see cref="ReactiveSystem{TComponent,TAdded,TRemoved}"/> removed an component
/// </summary>
/// <typeparam name="T"></typeparam>
public delegate void OnRemoved<T>(ref Entity entity, in T removedComponent) where T : struct, IComponentData;
/// <summary>
/// A delegate being invoked once the <see cref="ReactiveSystem{TComponent,TAdded,TRemoved}"/> added an component
/// </summary>
/// <typeparam name="T"></typeparam>
public delegate void OnAddedClass<T>(ref Entity entity, ref T addedComponent) where T : class;
/// <summary>
/// A delegate being invoked once the <see cref="ReactiveSystem{TComponent,TAdded,TRemoved}"/> removed an component
/// </summary>
/// <typeparam name="T"></typeparam>
public delegate void OnRemovedClass<T>(ref Entity entity, in T removedComponent) where T : class;
/// <summary>
/// There no callbacks or listeners for added/removed components on <see cref="Entity" />'s
/// Thats where this system comes in using <see cref="ISystemStateComponentData" /> for simulating those callbacks inside the ecs.
/// <typeparam name="Component">The component we wanna listen to</typeparam>
/// <typeparam name="Added">The component which indicates that our component has been added, gets attached for one frame to the entity</typeparam>
/// <typeparam name="Removed">The component which indicates that our component was removed, gets attached for one frame to the entity</typeparam>
/// </summary>
[UpdateInGroup(typeof(InitializationSystemGroup))]
public abstract class ReactiveSystem<TComponent, TAdded, TRemoved> : JobComponentSystem where TComponent : struct, IComponentData where TAdded : struct, IComponentData where TRemoved : struct,IComponentData {
private EndInitializationEntityCommandBufferSystem atFrameStartBuffer;
public OnAdded<TComponent> OnAdded;
public OnRemoved<TComponent> OnRemoved;
protected EntityQuery newEntities;
protected EntityQuery entitiesWithAdded;
protected EntityQuery entitiesWithStateOnly;
protected EntityQuery toRemoveEntities;
protected EntityQuery copyEntities;
protected override void OnCreate() {
base.OnCreate();
atFrameStartBuffer = World.GetOrCreateSystem<EndInitializationEntityCommandBufferSystem>();
OnAdded += (ref Entity en, ref TComponent component) => { };
OnRemoved += (ref Entity en, in TComponent component) => { };
// Query to get all newly created entities, without being marked as added
newEntities = GetEntityQuery(new EntityQueryDesc {
All = new[] {ComponentType.ReadOnly<TComponent>()},
None = new[] {ComponentType.ReadOnly<TAdded>(), ComponentType.ReadOnly<State>()}
});
// Query of all entities which where added this frame
entitiesWithAdded = GetEntityQuery(new EntityQueryDesc {
All = new[] { ComponentType.ReadOnly<State>(), ComponentType.ReadOnly<TAdded>()},
None = new[] {ComponentType.ReadOnly<TRemoved>()}
});
// Query of all entities which where added this frame
entitiesWithStateOnly = GetEntityQuery(new EntityQueryDesc {
All = new[] { ComponentType.ReadOnly<State>()},
None = new[] {ComponentType.ReadOnly<TComponent>(), ComponentType.ReadOnly<TAdded>(), ComponentType.ReadOnly<TRemoved>()}
});
// Query of all entities which where removed this frame
toRemoveEntities = GetEntityQuery(new EntityQueryDesc {
All = new[] {ComponentType.ReadOnly<State>(), ComponentType.ReadOnly<TRemoved>()},
None = new[] {ComponentType.ReadOnly<TComponent>(), ComponentType.ReadOnly<TAdded>()}
});
// Query entities which require a copy of the state each frame
copyEntities = GetEntityQuery(new EntityQueryDesc {
All = new[] {ComponentType.ReadOnly<TComponent>(), ComponentType.ReadWrite<State>()}
});
}
protected override JobHandle OnUpdate(JobHandle inputDeps) {
var ecb = atFrameStartBuffer.CreateCommandBuffer();
var ecbParallel = atFrameStartBuffer.CreateCommandBuffer().AsParallelWriter();
var addedEntityCount = newEntities.CalculateEntityCount();
var removedEntityCound = entitiesWithStateOnly.CalculateEntityCount();
var added = new NativeList<Transmution>(addedEntityCount, Allocator.TempJob);
var removed = new NativeList<Transmution>(removedEntityCound, Allocator.TempJob);
// Add the added component job
var addedJob = new AddedJob{
ecb = ecbParallel,
entityHandle = GetEntityTypeHandle(),
componentHandle = GetComponentTypeHandle<TComponent>(true),
added = added.AsParallelWriter()
};
addedJob.ScheduleParallel(newEntities, inputDeps).Complete();
// Call the reactor
for (var index = 0; index < added.Length; index++) {
var addedTransmution = added[index];
OnAdded(ref addedTransmution.entity, ref addedTransmution.component);
ecb.SetComponent(addedTransmution.entity, addedTransmution.component);
}
// Add the remove component job
var removeJob = new RemovedJob{
ecb = ecbParallel,
entityHandle = GetEntityTypeHandle(),
componentHandle = GetComponentTypeHandle<State>(true),
reactors = removed.AsParallelWriter()
};
removeJob.ScheduleParallel(entitiesWithStateOnly, inputDeps).Complete();
// Call the reactor to inform about removed
for (var index = 0; index < removed.Length; index++) {
var removedTransmution = removed[index];
OnRemoved(ref removedTransmution.entity, in removedTransmution.component);
}
// Remove the added component
var removeAddedJob = new RemoveAddedJob{
ecb = ecbParallel,
entityHandle = GetEntityTypeHandle(),
};
inputDeps = removeAddedJob.ScheduleParallel(entitiesWithAdded, inputDeps);
// Remove the removed component
var removeRemovedJob = new RemoveRemovedJob{
ecb = ecbParallel,
entityHandle = GetEntityTypeHandle(),
};
inputDeps = removeRemovedJob.ScheduleParallel(toRemoveEntities, inputDeps);
// Create job to copy the TComponent into the state
var copyJob = new CopyJob {
ComponentTypeHandle = GetComponentTypeHandle<TComponent>(true),
StateTypeHandle = GetComponentTypeHandle<State>()
};
inputDeps = copyJob.ScheduleParallel(copyEntities, inputDeps);
// Dispose and add make ecb concurrent
atFrameStartBuffer.AddJobHandleForProducer(inputDeps);
added.Dispose();
removed.Dispose();
return inputDeps;
}
/// <summary>
/// A job which runs asynchron and copies the <see cref="TComponent"/> into the <see cref="State"/> in an fast and efficient, generic way.
/// </summary>
[BurstCompile]
private struct AddedJob : IJobChunk {
public EntityCommandBuffer.ParallelWriter ecb;
[ReadOnly] public EntityTypeHandle entityHandle;
[ReadOnly] public ComponentTypeHandle<TComponent> componentHandle;
public NativeList<Transmution>.ParallelWriter added;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
// Get original component and the state array
var entityArray = chunk.GetNativeArray(entityHandle);
var componentArray = chunk.GetNativeArray(componentHandle);
// Copy the component into the state
for (var i = 0; i < chunk.Count; i++) {
var entity = entityArray[i];
var component = componentArray[i];
var transmution = new Transmution {entity = entity, component = component};
added.AddNoResize(transmution);
ecb.AddComponent(chunkIndex, entity, new TAdded());
ecb.AddComponent(chunkIndex, entity, new State {component = component});
}
}
}
/// <summary>
/// A job which removes the <see cref="TAdded"/> from the entity because its a one frame marker
/// </summary>
[BurstCompile]
private struct RemoveAddedJob : IJobChunk {
public EntityCommandBuffer.ParallelWriter ecb;
[ReadOnly] public EntityTypeHandle entityHandle;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
// Get original component and the state array
var entityArray = chunk.GetNativeArray(entityHandle);
// Remove the added
for (var i = 0; i < chunk.Count; i++) {
var entity = entityArray[i];
ecb.RemoveComponent<TAdded>(chunkIndex, entity);
}
}
}
/// <summary>
/// A job which runs asynchron and copies the <see cref="TComponent"/> into the <see cref="State"/> in an fast and efficient, generic way.
/// </summary>
[BurstCompile]
private struct RemovedJob : IJobChunk {
public EntityCommandBuffer.ParallelWriter ecb;
[ReadOnly] public EntityTypeHandle entityHandle;
[ReadOnly] public ComponentTypeHandle<State> componentHandle;
public NativeList<Transmution>.ParallelWriter reactors;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
// Get original component and the state array
var entityArray = chunk.GetNativeArray(entityHandle);
var componentArray = chunk.GetNativeArray(componentHandle);
// Copy the component into the state
for (var i = 0; i < chunk.Count; i++) {
var entity = entityArray[i];
var state = componentArray[i];
var oldState = state.component;
var transmution = new Transmution {entity = entity, component = oldState};
reactors.AddNoResize(transmution);
ecb.AddComponent(chunkIndex, entity, new TRemoved());
}
}
}
/// <summary>
/// A job which runs asynchron and copies the <see cref="TComponent"/> into the <see cref="State"/> in an fast and efficient, generic way.
/// </summary>
[BurstCompile]
private struct RemoveRemovedJob : IJobChunk {
public EntityCommandBuffer.ParallelWriter ecb;
[ReadOnly] public EntityTypeHandle entityHandle;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
// Get original component and the state array
var entityArray = chunk.GetNativeArray(entityHandle);
// Copy the component into the state
for (var i = 0; i < chunk.Count; i++) {
var entity = entityArray[i];
ecb.RemoveComponent<TRemoved>(chunkIndex, entity);
ecb.RemoveComponent<State>(chunkIndex, entity);
}
}
}
/// <summary>
/// A job which runs asynchron and copies the <see cref="TComponent"/> into the <see cref="State"/> in an fast and efficient, generic way.
/// </summary>
[BurstCompile]
private struct CopyJob : IJobChunk {
[ReadOnly] public ComponentTypeHandle<TComponent> ComponentTypeHandle;
public ComponentTypeHandle<State> StateTypeHandle;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
// Get original component and the state array
var componentArray = chunk.GetNativeArray(ComponentTypeHandle);
var stateArray = chunk.GetNativeArray(StateTypeHandle);
// Copy the component into the state
for (var i = 0; i < chunk.Count; i++) {
var component = componentArray[i];
stateArray[i] = new State {component = component};
}
}
}
/// <summary>
/// The internal state to mark entities
/// </summary>
[BurstCompile]
public struct State : ISystemStateComponentData {
public TComponent component;
}
/// <summary>
/// An struct which represents an transmution of the <see cref="TComponent"/> which was either added or removed.
/// </summary>
public struct Transmution {
public Entity entity;
public TComponent component;
}
}
/// <summary>
/// There no callbacks or listeners for added/removed components on <see cref="Entity" />'s
/// Thats where this system comes in using <see cref="ISystemStateComponentData" /> for simulating those callbacks inside the ecs.
/// <typeparam name="Component">The component we wanna listen to</typeparam>
/// <typeparam name="Added">The component which indicates that our component has been added, gets attached for one frame to the entity</typeparam>
/// <typeparam name="Removed">The component which indicates that our component was removed, gets attached for one frame to the entity</typeparam>
/// </summary>
[UpdateInGroup(typeof(InitializationSystemGroup))]
public abstract class ManagedReactiveSystem<TComponent, TAdded, TRemoved> : JobComponentSystem where TComponent : class where TAdded : struct, IComponentData where TRemoved : struct, IComponentData {
private EndInitializationEntityCommandBufferSystem atFrameStartBuffer;
public OnAddedClass<TComponent> OnAdded;
public OnRemovedClass<TComponent> OnRemoved;
protected EntityQuery newEntities;
protected EntityQuery entitiesWithAdded;
protected EntityQuery entitiesWithStateOnly;
protected EntityQuery toRemoveEntities;
protected EntityQuery copyEntities;
protected override void OnCreate() {
base.OnCreate();
atFrameStartBuffer = World.GetOrCreateSystem<EndInitializationEntityCommandBufferSystem>();
OnAdded += (ref Entity en, ref TComponent component) => { };
OnRemoved += (ref Entity en, in TComponent component) => { };
// Query to get all newly created entities, without being marked as added
newEntities = GetEntityQuery(new EntityQueryDesc {
All = new[] {ComponentType.ReadOnly<TComponent>()},
None = new[] {ComponentType.ReadOnly<TAdded>(), ComponentType.ReadOnly<State>()}
});
// Query of all entities which where added this frame
entitiesWithAdded = GetEntityQuery(new EntityQueryDesc {
All = new[] { ComponentType.ReadOnly<State>(), ComponentType.ReadOnly<TAdded>()},
None = new[] {ComponentType.ReadOnly<TRemoved>()}
});
// Query of all entities which where added this frame
entitiesWithStateOnly = GetEntityQuery(new EntityQueryDesc {
All = new[] { ComponentType.ReadOnly<State>()},
None = new[] {ComponentType.ReadOnly<TComponent>(), ComponentType.ReadOnly<TAdded>(), ComponentType.ReadOnly<TRemoved>()}
});
// Query of all entities which where removed this frame
toRemoveEntities = GetEntityQuery(new EntityQueryDesc {
All = new[] {ComponentType.ReadOnly<State>(), ComponentType.ReadOnly<TRemoved>()},
None = new[] {ComponentType.ReadOnly<TComponent>(), ComponentType.ReadOnly<TAdded>()}
});
// Query entities which require a copy of the state each frame
copyEntities = GetEntityQuery(new EntityQueryDesc {
All = new[] {ComponentType.ReadOnly<TComponent>(), ComponentType.ReadWrite<State>()}
});
}
protected override JobHandle OnUpdate(JobHandle inputDeps) {
var startCommandBuffer = atFrameStartBuffer.CreateCommandBuffer();
var newEntitiesIterator = newEntities.GetArchetypeChunkIterator();
var entitiesWithAddedIterator = entitiesWithAdded.GetArchetypeChunkIterator();
var entitiesWithStateOnlyIterator = entitiesWithStateOnly.GetArchetypeChunkIterator();
var toRemoveEntitiesIterator = toRemoveEntities.GetArchetypeChunkIterator();
var copyEntitiesIterator = copyEntities.GetArchetypeChunkIterator();
// Add the added component job
var addedJob = new AddedJob{
entityManager = EntityManager,
ecb = startCommandBuffer,
entityHandle = GetEntityTypeHandle(),
componentHandle = EntityManager.GetComponentTypeHandle<TComponent>(true),
reactor = OnAdded
};
addedJob.RunWithoutJobs(ref newEntitiesIterator);
// Add the remove component job
var removeJob = new RemovedJob{
entityManager = EntityManager,
ecb = startCommandBuffer,
entityHandle = GetEntityTypeHandle(),
componentHandle = EntityManager.GetComponentTypeHandle<State>(true),
reactor = OnRemoved
};
removeJob.RunWithoutJobs(ref entitiesWithStateOnlyIterator);
// Remove the added component
var removeAddedJob = new RemoveAddedJob{
ecb = startCommandBuffer,
entityHandle = GetEntityTypeHandle(),
};
removeAddedJob.RunWithoutJobs(ref entitiesWithAddedIterator);
// Remove the removed component
var removeRemovedJob = new RemoveRemovedJob{
ecb = startCommandBuffer,
entityHandle = GetEntityTypeHandle(),
};
removeRemovedJob.RunWithoutJobs(ref toRemoveEntitiesIterator);
// Create job to copy the TComponent into the state
var copyJob = new CopyJob {
entityManager = EntityManager,
componentTypeHandle = EntityManager.GetComponentTypeHandle<TComponent>(true),
stateTypeHandle = EntityManager.GetComponentTypeHandle<State>(false)
};
copyJob.RunWithoutJobs(ref copyEntitiesIterator);
return inputDeps;
}
/// <summary>
/// A job which runs asynchron and copies the <see cref="TComponent"/> into the <see cref="State"/> in an fast and efficient, generic way.
/// </summary>
private struct AddedJob : IJobChunk {
public EntityManager entityManager;
public EntityCommandBuffer ecb;
[ReadOnly] public EntityTypeHandle entityHandle;
[ReadOnly] public ComponentTypeHandle<TComponent> componentHandle;
public OnAddedClass<TComponent> reactor;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
// Get original component and the state array
var entityArray = chunk.GetNativeArray(entityHandle);
var componentArray = chunk.GetManagedComponentAccessor(componentHandle, entityManager);
// Copy the component into the state
for (var i = 0; i < chunk.Count; i++) {
var entity = entityArray[i];
var component = componentArray[i];
reactor(ref entity, ref component);
ecb.AddComponent(entity, new TAdded());
ecb.AddComponent(entity, new State {component = component});
}
}
}
/// <summary>
/// A job which removes the <see cref="TAdded"/> from the entity because its a one frame marker
/// </summary>
private struct RemoveAddedJob : IJobChunk {
public EntityCommandBuffer ecb;
[ReadOnly] public EntityTypeHandle entityHandle;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
// Get original component and the state array
var entityArray = chunk.GetNativeArray(entityHandle);
// Remove the added
for (var i = 0; i < chunk.Count; i++) {
var entity = entityArray[i];
ecb.RemoveComponent<TAdded>(entity);
}
}
}
/// <summary>
/// A job which runs asynchron and copies the <see cref="TComponent"/> into the <see cref="State"/> in an fast and efficient, generic way.
/// </summary>
private struct RemovedJob : IJobChunk {
public EntityManager entityManager;
public EntityCommandBuffer ecb;
[ReadOnly] public EntityTypeHandle entityHandle;
[ReadOnly] public ComponentTypeHandle<State> componentHandle;
public OnRemovedClass<TComponent> reactor;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
// Get original component and the state array
var entityArray = chunk.GetNativeArray(entityHandle);
var componentArray = chunk.GetManagedComponentAccessor(componentHandle, entityManager);
// Copy the component into the state
for (var i = 0; i < chunk.Count; i++) {
var entity = entityArray[i];
var state = componentArray[i];
var oldState = state.component;
reactor(ref entity, in oldState);
ecb.AddComponent(entity, new TRemoved());
}
}
}
/// <summary>
/// A job which runs asynchron and copies the <see cref="TComponent"/> into the <see cref="State"/> in an fast and efficient, generic way.
/// </summary>
private struct RemoveRemovedJob : IJobChunk {
public EntityCommandBuffer ecb;
[ReadOnly] public EntityTypeHandle entityHandle;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
// Get original component and the state array
var entityArray = chunk.GetNativeArray(entityHandle);
// Copy the component into the state
for (var i = 0; i < chunk.Count; i++) {
var entity = entityArray[i];
ecb.RemoveComponent<TRemoved>(entity);
ecb.RemoveComponent<State>(entity);
}
}
}
/// <summary>
/// A job which runs asynchron and copies the <see cref="TComponent"/> into the <see cref="State"/> in an fast and efficient, generic way.
/// </summary>
private struct CopyJob : IJobChunk {
public EntityManager entityManager;
[ReadOnly] public ComponentTypeHandle<TComponent> componentTypeHandle;
public ComponentTypeHandle<State> stateTypeHandle;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
// Get original component and the state array
var componentArray = chunk.GetManagedComponentAccessor(componentTypeHandle, entityManager);
var stateArray = chunk.GetManagedComponentAccessor(stateTypeHandle, entityManager);
// Copy the component into the state
for (var i = 0; i < chunk.Count; i++) {
var component = componentArray[i];
stateArray[i].component = component;
}
}
}
/// <summary>
/// The internal state to mark entities
/// </summary>
public class State : ISystemStateComponentData {
public TComponent component;
}
}