@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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
66
|
-
super.
|
|
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.
|
|
71
|
-
|
|
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, "
|
|
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
|
|
18
|
-
*
|
|
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:
|
|
62
|
+
val unityPlayerView: FrameLayout?
|
|
54
63
|
get() = unityPlayer?.frameLayout
|
|
55
64
|
|
|
56
|
-
fun initialize(activity: Activity) {
|
|
57
|
-
if (isInitialized)
|
|
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
|
-
|
|
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
|
-
*
|
|
81
|
-
*
|
|
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
|
|
84
|
-
val
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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)
|