Worlds, entities and lifecycle

Own a world, spawn and change entities, and dispose them safely.

Lifecycle in five rules

useWorld and GameView make a complete scene; these five rules keep it leak-free and crash-free.

1. Own the world with useWorld(capacity, setup), or create it in an effect after await warmUpEngine(); never during render or at module scope.

2. GameView steps the world right after onUpdate; call world.update(dt) yourself only to step differently, and GameView then skips its step (it watches world.updateCount).

3. Queue per-frame moves on world.transforms in onUpdate; the step applies them, so call flush() only before reading positions back.

4. Dispose the world on unmount (useWorld does) and release your subscriptions with it.

5. Check world.isDisposed instead of keeping a flag: afterwards set, despawn, has, animate, stopAnimation, impulse, clips, timers, queries and forEachCollision are no-ops, while spawn and update throw.

SpinningCube.tsx

React and React Native · shared engine API

A complete scene: useWorld owns the world, onUpdate queues the spin and GameView steps the world after it.

SpinningCube.tsx
import { useRef } from "react";
import { GameView, quat, useWorld, type Entity, type MutableQuat } from "@nayan-ui/engine";
const turn: MutableQuat = [0, 0, 0, 1]; // reused every frame
export function SpinningCube() {
const cube = useRef<Entity | null>(null);
const angle = useRef(0);
const world = useWorld(64, (w) => {
cube.current = w.spawn({ mesh: "cube", color: "#33B3FF" });
});
if (!world) return null; // ready after warm-up (one frame)
return (
<GameView source={world} style={{ flex: 1 }} camera={{ eye: [0, 2, 5], target: [0, 0, 0], fov: 0.9 }}
onUpdate={(dt) => {
if (!cube.current) return;
angle.current += dt;
quat.y(angle.current, turn);
world.transforms.rotation(cube.current, turn[0], turn[1], turn[2], turn[3]);
}} />
);
}

Spawn, set and despawn

new World(capacity) reserves slots for entities, text letters and particles; spawn returns an Entity handle and throws when the world is full or an option is invalid.

set(entity, options) changes only what you pass (null removes) and returns false if the entity is gone. despawn removes an entity with its children; has and count report what is alive.

Spawn, change and remove

React and React Native · shared engine API

Spawn, change and remove
import { quat, type World } from "@nayan-ui/engine";
export function addPlayer(world: World) {
const player = world.spawn({ mesh: "capsule", position: [0, 1, 0], color: "#4DB3FF" });
world.set(player, { scale: [1, 1.5, 1], rotation: quat.y(Math.PI / 2) });
world.set(player, { bob: null }); // null removes an option
console.log(world.count, world.has(player));
return () => world.despawn(player);
}

EntityOptions reference

spawn and set take the same EntityOptions; in set, null removes physics, parent, follow, bob, lifetime and impact.

EntityOptions reference reference
OptionBehavior
meshBuilt-in Shape or loaded Model; default cube. none draws nothing (groups, pivots).
texture / textureRegionTexture or null; atlas rect [u0, v0, u1, v1]. In set, pass mesh too.
text / font3D text (1 unit tall at scale 1) instead of a mesh; needs font.
align / letterSpacing / tabularText alignment (default center), extra em spacing, equal-width digits.
position / rotationVec3 and quat [x, y, z, w]; relative to the parent.
scaleNumber or Vec3; visual only once a body exists.
colorCSS string or 0..1 tuple; alpha < 1 is see-through, channels > 1 glow.
velocity / groundVelocityUnits per second; groundVelocity sets [x, z] and keeps vertical speed.
acceleration / spinUnits/s² for non-dynamic entities; radians per second per axis.
bob{ amplitude, speed, phase? }: visual sine offset of the drawn position.
follow{ target, speed }: chase an entity along the ground.
lifetimeSeconds until it shrinks away and despawns.
parentAttach to an entity: moves, turns, scales and despawns with it.
physics / impactBodyType or PhysicsOptions; automatic impact sound and haptic.
pickableWhether pick can hit it; default true.

Reusable options

React and React Native · shared engine API

Reusable options
import type { EntityOptions, World } from "@nayan-ui/engine";
const coin: EntityOptions = {
mesh: "cylinder",
scale: [0.6, 0.1, 0.6],
color: "#FFC83D",
spin: [0, 3, 0],
bob: { amplitude: [0, 0.2, 0], speed: 4 },
pickable: false,
};
export function dropCoin(world: World, x: number, z: number) {
return world.spawn({ ...coin, position: [x, 1, z], lifetime: 10 });
}

Rotations with quat

Rotations are quaternions [x, y, z, w] built with quat, with angles in radians. Every helper takes an optional out array; reuse one in per-frame code so nothing allocates.

Rotations with quat reference
HelperResult
quat.identityNo rotation (frozen).
quat.x / y / z(angle, out?)Pitch, yaw (heading) or roll; z spins 2D games.
quat.axisAngle(x, y, z, angle, out?)Rotation around a unit axis.
quat.multiply(a, b, out?)a after b (b turns first); out may be a or b.
quat.fromEuler(x, y, z, out?)Turns around x, then y, then z.
quat.slerp(a, b, t, out?)Shortest-path blend from a (t = 0) to b (t = 1).

Turn smoothly toward travel

React and React Native · shared engine API

Call the returned function from onUpdate; it allocates nothing.

Turn smoothly toward travel
import { quat, type Entity, type MutableQuat, type World } from "@nayan-ui/engine";
export function makeSteering(world: World, actor: Entity) {
const current: MutableQuat = [0, 0, 0, 1];
const target: MutableQuat = [0, 0, 0, 1];
return (dt: number, x: number, z: number) => {
if (x || z) quat.y(Math.atan2(x, z), target); // face +z toward (x, z)
quat.slerp(current, target, Math.min(1, dt * 10), current);
world.transforms.rotation(actor, current[0], current[1], current[2], current[3]);
};
}
export const tilted = quat.multiply(quat.y(Math.PI / 4), quat.x(-0.3)); // tilt, then turn

Per-frame moves with world.transforms

world.transforms queues draw-only position, rotation, scale, color and transform updates without allocating; the next world.update applies them in one native call.

Call flush() only before reading positions or animating in the same frame; set(entity, options) takes the object form. Use world.set for text entities.

Move many pieces in one call

React and React Native · shared engine API

Call from onUpdate; GameView's step applies the batch.

Move many pieces in one call
import type { Entity, World } from "@nayan-ui/engine";
type Piece = { entity: Entity; x: number; z: number };
export function placePieces(world: World, pieces: readonly Piece[], time: number) {
const batch = world.transforms;
for (const p of pieces) {
batch.position(p.entity, p.x, Math.sin(time + p.x) * 0.2, p.z);
batch.scale(p.entity, 1 + Math.sin(time) * 0.05);
}
}

Parents and groups

Children inherit their parent's position, rotation and scale and despawn with it. Put physics on the parent: attached entities can't have bodies.

Compose a collectible

React and React Native · shared engine API

Compose a collectible
import type { World } from "@nayan-ui/engine";
export function addCollectible(world: World) {
const root = world.spawn({ mesh: "none", position: [2, 1, 0], spin: [0, 1, 0] });
world.spawn({ mesh: "torus", parent: root, scale: 0.8, color: "orange" });
world.spawn({ mesh: "sphere", parent: root, scale: 0.3, color: "#FFF080" });
return root; // despawn(root) removes all three
}

Queries, bounds and gravity

position (parent-relative), worldPosition (as drawn) and velocity return null once an entity is gone; pass an out tuple per frame. toScreen and screenToWorld use the last drawn camera, so they return null before the first frame.

world.bounds = [minX, minZ, maxX, maxZ] keeps non-physics entities inside; bodies need walls. world.gravity defaults to [0, -9.81, 0], and world.listener pans impact sounds.

Read state without allocating

React and React Native · shared engine API

Read state without allocating
import type { Entity, World } from "@nayan-ui/engine";
export function makeTracker(world: World, player: Entity, onFall: () => void) {
const at: [number, number, number] = [0, 0, 0];
const screen: [number, number] = [0, 0];
world.bounds = [-20, -20, 20, 20];
world.listener = player;
return () => {
if (world.position(player, at) && at[1] < -5) onFall();
return world.toScreen(at, screen); // place a label over the player
};
}

Reset and dispose

A reset is your own function: cancelTimers(), despawn the round's entities, reset the scoreboard and spawn the next round in the same world. Loaded assets and mesh ids stay valid.

dispose() is idempotent, resolves pending animation promises and drops timers. Code that spawns after an await checks world.isDisposed; cleanup calls need no guard.

Spawn safely after an await

React and React Native · shared engine API

Spawn safely after an await
import type { Entity, World } from "@nayan-ui/engine";
export async function popAndReplace(world: World, piece: Entity) {
await world.animate(piece, { scale: 0 }, { duration: 0.2 });
world.despawn(piece); // safe even if the screen closed
if (world.isDisposed) return null; // spawn would throw
return world.spawn({ mesh: "cube", position: [0, 4, 0] });
}
export function resetRound(world: World, actors: Entity[]) {
world.cancelTimers();
for (const actor of actors.splice(0)) world.despawn(actor);
world.scoreboard.reset(3);
}