@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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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, "
|
|
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.
|
|
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
|
|
20
|
-
*
|
|
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
|
|
59
|
+
* Returns the UnityPlayer's FrameLayout for embedding.
|
|
60
|
+
* UnityPlayerForActivityOrService creates its own rendering surface internally.
|
|
56
61
|
*/
|
|
57
|
-
val unityPlayerView:
|
|
58
|
-
get() =
|
|
62
|
+
val unityPlayerView: FrameLayout?
|
|
63
|
+
get() = unityPlayer?.frameLayout
|
|
59
64
|
|
|
60
|
-
fun initialize(activity: Activity) {
|
|
61
|
-
if (isInitialized)
|
|
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
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
+
isReady = false
|
|
150
225
|
}
|
|
151
226
|
|
|
152
227
|
override fun onUnityPlayerQuitted() {
|
|
153
228
|
Log.i(TAG, "onUnityPlayerQuitted")
|
|
154
229
|
unityPlayer = null
|
|
155
|
-
|
|
230
|
+
isReady = false
|
|
156
231
|
}
|
|
157
232
|
|
|
158
233
|
// NativeCallProxy.MessageListener (Unity -> RN)
|