Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
b9adbe1
fix: Add Expo compatibility for iOS Unity framework integration
Th0mYT Nov 12, 2025
d2e7fc1
New Arch initWithFrame:: call initUnityModule so Unity starts regardl…
Th0mYT Feb 20, 2026
094ca48
fix: add iOS Fabric registration for RNUnityView and fix Android stri…
Th0mYT Feb 20, 2026
977ef46
Add private field to package.json
Th0mYT Feb 20, 2026
f707236
fix: type casting in UPlayer and update .gitignore
Th0mYT Feb 24, 2026
29fce81
fix: correct type casting in UPlayer and add pack script to package.json
Th0mYT Feb 24, 2026
e9aed18
fix: update plugin mods to prevent duplicate entries and extend .giti…
Th0mYT Feb 24, 2026
d360510
fix: update clean script and simplify plugin configuration
Th0mYT Feb 25, 2026
76a4a1d
fix: extend tsconfig paths and update build process for plugin
Th0mYT Feb 25, 2026
c94c4dd
fix: update plugin mod checks to prevent redundant unityLibrary entri…
Th0mYT Feb 27, 2026
947e74c
fix: remove unnecessary error throw in plugin mod to simplify config …
Th0mYT Feb 27, 2026
d4c4564
fix: refactor plugin mod to prevent duplicate
Th0mYT Mar 2, 2026
c8fe0c5
fix: update package version and extend .gitignore for Unity package
Th0mYT Mar 2, 2026
d0e9fbb
fix: trim the end of the lines to prevent trailing whitespace issues
Th0mYT Mar 2, 2026
d34890c
fix: bump package version to 1.0.13
Th0mYT Mar 2, 2026
d34e4ad
fix: bump package version to 1.0.13
Th0mYT Mar 2, 2026
e72563b
fix: remove "private" flag from package.json
Th0mYT Mar 3, 2026
9c59f3e
fix: remove jcenter() from the repositories block, it was shut down
Th0mYT Mar 3, 2026
6406919
feat: added imports, tag constants and fix the createPlayer method
Th0mYT Mar 3, 2026
3857919
feat: new architecture compatibility
Th0mYT Mar 3, 2026
a0c00c2
feat: replaced getConstructors()[1] with a loop that finds the constr…
Th0mYT Mar 3, 2026
c929764
fix: event dispatch, pause state, and view lifecycle
Th0mYT Mar 3, 2026
ea3aa97
Merge branch 'refs/heads/main' into fix/expo-compatibility
Th0mYT Mar 3, 2026
32f796f
fix: getSurfaceId is now wrapped in a try-catch
Th0mYT Mar 3, 2026
60c1aef
fix: disable predictive back gesture to prevent crashes on older Andr…
Th0mYT Mar 4, 2026
a105b74
chore: bump package version to 1.0.14
Th0mYT Mar 4, 2026
95fe733
fix: ensure thread safety for Unity callbacks and event dispatch
Th0mYT Mar 5, 2026
5f86bcb
chore: bump package version to 1.0.15
Th0mYT Mar 5, 2026
0ae02f3
fix: force Unity frame layout in new architecture to ensure valid dim…
Th0mYT Mar 5, 2026
4a8afd9
fix: improve Unity frame layout handling and event dispatch in new ar…
Th0mYT Mar 5, 2026
3d6cb0f
fix: improve Unity frame visibility and rendering timing
Th0mYT Mar 5, 2026
9efd858
chore: bump package version to 1.0.19
Th0mYT Mar 5, 2026
9ee29fc
fix: improve Unity surface handling to avoid black screens after re-p…
Th0mYT Mar 5, 2026
30b56c2
chore: bump package version to 1.0.20
Th0mYT Mar 5, 2026
2a98717
chore: update `pack` script to include `clean` and `prepare` steps be…
Th0mYT Mar 5, 2026
e050764
fix: improve Unity resume timing and rendering surface readiness
Th0mYT Mar 5, 2026
6982c6f
chore: bump package version to 1.0.21
Th0mYT Mar 5, 2026
2d9ec75
fix: pause Unity rendering before surface destruction to prevent crashes
Th0mYT Mar 5, 2026
932fb4f
chore: bump package version to 1.0.22
Th0mYT Mar 5, 2026
5b67c55
fix: update UnityPlayer instantiation and flag handling for API 30+ c…
Th0mYT Mar 6, 2026
6e1a9ab
chore: bump package version to 1.0.23
Th0mYT Mar 6, 2026
bd80290
fix: enhance Unity surface handling with timeout fallback and measure…
Th0mYT Mar 6, 2026
6c35fa1
chore: bump package version to 1.0.24
Th0mYT Mar 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,4 @@ android/keystores/debug.keystore

# generated by bob
lib/
/azesmway-react-native-unity-*.tgz
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ Attention! Added support for Unity 2023 and above

# Installation

## Install this package in your react-native project:

```sh
npm install @azesmway/react-native-unity

Expand All @@ -29,6 +27,20 @@ or
yarn add @azesmway/react-native-unity
```

For Expo projects (SDK 48+)

1. Run prebuild
```sh
npx expo prebuild --clean
```
2. Build your app
```sh
npx expo run:ios
```

**Note for Expo users**: The UnityFramework must be placed at `<project_root>/unity/builds/ios/` before running `expo prebuild`.


## Configure your Unity project:

1. Copy the contents of the folder `unity` to the root of your Unity project. This folder contains the necessary scripts and settings for the Unity project. You can find these files in your react-native project under `node_modules/@azesmway/react-native-unity/unity`. This is necessary to ensure iOS has access to the `NativeCallProxy` class from this library.
Expand Down Expand Up @@ -179,6 +191,20 @@ const Unity = () => {
export default Unity;
```

## Automatic Setup with Expo Config Plugin

Add the plugin to your `app.json`:

```json
{
"expo": {
"plugins": [
"@azesmway/react-native-unity"
]
}
}
```

## Props

- `style: ViewStyle` - styles the UnityView. (Won't show on Android without dimensions. Recommended to give it `flex: 1` as in the example)
Expand Down
1 change: 0 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ repositories {
}
google()
mavenCentral()
jcenter()
}


Expand Down
249 changes: 214 additions & 35 deletions android/src/main/java/com/azesmwayreactnativeunity/ReactNativeUnity.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ protected void onConfigurationChanged(Configuration newConfig) {
@Override
protected void onDetachedFromWindow() {
if (!this.keepPlayerMounted) {
// Pause Unity before moving to background so the render thread stops
// producing frames before the surface is destroyed, preventing:
// BufferQueueProducer disconnect: not connected
if (view != null) {
view.pause();
}
try {
addUnityViewToBackground();
} catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,30 @@
import static com.azesmwayreactnativeunity.ReactNativeUnity.*;

import android.os.Handler;
import android.util.Log;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.facebook.react.uimanager.events.EventDispatcher;

import java.lang.reflect.InvocationTargetException;
import java.util.Map;

@ReactModule(name = ReactNativeUnityViewManager.NAME)
public class ReactNativeUnityViewManager extends ReactNativeUnityViewManagerSpec<ReactNativeUnityView> implements LifecycleEventListener, View.OnAttachStateChangeListener {
private static final String TAG = "ReactNativeUnity";
ReactApplicationContext context;
static ReactNativeUnityView view;
public static final String NAME = "RNUnityView";
Expand All @@ -45,13 +46,17 @@ public String getName() {
@NonNull
@Override
public ReactNativeUnityView createViewInstance(@NonNull ThemedReactContext context) {
view = new ReactNativeUnityView(this.context);
// Use ThemedReactContext (not ReactApplicationContext) so Fabric can correctly associate
// this view with its surface and apply layout dimensions via Yoga.
view = new ReactNativeUnityView(context);
view.addOnAttachStateChangeListener(this);

if (getPlayer() != null) {
try {
view.setUnityPlayer(getPlayer());
} catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {}
} catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
Log.e(TAG, "setUnityPlayer failed", e);
}
} else {
try {
createPlayer(context.getCurrentActivity(), new UnityPlayerCallback() {
Expand All @@ -62,21 +67,17 @@ public void onReady() throws InvocationTargetException, NoSuchMethodException, I

@Override
public void onUnload() {
WritableMap data = Arguments.createMap();
data.putString("message", "MyMessage");
ReactContext reactContext = (ReactContext) view.getContext();
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(view.getId(), "onPlayerUnload", data);
dispatchEvent(view, "onPlayerUnload", "");
}

@Override
public void onQuit() {
WritableMap data = Arguments.createMap();
data.putString("message", "MyMessage");
ReactContext reactContext = (ReactContext) view.getContext();
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(view.getId(), "onPlayerQuit", data);
dispatchEvent(view, "onPlayerQuit", "");
}
});
} catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {}
} catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
Log.e(TAG, "createPlayer failed", e);
}
}

return view;
Expand Down Expand Up @@ -139,8 +140,7 @@ public void unloadUnity(ReactNativeUnityView view) {
@Override
public void pauseUnity(ReactNativeUnityView view, boolean pause) {
if (isUnityReady()) {
assert getPlayer() != null;
getPlayer().pause();
if (pause) { pause(); } else { resume(); }
}
}

Expand All @@ -160,17 +160,56 @@ public void windowFocusChanged(ReactNativeUnityView view, boolean hasFocus) {
}
}

private static void dispatchEvent(ReactNativeUnityView view, String eventName, String message) {
if (view == null) { Log.e(TAG, "dispatchEvent: null view for " + eventName); return; }
android.content.Context viewCtx = view.getContext();
ReactContext ctx = (ReactContext) viewCtx;
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
int surfaceId;
try {
// Primary: walk the view's parent chain to find the ReactRoot.
surfaceId = UIManagerHelper.getSurfaceId(view);
} catch (IllegalStateException e) {
// UIManagerHelper walks the view hierarchy which can fail if Unity's frame reparenting
// detached the view from its Fabric root. Use ThemedReactContext.getSurfaceId() directly
// since it holds the surface ID assigned at view creation time.
if (viewCtx instanceof ThemedReactContext) {
surfaceId = ((ThemedReactContext) viewCtx).getSurfaceId();
Log.d(TAG, "dispatchEvent: surfaceId from ThemedReactContext=" + surfaceId + " for " + eventName);
} else {
Log.w(TAG, "dispatchEvent: no surfaceId available for " + eventName + ", dropping");
return;
}
}
EventDispatcher ed = UIManagerHelper.getEventDispatcherForReactTag(ctx, view.getId());
if (ed != null) ed.dispatchEvent(new UnityEvent(eventName, message, surfaceId, view.getId()));
else Log.e(TAG, "No EventDispatcher for " + eventName);
} else {
com.facebook.react.bridge.WritableMap data = com.facebook.react.bridge.Arguments.createMap();
data.putString("message", message);
ctx.getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter.class)
.receiveEvent(view.getId(), eventName, data);
}
}

public static void sendMessageToMobileApp(String message) {
WritableMap data = Arguments.createMap();
data.putString("message", message);
ReactContext reactContext = (ReactContext) view.getContext();
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(view.getId(), "onUnityMessage", data);
if (view == null) { return; }
// Unity calls this from its own native thread. UIManagerHelper calls in dispatchEvent
// are not thread-safe from non-main threads, so post to the main thread.
final ReactNativeUnityView currentView = view;
new Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
if (currentView != null) dispatchEvent(currentView, "onUnityMessage", message);
}
});
}

@Override
public void onDropViewInstance(ReactNativeUnityView view) {
view.removeOnAttachStateChangeListener(this);
super.onDropViewInstance(view);
if (ReactNativeUnityViewManager.view == view) { ReactNativeUnityViewManager.view = null; }
}

@Override
Expand Down
69 changes: 63 additions & 6 deletions android/src/main/java/com/azesmwayreactnativeunity/UPlayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.app.Activity;
import android.content.res.Configuration;
import android.util.Log;
import android.widget.FrameLayout;

import com.unity3d.player.*;
Expand All @@ -11,20 +12,56 @@
import java.lang.reflect.Method;

public class UPlayer {
private static final String TAG = "ReactNativeUnity";
private static UnityPlayer unityPlayer;

public UPlayer(final Activity activity, final ReactNativeUnity.UnityPlayerCallback callback) throws ClassNotFoundException, InvocationTargetException, IllegalAccessException, InstantiationException {
public UPlayer(final Activity activity, final ReactNativeUnity.UnityPlayerCallback callback) throws ClassNotFoundException, InvocationTargetException, IllegalAccessException, InstantiationException, NoSuchMethodException {
super();
Class<?> _player = null;

try {
_player = Class.forName("com.unity3d.player.UnityPlayerForActivityOrService");
Log.d(TAG, "UPlayer: using UnityPlayerForActivityOrService");
} catch (ClassNotFoundException e) {
_player = Class.forName("com.unity3d.player.UnityPlayer");
Log.d(TAG, "UPlayer: using UnityPlayer");
}

Constructor<?> constructor = _player.getConstructors()[1];
unityPlayer = (UnityPlayer) constructor.newInstance(activity, new IUnityPlayerLifecycleEvents() {
// Log all available constructors to aid debugging on new Android versions.
Constructor<?>[] allConstructors = _player.getConstructors();
Log.d(TAG, "UPlayer: found " + allConstructors.length + " public constructor(s) for " + _player.getName());
for (Constructor<?> c : allConstructors) {
Log.d(TAG, "UPlayer: " + c.toGenericString());
}

// Prefer the 2-arg constructor (Context/Activity, IUnityPlayerLifecycleEvents).
Constructor<?> constructor = null;
for (Constructor<?> c : allConstructors) {
Class<?>[] params = c.getParameterTypes();
if (params.length == 2 && params[1].isAssignableFrom(IUnityPlayerLifecycleEvents.class)) {
constructor = c;
break;
}
}

// Fallback: some Unity versions may have added a 3rd parameter constructor.
if (constructor == null) {
for (Constructor<?> c : allConstructors) {
Class<?>[] params = c.getParameterTypes();
if (params.length >= 2 && params[1].isAssignableFrom(IUnityPlayerLifecycleEvents.class)) {
Log.w(TAG, "UPlayer: falling back to " + params.length + "-param constructor");
constructor = c;
break;
}
}
}

if (constructor == null) {
Log.e(TAG, "UPlayer: no suitable constructor found — Unity SDK may be incompatible");
throw new NoSuchMethodException("No matching UnityPlayer constructor found");
}

final IUnityPlayerLifecycleEvents lifecycleEvents = new IUnityPlayerLifecycleEvents() {
@Override
public void onUnityPlayerUnloaded() {
callback.onUnload();
Expand All @@ -34,7 +71,27 @@ public void onUnityPlayerUnloaded() {
public void onUnityPlayerQuitted() {
callback.onQuit();
}
});
};

try {
if (constructor.getParameterTypes().length == 2) {
unityPlayer = (UnityPlayer) constructor.newInstance(activity, lifecycleEvents);
} else {
// 3+ param constructor: pass null for extra params and hope for the best;
// realistically this branch means the Unity SDK needs an update.
Object[] args = new Object[constructor.getParameterTypes().length];
args[0] = activity;
args[1] = lifecycleEvents;
unityPlayer = (UnityPlayer) constructor.newInstance(args);
}
Log.d(TAG, "UPlayer: UnityPlayer instantiated successfully");
} catch (InvocationTargetException e) {
Log.e(TAG, "UPlayer: UnityPlayer constructor threw an exception", e.getCause() != null ? e.getCause() : e);
throw e;
} catch (InstantiationException | IllegalAccessException e) {
Log.e(TAG, "UPlayer: failed to instantiate UnityPlayer", e);
throw e;
}
}

public static void UnitySendMessage(String gameObject, String methodName, String message) {
Expand Down Expand Up @@ -97,13 +154,13 @@ public FrameLayout requestFrame() throws NoSuchMethodException {

return (FrameLayout) getFrameLayout.invoke(unityPlayer);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
return unityPlayer;
return (FrameLayout)(Object) unityPlayer;
}
}

public void setZ(float v) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
try {
Method setZ = unityPlayer.getClass().getMethod("setZ");
Method setZ = unityPlayer.getClass().getMethod("setZ", float.class);

setZ.invoke(unityPlayer, v);
} catch (NoSuchMethodException e) {}
Expand Down
29 changes: 29 additions & 0 deletions android/src/main/java/com/azesmwayreactnativeunity/UnityEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.azesmwayreactnativeunity;

import androidx.annotation.Nullable;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.Event;

public class UnityEvent extends Event<UnityEvent> {
private final String mEventName;
private final String mMessage;

public UnityEvent(String eventName, String message, int surfaceId, int viewTag) {
super(surfaceId, viewTag);
mEventName = eventName;
mMessage = message;
}

@Override public String getEventName() { return mEventName; }

@Override public boolean canCoalesce() { return false; } // events must not be dropped

@Nullable
@Override
protected WritableMap getEventData() {
WritableMap data = Arguments.createMap();
data.putString("message", mMessage);
return data;
}
}
5 changes: 5 additions & 0 deletions ios/RNUnityView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ - (instancetype)initWithFrame:(CGRect)frame {
gridViewEventEmitter->onUnityMessage(event);
}
};

// Start Unity immediately, don't wait for updateProps
if (![self unityIsInitialized]) {
[self initUnityModule];
}
}

return self;
Expand Down
Loading