Skip to content

Instantly share code, notes, and snippets.

@bddckr
Last active February 16, 2017 14:04
Show Gist options
  • Save bddckr/4a1ffd481257929952f92d03129d12e2 to your computer and use it in GitHub Desktop.
Save bddckr/4a1ffd481257929952f92d03129d12e2 to your computer and use it in GitHub Desktop.
A more detailed collector and reactive system for Entitas 0.37.0(+).
namespace Entitas
{
using System.Collections.Generic;
using System.Text;
using Entitas;
/// An Collector can observe one or more groups and collects
/// changed entities based on the specified groupEvent.
public sealed class DetailedCollector<TEntity> where TEntity : class, IEntity, new()
{
/// Returns all collected updated entities with update info.
/// Call collector.ClearCollectedEntities()
/// once you processed all entities.
public readonly Dictionary<EntityUpdateKey<TEntity>, IComponent> collectedPreviousComponentsByEntityUpdateKey
= new Dictionary<EntityUpdateKey<TEntity>, IComponent>(EntityUpdateKeyEqualityComparer<TEntity>.comparer);
private readonly IGroup<TEntity>[] _groups;
private readonly GroupEvent[] _groupEvents;
private readonly GroupChanged<TEntity> _addEntityCache;
private readonly GroupChanged<TEntity> _removeEntityCache;
private readonly GroupUpdated<TEntity> _updateEntityCache;
private string _toStringCache;
private StringBuilder _toStringBuilder;
/// Creates an Collector and will collect changed entities
/// based on the specified groupEvent.
public DetailedCollector(IGroup<TEntity> group, GroupEvent groupEvent)
: this(new[] { group }, new[] { groupEvent })
{
}
/// Creates an Collector and will collect changed entities
/// based on the specified groupEvents.
public DetailedCollector(IGroup<TEntity>[] groups, GroupEvent[] groupEvents)
{
if (groups.Length != groupEvents.Length)
{
throw new CollectorException(
"Unbalanced count with groups (" + groups.Length +
") and group events (" + groupEvents.Length + ").",
"Group and group events count must be equal."
);
}
_groups = groups;
_groupEvents = groupEvents;
_addEntityCache = addEntity;
_removeEntityCache = removeEntity;
_updateEntityCache = updateEntity;
Activate();
}
~DetailedCollector()
{
Deactivate();
}
/// Activates the Collector and will start collecting
/// changed entities. Collectors are activated by default.
public void Activate()
{
for (var i = 0; i < _groups.Length; i++)
{
var group = _groups[i];
var groupEvent = _groupEvents[i];
if (groupEvent == GroupEvent.Added || groupEvent == GroupEvent.AddedOrRemoved)
{
group.OnEntityAdded -= _addEntityCache;
group.OnEntityAdded += _addEntityCache;
}
if (groupEvent == GroupEvent.Removed || groupEvent == GroupEvent.AddedOrRemoved)
{
group.OnEntityRemoved -= _removeEntityCache;
group.OnEntityRemoved += _removeEntityCache;
}
if (groupEvent == GroupEvent.AddedOrRemoved)
{
group.OnEntityUpdated -= _updateEntityCache;
group.OnEntityUpdated += _updateEntityCache;
}
}
}
/// Deactivates the Collector.
/// This will also clear all collected entities.
/// Collectors are activated by default.
public void Deactivate()
{
foreach (var group in _groups)
{
group.OnEntityAdded -= _addEntityCache;
group.OnEntityRemoved -= _removeEntityCache;
group.OnEntityUpdated -= _updateEntityCache;
}
ClearCollectedEntities();
}
/// Clears all collected entities.
public void ClearCollectedEntities()
{
foreach (var pair in collectedPreviousComponentsByEntityUpdateKey)
{
var entityUpdateKey = pair.Key;
var entity = entityUpdateKey.entity;
var previousComponent = pair.Value;
if (previousComponent != null)
{
entity.GetComponentPool(entityUpdateKey.componentIndex).Push(previousComponent);
}
#if ENTITAS_FAST_AND_UNSAFE
entity.Release(this);
#else
if (entity.owners.Contains(this))
{
entity.Release(this);
}
#endif
}
collectedPreviousComponentsByEntityUpdateKey.Clear();
}
private void addEntity(IGroup<TEntity> group,
TEntity entity,
int index,
IComponent component)
{
updateEntity(group, entity, index, null, component);
}
private void removeEntity(IGroup<TEntity> group,
TEntity entity,
int index,
IComponent component)
{
updateEntity(group, entity, index, component, null);
}
private void updateEntity(IGroup<TEntity> group,
TEntity entity,
int index,
IComponent previousComponent,
IComponent newComponent)
{
var entityUpdateKey = new EntityUpdateKey<TEntity>(group, entity, index);
if (collectedPreviousComponentsByEntityUpdateKey.ContainsKey(entityUpdateKey))
{
return;
}
IComponent clonedComponent;
if (previousComponent == null)
{
clonedComponent = null;
}
else
{
clonedComponent = entity.CreateComponent(index, previousComponent.GetType());
previousComponent.CopyPublicMemberValues(clonedComponent);
}
collectedPreviousComponentsByEntityUpdateKey[entityUpdateKey] = clonedComponent;
#if ENTITAS_FAST_AND_UNSAFE
entity.Retain(this);
#else
if (!entity.owners.Contains(this))
{
entity.Retain(this);
}
#endif
}
public override string ToString()
{
if (_toStringCache == null)
{
if (_toStringBuilder == null)
{
_toStringBuilder = new StringBuilder();
}
_toStringBuilder.Length = 0;
_toStringBuilder.Append("DetailedCollector(");
const string separator = ", ";
var lastSeparator = _groups.Length - 1;
for (var i = 0; i < _groups.Length; i++)
{
_toStringBuilder.Append(_groups[i]);
if (i < lastSeparator)
{
_toStringBuilder.Append(separator);
}
}
_toStringBuilder.Append(")");
_toStringCache = _toStringBuilder.ToString();
}
return _toStringCache;
}
}
public sealed class EntityUpdateKey<TEntity> where TEntity : class, IEntity, new()
{
public readonly IGroup<TEntity> group;
public readonly TEntity entity;
public readonly int componentIndex;
public EntityUpdateKey(IGroup<TEntity> group, TEntity entity, int componentIndex)
{
this.group = group;
this.entity = entity;
this.componentIndex = componentIndex;
}
}
public sealed class EntityUpdateKeyEqualityComparer<TEntity> : IEqualityComparer<EntityUpdateKey<TEntity>>
where TEntity : class, IEntity, new()
{
public static readonly EntityUpdateKeyEqualityComparer<TEntity> comparer
= new EntityUpdateKeyEqualityComparer<TEntity>();
public bool Equals(EntityUpdateKey<TEntity> x, EntityUpdateKey<TEntity> y)
=> EntityEqualityComparer<TEntity>.comparer.Equals(x.entity, y.entity)
&& x.group == y.group
&& x.componentIndex == y.componentIndex;
public int GetHashCode(EntityUpdateKey<TEntity> obj)
=> EntityEqualityComparer<TEntity>.comparer.GetHashCode(obj.entity)
^ (obj.group.GetHashCode() << 2)
^ (obj.componentIndex.GetHashCode() >> 2);
}
}
namespace Entitas
{
using System.Collections.Generic;
using System.Linq;
using Entitas;
/// A DetailedReactiveSystem calls Execute() if there were changes based on
/// the specified Collector and will only pass in changed entities.
/// A common use-case is to react to changes, e.g. a change of the position
/// of an entity to update the gameObject.transform.position
/// of the related gameObject.
public abstract class DetailedReactiveSystem<TEntity> : IExecuteSystem
where TEntity : class, IEntity, new()
{
private readonly DetailedCollector<TEntity> _collector;
private readonly Dictionary<EntityUpdateKey<TEntity>, IComponent> _buffer
= new Dictionary<EntityUpdateKey<TEntity>, IComponent>();
private string _toStringCache;
protected DetailedReactiveSystem(IContext<TEntity> context)
{
_collector = GetTrigger(context);
}
protected DetailedReactiveSystem(DetailedCollector<TEntity> collector)
{
_collector = collector;
}
~DetailedReactiveSystem()
{
Deactivate();
}
/// Specify the collector that will trigger the DetailedReactiveSystem.
protected abstract DetailedCollector<TEntity> GetTrigger(IContext<TEntity> context);
/// This will exclude all entities which don't pass the filter.
protected abstract bool Filter(TEntity entity);
protected abstract void Execute(Dictionary<EntityUpdateKey<TEntity>, IComponent> collectedPreviousComponentsByEntityUpdateKey);
/// Activates the DetailedReactiveSystem and starts observing changes
/// based on the specified Collector.
/// DetailedReactiveSystem are activated by default.
public void Activate()
{
_collector.Activate();
}
/// Deactivates the DetailedReactiveSystem.
/// No changes will be tracked while deactivated.
/// This will also clear the DetailedReactiveSystem.
/// DetailedReactiveSystem are activated by default.
public void Deactivate()
{
_collector.Deactivate();
}
/// Clears all accumulated changes.
public void Clear()
{
_collector.ClearCollectedEntities();
}
/// Will call Execute() with added, removed and changed entities
/// if there are any. Otherwise it will not call Execute().
public void Execute()
{
var collectedPreviousComponentsByEntityUpdateKey = _collector.collectedPreviousComponentsByEntityUpdateKey;
if (collectedPreviousComponentsByEntityUpdateKey.Count == 0)
{
return;
}
foreach (var pair in collectedPreviousComponentsByEntityUpdateKey)
{
var key = pair.Key;
var entity = key.entity;
if (!Filter(entity))
{
continue;
}
#if ENTITAS_FAST_AND_UNSAFE
entity.Retain(this);
#else
if (!entity.owners.Contains(this))
{
entity.Retain(this);
}
#endif
_buffer[key] = pair.Value;
}
_collector.ClearCollectedEntities();
if (_buffer.Count == 0)
{
return;
}
Execute(_buffer);
foreach (var entity in _buffer.Select(pair => pair.Key.entity))
{
#if ENTITAS_FAST_AND_UNSAFE
entity.Release(this);
#else
if (entity.owners.Contains(this))
{
entity.Release(this);
}
#endif
}
_buffer.Clear();
}
public override string ToString()
{
if (_toStringCache == null)
{
_toStringCache = "DetailedReactiveSystem(" + GetType().Name + ")";
}
return _toStringCache;
}
}
}
namespace TestProject
{
using System.Collections.Generic;
using System.Text;
using Entitas;
using EntitasHelpers;
using JetBrains.Annotations;
using UnityEngine;
public sealed class TestSystem : DetailedReactiveSystem<MapEntity>
{
public TestSystem([NotNull] Contexts contexts) : base(contexts.map)
{
}
protected override DetailedCollector<MapEntity> GetTrigger(IContext<MapEntity> context) => new DetailedCollector<MapEntity>(
new[]
{
context.GetGroup(Matcher<MapEntity>.AllOf(MapMatcher.FloorY, MapMatcher.GridPosition))
},
new[]
{
GroupEvent.AddedOrRemoved
}
);
protected override bool Filter(MapEntity entity) => true;
protected override void Execute(Dictionary<EntityUpdateKey<MapEntity>, IComponent> collectedPreviousComponentsByEntityUpdateKey)
{
var stringBuilder = new StringBuilder();
foreach (var pair in collectedPreviousComponentsByEntityUpdateKey)
{
var key = pair.Key;
var entity = key.entity;
var componentIndex = key.componentIndex;
stringBuilder
.Append(entity)
.AppendFormat("({0}): ", key.group)
.Append(pair.Value?.ToString() ?? "null")
.Append(" --> ")
.Append(entity.HasComponent(componentIndex) ? entity.GetComponent(componentIndex).ToString() : "null");
Debug.Log(stringBuilder);
stringBuilder.Length = 0;
}
}
}
}
@bddckr
Copy link
Author

bddckr commented Feb 16, 2017

TestSystem logs things like:

Entity_7(*8)(FloorY(1), GridPosition(0, 0), Visible, Wall(North))(Group(AllOf(FloorY, GridPosition))): null --> GridPosition(0, 0)
Entity_7(*8)(FloorY(5), GridPosition(0, 0), Visible, Wall(North))(Group(AllOf(FloorY, GridPosition))): FloorY(1) --> FloorY(5)
Entity_7(*8)(FloorY(5), GridPosition(2, 0), Visible, Wall(North))(Group(AllOf(FloorY, GridPosition))): GridPosition(0, 0) --> GridPosition(2, 0)

Deleting the FloorY component while changing the GridPosition at the same time:

Entity_7(*4)(GridPosition(10, 12), Visible, Wall(North))(Group(AllOf(FloorY, GridPosition))): FloorY(8) --> null
Entity_7(*4)(GridPosition(10, 12), Visible, Wall(North))(Group(AllOf(FloorY, GridPosition))): GridPosition(1, 12) --> GridPosition(10, 12)

Notice that the removal of FloorY doesn't remove the entity from the list that gets supplied to Execute. To do that one would have to change TestSystem.Filter - the same when using the normal ReactiveSystem Entitas provides.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment