Entities & Components
Entities
Section titled “Entities”An entity is a named container for components with O(1) type-based lookups. Create entities through a scene:
const player = scene.spawn("player");const bullet = scene.spawn("bullet");Adding and Querying Components
Section titled “Adding and Querying Components”import { Transform, Vec2 } from "@yagejs/core";import { SpriteComponent, texture } from "@yagejs/renderer";
const HeroTex = texture("assets/hero.png");
// Add componentsplayer.add(new Transform({ position: new Vec2(100, 200) }));player.add(new SpriteComponent({ texture: HeroTex }));
// Get a component (throws if missing)const transform = player.get(Transform);
// Get a component (returns undefined if missing)const sprite = player.tryGet(SpriteComponent);
// Check existenceif (player.has(SpriteComponent)) { // ...}
// Remove a componentplayer.remove(SpriteComponent);entity.get(Type) throws an error when the component is not found — use it
when the component is required. entity.tryGet(Type) returns undefined
instead, which is useful for optional lookups.
Tags are lightweight labels with no data attached:
player.tags.add("friendly");player.tags.add("controllable");
if (player.tags.has("friendly")) { // skip damage}Tags are useful for fast filtering without creating empty marker components.
Transform inheritance
Section titled “Transform inheritance”A child entity’s Transform composes against its parent’s: position is
rotated and scaled by the parent before being added, rotation sums, and
scale multiplies through the chain. DisplaySystem reads each
entity’s worldScale every Render phase, so changing a parent’s scale
reaches every descendant sprite without any per-child bookkeeping.
The canonical use is a facing-flip on a multi-layer character. Set the
parent’s scale to (-1, 1) and every child sprite — body, head, outfit,
weapon — mirrors together via worldScale:
import { Entity, Transform, Vec2 } from "@yagejs/core";import { SpriteComponent } from "@yagejs/renderer";
class Character extends Entity { setup() { this.add(new Transform()); // parent — drives facing
const body = this.spawnChild("body"); body.add(new Transform()); body.add(new SpriteComponent({ texture: "body.png" }));
const head = this.spawnChild("head"); head.add(new Transform({ position: new Vec2(0, -20) })); head.add(new SpriteComponent({ texture: "head.png" })); }
faceLeft(): void { this.get(Transform).setScale(-1, 1); // mirrors body + head together } faceRight(): void { this.get(Transform).setScale(1, 1); }}Negative scale composes through nested entities too — a child at
(-1, 1) under a parent already at (-1, 1) ends up at
worldScale = (1, 1) (the mirrors cancel out). Positive non-unit scale
behaves the same way: a parent at 2x zooms its whole subtree.
Deferred Destruction
Section titled “Deferred Destruction”Destroying an entity does not happen immediately. Instead the entity is marked for removal and actually cleaned up during the EndOfFrame phase:
scene.destroyEntity(bullet);
// bullet still exists this frame — components can run final logic// actual removal happens at EndOfFrameThis prevents iterator invalidation and lets other systems react to the destruction during the same frame.
Entities also die when their scene leaves the stack (pop, replace, or
engine shutdown). Scene teardown applies the same contract as the
end-of-frame flush: every entity is marked destroyed (isDestroyed
becomes true before any component onDestroy runs), components are torn
down, and the engine bus emits entity:destroyed once per entity. After
destruction — on either path — entity.scene throws and entity.tryScene
returns null; use tryScene when a cached reference may outlive its
entity.
Turning an Entity Off
Section titled “Turning an Entity Off”Not every entity that leaves play has to be destroyed. setActive(false) puts
an entity to sleep and keeps everything it owns allocated — its components, its
physics body, its display object:
bullet.setActive(false); // hidden, body off, updates skipped
// later, for the next shotbullet.get(Transform).setPosition(muzzleX, muzzleY);bullet.setActive(true);An entity carrying a RigidBodyComponent has to be moved through the body
instead — rb.setPosition(x, y). Physics owns the transform of a dynamic body
and overwrites a direct Transform write on the next frame, so a recycled
bullet repositioned that way would reappear where it went to sleep.
A dormant entity is hidden, its physics body is out of the simulation, its
components stop updating, and it disappears from every query and from
scene.findEntity, scene.findEntitiesByTag, and scene.findEntities. It
stays in scene.getEntities(), so saving and teardown still see it.
Two properties read the state. activeSelf is the entity’s own switch, the
one setActive writes. isActive is activeSelf combined with every
ancestor’s, so deactivating a parent puts the whole subtree to sleep:
const enemy = scene.spawn("enemy");const healthBar = enemy.spawnChild("hp", HealthBar);
enemy.setActive(false);healthBar.activeSelf; // true — its own switch is untouchedhealthBar.isActive; // false — its parent is asleepBecause each descendant keeps its own switch, a child you deactivated individually stays asleep when the parent wakes up.
While an entity sleeps its components stop updating and its ProcessComponent
stops being ticked, so tweens and coroutines pause and pick up where they left
off on reactivation.
Reactivation restores the engine’s state, not your game state. The entity’s
timeScale, its animation position, its process progress, and its event
listeners all survive. Reset whatever the new life needs yourself, and register
listeners in setup() so they are not re-added on every reuse.
For a whole group of short-lived entities, EntityPool keeps the bookkeeping —
which ones are out, which are ready, how many exist — and calls a per-reuse
reset hook on each one. See Entity Pooling.
Components
Section titled “Components”Components hold state and game logic. Extend the Component base class:
import { Component } from "@yagejs/core";
class Health extends Component { current = 100; max = 100;
update(dt: number) { if (this.current <= 0) { scene.destroyEntity(this.entity); } }}Lifecycle Hooks
Section titled “Lifecycle Hooks”Components have several lifecycle hooks:
class PlayerController extends Component { onAdd() { // Called when the component is added to an entity. // The entity and scene are available here. }
onEnable() { // Called when the component starts running: `enabled` is true and the // entity is active. Fires right after onAdd() on an active entity. // Bring live resources online here. }
onDisable() { // Called when the component stops running — `enabled` went false, the // entity was deactivated, or the component is being torn down. // Put live resources to sleep here. }
onRemove() { // Called when the component is removed from the entity. }
onDestroy() { // Called when the owning entity is destroyed. // Use for final cleanup (unsubscribe events, release resources). }
update(dt: number) { // Called every frame during the Update phase. // dt is the variable delta time in seconds. }
fixedUpdate(dt: number) { // Called during the FixedUpdate phase at a deterministic timestep. // Use for physics and gameplay that must be framerate-independent. }}onEnable and onDisable track effective enabled-ness — component.enabled
combined with entity.isActive. Writing component.enabled fires them just
like deactivating the entity does, and component.effectiveEnabled reads the
current state.
The order is onAdd → query join → onEnable when a component is added, and
onDisable → onRemove / onDestroy when it is torn down. A component added
to a dormant entity gets onAdd but waits for onEnable until the entity is
activated.
onAdd is where a component checks that what it needs is there: a service, a
sibling component, a render layer. Throwing is how it reports a failed check.
class Follower extends Component { private target!: Transform;
onAdd() { const player = this.scene.findByKey("player"); if (!player) throw new Error('Follower needs an entity keyed "player".'); this.target = player.get(Transform); }}The throw is attributed to the component and recorded in
Inspector.getErrors().callbackErrors, the same treatment onEnable and
update get, then rethrown so it reaches whoever called entity.add(). When
the add happens in setup(), that caller is scene.spawn, which destroys the
half-built entity and its children before rethrowing.
onEnable sees whatever state the component held while it was dormant, so use
it for live resources — a looping sound, a physics body, a display object —
rather than for resetting game state.
The engine packages already implement the pair. Rigid bodies and colliders are
switched out of the Rapier simulation without freeing them, and the body’s
velocity, queued forces, and torques are cleared so it cannot resume a motion
from a previous life. Sprites, text, UI surfaces, particle emitters, and
tilemaps hide their display object. SoundComponent stops playback, and does
not restart on its own when the entity comes back.
One Rapier behavior to know: a collider disabled and re-enabled while it still
overlaps something gets no fresh collision-start event. An entity you reactivate
directly on top of an existing contact will not receive onCollision for it.
Update Order
Section titled “Update Order”Within one entity, components run update() and fixedUpdate() in the order
they were added. When that is not the order you need — a component added late
that must run first, or two components from different packages — set
updatePriority. Lower runs first; equal priorities keep add order; the default
is 0, so a negative value runs before siblings that keep the default and a
positive value runs after them.
class Player extends Entity { setup() { this.add(new Mover()); this.add(new Brain()).updatePriority = -1; // decides before Mover moves }}A component that always belongs after (or before) a sibling can declare the default for every instance with a static:
class BoundsClamp extends Component { static updatePriority = 10; // after the follow that moved the camera
update() { // clamp this.sibling(CameraComponent).position }}An instance’s own updatePriority overrides the class default and can be
written at any time, before or after add(). This orders siblings only:
entities still update in the order they were added to the scene. Save/load
keeps a per-instance value that differs from the class default.
Cached Service Resolution
Section titled “Cached Service Resolution”Use this.use(key) inside a component to resolve a service from the DI
container. The result is cached after the first call:
import { InputManagerKey } from "@yagejs/input";
class PlayerController extends Component { update(dt: number) { const input = this.use(InputManagerKey);
if (input.isPressed("jump")) { this.sibling(RigidBody).applyImpulse({ x: 0, y: -500 }); } }}Lazy Sibling References
Section titled “Lazy Sibling References”this.sibling(Type) returns a lazy reference to another component on the same
entity. The lookup is deferred until first access and then cached:
class Enemy extends Component { update(dt: number) { const transform = this.sibling(Transform); const health = this.sibling(Health);
if (health.current < health.max * 0.5) { // flee logic using transform.position } }}Error Boundary
Section titled “Error Boundary”A throw from update(), fixedUpdate(), or a lifecycle hook is attributed to
the component: the engine records it in Inspector.getErrors().callbackErrors
with the component and entity name, logs it, and rethrows it. Nothing is
disabled or muted. The throw escapes the frame, so the game loop stops and the
error reaches the host — your own try/catch, window.onerror, or the
browser console — with the culprit already named. Tests and tooling read the
recorded errors through the Inspector.