@dolami-inc/react-native-expo-unity 0.4.5 → 0.5.0

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.
@@ -2,8 +2,6 @@ package expo.modules.unity
2
2
 
3
3
  import android.content.Context
4
4
  import android.util.Log
5
- import android.view.ViewGroup
6
- import android.widget.FrameLayout
7
5
  import expo.modules.kotlin.AppContext
8
6
  import expo.modules.kotlin.viewevent.EventDispatcher
9
7
  import expo.modules.kotlin.views.ExpoView
@@ -29,47 +27,32 @@ class ExpoUnityView(context: Context, appContext: AppContext) : ExpoView(context
29
27
 
30
28
  val bridge = UnityBridge.getInstance()
31
29
 
32
- if (!bridge.isInitialized) {
33
- bridge.initialize(activity)
34
- }
35
-
36
30
  bridge.onMessage = { message ->
37
31
  post {
38
32
  onUnityMessage(mapOf("message" to message))
39
33
  }
40
34
  }
41
35
 
42
- mountUnityView()
43
- }
44
-
45
- private fun mountUnityView() {
46
- val playerView = UnityBridge.getInstance().unityPlayerView ?: run {
47
- Log.w(TAG, "Unity player view not available yet")
48
- return
36
+ if (bridge.isReady) {
37
+ // Unity already initialized — just reparent the view into this container
38
+ bridge.addUnityViewToGroup(this)
39
+ Log.i(TAG, "Unity already ready, reparented view")
40
+ } else {
41
+ // Initialize Unity with a callback that reparents when ready
42
+ bridge.initialize(activity) {
43
+ post {
44
+ bridge.addUnityViewToGroup(this)
45
+ Log.i(TAG, "Unity ready, view reparented into container")
46
+ }
47
+ }
49
48
  }
50
-
51
- // Remove from previous parent if needed
52
- (playerView.parent as? ViewGroup)?.removeView(playerView)
53
-
54
- addView(
55
- playerView,
56
- FrameLayout.LayoutParams(
57
- FrameLayout.LayoutParams.MATCH_PARENT,
58
- FrameLayout.LayoutParams.MATCH_PARENT
59
- )
60
- )
61
-
62
- Log.i(TAG, "Unity view mounted")
63
49
  }
64
50
 
65
- override fun onAttachedToWindow() {
66
- super.onAttachedToWindow()
67
- // Start rendering after the view is in the window hierarchy,
68
- // so Unity's surface is properly connected to the display.
51
+ override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
52
+ super.onWindowFocusChanged(hasWindowFocus)
69
53
  val bridge = UnityBridge.getInstance()
70
- if (bridge.isInitialized) {
71
- bridge.startRendering()
72
- }
54
+ if (!bridge.isReady) return
55
+ bridge.unityPlayer?.windowFocusChanged(hasWindowFocus)
73
56
  }
74
57
 
75
58
  override fun onDetachedFromWindow() {
@@ -81,8 +64,10 @@ class ExpoUnityView(context: Context, appContext: AppContext) : ExpoView(context
81
64
  bridge.unload()
82
65
  Log.i(TAG, "Auto-unloaded (view detached)")
83
66
  } else {
67
+ // Park Unity in the background instead of unloading
68
+ bridge.parkUnityViewInBackground()
84
69
  bridge.setPaused(true)
85
- Log.i(TAG, "Auto-paused on unmount (autoUnloadOnUnmount=false)")
70
+ Log.i(TAG, "Parked in background (autoUnloadOnUnmount=false)")
86
71
  }
87
72
  }
88
73
 
@@ -1,10 +1,14 @@
1
1
  package expo.modules.unity
2
2
 
3
3
  import android.app.Activity
4
+ import android.graphics.PixelFormat
4
5
  import android.os.Handler
5
6
  import android.os.Looper
6
7
  import android.util.Log
7
8
  import android.view.View
9
+ import android.view.ViewGroup
10
+ import android.view.WindowManager
11
+ import android.widget.FrameLayout
8
12
  import com.expounity.bridge.NativeCallProxy
9
13
  import com.unity3d.player.IUnityPlayerLifecycleEvents
10
14
  import com.unity3d.player.UnityPlayer
@@ -14,8 +18,10 @@ import com.unity3d.player.UnityPlayerForActivityOrService
14
18
  * Singleton managing the UnityPlayer lifecycle.
15
19
  * Android equivalent of ios/UnityBridge.mm.
16
20
  *
17
- * Uses UnityPlayerForActivityOrService which manages its own rendering
18
- * surface internally suitable for UaaL (Unity as a Library) embedding.
21
+ * Uses a "background parking" pattern: Unity is always attached to the
22
+ * Activity's content view (at 1x1px, Z=-1) so it stays alive. When a
23
+ * React Native view wants to show Unity, we reparent the FrameLayout
24
+ * into that view. When it unmounts, we park it back in the background.
19
25
  */
20
26
  class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCallProxy.MessageListener {
21
27
 
@@ -43,6 +49,9 @@ class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCal
43
49
  /** Tracked here (not on the Module) so it survives module recreation. */
44
50
  var wasRunningBeforeBackground: Boolean = false
45
51
 
52
+ var isReady: Boolean = false
53
+ private set
54
+
46
55
  val isInitialized: Boolean
47
56
  get() = unityPlayer != null
48
57
 
@@ -50,20 +59,51 @@ class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCal
50
59
  * Returns the UnityPlayer's FrameLayout for embedding.
51
60
  * UnityPlayerForActivityOrService creates its own rendering surface internally.
52
61
  */
53
- val unityPlayerView: View?
62
+ val unityPlayerView: FrameLayout?
54
63
  get() = unityPlayer?.frameLayout
55
64
 
56
- fun initialize(activity: Activity) {
57
- if (isInitialized) return
65
+ fun initialize(activity: Activity, onReady: (() -> Unit)? = null) {
66
+ if (isInitialized) {
67
+ onReady?.invoke()
68
+ return
69
+ }
58
70
 
59
71
  val runInit = Runnable {
60
72
  try {
73
+ // Set RGBA_8888 format for proper rendering
74
+ activity.window.setFormat(PixelFormat.RGBA_8888)
75
+
76
+ // Save fullscreen state before Unity potentially changes it
77
+ val flags = activity.window.attributes.flags
78
+ val wasFullScreen = (flags and WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0
79
+
61
80
  val player = UnityPlayerForActivityOrService(activity, this)
62
81
  unityPlayer = player
63
82
 
64
83
  NativeCallProxy.registerListener(this)
84
+ Log.i(TAG, "Unity player created")
85
+
86
+ // Give Unity time to initialize its rendering pipeline
87
+ Thread.sleep(1000)
88
+
89
+ // Park the Unity view in the background (1x1px, behind everything)
90
+ addUnityViewToBackground(activity)
91
+
92
+ // Kick-start rendering
93
+ player.windowFocusChanged(true)
94
+ player.frameLayout?.requestFocus()
95
+ player.resume()
65
96
 
66
- Log.i(TAG, "Unity initialized")
97
+ // Restore fullscreen state if Unity changed it
98
+ if (!wasFullScreen) {
99
+ activity.window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
100
+ activity.window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
101
+ }
102
+
103
+ isReady = true
104
+ Log.i(TAG, "Unity initialized and ready")
105
+
106
+ onReady?.invoke()
67
107
  } catch (e: Exception) {
68
108
  Log.e(TAG, "Failed to initialize Unity", e)
69
109
  }
@@ -77,14 +117,67 @@ class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCal
77
117
  }
78
118
 
79
119
  /**
80
- * Must be called after the Unity view is attached to the window hierarchy.
81
- * Triggers Unity's rendering pipeline.
120
+ * Parks the Unity view in the Activity's content view at 1x1 pixels
121
+ * behind all other views (Z=-1). This keeps Unity alive but invisible.
122
+ */
123
+ private fun addUnityViewToBackground(activity: Activity) {
124
+ val frame = unityPlayerView ?: return
125
+
126
+ // Remove from current parent if any
127
+ (frame.parent as? ViewGroup)?.let { parent ->
128
+ parent.endViewTransition(frame)
129
+ parent.removeView(frame)
130
+ }
131
+
132
+ frame.z = -1f
133
+
134
+ val layoutParams = ViewGroup.LayoutParams(1, 1)
135
+ activity.addContentView(frame, layoutParams)
136
+ Log.i(TAG, "Unity view parked in background")
137
+ }
138
+
139
+ /**
140
+ * Moves the Unity view from wherever it currently is into the
141
+ * specified ViewGroup with MATCH_PARENT layout. Called when the
142
+ * React Native component mounts.
82
143
  */
83
- fun startRendering() {
84
- val player = unityPlayer ?: return
85
- player.resume()
86
- player.windowFocusChanged(true)
87
- Log.i(TAG, "Rendering started")
144
+ fun addUnityViewToGroup(group: ViewGroup) {
145
+ val frame = unityPlayerView ?: return
146
+
147
+ // Remove from current parent
148
+ (frame.parent as? ViewGroup)?.removeView(frame)
149
+
150
+ val layoutParams = ViewGroup.LayoutParams(
151
+ ViewGroup.LayoutParams.MATCH_PARENT,
152
+ ViewGroup.LayoutParams.MATCH_PARENT
153
+ )
154
+ group.addView(frame, 0, layoutParams)
155
+
156
+ unityPlayer?.windowFocusChanged(true)
157
+ frame.requestFocus()
158
+ unityPlayer?.resume()
159
+
160
+ Log.i(TAG, "Unity view moved to visible container")
161
+ }
162
+
163
+ /**
164
+ * Parks the Unity view back to the background. Called when the
165
+ * React Native component unmounts.
166
+ */
167
+ fun parkUnityViewInBackground() {
168
+ val frame = unityPlayerView ?: return
169
+ val activity = frame.context as? Activity ?: return
170
+
171
+ (frame.parent as? ViewGroup)?.let { parent ->
172
+ parent.endViewTransition(frame)
173
+ parent.removeView(frame)
174
+ }
175
+
176
+ frame.z = -1f
177
+
178
+ val layoutParams = ViewGroup.LayoutParams(1, 1)
179
+ activity.addContentView(frame, layoutParams)
180
+ Log.i(TAG, "Unity view parked back to background")
88
181
  }
89
182
 
90
183
  fun sendMessage(gameObject: String, methodName: String, message: String) {
@@ -110,6 +203,7 @@ class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCal
110
203
 
111
204
  fun unload() {
112
205
  if (!isInitialized) return
206
+ isReady = false
113
207
  Log.i(TAG, "unload called")
114
208
  val action = Runnable {
115
209
  unityPlayer?.unload()
@@ -127,11 +221,13 @@ class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCal
127
221
  override fun onUnityPlayerUnloaded() {
128
222
  Log.i(TAG, "onUnityPlayerUnloaded")
129
223
  unityPlayer = null
224
+ isReady = false
130
225
  }
131
226
 
132
227
  override fun onUnityPlayerQuitted() {
133
228
  Log.i(TAG, "onUnityPlayerQuitted")
134
229
  unityPlayer = null
230
+ isReady = false
135
231
  }
136
232
 
137
233
  // NativeCallProxy.MessageListener (Unity -> RN)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dolami-inc/react-native-expo-unity",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
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",