[英]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.