diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp
index 0e078c32c978..deffcc3f50cd 100644
--- a/core/config/project_settings.cpp
+++ b/core/config/project_settings.cpp
@@ -1852,6 +1852,7 @@ ProjectSettings::ProjectSettings() {
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/pointing/android/disable_scroll_deadzone", false);
+ GLOBAL_DEF_BASIC("input_devices/virtual_controller/enable_controller", false);
// 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/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index 14e3133aee36..6651884df5f4 100644
--- a/doc/classes/ProjectSettings.xml
+++ b/doc/classes/ProjectSettings.xml
@@ -1716,6 +1716,9 @@
If [code]true[/code], the magnetometer sensor is enabled and [method Input.get_magnetometer] returns valid data.
+
+ If [code]true[/code], the virtual controller is enabled and will be created in the scene.
+
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/VirtualJoystick.xml b/doc/classes/VirtualJoystick.xml
index 4538c0acf19e..4c8e6836b190 100644
--- a/doc/classes/VirtualJoystick.xml
+++ b/doc/classes/VirtualJoystick.xml
@@ -62,6 +62,12 @@
Emitted when the tip moved outside the deadzone and the joystick is released. The [param input_vector] contains the last input direction and strength before release. Its length is between [code]0.0[/code] and [code]1.0[/code].
+
+
+
+ Emitted when the input vector changes.
+
+
Emitted when the joystick is pressed.
diff --git a/scene/gui/virtual_controller.cpp b/scene/gui/virtual_controller.cpp
new file mode 100644
index 000000000000..5c28dfa4d131
--- /dev/null
+++ b/scene/gui/virtual_controller.cpp
@@ -0,0 +1,587 @@
+/**************************************************************************/
+/* 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"
+
+#include "core/config/engine.h"
+#include "core/input/input.h"
+#include "core/object/callable_mp.h"
+#include "scene/gui/button.h"
+#include "scene/gui/virtual_joystick.h"
+#include "scene/theme/theme_db.h"
+
+void VirtualController::_setup_controls() {
+ float min_size = MIN(get_size().x, get_size().y);
+ int margin = min_size * 0.05f;
+ Size2 joystick_size(min_size * 0.25f, min_size * 0.25f);
+
+ left_joystick = memnew(VirtualJoystick);
+ left_joystick->set_joystick_size(joystick_size.x);
+ left_joystick->set_h_grow_direction(Control::GROW_DIRECTION_END);
+ left_joystick->set_v_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ left_joystick->set_custom_minimum_size(joystick_size);
+ left_joystick->set_anchors_and_offsets_preset(Control::PRESET_BOTTOM_LEFT);
+ left_joystick->set_offset(Side::SIDE_BOTTOM, -margin);
+ left_joystick->set_offset(Side::SIDE_LEFT, margin + min_size * 0.3f);
+ add_child(left_joystick, true, Node::INTERNAL_MODE_FRONT);
+ _update_left_joystick_style(left_joystick);
+
+ right_joystick = memnew(VirtualJoystick);
+ right_joystick->set_joystick_size(joystick_size.x);
+ right_joystick->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ right_joystick->set_v_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ right_joystick->set_custom_minimum_size(joystick_size);
+ right_joystick->set_anchors_and_offsets_preset(Control::PRESET_BOTTOM_RIGHT);
+ right_joystick->set_offset(Side::SIDE_BOTTOM, -margin);
+ right_joystick->set_offset(Side::SIDE_RIGHT, -margin + min_size * -0.3f);
+ add_child(right_joystick, true, Node::INTERNAL_MODE_FRONT);
+ _update_right_joystick_style(right_joystick);
+
+ Size2 button_size(min_size * 0.125f, min_size * 0.125f);
+
+ left_joystick_button = memnew(Button);
+ left_joystick_button->set_text("L3");
+ left_joystick_button->set_focus_mode(FOCUS_NONE);
+ left_joystick_button->set_h_grow_direction(Control::GROW_DIRECTION_END);
+ left_joystick_button->set_v_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ left_joystick_button->set_custom_minimum_size(button_size);
+ left_joystick_button->set_anchors_and_offsets_preset(Control::PRESET_BOTTOM_LEFT);
+ left_joystick_button->set_offset(SIDE_BOTTOM, -margin);
+ left_joystick_button->set_offset(SIDE_LEFT, margin);
+ add_child(left_joystick_button, true, Node::INTERNAL_MODE_FRONT);
+ _update_button_style(left_joystick_button);
+
+ right_joystick_button = memnew(Button);
+ right_joystick_button->set_text("R3");
+ right_joystick_button->set_focus_mode(FOCUS_NONE);
+ right_joystick_button->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ right_joystick_button->set_v_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ right_joystick_button->set_custom_minimum_size(button_size);
+ right_joystick_button->set_anchors_and_offsets_preset(Control::PRESET_BOTTOM_RIGHT);
+ right_joystick_button->set_offset(SIDE_BOTTOM, -margin);
+ right_joystick_button->set_offset(SIDE_RIGHT, -margin);
+ add_child(right_joystick_button, true, Node::INTERNAL_MODE_FRONT);
+ _update_button_style(right_joystick_button);
+
+ float start_bottom_offset = min_size * -0.25f;
+
+ dpad_down = memnew(Button);
+ dpad_down->set_text("D");
+ dpad_down->set_focus_mode(FOCUS_NONE);
+ dpad_down->set_h_grow_direction(Control::GROW_DIRECTION_END);
+ dpad_down->set_v_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ dpad_down->set_custom_minimum_size(button_size);
+ dpad_down->set_anchors_and_offsets_preset(Control::PRESET_BOTTOM_LEFT);
+ dpad_down->set_offset(SIDE_BOTTOM, start_bottom_offset);
+ dpad_down->set_offset(SIDE_LEFT, margin + button_size.x);
+ add_child(dpad_down, true, Node::INTERNAL_MODE_FRONT);
+ _update_button_style(dpad_down);
+
+ dpad_up = memnew(Button);
+ dpad_up->set_text("U");
+ dpad_up->set_focus_mode(FOCUS_NONE);
+ dpad_up->set_h_grow_direction(Control::GROW_DIRECTION_END);
+ dpad_up->set_v_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ dpad_up->set_custom_minimum_size(button_size);
+ dpad_up->set_anchors_and_offsets_preset(Control::PRESET_BOTTOM_LEFT);
+ dpad_up->set_offset(SIDE_BOTTOM, start_bottom_offset - 2 * button_size.y);
+ dpad_up->set_offset(SIDE_LEFT, margin + button_size.x);
+ add_child(dpad_up, true, Node::INTERNAL_MODE_FRONT);
+ _update_button_style(dpad_up);
+
+ dpad_left = memnew(Button);
+ dpad_left->set_text("L");
+ dpad_left->set_focus_mode(FOCUS_NONE);
+ dpad_left->set_h_grow_direction(Control::GROW_DIRECTION_END);
+ dpad_left->set_v_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ dpad_left->set_custom_minimum_size(button_size);
+ dpad_left->set_anchors_and_offsets_preset(Control::PRESET_BOTTOM_LEFT);
+ dpad_left->set_offset(SIDE_BOTTOM, start_bottom_offset - button_size.y);
+ dpad_left->set_offset(SIDE_LEFT, margin);
+ add_child(dpad_left, true, Node::INTERNAL_MODE_FRONT);
+ _update_button_style(dpad_left);
+
+ dpad_right = memnew(Button);
+ dpad_right->set_text("R");
+ dpad_right->set_focus_mode(FOCUS_NONE);
+ dpad_right->set_h_grow_direction(Control::GROW_DIRECTION_END);
+ dpad_right->set_v_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ dpad_right->set_custom_minimum_size(button_size);
+ dpad_right->set_anchors_and_offsets_preset(Control::PRESET_BOTTOM_LEFT);
+ dpad_right->set_offset(SIDE_BOTTOM, start_bottom_offset - button_size.y);
+ dpad_right->set_offset(SIDE_LEFT, margin + 2 * button_size.x);
+ add_child(dpad_right, true, Node::INTERNAL_MODE_FRONT);
+ _update_button_style(dpad_right);
+
+ button_a = memnew(Button);
+ button_a->set_text("A");
+ button_a->set_focus_mode(FOCUS_NONE);
+ button_a->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ button_a->set_v_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ button_a->set_custom_minimum_size(button_size);
+ button_a->set_anchors_and_offsets_preset(Control::PRESET_BOTTOM_RIGHT);
+ button_a->set_offset(SIDE_BOTTOM, start_bottom_offset);
+ button_a->set_offset(SIDE_RIGHT, -margin - button_size.x);
+ add_child(button_a, true, Node::INTERNAL_MODE_FRONT);
+ _update_button_style(button_a);
+
+ button_y = memnew(Button);
+ button_y->set_text("Y");
+ button_y->set_focus_mode(FOCUS_NONE);
+ button_y->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ button_y->set_v_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ button_y->set_custom_minimum_size(button_size);
+ button_y->set_anchors_and_offsets_preset(Control::PRESET_BOTTOM_RIGHT);
+ button_y->set_offset(SIDE_BOTTOM, start_bottom_offset - 2 * button_size.y);
+ button_y->set_offset(SIDE_RIGHT, -margin - button_size.x);
+ add_child(button_y, true, Node::INTERNAL_MODE_FRONT);
+ _update_button_style(button_y);
+
+ button_b = memnew(Button);
+ button_b->set_text("B");
+ button_b->set_focus_mode(FOCUS_NONE);
+ button_b->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ button_b->set_v_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ button_b->set_custom_minimum_size(button_size);
+ button_b->set_anchors_and_offsets_preset(Control::PRESET_BOTTOM_RIGHT);
+ button_b->set_offset(SIDE_BOTTOM, start_bottom_offset - button_size.y);
+ button_b->set_offset(SIDE_RIGHT, -margin);
+ add_child(button_b, true, Node::INTERNAL_MODE_FRONT);
+ _update_button_style(button_b);
+
+ button_x = memnew(Button);
+ button_x->set_text("X");
+ button_x->set_focus_mode(FOCUS_NONE);
+ button_x->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ button_x->set_v_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ button_x->set_custom_minimum_size(button_size);
+ button_x->set_anchors_and_offsets_preset(Control::PRESET_BOTTOM_RIGHT);
+ button_x->set_offset(SIDE_BOTTOM, start_bottom_offset - button_size.y);
+ button_x->set_offset(SIDE_RIGHT, -margin - 2 * button_size.x);
+ add_child(button_x, true, Node::INTERNAL_MODE_FRONT);
+ _update_button_style(button_x);
+
+ left_trigger = memnew(Button);
+ left_trigger->set_text("LT");
+ left_trigger->set_focus_mode(FOCUS_NONE);
+ left_trigger->set_h_grow_direction(Control::GROW_DIRECTION_END);
+ left_trigger->set_v_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ left_trigger->set_custom_minimum_size(button_size);
+ left_trigger->set_anchors_and_offsets_preset(Control::PRESET_BOTTOM_LEFT);
+ left_trigger->set_offset(SIDE_BOTTOM, start_bottom_offset - 3 * button_size.y - margin);
+ left_trigger->set_offset(SIDE_LEFT, margin);
+ add_child(left_trigger, true, Node::INTERNAL_MODE_FRONT);
+ _update_button_style(left_trigger);
+
+ left_shoulder = memnew(Button);
+ left_shoulder->set_text("LB");
+ left_shoulder->set_focus_mode(FOCUS_NONE);
+ left_shoulder->set_h_grow_direction(Control::GROW_DIRECTION_END);
+ left_shoulder->set_v_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ left_shoulder->set_custom_minimum_size(button_size);
+ left_shoulder->set_anchors_and_offsets_preset(Control::PRESET_BOTTOM_LEFT);
+ left_shoulder->set_offset(SIDE_BOTTOM, start_bottom_offset - 3 * button_size.y - margin);
+ left_shoulder->set_offset(SIDE_LEFT, margin + 2 * button_size.x);
+ add_child(left_shoulder, true, Node::INTERNAL_MODE_FRONT);
+ _update_button_style(left_shoulder);
+
+ right_trigger = memnew(Button);
+ right_trigger->set_text("RT");
+ right_trigger->set_focus_mode(FOCUS_NONE);
+ right_trigger->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ right_trigger->set_v_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ right_trigger->set_custom_minimum_size(button_size);
+ right_trigger->set_anchors_and_offsets_preset(Control::PRESET_BOTTOM_RIGHT);
+ right_trigger->set_offset(SIDE_BOTTOM, start_bottom_offset - 3 * button_size.y - margin);
+ right_trigger->set_offset(SIDE_RIGHT, -margin);
+ add_child(right_trigger, true, Node::INTERNAL_MODE_FRONT);
+ _update_button_style(right_trigger);
+
+ right_shoulder = memnew(Button);
+ right_shoulder->set_text("RB");
+ right_shoulder->set_focus_mode(FOCUS_NONE);
+ right_shoulder->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ right_shoulder->set_v_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ right_shoulder->set_custom_minimum_size(button_size);
+ right_shoulder->set_anchors_and_offsets_preset(Control::PRESET_BOTTOM_RIGHT);
+ right_shoulder->set_offset(SIDE_BOTTOM, start_bottom_offset - 3 * button_size.y - margin);
+ right_shoulder->set_offset(SIDE_RIGHT, -margin - 2 * button_size.x);
+ add_child(right_shoulder, true, Node::INTERNAL_MODE_FRONT);
+ _update_button_style(right_shoulder);
+
+ guide_button = memnew(Button);
+ guide_button->set_text("G");
+ guide_button->set_focus_mode(FOCUS_NONE);
+ guide_button->set_h_grow_direction(Control::GROW_DIRECTION_BOTH);
+ guide_button->set_v_grow_direction(Control::GROW_DIRECTION_END);
+ guide_button->set_custom_minimum_size(button_size);
+ guide_button->set_anchors_and_offsets_preset(Control::PRESET_CENTER_TOP);
+ guide_button->set_offset(SIDE_TOP, margin);
+ add_child(guide_button, true, Node::INTERNAL_MODE_FRONT);
+ _update_button_style(guide_button);
+
+ back_button = memnew(Button);
+ back_button->set_text("<-");
+ back_button->set_focus_mode(FOCUS_NONE);
+ back_button->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN);
+ back_button->set_v_grow_direction(Control::GROW_DIRECTION_END);
+ back_button->set_custom_minimum_size(button_size);
+ back_button->set_anchors_and_offsets_preset(Control::PRESET_CENTER_TOP);
+ back_button->set_offset(SIDE_TOP, margin);
+ back_button->set_offset(SIDE_RIGHT, -button_size.x - margin);
+ add_child(back_button, true, Node::INTERNAL_MODE_FRONT);
+ _update_button_style(back_button);
+
+ start_button = memnew(Button);
+ start_button->set_text("->");
+ start_button->set_focus_mode(FOCUS_NONE);
+ start_button->set_h_grow_direction(Control::GROW_DIRECTION_END);
+ start_button->set_v_grow_direction(Control::GROW_DIRECTION_END);
+ start_button->set_custom_minimum_size(button_size);
+ start_button->set_anchors_and_offsets_preset(Control::PRESET_CENTER_TOP);
+ start_button->set_offset(SIDE_TOP, margin);
+ start_button->set_offset(SIDE_LEFT, button_size.x + margin);
+ add_child(start_button, true, Node::INTERNAL_MODE_FRONT);
+ _update_button_style(start_button);
+
+ left_joystick->connect("input_changed", callable_mp(this, &VirtualController::_on_left_joystick_motion));
+ left_joystick_button->connect("button_down", callable_mp(this, &VirtualController::_on_left_joystick_pressed));
+ left_joystick_button->connect("button_up", callable_mp(this, &VirtualController::_on_left_joystick_released));
+ right_joystick->connect("input_changed", callable_mp(this, &VirtualController::_on_right_joystick_motion));
+ right_joystick_button->connect("button_down", callable_mp(this, &VirtualController::_on_right_joystick_pressed));
+ right_joystick_button->connect("button_up", callable_mp(this, &VirtualController::_on_right_joystick_released));
+ dpad_up->connect("button_down", callable_mp(this, &VirtualController::_on_dpad_up_pressed));
+ dpad_up->connect("button_up", callable_mp(this, &VirtualController::_on_dpad_up_released));
+ dpad_down->connect("button_down", callable_mp(this, &VirtualController::_on_dpad_down_pressed));
+ dpad_down->connect("button_up", callable_mp(this, &VirtualController::_on_dpad_down_released));
+ dpad_left->connect("button_down", callable_mp(this, &VirtualController::_on_dpad_left_pressed));
+ dpad_left->connect("button_up", callable_mp(this, &VirtualController::_on_dpad_left_released));
+ dpad_right->connect("button_down", callable_mp(this, &VirtualController::_on_dpad_right_pressed));
+ dpad_right->connect("button_up", callable_mp(this, &VirtualController::_on_dpad_right_released));
+ button_a->connect("button_down", callable_mp(this, &VirtualController::_on_button_a_pressed));
+ button_a->connect("button_up", callable_mp(this, &VirtualController::_on_button_a_released));
+ button_b->connect("button_down", callable_mp(this, &VirtualController::_on_button_b_pressed));
+ button_b->connect("button_up", callable_mp(this, &VirtualController::_on_button_b_released));
+ button_x->connect("button_down", callable_mp(this, &VirtualController::_on_button_x_pressed));
+ button_x->connect("button_up", callable_mp(this, &VirtualController::_on_button_x_released));
+ button_y->connect("button_down", callable_mp(this, &VirtualController::_on_button_y_pressed));
+ button_y->connect("button_up", callable_mp(this, &VirtualController::_on_button_y_released));
+ left_shoulder->connect("button_down", callable_mp(this, &VirtualController::_on_left_shoulder_pressed));
+ left_shoulder->connect("button_up", callable_mp(this, &VirtualController::_on_left_shoulder_released));
+ right_shoulder->connect("button_down", callable_mp(this, &VirtualController::_on_right_shoulder_pressed));
+ right_shoulder->connect("button_up", callable_mp(this, &VirtualController::_on_right_shoulder_released));
+ left_trigger->connect("button_down", callable_mp(this, &VirtualController::_on_left_trigger_pressed));
+ left_trigger->connect("button_up", callable_mp(this, &VirtualController::_on_left_trigger_released));
+ right_trigger->connect("button_down", callable_mp(this, &VirtualController::_on_right_trigger_pressed));
+ right_trigger->connect("button_up", callable_mp(this, &VirtualController::_on_right_trigger_released));
+ start_button->connect("button_down", callable_mp(this, &VirtualController::_on_start_button_pressed));
+ start_button->connect("button_up", callable_mp(this, &VirtualController::_on_start_button_released));
+ back_button->connect("button_down", callable_mp(this, &VirtualController::_on_back_button_pressed));
+ back_button->connect("button_up", callable_mp(this, &VirtualController::_on_back_button_released));
+ guide_button->connect("button_down", callable_mp(this, &VirtualController::_on_guide_button_pressed));
+ guide_button->connect("button_up", callable_mp(this, &VirtualController::_on_guide_button_released));
+}
+
+void VirtualController::_bind_methods() {
+ BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, VirtualController, button_normal);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, VirtualController, button_pressed);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, VirtualController, left_normal_joystick);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, VirtualController, left_normal_tip);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, VirtualController, left_pressed_joystick);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, VirtualController, left_pressed_tip);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, VirtualController, right_normal_joystick);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, VirtualController, right_normal_tip);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, VirtualController, right_pressed_joystick);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, VirtualController, right_pressed_tip);
+}
+
+void VirtualController::_notification(int p_notification) {
+ ERR_MAIN_THREAD_GUARD;
+ switch (p_notification) {
+ case NOTIFICATION_ENTER_TREE: {
+ if (Engine::get_singleton()->is_editor_hint()) {
+ return;
+ }
+
+ device_id = Input::get_singleton()->get_unused_joy_id();
+ Dictionary info;
+ info["mapping_handled"] = true;
+ Input::get_singleton()->joy_connection_changed(device_id, true, "Virtual Controller", "virtual_controller", info);
+ } break;
+
+ case NOTIFICATION_EXIT_TREE: {
+ if (Engine::get_singleton()->is_editor_hint()) {
+ return;
+ }
+
+ Input::get_singleton()->joy_connection_changed(device_id, false, "", "", Dictionary());
+ device_id = -1;
+ } break;
+
+ case NOTIFICATION_PROCESS: {
+ if (Engine::get_singleton()->is_editor_hint()) {
+ return;
+ }
+ //set_visible(Input::get_singleton()->is_virtual_controller_enabled());
+ } break;
+
+ case NOTIFICATION_THEME_CHANGED: {
+ _update_left_joystick_style(left_joystick);
+ _update_right_joystick_style(right_joystick);
+ _update_button_style(left_joystick_button);
+ _update_button_style(right_joystick_button);
+ _update_button_style(dpad_up);
+ _update_button_style(dpad_down);
+ _update_button_style(dpad_left);
+ _update_button_style(dpad_right);
+ _update_button_style(button_a);
+ _update_button_style(button_b);
+ _update_button_style(button_x);
+ _update_button_style(button_y);
+ _update_button_style(left_shoulder);
+ _update_button_style(right_shoulder);
+ _update_button_style(left_trigger);
+ _update_button_style(right_trigger);
+ _update_button_style(start_button);
+ _update_button_style(back_button);
+ _update_button_style(guide_button);
+ } break;
+ }
+}
+
+void VirtualController::_create_button_event(JoyButton p_button, bool p_pressed) {
+ Ref button_event;
+ button_event.instantiate();
+ button_event->set_button_index(p_button);
+ button_event->set_pressed(p_pressed);
+ button_event->set_device(device_id);
+
+ Input::get_singleton()->parse_input_event(button_event);
+}
+
+void VirtualController::_create_motion_event(JoyAxis p_axis, float p_value) {
+ Ref motion_event;
+ motion_event.instantiate();
+ motion_event->set_axis(p_axis);
+ motion_event->set_axis_value(p_value);
+ motion_event->set_device(device_id);
+
+ Input::get_singleton()->parse_input_event(motion_event);
+}
+
+void VirtualController::_update_left_joystick_style(VirtualJoystick *p_joystick) {
+ if (p_joystick == nullptr) {
+ return;
+ }
+
+ p_joystick->add_theme_style_override("normal_joystick", theme_cache.left_normal_joystick);
+ p_joystick->add_theme_style_override("normal_tip", theme_cache.left_normal_tip);
+ p_joystick->add_theme_style_override("pressed_joystick", theme_cache.left_pressed_joystick);
+ p_joystick->add_theme_style_override("pressed_tip", theme_cache.left_pressed_tip);
+}
+
+void VirtualController::_update_right_joystick_style(VirtualJoystick *p_joystick) {
+ if (p_joystick == nullptr) {
+ return;
+ }
+
+ p_joystick->add_theme_style_override("normal_joystick", theme_cache.right_normal_joystick);
+ p_joystick->add_theme_style_override("normal_tip", theme_cache.right_normal_tip);
+ p_joystick->add_theme_style_override("pressed_joystick", theme_cache.right_pressed_joystick);
+ p_joystick->add_theme_style_override("pressed_tip", theme_cache.right_pressed_tip);
+}
+
+void VirtualController::_update_button_style(Button *p_button) {
+ if (p_button == nullptr) {
+ return;
+ }
+
+ p_button->add_theme_style_override(CoreStringName(normal), theme_cache.button_normal);
+ p_button->add_theme_style_override(SceneStringName(hover), theme_cache.button_normal);
+ p_button->add_theme_style_override(SceneStringName(pressed), theme_cache.button_pressed);
+}
+
+void VirtualController::_on_left_joystick_motion(Vector2 p_value) {
+ _create_motion_event(JoyAxis::LEFT_X, p_value.x);
+ _create_motion_event(JoyAxis::LEFT_Y, p_value.y);
+}
+
+void VirtualController::_on_left_joystick_pressed() {
+ _create_button_event(JoyButton::LEFT_STICK, true);
+}
+
+void VirtualController::_on_left_joystick_released() {
+ _create_button_event(JoyButton::LEFT_STICK, false);
+}
+
+void VirtualController::_on_right_joystick_motion(Vector2 p_value) {
+ _create_motion_event(JoyAxis::RIGHT_X, p_value.x);
+ _create_motion_event(JoyAxis::RIGHT_Y, p_value.y);
+}
+
+void VirtualController::_on_right_joystick_pressed() {
+ _create_button_event(JoyButton::RIGHT_STICK, true);
+}
+
+void VirtualController::_on_right_joystick_released() {
+ _create_button_event(JoyButton::RIGHT_STICK, false);
+}
+
+void VirtualController::_on_dpad_up_pressed() {
+ _create_button_event(JoyButton::DPAD_UP, true);
+}
+
+void VirtualController::_on_dpad_up_released() {
+ _create_button_event(JoyButton::DPAD_UP, false);
+}
+
+void VirtualController::_on_dpad_down_pressed() {
+ _create_button_event(JoyButton::DPAD_DOWN, true);
+}
+
+void VirtualController::_on_dpad_down_released() {
+ _create_button_event(JoyButton::DPAD_DOWN, false);
+}
+
+void VirtualController::_on_dpad_left_pressed() {
+ _create_button_event(JoyButton::DPAD_LEFT, true);
+}
+
+void VirtualController::_on_dpad_left_released() {
+ _create_button_event(JoyButton::DPAD_LEFT, false);
+}
+
+void VirtualController::_on_dpad_right_pressed() {
+ _create_button_event(JoyButton::DPAD_RIGHT, true);
+}
+
+void VirtualController::_on_dpad_right_released() {
+ _create_button_event(JoyButton::DPAD_RIGHT, false);
+}
+
+void VirtualController::_on_button_a_pressed() {
+ _create_button_event(JoyButton::A, true);
+}
+
+void VirtualController::_on_button_a_released() {
+ _create_button_event(JoyButton::A, false);
+}
+
+void VirtualController::_on_button_b_pressed() {
+ _create_button_event(JoyButton::B, true);
+}
+
+void VirtualController::_on_button_b_released() {
+ _create_button_event(JoyButton::B, false);
+}
+
+void VirtualController::_on_button_x_pressed() {
+ _create_button_event(JoyButton::X, true);
+}
+
+void VirtualController::_on_button_x_released() {
+ _create_button_event(JoyButton::X, false);
+}
+
+void VirtualController::_on_button_y_pressed() {
+ _create_button_event(JoyButton::Y, true);
+}
+
+void VirtualController::_on_button_y_released() {
+ _create_button_event(JoyButton::Y, false);
+}
+
+void VirtualController::_on_left_shoulder_pressed() {
+ _create_button_event(JoyButton::LEFT_SHOULDER, true);
+}
+
+void VirtualController::_on_left_shoulder_released() {
+ _create_button_event(JoyButton::LEFT_SHOULDER, false);
+}
+
+void VirtualController::_on_right_shoulder_pressed() {
+ _create_button_event(JoyButton::RIGHT_SHOULDER, true);
+}
+
+void VirtualController::_on_right_shoulder_released() {
+ _create_button_event(JoyButton::RIGHT_SHOULDER, false);
+}
+
+void VirtualController::_on_left_trigger_pressed() {
+ _create_motion_event(JoyAxis::TRIGGER_LEFT, 1.0f);
+}
+
+void VirtualController::_on_left_trigger_released() {
+ _create_motion_event(JoyAxis::TRIGGER_LEFT, 0.0f);
+}
+
+void VirtualController::_on_right_trigger_pressed() {
+ _create_motion_event(JoyAxis::TRIGGER_RIGHT, 1.0f);
+}
+
+void VirtualController::_on_right_trigger_released() {
+ _create_motion_event(JoyAxis::TRIGGER_RIGHT, 0.0f);
+}
+
+void VirtualController::_on_start_button_pressed() {
+ _create_button_event(JoyButton::START, true);
+}
+
+void VirtualController::_on_start_button_released() {
+ _create_button_event(JoyButton::START, false);
+}
+
+void VirtualController::_on_back_button_pressed() {
+ _create_button_event(JoyButton::BACK, true);
+}
+
+void VirtualController::_on_back_button_released() {
+ _create_button_event(JoyButton::BACK, false);
+}
+
+void VirtualController::_on_guide_button_pressed() {
+ _create_button_event(JoyButton::GUIDE, true);
+}
+
+void VirtualController::_on_guide_button_released() {
+ _create_button_event(JoyButton::GUIDE, false);
+}
+
+VirtualController::VirtualController() {
+ if (Engine::get_singleton()->is_editor_hint()) {
+ return;
+ }
+
+ set_process(true);
+ set_focus_mode(FOCUS_NONE);
+ set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
+
+ callable_mp(this, &VirtualController::_setup_controls).call_deferred();
+}
diff --git a/scene/gui/virtual_controller.h b/scene/gui/virtual_controller.h
new file mode 100644
index 000000000000..4dc2bb07795f
--- /dev/null
+++ b/scene/gui/virtual_controller.h
@@ -0,0 +1,128 @@
+/**************************************************************************/
+/* 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 "scene/gui/control.h"
+
+class Button;
+class VirtualJoystick;
+
+class VirtualController : public Control {
+ GDCLASS(VirtualController, Control);
+
+private:
+ int device_id = -1;
+
+ struct ThemeCache {
+ Ref button_normal;
+ Ref button_pressed;
+ Ref left_normal_joystick;
+ Ref left_normal_tip;
+ Ref left_pressed_joystick;
+ Ref left_pressed_tip;
+ Ref right_normal_joystick;
+ Ref right_normal_tip;
+ Ref right_pressed_joystick;
+ Ref right_pressed_tip;
+ } theme_cache;
+
+ VirtualJoystick *left_joystick = nullptr;
+ VirtualJoystick *right_joystick = nullptr;
+ Button *left_joystick_button = nullptr;
+ Button *right_joystick_button = nullptr;
+ Button *dpad_up = nullptr;
+ Button *dpad_down = nullptr;
+ Button *dpad_left = nullptr;
+ Button *dpad_right = nullptr;
+ Button *button_a = nullptr;
+ Button *button_b = nullptr;
+ Button *button_x = nullptr;
+ Button *button_y = nullptr;
+ Button *left_shoulder = nullptr;
+ Button *right_shoulder = nullptr;
+ Button *left_trigger = nullptr;
+ Button *right_trigger = nullptr;
+ Button *start_button = nullptr;
+ Button *back_button = nullptr;
+ Button *guide_button = nullptr;
+
+ void _setup_controls();
+
+ void _create_button_event(JoyButton p_button, bool p_pressed);
+ void _create_motion_event(JoyAxis p_axis, float p_value);
+ void _update_left_joystick_style(VirtualJoystick *p_joystick);
+ void _update_right_joystick_style(VirtualJoystick *p_joystick);
+ void _update_button_style(Button *p_button);
+
+ void _on_left_joystick_motion(Vector2 p_value);
+ void _on_right_joystick_motion(Vector2 p_value);
+ void _on_left_joystick_pressed();
+ void _on_left_joystick_released();
+ void _on_right_joystick_pressed();
+ void _on_right_joystick_released();
+ void _on_dpad_up_pressed();
+ void _on_dpad_up_released();
+ void _on_dpad_down_pressed();
+ void _on_dpad_down_released();
+ void _on_dpad_left_pressed();
+ void _on_dpad_left_released();
+ void _on_dpad_right_pressed();
+ void _on_dpad_right_released();
+ void _on_button_a_pressed();
+ void _on_button_a_released();
+ void _on_button_b_pressed();
+ void _on_button_b_released();
+ void _on_button_x_pressed();
+ void _on_button_x_released();
+ void _on_button_y_pressed();
+ void _on_button_y_released();
+ void _on_left_shoulder_pressed();
+ void _on_left_shoulder_released();
+ void _on_right_shoulder_pressed();
+ void _on_right_shoulder_released();
+ void _on_left_trigger_pressed();
+ void _on_left_trigger_released();
+ void _on_right_trigger_pressed();
+ void _on_right_trigger_released();
+ void _on_start_button_pressed();
+ void _on_start_button_released();
+ void _on_back_button_pressed();
+ void _on_back_button_released();
+ void _on_guide_button_pressed();
+ void _on_guide_button_released();
+
+protected:
+ static void _bind_methods();
+ void _notification(int p_what);
+
+public:
+ VirtualController();
+};
diff --git a/scene/gui/virtual_joystick.cpp b/scene/gui/virtual_joystick.cpp
index b05d332cefb7..5399bc072a49 100644
--- a/scene/gui/virtual_joystick.cpp
+++ b/scene/gui/virtual_joystick.cpp
@@ -56,6 +56,7 @@ void VirtualJoystick::gui_input(const Ref &p_event) {
} else if (touch->get_index() == touch_index) {
is_pressed = false;
emit_signal(SNAME("released"), input_vector);
+ emit_signal(SNAME("input_changed"), Vector2());
if (!is_flick_canceled && !has_moved) {
emit_signal(SNAME("tapped"));
@@ -127,6 +128,8 @@ void VirtualJoystick::_update_joystick(const Vector2 &p_pos) {
input_vector = Vector2();
}
+ emit_signal(SNAME("input_changed"), input_vector);
+
if (!is_flick_canceled && was_pressed && !has_input) {
is_flick_canceled = true;
emit_signal(SNAME("flick_canceled"));
@@ -247,6 +250,7 @@ void VirtualJoystick::_bind_methods() {
ADD_SIGNAL(MethodInfo("released", PropertyInfo(Variant::VECTOR2, "input_vector")));
ADD_SIGNAL(MethodInfo("flicked", PropertyInfo(Variant::VECTOR2, "input_vector")));
ADD_SIGNAL(MethodInfo("flick_canceled"));
+ ADD_SIGNAL(MethodInfo("input_changed", PropertyInfo(Variant::VECTOR2, "input_vector")));
BIND_ENUM_CONSTANT(JOYSTICK_FIXED);
BIND_ENUM_CONSTANT(JOYSTICK_DYNAMIC);
diff --git a/scene/main/scene_tree.cpp b/scene/main/scene_tree.cpp
index dd92f0f498c9..64a6e6807c9f 100644
--- a/scene/main/scene_tree.cpp
+++ b/scene/main/scene_tree.cpp
@@ -45,6 +45,8 @@ STATIC_ASSERT_INCOMPLETE_TYPE(class, RenderingServer);
#include "scene/animation/tween.h"
#include "scene/debugger/scene_debugger.h"
#include "scene/gui/control.h"
+#include "scene/gui/virtual_controller.h"
+#include "scene/main/canvas_layer.h"
#include "scene/main/multiplayer_api.h"
#include "scene/main/node.h"
#include "scene/main/viewport.h"
@@ -1887,6 +1889,43 @@ bool SceneTree::is_multiplayer_poll_enabled() const {
return multiplayer_poll;
}
+void SceneTree::set_virtual_controller_visible(bool p_visible) {
+ if (p_visible) {
+ if (virtual_controller_canvas_layer == nullptr) {
+ virtual_controller_canvas_layer = memnew(CanvasLayer);
+ // Begin name with an underscore to avoid conflict with project nodes.
+ virtual_controller_canvas_layer->set_name("_VirtualControllerCanvasLayer");
+ get_root()->add_child(virtual_controller_canvas_layer, false, Node::INTERNAL_MODE_BACK);
+
+ virtual_controller = memnew(VirtualController);
+ virtual_controller->set_name("_VirtualController");
+ virtual_controller_canvas_layer->add_child(virtual_controller, false, Node::INTERNAL_MODE_BACK);
+ } else {
+ ERR_PRINT("Virtual controller already exists.");
+ }
+ } else {
+ if (virtual_controller_canvas_layer != nullptr) {
+ // Free when closing to avoid reserving memory during the project's run duration.
+ virtual_controller_canvas_layer->queue_free();
+ virtual_controller_canvas_layer = nullptr;
+ virtual_controller = nullptr;
+ }
+ }
+}
+
+bool SceneTree::is_virtual_controller_visible() const {
+ if (virtual_controller_canvas_layer) {
+ return virtual_controller_canvas_layer->is_visible();
+ }
+
+ // Licenses dialog isn't created yet. Therefore, it's not visible.
+ return false;
+}
+
+void SceneTree::_project_settings_changed() {
+ set_virtual_controller_visible(ProjectSettings::get_singleton()->get("input_devices/virtual_controller/enable_controller"));
+}
+
void SceneTree::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_root"), &SceneTree::get_root);
ClassDB::bind_method(D_METHOD("has_group", "name"), &SceneTree::has_group);
@@ -2178,6 +2217,8 @@ SceneTree::SceneTree() {
Viewport::SDFScale sdf_scale = Viewport::SDFScale(int(GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/2d/sdf/scale", PROPERTY_HINT_ENUM, "100%,50%,25%"), 1)));
root->set_sdf_scale(sdf_scale);
+ ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &SceneTree::_project_settings_changed));
+
#ifndef _3D_DISABLED
{ // Load default fallback environment.
// Get possible extensions.
diff --git a/scene/main/scene_tree.h b/scene/main/scene_tree.h
index b7098290b411..851b7ec0da1d 100644
--- a/scene/main/scene_tree.h
+++ b/scene/main/scene_tree.h
@@ -41,6 +41,7 @@
#include
class ArrayMesh;
+class CanvasLayer;
class InputEvent;
class Material;
class MultiplayerAPI;
@@ -48,6 +49,7 @@ class Node;
class PackedScene;
class Tween;
class Viewport;
+class VirtualController;
class Window;
#ifndef _3D_DISABLED
@@ -204,6 +206,9 @@ class SceneTree : public MainLoop {
ObjectID prev_scene_id;
ObjectID pending_new_scene_id;
+ CanvasLayer *virtual_controller_canvas_layer = nullptr;
+ VirtualController *virtual_controller = nullptr;
+
Color debug_collisions_color;
Color debug_collision_contact_color;
Color debug_paths_color;
@@ -283,6 +288,8 @@ class SceneTree : public MainLoop {
//used by viewport
void _call_input_pause(const StringName &p_group, CallInputType p_call_type, const Ref &p_input, Viewport *p_viewport);
+ void _project_settings_changed();
+
protected:
void _notification(int p_notification);
static void _bind_methods();
@@ -451,6 +458,9 @@ class SceneTree : public MainLoop {
void set_multiplayer_poll_enabled(bool p_enabled);
bool is_multiplayer_poll_enabled() const;
+ void set_virtual_controller_visible(bool p_visible);
+ bool is_virtual_controller_visible() const;
+
static void add_idle_callback(IdleCallback p_callback);
void set_disable_node_threading(bool p_disable);
diff --git a/scene/theme/default_theme.cpp b/scene/theme/default_theme.cpp
index 3de32c443a7e..37a91d765569 100644
--- a/scene/theme/default_theme.cpp
+++ b/scene/theme/default_theme.cpp
@@ -681,6 +681,25 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const
theme->set_stylebox("pressed_joystick", "VirtualJoystick", style_joystick);
theme->set_stylebox("pressed_tip", "VirtualJoystick", style_joystick_tip);
+ // Virtual Controller
+
+ Ref style_virtual_controller_button = make_flat_stylebox(style_normal_color, 0, 0, 0, 0, 10000, false, 4 * scale);
+ style_virtual_controller_button->set_corner_detail(24 * scale);
+
+ Ref style_virtual_controller_button_pressed = make_flat_stylebox(style_pressed_color, 0, 0, 0, 0, 10000, true, 4 * scale);
+ style_virtual_controller_button_pressed->set_corner_detail(24 * scale);
+
+ theme->set_stylebox("button_normal", "VirtualController", style_virtual_controller_button);
+ theme->set_stylebox("button_pressed", "VirtualController", style_virtual_controller_button_pressed);
+ theme->set_stylebox("left_normal_joystick", "VirtualController", style_joystick);
+ theme->set_stylebox("left_normal_tip", "VirtualController", style_joystick_tip);
+ theme->set_stylebox("left_pressed_joystick", "VirtualController", style_joystick);
+ theme->set_stylebox("left_pressed_tip", "VirtualController", style_joystick_tip);
+ theme->set_stylebox("right_normal_joystick", "VirtualController", style_joystick);
+ theme->set_stylebox("right_normal_tip", "VirtualController", style_joystick_tip);
+ theme->set_stylebox("right_pressed_joystick", "VirtualController", style_joystick);
+ theme->set_stylebox("right_pressed_tip", "VirtualController", style_joystick_tip);
+
// Window
theme->set_stylebox("embedded_border", "Window", sb_expand(make_flat_stylebox(style_popup_color, 10, 28, 10, 8), 8, 32, 8, 6));