@dolami-inc/react-native-expo-unity 0.4.4 → 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
@@ -18,9 +16,6 @@ class ExpoUnityView(context: Context, appContext: AppContext) : ExpoView(context
18
16
  var autoUnloadOnUnmount: Boolean = true
19
17
 
20
18
  init {
21
- // post {} dispatches to main thread. setupUnity -> initialize runs
22
- // synchronously when already on main thread, so mountUnityView sees the
23
- // fully-initialized player without a race condition.
24
19
  post { setupUnity() }
25
20
  }
26
21
 
@@ -32,35 +27,32 @@ class ExpoUnityView(context: Context, appContext: AppContext) : ExpoView(context
32
27
 
33
28
  val bridge = UnityBridge.getInstance()
34
29
 
35
- if (!bridge.isInitialized) {
36
- bridge.initialize(activity)
37
- }
38
-
39
30
  bridge.onMessage = { message ->
40
31
  post {
41
32
  onUnityMessage(mapOf("message" to message))
42
33
  }
43
34
  }
44
35
 
45
- mountUnityView()
46
- }
47
-
48
- private fun mountUnityView() {
49
- val playerView = UnityBridge.getInstance().unityPlayerView ?: run {
50
- Log.w(TAG, "Unity player view not available yet")
51
- 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
+ }
52
48
  }
49
+ }
53
50
 
54
- // Remove from previous parent if needed
55
- (playerView.parent as? ViewGroup)?.removeView(playerView)
56
-
57
- addView(
58
- playerView,
59
- FrameLayout.LayoutParams(
60
- FrameLayout.LayoutParams.MATCH_PARENT,
61
- FrameLayout.LayoutParams.MATCH_PARENT
62
- )
63
- )
51
+ override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
52
+ super.onWindowFocusChanged(hasWindowFocus)
53
+ val bridge = UnityBridge.getInstance()
54
+ if (!bridge.isReady) return
55
+ bridge.unityPlayer?.windowFocusChanged(hasWindowFocus)
64
56
  }
65
57
 
66
58
  override fun onDetachedFromWindow() {
@@ -72,8 +64,10 @@ class ExpoUnityView(context: Context, appContext: AppContext) : ExpoView(context
72
64
  bridge.unload()
73
65
  Log.i(TAG, "Auto-unloaded (view detached)")
74
66
  } else {
67
+ // Park Unity in the background instead of unloading
68
+ bridge.parkUnityViewInBackground()
75
69
  bridge.setPaused(true)
76
- Log.i(TAG, "Auto-paused on unmount (autoUnloadOnUnmount=false)")
70
+ Log.i(TAG, "Parked in background (autoUnloadOnUnmount=false)")
77
71
  }
78
72
  }
79
73
 
@@ -1,23 +1,27 @@
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
- import android.view.SurfaceView
8
8
  import android.view.View
9
+ import android.view.ViewGroup
10
+ import android.view.WindowManager
9
11
  import android.widget.FrameLayout
10
12
  import com.expounity.bridge.NativeCallProxy
11
13
  import com.unity3d.player.IUnityPlayerLifecycleEvents
12
14
  import com.unity3d.player.UnityPlayer
13
- import com.unity3d.player.UnityPlayerForGameActivity
15
+ import com.unity3d.player.UnityPlayerForActivityOrService
14
16
 
15
17
  /**
16
18
  * Singleton managing the UnityPlayer lifecycle.
17
19
  * Android equivalent of ios/UnityBridge.mm.
18
20
  *
19
- * Unity 6+ uses GameActivity mode. UnityPlayerForGameActivity requires
20
- * a FrameLayout container and SurfaceView that it renders into.
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.
21
25
  */
22
26
  class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCallProxy.MessageListener {
23
27
 
@@ -40,60 +44,66 @@ class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCal
40
44
  var unityPlayer: UnityPlayer? = null
41
45
  private set
42
46
 
43
- /** The FrameLayout container that Unity renders into. */
44
- private var containerView: FrameLayout? = null
45
-
46
47
  var onMessage: ((String) -> Unit)? = null
47
48
 
48
49
  /** Tracked here (not on the Module) so it survives module recreation. */
49
50
  var wasRunningBeforeBackground: Boolean = false
50
51
 
52
+ var isReady: Boolean = false
53
+ private set
54
+
51
55
  val isInitialized: Boolean
52
56
  get() = unityPlayer != null
53
57
 
54
58
  /**
55
- * Returns the FrameLayout container that holds Unity's rendering surface.
59
+ * Returns the UnityPlayer's FrameLayout for embedding.
60
+ * UnityPlayerForActivityOrService creates its own rendering surface internally.
56
61
  */
57
- val unityPlayerView: View?
58
- get() = containerView
62
+ val unityPlayerView: FrameLayout?
63
+ get() = unityPlayer?.frameLayout
59
64
 
60
- fun initialize(activity: Activity) {
61
- if (isInitialized) return
65
+ fun initialize(activity: Activity, onReady: (() -> Unit)? = null) {
66
+ if (isInitialized) {
67
+ onReady?.invoke()
68
+ return
69
+ }
62
70
 
63
71
  val runInit = Runnable {
64
72
  try {
65
- // Load native libraries required by Unity's GameActivity mode.
66
- // Unity's own launcher does this via android.app.lib_name metadata,
67
- // but since we bypass GameActivity lifecycle we load manually.
68
- System.loadLibrary("game")
69
-
70
- // Create the container and surface that Unity will render into
71
- val frameLayout = FrameLayout(activity)
72
- frameLayout.layoutParams = FrameLayout.LayoutParams(
73
- FrameLayout.LayoutParams.MATCH_PARENT,
74
- FrameLayout.LayoutParams.MATCH_PARENT
75
- )
76
-
77
- val surfaceView = SurfaceView(activity)
78
- frameLayout.addView(
79
- surfaceView,
80
- FrameLayout.LayoutParams(
81
- FrameLayout.LayoutParams.MATCH_PARENT,
82
- FrameLayout.LayoutParams.MATCH_PARENT
83
- )
84
- )
85
-
86
- val player = UnityPlayerForGameActivity(activity, frameLayout, surfaceView, this)
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
+
80
+ val player = UnityPlayerForActivityOrService(activity, this)
87
81
  unityPlayer = player
88
- containerView = frameLayout
89
82
 
90
- // Start Unity rendering
91
- player.resume()
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
92
93
  player.windowFocusChanged(true)
94
+ player.frameLayout?.requestFocus()
95
+ player.resume()
93
96
 
94
- NativeCallProxy.registerListener(this)
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
+ }
95
102
 
96
- Log.i(TAG, "Unity initialized (GameActivity mode)")
103
+ isReady = true
104
+ Log.i(TAG, "Unity initialized and ready")
105
+
106
+ onReady?.invoke()
97
107
  } catch (e: Exception) {
98
108
  Log.e(TAG, "Failed to initialize Unity", e)
99
109
  }
@@ -106,6 +116,70 @@ class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCal
106
116
  }
107
117
  }
108
118
 
119
+ /**
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.
143
+ */
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")
181
+ }
182
+
109
183
  fun sendMessage(gameObject: String, methodName: String, message: String) {
110
184
  if (!isInitialized) return
111
185
  UnityPlayer.UnitySendMessage(gameObject, methodName, message)
@@ -129,6 +203,7 @@ class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCal
129
203
 
130
204
  fun unload() {
131
205
  if (!isInitialized) return
206
+ isReady = false
132
207
  Log.i(TAG, "unload called")
133
208
  val action = Runnable {
134
209
  unityPlayer?.unload()
@@ -146,13 +221,13 @@ class UnityBridge private constructor() : IUnityPlayerLifecycleEvents, NativeCal
146
221
  override fun onUnityPlayerUnloaded() {
147
222
  Log.i(TAG, "onUnityPlayerUnloaded")
148
223
  unityPlayer = null
149
- containerView = null
224
+ isReady = false
150
225
  }
151
226
 
152
227
  override fun onUnityPlayerQuitted() {
153
228
  Log.i(TAG, "onUnityPlayerQuitted")
154
229
  unityPlayer = null
155
- containerView = null
230
+ isReady = false
156
231
  }
157
232
 
158
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.4",
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",