Skip to content

Gestalt Entity System Quick Start

Nicholas Bates edited this page Dec 17, 2019 · 20 revisions

Entity Basics

The core elements of an entity system are:

Entities are identified objects. They have no behaviour or data of their own beyond their identifier, but are composed of Components.

Components are data objects that define the state of an entity. Each component implies a behaviour - a Physics component would imply the entity is affected by physics, a Sprite component would imply it is something that can be rendered in 2D, a Health component would imply the entity can be damaged. Each component then contains configuration and state about that behaviour - a health component may define the maximum health the entity can have (configuration) and its current health (state).

Systems process entities and components in order to produce desired behaviour. There's no strict standard for what a system looks like, although a typical system may process all entities with a particular type of component or set of components every frame. For example, a health regeneration system may iterate all entities with a Health component each frame, restoring a small amount of missing health based on a regeneration rate on that component.

Events are signals that can be sent against an entity. Systems can register to react to events based on the types of components the receiving entity has. For example, a Damage event could be sent against an entity when a situation would cause it to be damaged - such as landing at high speed or being shot. A system could subscribe to this event for entities with a Health component in order to apply that damage to the entity. Another system could subscribe to the event to make a damage sound based on a EventAudio component.

Prefabs are recipes for creating entities. A prefab may be for a single entity, or for a collection of related entities. For example, a vehicle might be comprised of a main entity for the vehicle, an entity for each wheel and an entity for an attached turret.

Component types

Components types are defined as classes implementing the Component interface.

public final class HealthComponent implements Component<HealthComponent> {
  private int maxHealth = 100;
  private int health = 100;

  public HealthComponent () {
  }

  public HealthComponent(HealthComponent other) {
    copy(other);
  }

  public int getMaxHealth() {
    return maxHealth;
  }

  public void setMaxHealth(int value) {
    this.maxHealth = value;
  }

  public int getHealth() {
    return health;
  }

  public void setHealth(int value) {
    this.health = value;
  }

  public void copy(BasicComponent other) {
    this.maxHealth = other.maxHealth;
    this.health = other.health;
  }
}

Component classes must implement a copy method. They may optionally implement a copy constructor in addition to an empty constructor. The copy method is very important - gestalt entity system copies components into its entity stores and then copies them back out when requested, so any value that isn't copied will be lost. Copies should be deep, so that components do not end up sharing references to mutable objects.

Some general rules when implementing components:

  • Component should generally not inherit other components. For instance, you should not have a SpherePhysics component inheriting a Physics component. Rather, you would have a SphereCollider component and a Physics component, and behaviour would be driven by an entity having both. Remember entity systems are a compositional approach, not inheritance driven.
  • Components should not reference other components. They may reference entities (via EntityRef) and component types (via Class<? extends Component>).

Setting up the entity system

To set up the entity system, first you will want to create a component store for each type of component the entity system should support. There are two main choices for component store:

  • ArrayComponentStore is a high performing component store, but uses memory based on the total number of entities, regardless of whether they have a component or not. It should be used at least for all component types that are frequently present on entities, and potentially for other component types if memory is not an issue.
  • SparseComponentStore has potentially lower performance, but a much smaller memory footprint in situations where a component has less frequent use. It should be be used for component types which are less commonly used.

Additionally both types of component store can be wrapped in a ConcurrentComponentStore - this provides thread safety for read and write operations across threads. Note that this does not protect against lost update situations where a thread reads a component and then later writes it without knowing it has been changed by another thread in the meantime - that would require some sort of locking or transaction system which is not provided by gestalt-entity-system.

If using gestalt-module, it is possible to discover all component subtypes from the ModuleEnvironment.

ComponentManager componentManager = new ComponentManager();

List<ComponentStore<?>> stores = Lists.newArrayList();
for (Class<? extends Component> componentType : environment.getSubtypesOf(Component.class)) {
  stores.add(new ConcurrentComponentStore(new ArrayComponentStore(componentManager.getType(componentType))));
}

EntityManager entityManager = new CoreEntityManager(stores);

Working with entities

An entity can be created with the entity manager:

EntityRef entity = entityManager.createEntity();
EntityRef entityWithComponents = entityManager.createEntity(component1, component2, ...);

Entities are generally referenced by an EntityRef. This provides functionality to retrieve, set or remove components:

entity.setComponent(new LocationComponent());
HealthComponent health = entity.getComponent(HealthComponent.class).orElse(null);
entity.removeComponent(HealthComponent.class);

Components are copied in or out of entities - changes to components never affect an entity until they EntityRef::setComponent is used to update it. For maximum performance when working with many entities it is best to reuse components:

LocationComponent location = new LocationComponent();
for (EntityRef entity : entities) {
  entity.getComponent(location);
  // location is now a copy of entity's location
  location.setY(location.getY() + 10f);
  entity.setComponent(location);
  // entity's location is now updated
}

Entities can be destroyed using EntityRef::delete. When this is called:

  • All components are removed from the entity.
  • The EntityRef is marked as deleted.
  • The entity's id is freed up for reuse.

It is important to reference entities using their EntityRef rather than id, as this allows the reference to be invalidated when the entity is destroyed - otherwise there will be issues when the id is reused.

The existence of an entity can be checked for using EntityRef::exists

if (entity.exists()) {
  update(entity);
}

Otherwise a destroyed entity will act as an entity with no components and ignore attempts to alter it.

NullEntityRef

NullEntityRef is a special EntityRef that should be used for unset entities rather than null - it behaves much like a deleted entity. This avoids the need to null check EntityRef variables.

private EntityRef attachedEntity = NullEntityRef.get();

Iterating entities

Iterating over entities with a particular component types or set of component types is a common use case - for example, rendering all entities with a Mesh component every frame:

MeshComponent meshComp = new MeshComponent();
LocationComponent locationComp = new LocationComponent();
EntityIterator iterator = entityManager.iterate(meshComp, locationComp);
while (iterator.next()) {
  render(locationComp, meshComp);
}

For maximum performance when iterating entities and updating components, you should avoid repeatedly dereferencing component stores - this includes using EntityRef's setComponent method. Instead you can obtain the component stores before the loop and use them directly.

ComponentStore<LocationComponent> locationStore = entityManager.getComponentStore(LocationComponent.class)
LocationComponent locationComp = new LocationComponent();
EntityIterator iterator = entityManager.iterate(locationComp);
while (iterator.next()) {
  locationComp.setY(locationComp.getY() + 10);
  locationStore.set(iterator.getEntity().getId(), locationComp);
}

Sending events

Defining events

An event is a signal that can be sent against an entity, and handled based on the components that entity has. An event is created by implementing the Event interface:

public class DamageEvent implements Event {
  private int amount;
  private DamageType damageType;

  public DamageEvent(int amount, DamageType type) {
    this.amount= amount;
    this.damageType = type;
  } 

  public int getAmount() {
    return amount;
  }

  public DamageType getDamageType() {
    return damageType;
  }
}

Defining Event Handlers

Defining Event Receivers

Another way to receive events is using the @ReceiveEvent annotation on methods of a system or other class. These objects can then be processed to generate event handlers.

public class HealthSystem {

  // Called when a DamageEvent is sent to an entity with a HealthComponent
  @ReceiveEvent(components = HealthComponent.class)
  public EventResult onDamage(DamageEvent event, EntityRef entity) {
    entity.getComponent(HealthComponent.class).ifPresent(health -> {
      health.setAmount(health.getAmount() - event.getAmount());
      entity.setComponent(health);
    });
    return EventResult.CONTINUE;
  }

  // Also called when a DamageEvent is sent to an entity with a HealthComponent, but provides the health component
  @ReceiveEvent
  public EventResult onDamage(DamageEvent event, EntityRef entity, HealthComponent healthComp) {
    healthComp.setAmount(health.getAmount() - damage.getAmount());
    entity.setComponent(health);
    return EventResult.CONTINUE;
  }
}

The expected signature for an event receiving method is EventResult func(Event, EntityRef, Component...). The method will only be called if all required components are present on the entity, both from the method signature and from the annotation.

In some cases order in which events are processed by multiple methods can matter. In these cases the @Before and @After annotation can be used:

public class ShieldSystem {

  @Before(HealthSystem.class)
  @ReceiveEvent
  public EventResult blockDamage(DamageEvent event, EntityRef entity, ShieldComponent shield) {
    event.setAmount(Math.max(0, event.getAmount() - shield.getDamageBlocked()));
    return EventResult.CONTINUE;
  }
}

EventResult can be used to stop the event continuing to further system by returning EventResult.COMPLETE or EventResult.CANCEL. The difference between these is whether the event is considered to have completed successfully - this doesn't currently matter, but could in future event system variants.

Setting up an EventSystem

Lifecycle events

Prefabs

Concurrency

Performance