@dolami-inc/react-native-expo-unity
v0.5.3
Published
Unity as a Library (UaaL) bridge for React Native / Expo
Maintainers
Readme
@dolami-inc/react-native-expo-unity
Unity as a Library (UaaL) bridge for React Native / Expo.
Install
npm install @dolami-inc/react-native-expo-unity
# or
yarn add @dolami-inc/react-native-expo-unity
# or
bun add @dolami-inc/react-native-expo-unityQuick Start
import { UnityView, type UnityViewRef } from "@dolami-inc/react-native-expo-unity";
const unityRef = useRef<UnityViewRef>(null);
<UnityView
ref={unityRef}
style={{ flex: 1 }}
onUnityMessage={(e) => console.log(e.message)}
/>
// Send message to Unity
unityRef.current?.postMessage("GameObject", "Method", "payload");API
<UnityView />
| Prop | Type | Default | Description |
|---|---|---|---|
| onUnityMessage | (e: { message: string }) => void | — | Message from Unity |
| autoUnloadOnUnmount | boolean | true | Unload Unity when view unmounts. Set false to pause only (keeps state). |
| style | ViewStyle | — | Must have dimensions (e.g. flex: 1) |
| ref | UnityViewRef | — | Imperative methods |
Ref Methods
unityRef.current?.postMessage(gameObject, methodName, message)
unityRef.current?.pauseUnity()
unityRef.current?.resumeUnity()
unityRef.current?.unloadUnity()Standalone Functions
Same as ref methods, callable anywhere (operates on the singleton):
import { postMessage, pauseUnity, resumeUnity, unloadUnity, isInitialized } from "@dolami-inc/react-native-expo-unity";Setup
1. Unity project — add plugins
Copy the platform bridge files into your Unity project:
iOS:
# From node_modules after install
cp node_modules/@dolami-inc/react-native-expo-unity/plugin/NativeCallProxy.h <UnityProject>/Assets/Plugins/iOS/
cp node_modules/@dolami-inc/react-native-expo-unity/plugin/NativeCallProxy.mm <UnityProject>/Assets/Plugins/iOS/Android:
No plugin files need to be copied — the NativeCallProxy Java class ships with the module and is available at runtime automatically. Your Unity C# code calls it via AndroidJavaClass (see Messaging Guide).
2. Unity project — build for your target platform
iOS
- Unity → File → Build Settings → iOS → Build
- Open generated Xcode project
- Select
NativeCallProxy.hin Libraries/Plugins/iOS/ - Set Target Membership →
UnityFramework→ Public - Select the
Datafolder in the Project Navigator - In the right panel under Target Membership, check
UnityFrameworkThis is critical. Without this, the
Datafolder (which containsglobal-metadata.datand all Unity assets) will NOT be included insideUnityFramework.framework. The app will crash at launch with:Could not open .../global-metadata.dat — IL2CPP initialization failed - Build
UnityFrameworkscheme
Android
- Unity → File → Build Settings → Android
- Check Export Project (do not "Build" directly — you need the Gradle project)
- Set Scripting Backend to IL2CPP
- Set Target Architectures: ARMv7 and ARM64
- Click Export and save to a directory (e.g.
unity/builds/android)
3. Copy build artifacts to your RN project
iOS
Create unity/builds/ios/ in your project root and copy the built framework and static libraries:
mkdir -p unity/builds/ios
# Copy the compiled framework (should already contain Data/ inside after step 2.6)
cp -R <xcode-build-output>/UnityFramework.framework unity/builds/ios/
# Copy static libraries from the Unity Xcode project root
cp <unity-xcode-project>/*.a unity/builds/ios/Verify that Data/ exists inside the framework:
ls unity/builds/ios/UnityFramework.framework/Data
# Should show: Managed/ Resources/ etc.The podspec references these files directly by path — nothing is copied or embedded into the npm package. Updating your Unity build is as simple as replacing the contents of unity/builds/ios/ and re-running pod install.
Custom path? Set
EXPO_UNITY_PATHenvironment variable pointing to your Unity build directory, or passunityPathto the config plugin (see step 4).
Android
The Unity export directory (containing the unityLibrary folder) should be at unity/builds/android/ in your project root. The config plugin will automatically include the :unityLibrary Gradle module.
# Verify the structure
ls unity/builds/android/unityLibrary
# Should show: libs/ src/ build.gradle etc.Custom path? Set
EXPO_UNITY_ANDROID_PATHenvironment variable, or passandroidUnityPathto the config plugin.
4. Add the config plugin to app.json
{
"expo": {
"plugins": [
"@dolami-inc/react-native-expo-unity"
]
}
}The plugin automatically configures:
iOS:
ENABLE_BITCODE = NO— Unity does not support bitcodeCLANG_CXX_LANGUAGE_STANDARD = c++17— required for Unity headers- Embeds
UnityFramework.frameworkvia a build script phase
Android:
- Includes
:unityLibrarymodule insettings.gradle - Adds
flatDirrepository for Unity's native libs - Adds
ndk.abiFiltersforarmeabi-v7aandarm64-v8a
If your Unity artifacts are in custom paths:
["@dolami-inc/react-native-expo-unity", {
"unityPath": "/absolute/path/to/unity/builds/ios",
"androidUnityPath": "/absolute/path/to/unity/builds/android"
}]5. Build
# iOS
expo prebuild --platform ios --clean
expo run:ios --device
# Android
expo prebuild --platform android --clean
expo run:androidLifecycle
Unity is a singleton — one instance for the entire app.
| State | Memory | Re-entry |
|---|---|---|
| Running | ~200-500MB+ (depends on scene/assets) | Already running |
| Paused | Same (frozen in memory, no CPU/GPU usage) | resumeUnity() — instant, state preserved |
| Unloaded | ~80-180MB retained (Unity limitation) | Remount <UnityView /> — ~1-2s reinit, state reset |
Auto behavior
| Event | What happens |
|---|---|
| <UnityView /> mounts | Unity initializes and starts rendering |
| <UnityView /> unmounts | Unity unloads (or pauses if autoUnloadOnUnmount={false}) |
| App → background | Unity pauses |
| App → foreground | Unity resumes |
Manual control
Screen focus/blur is not automatic — handle with useFocusEffect:
useFocusEffect(
useCallback(() => {
unityRef.current?.resumeUnity();
return () => unityRef.current?.pauseUnity();
}, [])
);Messaging
RN → Unity
unityRef.current?.postMessage("GameManager", "LoadAvatar", '{"id":"avatar_01"}');// Unity C# — on "GameManager" GameObject
public void LoadAvatar(string json) { /* ... */ }Unity → RN
// iOS — uses extern "C" DllImport
#if UNITY_IOS && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern void sendMessageToMobileApp(string message);
#endif
// Android — uses AndroidJavaClass
private static void SendToMobile(string message) {
#if UNITY_IOS && !UNITY_EDITOR
sendMessageToMobileApp(message);
#elif UNITY_ANDROID && !UNITY_EDITOR
using (var proxy = new AndroidJavaClass("com.expounity.bridge.NativeCallProxy")) {
proxy.CallStatic("sendMessageToMobileApp", message);
}
#endif
}
// Usage:
SendToMobile("{\"event\":\"image_taken\",\"data\":{\"path\":\"/tmp/photo.jpg\"}}");<UnityView onUnityMessage={(e) => {
const msg = JSON.parse(e.message);
// msg.event, msg.data
}} />See Messaging Guide for recommended patterns.
Docs
- Lifecycle Deep Dive — navigation scenarios, state management, trade-offs
- Messaging Guide — recommended JSON format, Unity C# + RN examples
Requirements
- Expo SDK 54+
- React Native New Architecture (Fabric) — old architecture not supported
- Physical device — iOS: Unity renders only on device, Simulator shows a placeholder. Android: physical device or emulator with ARM support.
- Unity build artifacts — must be exported/copied manually into your project (not bundled via npm)
Platform Support
| Platform | Status | |---|---| | iOS Device | Supported | | iOS Simulator | Not supported — renders a placeholder view | | Android Device | Supported | | Android Emulator | Supported (ARM-based emulators only) |
Limitations
- Single instance — only one Unity view at a time, cannot run multiple
- Full-screen rendering only — Unity renders full-screen within its view (Unity limitation)
- Memory retention — after
unloadUnity(), Unity retains 80-180MB in memory (Unity limitation) - No reload after quit — if Unity calls
Application.Quit()on iOS, it cannot be restarted without restarting the app - No hot reload — native code changes require a full rebuild
License
MIT
