Skip to content

Components

Jeremy Ries edited this page Jan 15, 2021 · 5 revisions

Basics

In pretty much all ECS Frameworks entities are only defined by the sum of their components. Systems will filter for a certain combination of components to execute actions, for example manipulating components or creating new entities.

In Entitas a Component can act as:

  • a data storage (both temporary and session wide)
  • a filter (narrowing down the application of a certain system)
  • a trigger/message (triggering reactive and event systems)
  • an identifier (making it easier to find one specific entity)

Or any combination of the above, Entitas doesn't really define these "roles", but they can be a useful tool for your mental model. Components should be as small as possible only containing the minimum amount of information - then they can be reused for different purposes.

The following examples use the roslyn code generator but the concepts can be replicated manually.

Data storage

[Game]
public class PlayerComponent : IComponent{}

[Game]
public class HealthComponent : IComponent
{
    public int Value;
}

public class PlayerInitializeSystem : IInitializeSystem
{
    private readonly Contexts _contexts;

    public PlayerInitializeSystem(Contexts contexts)
    {
        _contexts = contexts;
    }
    
    public void Initialize()
    {
        var e = _contexts.game.CreateEntity();
        e.isPlayer = true;
        e.AddHealth(10);

        // e.ReplaceHealth(5);
        // e.RemoveHealth();
    }
}

In this example an entity is created at the start of the game and by adding the PlayerComponent
it basically becomes the "Player Entity" for the programmers mental model. The Health component acts as a data storage for the players health.
The [Game] attribute is used to indicate for which context the component api is to be generated.

Note the different syntaxes e.isPlayer and e.addHealth(10).

Components without any public fields always generate the .is api, because an entity can only have the component or not.

For Components that have public fields the .Add .Replace .Remove .has syntax will be generated. Calling Add twice or Remove when there is nothing to remove will result in an error, Replace can always be called safely. .has is equivalent to .is but is readonly.

Filter and Trigger / Message

[Cleanup(CleanupMode.RemoveComponent)]
public sealed class DamageComponent : IComponent
{
    public int Value;
}

public sealed class DamageSystem : ReactiveSystem<GameEntity>
{
    readonly Contexts _contexts;

    public DamageSystem(Contexts contexts) : base(contexts.game)
    {
        _contexts = contexts;
    }

    protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context) =>
        context.CreateCollector(GameMatcher.Damage);

    protected override bool Filter(GameEntity entity) => entity.hasDamage && entity.hasHealth;

    protected override void Execute(List<GameEntity> entities)
    {
        foreach (var e in entities)
        {
            var healthAfterDamage = e.health.Value - e.damage.Value; 
            e.ReplaceHealth(healthAfterDamage);
        }
    }
}

This snippet is an extension of the earlier example. Now the damage component can be added to any entity.

If DamageComponent is added to an entity that also has the HealthComponent,
the DamageSystem is triggered because the Filter is satisfied so the system subtracts the damage from the health.

This System applies to the "Player Entity" we created earlier -> if it's health falls below 0 you could respawn it with another system. The DamageSystem also works with any other Entity which has both Health and Damage Components, so you could create an "Enemy Entity" which also uses Health and Damage Component but gets destroyed if health falls below 0.

Since the DamageComponent has a [Cleanup] Attribute it is removed at the end of the Frame. This can be done manually with .Remove() as well but the attribute highlights the Components Lifecycle at it's definition already.

Identifier

[Game]
public class PlayerIdComponent : IComponent
{ 
    [PrimaryEntityIndex] public int Value;
}

[Game]
public class PlayerFollowerIdComponent : IComponent
{ 
    [EntityIndex] public int Value;
}

public class FollowerSystem : IExecuteSystem
{
    private readonly Contexts _contexts;
    private readonly IGroup<GameEntity> _players;

    public FollowerSystem(Contexts contexts)
    {
        _contexts = contexts;
        _players = contexts.game.GetGroup(GameMatcher.PlayerId);
    }
    
    public void Execute()
    {
        foreach (var player in _players)
        {
            var id = player.playerId.Value;
            var followerEntities = _contexts.game.GetEntitiesWithPlayerFollowerId(id);
            foreach (var follower in followerEntities)
            {
                //move follower towards player
            }
        }
    }
}

Here each "Player Entity" also has a PlayerID. Using the [PrimaryEntityIndex] Attribute we can assure there is only one Entity that has a specific ID.

Each Player can have Followers that know who to follow by using the same ID as the Player does. Using the [EntityIndex] Attribute generates the .GetEntitiesWith... API you can get all Followers of all Players.

This essentially creates a "one to many" relation between Players and Followers

Attributes

Entitas uses attributes to further specify how the component can be used.

  • [Unique] attribute assures there can only be one entity with this component in the context. You can access that entity with _contexts.game.[COMPONENT_NAME]Entity

  • [Event] attribute generates an event system - they are especially useful for updating views
    (note they are not needed to trigger reactive systems). You can choose to make a change to a component only locally available for the entity it is attached to or publicly for the whole application to subscribe to. Events can be triggered on adding and/or removing components (adding is the default). Here is an example to get all combinations: [Game, Event(EventTarget.Any), Event(EventTarget.Any, EventType.Removed), Event(EventTarget.Self), Event(EventTarget.Self, EventType.Removed)] EventSystems

  • [Cleanup] attribute generates a cleanup system - you can choose to either remove the component or delete the whole entity at the end of each Frame.

  • [PrimaryEntityIndex] attribute is placed before a public field to indicate there can be only one Entity with that particular value.
    Access the specific entity with _contexts.game.GetEntityWithPlayerId(id)

  • [EntityIndex] attribute works similiar but there can be multiple entities with the same ID.
    Access those entities with contexts.game.GetEntitiesWithPlayerFollowerId(id)

Clone this wiki locally