简体   繁体   中英

create references to objects and hold these references in a cache

I would like to create a chache for each system in my Entity-Component-System . Currently each system would loop through all entities and check for required components.

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 just requires to implement a Run method. I think this approach might get really slow if each system has to check for the right components.

I save all the components to a pool of the component type and these pools get stored to a collection.

private Dictionary<Type, object> componentPools = new Dictionary<Type, object>();

where object of Dictionary<Type, object> is always Dictionary<Guid, TComponent>() .

When running a system it would be better passing in a collection of the required components only.

These are the methods from my EntityManager class that would affect the cache of each system

    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
    }

How can I create systems that refer to the required components only and update their cache when calling one of these methods shown above?

I tried to create a pseudo code example to show what I mean

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
        }
    }
}

and maybe it's possible to create collections that require no entityId , so they only store the reference to the component that should get updated.

The Entity-components-system asks for a specific design.

ECS follows the composition over inheritance principle

Dealing with pools of components, which are in essence raw data, it makes sense to handle this data with reference to the actual component type -- given you'll want to apply specific behaviors for each.

The decorator pattern plays nicely with composition, adding behaviors by wrapping types. It also allows EntityManager to delegate responsibilities to the component pools, instead of having one massive decision tree that has to handle all cases.

Let's implement an example.

Suppose there is a drawing function. This would be a "System" that iterates through all entities that have both a physical and a visible component, and draws them. The visible component could typically have some information about how an entity should look (eg human, monster, sparks flying around, flying arrow), and use the physical component to know where to draw it.

  1. Entity.cs

An entity will be typically made up of an ID and a list of components that are attached to it.

class Entity
{
    public Entity(Guid entityId)
    {
        EntityId = entityId;
        Components = new List<IComponent>();
    }

    public Guid EntityId { get; }
    public List<IComponent> Components { get; }
}
  1. Component.cs

Starting with the marker interface.

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; }
}
  1. System.cs

Adding a collection for the 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*/ }
}
  1. ComponentPool.cs

Next, we'll lay the ground work for what's to come. Our component pools should also have a non-generic interface, that we can fall back upon when we can't supply a component type.

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);
    }
}

Next step is the pool decorator. The decorator pattern is implemented by exposing the same interface as the class it wraps, applying any desired behavior in the process. In our case, we want to check if added entities posses all the component types a system requires. And if they do, add them to the collection.

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);
    }
}

As said, you could place the burden of checking, and managing the system collections, on EntityManager . But our current design tends to diminish complexity and provide more flexibility in the long run. Just wrap a pool once for each system it belongs to. If the system requires non default behavior, then you can create a new decorator specialized for that system -- without interfering with other systems.

  1. EntityManager.cs

The orchestrator (aka mediator, controller,...)

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);
    }
}
  1. Program.cs

Where it all comes together.

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();
    }
}

and maybe it's possible to create collections that require no entityId , so they only store the reference to the component that should get updated.

As mentioned in the referenced wiki, that's actually discouraged.

It is a common practice to use a unique ID for each entity. This is not a requirement, but it has several advantages:

  • The entity can be referred using the ID instead of a pointer. This is more robust, as it would allow for the entity to be destroyed without leaving dangling pointers.
  • It helps for saving state externally. When the state is loaded again, there is no need for pointers to be reconstructed.
  • Data can be shuffled around in memory as needed.
  • Entity ids can be used when communicating over a network to uniquely identify the entity.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM