Improve support for XR projects

This commit is contained in:
Fredia Huya-Kouadio 2024-04-20 10:24:11 -07:00
parent 835808ed8f
commit 9dc0543da7
22 changed files with 572 additions and 99 deletions

View File

@ -85,6 +85,7 @@ jobs:
run: | run: |
cd platform/android/java cd platform/android/java
./gradlew generateGodotEditor ./gradlew generateGodotEditor
./gradlew generateGodotMetaEditor
cd ../../.. cd ../../..
ls -l bin/android_editor_builds/ ls -l bin/android_editor_builds/

View File

@ -1037,7 +1037,8 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
if (arg == "--audio-driver" || if (arg == "--audio-driver" ||
arg == "--display-driver" || arg == "--display-driver" ||
arg == "--rendering-method" || arg == "--rendering-method" ||
arg == "--rendering-driver") { arg == "--rendering-driver" ||
arg == "--xr-mode") {
if (N) { if (N) {
forwardable_cli_arguments[CLI_SCOPE_TOOL].push_back(arg); forwardable_cli_arguments[CLI_SCOPE_TOOL].push_back(arg);
forwardable_cli_arguments[CLI_SCOPE_TOOL].push_back(N->get()); forwardable_cli_arguments[CLI_SCOPE_TOOL].push_back(N->get());

View File

@ -271,17 +271,14 @@ OpenXRAPI *OpenXRAPI::singleton = nullptr;
Vector<OpenXRExtensionWrapper *> OpenXRAPI::registered_extension_wrappers; Vector<OpenXRExtensionWrapper *> OpenXRAPI::registered_extension_wrappers;
bool OpenXRAPI::openxr_is_enabled(bool p_check_run_in_editor) { bool OpenXRAPI::openxr_is_enabled(bool p_check_run_in_editor) {
// @TODO we need an overrule switch so we can force enable openxr, i.e run "godot --openxr_enabled" if (XRServer::get_xr_mode() == XRServer::XRMODE_DEFAULT) {
if (Engine::get_singleton()->is_editor_hint() && p_check_run_in_editor) {
if (Engine::get_singleton()->is_editor_hint() && p_check_run_in_editor) { return GLOBAL_GET("xr/openxr/enabled.editor");
// Disabled for now, using XR inside of the editor we'll be working on during the coming months.
return false;
} else {
if (XRServer::get_xr_mode() == XRServer::XRMODE_DEFAULT) {
return GLOBAL_GET("xr/openxr/enabled");
} else { } else {
return XRServer::get_xr_mode() == XRServer::XRMODE_ON; return GLOBAL_GET("xr/openxr/enabled");
} }
} else {
return XRServer::get_xr_mode() == XRServer::XRMODE_ON;
} }
} }
@ -557,14 +554,11 @@ bool OpenXRAPI::create_instance() {
extension_ptrs.push_back(enabled_extensions[i].get_data()); extension_ptrs.push_back(enabled_extensions[i].get_data());
} }
// Get our project name
String project_name = GLOBAL_GET("application/config/name");
// Create our OpenXR instance // Create our OpenXR instance
XrApplicationInfo application_info{ XrApplicationInfo application_info{
"", // applicationName, we'll set this down below "Godot Engine", // applicationName, if we're running a game we'll update this down below.
1, // applicationVersion, we don't currently have this 1, // applicationVersion, we don't currently have this
"Godot Game Engine", // engineName "Godot Engine", // engineName
VERSION_MAJOR * 10000 + VERSION_MINOR * 100 + VERSION_PATCH, // engineVersion 4.0 -> 40000, 4.0.1 -> 40001, 4.1 -> 40100, etc. VERSION_MAJOR * 10000 + VERSION_MINOR * 100 + VERSION_PATCH, // engineVersion 4.0 -> 40000, 4.0.1 -> 40001, 4.1 -> 40100, etc.
XR_API_VERSION_1_0 // apiVersion XR_API_VERSION_1_0 // apiVersion
}; };
@ -588,7 +582,11 @@ bool OpenXRAPI::create_instance() {
extension_ptrs.ptr() // enabledExtensionNames extension_ptrs.ptr() // enabledExtensionNames
}; };
copy_string_to_char_buffer(project_name, instance_create_info.applicationInfo.applicationName, XR_MAX_APPLICATION_NAME_SIZE); // Get our project name
String project_name = GLOBAL_GET("application/config/name");
if (!project_name.is_empty()) {
copy_string_to_char_buffer(project_name, instance_create_info.applicationInfo.applicationName, XR_MAX_APPLICATION_NAME_SIZE);
}
XrResult result = xrCreateInstance(&instance_create_info, &instance); XrResult result = xrCreateInstance(&instance_create_info, &instance);
ERR_FAIL_COND_V_MSG(XR_FAILED(result), false, "Failed to create XR instance."); ERR_FAIL_COND_V_MSG(XR_FAILED(result), false, "Failed to create XR instance.");
@ -2583,7 +2581,6 @@ OpenXRAPI::OpenXRAPI() {
if (Engine::get_singleton()->is_editor_hint()) { if (Engine::get_singleton()->is_editor_hint()) {
// Enabled OpenXR in the editor? Adjust our settings for the editor // Enabled OpenXR in the editor? Adjust our settings for the editor
} else { } else {
// Load settings from project settings // Load settings from project settings
int form_factor_setting = GLOBAL_GET("xr/openxr/form_factor"); int form_factor_setting = GLOBAL_GET("xr/openxr/form_factor");

View File

@ -12,6 +12,7 @@ allprojects {
mavenCentral() mavenCentral()
gradlePluginPortal() gradlePluginPortal()
maven { url "https://plugins.gradle.org/m2/" } maven { url "https://plugins.gradle.org/m2/" }
maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/"}
// Godot user plugins custom maven repos // Godot user plugins custom maven repos
String[] mavenRepos = getGodotPluginsMavenRepos() String[] mavenRepos = getGodotPluginsMavenRepos()

View File

@ -11,6 +11,7 @@ pluginManagement {
mavenCentral() mavenCentral()
gradlePluginPortal() gradlePluginPortal()
maven { url "https://plugins.gradle.org/m2/" } maven { url "https://plugins.gradle.org/m2/" }
maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/"}
} }
} }

View File

@ -18,12 +18,14 @@ allprojects {
mavenCentral() mavenCentral()
gradlePluginPortal() gradlePluginPortal()
maven { url "https://plugins.gradle.org/m2/" } maven { url "https://plugins.gradle.org/m2/" }
maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/"}
} }
} }
ext { ext {
supportedAbis = ["arm32", "arm64", "x86_32", "x86_64"] supportedAbis = ["arm32", "arm64", "x86_32", "x86_64"]
supportedFlavors = ["editor", "template"] supportedFlavors = ["editor", "template"]
supportedEditorVendors = ["google", "meta"]
supportedFlavorsBuildTypes = [ supportedFlavorsBuildTypes = [
"editor": ["dev", "debug", "release"], "editor": ["dev", "debug", "release"],
"template": ["dev", "debug", "release"] "template": ["dev", "debug", "release"]
@ -92,15 +94,20 @@ def templateExcludedBuildTask() {
/** /**
* Generates the build tasks for the given flavor * Generates the build tasks for the given flavor
* @param flavor Must be one of the supported flavors ('template' / 'editor') * @param flavor Must be one of the supported flavors ('template' / 'editor')
* @param editorVendor Must be one of the supported editor vendors ('google' / 'meta')
*/ */
def generateBuildTasks(String flavor = "template") { def generateBuildTasks(String flavor = "template", String editorVendor = "google") {
if (!supportedFlavors.contains(flavor)) { if (!supportedFlavors.contains(flavor)) {
throw new GradleException("Invalid build flavor: $flavor") throw new GradleException("Invalid build flavor: $flavor")
} }
if (!supportedEditorVendors.contains(editorVendor)) {
throw new GradleException("Invalid editor vendor: $editorVendor")
}
String capitalizedEditorVendor = editorVendor.capitalize()
def buildTasks = [] def buildTasks = []
// Only build the apks and aar files for which we have native shared libraries unless we intend // Only build the binary files for which we have native shared libraries unless we intend
// to run the scons build tasks. // to run the scons build tasks.
boolean excludeSconsBuildTasks = excludeSconsBuildTasks() boolean excludeSconsBuildTasks = excludeSconsBuildTasks()
boolean isTemplate = flavor == "template" boolean isTemplate = flavor == "template"
@ -163,28 +170,28 @@ def generateBuildTasks(String flavor = "template") {
} }
} else { } else {
// Copy the generated editor apk to the bin directory. // Copy the generated editor apk to the bin directory.
String copyEditorApkTaskName = "copyEditor${capitalizedTarget}ApkToBin" String copyEditorApkTaskName = "copyEditor${capitalizedEditorVendor}${capitalizedTarget}ApkToBin"
if (tasks.findByName(copyEditorApkTaskName) != null) { if (tasks.findByName(copyEditorApkTaskName) != null) {
buildTasks += tasks.getByName(copyEditorApkTaskName) buildTasks += tasks.getByName(copyEditorApkTaskName)
} else { } else {
buildTasks += tasks.create(name: copyEditorApkTaskName, type: Copy) { buildTasks += tasks.create(name: copyEditorApkTaskName, type: Copy) {
dependsOn ":editor:assemble${capitalizedTarget}" dependsOn ":editor:assemble${capitalizedEditorVendor}${capitalizedTarget}"
from("editor/build/outputs/apk/${target}") from("editor/build/outputs/apk/${editorVendor}/${target}")
into(androidEditorBuildsDir) into(androidEditorBuildsDir)
include("android_editor-${target}*.apk") include("android_editor-${editorVendor}-${target}*.apk")
} }
} }
// Copy the generated editor aab to the bin directory. // Copy the generated editor aab to the bin directory.
String copyEditorAabTaskName = "copyEditor${capitalizedTarget}AabToBin" String copyEditorAabTaskName = "copyEditor${capitalizedEditorVendor}${capitalizedTarget}AabToBin"
if (tasks.findByName(copyEditorAabTaskName) != null) { if (tasks.findByName(copyEditorAabTaskName) != null) {
buildTasks += tasks.getByName(copyEditorAabTaskName) buildTasks += tasks.getByName(copyEditorAabTaskName)
} else { } else {
buildTasks += tasks.create(name: copyEditorAabTaskName, type: Copy) { buildTasks += tasks.create(name: copyEditorAabTaskName, type: Copy) {
dependsOn ":editor:bundle${capitalizedTarget}" dependsOn ":editor:bundle${capitalizedEditorVendor}${capitalizedTarget}"
from("editor/build/outputs/bundle/${target}") from("editor/build/outputs/bundle/${editorVendor}${capitalizedTarget}")
into(androidEditorBuildsDir) into(androidEditorBuildsDir)
include("android_editor-${target}*.aab") include("android_editor-${editorVendor}-${target}*.aab")
} }
} }
} }
@ -197,15 +204,27 @@ def generateBuildTasks(String flavor = "template") {
} }
/** /**
* Generate the Godot Editor Android apk. * Generate the Godot Editor Android binaries.
* *
* Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries * Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries
* must have been generated (via scons) prior to running this gradle task. * must have been generated (via scons) prior to running this gradle task.
* The task will only build the apk(s) for which the shared libraries is available. * The task will only build the binaries for which the shared libraries is available.
*/ */
task generateGodotEditor { task generateGodotEditor {
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask() gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
dependsOn = generateBuildTasks("editor") dependsOn = generateBuildTasks("editor", "google")
}
/**
* Generate the Godot Editor Android binaries for Meta devices.
*
* Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries
* must have been generated (via scons) prior to running this gradle task.
* The task will only build the binaries for which the shared libraries is available.
*/
task generateGodotMetaEditor {
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
dependsOn = generateBuildTasks("editor", "meta")
} }
/** /**

View File

@ -5,16 +5,6 @@ plugins {
id 'base' id 'base'
} }
dependencies {
implementation "androidx.fragment:fragment:$versions.fragmentVersion"
implementation project(":lib")
implementation "androidx.window:window:1.3.0"
implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "org.bouncycastle:bcprov-jdk15to18:1.77"
}
ext { ext {
// Retrieve the build number from the environment variable; default to 0 if none is specified. // Retrieve the build number from the environment variable; default to 0 if none is specified.
// The build number is added as a suffix to the version code for upload to the Google Play store. // The build number is added as a suffix to the version code for upload to the Google Play store.
@ -154,4 +144,37 @@ android {
doNotStrip '**/*.so' doNotStrip '**/*.so'
} }
} }
flavorDimensions = ["vendor"]
productFlavors {
google {
dimension "vendor"
missingDimensionStrategy 'products', 'editor'
}
meta {
dimension "vendor"
missingDimensionStrategy 'products', 'editor'
ndk {
//noinspection ChromeOsAbiSupport
abiFilters "arm64-v8a"
}
applicationIdSuffix ".meta"
versionNameSuffix "-meta"
minSdkVersion 23
targetSdkVersion 32
}
}
}
dependencies {
implementation "androidx.fragment:fragment:$versions.fragmentVersion"
implementation project(":lib")
implementation "androidx.window:window:1.3.0"
implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "org.bouncycastle:bcprov-jdk15to18:1.77"
// Meta dependencies
metaImplementation "org.godotengine:godot-openxr-vendors-meta:3.0.0-stable"
} }

View File

@ -0,0 +1,39 @@
/**************************************************************************/
/* GodotEditor.kt */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
package org.godotengine.editor
/**
* Primary window of the Godot Editor.
*
* This is the implementation of the editor used when running on regular Android devices.
*/
open class GodotEditor : BaseGodotEditor() {
}

View File

@ -1,5 +1,5 @@
/**************************************************************************/ /**************************************************************************/
/* GodotEditor.kt */ /* BaseGodotEditor.kt */
/**************************************************************************/ /**************************************************************************/
/* This file is part of: */ /* This file is part of: */
/* GODOT ENGINE */ /* GODOT ENGINE */
@ -52,6 +52,8 @@ import org.godotengine.godot.GodotLib
import org.godotengine.godot.error.Error import org.godotengine.godot.error.Error
import org.godotengine.godot.utils.PermissionsUtil import org.godotengine.godot.utils.PermissionsUtil
import org.godotengine.godot.utils.ProcessPhoenix import org.godotengine.godot.utils.ProcessPhoenix
import org.godotengine.godot.utils.isHorizonOSDevice
import org.godotengine.godot.utils.isNativeXRDevice
import java.util.* import java.util.*
import kotlin.math.min import kotlin.math.min
@ -61,13 +63,11 @@ import kotlin.math.min
* This provides the basic templates for the activities making up this application. * This provides the basic templates for the activities making up this application.
* Each derived activity runs in its own process, which enable up to have several instances of * Each derived activity runs in its own process, which enable up to have several instances of
* the Godot engine up and running at the same time. * the Godot engine up and running at the same time.
*
* It also plays the role of the primary editor window.
*/ */
open class GodotEditor : GodotActivity() { abstract class BaseGodotEditor : GodotActivity() {
companion object { companion object {
private val TAG = GodotEditor::class.java.simpleName private val TAG = BaseGodotEditor::class.java.simpleName
private const val WAIT_FOR_DEBUGGER = false private const val WAIT_FOR_DEBUGGER = false
@ -81,12 +81,13 @@ open class GodotEditor : GodotActivity() {
// Command line arguments // Command line arguments
private const val FULLSCREEN_ARG = "--fullscreen" private const val FULLSCREEN_ARG = "--fullscreen"
private const val FULLSCREEN_ARG_SHORT = "-f" private const val FULLSCREEN_ARG_SHORT = "-f"
private const val EDITOR_ARG = "--editor" internal const val EDITOR_ARG = "--editor"
private const val EDITOR_ARG_SHORT = "-e" internal const val EDITOR_ARG_SHORT = "-e"
private const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager" internal const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager"
private const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p" internal const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p"
private const val BREAKPOINTS_ARG = "--breakpoints" internal const val BREAKPOINTS_ARG = "--breakpoints"
private const val BREAKPOINTS_ARG_SHORT = "-b" internal const val BREAKPOINTS_ARG_SHORT = "-b"
internal const val XR_MODE_ARG = "--xr-mode"
// Info for the various classes used by the editor // Info for the various classes used by the editor
internal val EDITOR_MAIN_INFO = EditorWindowInfo(GodotEditor::class.java, 777, "") internal val EDITOR_MAIN_INFO = EditorWindowInfo(GodotEditor::class.java, 777, "")
@ -122,6 +123,20 @@ open class GodotEditor : GodotActivity() {
internal open fun getEditorWindowInfo() = EDITOR_MAIN_INFO internal open fun getEditorWindowInfo() = EDITOR_MAIN_INFO
/**
* Set of permissions to be excluded when requesting all permissions at startup.
*
* The permissions in this set will be requested on demand based on use cases.
*/
@CallSuper
protected open fun getExcludedPermissions(): MutableSet<String> {
return mutableSetOf(
// The RECORD_AUDIO permission is requested when the "audio/driver/enable_input" project
// setting is enabled.
Manifest.permission.RECORD_AUDIO
)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen() installSplashScreen()
@ -131,8 +146,8 @@ open class GodotEditor : GodotActivity() {
} }
// We exclude certain permissions from the set we request at startup, as they'll be // We exclude certain permissions from the set we request at startup, as they'll be
// requested on demand based on use-cases. // requested on demand based on use cases.
PermissionsUtil.requestManifestPermissions(this, setOf(Manifest.permission.RECORD_AUDIO)) PermissionsUtil.requestManifestPermissions(this, getExcludedPermissions())
val params = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS) val params = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
Log.d(TAG, "Starting intent $intent with parameters ${params.contentToString()}") Log.d(TAG, "Starting intent $intent with parameters ${params.contentToString()}")
@ -152,8 +167,6 @@ open class GodotEditor : GodotActivity() {
val longPressEnabled = enableLongPressGestures() val longPressEnabled = enableLongPressGestures()
val panScaleEnabled = enablePanAndScaleGestures() val panScaleEnabled = enablePanAndScaleGestures()
checkForProjectPermissionsToEnable()
runOnUiThread { runOnUiThread {
// Enable long press, panning and scaling gestures // Enable long press, panning and scaling gestures
godotFragment?.godot?.renderView?.inputHandler?.apply { godotFragment?.godot?.renderView?.inputHandler?.apply {
@ -171,17 +184,6 @@ open class GodotEditor : GodotActivity() {
} }
} }
/**
* Check for project permissions to enable
*/
protected open fun checkForProjectPermissionsToEnable() {
// Check for RECORD_AUDIO permission
val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input"))
if (audioInputEnabled) {
PermissionsUtil.requestPermission(Manifest.permission.RECORD_AUDIO, this)
}
}
@CallSuper @CallSuper
protected open fun updateCommandLineParams(args: List<String>) { protected open fun updateCommandLineParams(args: List<String>) {
// Update the list of command line params with the new args // Update the list of command line params with the new args
@ -196,7 +198,7 @@ open class GodotEditor : GodotActivity() {
final override fun getCommandLine() = commandLineParams final override fun getCommandLine() = commandLineParams
protected open fun getEditorWindowInfo(args: Array<String>): EditorWindowInfo { protected open fun retrieveEditorWindowInfo(args: Array<String>): EditorWindowInfo {
var hasEditor = false var hasEditor = false
var i = 0 var i = 0
@ -273,7 +275,7 @@ open class GodotEditor : GodotActivity() {
} }
override fun onNewGodotInstanceRequested(args: Array<String>): Int { override fun onNewGodotInstanceRequested(args: Array<String>): Int {
val editorWindowInfo = getEditorWindowInfo(args) val editorWindowInfo = retrieveEditorWindowInfo(args)
// Launch a new activity // Launch a new activity
val sourceView = godotFragment?.view val sourceView = godotFragment?.view
@ -405,20 +407,26 @@ open class GodotEditor : GodotActivity() {
return when (policy) { return when (policy) {
LaunchPolicy.AUTO -> { LaunchPolicy.AUTO -> {
try { if (isHorizonOSDevice()) {
when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) { // Horizon OS UX is more desktop-like and has support for launching adjacent
ANDROID_WINDOW_SAME_AS_EDITOR -> LaunchPolicy.SAME // windows. So we always want to launch in adjacent mode when auto is selected.
ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> LaunchPolicy.ADJACENT LaunchPolicy.ADJACENT
ANDROID_WINDOW_SAME_AS_EDITOR_AND_LAUNCH_IN_PIP_MODE -> LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE } else {
else -> { try {
// ANDROID_WINDOW_AUTO when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) {
defaultLaunchPolicy ANDROID_WINDOW_SAME_AS_EDITOR -> LaunchPolicy.SAME
ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> LaunchPolicy.ADJACENT
ANDROID_WINDOW_SAME_AS_EDITOR_AND_LAUNCH_IN_PIP_MODE -> LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE
else -> {
// ANDROID_WINDOW_AUTO
defaultLaunchPolicy
}
} }
} catch (e: NumberFormatException) {
Log.w(TAG, "Error parsing the Android window placement editor setting", e)
// Fall-back to the default launch policy
defaultLaunchPolicy
} }
} catch (e: NumberFormatException) {
Log.w(TAG, "Error parsing the Android window placement editor setting", e)
// Fall-back to the default launch policy
defaultLaunchPolicy
} }
} }
@ -431,8 +439,16 @@ open class GodotEditor : GodotActivity() {
/** /**
* Returns true the if the device supports picture-in-picture (PiP) * Returns true the if the device supports picture-in-picture (PiP)
*/ */
protected open fun hasPiPSystemFeature() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && protected open fun hasPiPSystemFeature(): Boolean {
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) if (isNativeXRDevice()) {
// Known native XR devices do not support PiP.
// Will need to revisit as they update their OS.
return false
}
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)

View File

@ -42,9 +42,9 @@ import android.util.Log
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
/** /**
* Used by the [GodotEditor] classes to dispatch messages across processes. * Used by the [BaseGodotEditor] classes to dispatch messages across processes.
*/ */
internal class EditorMessageDispatcher(private val editor: GodotEditor) { internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
companion object { companion object {
private val TAG = EditorMessageDispatcher::class.java.simpleName private val TAG = EditorMessageDispatcher::class.java.simpleName
@ -173,7 +173,11 @@ internal class EditorMessageDispatcher(private val editor: GodotEditor) {
// to the sender. // to the sender.
val senderId = messengerBundle.getInt(KEY_EDITOR_ID) val senderId = messengerBundle.getInt(KEY_EDITOR_ID)
val senderMessenger: Messenger? = messengerBundle.getParcelable(KEY_EDITOR_MESSENGER) val senderMessenger: Messenger? = messengerBundle.getParcelable(KEY_EDITOR_MESSENGER)
registerMessenger(senderId, senderMessenger) registerMessenger(senderId, senderMessenger) {
// Terminate current instance when parent is no longer available.
Log.d(TAG, "Terminating current editor instance because parent is no longer available")
editor.finish()
}
// Register ourselves to the sender so that it can communicate with us. // Register ourselves to the sender so that it can communicate with us.
registerSelfTo(pm, senderMessenger, editor.getEditorWindowInfo().windowId) registerSelfTo(pm, senderMessenger, editor.getEditorWindowInfo().windowId)

View File

@ -30,6 +30,7 @@
package org.godotengine.editor package org.godotengine.editor
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.PictureInPictureParams import android.app.PictureInPictureParams
import android.content.Intent import android.content.Intent
@ -38,12 +39,15 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import androidx.annotation.CallSuper
import org.godotengine.godot.GodotLib import org.godotengine.godot.GodotLib
import org.godotengine.godot.utils.PermissionsUtil
import org.godotengine.godot.utils.ProcessPhoenix
/** /**
* Drives the 'run project' window of the Godot Editor. * Drives the 'run project' window of the Godot Editor.
*/ */
class GodotGame : GodotEditor() { open class GodotGame : GodotEditor() {
companion object { companion object {
private val TAG = GodotGame::class.java.simpleName private val TAG = GodotGame::class.java.simpleName
@ -136,8 +140,53 @@ class GodotGame : GodotEditor() {
override fun enablePanAndScaleGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures")) override fun enablePanAndScaleGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures"))
override fun checkForProjectPermissionsToEnable() { override fun onGodotSetupCompleted() {
// Nothing to do.. by the time we get here, the project permissions will have already super.onGodotSetupCompleted()
// been requested by the Editor window. Log.v(TAG, "OnGodotSetupCompleted")
// Check if we should be running in XR instead (if available) as it's possible we were
// launched from the project manager which doesn't have that information.
val launchingArgs = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
if (launchingArgs != null) {
val editorWindowInfo = retrieveEditorWindowInfo(launchingArgs)
if (editorWindowInfo != getEditorWindowInfo()) {
val relaunchIntent = getNewGodotInstanceIntent(editorWindowInfo, launchingArgs)
relaunchIntent.putExtra(EXTRA_NEW_LAUNCH, true)
.putExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD, intent.getBundleExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD))
Log.d(TAG, "Relaunching XR project using ${editorWindowInfo.windowClassName} with parameters ${launchingArgs.contentToString()}")
val godot = godot
if (godot != null) {
godot.destroyAndKillProcess {
ProcessPhoenix.triggerRebirth(this, relaunchIntent)
}
} else {
ProcessPhoenix.triggerRebirth(this, relaunchIntent)
}
return
}
}
// Request project runtime permissions if necessary
val permissionsToEnable = getProjectPermissionsToEnable()
if (permissionsToEnable.isNotEmpty()) {
PermissionsUtil.requestPermissions(this, permissionsToEnable)
}
}
/**
* Check for project permissions to enable
*/
@CallSuper
protected open fun getProjectPermissionsToEnable(): MutableList<String> {
val permissionsToEnable = mutableListOf<String>()
// Check for RECORD_AUDIO permission
val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input"))
if (audioInputEnabled) {
permissionsToEnable.add(Manifest.permission.RECORD_AUDIO)
}
return permissionsToEnable
} }
} }

View File

@ -0,0 +1,99 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:horizonos="http://schemas.horizonos/sdk">
<horizonos:uses-horizonos-sdk
horizonos:minSdkVersion="69"
horizonos:targetSdkVersion="69" />
<uses-feature
android:name="android.hardware.vr.headtracking"
android:required="true"
android:version="1"/>
<!-- Oculus Quest hand tracking -->
<uses-permission android:name="com.oculus.permission.HAND_TRACKING" />
<uses-feature
android:name="oculus.software.handtracking"
android:required="false" />
<!-- Passthrough feature flag -->
<uses-feature android:name="com.oculus.feature.PASSTHROUGH"
android:required="false" />
<!-- Overlay keyboard support -->
<uses-feature android:name="oculus.software.overlay_keyboard" android:required="false"/>
<!-- Render model -->
<uses-permission android:name="com.oculus.permission.RENDER_MODEL" />
<uses-feature android:name="com.oculus.feature.RENDER_MODEL" android:required="false" />
<!-- Anchor api -->
<uses-permission android:name="com.oculus.permission.USE_ANCHOR_API" />
<!-- Scene api -->
<uses-permission android:name="com.oculus.permission.USE_SCENE" />
<application>
<activity
android:name=".GodotEditor"
android:exported="true"
android:screenOrientation="landscape"
tools:node="merge"
tools:replace="android:screenOrientation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="com.oculus.intent.category.2D" />
</intent-filter>
<meta-data android:name="com.oculus.vrshell.free_resizing_lock_aspect_ratio" android:value="true"/>
</activity>
<activity
android:name=".GodotXRGame"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
android:process=":GodotXRGame"
android:launchMode="singleTask"
android:icon="@mipmap/ic_play_window"
android:label="@string/godot_game_activity_name"
android:exported="false"
android:screenOrientation="landscape"
android:resizeableActivity="false"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="com.oculus.intent.category.VR" />
<category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" />
</intent-filter>
</activity>
<!-- Supported Meta devices -->
<meta-data
android:name="com.oculus.supportedDevices"
android:value="quest3|questpro"
tools:replace="android:value" />
<!--
We remove this meta-data originating from the vendors plugin as we only need the loader for
now since the project being edited provides its own version of the vendors plugin.
This needs to be removed once we start implementing the immersive version of the project
manager and editor windows.
-->
<meta-data
android:name="org.godotengine.plugin.v2.GodotOpenXRMeta"
android:value="org.godotengine.openxr.vendors.meta.GodotOpenXRMeta"
tools:node="remove" />
<!-- Enable system splash screen -->
<meta-data android:name="com.oculus.ossplash" android:value="true"/>
<!-- Enable passthrough background during the splash screen -->
<meta-data android:name="com.oculus.ossplash.background" android:value="passthrough-contextual"/>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,94 @@
/**************************************************************************/
/* GodotEditor.kt */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
package org.godotengine.editor
import org.godotengine.godot.GodotLib
import org.godotengine.godot.utils.isNativeXRDevice
/**
* Primary window of the Godot Editor.
*
* This is the implementation of the editor used when running on Meta devices.
*/
open class GodotEditor : BaseGodotEditor() {
companion object {
private val TAG = GodotEditor::class.java.simpleName
internal val XR_RUN_GAME_INFO = EditorWindowInfo(GodotXRGame::class.java, 1667, ":GodotXRGame")
internal const val USE_ANCHOR_API_PERMISSION = "com.oculus.permission.USE_ANCHOR_API"
internal const val USE_SCENE_PERMISSION = "com.oculus.permission.USE_SCENE"
}
override fun getExcludedPermissions(): MutableSet<String> {
val excludedPermissions = super.getExcludedPermissions()
// The USE_ANCHOR_API and USE_SCENE permissions are requested when the "xr/openxr/enabled"
// project setting is enabled.
excludedPermissions.add(USE_ANCHOR_API_PERMISSION)
excludedPermissions.add(USE_SCENE_PERMISSION)
return excludedPermissions
}
override fun retrieveEditorWindowInfo(args: Array<String>): EditorWindowInfo {
var hasEditor = false
var xrModeOn = false
var i = 0
while (i < args.size) {
when (args[i++]) {
EDITOR_ARG, EDITOR_ARG_SHORT, EDITOR_PROJECT_MANAGER_ARG, EDITOR_PROJECT_MANAGER_ARG_SHORT -> hasEditor = true
XR_MODE_ARG -> {
val argValue = args[i++]
xrModeOn = xrModeOn || ("on" == argValue)
}
}
}
return if (hasEditor) {
EDITOR_MAIN_INFO
} else {
val openxrEnabled = GodotLib.getGlobal("xr/openxr/enabled").toBoolean()
if (openxrEnabled && isNativeXRDevice()) {
XR_RUN_GAME_INFO
} else {
RUN_GAME_INFO
}
}
}
override fun getEditorWindowInfoForInstanceId(instanceId: Int): EditorWindowInfo? {
return when (instanceId) {
XR_RUN_GAME_INFO.windowId -> XR_RUN_GAME_INFO
else -> super.getEditorWindowInfoForInstanceId(instanceId)
}
}
}

View File

@ -0,0 +1,71 @@
/*************************************************************************/
/* GodotXRGame.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.editor
import org.godotengine.godot.GodotLib
import org.godotengine.godot.utils.PermissionsUtil
import org.godotengine.godot.xr.XRMode
/**
* Provide support for running XR apps / games from the editor window.
*/
open class GodotXRGame: GodotGame() {
override fun overrideOrientationRequest() = true
override fun updateCommandLineParams(args: List<String>) {
val updatedArgs = ArrayList<String>()
if (!args.contains(XRMode.OPENXR.cmdLineArg)) {
updatedArgs.add(XRMode.OPENXR.cmdLineArg)
}
if (!args.contains(XR_MODE_ARG)) {
updatedArgs.add(XR_MODE_ARG)
updatedArgs.add("on")
}
updatedArgs.addAll(args)
super.updateCommandLineParams(updatedArgs)
}
override fun getEditorWindowInfo() = XR_RUN_GAME_INFO
override fun getProjectPermissionsToEnable(): MutableList<String> {
val permissionsToEnable = super.getProjectPermissionsToEnable()
val openxrEnabled = GodotLib.getGlobal("xr/openxr/enabled").toBoolean()
if (openxrEnabled) {
permissionsToEnable.add(USE_ANCHOR_API_PERMISSION)
permissionsToEnable.add(USE_SCENE_PERMISSION)
}
return permissionsToEnable
}
}

View File

@ -51,7 +51,7 @@ android {
} }
} }
flavorDimensions "products" flavorDimensions = ["products"]
productFlavors { productFlavors {
editor {} editor {}
template {} template {}
@ -104,7 +104,7 @@ android {
} }
boolean devBuild = buildType == "dev" boolean devBuild = buildType == "dev"
boolean debugSymbols = devBuild || isAndroidStudio() boolean debugSymbols = devBuild
boolean runTests = devBuild boolean runTests = devBuild
boolean productionBuild = !devBuild boolean productionBuild = !devBuild
boolean storeRelease = buildType == "release" boolean storeRelease = buildType == "release"

View File

@ -31,6 +31,7 @@
package org.godotengine.godot; package org.godotengine.godot;
import org.godotengine.godot.input.GodotInputHandler; import org.godotengine.godot.input.GodotInputHandler;
import org.godotengine.godot.utils.DeviceUtils;
import android.view.SurfaceView; import android.view.SurfaceView;
@ -63,7 +64,11 @@ public interface GodotRenderView {
void setPointerIcon(int pointerType); void setPointerIcon(int pointerType);
/**
* @return true if pointer capture is supported.
*/
default boolean canCapturePointer() { default boolean canCapturePointer() {
return getInputHandler().canCapturePointer(); // Pointer capture is not supported on Horizon OS
return !DeviceUtils.isHorizonOSDevice() && getInputHandler().canCapturePointer();
} }
} }

View File

@ -68,6 +68,7 @@ class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT));
} }
setFocusableInTouchMode(true); setFocusableInTouchMode(true);
setClickable(false);
} }
@Override @Override
@ -132,17 +133,17 @@ class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
@Override @Override
public boolean onKeyUp(final int keyCode, KeyEvent event) { public boolean onKeyUp(final int keyCode, KeyEvent event) {
return mInputHandler.onKeyUp(keyCode, event); return mInputHandler.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
} }
@Override @Override
public boolean onKeyDown(final int keyCode, KeyEvent event) { public boolean onKeyDown(final int keyCode, KeyEvent event) {
return mInputHandler.onKeyDown(keyCode, event); return mInputHandler.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
} }
@Override @Override
public boolean onGenericMotionEvent(MotionEvent event) { public boolean onGenericMotionEvent(MotionEvent event) {
return mInputHandler.onGenericMotionEvent(event); return mInputHandler.onGenericMotionEvent(event) || super.onGenericMotionEvent(event);
} }
@Override @Override

View File

@ -0,0 +1,52 @@
/**************************************************************************/
/* DeviceUtils.kt */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
/**
* Contains utility methods for detecting specific devices.
*/
@file:JvmName("DeviceUtils")
package org.godotengine.godot.utils
import android.os.Build
/**
* Returns true if running on Meta's Horizon OS.
*/
fun isHorizonOSDevice(): Boolean {
return "Oculus".equals(Build.BRAND, true)
}
/**
* Returns true if running on a native Android XR device.
*/
fun isNativeXRDevice(): Boolean {
return isHorizonOSDevice()
}

View File

@ -8,6 +8,7 @@ set(CMAKE_CXX_EXTENSIONS OFF)
set(GODOT_ROOT_DIR ../../../..) set(GODOT_ROOT_DIR ../../../..)
set(ANDROID_ROOT_DIR "${GODOT_ROOT_DIR}/platform/android" CACHE STRING "") set(ANDROID_ROOT_DIR "${GODOT_ROOT_DIR}/platform/android" CACHE STRING "")
set(OPENXR_INCLUDE_DIR "${GODOT_ROOT_DIR}/thirdparty/openxr/include" CACHE STRING "")
# Get sources # Get sources
file(GLOB_RECURSE SOURCES ${GODOT_ROOT_DIR}/*.c**) file(GLOB_RECURSE SOURCES ${GODOT_ROOT_DIR}/*.c**)
@ -17,6 +18,7 @@ add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS})
target_include_directories(${PROJECT_NAME} target_include_directories(${PROJECT_NAME}
SYSTEM PUBLIC SYSTEM PUBLIC
${GODOT_ROOT_DIR} ${GODOT_ROOT_DIR}
${ANDROID_ROOT_DIR}) ${ANDROID_ROOT_DIR}
${OPENXR_INCLUDE_DIR})
add_definitions(-DUNIX_ENABLED -DVULKAN_ENABLED -DANDROID_ENABLED -DGLES3_ENABLED -DTOOLS_ENABLED) add_definitions(-DUNIX_ENABLED -DVULKAN_ENABLED -DANDROID_ENABLED -DGLES3_ENABLED -DTOOLS_ENABLED)

View File

@ -14,6 +14,7 @@ pluginManagement {
mavenCentral() mavenCentral()
gradlePluginPortal() gradlePluginPortal()
maven { url "https://plugins.gradle.org/m2/" } maven { url "https://plugins.gradle.org/m2/" }
maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/"}
} }
} }

View File

@ -35,7 +35,6 @@
#include "string_android.h" #include "string_android.h"
#include "core/config/engine.h" #include "core/config/engine.h"
#include "core/config/project_settings.h"
#include "core/error/error_macros.h" #include "core/error/error_macros.h"
static HashMap<String, JNISingleton *> jni_singletons; static HashMap<String, JNISingleton *> jni_singletons;
@ -43,7 +42,6 @@ static HashMap<String, JNISingleton *> jni_singletons;
void unregister_plugins_singletons() { void unregister_plugins_singletons() {
for (const KeyValue<String, JNISingleton *> &E : jni_singletons) { for (const KeyValue<String, JNISingleton *> &E : jni_singletons) {
Engine::get_singleton()->remove_singleton(E.key); Engine::get_singleton()->remove_singleton(E.key);
ProjectSettings::get_singleton()->set(E.key, Variant());
if (E.value) { if (E.value) {
memdelete(E.value); memdelete(E.value);
@ -64,7 +62,6 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeR
jni_singletons[singname] = s; jni_singletons[singname] = s;
Engine::get_singleton()->add_singleton(Engine::Singleton(singname, s)); Engine::get_singleton()->add_singleton(Engine::Singleton(singname, s));
ProjectSettings::get_singleton()->set(singname, s);
return true; return true;
} }