1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-11 13:10:58 +00:00

Add support for running hybrid apps from the XR editor

This commit is contained in:
Fredia Huya-Kouadio
2025-03-09 16:43:46 -07:00
parent 019ab8745f
commit 09f5be761c
20 changed files with 438 additions and 62 deletions

View File

@@ -60,6 +60,22 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Intent filter used to intercept hybrid PANEL launch for the current editor project, and route it
properly through the editor 'run' logic (e.g: debugger setup) -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="org.godotengine.xr.hybrid.PANEL" />
</intent-filter>
<!-- Intent filter used to intercept hybrid IMMERSIVE launch for the current editor project, and route it
properly through the editor 'run' logic (e.g: debugger setup) -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="org.godotengine.xr.hybrid.IMMERSIVE" />
</intent-filter>
</activity>
<activity
android:name=".GodotGame"
@@ -101,8 +117,7 @@
android:autoRemoveFromRecents="true"
android:screenOrientation="landscape"
android:resizeableActivity="false"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
</activity>
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" />
<!--
We remove this meta-data originating from the vendors plugin as we only need the loader for

View File

@@ -60,14 +60,19 @@ import org.godotengine.editor.utils.verifyApk
import org.godotengine.godot.BuildConfig
import org.godotengine.godot.GodotActivity
import org.godotengine.godot.GodotLib
import org.godotengine.godot.editor.utils.EditorUtils
import org.godotengine.godot.editor.utils.GameMenuUtils
import org.godotengine.godot.editor.utils.GameMenuUtils.GameEmbedMode
import org.godotengine.godot.editor.utils.GameMenuUtils.fetchGameEmbedMode
import org.godotengine.godot.error.Error
import org.godotengine.godot.utils.DialogUtils
import org.godotengine.godot.utils.GameMenuUtils
import org.godotengine.godot.utils.GameMenuUtils.GameEmbedMode
import org.godotengine.godot.utils.GameMenuUtils.fetchGameEmbedMode
import org.godotengine.godot.utils.PermissionsUtil
import org.godotengine.godot.utils.ProcessPhoenix
import org.godotengine.godot.utils.isNativeXRDevice
import org.godotengine.godot.xr.HybridMode
import org.godotengine.godot.xr.getHybridAppLaunchMode
import org.godotengine.godot.xr.HYBRID_APP_PANEL_CATEGORY
import org.godotengine.godot.xr.HYBRID_APP_IMMERSIVE_CATEGORY
import kotlin.math.min
/**
@@ -98,6 +103,8 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
private const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager"
private const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p"
internal const val XR_MODE_ARG = "--xr-mode"
private const val SCENE_ARG = "--scene"
private const val PATH_ARG = "--path"
// Info for the various classes used by the editor.
internal val EDITOR_MAIN_INFO = EditorWindowInfo(GodotEditor::class.java, 777, "")
@@ -236,6 +243,50 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
setupGameMenuBar()
}
override fun onNewIntent(newIntent: Intent) {
if (newIntent.hasCategory(HYBRID_APP_PANEL_CATEGORY) || newIntent.hasCategory(HYBRID_APP_IMMERSIVE_CATEGORY)) {
val params = newIntent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
Log.d(TAG, "Received hybrid transition intent $newIntent with parameters ${params.contentToString()}")
// Override EXTRA_NEW_LAUNCH so the editor is not restarted
newIntent.putExtra(EXTRA_NEW_LAUNCH, false)
godot?.runOnRenderThread {
// Look for the scene and xr-mode arguments
var scene = ""
var xrMode = XR_MODE_DEFAULT
var path = ""
if (params != null) {
val sceneIndex = params.indexOf(SCENE_ARG)
if (sceneIndex != -1 && sceneIndex + 1 < params.size) {
scene = params[sceneIndex +1]
}
val xrModeIndex = params.indexOf(XR_MODE_ARG)
if (xrModeIndex != -1 && xrModeIndex + 1 < params.size) {
xrMode = params[xrModeIndex + 1]
}
val pathIndex = params.indexOf(PATH_ARG)
if (pathIndex != -1 && pathIndex + 1 < params.size) {
path = params[pathIndex + 1]
}
}
val sceneArgs = mutableSetOf(XR_MODE_ARG, xrMode).apply {
if (path.isNotEmpty() && scene.isEmpty()) {
add(PATH_ARG)
add(path)
}
}
Log.d(TAG, "Running scene $scene with arguments: $sceneArgs")
EditorUtils.runScene(scene, sceneArgs.toTypedArray())
}
}
super.onNewIntent(newIntent)
}
protected open fun shouldShowGameMenuBar() = gameMenuContainer != null
private fun setupGameMenuBar() {
@@ -327,26 +378,41 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
}
}
return if (hasEditor) {
EDITOR_MAIN_INFO
} else {
// Launching a game.
val openxrEnabled = xrMode == XR_MODE_ON ||
(xrMode == XR_MODE_DEFAULT && GodotLib.getGlobal("xr/openxr/enabled").toBoolean())
if (openxrEnabled && isNativeXRDevice(applicationContext)) {
XR_RUN_GAME_INFO
} else {
if (godot?.isProjectManagerHint() == true || isNativeXRDevice(applicationContext)) {
if (hasEditor) {
return EDITOR_MAIN_INFO
}
// Launching a game.
if (isNativeXRDevice(applicationContext)) {
if (xrMode == XR_MODE_ON) {
return XR_RUN_GAME_INFO
}
if ((xrMode == XR_MODE_DEFAULT && GodotLib.getGlobal("xr/openxr/enabled").toBoolean())) {
val hybridLaunchMode = getHybridAppLaunchMode()
return if (hybridLaunchMode == HybridMode.PANEL) {
RUN_GAME_INFO
} else {
val resolvedEmbedMode = resolveGameEmbedModeIfNeeded(gameEmbedMode)
if (resolvedEmbedMode == GameEmbedMode.DISABLED) {
RUN_GAME_INFO
} else {
EMBEDDED_RUN_GAME_INFO
}
XR_RUN_GAME_INFO
}
}
// Native XR devices don't support embed mode yet.
return RUN_GAME_INFO
}
// Project manager doesn't support embed mode.
if (godot?.isProjectManagerHint() == true) {
return RUN_GAME_INFO
}
// Check for embed mode launch.
val resolvedEmbedMode = resolveGameEmbedModeIfNeeded(gameEmbedMode)
return if (resolvedEmbedMode == GameEmbedMode.DISABLED) {
RUN_GAME_INFO
} else {
EMBEDDED_RUN_GAME_INFO
}
}
@@ -626,6 +692,7 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
return verifyApk(godot.fileAccessHandler, apkPath)
}
@CallSuper
override fun supportsFeature(featureTag: String): Boolean {
if (featureTag == "xr_editor") {
return isNativeXRDevice(applicationContext)
@@ -639,11 +706,12 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
return BuildConfig.FLAVOR == "picoos"
}
return false
return super.supportsFeature(featureTag)
}
internal fun onEditorConnected(connectedEditorId: Int) {
when (connectedEditorId) {
internal fun onEditorConnected(editorId: Int) {
Log.d(TAG, "Editor $editorId connected!")
when (editorId) {
EMBEDDED_RUN_GAME_INFO.windowId, RUN_GAME_INFO.windowId -> {
runOnUiThread {
embeddedGameViewContainerWindow?.isVisible = false
@@ -652,12 +720,16 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
XR_RUN_GAME_INFO.windowId -> {
runOnUiThread {
updateEmbeddedGameView(true, false)
updateEmbeddedGameView(gameRunning = true, gameEmbedded = false)
}
}
}
}
internal fun onEditorDisconnected(editorId: Int) {
Log.d(TAG, "Editor $editorId disconnected!")
}
private fun updateEmbeddedGameView(gameRunning: Boolean, gameEmbedded: Boolean) {
if (gameRunning) {
embeddedGameStateLabel?.apply {

View File

@@ -35,9 +35,11 @@ import android.util.Log
import androidx.annotation.CallSuper
import org.godotengine.godot.Godot
import org.godotengine.godot.GodotLib
import org.godotengine.godot.utils.GameMenuUtils
import org.godotengine.godot.editor.utils.GameMenuUtils
import org.godotengine.godot.utils.PermissionsUtil
import org.godotengine.godot.utils.ProcessPhoenix
import org.godotengine.godot.xr.HYBRID_APP_FEATURE
import org.godotengine.godot.xr.isHybridAppEnabled
/**
* Base class for the Godot play windows.
@@ -101,4 +103,14 @@ abstract class BaseGodotGame: GodotEditor() {
}
protected open fun getEditorGameEmbedMode() = GameMenuUtils.GameEmbedMode.AUTO
@CallSuper
override fun supportsFeature(featureTag: String): Boolean {
if (HYBRID_APP_FEATURE == featureTag) {
// Check if hybrid is enabled
return isHybridAppEnabled()
}
return super.supportsFeature(featureTag)
}
}

View File

@@ -104,7 +104,9 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
MSG_REGISTER_MESSENGER -> {
val editorId = msg.arg1
val messenger = msg.replyTo
registerMessenger(editorId, messenger)
registerMessenger(editorId, messenger) {
editor.onEditorDisconnected(editorId)
}
}
MSG_DISPATCH_GAME_MENU_ACTION -> {
@@ -211,8 +213,8 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
} else if (messenger.binder.isBinderAlive) {
messenger.binder.linkToDeath({
Log.v(TAG, "Removing messenger for $editorId")
cleanEditorConnection(editorId)
messengerDeathCallback?.run()
cleanEditorConnection(editorId)
}, 0)
editorConnectionsInfos[editorId] = EditorConnectionInfo(messenger)
editor.onEditorConnected(editorId)
@@ -234,7 +236,8 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
/**
* Utility method to register a [Messenger] attached to this handler with a host.
*
* This is done so that the host can send request to the editor instance attached to this handle.
* This is done so that the host can send request (e.g: force-quit when the host exits) to the editor instance
* attached to this handle.
*
* Note that this is only done when the editor instance is internal (not exported) to prevent
* arbitrary apps from having the ability to send requests.

View File

@@ -37,12 +37,17 @@ import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.annotation.CallSuper
import androidx.core.view.isVisible
import org.godotengine.editor.embed.GameMenuFragment
import org.godotengine.godot.utils.GameMenuUtils
import org.godotengine.godot.GodotLib
import org.godotengine.godot.editor.utils.GameMenuUtils
import org.godotengine.godot.utils.ProcessPhoenix
import org.godotengine.godot.utils.isHorizonOSDevice
import org.godotengine.godot.utils.isNativeXRDevice
import org.godotengine.godot.xr.HYBRID_APP_PANEL_FEATURE
import org.godotengine.godot.xr.XRMode
import org.godotengine.godot.xr.isHybridAppEnabled
/**
* Drives the 'run project' window of the Godot Editor.
@@ -82,6 +87,18 @@ open class GodotGame : BaseGodotGame() {
}
}
override fun getCommandLine(): MutableList<String> {
val updatedArgs = super.getCommandLine()
if (!updatedArgs.contains(XRMode.REGULAR.cmdLineArg)) {
updatedArgs.add(XRMode.REGULAR.cmdLineArg)
}
if (!updatedArgs.contains(XR_MODE_ARG)) {
updatedArgs.add(XR_MODE_ARG)
updatedArgs.add("off")
}
return updatedArgs
}
override fun enterPiPMode() {
if (hasPiPSystemFeature()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -245,4 +262,19 @@ open class GodotGame : BaseGodotGame() {
expandGameMenuButton?.isVisible = shouldShowGameMenuBar() && isMenuBarCollapsable() && collapsed
}
@CallSuper
override fun supportsFeature(featureTag: String): Boolean {
if (HYBRID_APP_PANEL_FEATURE == featureTag) {
// Check if openxr is enabled
if (!GodotLib.getGlobal("xr/openxr/enabled").toBoolean()) {
return false
}
// Check if hybrid is enabled
return isHybridAppEnabled()
}
return super.supportsFeature(featureTag)
}
}

View File

@@ -40,7 +40,7 @@ import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
import android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
import org.godotengine.editor.GodotGame
import org.godotengine.editor.R
import org.godotengine.godot.utils.GameMenuUtils
import org.godotengine.godot.editor.utils.GameMenuUtils
/**
* Host the Godot game from the editor when the embedded mode is enabled.