Draw a world, run the frame loop, choose quality and layer overlays.
GameView draws a RenderSource (usually a World) and calls onUpdate once a frame. Other props are read live; changing source restarts the view.
| Prop | Behavior |
|---|---|
| source | Required RenderSource; never pass a disposed world. |
| onUpdate(dt) | Game logic per frame; GameView steps source and overlay after it. |
| camera / light | Mutable Camera and Light objects, read every frame. |
| background / fog | Sky and fog color (CSS or tuple); fog density, default 0. |
| quality | auto (default), high, medium, low or lowest. |
| paused | Stops updates and drawing, cancels gestures, keeps the surface. |
| style | ViewStyle on React Native; CSS properties on the web. |
| onTap / onLongPress | (x, y) in view points; world.pick(x, y) finds the entity. |
| onSwipe / onDrag / onPinch | Flick direction, drag phases and two-finger scale/rotation. |
| onPrimaryAction | Keyboard primary action, such as Start or Play Again. |
| overlay / onOverlayTouch | Screen-space RenderSource drawn on top, and its touch handler. |
| onStats | GameViewStats once a second. |
| onError | Setup and rendering errors; default console.error. |
Scene.tsx
React and React Native · shared engine API
import { GameView, useWorld } from "@nayan-ui/engine";export function Scene() {const world = useWorld(100, (w) => {w.spawn({ mesh: "plane", scale: [20, 1, 20], color: "#4C9A4C", physics: "fixed" });w.spawn({ mesh: "sphere", position: [0, 4, 0], color: "#3399FF", physics: "dynamic" });});if (!world) return null;return (<GameView source={world} style={{ flex: 1 }} background="#87CEEB" quality="auto"camera={{ eye: [0, 8, 14], target: [0, 0, 0], fov: Math.PI / 3 }}onTap={(x, y) => {const hit = world.pick(x, y);if (hit) world.impulse(hit.entity, [0, 6, 0]);}} />);}
On iOS and Android frames come from the display clock at 60 Hz: onUpdate steps the game and hands a copy to the engine's render thread, so JavaScript never waits on the GPU. A frame not yet drawn is replaced by a newer one, not queued; the web uses requestAnimationFrame.
dt is capped at 0.1 s and is 0 until the first frame is on screen, so intros don't play unseen. Step the world yourself only for a different dt, such as slow motion.
SlowMotion.tsx
React and React Native · shared engine API
Calling world.update in onUpdate replaces GameView's own step for that frame (updateCount changed).
import { GameView, type World } from "@nayan-ui/engine";export function SlowMotion({ world, slow }: { world: World; slow: boolean }) {return (<GameView source={world} style={{ flex: 1 }}onUpdate={(dt) => {if (slow) world.update(dt * 0.25); // GameView skips its step this frame}} />);}
auto starts at high and steps down only while drawing can't keep up: after one second under ~33 fps, or a few seconds missing ~50 fps. It drops shadow detail first, then MSAA, then resolution, and climbs back after about ten smooth seconds.
Quality can't fix a slow game loop; that shows in updateMs, not renderMs. The chosen level survives pauses and only restarts with a new source.
| Level | Pixel ratio cap | MSAA | Shadow map |
|---|---|---|---|
| high | 3 | 4× | 2048, soft |
| medium | 3 | 4× | 1024, soft |
| low | 3 | off | 1024, hard |
| lowest | 2 | off | 512, hard |
Battery saver
React and React Native · shared engine API
import { GameView, type RenderQualitySetting, type World } from "@nayan-ui/engine";export function Game({ world, saver }: { world: World; saver: boolean }) {const quality: RenderQualitySetting = saver ? "low" : "auto";return <GameView source={world} quality={quality} style={{ flex: 1 }} />;}
onStats receives GameViewStats once a second. On iOS and Android fps counts frames actually presented and renderMs is the render thread's time per frame.
| Field | Meaning |
|---|---|
| fps | Frames per second (presented frames on native). |
| updateMs | Mean milliseconds in onUpdate and the step. |
| renderMs | Mean milliseconds drawing (render thread on native, CPU side on web). |
| quality | Level drawn with; with auto, where it settled so far. |
| backend | metal, vulkan or gl on native; webgpu or gl on the web. |
Log rendering cost
React and React Native · shared engine API
import { GameView, type GameViewStats, type World } from "@nayan-ui/engine";const log = ({ fps, updateMs, renderMs, quality, backend }: GameViewStats) =>console.log(`${fps.toFixed(0)} fps, update ${updateMs.toFixed(1)} ms, render ${renderMs.toFixed(1)} ms, ${quality}, ${backend}`);export function Profiled({ world }: { world: World }) {return <GameView source={world} onStats={log} onError={(e) => console.error(e.message)} />;}
overlay draws a second World in view points from the top-left, y up (visible y ≤ 0) and z within -100..100. It is unlit, always in front and stepped after onUpdate like the scene.
Return true from onOverlayTouch("start") to claim a touch: its moves and end then go to the overlay, never to scene gestures. "cancel" arrives when the system takes the touch.
Overlay button
React and React Native · shared engine API
import { GameView, type World } from "@nayan-ui/engine";export function WithHud({ world, hud, onStart }: { world: World; hud: World; onStart: () => void }) {const inside = (x: number, y: number) => x >= 20 && x <= 140 && y >= 20 && y <= 70;return (<GameView source={world} overlay={hud} style={{ flex: 1 }}onOverlayTouch={(phase, x, y) => {if (phase === "start") return inside(x, y);if (phase === "end" && inside(x, y)) onStart();return true;}} />);}
paused stops onUpdate, stepping and drawing and cancels gestures while the surface stays mounted. The view also suspends itself when the app goes to the background.
Pause behind a settings sheet
React and React Native · shared engine API
import { GameView, type World } from "@nayan-ui/engine";export function Paused({ world, settingsOpen }: { world: World; settingsOpen: boolean }) {return <GameView source={world} paused={settingsOpen} style={{ flex: 1 }} />;}