Ix Dev 5 - Entities
Entities are one of two in-world object types (the other being Tiles; actually tightly-packed bitmaps), representing a given archetype in a convenient and strongly-typed object structure. While entities are state-only and have composable data, they don't use a typical components array (or lookup structures). Instead, the entity system makes heavy use of TypeScript's type inference, narrowing, and guard clauses to enable more convenient top-level properties, while still getting strongly-typed objects from queries and effects.
Defining Entities - Types
Here's a snippet of my base entity and a few other Entity types.
Loading syntax highlighter...
I try to keep the core BaseEntity type slim - the Player entity, for instance, needs very few properties.
Shaping Entities - Templates
Before an entity can be created, there must be an EntityTemplate registered. This is a base data needed to construct an entity of a given type - sometimes 1:1 with an entity archetype, but sometimes just a variation. For instance, the entity templates projectile and projectile-orbiting would share similar base properties, but have a different movement behavior.
Loading syntax highlighter...
Entity Templates are separate types from entity archetypes, using a simplified (often flattened) model to make creating valid entities simpler and safer. Each entity type has an Overrides type, which contains the data that can be modeled in the Entity Template and when creating an entity from a template.
Loading syntax highlighter...
Creating Entities - Overrides
With a base template and overrides in place, a type-safe entity can be created using createEntityFromTemplate:
Loading syntax highlighter...
Adding, removing
Entities are added to the game world after creation using addEntity. This finds a new entity ID, adds it to state, and passes it to the EntityIndex to track.
Entities are similarly removed via removeEntity, which handles index cleanup and cleaning up any lingering references to the entity in other parts of state.
Querying
I very rarely read the underlying entities array directly - not efficient for direct lookup, and I frequently need a smaller subset. I instead use a unified queryEntities() method to handle filtering entities efficiently, reproduced below (slimmed down a bit, but not much).
Loading syntax highlighter...
I use the EntityId option to make use of the indexed lookups in EntityIndex, making my findEntity/getEntity methods much faster.
Usage Patterns
Most exported functions that can accept an entity (or multiple) can accept a helper type type EntityOrId = AnyEntity | {entityId: EntityId} | EntityId;, including findEntity and getEntity; a set of standardized helpers covers extracting either an ID or object from an EntityOrId.
Loading syntax highlighter...
This provides a nice tradeoff of developer ease - I can toss anything that looks like an entity and get the full value out for an o(1) lookup - and performance, letting me use the concrete value when needed. In code where an EntityOrId will be repeatedly used/fetched, I'll typically reassign the value to be the concrete entity at the start of the process, taking care to check for entities destroyed by a partial operation.
Usage - Systems
In system functions, I generally use queryEntities to fetch the relevant entities and then loop through them to update; if the fetch is slow (eg filtering a large non-indexed set every frame), I'll optimize with another index, but the baseline performance of type-specific arrays generally makes it a non-issue.