我想为我的 Entity-Component-System 中的每个系统创建一个chache。当前,每个系统都会遍历所有实体并检查所需的组件。
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
的集合,因此它们仅存储对应更新组件的引用。
答案 0 :(得分:1)
Entity-components-system要求特定的设计。
ECS遵循着重继承原则
处理实质上是原始数据的组件池,有意义的是参考实际的组件类型来处理这些数据-假设您要为每个组件应用特定的行为。
decorator pattern可以很好地与合成配合,通过包装类型来增加行为。它还允许EntityManager
将职责委派给组件池,而不用拥有一个庞大的决策树来处理所有情况。
让我们实现一个例子。
假设有一个绘图功能。这将是一个“系统”,它迭代具有物理和可见组件的所有实体,并绘制它们。可见组件通常可以提供有关实体外观的一些信息(例如人,怪物,火花四处飞动,飞行箭头),并使用物理组件知道在何处绘制它。
实体通常由ID和与其相连的组件列表组成。
class Entity
{
public Entity(Guid entityId)
{
EntityId = entityId;
Components = new List<IComponent>();
}
public Guid EntityId { get; }
public List<IComponent> Components { get; }
}
从标记界面开始。
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; }
}
为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*/ }
}
接下来,我们将为以后的工作打下基础。我们的组件池也应该有一个非通用的接口,当我们不能提供组件类型时,我们可以依靠它。
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
上。但是从长远来看,我们当前的设计往往会降低复杂性并提供更大的灵活性。只需为它所属的每个系统包装一个池。如果系统要求非默认行为,则可以创建专门用于该系统的新装饰器-而不干扰其他系统。
编排器(又名调解员,控制员,...)
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);
}
}
所有地方都放在一起。
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
的集合,因此它们仅存储对应更新组件的引用。
如所引用的Wiki中所述,实际上不建议这样做。
通常的做法是为每个实体使用唯一的ID。这不是必需的,但有几个优点:
- 可以使用ID而不是指针来引用实体。这更加健壮,因为它可以破坏实体而不会留下悬空的指针。
- 它有助于在外部保存状态。再次加载状态后,无需重建指针。
- 可以根据需要在内存中随机移动数据。
- 通过网络进行通信时,可以使用实体ID来唯一标识实体。