Entities and components
An Entity is a game object: something that has components, can sit in a hierarchy, and has a lifecycle. It has no position, no renderer node and no required base class for its components. You decide what an entity is by what you attach to it.
Use entities on their own for a small game or a hierarchy of objects (a room with furniture, a ship with turrets). Put them in a World when you want queries and systems over many of them.
Components and keys
ts
import { ComponentKey, Entity } from "gamekit";
class Health {
current: int = 100;
}
const HEALTH: ComponentKey<Health> = new ComponentKey<Health>("health");
const hero: Entity = new Entity();
hero.addComponent(HEALTH, new Health());
const health: Health | null = hero.getComponent(HEALTH);
if (health != null) {
health.current -= 25;
console.log("hp " + health.current);
}| Method | What it does |
|---|---|
addComponent(key, value) | Attaches a component. Throws if the key is already present. |
replaceComponent(key, value) | Swaps in a new value and returns the old one, or null. |
removeComponent(key) | Removes and returns the value, or null. |
getComponent(key) | Returns the value or null. Never throws for a missing key. |
getAncestorComponent(key) | The nearest ancestor's value for the key, not counting this entity. |
getDescendantComponents(key) | Every descendant's value for the key, parent before child. |
Components are visible the moment they are added. Adding, replacing or removing a component from inside one of the lifecycle hooks below throws.
The lifecycle
An entity is in one of four states (EntityState): detached, attached (it has a parent), active, or disposed. A parentless entity becomes an active root when you call activate(). A child becomes active when it is attached to an active parent.
Components may define any of these optional hooks, and an Entity subclass may override them too:
| Hook | Called when | Order |
|---|---|---|
onAttach(owner) | The entity joins a hierarchy. | Parent before child. |
onActivate(owner) | The entity becomes active. | Parent before child. |
onDeactivate(owner) | The entity stops being active. | Child before parent. |
onDetach(owner) | The entity leaves its parent. | Child before parent. |
onDispose(owner) | The entity is disposed. | Child before parent. |
The entity's own hook runs first, then its components in the order they were added (reverse order for deactivate and detach).
| Call | What happens |
|---|---|
attach(parent) | onAttach on the entity and any detached descendants; if the parent is active, onActivate too. |
activate() | onActivate, parent before child. On a parentless entity this makes it an active root. |
deactivate() | onDeactivate, child before parent. Only valid on an active root. |
detach() | Deactivates if active, then onDetach, then unlinks. The subtree stays intact. |
reparent(parent) | Deactivate, detach, attach to the new parent, activate if that parent is active. |
dispose() | Deactivate, detach, then onDispose for the whole subtree. Disposing twice does nothing. |
Contexts and deferred teardown
This example uses a lifecycle hook, a context and a teardown queue:
ts
import { ComponentKey, ContextKey, Entity, EntityState, EntityTeardownQueue } from "gamekit";
class Lamp {
onAttach(owner: Entity): void { console.log("lamp attached"); }
onActivate(owner: Entity): void { console.log("lamp on"); }
onDeactivate(owner: Entity): void { console.log("lamp off"); }
onDispose(owner: Entity): void { console.log("lamp disposed"); }
}
class Theme {
name: string;
constructor(name: string) { this.name = name; }
}
const LAMP: ComponentKey<Lamp> = new ComponentKey<Lamp>("lamp");
const THEME: ContextKey<Theme> = new ContextKey<Theme>("theme");
const room: Entity = new Entity();
room.setContext(THEME, new Theme("night"));
room.activate(); // a parentless entity becomes an active root
const desk: Entity = new Entity();
desk.addComponent(LAMP, new Lamp());
desk.attach(room); // prints "lamp attached", then "lamp on"
const theme: Theme | null = desk.getContext(THEME);
if (theme != null) {
console.log("desk sees theme " + theme.name); // resolved from the nearest owner outward
}
const queue: EntityTeardownQueue = new EntityTeardownQueue();
desk.disposeDeferred(queue); // still live until the queue flushes
console.log("desk state: " + desk.state);
queue.flush(); // prints "lamp off", "lamp disposed"
console.log("disposed: " + (desk.state == EntityState.DISPOSED));text
lamp attached
lamp on
desk sees theme night
desk state: Active
lamp off
lamp disposed
disposed: true- A context (
ContextKey<T>) is a value an entity provides to its descendants.getContextlooks from the entity outward to the nearest owner that set it. Setting or removing a context callsonContextChanged(key)on that entity and its descendants. - An
EntityTeardownQueuedelays disposal to a moment you choose.disposeDeferred(queue)leaves the entity live untilqueue.flush(), which runs the normaldispose()sequence. Use it when an entity must not vanish in the middle of a loop that is still using it.
Each entity has its own events and effects
entity.events is an EventScope that closes when the entity is disposed. entity.effects and addEffect hold effects whose lifecycle follows the entity.
Identity
There are three different identities, and gamekit keeps them apart:
- Definition identity, such as
"soldier": the template an entity was made from. See Definitions and prefabs. - Runtime identity, an
EntityIdwith an index and a generation, issued by aWorldwhile it owns the entity. A disposed entity's id stops resolving, and a reused index gets a new generation. - Save identity: a durable id your save code assigns. See Saves.
Reference: Entity, EntityState, ComponentKey, ContextKey, EntityTeardownQueue.