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.
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. }
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. }}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”If update() or fixedUpdate() throws an exception, the engine does not
crash. Instead:
- The error is logged.
component.enabledis set tofalse— the component stops receiving updates.- The rest of the game continues running.
This keeps a single broken component from taking down the entire game during development.