Создавать ссылки на объекты и хранить эти ссылки в кэше
Я хотел бы создать цепочку для каждой системы в моей Entity-Component-System. В настоящее время каждая система будет проходить через все объекты и проверять наличие необходимых компонентов.
internal class MySystem : ISystem
{
public void Run()
{
for (int i = 0; i < EntityManager.activeEntities.Count; i++)
{
Guid entityId = EntityManager.activeEntities[i];
if (EntityManager.GetComponentPool<Position>().TryGetValue(entityId, out Position positionComponent)) // Get the position component
{
// Update the position of the entity
}
}
}
}
ISystem
просто требует реализовать Run
метод. Я думаю, что этот подход может стать очень медленным, если каждая система должна проверять правильные компоненты.
Я сохраняю все компоненты в пуле типа компонента, и эти пулы сохраняются в коллекции.
private Dictionary<Type, object> componentPools = new Dictionary<Type, object>();
где object
из Dictionary<Type, object>
всегда Dictionary<Guid, TComponent>()
,
При запуске системы лучше передать только набор необходимых компонентов.
Это методы из моего класса EntityManager, которые влияют на кеш каждой системы.
public Guid CreateEntity()
{
// Add and return entityID
}
public void DestroyEntity(Guid entityId)
{
// Remove entity by ID
// Remove all components from all pools by refering to the entityID
}
public void AddComponentToEntity<TComponent>(Guid entityId, IComponent component) where TComponent : IComponent
{
// Add the component to the component pool by refering to the entityID
}
public void RemoveComponentFromEntity<TComponent>(Guid entityId) where TComponent : IComponent
{
// Remove the component from the component pool by refering to the entityID
}
public void AddComponentPool<TComponent>() where TComponent : IComponent
{
// Add a new component pool by its type
}
public void RemoveComponentPool<TComponent>() where TComponent : IComponent
{
// Remove a component pool by its type
}
Как я могу создать системы, которые ссылаются только на обязательные компоненты и обновить их кеш при вызове одного из этих методов, показанных выше?
Я попытался создать пример псевдокода, чтобы показать, что я имею в виду
internal class Movement : ISystem
{
// Just add entities with a Position and a MovementSpeed component
List<Guid> cacheEntities = new List<Guid>();
public void Run()
{
for (int i = 0; i < cacheEntities.Count; i++)
{
Guid entityId = cacheEntities[i];
Position positionComponent = EntityManager.GetComponentPool<Position>()[entityId];
MovementSpeed movementSpeedComponent = EntityManager.GetComponentPool<MovementSpeed>()[entityId];
// Move
}
}
}
и, возможно, можно создавать коллекции, которые не требуют entityId
поэтому они хранят только ссылку на компонент, который должен быть обновлен.
1 ответ
Система компонентов-сущностей запрашивает конкретный дизайн.
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
поэтому они хранят только ссылку на компонент, который должен быть обновлен.
Как упоминалось в ссылочной вики, это на самом деле не рекомендуется.
Обычной практикой является использование уникального идентификатора для каждого объекта. Это не является обязательным требованием, но имеет несколько преимуществ:
- Сущность может быть передана с использованием идентификатора вместо указателя. Это более надежно, поскольку позволяет уничтожить сущность, не оставляя висящих указателей.
- Это помогает для сохранения состояния извне. Когда состояние загружается снова, нет необходимости восстанавливать указатели.
- Данные могут быть перемешаны в памяти по мере необходимости.
- Идентификаторы объекта могут использоваться при связи по сети для однозначной идентификации объекта.