[英]create references to objects and hold these references in a cache
我想为我的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
,因此它们只存储对应该更新的组件的引用。
实体组件系统要求特定的设计。
ECS遵循组合优于继承原则
处理组件池(本质上是原始数据),参考实际组件类型来处理这些数据是有意义的——假设您希望为每个组件应用特定的行为。
装饰器模式与组合配合得很好,通过包装类型来添加行为。 它还允许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 来唯一标识实体。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.