Search code examples
c#entity-component-system

connect systems with events


Using the Entity-Component-System pattern I want to connect some systems with events. So some systems shouldn't run in a loop, they should just run on demand.

Given the example of a Health system a Death system should only run when a component gets below 1 health.

I thought about having two types of systems. The first type is a periodic system. This runs once per frame, for example a Render or Movement System. The other type is an event based system. As mentioned before a connection between Health and Death.

First I created a basic interface used by both system types.

internal interface ISystem
{
    List<Guid> EntityCache { get; } // Only relevant entities get stored in there

    ComponentRequirements ComponentRequirements { get; } // the required components for this system

    void InitComponentRequirements();

    void InitComponentPools(EntityManager entityManager);

    void UpdateCacheEntities(); // update all entities from the cache

    void UpdateCacheEntity(Guid cacheEntityId); // update a single entity from the cache
}

Further I created the interfaces

internal interface IReactiveSystem : ISystem
{
// event based
}

and

internal interface IPeriodicSystem : ISystem
{
// runs in a loop
}

but I'm not sure if they will be necessary. There is no problem using

foreach (ISystem system in entityManager.Systems)
{
    system.UpdateCacheEntities();
}

but I don't want to run a system if not needed.

There are two types of Events, a ChangeEvent and a ExecuteEvent. The first gets triggered when a value from a component has changed. The second one gets triggered when something should be done with a specific entity.

If you Need or want to you can have a look at the EntityManager

https://pastebin.com/NnfBc0N9

the ComponentRequirements

https://pastebin.com/xt3YGVSv

and the usage of the ECS

https://pastebin.com/Yuze72xf

An example System would be something like this

internal class HealthSystem : IReactiveSystem
{
    public HealthSystem(EntityManager entityManager)
    {
        InitComponentRequirements();
        InitComponentPools(entityManager);
    }

    private Dictionary<Guid, HealthComponent> healthComponentPool;

    public List<Guid> EntityCache { get; } = new List<Guid>();

    public ComponentRequirements ComponentRequirements { get; } = new ComponentRequirements();

    public void InitComponentRequirements()
    {
        ComponentRequirements.AddRequiredType<HealthComponent>();
    }

    public void InitComponentPools(EntityManager entityManager)
    {
        healthComponentPool = entityManager.GetComponentPoolByType<HealthComponent>();
    }

    public void UpdateCacheEntities()
    {
        for (int i = 0; i < EntityCache.Count; i++)
        {
            UpdateCacheEntity(EntityCache[i]);
        }
    }

    public void UpdateCacheEntity(Guid cacheEntityId)
    {
        Health healthComponent = healthComponentPool[cacheEntityId];
        healthComponent.Value += 10; // just some tests
        // update UI 
    }
}

How can I create ChangeEvents and ExecuteEvents for the different systems?


EDIT

Is there a way to add event delegates to the components to run a specific system for this entity on change if a change event is listening or on demand if an execute event is listening?

By mentioning ChangeEvent and ExecuteEvent I just mean event delegates.

Currently I could do something like this

internal class HealthSystem : IReactiveSystem
{
    //… other stuff

    IReactiveSystem deathSystem = entityManager.GetSystem<Death>(); // Get a system by its type

    public void UpdateCacheEntity(Guid cacheEntityId)
    {
        // Change Health component
        // Update UI

        if(currentHealth < 1) // call the death system if the entity will be dead
        {
            deathSystem.UpdateCacheEntity(cacheEntityId);
        }
    }
}

But I was hoping to achieve a better architecture by using event delegates to make systems communicate and share data between each other.


Solution

  • I am not an expert on this design pattern but I read something on it and my advice is: try not to forget the real purpose of this pattern. This time I found the article on Wikipedia really interesting. It is basically saying (at least it is what I understood) that this pattern has been "designed" to avoid creating too many dependencies, losing the decoupling. Here an example I took from the article:

    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 (e.g. human, monster, sparks flying around, flying arrow), and use the physical component to know where to draw it. Another system could be collision detection. It would iterate through all entities that have a physical component, as it would not care how the entity is drawn. This system would then, for instance, detect arrows that collide with monsters, and generate an event when that happens. It should not need to understand what an arrow is, and what it means when another object is hit by an arrow. Yet another component could be health data, and a system that manages health. Health components would be attached to the human and monster entities, but not to arrow entities. The health management system would subscribe to the event generated from collisions and update health accordingly. This system could also now and then iterate through all entities with the health component, and regenerate health.

    I think that you overcomplicated your architecture, losing the advantages that this pattern can give you.

    First of all: why do you need the EntityManager? I quote again:

    The ECS architecture handles dependencies in a very safe and simple way. Since components are simple data buckets, they have no dependencies.

    Instead your components are constructed with the EntityManager dependency injected:

    entityManager.AddSystem(new Movement(entityManager));
    

    The outcome is a relatively complex internal structure to store entities and the associated components.

    After fixing this, the question is: how can you "communicate" with the ISystems? Again, answer is in the article: Observer Pattern. Essentially each component has a set of attached systems, which are notified every time a certain action occurs.