Skip to content

Gestalt Entity System Quick Start

Nicholas Bates edited this page Jan 2, 2020 · 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

EventHandlers can be registered to receive events:

public class MyEventHandler<DamageEvent> {

    public EventResult onEvent(DamageEvent event, EntityRef entity) {
      // Do something
      return EventResult.CONTINUE;
    }
}

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 or relevant to the event, 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 systems 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 the EventSystem

EventSystem eventSystem = new EventSystemImpl();
// Register a handler that will be called for entities with a HealthComponent.
eventSystem.registerHandler(DamageEvent.class, reduceHealthHandler, HealthComponent.class /* other required components can be listed*/);
// Register a handler that will be called before the reduceHealthHandler
eventSystem.registerHandler(DamageEvent.class, shieldHandler, /* before handlers */ reduceHealthHandler.getClass(), /* after handlers */ Collections.emptySet(), ShieldComponent.class)

// Register EventReceiver annotated methods
EventReceiverMethodSupport = new EventReceiverMethodSupport();
eventReceiverMethodSupport.register(someEventReceiverObject, eventSystem);

Sending and Processing events

Events can be sent with the send method, with the event and the entity it is being sent against.

eventSystem.send(event, entity);

If the event is annotated with the @Synchronous annotation, then it will be processed straight away. Otherwise it will be processed later - by default when processEvents is called.

eventSystem.processEvents();

It is possible to also send events with a list of triggering events. This has effects on event sending:

  1. The entity will act as if it has those components, even if it doesn't.
  2. Only event handlers that require the triggering components will be called.

This is generally only useful for implementing component lifecycle events.

Lifecycle events

Lifecycle events are special events that are sent when entities are changed. There are three such events:

  • OnAdded when components are added to an entity.
  • OnChanged when components are modified.
  • OnRemoved when components are removed.

Sending of these events make use of the triggering components feature of event system, so if you have an event handler subscribing to the OnAdded event that requires a HealthComponent and ShieldComponent, it will only be called when one or both of those components is added (and the other component is present).

Setting up the lifecycle events has two parts - one is the LifecycleEventManager, which tracks entity changes and sends cached events when requested, and the other is wrapping component stores with the LifecycleAwareComponentStore wrapper.

Then, regularly, call LifecycleEventManager#sendPendingEvents to send any lifecycle events.

LifecycleEventManager accumulates lifecycle events so they can be sent as batches. It will also attempt to drop events that cease to be relevant if there are multiple events involving the same component.

LifecycleEventManager lifecycleEventManager = new LifecycleEventManager();
CoreEntityManager entityManager = new CoreEntityManager();

for (Class<? extends Component> componentType : environment.getSubtypesOf(Component.class)) {
    entityManager.addComponentStore(new ConcurrentComponentStore(new LifecycleAwareComponentStore(lifecycleEventManager, entityManager, new ArrayComponentStore(componentManager.getType(componentType)))));
}

// Regularly, at least once a frame
lifecycleEventManager.sendPendingEvents(eventSystem);

Prefabs

Prefabs are recipes or templates for entities. Prefabs are supported as an asset type which can be registered with gestalt-asset-core:

AssetType<Prefab, PrefabData> prefabAssetType = assetTypeManager.createAssetType(Prefab.class, Prefab::new, "prefabs");
AssetFileDataProducer<PrefabData> prefabDataProducer = assetTypeManager.getAssetFileDataProducer(prefabAssetType);
prefabDataProducer.addAssetFormat(new PrefabJsonFormat.Builder(moduleEnvironment, componentManager, assetManager).create());

Prefabs are json files. A basic prefab can define a single entity (this one defines a single entity with a Sample component with name = "Test Name"):

{
  "entity": {
    "sample": {
      "name": "Test Name"
    }
  }
}

More complex prefabs can define multiple related entities (this one defines two entities, where the root entity has a reference to the second entity):

{
  "entities" : {
    "root": {
      "sample": {
        "name": "Test Name"
      },
      "reference": {
        "reference": "this:second"
      }
    },
    "second": {
      "sample": {
        "name": "Second Entity"
      }
    }
  }
}

Multiple entity prefabs should have a root entity - this is the entity returned when creating the prefab when not asking for all entities. Any entity in the prefab named "root" will be this entity - alternatively the root entity can be explicitly specified:

{
  "root" : "first",
  "entities" : {
    "second": {
      "sample": {
        "name": "Second Entity"
      }
    },
    "first": {
      "sample": {
        "name": "Test Name"
      },
      "reference": {
        "reference": "this:second"
      }
    }
  }
}

EntityRef attributes of components can be specified as part of a prefab. To specify another entity in the same prefab, the format used is "this:entityName" as demonstrated above. It is also possible to specify another prefab with that prefab's resource urn, in which case that prefab will also be instantiated and the root entity populated into the attribute.

Prefabs are instantiated with the EntityManager:

// Note: all entities required by the prefab will be created, but only the root entity will be returned.
EntityRef rootEntity = entityManager.createEntity(prefab);
Map<Name, EntityRef> allEntities = entityManager.createEntities(prefab);

Because prefabs are just a type of asset, they can be used as a variable on a component. This allows for some interesting use cases - for example, a weapon component could have a prefab for the bullet it spawns, allowing it to be used generically for many sorts of weapons (such as a weapon that fires pies).

Concurrency

Much of the entity system is designed to be thread safe, however it does not build in a transaction system so some care should still be used.

Reading components and sending events are safe from any thread.

Writing components and processing events should be limited to a single thread. While writing components is thread safe and atomic, nothing inherently prevents multiple threads reading and making different changes to the same components, resulting in lost updates or missed state. This can be alleviated by adding some sort of entity-level locking mechanism or similar. It is worth noting the provided LifecycleEventManager will break in the presence of multi-thread interactions, so this would need to be addressed as well if lifecycle events are being used.

Performance

For improved performance, an additional library gestalt-es-perf provides improved mechanisms for working with ComponentTypes and EventReceiving methods that can be plugged into ComponentManager and EventReceiverMethodSupport respectively. These make use of functionality available in more recent jdk or android versions, which replace the default reflections based mechanisms.

// Available in Java 7+
ComponentManager componentManager = new ComponentManager(new MethodHandleComponentTypeFactory());
// Available in Java 8+
ComponentManager componentManager = new ComponentManager(new LambdaComponentTypeFactory());

// Available in Java 7+
EventReceiverMethodSupport eventReceiverMethodSupport = new EventReceiverMethodSupport(MethodHandleEventHandle::new);