Common game recipes

Short, copyable solutions for everyday game tasks.

Move without React state

Queue per-frame moves on world.transforms from onUpdate; they apply in one native call when GameView steps the world. Each recipe returning a function is meant to be called from onUpdate.

Slide a box

React and React Native · shared engine API

Slide a box
import type { World } from "@nayan-ui/engine";
export function addSlider(world: World) {
const box = world.spawn({ mesh: "cube", position: [-4, 1, 0], color: "#33B3FF" });
let x = -4;
return (dt: number) => {
x = x > 4 ? -4 : x + 2 * dt; // 2 units per second
world.transforms.position(box, x, 1, 0);
};
}

Fall and bounce

A fixed floor and a dynamic ball: physics handles gravity and the bounce.

Bouncing ball

React and React Native · shared engine API

Bouncing ball
import { GameView, useWorld } from "@nayan-ui/engine";
export function Bounce() {
const world = useWorld(16, (w) => {
w.spawn({ mesh: "plane", scale: [20, 1, 20], color: "#4C9A4C", physics: "fixed" });
w.spawn({ mesh: "sphere", position: [0, 5, 0], color: "#FF801A", physics: { type: "dynamic", bounce: 0.7 } });
});
return world && <GameView source={world} camera={{ eye: [0, 4, 8], target: [0, 0, 0], fov: 1 }} style={{ flex: 1 }} />;
}

Steer with a joystick

useJoystick returns a mutable state: read it in onUpdate, never mirror it into React state. groundVelocity sets X/Z speed and keeps gravity.

Joystick player

React and React Native · shared engine API

Joystick player
import { GameView, Joystick, useJoystick, type Entity, type World } from "@nayan-ui/engine";
export function Steer({ world, player }: { world: World; player: Entity }) {
const stick = useJoystick();
return (
<>
<GameView source={world} style={{ flex: 1 }}
onUpdate={() => world.set(player, { groundVelocity: [stick.x * 5, -stick.y * 5] })} />
<Joystick state={stick} />
</>
);
}

Jump only when grounded

Cast a short ray down against the floor's layer and skip the jump while already rising.

Grounded jump

React and React Native · shared engine API

Grounded jump
import type { Entity, World } from "@nayan-ui/engine";
const at: [number, number, number] = [0, 0, 0];
const vel: [number, number, number] = [0, 0, 0];
// Player: a 1.8-unit capsule; floor on layer 1.
export function jump(world: World, player: Entity) {
if (!world.position(player, at) || !world.velocity(player, vel) || vel[1] > 0.1) return false;
if (!world.raycast(at, [0, -1, 0], 1.05, 1)) return false;
world.impulse(player, [0, 5, 0]);
return true;
}

Pick up on contact

A sensor reports contact without pushing. forEachCollision in onUpdate reads the last step's contacts, each once.

Coin pickup

React and React Native · shared engine API

Coin pickup
import type { Entity, World } from "@nayan-ui/engine";
export function addCoin(world: World, player: Entity) {
const coin = world.spawn({ mesh: "cylinder", position: [3, 1, 0], scale: [0.6, 0.1, 0.6],
color: "#FFCC1A", spin: [0, 2, 0], physics: { type: "fixed", sensor: true } });
return () =>
world.forEachCollision((a, b, { started }) => {
if (started && ((a === coin && b === player) || (a === player && b === coin))) {
world.despawn(coin);
world.scoreboard.add(100);
}
});
}

Tap to recolor

world.pick finds the entity under a tap; no physics body needed.

Tap to paint

React and React Native · shared engine API

Tap to paint
import { GameView, type World } from "@nayan-ui/engine";
export function TapToPaint({ world }: { world: World }) {
return (
<GameView source={world} style={{ flex: 1 }}
onTap={(x, y) => {
const hit = world.pick(x, y);
if (hit) world.set(hit.entity, { color: "#FF4D66" });
}} />
);
}

Chase the player

Set follow once and the engine steers the chaser along the ground; follow: null stops it.

Chaser

React and React Native · shared engine API

Chaser
import type { Entity, World } from "@nayan-ui/engine";
export function addChaser(world: World, player: Entity) {
const enemy = world.spawn({ mesh: "sphere", position: [5, 1, 5], color: "red", follow: { target: player, speed: 2 } });
return () => world.set(enemy, { follow: null }); // stop chasing
}

Pop and remove

effects.pop swells, flashes and shrinks a piece away; despawn when it resolves. No disposal guard is needed: despawn is a no-op on a disposed world.

Clear a piece

React and React Native · shared engine API

Clear a piece
import { effects, type Entity, type World } from "@nayan-ui/engine";
export async function clearPiece(world: World, piece: Entity) {
await effects.pop(world, piece, { scale: 1, color: "#FF4D4D" });
world.despawn(piece);
}

Show the score in React

scoreboard.listen adds a UI observer beside the game's handlers and returns its unlisten, so it fits a useEffect. It fires only when the value changes.

useScore

React and React Native · shared engine API

useScore
import { useEffect, useState } from "react";
import type { World } from "@nayan-ui/engine";
export function useScore(world: World) {
const [score, setScore] = useState(world.scoreboard.score);
useEffect(() => world.scoreboard.listen({ onScore: setScore }), [world]);
return score;
}

Delay in game time

world.after counts game time, so it pauses with the game. Call cancelTimers on reset so an old round can't act in the new one.

Delayed bonus

React and React Native · shared engine API

Delayed bonus
import type { World } from "@nayan-ui/engine";
export function scheduleBonus(world: World) {
const id = world.after(3, () => world.spawn({ mesh: "torus", position: [0, 2, 0], color: "#FFCC1A" }));
return () => world.cancelTimer(id);
}

Build a model once, spawn it many times

Mesh ids are never freed: build each model once per app (after warm-up, e.g. in useWorld's setup) and reuse it.

Reusable star

React and React Native · shared engine API

Reusable star
import { createShape, extrude, Path, type Model, type World } from "@nayan-ui/engine";
let star: Model | undefined;
const starMesh = () => (star ??= createShape(extrude(new Path().addStar(0, 0, 0.5, 0.22, 5), { depth: 0.15 }), { shine: 0.4 }));
export function addStars(world: World) {
for (let i = 0; i < 5; i++) world.spawn({ mesh: starMesh(), position: [i - 2, 1, 0], color: "#FFCC33" });
}

Sound and haptics on an action

Load the bank once, outside the frame loop (a URL on the web, require(...) on native). Mirror the player's settings onto audio.enabled and haptics.enabled, then play freely.

Pickup feedback

React and React Native · shared engine API

Pickup feedback
import { audio, haptics } from "@nayan-ui/engine";
const sfx = audio.load({ pickup: "/sounds/pickup.wav" }); // native: require("./pickup.wav")
export function applySettings(sounds: boolean, vibration: boolean) {
audio.enabled = sounds;
haptics.enabled = vibration;
}
export function onPickup() {
sfx.play("pickup", { volume: 0.7 });
haptics.selection();
}

Confetti on a win

One call that cleans itself up. Leave room in the world's capacity for the particles (about count).

Confetti

React and React Native · shared engine API

Confetti
import { effects, type World } from "@nayan-ui/engine";
export function celebrate(world: World) {
effects.confetti(world, { position: [0, 3, 0], colors: ["#FF4D4D", "#FFD933", "#4DB3FF"],
size: 0.12, speed: 6, count: 40, lifetime: 1.4, gravity: -6 });
}

Face the movement direction

quat.y builds the heading and quat.slerp eases toward it, both into reused arrays. For a dynamic body, rotate a visual child (parent: player) so physics keeps the body.

Turn toward the stick

React and React Native · shared engine API

Turn toward the stick
import { quat, type Entity, type JoystickState, type MutableQuat, type World } from "@nayan-ui/engine";
export function makeFacing(world: World, model: Entity, stick: JoystickState) {
const facing: MutableQuat = [0, 0, 0, 1];
const goal: MutableQuat = [0, 0, 0, 1];
return (dt: number) => {
if (stick.x || stick.y) quat.y(Math.atan2(stick.x, -stick.y), goal); // front faces +z
quat.slerp(facing, goal, Math.min(1, dt * 12), facing);
world.transforms.rotation(model, facing[0], facing[1], facing[2], facing[3]);
};
}