Install the package and set up Expo, bare React Native or a browser host.
The CLI creates a Bun workspace with an Expo app, a Vite app and the Car Racing example they share, already set up as below. Skip to your own scene, or install the package into an existing app instead.
Create a project
Terminal / configuration
npx @nayan-ui/cli new my-game -t gamescd my-gamebun installbun run web # or: bun run ios / bun run android (development build)
One package serves both hosts; react is a peer dependency, plus react-native for apps. expo-asset is optional and only needed to load require(...) files in Expo.
React (web)
React · browser
npm install @nayan-ui/engine react react-dom
React Native (Expo)
React Native · iOS and Android
npm install @nayan-ui/enginenpx expo install expo-asset expo-build-properties
Metro doesn't bundle glTF files by default. Add glb and gltf to its asset extensions in metro.config.js.
metro.config.js
React Native · iOS and Android
const { getDefaultConfig } = require("expo/metro-config");const config = getDefaultConfig(__dirname);config.resolver.assetExts.push("glb", "gltf");module.exports = config;
Android needs minSdkVersion 26 (Android 8.0), set through expo-build-properties. Haptics need the VIBRATE permission.
app.json
React Native · iOS and Android
{"expo": {"android": { "permissions": ["android.permission.VIBRATE"] },"plugins": [["expo-build-properties", { "android": { "minSdkVersion": 26 } }]]}}
The engine is native code, so it runs in a development or store build; Expo Go doesn't include it. Prebuilt libraries ship in the package, so no Rust toolchain is needed, but rebuild the app after installing or upgrading the engine.
Build and run
React Native · iOS and Android
npx expo run:iosnpx expo run:android# or in the cloud:eas build --profile development
Bare apps autolink the module. Install the pods, set minSdkVersion 26 in Gradle and add the VIBRATE permission.
Bare setup
React Native · iOS and Android
npm install @nayan-ui/enginecd ios && pod install && cd ..# android/build.gradle: minSdkVersion = 26# android/app/src/main/AndroidManifest.xml:# <uses-permission android:name="android.permission.VIBRATE" />
isEngineAvailable is false when the native module isn't in the build, as in Expo Go or a build made before installing. Check it before opening a game screen; useWorld also reports it through onError.
EngineGate.tsx
React Native · iOS and Android
import type { ReactNode } from "react";import { Text } from "react-native";import { isEngineAvailable } from "@nayan-ui/engine";export function EngineGate({ children }: { children: ReactNode }) {if (!isEngineAvailable) return <Text>This screen needs a development build.</Text>;return <>{children}</>;}
Types ship with the package. Bundlers pick the build by export condition: browser for the web, react-native (or react-native-strict-api) for Metro, so web apps never install React Native.
Import @nayan-ui/engine/web to force the browser build, for example in server-rendered code. TypeScript follows the same conditions with moduleResolution bundler.
tsconfig.json
React Native · iOS and Android
For a React Native app; web apps leave out customConditions.
{"compilerOptions": {"strict": true,"jsx": "react-jsx","moduleResolution": "bundler","customConditions": ["react-native"]}}
Serve the page over HTTPS or localhost so WebGPU and AudioWorklet are available; WebGL2 is the fallback, and no cross-origin isolation headers are needed. useWorld loads the WebAssembly core; if you create worlds yourself, await warmUpEngine() first.
Keep index.js, engine_core_bg.wasm and audio-worklet.js together (Vite emits them for you) and serve the wasm as application/wasm. Assets on another origin need CORS, and a strict CSP must allow WebAssembly.
Check the wasm response
Terminal / configuration
Use the wasm URL from the browser's Network tab.
curl -sI https://your-site.example/assets/engine_core_bg.wasm | grep -i content-type# content-type: application/wasm (not text/html from a fallback page)