Система Entity-Components-System запрашивает конкретный дизайн.
ECS следует композиции по принципу наследования
Имея дело с пулами компонентов, которые по сути являются необработанными данными, имеет смысл обрабатывать эти данные со ссылкой на фактический тип компонента - учитывая, что вы захотите применить определенные поведения для каждого.
Шаблон декоратора прекрасно сочетается с композицией, добавляя поведение при переносе типов. Это также позволяет EntityManager
делегировать обязанности пулам компонентов вместо одного массивного дерева решений, которое должно обрабатывать все случаи.
Давайте реализуем пример.
Предположим, есть функция рисования. Это будет «Система», которая перебирает все объекты, которые имеют как физический, так и видимый компоненты, и рисует их. Видимый компонент обычно может иметь некоторую информацию о том, как должен выглядеть объект (например, человек, монстр, искры, летающие стрелы) и использовать физический компонент, чтобы знать, где его нарисовать.
- Entity.cs
Объект обычно состоит из идентификатора и списка компонентов, которые к нему прикреплены.
class Entity
{
public Entity(Guid entityId)
{
EntityId = entityId;
Components = new List<IComponent>();
}
public Guid EntityId { get; }
public List<IComponent> Components { get; }
}
- Component.cs
Начиная с интерфейса маркера.
interface IComponent { }
enum Appearance : byte
{
Human,
Monster,
SparksFlyingAround,
FlyingArrow
}
class VisibleComponent : IComponent
{
public Appearance Appearance { get; set; }
}
class PhysicalComponent : IComponent
{
public double X { get; set; }
public double Y { get; set; }
}
- System.cs
Добавление коллекции для SystemEntities
.
interface ISystem
{
ISet<Guid> SystemEntities { get; }
Type[] ComponentTypes { get; }
void Run();
}
class DrawingSystem : ISystem
{
public DrawingSystem(params Type[] componentTypes)
{
ComponentTypes = componentTypes;
SystemEntities = new HashSet<Guid>();
}
public ISet<Guid> SystemEntities { get; }
public Type[] ComponentTypes { get; }
public void Run()
{
foreach (var entity in SystemEntities)
{
Draw(entity);
}
}
private void Draw(Guid entity) { /*Do Magic*/ }
}
- ComponentPool.cs
Далее мы заложим основу для того, что должно произойти. Наши пулы компонентов также должны иметь неуниверсальный интерфейс, к которому мы можем прибегнуть, когда не можем предоставить тип компонента.
interface IComponentPool
{
void RemoveEntity(Guid entityId);
bool ContainsEntity(Guid entityId);
}
interface IComponentPool<T> : IComponentPool
{
void AddEntity(Guid entityId, T component);
}
class ComponentPool<T> : IComponentPool<T>
{
private Dictionary<Guid, T> component = new Dictionary<Guid, T>();
public void AddEntity(Guid entityId, T component)
{
this.component.Add(entityId, component);
}
public void RemoveEntity(Guid entityId)
{
component.Remove(entityId);
}
public bool ContainsEntity(Guid entityId)
{
return component.ContainsKey(entityId);
}
}
Следующий шаг - декоратор бассейна. Шаблон декоратора реализуется посредством предоставления того же интерфейса, что и класс, который он переносит, применяя любое желаемое поведение в процессе. В нашем случае мы хотим проверить, обладают ли добавленные объекты всеми типами компонентов, которые требуются системе. И если они это сделают, добавьте их в коллекцию.
class PoolDecorator<T> : IComponentPool<T>
{
private readonly IComponentPool<T> wrappedPool;
private readonly EntityManager entityManager;
private readonly ISystem system;
public PoolDecorator(IComponentPool<T> componentPool, EntityManager entityManager, ISystem system)
{
this.wrappedPool = componentPool;
this.entityManager = entityManager;
this.system = system;
}
public void AddEntity(Guid entityId, T component)
{
wrappedPool.AddEntity(entityId, component);
if (system.ComponentTypes
.Select(t => entityManager.GetComponentPool(t))
.All(p => p.ContainsEntity(entityId)))
{
system.SystemEntities.Add(entityId);
}
}
public void RemoveEntity(Guid entityId)
{
wrappedPool.RemoveEntity(entityId);
system.SystemEntities.Remove(entityId);
}
public bool ContainsEntity(Guid entityId)
{
return wrappedPool.ContainsEntity(entityId);
}
}
Как уже говорилось, вы можете возложить бремя проверки и управления системными коллекциями на EntityManager
. Но наш текущий дизайн имеет тенденцию уменьшать сложность и обеспечивать большую гибкость в долгосрочной перспективе. Просто оберните пул один раз для каждой системы, к которой он принадлежит. Если система требует поведения не по умолчанию, то вы можете создать новый декоратор, специализированный для этой системы - без вмешательства в другие системы.
- EntityManager.cs
Оркестратор (он же посредник, контроллер, ...)
class EntityManager
{
List<ISystem> systems;
Dictionary<Type, object> componentPools;
public EntityManager()
{
systems = new List<ISystem>();
componentPools = new Dictionary<Type, object>();
ActiveEntities = new HashSet<Guid>();
}
public ISet<Guid> ActiveEntities { get; }
public Guid CreateEntity()
{
Guid entityId;
do entityId = Guid.NewGuid();
while (!ActiveEntities.Add(entityId));
return entityId;
}
public void DestroyEntity(Guid entityId)
{
componentPools.Values.Select(kp => (IComponentPool)kp).ToList().ForEach(c => c.RemoveEntity(entityId));
systems.ForEach(c => c.SystemEntities.Remove(entityId));
ActiveEntities.Remove(entityId);
}
public void AddSystems(params ISystem[] system)
{
systems.AddRange(systems);
}
public IComponentPool GetComponentPool(Type componentType)
{
return (IComponentPool)componentPools[componentType];
}
public IComponentPool<TComponent> GetComponentPool<TComponent>() where TComponent : IComponent
{
return (IComponentPool<TComponent>)componentPools[typeof(TComponent)];
}
public void AddComponentPool<TComponent>(IComponentPool<TComponent> componentPool) where TComponent : IComponent
{
componentPools.Add(typeof(TComponent), componentPool);
}
public void AddComponentToEntity<TComponent>(Guid entityId, TComponent component) where TComponent : IComponent
{
var pool = GetComponentPool<TComponent>();
pool.AddEntity(entityId, component);
}
public void RemoveComponentFromEntity<TComponent>(Guid entityId) where TComponent : IComponent
{
var pool = GetComponentPool<TComponent>();
pool.RemoveEntity(entityId);
}
}
- Program.cs
Где все это объединяется.
class Program
{
static void Main(string[] args)
{
#region Composition Root
var entityManager = new EntityManager();
var drawingComponentTypes =
new Type[] {
typeof(VisibleComponent),
typeof(PhysicalComponent) };
var drawingSystem = new DrawingSystem(drawingComponentTypes);
var visibleComponent =
new PoolDecorator<VisibleComponent>(
new ComponentPool<VisibleComponent>(), entityManager, drawingSystem);
var physicalComponent =
new PoolDecorator<PhysicalComponent>(
new ComponentPool<PhysicalComponent>(), entityManager, drawingSystem);
entityManager.AddSystems(drawingSystem);
entityManager.AddComponentPool(visibleComponent);
entityManager.AddComponentPool(physicalComponent);
#endregion
var entity = new Entity(entityManager.CreateEntity());
entityManager.AddComponentToEntity(
entity.EntityId,
new PhysicalComponent() { X = 0, Y = 0 });
Console.WriteLine($"Added physical component, number of drawn entities: {drawingSystem.SystemEntities.Count}.");
entityManager.AddComponentToEntity(
entity.EntityId,
new VisibleComponent() { Appearance = Appearance.Monster });
Console.WriteLine($"Added visible component, number of drawn entities: {drawingSystem.SystemEntities.Count}.");
entityManager.RemoveComponentFromEntity<VisibleComponent>(entity.EntityId);
Console.WriteLine($"Removed visible component, number of drawn entities: {drawingSystem.SystemEntities.Count}.");
Console.ReadLine();
}
}
и, возможно, можно создавать коллекции, для которых не требуется entityId
, поэтому они хранят только ссылку на компонент, который должен быть обновлен.
Как упоминалось в ссылочной вики, это на самом деле не рекомендуется.
Обычной практикой является использование уникального идентификатора для каждого объекта. Это не является обязательным требованием, но имеет несколько преимуществ:
- На объект можно ссылаться, используя идентификатор вместо указателя. Это более надежно, поскольку позволяет уничтожить объект, не оставляя висящих указателей.
- Помогает для внешнего сохранения состояния. Когда состояние загружается снова, нет необходимости восстанавливать указатели.
- Данные могут быть перемешаны в памяти по мере необходимости.
- Идентификаторы объектов могут использоваться при обмене данными по сети для однозначной идентификации объекта.