Nayan Engine

A 3D game engine for React and React Native on one Rust core.

What the engine is

@nayan-ui/engine is one TypeScript API for games on iOS, Android and the web. Rendering (wgpu), physics (Rapier), animation, particles, audio and drawing run in Rust: natively on phones, as WebAssembly in the browser.

useWorld creates a world and GameView draws and steps it, so this is a complete scene on every platform.

BouncingBall.tsx

React and React Native · shared engine API

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

The core pieces

Everything is imported from @nayan-ui/engine, and each piece has its own guide.

The core pieces reference
PieceWhat it doesGuide
World / useWorldEntities, physics, animation, timers and scoring/game-engine/world-entities
GameViewDraws a world, steps it each frame, forwards touch input/game-engine/game-view
loadModel / loadTexture / loadFontglTF models, images and fonts, decoded off the JS thread/game-engine/models-textures
createShape, lathe, tube, loft…Meshes built in code, baked once and reused/game-engine/procedural-geometry
effectsConfetti, sparkle, shatter, splash, sparks, explosion, dust, flash, pop/game-engine/particles
audio / hapticsSounds mixed in Rust; vibration feedback/game-engine/audio-haptics
DrawingDocument / DrawingCanvas2D painting, also usable as a game texture/game-engine/drawing

crate.ts

React and React Native · shared engine API

Call from useWorld's setup and pass the returned function to GameView's onTap.

crate.ts
import { audio, effects, haptics, loadTexture, type AssetSource, type World } from "@nayan-ui/engine";
export function addCrate(world: World, image: AssetSource, crack: AssetSource) {
const sfx = audio.load({ crack });
const crate = world.spawn({ mesh: "cube", position: [0, 0.5, 0], color: "#C8763A" });
void loadTexture(image).then((texture) => world.set(crate, { texture }));
return (x: number, y: number) => {
if (world.pick(x, y)?.entity !== crate) return;
world.despawn(crate);
effects.shatter(world, { position: [0, 0.5, 0], colors: "#C8763A", size: 0.15, speed: 4, count: 24, lifetime: 0.8, gravity: -9 });
sfx.play("crack");
haptics.impact(0.6);
};
}

Coordinates, colors and handles

Y is up and ground games move on X/Z; angles are radians and rotations are quaternions from quat.y(angle) and friends. Screen callbacks use points (CSS pixels on the web) from the view's top-left.

Colors are CSS strings ('#33B3FF', 'rgba(0, 0, 0, 0.5)', or black, white, gray, red, green, blue, yellow, orange, purple, transparent) or 0..1 tuples, where channels above 1 glow. spawn returns an Entity handle: change it with set, remove it with despawn.

marker.ts

React and React Native · shared engine API

marker.ts
import { quat, type World } from "@nayan-ui/engine";
export function placeMarker(world: World) {
const marker = world.spawn({ mesh: "cone", position: [2, 0.5, -3], color: "#33B3FF" });
world.set(marker, { rotation: quat.y(Math.PI / 2), color: [1.6, 0.4, 0.2] }); // a quarter turn, glowing orange
return () => world.despawn(marker);
}

How frames work

On iOS and Android the display's clock asks for each frame: onUpdate(dt) runs your rules, then a native render thread culls and draws, so JavaScript never waits on the GPU. On the web, requestAnimationFrame drives the same loop.

GameView steps the world right after onUpdate, so don't run your own loop. Drawing stops in the background and in hidden tabs; spread long setup work over frames with afterNextFrame.

Orbit.tsx

React and React Native · shared engine API

Orbit.tsx
import { useRef } from "react";
import { GameView, useWorld, type Camera } from "@nayan-ui/engine";
export function Orbit() {
const world = useWorld(16, (w) => {
w.spawn({ mesh: "torus", color: "#FFBF33" });
});
const camera = useRef<Camera>({ eye: [0, 3, 8], target: [0, 0, 0], fov: 1 }).current;
const time = useRef(0);
const onUpdate = (dt: number) => {
time.current += dt; // GameView steps the world after this returns
camera.eye[0] = Math.sin(time.current) * 8;
camera.eye[2] = Math.cos(time.current) * 8;
};
return world && <GameView source={world} camera={camera} onUpdate={onUpdate} style={{ flex: 1 }} />;
}

React and React Native

Game code is identical on both hosts. Only the container, its style and asset sources change.

React and React Native reference
React (web)React Native
Renders withWebAssembly + WebGPU, WebGL2 fallbackMetal (iOS), Vulkan or GLES (Android)
Frame clockrequestAnimationFrameDisplay clock + native render thread
styleCSS properties; the container needs a heightViewStyle
AssetsURL stringsrequire(...) or URI strings
AudioStarts after the first user gestureReady at once
HapticsBrowser vibration where supportedNative intensity and sharpness
Runs inA modern browser over HTTPS or localhostA development or store build, not Expo Go

Board.tsx

React · browser

Board.tsx
import { GameView, loadTexture, useWorld } from "@nayan-ui/engine";
export function Board() {
const world = useWorld(16, (w) => {
const board = w.spawn({ mesh: "plane", scale: [8, 1, 8] });
void loadTexture("/textures/board.png").then((texture) => w.set(board, { texture }));
});
return <div style={{ height: 480 }}>{world && <GameView source={world} style={{ height: "100%" }} />}</div>;
}

Board.tsx

React Native · iOS and Android

Board.tsx
import { View } from "react-native";
import { GameView, loadTexture, useWorld } from "@nayan-ui/engine";
export function Board() {
const world = useWorld(16, (w) => {
const board = w.spawn({ mesh: "plane", scale: [8, 1, 8] });
void loadTexture(require("./board.png")).then((texture) => w.set(board, { texture }));
});
return <View style={{ flex: 1 }}>{world && <GameView source={world} style={{ flex: 1 }} />}</View>;
}