Skip to content

Instantly share code, notes, and snippets.

@grofit
Created September 11, 2025 08:05
Show Gist options
  • Select an option

  • Save grofit/b5ba1a66cbdcb8f6496fea76a64ec910 to your computer and use it in GitHub Desktop.

Select an option

Save grofit/b5ba1a66cbdcb8f6496fea76a64ec910 to your computer and use it in GitHub Desktop.
Example of publishing all entity add/remove changes
using System;
using System.Collections.Generic;
using System.Linq;
using EcsR3.Components.Lookups;
using R3;
using SystemsR3.Extensions;
namespace EcsR3.Entities.Routing
{
public class CustomEntityChangeRouter : IEntityChangeRouter
{
private readonly Dictionary<ComponentContract, Subject<EntityChanges>> _onComponentAddedForGroup = new Dictionary<ComponentContract, Subject<EntityChanges>>();
private readonly Dictionary<ComponentContract, Subject<EntityChanges>> _onComponentRemovingForGroup = new Dictionary<ComponentContract, Subject<EntityChanges>>();
private readonly Dictionary<ComponentContract, Subject<EntityChanges>> _onComponentRemovedForGroup = new Dictionary<ComponentContract, Subject<EntityChanges>>();
private Subject<EntityChanges> _onEntityComponentsAdded = new Subject<EntityChanges>();
public Observable<EntityChanges> OnEntityComponentsAdded => _onEntityChanged;
private Subject<EntityChanges> _onEntityComponentsRemoved = new Subject<EntityChanges>();
public Observable<EntityChanges> OnEntityComponentsRemoved => _onEntityComponentsRemoved;
public IComponentTypeLookup ComponentTypeLookup { get; }
public EntityChangeRouter(IComponentTypeLookup componentTypeLookup)
{ ComponentTypeLookup = componentTypeLookup; }
public void Dispose()
{
_onComponentAddedForGroup.ForEachRun(x => x.Value.Dispose());
_onComponentRemovingForGroup.ForEachRun(x => x.Value.Dispose());
_onComponentRemovedForGroup.ForEachRun(x => x.Value.Dispose());
_onEntityChanged.Dispose();
_onEntityComponentsRemoved.Dispose();
}
public Observable<EntityChanges> OnEntityAddedComponents(params int[] componentTypes)
{ return OnEntityComponentEvent(_onComponentAddedForGroup, componentTypes); }
public Observable<EntityChanges> OnEntityRemovingComponents(params int[] componentTypes)
{ return OnEntityComponentEvent(_onComponentRemovingForGroup, componentTypes); }
public Observable<EntityChanges> OnEntityRemovedComponents(params int[] componentTypes)
{ return OnEntityComponentEvent(_onComponentRemovedForGroup, componentTypes); }
public void PublishEntityAddedComponents(Entity entity, int[] componentIds)
{
var entityChanges = new EntityChanges(entity, componentIds);
_onEntityComponentsAdded.OnNext(entityChanges);
PublishEntityComponentEvent(entity, componentIds, _onComponentAddedForGroup);
}
public void PublishEntityRemovingComponents(Entity entity, int[] componentIds)
{ PublishEntityComponentEvent(entity, componentIds, _onComponentRemovingForGroup); }
public void PublishEntityRemovedComponents(Entity[] entities, int[] componentIds)
{
var entityChanges = new EntityChanges(entity, componentIds);
_onEntityComponentsRemoved.OnNext(entityChanges);
PublishEntityComponentEvent(entities, componentIds, _onComponentRemovedForGroup);
}
public void PublishEntityAddedComponents(Entity[] entities, int[] componentIds)
{
foreach(var entity in entities)
{
var entityChanges = new EntityChanges(entity, componentIds);
_onEntityComponentsAdded.OnNext(entityChanges);
}
PublishEntityComponentEvent(entities, componentIds, _onComponentAddedForGroup);
}
public void PublishEntityRemovingComponents(Entity[] entities, int[] componentIds)
{ PublishEntityComponentEvent(entities, componentIds, _onComponentRemovingForGroup); }
public void PublishEntityRemovedComponents(Entity entity, int[] componentIds)
{
foreach(var entity in entities)
{
var entityChanges = new EntityChanges(entity, componentIds);
_onEntityComponentsRemoved.OnNext(entityChanges);
}
PublishEntityComponentEvent(entity, componentIds, _onComponentRemovedForGroup);
}
public Observable<EntityChanges> OnEntityComponentEvent(Dictionary<ComponentContract, Subject<EntityChanges>> source, params int[] componentTypes)
{
var contract = new ComponentContract(componentTypes);
if(source.TryGetValue(contract, out var existingObservable))
{ return existingObservable; }
var newSub = new Subject<EntityChanges>();
source.Add(contract, newSub);
return newSub;
}
public void PublishEntityComponentEvent(Entity entity, int[] componentIds, Dictionary<ComponentContract, Subject<EntityChanges>> source)
{
var buffer = new int[componentIds.Length];
var allContracts = source.Keys.ToArray();
for (var i = 0; i < allContracts.Length; i++)
{
var currentContract = allContracts[i];
var lastUsedIndexInBuffer = currentContract.GetMatchingComponentIdsNoAlloc(componentIds, buffer);
if (lastUsedIndexInBuffer == -1) { continue; }
/*
This is an optimization as we know the EntityRouterComputedEntityGroupTracker subscriber will instantly
use the data as its called synchronously so we can use Memory<T> and it can be re-used.
There is a worry that anyone could subscribe elsewhere and want to use the data later but they would
need to convert it to an array in that scenario, if they didnt they would just have garbage data.
*/
ReadOnlyMemory<int> bufferAsMemory = buffer;
source[currentContract].OnNext(new EntityChanges(entity, bufferAsMemory[..(lastUsedIndexInBuffer + 1)]));
}
}
public void PublishEntityComponentEvent(Entity[] entities, int[] componentIds, Dictionary<ComponentContract, Subject<EntityChanges>> source)
{
var buffer = new int[componentIds.Length];
var allContracts = source.Keys.ToArray();
for (var i = 0; i < allContracts.Length; i++)
{
var currentContract = allContracts[i];
var lastUsedIndexInBuffer = currentContract.GetMatchingComponentIdsNoAlloc(componentIds, buffer);
if(lastUsedIndexInBuffer == -1) { continue; }
/*
This is an optimization as we know the EntityRouterComputedEntityGroupTracker subscriber will instantly
use the data as its called synchronously so we can use Memory<T> and it can be re-used.
There is a worry that anyone could subscribe elsewhere and want to use the data later but they would
need to convert it to an array in that scenario, if they didnt they would just have garbage data.
*/
ReadOnlyMemory<int> bufferAsMemory = buffer;
for (var j = 0; j < entities.Length; j++)
{ source[currentContract].OnNext(new EntityChanges(entities[j], bufferAsMemory[..(lastUsedIndexInBuffer+1)])); }
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment