diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp
index 8183c7737bcf..63dbc0acd119 100644
--- a/core/config/project_settings.cpp
+++ b/core/config/project_settings.cpp
@@ -1613,6 +1613,13 @@ ProjectSettings::ProjectSettings() {
GLOBAL_DEF_BASIC("input_devices/pointing/android/enable_pan_and_scale_gestures", false);
GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "input_devices/pointing/android/rotary_input_scroll_axis", PROPERTY_HINT_ENUM, "Horizontal,Vertical"), 1);
GLOBAL_DEF("input_devices/pointing/android/override_volume_buttons", false);
+ GLOBAL_DEF_BASIC("input_devices/virtual_controller/ios/enable_controller", false);
+ GLOBAL_DEF_BASIC("input_devices/virtual_controller/ios/enable_left_thumbstick", true);
+ GLOBAL_DEF_BASIC("input_devices/virtual_controller/ios/enable_right_thumbstick", true);
+ GLOBAL_DEF_BASIC("input_devices/virtual_controller/ios/enable_button_a", true);
+ GLOBAL_DEF_BASIC("input_devices/virtual_controller/ios/enable_button_b", true);
+ GLOBAL_DEF_BASIC("input_devices/virtual_controller/ios/enable_button_x", true);
+ GLOBAL_DEF_BASIC("input_devices/virtual_controller/ios/enable_button_y", true);
// These properties will not show up in the dialog. If you want to exclude whole groups, use add_hidden_prefix().
GLOBAL_DEF_INTERNAL("application/config/features", PackedStringArray());
diff --git a/core/input/input.cpp b/core/input/input.cpp
index 6b9fec6f6003..92958bb14a42 100644
--- a/core/input/input.cpp
+++ b/core/input/input.cpp
@@ -170,6 +170,7 @@ void Input::_bind_methods() {
ClassDB::bind_method(D_METHOD("is_emulating_mouse_from_touch"), &Input::is_emulating_mouse_from_touch);
ClassDB::bind_method(D_METHOD("set_emulate_touch_from_mouse", "enable"), &Input::set_emulate_touch_from_mouse);
ClassDB::bind_method(D_METHOD("is_emulating_touch_from_mouse"), &Input::is_emulating_touch_from_mouse);
+ ClassDB::bind_method(D_METHOD("get_virtual_controller"), &Input::get_virtual_controller);
ADD_PROPERTY(PropertyInfo(Variant::INT, "mouse_mode"), "set_mouse_mode", "get_mouse_mode");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_accumulated_input"), "set_use_accumulated_input", "is_using_accumulated_input");
@@ -1056,6 +1057,10 @@ bool Input::is_emulating_touch_from_mouse() const {
return emulate_touch_from_mouse;
}
+VirtualController *Input::get_virtual_controller() {
+ return OS::get_singleton()->get_virtual_controller();
+}
+
// Calling this whenever the game window is focused helps unsticking the "touch mouse"
// if the OS or its abstraction class hasn't properly reported that touch pointers raised
void Input::ensure_touch_mouse_raised() {
diff --git a/core/input/input.h b/core/input/input.h
index 8fc1b9c5dacf..514f3b96c22d 100644
--- a/core/input/input.h
+++ b/core/input/input.h
@@ -31,6 +31,7 @@
#pragma once
#include "core/input/input_event.h"
+#include "core/input/virtual_controller.h"
#include "core/object/object.h"
#include "core/os/keyboard.h"
#include "core/os/thread_safe.h"
@@ -389,6 +390,8 @@ class Input : public Object {
void set_use_accumulated_input(bool p_enable);
bool is_using_accumulated_input();
+ VirtualController *get_virtual_controller();
+
void release_pressed_events();
void set_event_dispatch_function(EventDispatchFunc p_function);
diff --git a/core/input/virtual_controller.cpp b/core/input/virtual_controller.cpp
new file mode 100644
index 000000000000..a7f702a2c335
--- /dev/null
+++ b/core/input/virtual_controller.cpp
@@ -0,0 +1,49 @@
+/**************************************************************************/
+/* virtual_controller.cpp */
+/**************************************************************************/
+/* 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. */
+/**************************************************************************/
+
+#include "virtual_controller.h"
+
+void VirtualController::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("enable"), &VirtualController::enable);
+ ClassDB::bind_method(D_METHOD("disable"), &VirtualController::disable);
+ ClassDB::bind_method(D_METHOD("is_enabled"), &VirtualController::is_enabled);
+ ClassDB::bind_method(D_METHOD("set_enabled_left_thumbstick", "enable"), &VirtualController::set_enabled_left_thumbstick);
+ ClassDB::bind_method(D_METHOD("is_enabled_left_thumbstick"), &VirtualController::is_enabled_left_thumbstick);
+ ClassDB::bind_method(D_METHOD("set_enabled_right_thumbstick", "enable"), &VirtualController::set_enabled_right_thumbstick);
+ ClassDB::bind_method(D_METHOD("is_enabled_right_thumbstick"), &VirtualController::is_enabled_right_thumbstick);
+ ClassDB::bind_method(D_METHOD("set_enabled_button_a", "enable"), &VirtualController::set_enabled_button_a);
+ ClassDB::bind_method(D_METHOD("is_enabled_button_a"), &VirtualController::is_enabled_button_a);
+ ClassDB::bind_method(D_METHOD("set_enabled_button_b", "enable"), &VirtualController::set_enabled_button_b);
+ ClassDB::bind_method(D_METHOD("is_enabled_button_b"), &VirtualController::is_enabled_button_b);
+ ClassDB::bind_method(D_METHOD("set_enabled_button_x", "enable"), &VirtualController::set_enabled_button_x);
+ ClassDB::bind_method(D_METHOD("is_enabled_button_x"), &VirtualController::is_enabled_button_x);
+ ClassDB::bind_method(D_METHOD("set_enabled_button_y", "enable"), &VirtualController::set_enabled_button_y);
+ ClassDB::bind_method(D_METHOD("is_enabled_button_y"), &VirtualController::is_enabled_button_y);
+}
diff --git a/core/input/virtual_controller.h b/core/input/virtual_controller.h
new file mode 100644
index 000000000000..0188e7fb7128
--- /dev/null
+++ b/core/input/virtual_controller.h
@@ -0,0 +1,57 @@
+/**************************************************************************/
+/* virtual_controller.h */
+/**************************************************************************/
+/* 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. */
+/**************************************************************************/
+
+#pragma once
+
+#include "core/object/class_db.h"
+
+class VirtualController : public Object {
+ GDCLASS(VirtualController, Object);
+
+protected:
+ static void _bind_methods();
+
+public:
+ virtual void enable() = 0;
+ virtual void disable() = 0;
+ virtual bool is_enabled() = 0;
+ virtual void set_enabled_left_thumbstick(bool p_enabled) = 0;
+ virtual bool is_enabled_left_thumbstick() = 0;
+ virtual void set_enabled_right_thumbstick(bool p_enabled) = 0;
+ virtual bool is_enabled_right_thumbstick() = 0;
+ virtual void set_enabled_button_a(bool p_enabled) = 0;
+ virtual bool is_enabled_button_a() = 0;
+ virtual void set_enabled_button_b(bool p_enabled) = 0;
+ virtual bool is_enabled_button_b() = 0;
+ virtual void set_enabled_button_x(bool p_enabled) = 0;
+ virtual bool is_enabled_button_x() = 0;
+ virtual void set_enabled_button_y(bool p_enabled) = 0;
+ virtual bool is_enabled_button_y() = 0;
+};
diff --git a/core/os/os.cpp b/core/os/os.cpp
index bbeb5b1fb285..131a78a8f3ad 100644
--- a/core/os/os.cpp
+++ b/core/os/os.cpp
@@ -771,6 +771,16 @@ void OS::benchmark_dump() {
#endif
}
+VirtualController *OS::get_virtual_controller() const {
+ return nullptr;
+}
+
+void OS::controller_connected() const {
+}
+
+void OS::controller_disconnected() const {
+}
+
OS::OS() {
singleton = this;
diff --git a/core/os/os.h b/core/os/os.h
index 49028b9cad66..a6fc4c05749a 100644
--- a/core/os/os.h
+++ b/core/os/os.h
@@ -31,6 +31,7 @@
#pragma once
#include "core/config/engine.h"
+#include "core/input/virtual_controller.h"
#include "core/io/logger.h"
#include "core/io/remote_filesystem_client.h"
#include "core/os/time_enums.h"
@@ -365,6 +366,12 @@ class OS {
virtual bool _test_create_rendering_device_and_gl(const String &p_display_driver) const { return true; }
virtual bool _test_create_rendering_device(const String &p_display_driver) const { return true; }
+ virtual VirtualController *get_virtual_controller() const;
+
+ virtual void controller_connected() const;
+
+ virtual void controller_disconnected() const;
+
OS();
virtual ~OS();
};
diff --git a/core/register_core_types.cpp b/core/register_core_types.cpp
index 568694bdf4d0..29fa95d4a339 100644
--- a/core/register_core_types.cpp
+++ b/core/register_core_types.cpp
@@ -42,6 +42,7 @@
#include "core/input/input.h"
#include "core/input/input_map.h"
#include "core/input/shortcut.h"
+#include "core/input/virtual_controller.h"
#include "core/io/config_file.h"
#include "core/io/dir_access.h"
#include "core/io/dtls_server.h"
@@ -296,6 +297,8 @@ void register_core_types() {
GDREGISTER_NATIVE_STRUCT(AudioFrame, "float left;float right");
GDREGISTER_NATIVE_STRUCT(ScriptLanguageExtensionProfilingInfo, "StringName signature;uint64_t call_count;uint64_t total_time;uint64_t self_time");
+ GDREGISTER_ABSTRACT_CLASS(VirtualController);
+
worker_thread_pool = memnew(WorkerThreadPool);
OS::get_singleton()->benchmark_end_measure("Core", "Register Types");
diff --git a/doc/classes/Input.xml b/doc/classes/Input.xml
index ca54a44f489a..f6a4be1ab4a2 100644
--- a/doc/classes/Input.xml
+++ b/doc/classes/Input.xml
@@ -202,6 +202,13 @@
By default, the deadzone is automatically calculated from the average of the action deadzones. However, you can override the deadzone to be whatever you want (on the range of 0 to 1).
+
+
+
+ Return the platform-specific virtual controller or [code]null[/code] if it is not available. See also [VirtualController].
+ [b]Note:[/b] Currently implemented only on iOS 15.0+.
+
+
diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index a028c0aa1db4..b06f9a162bb8 100644
--- a/doc/classes/ProjectSettings.xml
+++ b/doc/classes/ProjectSettings.xml
@@ -1472,6 +1472,28 @@
If [code]true[/code], the magnetometer sensor is enabled and [method Input.get_magnetometer] returns valid data.
+
+ If [code]true[/code], the button "A" shows in the virtual controller.
+
+
+ If [code]true[/code], the button "B" shows in the virtual controller.
+
+
+ If [code]true[/code], the button "X" shows in the virtual controller.
+
+
+ If [code]true[/code], the button "Y" shows in the virtual controller.
+
+
+ If [code]true[/code], if there are no physical controllers connected, the game shows the virtual controller.
+ [b]Note:[/b] Currently implemented only on iOS 15.0+.
+
+
+ If [code]true[/code], the left thumbstick shows in the virtual controller.
+
+
+ If [code]true[/code], the right thumbstick shows in the virtual controller.
+
The locale to fall back to if a translation isn't available in a given language. If left empty, [code]en[/code] (English) will be used.
[b]Note:[/b] Not to be confused with [TextServerFallback].
diff --git a/doc/classes/VirtualController.xml b/doc/classes/VirtualController.xml
new file mode 100644
index 000000000000..b2d510a0a6ba
--- /dev/null
+++ b/doc/classes/VirtualController.xml
@@ -0,0 +1,110 @@
+
+
+
+ A software emulation of a real controller that you configure specifically for your game.
+
+
+ Use a virtual controller to display software controls that you can customize over your game. You create a virtual controller from a configuration where you choose the input elements to display. When you connect the controller to the device, users interact with it similarly to a real controller.
+ [b]Note:[/b] Currently implemented only on iOS 15.0+.
+
+
+
+
+
+
+
+ Disconnects the virtual controller from the device and removes it from the screen.
+
+
+
+
+
+ If there are no physical controllers connected, connects the virtual controller to the device and displays it on the screen.
+
+
+
+
+
+ Returns [code]true[/code] if the virtual controller is enabled.
+
+
+
+
+
+ Returns [code]true[/code] if the button "A" is enabled.
+
+
+
+
+
+ Returns [code]true[/code] if the button "B" is enabled.
+
+
+
+
+
+ Returns [code]true[/code] if the button "X" is enabled.
+
+
+
+
+
+ Returns [code]true[/code] if the button "Y" is enabled.
+
+
+
+
+
+ Returns [code]true[/code] if the left thumbstick is enabled.
+
+
+
+
+
+ Returns [code]true[/code] if the right thumbstick is enabled.
+
+
+
+
+
+
+ Changes the visibility of a button "A" element. Default is [code]true[/code].
+
+
+
+
+
+
+ Changes the visibility of a button "B" element. Default is [code]true[/code].
+
+
+
+
+
+
+ Changes the visibility of a button "A" element. Default is [code]true[/code].
+
+
+
+
+
+
+ Changes the visibility of a button "Y" element. Default is [code]true[/code].
+
+
+
+
+
+
+ Changes the visibility of a left thumbstick element. Default is [code]true[/code].
+
+
+
+
+
+
+ Changes the visibility of a right thumbstick element. Default is [code]true[/code].
+
+
+
+
diff --git a/drivers/apple/joypad_apple.mm b/drivers/apple/joypad_apple.mm
index 312b3da71b2f..7acc686b281e 100644
--- a/drivers/apple/joypad_apple.mm
+++ b/drivers/apple/joypad_apple.mm
@@ -34,6 +34,7 @@
#import
#include "core/config/project_settings.h"
+#include "core/os/os.h"
#include "main/main.h"
class API_AVAILABLE(macos(11), ios(14.0), tvos(14.0)) RumbleMotor {
@@ -257,6 +258,7 @@ void play_strong_pattern(CHHapticPattern *p_pattern) {
return;
}
add_joypad(controller);
+ OS::get_singleton()->controller_connected();
}];
disconnect_observer = [NSNotificationCenter.defaultCenter
@@ -269,6 +271,7 @@ void play_strong_pattern(CHHapticPattern *p_pattern) {
return;
}
remove_joypad(controller);
+ OS::get_singleton()->controller_disconnected();
}];
if (@available(macOS 11.3, iOS 14.5, tvOS 14.5, *)) {
diff --git a/platform/ios/SCsub b/platform/ios/SCsub
index d16dcb1a95a3..08e14bd4126f 100644
--- a/platform/ios/SCsub
+++ b/platform/ios/SCsub
@@ -77,6 +77,7 @@ ios_lib = [
"keyboard_input_view.mm",
"key_mapping_ios.mm",
"ios_terminal_logger.mm",
+ "virtual_controller_ios.mm",
]
env_ios = env.Clone()
diff --git a/platform/ios/os_ios.h b/platform/ios/os_ios.h
index f4283958c1fe..3a0912aabc07 100644
--- a/platform/ios/os_ios.h
+++ b/platform/ios/os_ios.h
@@ -33,6 +33,7 @@
#ifdef IOS_ENABLED
#import "ios.h"
+#import "virtual_controller_ios.h"
#import "drivers/apple/joypad_apple.h"
#import "drivers/coreaudio/audio_driver_coreaudio.h"
@@ -61,6 +62,8 @@ class OS_IOS : public OS_Unix {
MainLoop *main_loop = nullptr;
+ IOSVirtualController *virtual_controller = nullptr;
+
virtual void initialize_core() override;
virtual void initialize() override;
@@ -126,6 +129,8 @@ class OS_IOS : public OS_Unix {
virtual bool _check_internal_feature_support(const String &p_feature) override;
+ virtual VirtualController *get_virtual_controller() const override;
+
void on_focus_out();
void on_focus_in();
@@ -133,6 +138,10 @@ class OS_IOS : public OS_Unix {
void on_exit_background();
virtual Rect2 calculate_boot_screen_rect(const Size2 &p_window_size, const Size2 &p_imgrect_size) const override;
+
+ virtual void controller_connected() const override;
+
+ virtual void controller_disconnected() const override;
};
#endif // IOS_ENABLED
diff --git a/platform/ios/os_ios.mm b/platform/ios/os_ios.mm
index e1489f02f13c..1c2a25b730f8 100644
--- a/platform/ios/os_ios.mm
+++ b/platform/ios/os_ios.mm
@@ -168,6 +168,8 @@ Rect2 fit_keep_aspect_covered(const Vector2 &p_container, const Vector2 &p_rect)
void OS_IOS::initialize_joypads() {
joypad_apple = memnew(JoypadApple);
+
+ virtual_controller = memnew(IOSVirtualController);
}
void OS_IOS::initialize_modules() {
@@ -176,6 +178,10 @@ Rect2 fit_keep_aspect_covered(const Vector2 &p_container, const Vector2 &p_rect)
}
void OS_IOS::deinitialize_modules() {
+ if (virtual_controller) {
+ memdelete(virtual_controller);
+ }
+
if (joypad_apple) {
memdelete(joypad_apple);
}
@@ -220,6 +226,10 @@ Rect2 fit_keep_aspect_covered(const Vector2 &p_container, const Vector2 &p_rect)
if (Main::start() == EXIT_SUCCESS) {
main_loop->initialize();
}
+
+ if (virtual_controller) {
+ virtual_controller->update_state();
+ }
}
void OS_IOS::finalize() {
@@ -644,6 +654,10 @@ Rect2 fit_keep_aspect_covered(const Vector2 &p_container, const Vector2 &p_rect)
return false;
}
+VirtualController *OS_IOS::get_virtual_controller() const {
+ return virtual_controller;
+}
+
void OS_IOS::on_focus_out() {
if (is_focused) {
is_focused = false;
@@ -717,4 +731,16 @@ Rect2 fit_keep_aspect_covered(const Vector2 &p_container, const Vector2 &p_rect)
}
}
+void OS_IOS::controller_connected() const {
+ if (virtual_controller) {
+ virtual_controller->controller_connected();
+ }
+}
+
+void OS_IOS::controller_disconnected() const {
+ if (virtual_controller) {
+ virtual_controller->controller_disconnected();
+ }
+}
+
#endif // IOS_ENABLED
diff --git a/platform/ios/virtual_controller_ios.h b/platform/ios/virtual_controller_ios.h
new file mode 100644
index 000000000000..85fc3baf0973
--- /dev/null
+++ b/platform/ios/virtual_controller_ios.h
@@ -0,0 +1,83 @@
+/**************************************************************************/
+/* virtual_controller_ios.h */
+/**************************************************************************/
+/* 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. */
+/**************************************************************************/
+
+#pragma once
+
+#import "core/input/virtual_controller.h"
+
+#import
+
+class IOSVirtualController : public VirtualController {
+private:
+#if defined(__IPHONE_15_0)
+ API_AVAILABLE(ios(15.0))
+ GCVirtualController *gcv_controller;
+#endif
+
+public:
+ IOSVirtualController();
+ ~IOSVirtualController();
+
+ virtual void enable() override;
+ virtual void disable() override;
+ virtual bool is_enabled() override;
+ virtual void set_enabled_left_thumbstick(bool p_enabled) override;
+ virtual bool is_enabled_left_thumbstick() override;
+ virtual void set_enabled_right_thumbstick(bool p_enabled) override;
+ virtual bool is_enabled_right_thumbstick() override;
+ virtual void set_enabled_button_a(bool p_enabled) override;
+ virtual bool is_enabled_button_a() override;
+ virtual void set_enabled_button_b(bool p_enabled) override;
+ virtual bool is_enabled_button_b() override;
+ virtual void set_enabled_button_x(bool p_enabled) override;
+ virtual bool is_enabled_button_x() override;
+ virtual void set_enabled_button_y(bool p_enabled) override;
+ virtual bool is_enabled_button_y() override;
+
+ void controller_connected();
+ void controller_disconnected();
+ void update_state();
+
+private:
+ void connect_controller();
+ void disconnect_controller();
+ void initialize();
+ void elements_changed(GCInputElementName p_name, bool p_hidden);
+ void read_project_settings();
+
+private:
+ bool enabled = false;
+ bool enabled_left_thumbstick = true;
+ bool enabled_right_thumbstick = true;
+ bool enabled_button_a = true;
+ bool enabled_button_b = true;
+ bool enabled_button_x = true;
+ bool enabled_button_y = true;
+};
diff --git a/platform/ios/virtual_controller_ios.mm b/platform/ios/virtual_controller_ios.mm
new file mode 100644
index 000000000000..546f75fed20d
--- /dev/null
+++ b/platform/ios/virtual_controller_ios.mm
@@ -0,0 +1,247 @@
+/**************************************************************************/
+/* virtual_controller_ios.mm */
+/**************************************************************************/
+/* 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. */
+/**************************************************************************/
+
+#import "virtual_controller_ios.h"
+
+#include "core/config/project_settings.h"
+
+IOSVirtualController::IOSVirtualController() {
+ initialize();
+}
+
+IOSVirtualController::~IOSVirtualController() {
+ if (@available(iOS 15.0, *)) {
+ gcv_controller = nullptr;
+ }
+}
+
+void IOSVirtualController::initialize() {
+ if (@available(iOS 15.0, *)) {
+ if (!gcv_controller) {
+ read_project_settings();
+
+ GCVirtualControllerConfiguration *config = [[GCVirtualControllerConfiguration alloc] init];
+
+ NSMutableSet *elements = [[NSMutableSet alloc] init];
+
+ if (is_enabled_left_thumbstick()) {
+ [elements addObject:GCInputLeftThumbstick];
+ }
+ if (is_enabled_right_thumbstick()) {
+ [elements addObject:GCInputRightThumbstick];
+ }
+ if (is_enabled_button_a()) {
+ [elements addObject:GCInputButtonA];
+ }
+ if (is_enabled_button_b()) {
+ [elements addObject:GCInputButtonB];
+ }
+ if (is_enabled_button_x()) {
+ [elements addObject:GCInputButtonX];
+ }
+ if (is_enabled_button_y()) {
+ [elements addObject:GCInputButtonY];
+ }
+
+ config.elements = elements;
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ gcv_controller = [[GCVirtualController alloc] initWithConfiguration:config];
+ });
+ }
+ }
+}
+
+void IOSVirtualController::read_project_settings() {
+ enabled = GLOBAL_GET("input_devices/virtual_controller/ios/enable_controller");
+ enabled_left_thumbstick = GLOBAL_GET("input_devices/virtual_controller/ios/enable_left_thumbstick");
+ enabled_right_thumbstick = GLOBAL_GET("input_devices/virtual_controller/ios/enable_right_thumbstick");
+ enabled_button_a = GLOBAL_GET("input_devices/virtual_controller/ios/enable_button_a");
+ enabled_button_b = GLOBAL_GET("input_devices/virtual_controller/ios/enable_button_b");
+ enabled_button_x = GLOBAL_GET("input_devices/virtual_controller/ios/enable_button_x");
+ enabled_button_y = GLOBAL_GET("input_devices/virtual_controller/ios/enable_button_y");
+}
+
+void IOSVirtualController::elements_changed(GCInputElementName p_name, bool p_hidden) {
+ if (@available(iOS 15.0, *)) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if (gcv_controller) {
+ [gcv_controller updateConfigurationForElement:p_name
+ configuration:^(GCVirtualControllerElementConfiguration *configuration) {
+ configuration.hidden = p_hidden;
+ return configuration;
+ }];
+ }
+ });
+ }
+}
+
+void IOSVirtualController::enable() {
+ enabled = true;
+ update_state();
+}
+
+void IOSVirtualController::disable() {
+ enabled = false;
+ update_state();
+}
+
+void IOSVirtualController::update_state() {
+ if (is_enabled()) {
+ connect_controller();
+ } else {
+ disconnect_controller();
+ }
+}
+
+bool IOSVirtualController::is_enabled() {
+ return enabled;
+}
+
+void IOSVirtualController::connect_controller() {
+ if (@available(iOS 15.0, *)) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if (GCController.controllers.count == 0 && gcv_controller != nil) {
+ [gcv_controller connectWithReplyHandler:nil];
+ }
+ });
+ }
+}
+
+void IOSVirtualController::disconnect_controller() {
+ if (@available(iOS 15.0, *)) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if (gcv_controller) {
+ [gcv_controller disconnect];
+ }
+ });
+ }
+}
+
+void IOSVirtualController::controller_connected() {
+ if (@available(iOS 15.0, *)) {
+ if (gcv_controller != nil) {
+ BOOL has_physical_controller = NO;
+ for (GCController *controller in GCController.controllers) {
+ if (controller != gcv_controller.controller) {
+ has_physical_controller = YES;
+ break;
+ }
+ }
+ if (has_physical_controller) {
+ disconnect_controller();
+ }
+ }
+ }
+}
+
+void IOSVirtualController::controller_disconnected() {
+ if (is_enabled()) {
+ connect_controller();
+ }
+}
+
+void IOSVirtualController::set_enabled_left_thumbstick(bool p_enabled) {
+ if (enabled_left_thumbstick != p_enabled) {
+ enabled_left_thumbstick = p_enabled;
+ if (@available(iOS 15.0, *)) {
+ elements_changed(GCInputLeftThumbstick, !p_enabled);
+ }
+ }
+}
+
+bool IOSVirtualController::is_enabled_left_thumbstick() {
+ return enabled_left_thumbstick;
+}
+
+void IOSVirtualController::set_enabled_right_thumbstick(bool p_enabled) {
+ if (enabled_right_thumbstick != p_enabled) {
+ enabled_right_thumbstick = p_enabled;
+ if (@available(iOS 15.0, *)) {
+ elements_changed(GCInputRightThumbstick, !p_enabled);
+ }
+ }
+}
+
+bool IOSVirtualController::is_enabled_right_thumbstick() {
+ return enabled_right_thumbstick;
+}
+
+void IOSVirtualController::set_enabled_button_a(bool p_enabled) {
+ if (enabled_button_a != p_enabled) {
+ enabled_button_a = p_enabled;
+ if (@available(iOS 15.0, *)) {
+ elements_changed(GCInputButtonA, !p_enabled);
+ }
+ }
+}
+
+bool IOSVirtualController::is_enabled_button_a() {
+ return enabled_button_a;
+}
+
+void IOSVirtualController::set_enabled_button_b(bool p_enabled) {
+ if (enabled_button_b != p_enabled) {
+ enabled_button_b = p_enabled;
+ if (@available(iOS 15.0, *)) {
+ elements_changed(GCInputButtonB, !p_enabled);
+ }
+ }
+}
+
+bool IOSVirtualController::is_enabled_button_b() {
+ return enabled_button_b;
+}
+
+void IOSVirtualController::set_enabled_button_x(bool p_enabled) {
+ if (enabled_button_x != p_enabled) {
+ enabled_button_x = p_enabled;
+ if (@available(iOS 15.0, *)) {
+ elements_changed(GCInputButtonX, !p_enabled);
+ }
+ }
+}
+
+bool IOSVirtualController::is_enabled_button_x() {
+ return enabled_button_x;
+}
+
+void IOSVirtualController::set_enabled_button_y(bool p_enabled) {
+ if (enabled_button_y != p_enabled) {
+ enabled_button_y = p_enabled;
+ if (@available(iOS 15.0, *)) {
+ elements_changed(GCInputButtonY, !p_enabled);
+ }
+ }
+}
+
+bool IOSVirtualController::is_enabled_button_y() {
+ return enabled_button_y;
+}