@dolami-inc/react-native-expo-unity 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,13 +33,17 @@ class ExpoUnityView(context: Context, appContext: AppContext) : ExpoView(context
33
33
  }
34
34
  }
35
35
 
36
- if (bridge.isInitialized) {
37
- // Unity already createdattach the view to this container
38
- bridge.attachToContainer(this)
36
+ if (bridge.isReady) {
37
+ // Unity already initializedreparent into this container
38
+ bridge.reparentInto(this)
39
39
  } else {
40
- // Create Unity player, then attach the view once ready
40
+ // Initialize Unity, then reparent once engine is ready.
41
+ // Use postDelayed to give the engine time to boot before
42
+ // reparenting (avoids window detach timeout).
41
43
  bridge.initialize(activity) {
42
- bridge.attachToContainer(this)
44
+ postDelayed({
45
+ bridge.reparentInto(this)
46
+ }, 3000)
43
47
  }
44
48
  }
45
49
  }
@@ -47,7 +51,7 @@ class ExpoUnityView(context: Context, appContext: AppContext) : ExpoView(context
47
51
  override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
48
52
  super.onWindowFocusChanged(hasWindowFocus)
49
53
  val bridge = UnityBridge.getInstance()
50
- if (!bridge.isInitialized) return
54
+ if (!bridge.isReady) return
51
55
  bridge.unityPlayer?.windowFocusChanged(hasWindowFocus)
52
56
  }
53
57
 
@@ -57,7 +61,7 @@ class ExpoUnityView(context: Context, appContext: AppContext) : ExpoView(context
57
61
 
58
62
  if (bridge.isInitialized) {
59
63
  if (autoUnloadOnUnmount) {
60
- bridge.detachFromContainer()
64
+ bridge.detachView()
61
65
  bridge.unload()
62
66
  Log.i(TAG, "Detached and unloaded (view detached)")
63
67
  } else {
@@ -18,9 +18,9 @@ import com.unity3d.player.UnityPlayerForActivityOrService
18
18
  * Singleton managing the UnityPlayer lifecycle.
19
19
  * Android equivalent of ios/UnityBridge.mm.
20
20
  *
21
- * Creates the Unity player and lets the ExpoUnityView add the
22
- * FrameLayout directly no background parking, since Unity 6's
23
- * window management times out when reparenting from a background view.
21
+ * Unity 6's engine only boots when the view is in the Activity's content
22
+ * view hierarchy. We park the view at MATCH_PARENT behind everything (Z=-1)
23
+ * to let the engine start, then reparent into the React Native container.
24
24
  */
25
25
  class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCallProxy.MessageListener {
26
26
 
@@ -48,19 +48,18 @@ class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCal
48
48
  /** Tracked here (not on the Module) so it survives module recreation. */
49
49
  var wasRunningBeforeBackground: Boolean = false
50
50
 
51
+ var isReady: Boolean = false
52
+ private set
53
+
51
54
  val isInitialized: Boolean
52
55
  get() = unityPlayer != null
53
56
 
54
- /**
55
- * Returns the UnityPlayer's FrameLayout for embedding.
56
- * UnityPlayerForActivityOrService creates its own rendering surface internally.
57
- */
58
57
  val unityPlayerView: FrameLayout?
59
58
  get() = unityPlayer?.frameLayout
60
59
 
61
60
  /**
62
- * Creates the UnityPlayer. The caller is responsible for adding
63
- * [unityPlayerView] to the view hierarchy immediately after [onReady] fires.
61
+ * Creates the Unity player, parks it in the Activity's content view
62
+ * (behind everything) to let the engine start, then fires [onReady].
64
63
  */
65
64
  fun initialize(activity: Activity, onReady: (() -> Unit)? = null) {
66
65
  if (isInitialized) {
@@ -70,10 +69,8 @@ class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCal
70
69
 
71
70
  val runInit = Runnable {
72
71
  try {
73
- // Set RGBA_8888 format for proper rendering
74
72
  activity.window.setFormat(PixelFormat.RGBA_8888)
75
73
 
76
- // Save fullscreen state before Unity potentially changes it
77
74
  val flags = activity.window.attributes.flags
78
75
  val wasFullScreen = (flags and WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0
79
76
 
@@ -83,12 +80,31 @@ class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCal
83
80
  NativeCallProxy.registerListener(this)
84
81
  Log.i(TAG, "Unity player created")
85
82
 
86
- // Restore fullscreen state if Unity changed it
83
+ // Park in Activity's content view at full size but behind
84
+ // everything. Unity's engine only starts when the view is
85
+ // in the Activity's window hierarchy.
86
+ val frame = player.frameLayout
87
+ frame.z = -1f
88
+ activity.addContentView(frame, ViewGroup.LayoutParams(
89
+ ViewGroup.LayoutParams.MATCH_PARENT,
90
+ ViewGroup.LayoutParams.MATCH_PARENT
91
+ ))
92
+ Log.i(TAG, "Unity view parked in Activity (background)")
93
+
94
+ // Start the rendering pipeline
95
+ player.windowFocusChanged(true)
96
+ frame.requestFocus()
97
+ player.resume()
98
+
99
+ // Restore fullscreen state
87
100
  if (!wasFullScreen) {
88
101
  activity.window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
89
102
  activity.window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
90
103
  }
91
104
 
105
+ isReady = true
106
+ Log.i(TAG, "Unity initialized and ready")
107
+
92
108
  onReady?.invoke()
93
109
  } catch (e: Exception) {
94
110
  Log.e(TAG, "Failed to initialize Unity", e)
@@ -103,42 +119,46 @@ class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCal
103
119
  }
104
120
 
105
121
  /**
106
- * Adds the Unity FrameLayout to the given container and starts rendering.
107
- * Must be called after [initialize] completes.
122
+ * Moves the Unity view from the Activity background into the given
123
+ * container. Called when the React Native component is ready to show Unity.
108
124
  */
109
- fun attachToContainer(container: ViewGroup) {
125
+ fun reparentInto(container: ViewGroup) {
110
126
  val frame = unityPlayerView ?: run {
111
127
  Log.w(TAG, "Unity player view not available")
112
128
  return
113
129
  }
114
130
 
115
- // Remove from current parent if any
116
- (frame.parent as? ViewGroup)?.removeView(frame)
131
+ // Remove from Activity's content view
132
+ (frame.parent as? ViewGroup)?.let { parent ->
133
+ parent.endViewTransition(frame)
134
+ parent.removeView(frame)
135
+ }
117
136
 
118
- val layoutParams = FrameLayout.LayoutParams(
137
+ // Reset Z and add to the React Native container
138
+ frame.z = 0f
139
+ container.addView(frame, 0, FrameLayout.LayoutParams(
119
140
  FrameLayout.LayoutParams.MATCH_PARENT,
120
141
  FrameLayout.LayoutParams.MATCH_PARENT
121
- )
122
- container.addView(frame, 0, layoutParams)
123
- Log.i(TAG, "Unity view attached to container")
124
-
125
- // Kick-start rendering after the view is in the hierarchy.
126
- // Use post to let the layout pass complete first.
127
- frame.post {
128
- unityPlayer?.windowFocusChanged(true)
129
- frame.requestFocus()
130
- unityPlayer?.resume()
131
- Log.i(TAG, "Rendering started")
132
- }
142
+ ))
143
+
144
+ // Re-kick rendering after reparenting
145
+ unityPlayer?.windowFocusChanged(true)
146
+ frame.requestFocus()
147
+ unityPlayer?.resume()
148
+
149
+ Log.i(TAG, "Unity view reparented into container")
133
150
  }
134
151
 
135
152
  /**
136
- * Detaches the Unity view from its current parent without destroying it.
153
+ * Detaches the Unity view from its current parent.
137
154
  */
138
- fun detachFromContainer() {
155
+ fun detachView() {
139
156
  val frame = unityPlayerView ?: return
140
- (frame.parent as? ViewGroup)?.removeView(frame)
141
- Log.i(TAG, "Unity view detached from container")
157
+ (frame.parent as? ViewGroup)?.let { parent ->
158
+ parent.endViewTransition(frame)
159
+ parent.removeView(frame)
160
+ }
161
+ Log.i(TAG, "Unity view detached")
142
162
  }
143
163
 
144
164
  fun sendMessage(gameObject: String, methodName: String, message: String) {
@@ -164,6 +184,7 @@ class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCal
164
184
 
165
185
  fun unload() {
166
186
  if (!isInitialized) return
187
+ isReady = false
167
188
  Log.i(TAG, "unload called")
168
189
  val action = Runnable {
169
190
  unityPlayer?.unload()
@@ -181,11 +202,13 @@ class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCal
181
202
  override fun onUnityPlayerUnloaded() {
182
203
  Log.i(TAG, "onUnityPlayerUnloaded")
183
204
  unityPlayer = null
205
+ isReady = false
184
206
  }
185
207
 
186
208
  override fun onUnityPlayerQuitted() {
187
209
  Log.i(TAG, "onUnityPlayerQuitted")
188
210
  unityPlayer = null
211
+ isReady = false
189
212
  }
190
213
 
191
214
  // NativeCallProxy.MessageListener (Unity -> RN)
@@ -72,6 +72,13 @@ class ExpoUnityView: ExpoView {
72
72
  return
73
73
  }
74
74
 
75
+ // Wrap window manipulation in an explicit CA transaction to ensure all
76
+ // layer tree changes from the window swap are committed atomically.
77
+ // Without this, pending SDWebImage callbacks in the same main queue
78
+ // drain cycle can trigger CA::Transaction::commit() on a stale context,
79
+ // crashing in _dispatch_async_f_slow.
80
+ CATransaction.begin()
81
+
75
82
  if let unityWindow = UnityBridge.shared().unityWindow() {
76
83
  if let myWindow = self.window, unityWindow != myWindow {
77
84
  unityWindow.isHidden = true
@@ -84,6 +91,8 @@ class ExpoUnityView: ExpoView {
84
91
  if unityView.superview != self {
85
92
  self.addSubview(unityView)
86
93
  }
94
+
95
+ CATransaction.commit()
87
96
  }
88
97
 
89
98
  override func layoutSubviews() {
@@ -40,6 +40,7 @@ static UnityBridge *_shared = nil;
40
40
 
41
41
  #import <UnityFramework/UnityFramework.h>
42
42
  #import <UnityFramework/NativeCallProxy.h>
43
+ #import <QuartzCore/CATransaction.h>
43
44
 
44
45
  #ifdef DEBUG
45
46
  #include <mach-o/ldsyms.h>
@@ -105,12 +106,17 @@ static UnityBridge *_shared = nil;
105
106
 
106
107
  self.ufwInternal = ufw;
107
108
 
108
- // Hide Unity's window — we embed its rootView in our own view
109
+ // Hide Unity's window — we embed its rootView in our own view.
110
+ // Wrap in CATransaction to commit layer tree changes atomically
111
+ // before other main queue callbacks (e.g. SDWebImage) can fire
112
+ // and attempt CA commits on a stale rendering context.
113
+ [CATransaction begin];
109
114
  UIWindow *unityWindow = [ufw appController].window;
110
115
  if (unityWindow) {
111
116
  unityWindow.hidden = YES;
112
117
  unityWindow.userInteractionEnabled = NO;
113
118
  }
119
+ [CATransaction commit];
114
120
 
115
121
  NSLog(@"[ExpoUnity] Unity initialized");
116
122
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dolami-inc/react-native-expo-unity",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Unity as a Library (UaaL) bridge for React Native / Expo",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",