summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichael Alexsander <michaelalexsander@protonmail.com>2024-09-18 18:07:18 -0300
committerMichael Alexsander <michaelalexsander@protonmail.com>2024-10-30 11:42:17 -0300
commit16524a8a01408480ce2063461c8c042bc4eb3fa8 (patch)
treef82806e99d030ed26ddec9347681430db49fe199
parent8004c7524fb9f43425c4d6f614410a76678e0f7c (diff)
downloadredot-engine-16524a8a01408480ce2063461c8c042bc4eb3fa8.tar.gz
Add "Game" editor for better runtime debugging
-rw-r--r--core/config/engine.cpp8
-rw-r--r--core/config/engine.h5
-rw-r--r--core/input/input.cpp108
-rw-r--r--core/input/input.h10
-rw-r--r--doc/classes/EditorFeatureProfile.xml5
-rw-r--r--editor/debugger/editor_debugger_node.cpp8
-rw-r--r--editor/debugger/editor_debugger_node.h8
-rw-r--r--editor/debugger/editor_debugger_tree.cpp38
-rw-r--r--editor/debugger/editor_debugger_tree.h3
-rw-r--r--editor/debugger/script_editor_debugger.cpp89
-rw-r--r--editor/editor_feature_profile.cpp4
-rw-r--r--editor/editor_feature_profile.h1
-rw-r--r--editor/editor_main_screen.h1
-rw-r--r--editor/editor_node.cpp12
-rw-r--r--editor/icons/2DNodes.svg1
-rw-r--r--editor/icons/Camera.svg1
-rw-r--r--editor/icons/Game.svg1
-rw-r--r--editor/icons/NextFrame.svg1
-rw-r--r--editor/plugins/canvas_item_editor_plugin.cpp43
-rw-r--r--editor/plugins/canvas_item_editor_plugin.h4
-rw-r--r--editor/plugins/game_view_plugin.cpp478
-rw-r--r--editor/plugins/game_view_plugin.h152
-rw-r--r--editor/plugins/node_3d_editor_plugin.cpp96
-rw-r--r--editor/plugins/node_3d_editor_plugin.h10
-rw-r--r--editor/themes/editor_theme_manager.cpp6
-rw-r--r--misc/extension_api_validation/4.3-stable.expected8
-rw-r--r--scene/2d/camera_2d.cpp1
-rw-r--r--scene/2d/gpu_particles_2d.cpp2
-rw-r--r--scene/2d/navigation_agent_2d.cpp8
-rw-r--r--scene/2d/navigation_obstacle_2d.cpp8
-rw-r--r--scene/2d/touch_screen_button.cpp1
-rw-r--r--scene/3d/camera_3d.cpp1
-rw-r--r--scene/3d/gpu_particles_3d.cpp2
-rw-r--r--scene/3d/navigation_agent_3d.cpp8
-rw-r--r--scene/3d/navigation_obstacle_3d.cpp8
-rw-r--r--scene/audio/audio_stream_player_internal.cpp8
-rw-r--r--scene/debugger/scene_debugger.cpp1192
-rw-r--r--scene/debugger/scene_debugger.h161
-rw-r--r--scene/gui/video_stream_player.cpp8
-rw-r--r--scene/main/node.cpp13
-rw-r--r--scene/main/node.h3
-rw-r--r--scene/main/scene_tree.cpp27
-rw-r--r--scene/main/scene_tree.h3
-rw-r--r--scene/main/viewport.cpp83
-rw-r--r--scene/main/viewport.h8
45 files changed, 2375 insertions, 271 deletions
diff --git a/core/config/engine.cpp b/core/config/engine.cpp
index 12ada98d43..aac048e93f 100644
--- a/core/config/engine.cpp
+++ b/core/config/engine.cpp
@@ -116,6 +116,10 @@ void Engine::set_time_scale(double p_scale) {
}
double Engine::get_time_scale() const {
+ return freeze_time_scale ? 0 : _time_scale;
+}
+
+double Engine::get_unfrozen_time_scale() const {
return _time_scale;
}
@@ -404,6 +408,10 @@ bool Engine::notify_frame_server_synced() {
return server_syncs > SERVER_SYNC_FRAME_COUNT_WARNING;
}
+void Engine::set_freeze_time_scale(bool p_frozen) {
+ freeze_time_scale = p_frozen;
+}
+
Engine::Engine() {
singleton = this;
}
diff --git a/core/config/engine.h b/core/config/engine.h
index fd7fbd717e..b38412308a 100644
--- a/core/config/engine.h
+++ b/core/config/engine.h
@@ -99,6 +99,8 @@ private:
int server_syncs = 0;
bool frame_server_synced = false;
+ bool freeze_time_scale = false;
+
public:
static Engine *get_singleton();
@@ -130,6 +132,7 @@ public:
void set_time_scale(double p_scale);
double get_time_scale() const;
+ double get_unfrozen_time_scale() const;
void set_print_to_stdout(bool p_enabled);
bool is_printing_to_stdout() const;
@@ -197,6 +200,8 @@ public:
void increment_frames_drawn();
bool notify_frame_server_synced();
+ void set_freeze_time_scale(bool p_frozen);
+
Engine();
virtual ~Engine();
};
diff --git a/core/input/input.cpp b/core/input/input.cpp
index 6261a435fa..4117193c8b 100644
--- a/core/input/input.cpp
+++ b/core/input/input.cpp
@@ -87,11 +87,50 @@ Input *Input::get_singleton() {
void Input::set_mouse_mode(MouseMode p_mode) {
ERR_FAIL_INDEX((int)p_mode, 5);
+
+ if (p_mode == mouse_mode) {
+ return;
+ }
+
+ // Allow to be set even if overridden, to see if the platform allows the mode.
set_mouse_mode_func(p_mode);
+ mouse_mode = get_mouse_mode_func();
+
+ if (mouse_mode_override_enabled) {
+ set_mouse_mode_func(mouse_mode_override);
+ }
}
Input::MouseMode Input::get_mouse_mode() const {
- return get_mouse_mode_func();
+ return mouse_mode;
+}
+
+void Input::set_mouse_mode_override_enabled(bool p_enabled) {
+ if (p_enabled == mouse_mode_override_enabled) {
+ return;
+ }
+
+ mouse_mode_override_enabled = p_enabled;
+
+ if (p_enabled) {
+ set_mouse_mode_func(mouse_mode_override);
+ mouse_mode_override = get_mouse_mode_func();
+ } else {
+ set_mouse_mode_func(mouse_mode);
+ }
+}
+
+void Input::set_mouse_mode_override(MouseMode p_mode) {
+ ERR_FAIL_INDEX((int)p_mode, 5);
+
+ if (p_mode == mouse_mode_override) {
+ return;
+ }
+
+ if (mouse_mode_override_enabled) {
+ set_mouse_mode_func(p_mode);
+ mouse_mode_override = get_mouse_mode_func();
+ }
}
void Input::_bind_methods() {
@@ -252,6 +291,10 @@ Input::VelocityTrack::VelocityTrack() {
bool Input::is_anything_pressed() const {
_THREAD_SAFE_METHOD_
+ if (disable_input) {
+ return false;
+ }
+
if (!keys_pressed.is_empty() || !joy_buttons_pressed.is_empty() || !mouse_button_mask.is_empty()) {
return true;
}
@@ -267,21 +310,41 @@ bool Input::is_anything_pressed() const {
bool Input::is_key_pressed(Key p_keycode) const {
_THREAD_SAFE_METHOD_
+
+ if (disable_input) {
+ return false;
+ }
+
return keys_pressed.has(p_keycode);
}
bool Input::is_physical_key_pressed(Key p_keycode) const {
_THREAD_SAFE_METHOD_
+
+ if (disable_input) {
+ return false;
+ }
+
return physical_keys_pressed.has(p_keycode);
}
bool Input::is_key_label_pressed(Key p_keycode) const {
_THREAD_SAFE_METHOD_
+
+ if (disable_input) {
+ return false;
+ }
+
return key_label_pressed.has(p_keycode);
}
bool Input::is_mouse_button_pressed(MouseButton p_button) const {
_THREAD_SAFE_METHOD_
+
+ if (disable_input) {
+ return false;
+ }
+
return mouse_button_mask.has_flag(mouse_button_to_mask(p_button));
}
@@ -295,11 +358,21 @@ static JoyButton _combine_device(JoyButton p_value, int p_device) {
bool Input::is_joy_button_pressed(int p_device, JoyButton p_button) const {
_THREAD_SAFE_METHOD_
+
+ if (disable_input) {
+ return false;
+ }
+
return joy_buttons_pressed.has(_combine_device(p_button, p_device));
}
bool Input::is_action_pressed(const StringName &p_action, bool p_exact) const {
ERR_FAIL_COND_V_MSG(!InputMap::get_singleton()->has_action(p_action), false, InputMap::get_singleton()->suggest_actions(p_action));
+
+ if (disable_input) {
+ return false;
+ }
+
HashMap<StringName, ActionState>::ConstIterator E = action_states.find(p_action);
if (!E) {
return false;
@@ -310,6 +383,11 @@ bool Input::is_action_pressed(const StringName &p_action, bool p_exact) const {
bool Input::is_action_just_pressed(const StringName &p_action, bool p_exact) const {
ERR_FAIL_COND_V_MSG(!InputMap::get_singleton()->has_action(p_action), false, InputMap::get_singleton()->suggest_actions(p_action));
+
+ if (disable_input) {
+ return false;
+ }
+
HashMap<StringName, ActionState>::ConstIterator E = action_states.find(p_action);
if (!E) {
return false;
@@ -331,6 +409,11 @@ bool Input::is_action_just_pressed(const StringName &p_action, bool p_exact) con
bool Input::is_action_just_released(const StringName &p_action, bool p_exact) const {
ERR_FAIL_COND_V_MSG(!InputMap::get_singleton()->has_action(p_action), false, InputMap::get_singleton()->suggest_actions(p_action));
+
+ if (disable_input) {
+ return false;
+ }
+
HashMap<StringName, ActionState>::ConstIterator E = action_states.find(p_action);
if (!E) {
return false;
@@ -352,6 +435,11 @@ bool Input::is_action_just_released(const StringName &p_action, bool p_exact) co
float Input::get_action_strength(const StringName &p_action, bool p_exact) const {
ERR_FAIL_COND_V_MSG(!InputMap::get_singleton()->has_action(p_action), 0.0, InputMap::get_singleton()->suggest_actions(p_action));
+
+ if (disable_input) {
+ return 0.0f;
+ }
+
HashMap<StringName, ActionState>::ConstIterator E = action_states.find(p_action);
if (!E) {
return 0.0f;
@@ -366,6 +454,11 @@ float Input::get_action_strength(const StringName &p_action, bool p_exact) const
float Input::get_action_raw_strength(const StringName &p_action, bool p_exact) const {
ERR_FAIL_COND_V_MSG(!InputMap::get_singleton()->has_action(p_action), 0.0, InputMap::get_singleton()->suggest_actions(p_action));
+
+ if (disable_input) {
+ return 0.0f;
+ }
+
HashMap<StringName, ActionState>::ConstIterator E = action_states.find(p_action);
if (!E) {
return 0.0f;
@@ -410,6 +503,11 @@ Vector2 Input::get_vector(const StringName &p_negative_x, const StringName &p_po
float Input::get_joy_axis(int p_device, JoyAxis p_axis) const {
_THREAD_SAFE_METHOD_
+
+ if (disable_input) {
+ return 0;
+ }
+
JoyAxis c = _combine_device(p_axis, p_device);
if (_joy_axis.has(c)) {
return _joy_axis[c];
@@ -1664,6 +1762,14 @@ int Input::get_unused_joy_id() {
return -1;
}
+void Input::set_disable_input(bool p_disable) {
+ disable_input = p_disable;
+}
+
+bool Input::is_input_disabled() const {
+ return disable_input;
+}
+
Input::Input() {
singleton = this;
diff --git a/core/input/input.h b/core/input/input.h
index 95dd623cc0..a189ae7d9a 100644
--- a/core/input/input.h
+++ b/core/input/input.h
@@ -103,6 +103,11 @@ private:
Vector2 mouse_pos;
int64_t mouse_window = 0;
bool legacy_just_pressed_behavior = false;
+ bool disable_input = false;
+
+ MouseMode mouse_mode = MOUSE_MODE_VISIBLE;
+ bool mouse_mode_override_enabled = false;
+ MouseMode mouse_mode_override = MOUSE_MODE_VISIBLE;
struct ActionState {
uint64_t pressed_physics_frame = UINT64_MAX;
@@ -279,6 +284,8 @@ protected:
public:
void set_mouse_mode(MouseMode p_mode);
MouseMode get_mouse_mode() const;
+ void set_mouse_mode_override_enabled(bool p_enabled);
+ void set_mouse_mode_override(MouseMode p_mode);
#ifdef TOOLS_ENABLED
void get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const override;
@@ -380,6 +387,9 @@ public:
void set_event_dispatch_function(EventDispatchFunc p_function);
+ void set_disable_input(bool p_disable);
+ bool is_input_disabled() const;
+
Input();
~Input();
};
diff --git a/doc/classes/EditorFeatureProfile.xml b/doc/classes/EditorFeatureProfile.xml
index 3aa1e63aac..c125c923ef 100644
--- a/doc/classes/EditorFeatureProfile.xml
+++ b/doc/classes/EditorFeatureProfile.xml
@@ -121,7 +121,10 @@
<constant name="FEATURE_HISTORY_DOCK" value="7" enum="Feature">
The History dock. If this feature is disabled, the History dock won't be visible.
</constant>
- <constant name="FEATURE_MAX" value="8" enum="Feature">
+ <constant name="FEATURE_GAME" value="8" enum="Feature">
+ The Game tab, which allows embedding the game window and selecting nodes by clicking inside of it. If this feature is disabled, the Game tab won't display.
+ </constant>
+ <constant name="FEATURE_MAX" value="9" enum="Feature">
Represents the size of the [enum Feature] enum.
</constant>
</constants>
diff --git a/editor/debugger/editor_debugger_node.cpp b/editor/debugger/editor_debugger_node.cpp
index ee8aae661b..0f948b4ed5 100644
--- a/editor/debugger/editor_debugger_node.cpp
+++ b/editor/debugger/editor_debugger_node.cpp
@@ -105,6 +105,7 @@ ScriptEditorDebugger *EditorDebuggerNode::_add_debugger() {
node->connect("breakpoint_selected", callable_mp(this, &EditorDebuggerNode::_error_selected).bind(id));
node->connect("clear_execution", callable_mp(this, &EditorDebuggerNode::_clear_execution));
node->connect("breaked", callable_mp(this, &EditorDebuggerNode::_breaked).bind(id));
+ node->connect("remote_tree_select_requested", callable_mp(this, &EditorDebuggerNode::_remote_tree_select_requested).bind(id));
node->connect("remote_tree_updated", callable_mp(this, &EditorDebuggerNode::_remote_tree_updated).bind(id));
node->connect("remote_object_updated", callable_mp(this, &EditorDebuggerNode::_remote_object_updated).bind(id));
node->connect("remote_object_property_updated", callable_mp(this, &EditorDebuggerNode::_remote_object_property_updated).bind(id));
@@ -637,6 +638,13 @@ void EditorDebuggerNode::request_remote_tree() {
get_current_debugger()->request_remote_tree();
}
+void EditorDebuggerNode::_remote_tree_select_requested(ObjectID p_id, int p_debugger) {
+ if (p_debugger != tabs->get_current_tab()) {
+ return;
+ }
+ remote_scene_tree->select_node(p_id);
+}
+
void EditorDebuggerNode::_remote_tree_updated(int p_debugger) {
if (p_debugger != tabs->get_current_tab()) {
return;
diff --git a/editor/debugger/editor_debugger_node.h b/editor/debugger/editor_debugger_node.h
index 12e097f652..12c0d30c42 100644
--- a/editor/debugger/editor_debugger_node.h
+++ b/editor/debugger/editor_debugger_node.h
@@ -51,11 +51,8 @@ class EditorDebuggerNode : public MarginContainer {
public:
enum CameraOverride {
OVERRIDE_NONE,
- OVERRIDE_2D,
- OVERRIDE_3D_1, // 3D Viewport 1
- OVERRIDE_3D_2, // 3D Viewport 2
- OVERRIDE_3D_3, // 3D Viewport 3
- OVERRIDE_3D_4 // 3D Viewport 4
+ OVERRIDE_INGAME,
+ OVERRIDE_EDITORS,
};
private:
@@ -132,6 +129,7 @@ protected:
void _debugger_stopped(int p_id);
void _debugger_wants_stop(int p_id);
void _debugger_changed(int p_tab);
+ void _remote_tree_select_requested(ObjectID p_id, int p_debugger);
void _remote_tree_updated(int p_debugger);
void _remote_tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button);
void _remote_object_updated(ObjectID p_id, int p_debugger);
diff --git a/editor/debugger/editor_debugger_tree.cpp b/editor/debugger/editor_debugger_tree.cpp
index a900842651..4d67800e6e 100644
--- a/editor/debugger/editor_debugger_tree.cpp
+++ b/editor/debugger/editor_debugger_tree.cpp
@@ -30,6 +30,7 @@
#include "editor_debugger_tree.h"
+#include "editor/debugger/editor_debugger_node.h"
#include "editor/editor_node.h"
#include "editor/editor_string_names.h"
#include "editor/gui/editor_file_dialog.h"
@@ -148,7 +149,8 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int
updating_scene_tree = true;
const String last_path = get_selected_path();
const String filter = SceneTreeDock::get_singleton()->get_filter();
- bool filter_changed = filter != last_filter;
+ bool should_scroll = scrolling_to_item || filter != last_filter;
+ scrolling_to_item = false;
TreeItem *scroll_item = nullptr;
// Nodes are in a flatten list, depth first. Use a stack of parents, avoid recursion.
@@ -185,8 +187,18 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int
// Select previously selected node.
if (debugger_id == p_debugger) { // Can use remote id.
if (node.id == inspected_object_id) {
+ if (selection_uncollapse_all) {
+ selection_uncollapse_all = false;
+
+ // Temporarily set to `false`, to allow caching the unfolds.
+ updating_scene_tree = false;
+ item->uncollapse_tree();
+ updating_scene_tree = true;
+ }
+
item->select(0);
- if (filter_changed) {
+
+ if (should_scroll) {
scroll_item = item;
}
}
@@ -194,7 +206,7 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int
if (last_path == _get_path(item)) {
updating_scene_tree = false; // Force emission of new selection.
item->select(0);
- if (filter_changed) {
+ if (should_scroll) {
scroll_item = item;
}
updating_scene_tree = true;
@@ -258,14 +270,30 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int
}
}
}
- debugger_id = p_debugger; // Needed by hook, could be avoided if every debugger had its own tree
+
+ debugger_id = p_debugger; // Needed by hook, could be avoided if every debugger had its own tree.
if (scroll_item) {
- callable_mp((Tree *)this, &Tree::scroll_to_item).call_deferred(scroll_item, false);
+ scroll_to_item(scroll_item, false);
}
last_filter = filter;
updating_scene_tree = false;
}
+void EditorDebuggerTree::select_node(ObjectID p_id) {
+ // Manually select, as the tree control may be out-of-date for some reason (e.g. not shown yet).
+ selection_uncollapse_all = true;
+ inspected_object_id = uint64_t(p_id);
+ scrolling_to_item = true;
+ emit_signal(SNAME("object_selected"), inspected_object_id, debugger_id);
+
+ if (!updating_scene_tree) {
+ // Request a tree refresh.
+ EditorDebuggerNode::get_singleton()->request_remote_tree();
+ }
+ // Set the value immediately, so no update flooding happens and causes a crash.
+ updating_scene_tree = true;
+}
+
Variant EditorDebuggerTree::get_drag_data(const Point2 &p_point) {
if (get_button_id_at_position(p_point) != -1) {
return Variant();
diff --git a/editor/debugger/editor_debugger_tree.h b/editor/debugger/editor_debugger_tree.h
index 705df17baf..d048688cad 100644
--- a/editor/debugger/editor_debugger_tree.h
+++ b/editor/debugger/editor_debugger_tree.h
@@ -49,6 +49,8 @@ private:
ObjectID inspected_object_id;
int debugger_id = 0;
bool updating_scene_tree = false;
+ bool scrolling_to_item = false;
+ bool selection_uncollapse_all = false;
HashSet<ObjectID> unfold_cache;
PopupMenu *item_menu = nullptr;
EditorFileDialog *file_dialog = nullptr;
@@ -78,6 +80,7 @@ public:
ObjectID get_selected_object();
int get_current_debugger(); // Would love to have one tree for every debugger.
void update_scene_tree(const SceneDebuggerTree *p_tree, int p_debugger);
+ void select_node(ObjectID p_id);
EditorDebuggerTree();
};
diff --git a/editor/debugger/script_editor_debugger.cpp b/editor/debugger/script_editor_debugger.cpp
index c42740de50..b78aad1721 100644
--- a/editor/debugger/script_editor_debugger.cpp
+++ b/editor/debugger/script_editor_debugger.cpp
@@ -806,6 +806,10 @@ void ScriptEditorDebugger::_parse_message(const String &p_msg, uint64_t p_thread
} else if (p_msg == "request_quit") {
emit_signal(SNAME("stop_requested"));
_stop_and_notify();
+ } else if (p_msg == "remote_node_clicked") {
+ if (!p_data.is_empty()) {
+ emit_signal(SNAME("remote_tree_select_requested"), p_data[0]);
+ }
} else if (p_msg == "performance:profile_names") {
Vector<StringName> monitors;
monitors.resize(p_data.size());
@@ -905,37 +909,42 @@ void ScriptEditorDebugger::_notification(int p_what) {
if (is_session_active()) {
peer->poll();
- if (camera_override == CameraOverride::OVERRIDE_2D) {
- Dictionary state = CanvasItemEditor::get_singleton()->get_state();
- float zoom = state["zoom"];
- Point2 offset = state["ofs"];
- Transform2D transform;
-
- transform.scale_basis(Size2(zoom, zoom));
- transform.columns[2] = -offset * zoom;
-
- Array msg;
- msg.push_back(transform);
- _put_msg("scene:override_camera_2D:transform", msg);
-
- } else if (camera_override >= CameraOverride::OVERRIDE_3D_1) {
- int viewport_idx = camera_override - CameraOverride::OVERRIDE_3D_1;
- Node3DEditorViewport *viewport = Node3DEditor::get_singleton()->get_editor_viewport(viewport_idx);
- Camera3D *const cam = viewport->get_camera_3d();
-
- Array msg;
- msg.push_back(cam->get_camera_transform());
- if (cam->get_projection() == Camera3D::PROJECTION_ORTHOGONAL) {
- msg.push_back(false);
- msg.push_back(cam->get_size());
- } else {
- msg.push_back(true);
- msg.push_back(cam->get_fov());
+ if (camera_override == CameraOverride::OVERRIDE_EDITORS) {
+ // CanvasItem Editor
+ {
+ Dictionary state = CanvasItemEditor::get_singleton()->get_state();
+ float zoom = state["zoom"];
+ Point2 offset = state["ofs"];
+ Transform2D transform;
+
+ transform.scale_basis(Size2(zoom, zoom));
+ transform.columns[2] = -offset * zoom;
+
+ Array msg;
+ msg.push_back(transform);
+ _put_msg("scene:transform_camera_2d", msg);
+ }
+
+ // Node3D Editor
+ {
+ Node3DEditorViewport *viewport = Node3DEditor::get_singleton()->get_last_used_viewport();
+ const Camera3D *cam = viewport->get_camera_3d();
+
+ Array msg;
+ msg.push_back(cam->get_camera_transform());
+ if (cam->get_projection() == Camera3D::PROJECTION_ORTHOGONAL) {
+ msg.push_back(false);
+ msg.push_back(cam->get_size());
+ } else {
+ msg.push_back(true);
+ msg.push_back(cam->get_fov());
+ }
+ msg.push_back(cam->get_near());
+ msg.push_back(cam->get_far());
+ _put_msg("scene:transform_camera_3d", msg);
}
- msg.push_back(cam->get_near());
- msg.push_back(cam->get_far());
- _put_msg("scene:override_camera_3D:transform", msg);
}
+
if (is_breaked() && can_request_idle_draw) {
_put_msg("servers:draw", Array());
can_request_idle_draw = false;
@@ -1469,23 +1478,10 @@ CameraOverride ScriptEditorDebugger::get_camera_override() const {
}
void ScriptEditorDebugger::set_camera_override(CameraOverride p_override) {
- if (p_override == CameraOverride::OVERRIDE_2D && camera_override != CameraOverride::OVERRIDE_2D) {
- Array msg;
- msg.push_back(true);
- _put_msg("scene:override_camera_2D:set", msg);
- } else if (p_override != CameraOverride::OVERRIDE_2D && camera_override == CameraOverride::OVERRIDE_2D) {
- Array msg;
- msg.push_back(false);
- _put_msg("scene:override_camera_2D:set", msg);
- } else if (p_override >= CameraOverride::OVERRIDE_3D_1 && camera_override < CameraOverride::OVERRIDE_3D_1) {
- Array msg;
- msg.push_back(true);
- _put_msg("scene:override_camera_3D:set", msg);
- } else if (p_override < CameraOverride::OVERRIDE_3D_1 && camera_override >= CameraOverride::OVERRIDE_3D_1) {
- Array msg;
- msg.push_back(false);
- _put_msg("scene:override_camera_3D:set", msg);
- }
+ Array msg;
+ msg.push_back(p_override != CameraOverride::OVERRIDE_NONE);
+ msg.push_back(p_override == CameraOverride::OVERRIDE_EDITORS);
+ _put_msg("scene:override_cameras", msg);
camera_override = p_override;
}
@@ -1776,6 +1772,7 @@ void ScriptEditorDebugger::_bind_methods() {
ADD_SIGNAL(MethodInfo("remote_object_updated", PropertyInfo(Variant::INT, "id")));
ADD_SIGNAL(MethodInfo("remote_object_property_updated", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::STRING, "property")));
ADD_SIGNAL(MethodInfo("remote_tree_updated"));
+ ADD_SIGNAL(MethodInfo("remote_tree_select_requested", PropertyInfo(Variant::NODE_PATH, "path")));
ADD_SIGNAL(MethodInfo("output", PropertyInfo(Variant::STRING, "msg"), PropertyInfo(Variant::INT, "level")));
ADD_SIGNAL(MethodInfo("stack_dump", PropertyInfo(Variant::ARRAY, "stack_dump")));
ADD_SIGNAL(MethodInfo("stack_frame_vars", PropertyInfo(Variant::INT, "num_vars")));
diff --git a/editor/editor_feature_profile.cpp b/editor/editor_feature_profile.cpp
index 44fc9e3702..7ffbc39d75 100644
--- a/editor/editor_feature_profile.cpp
+++ b/editor/editor_feature_profile.cpp
@@ -43,6 +43,7 @@
const char *EditorFeatureProfile::feature_names[FEATURE_MAX] = {
TTRC("3D Editor"),
TTRC("Script Editor"),
+ TTRC("Game View"),
TTRC("Asset Library"),
TTRC("Scene Tree Editing"),
TTRC("Node Dock"),
@@ -54,6 +55,7 @@ const char *EditorFeatureProfile::feature_names[FEATURE_MAX] = {
const char *EditorFeatureProfile::feature_descriptions[FEATURE_MAX] = {
TTRC("Allows to view and edit 3D scenes."),
TTRC("Allows to edit scripts using the integrated script editor."),
+ TTRC("Provides tools for selecting and debugging nodes at runtime."),
TTRC("Provides built-in access to the Asset Library."),
TTRC("Allows editing the node hierarchy in the Scene dock."),
TTRC("Allows to work with signals and groups of the node selected in the Scene dock."),
@@ -65,6 +67,7 @@ const char *EditorFeatureProfile::feature_descriptions[FEATURE_MAX] = {
const char *EditorFeatureProfile::feature_identifiers[FEATURE_MAX] = {
"3d",
"script",
+ "game",
"asset_lib",
"scene_tree",
"node_dock",
@@ -307,6 +310,7 @@ void EditorFeatureProfile::_bind_methods() {
BIND_ENUM_CONSTANT(FEATURE_FILESYSTEM_DOCK);
BIND_ENUM_CONSTANT(FEATURE_IMPORT_DOCK);
BIND_ENUM_CONSTANT(FEATURE_HISTORY_DOCK);
+ BIND_ENUM_CONSTANT(FEATURE_GAME);
BIND_ENUM_CONSTANT(FEATURE_MAX);
}
diff --git a/editor/editor_feature_profile.h b/editor/editor_feature_profile.h
index 7458a04e19..e84936dd34 100644
--- a/editor/editor_feature_profile.h
+++ b/editor/editor_feature_profile.h
@@ -55,6 +55,7 @@ public:
FEATURE_FILESYSTEM_DOCK,
FEATURE_IMPORT_DOCK,
FEATURE_HISTORY_DOCK,
+ FEATURE_GAME,
FEATURE_MAX
};
diff --git a/editor/editor_main_screen.h b/editor/editor_main_screen.h
index 153a182bc2..ca78ceaa88 100644
--- a/editor/editor_main_screen.h
+++ b/editor/editor_main_screen.h
@@ -47,6 +47,7 @@ public:
EDITOR_2D = 0,
EDITOR_3D,
EDITOR_SCRIPT,
+ EDITOR_GAME,
EDITOR_ASSETLIB,
};
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index 161e518fc6..36b43b7e9b 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -145,6 +145,7 @@
#include "editor/plugins/editor_plugin.h"
#include "editor/plugins/editor_preview_plugins.h"
#include "editor/plugins/editor_resource_conversion_plugin.h"
+#include "editor/plugins/game_view_plugin.h"
#include "editor/plugins/gdextension_export_plugin.h"
#include "editor/plugins/material_editor_plugin.h"
#include "editor/plugins/mesh_library_editor_plugin.h"
@@ -357,6 +358,8 @@ void EditorNode::shortcut_input(const Ref<InputEvent> &p_event) {
editor_main_screen->select(EditorMainScreen::EDITOR_3D);
} else if (ED_IS_SHORTCUT("editor/editor_script", p_event)) {
editor_main_screen->select(EditorMainScreen::EDITOR_SCRIPT);
+ } else if (ED_IS_SHORTCUT("editor/editor_game", p_event)) {
+ editor_main_screen->select(EditorMainScreen::EDITOR_GAME);
} else if (ED_IS_SHORTCUT("editor/editor_help", p_event)) {
emit_signal(SNAME("request_help_search"), "");
} else if (ED_IS_SHORTCUT("editor/editor_assetlib", p_event) && AssetLibraryEditorPlugin::is_available()) {
@@ -6577,6 +6580,7 @@ void EditorNode::_feature_profile_changed() {
editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_3D, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_3D));
editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_SCRIPT, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_SCRIPT));
+ editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_GAME, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_GAME));
if (AssetLibraryEditorPlugin::is_available()) {
editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_ASSETLIB, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_ASSET_LIB));
}
@@ -6587,6 +6591,7 @@ void EditorNode::_feature_profile_changed() {
editor_dock_manager->set_dock_enabled(history_dock, true);
editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_3D, true);
editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_SCRIPT, true);
+ editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_GAME, true);
if (AssetLibraryEditorPlugin::is_available()) {
editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_ASSETLIB, true);
}
@@ -7714,6 +7719,7 @@ EditorNode::EditorNode() {
add_editor_plugin(memnew(CanvasItemEditorPlugin));
add_editor_plugin(memnew(Node3DEditorPlugin));
add_editor_plugin(memnew(ScriptEditorPlugin));
+ add_editor_plugin(memnew(GameViewPlugin));
EditorAudioBuses *audio_bus_editor = EditorAudioBuses::register_editor();
@@ -7896,12 +7902,14 @@ EditorNode::EditorNode() {
ED_SHORTCUT_AND_COMMAND("editor/editor_2d", TTR("Open 2D Editor"), KeyModifierMask::CTRL | Key::F1);
ED_SHORTCUT_AND_COMMAND("editor/editor_3d", TTR("Open 3D Editor"), KeyModifierMask::CTRL | Key::F2);
ED_SHORTCUT_AND_COMMAND("editor/editor_script", TTR("Open Script Editor"), KeyModifierMask::CTRL | Key::F3);
- ED_SHORTCUT_AND_COMMAND("editor/editor_assetlib", TTR("Open Asset Library"), KeyModifierMask::CTRL | Key::F4);
+ ED_SHORTCUT_AND_COMMAND("editor/editor_game", TTR("Open Game View"), KeyModifierMask::CTRL | Key::F4);
+ ED_SHORTCUT_AND_COMMAND("editor/editor_assetlib", TTR("Open Asset Library"), KeyModifierMask::CTRL | Key::F5);
ED_SHORTCUT_OVERRIDE("editor/editor_2d", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_1);
ED_SHORTCUT_OVERRIDE("editor/editor_3d", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_2);
ED_SHORTCUT_OVERRIDE("editor/editor_script", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_3);
- ED_SHORTCUT_OVERRIDE("editor/editor_assetlib", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_4);
+ ED_SHORTCUT_OVERRIDE("editor/editor_game", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_4);
+ ED_SHORTCUT_OVERRIDE("editor/editor_assetlib", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_5);
ED_SHORTCUT_AND_COMMAND("editor/editor_next", TTR("Open the next Editor"));
ED_SHORTCUT_AND_COMMAND("editor/editor_prev", TTR("Open the previous Editor"));
diff --git a/editor/icons/2DNodes.svg b/editor/icons/2DNodes.svg
new file mode 100644
index 0000000000..90b92a4bc7
--- /dev/null
+++ b/editor/icons/2DNodes.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="16" height="16"><path fill="none" stroke="#8da5f3" stroke-width="2" d="M 8,13 C 5.2385763,13 3,10.761424 3,8 3,5.2385763 5.2385763,3 8,3"/><path fill="none" stroke="#8eef97" stroke-width="2" d="m 8,13 c 2.761424,0 5,-2.238576 5,-5 C 13,5.2385763 10.761424,3 8,3"/></svg> \ No newline at end of file
diff --git a/editor/icons/Camera.svg b/editor/icons/Camera.svg
new file mode 100644
index 0000000000..8612d458a7
--- /dev/null
+++ b/editor/icons/Camera.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="16" height="16"><path fill="#e0e0e0" d="M9 2a3 3 0 0 0-3 2.777 3 3 0 1 0-3 5.047V12a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-1l3 2V7l-3 2V7.23A3 3 0 0 0 9 2z"/></svg> \ No newline at end of file
diff --git a/editor/icons/Game.svg b/editor/icons/Game.svg
new file mode 100644
index 0000000000..e75e5c5312
--- /dev/null
+++ b/editor/icons/Game.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e0e0e0" d="M 1,15 V 12 C 1,11.5 1.5,11 2,11 H 3 V 10 C 3,9.5 3.5,9 4,9 h 1 c 0.5,0 1,0.5 1,1 v 1 H 8 V 5 h 2 v 6 h 4 c 0.5,0 1,0.5 1,1 v 3 z"/><circle cx="9" cy="4" r="3" fill="#e0e0e0"/></svg> \ No newline at end of file
diff --git a/editor/icons/NextFrame.svg b/editor/icons/NextFrame.svg
new file mode 100644
index 0000000000..9609b2538b
--- /dev/null
+++ b/editor/icons/NextFrame.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e0e0e0" d="m 12,3 c -0.552285,0 -1,0.4477153 -1,1 v 8 c 0,0.552285 0.447715,1 1,1 h 1 c 0.552285,0 1,-0.447715 1,-1 V 4 C 14,3.4477153 13.552285,3 13,3 Z M 2.975,3.002 C 2.4332786,3.0155465 2.0009144,3.45811 2,4 v 8 c -3.148e-4,0.838862 0.9701632,1.305289 1.625,0.781 l 5,-4 c 0.4989606,-0.4003069 0.4989606,-1.1596931 0,-1.56 l -5,-4 C 3.4409271,3.0736532 3.2107095,2.9960875 2.975,3.002 Z"/></svg> \ No newline at end of file
diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp
index 51992a0b21..e3cf3dbbf2 100644
--- a/editor/plugins/canvas_item_editor_plugin.cpp
+++ b/editor/plugins/canvas_item_editor_plugin.cpp
@@ -3977,7 +3977,6 @@ void CanvasItemEditor::_update_editor_settings() {
grid_snap_button->set_button_icon(get_editor_theme_icon(SNAME("SnapGrid")));
snap_config_menu->set_button_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl")));
skeleton_menu->set_button_icon(get_editor_theme_icon(SNAME("Bone")));
- override_camera_button->set_button_icon(get_editor_theme_icon(SNAME("Camera2D")));
pan_button->set_button_icon(get_editor_theme_icon(SNAME("ToolPan")));
ruler_button->set_button_icon(get_editor_theme_icon(SNAME("Ruler")));
pivot_button->set_button_icon(get_editor_theme_icon(SNAME("EditPivot")));
@@ -4016,8 +4015,6 @@ void CanvasItemEditor::_notification(int p_what) {
case NOTIFICATION_READY: {
_update_lock_and_group_button();
- EditorRunBar::get_singleton()->connect("play_pressed", callable_mp(this, &CanvasItemEditor::_update_override_camera_button).bind(true));
- EditorRunBar::get_singleton()->connect("stop_pressed", callable_mp(this, &CanvasItemEditor::_update_override_camera_button).bind(false));
ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &CanvasItemEditor::_project_settings_changed));
} break;
@@ -4116,15 +4113,6 @@ void CanvasItemEditor::_notification(int p_what) {
_update_editor_settings();
} break;
- case NOTIFICATION_VISIBILITY_CHANGED: {
- if (!is_visible() && override_camera_button->is_pressed()) {
- EditorDebuggerNode *debugger = EditorDebuggerNode::get_singleton();
-
- debugger->set_camera_override(EditorDebuggerNode::OVERRIDE_NONE);
- override_camera_button->set_pressed(false);
- }
- } break;
-
case NOTIFICATION_APPLICATION_FOCUS_OUT:
case NOTIFICATION_WM_WINDOW_FOCUS_OUT: {
if (drag_type != DRAG_NONE) {
@@ -4282,16 +4270,6 @@ void CanvasItemEditor::_button_toggle_grid_snap(bool p_status) {
viewport->queue_redraw();
}
-void CanvasItemEditor::_button_override_camera(bool p_pressed) {
- EditorDebuggerNode *debugger = EditorDebuggerNode::get_singleton();
-
- if (p_pressed) {
- debugger->set_camera_override(EditorDebuggerNode::OVERRIDE_2D);
- } else {
- debugger->set_camera_override(EditorDebuggerNode::OVERRIDE_NONE);
- }
-}
-
void CanvasItemEditor::_button_tool_select(int p_index) {
Button *tb[TOOL_MAX] = { select_button, list_select_button, move_button, scale_button, rotate_button, pivot_button, pan_button, ruler_button };
for (int i = 0; i < TOOL_MAX; i++) {
@@ -4398,17 +4376,6 @@ void CanvasItemEditor::_insert_animation_keys(bool p_location, bool p_rotation,
te->commit_insert_queue();
}
-void CanvasItemEditor::_update_override_camera_button(bool p_game_running) {
- if (p_game_running) {
- override_camera_button->set_disabled(false);
- override_camera_button->set_tooltip_text(TTR("Project Camera Override\nOverrides the running project's camera with the editor viewport camera."));
- } else {
- override_camera_button->set_disabled(true);
- override_camera_button->set_pressed(false);
- override_camera_button->set_tooltip_text(TTR("Project Camera Override\nNo project instance running. Run the project from the editor to use this feature."));
- }
-}
-
void CanvasItemEditor::_popup_callback(int p_op) {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
last_option = MenuOption(p_op);
@@ -5514,16 +5481,6 @@ CanvasItemEditor::CanvasItemEditor() {
main_menu_hbox->add_child(memnew(VSeparator));
- override_camera_button = memnew(Button);
- override_camera_button->set_theme_type_variation("FlatButton");
- main_menu_hbox->add_child(override_camera_button);
- override_camera_button->connect(SceneStringName(toggled), callable_mp(this, &CanvasItemEditor::_button_override_camera));
- override_camera_button->set_toggle_mode(true);
- override_camera_button->set_disabled(true);
- _update_override_camera_button(false);
-
- main_menu_hbox->add_child(memnew(VSeparator));
-
view_menu = memnew(MenuButton);
view_menu->set_flat(false);
view_menu->set_theme_type_variation("FlatMenuButton");
diff --git a/editor/plugins/canvas_item_editor_plugin.h b/editor/plugins/canvas_item_editor_plugin.h
index bae9efebc9..c5335bf9c1 100644
--- a/editor/plugins/canvas_item_editor_plugin.h
+++ b/editor/plugins/canvas_item_editor_plugin.h
@@ -335,7 +335,6 @@ private:
Button *group_button = nullptr;
Button *ungroup_button = nullptr;
- Button *override_camera_button = nullptr;
MenuButton *view_menu = nullptr;
PopupMenu *grid_menu = nullptr;
PopupMenu *theme_menu = nullptr;
@@ -518,11 +517,8 @@ private:
void _zoom_on_position(real_t p_zoom, Point2 p_position = Point2());
void _button_toggle_smart_snap(bool p_status);
void _button_toggle_grid_snap(bool p_status);
- void _button_override_camera(bool p_pressed);
void _button_tool_select(int p_index);
- void _update_override_camera_button(bool p_game_running);
-
HSplitContainer *left_panel_split = nullptr;
HSplitContainer *right_panel_split = nullptr;
VSplitContainer *bottom_split = nullptr;
diff --git a/editor/plugins/game_view_plugin.cpp b/editor/plugins/game_view_plugin.cpp
new file mode 100644
index 0000000000..f45af72e90
--- /dev/null
+++ b/editor/plugins/game_view_plugin.cpp
@@ -0,0 +1,478 @@
+/**************************************************************************/
+/* game_view_plugin.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 "game_view_plugin.h"
+
+#include "editor/editor_main_screen.h"
+#include "editor/editor_node.h"
+#include "editor/editor_settings.h"
+#include "editor/themes/editor_scale.h"
+#include "scene/gui/button.h"
+#include "scene/gui/menu_button.h"
+#include "scene/gui/panel.h"
+#include "scene/gui/separator.h"
+
+void GameViewDebugger::_session_started(Ref<EditorDebuggerSession> p_session) {
+ p_session->send_message("scene:runtime_node_select_setup", Array());
+
+ Array type;
+ type.append(node_type);
+ p_session->send_message("scene:runtime_node_select_set_type", type);
+ Array visible;
+ visible.append(selection_visible);
+ p_session->send_message("scene:runtime_node_select_set_visible", visible);
+ Array mode;
+ mode.append(select_mode);
+ p_session->send_message("scene:runtime_node_select_set_mode", mode);
+
+ emit_signal(SNAME("session_started"));
+}
+
+void GameViewDebugger::_session_stopped() {
+ emit_signal(SNAME("session_stopped"));
+}
+
+void GameViewDebugger::set_suspend(bool p_enabled) {
+ Array message;
+ message.append(p_enabled);
+
+ for (Ref<EditorDebuggerSession> &I : sessions) {
+ if (I->is_active()) {
+ I->send_message("scene:suspend_changed", message);
+ }
+ }
+}
+
+void GameViewDebugger::next_frame() {
+ for (Ref<EditorDebuggerSession> &I : sessions) {
+ if (I->is_active()) {
+ I->send_message("scene:next_frame", Array());
+ }
+ }
+}
+
+void GameViewDebugger::set_node_type(int p_type) {
+ node_type = p_type;
+
+ Array message;
+ message.append(p_type);
+
+ for (Ref<EditorDebuggerSession> &I : sessions) {
+ if (I->is_active()) {
+ I->send_message("scene:runtime_node_select_set_type", message);
+ }
+ }
+}
+
+void GameViewDebugger::set_selection_visible(bool p_visible) {
+ selection_visible = p_visible;
+
+ Array message;
+ message.append(p_visible);
+
+ for (Ref<EditorDebuggerSession> &I : sessions) {
+ if (I->is_active()) {
+ I->send_message("scene:runtime_node_select_set_visible", message);
+ }
+ }
+}
+
+void GameViewDebugger::set_select_mode(int p_mode) {
+ select_mode = p_mode;
+
+ Array message;
+ message.append(p_mode);
+
+ for (Ref<EditorDebuggerSession> &I : sessions) {
+ if (I->is_active()) {
+ I->send_message("scene:runtime_node_select_set_mode", message);
+ }
+ }
+}
+
+void GameViewDebugger::set_camera_override(bool p_enabled) {
+ EditorDebuggerNode::get_singleton()->set_camera_override(p_enabled ? camera_override_mode : EditorDebuggerNode::OVERRIDE_NONE);
+}
+
+void GameViewDebugger::set_camera_manipulate_mode(EditorDebuggerNode::CameraOverride p_mode) {
+ camera_override_mode = p_mode;
+
+ if (EditorDebuggerNode::get_singleton()->get_camera_override() != EditorDebuggerNode::OVERRIDE_NONE) {
+ set_camera_override(true);
+ }
+}
+
+void GameViewDebugger::reset_camera_2d_position() {
+ for (Ref<EditorDebuggerSession> &I : sessions) {
+ if (I->is_active()) {
+ I->send_message("scene:runtime_node_select_reset_camera_2d", Array());
+ }
+ }
+}
+
+void GameViewDebugger::reset_camera_3d_position() {
+ for (Ref<EditorDebuggerSession> &I : sessions) {
+ if (I->is_active()) {
+ I->send_message("scene:runtime_node_select_reset_camera_3d", Array());
+ }
+ }
+}
+
+void GameViewDebugger::setup_session(int p_session_id) {
+ Ref<EditorDebuggerSession> session = get_session(p_session_id);
+ ERR_FAIL_COND(session.is_null());
+
+ sessions.append(session);
+
+ session->connect("started", callable_mp(this, &GameViewDebugger::_session_started).bind(session));
+ session->connect("stopped", callable_mp(this, &GameViewDebugger::_session_stopped));
+}
+
+void GameViewDebugger::_bind_methods() {
+ ADD_SIGNAL(MethodInfo("session_started"));
+ ADD_SIGNAL(MethodInfo("session_stopped"));
+}
+
+///////
+
+void GameView::_sessions_changed() {
+ // The debugger session's `session_started/stopped` signal can be unreliable, so count it manually.
+ active_sessions = 0;
+ Array sessions = debugger->get_sessions();
+ for (int i = 0; i < sessions.size(); i++) {
+ if (Object::cast_to<EditorDebuggerSession>(sessions[i])->is_active()) {
+ active_sessions++;
+ }
+ }
+
+ _update_debugger_buttons();
+}
+
+void GameView::_update_debugger_buttons() {
+ bool empty = active_sessions == 0;
+
+ suspend_button->set_disabled(empty);
+ camera_override_button->set_disabled(empty);
+
+ PopupMenu *menu = camera_override_menu->get_popup();
+
+ bool disable_camera_reset = empty || !camera_override_button->is_pressed() || !menu->is_item_checked(menu->get_item_index(CAMERA_MODE_INGAME));
+ menu->set_item_disabled(CAMERA_RESET_2D, disable_camera_reset);
+ menu->set_item_disabled(CAMERA_RESET_3D, disable_camera_reset);
+
+ if (empty) {
+ suspend_button->set_pressed(false);
+ camera_override_button->set_pressed(false);
+ }
+ next_frame_button->set_disabled(!suspend_button->is_pressed());
+}
+
+void GameView::_suspend_button_toggled(bool p_pressed) {
+ _update_debugger_buttons();
+
+ debugger->set_suspend(p_pressed);
+}
+
+void GameView::_node_type_pressed(int p_option) {
+ RuntimeNodeSelect::NodeType type = (RuntimeNodeSelect::NodeType)p_option;
+ for (int i = 0; i < RuntimeNodeSelect::NODE_TYPE_MAX; i++) {
+ node_type_button[i]->set_pressed_no_signal(i == type);
+ }
+
+ _update_debugger_buttons();
+
+ debugger->set_node_type(type);
+}
+
+void GameView::_select_mode_pressed(int p_option) {
+ RuntimeNodeSelect::SelectMode mode = (RuntimeNodeSelect::SelectMode)p_option;
+ for (int i = 0; i < RuntimeNodeSelect::SELECT_MODE_MAX; i++) {
+ select_mode_button[i]->set_pressed_no_signal(i == mode);
+ }
+
+ debugger->set_select_mode(mode);
+}
+
+void GameView::_hide_selection_toggled(bool p_pressed) {
+ hide_selection->set_button_icon(get_editor_theme_icon(p_pressed ? SNAME("GuiVisibilityHidden") : SNAME("GuiVisibilityVisible")));
+
+ debugger->set_selection_visible(!p_pressed);
+}
+
+void GameView::_camera_override_button_toggled(bool p_pressed) {
+ _update_debugger_buttons();
+
+ debugger->set_camera_override(p_pressed);
+}
+
+void GameView::_camera_override_menu_id_pressed(int p_id) {
+ PopupMenu *menu = camera_override_menu->get_popup();
+ if (p_id != CAMERA_RESET_2D && p_id != CAMERA_RESET_3D) {
+ for (int i = 0; i < menu->get_item_count(); i++) {
+ menu->set_item_checked(i, false);
+ }
+ }
+
+ switch (p_id) {
+ case CAMERA_RESET_2D: {
+ debugger->reset_camera_2d_position();
+ } break;
+ case CAMERA_RESET_3D: {
+ debugger->reset_camera_3d_position();
+ } break;
+ case CAMERA_MODE_INGAME: {
+ debugger->set_camera_manipulate_mode(EditorDebuggerNode::OVERRIDE_INGAME);
+ menu->set_item_checked(menu->get_item_index(p_id), true);
+
+ _update_debugger_buttons();
+ } break;
+ case CAMERA_MODE_EDITORS: {
+ debugger->set_camera_manipulate_mode(EditorDebuggerNode::OVERRIDE_EDITORS);
+ menu->set_item_checked(menu->get_item_index(p_id), true);
+
+ _update_debugger_buttons();
+ } break;
+ }
+}
+
+void GameView::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_THEME_CHANGED: {
+ suspend_button->set_button_icon(get_editor_theme_icon(SNAME("Pause")));
+ next_frame_button->set_button_icon(get_editor_theme_icon(SNAME("NextFrame")));
+
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE]->set_button_icon(get_editor_theme_icon(SNAME("InputEventJoypadMotion")));
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_2D]->set_button_icon(get_editor_theme_icon(SNAME("2DNodes")));
+#ifndef _3D_DISABLED
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_3D]->set_button_icon(get_editor_theme_icon(SNAME("Node3D")));
+#endif // _3D_DISABLED
+
+ select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE]->set_button_icon(get_editor_theme_icon(SNAME("ToolSelect")));
+ select_mode_button[RuntimeNodeSelect::SELECT_MODE_LIST]->set_button_icon(get_editor_theme_icon(SNAME("ListSelect")));
+
+ hide_selection->set_button_icon(get_editor_theme_icon(hide_selection->is_pressed() ? SNAME("GuiVisibilityHidden") : SNAME("GuiVisibilityVisible")));
+
+ camera_override_button->set_button_icon(get_editor_theme_icon(SNAME("Camera")));
+ camera_override_menu->set_button_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl")));
+ } break;
+ }
+}
+
+void GameView::set_state(const Dictionary &p_state) {
+ if (p_state.has("hide_selection")) {
+ hide_selection->set_pressed(p_state["hide_selection"]);
+ _hide_selection_toggled(hide_selection->is_pressed());
+ }
+ if (p_state.has("select_mode")) {
+ _select_mode_pressed(p_state["select_mode"]);
+ }
+ if (p_state.has("camera_override_mode")) {
+ _camera_override_menu_id_pressed(p_state["camera_override_mode"]);
+ }
+}
+
+Dictionary GameView::get_state() const {
+ Dictionary d;
+ d["hide_selection"] = hide_selection->is_pressed();
+
+ for (int i = 0; i < RuntimeNodeSelect::SELECT_MODE_MAX; i++) {
+ if (select_mode_button[i]->is_pressed()) {
+ d["select_mode"] = i;
+ break;
+ }
+ }
+
+ PopupMenu *menu = camera_override_menu->get_popup();
+ for (int i = CAMERA_MODE_INGAME; i < CAMERA_MODE_EDITORS + 1; i++) {
+ if (menu->is_item_checked(menu->get_item_index(i))) {
+ d["camera_override_mode"] = i;
+ break;
+ }
+ }
+
+ return d;
+}
+
+GameView::GameView(Ref<GameViewDebugger> p_debugger) {
+ debugger = p_debugger;
+
+ // Add some margin to the sides for better aesthetics.
+ // This prevents the first button's hover/pressed effect from "touching" the panel's border,
+ // which looks ugly.
+ MarginContainer *toolbar_margin = memnew(MarginContainer);
+ toolbar_margin->add_theme_constant_override("margin_left", 4 * EDSCALE);
+ toolbar_margin->add_theme_constant_override("margin_right", 4 * EDSCALE);
+ add_child(toolbar_margin);
+
+ HBoxContainer *main_menu_hbox = memnew(HBoxContainer);
+ toolbar_margin->add_child(main_menu_hbox);
+
+ suspend_button = memnew(Button);
+ main_menu_hbox->add_child(suspend_button);
+ suspend_button->set_toggle_mode(true);
+ suspend_button->set_theme_type_variation("FlatButton");
+ suspend_button->connect(SceneStringName(toggled), callable_mp(this, &GameView::_suspend_button_toggled));
+ suspend_button->set_tooltip_text(TTR("Suspend"));
+
+ next_frame_button = memnew(Button);
+ main_menu_hbox->add_child(next_frame_button);
+ next_frame_button->set_theme_type_variation("FlatButton");
+ next_frame_button->connect(SceneStringName(pressed), callable_mp(*debugger, &GameViewDebugger::next_frame));
+ next_frame_button->set_tooltip_text(TTR("Next Frame"));
+
+ main_menu_hbox->add_child(memnew(VSeparator));
+
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE] = memnew(Button);
+ main_menu_hbox->add_child(node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE]);
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE]->set_text(TTR("Input"));
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE]->set_toggle_mode(true);
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE]->set_pressed(true);
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE]->set_theme_type_variation("FlatButton");
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE]->connect(SceneStringName(pressed), callable_mp(this, &GameView::_node_type_pressed).bind(RuntimeNodeSelect::NODE_TYPE_NONE));
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE]->set_tooltip_text(TTR("Allow game input."));
+
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_2D] = memnew(Button);
+ main_menu_hbox->add_child(node_type_button[RuntimeNodeSelect::NODE_TYPE_2D]);
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_2D]->set_text(TTR("2D"));
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_2D]->set_toggle_mode(true);
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_2D]->set_theme_type_variation("FlatButton");
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_2D]->connect(SceneStringName(pressed), callable_mp(this, &GameView::_node_type_pressed).bind(RuntimeNodeSelect::NODE_TYPE_2D));
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_2D]->set_tooltip_text(TTR("Disable game input and allow to select Node2Ds, Controls, and manipulate the 2D camera."));
+
+#ifndef _3D_DISABLED
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_3D] = memnew(Button);
+ main_menu_hbox->add_child(node_type_button[RuntimeNodeSelect::NODE_TYPE_3D]);
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_3D]->set_text(TTR("3D"));
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_3D]->set_toggle_mode(true);
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_3D]->set_theme_type_variation("FlatButton");
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_3D]->connect(SceneStringName(pressed), callable_mp(this, &GameView::_node_type_pressed).bind(RuntimeNodeSelect::NODE_TYPE_3D));
+ node_type_button[RuntimeNodeSelect::NODE_TYPE_3D]->set_tooltip_text(TTR("Disable game input and allow to select Node3Ds and manipulate the 3D camera."));
+#endif // _3D_DISABLED
+
+ main_menu_hbox->add_child(memnew(VSeparator));
+
+ hide_selection = memnew(Button);
+ main_menu_hbox->add_child(hide_selection);
+ hide_selection->set_toggle_mode(true);
+ hide_selection->set_theme_type_variation("FlatButton");
+ hide_selection->connect(SceneStringName(toggled), callable_mp(this, &GameView::_hide_selection_toggled));
+ hide_selection->set_tooltip_text(TTR("Toggle Selection Visibility"));
+
+ main_menu_hbox->add_child(memnew(VSeparator));
+
+ select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE] = memnew(Button);
+ main_menu_hbox->add_child(select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE]);
+ select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE]->set_toggle_mode(true);
+ select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE]->set_pressed(true);
+ select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE]->set_theme_type_variation("FlatButton");
+ select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE]->connect(SceneStringName(pressed), callable_mp(this, &GameView::_select_mode_pressed).bind(RuntimeNodeSelect::SELECT_MODE_SINGLE));
+ select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE]->set_shortcut(ED_SHORTCUT("spatial_editor/tool_select", TTR("Select Mode"), Key::Q));
+ select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE]->set_shortcut_context(this);
+ select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE]->set_tooltip_text(keycode_get_string((Key)KeyModifierMask::CMD_OR_CTRL) + TTR("Alt+RMB: Show list of all nodes at position clicked."));
+
+ select_mode_button[RuntimeNodeSelect::SELECT_MODE_LIST] = memnew(Button);
+ main_menu_hbox->add_child(select_mode_button[RuntimeNodeSelect::SELECT_MODE_LIST]);
+ select_mode_button[RuntimeNodeSelect::SELECT_MODE_LIST]->set_toggle_mode(true);
+ select_mode_button[RuntimeNodeSelect::SELECT_MODE_LIST]->set_theme_type_variation("FlatButton");
+ select_mode_button[RuntimeNodeSelect::SELECT_MODE_LIST]->connect(SceneStringName(pressed), callable_mp(this, &GameView::_select_mode_pressed).bind(RuntimeNodeSelect::SELECT_MODE_LIST));
+ select_mode_button[RuntimeNodeSelect::SELECT_MODE_LIST]->set_tooltip_text(TTR("Show list of selectable nodes at position clicked."));
+
+ main_menu_hbox->add_child(memnew(VSeparator));
+
+ camera_override_button = memnew(Button);
+ main_menu_hbox->add_child(camera_override_button);
+ camera_override_button->set_toggle_mode(true);
+ camera_override_button->set_theme_type_variation("FlatButton");
+ camera_override_button->connect(SceneStringName(toggled), callable_mp(this, &GameView::_camera_override_button_toggled));
+ camera_override_button->set_tooltip_text(TTR("Override the in-game camera."));
+
+ camera_override_menu = memnew(MenuButton);
+ main_menu_hbox->add_child(camera_override_menu);
+ camera_override_menu->set_flat(false);
+ camera_override_menu->set_theme_type_variation("FlatMenuButton");
+ camera_override_menu->set_h_size_flags(SIZE_SHRINK_END);
+ camera_override_menu->set_tooltip_text(TTR("Camera Override Options"));
+
+ PopupMenu *menu = camera_override_menu->get_popup();
+ menu->connect(SceneStringName(id_pressed), callable_mp(this, &GameView::_camera_override_menu_id_pressed));
+ menu->add_item(TTR("Reset 2D Camera"), CAMERA_RESET_2D);
+ menu->add_item(TTR("Reset 3D Camera"), CAMERA_RESET_3D);
+ menu->add_separator();
+ menu->add_radio_check_item(TTR("Manipulate In-Game"), CAMERA_MODE_INGAME);
+ menu->set_item_checked(menu->get_item_index(CAMERA_MODE_INGAME), true);
+ menu->add_radio_check_item(TTR("Manipulate From Editors"), CAMERA_MODE_EDITORS);
+
+ _update_debugger_buttons();
+
+ panel = memnew(Panel);
+ add_child(panel);
+ panel->set_theme_type_variation("GamePanel");
+ panel->set_v_size_flags(SIZE_EXPAND_FILL);
+
+ p_debugger->connect("session_started", callable_mp(this, &GameView::_sessions_changed));
+ p_debugger->connect("session_stopped", callable_mp(this, &GameView::_sessions_changed));
+}
+
+///////
+
+void GameViewPlugin::make_visible(bool p_visible) {
+ game_view->set_visible(p_visible);
+}
+
+void GameViewPlugin::set_state(const Dictionary &p_state) {
+ game_view->set_state(p_state);
+}
+
+Dictionary GameViewPlugin::get_state() const {
+ return game_view->get_state();
+}
+
+void GameViewPlugin::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_ENTER_TREE: {
+ add_debugger_plugin(debugger);
+ } break;
+ case NOTIFICATION_EXIT_TREE: {
+ remove_debugger_plugin(debugger);
+ } break;
+ }
+}
+
+GameViewPlugin::GameViewPlugin() {
+ debugger.instantiate();
+
+ game_view = memnew(GameView(debugger));
+ game_view->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+ EditorNode::get_singleton()->get_editor_main_screen()->get_control()->add_child(game_view);
+ game_view->hide();
+}
+
+GameViewPlugin::~GameViewPlugin() {
+}
diff --git a/editor/plugins/game_view_plugin.h b/editor/plugins/game_view_plugin.h
new file mode 100644
index 0000000000..f8701c3e76
--- /dev/null
+++ b/editor/plugins/game_view_plugin.h
@@ -0,0 +1,152 @@
+/**************************************************************************/
+/* game_view_plugin.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. */
+/**************************************************************************/
+
+#ifndef GAME_VIEW_PLUGIN_H
+#define GAME_VIEW_PLUGIN_H
+
+#include "editor/debugger/editor_debugger_node.h"
+#include "editor/plugins/editor_debugger_plugin.h"
+#include "editor/plugins/editor_plugin.h"
+#include "scene/debugger/scene_debugger.h"
+#include "scene/gui/box_container.h"
+
+class GameViewDebugger : public EditorDebuggerPlugin {
+ GDCLASS(GameViewDebugger, EditorDebuggerPlugin);
+
+private:
+ Vector<Ref<EditorDebuggerSession>> sessions;
+
+ int node_type = RuntimeNodeSelect::NODE_TYPE_NONE;
+ bool selection_visible = true;
+ int select_mode = RuntimeNodeSelect::SELECT_MODE_SINGLE;
+ EditorDebuggerNode::CameraOverride camera_override_mode = EditorDebuggerNode::OVERRIDE_INGAME;
+
+ void _session_started(Ref<EditorDebuggerSession> p_session);
+ void _session_stopped();
+
+protected:
+ static void _bind_methods();
+
+public:
+ void set_suspend(bool p_enabled);
+ void next_frame();
+
+ void set_node_type(int p_type);
+ void set_select_mode(int p_mode);
+
+ void set_selection_visible(bool p_visible);
+
+ void set_camera_override(bool p_enabled);
+ void set_camera_manipulate_mode(EditorDebuggerNode::CameraOverride p_mode);
+
+ void reset_camera_2d_position();
+ void reset_camera_3d_position();
+
+ virtual void setup_session(int p_session_id) override;
+
+ GameViewDebugger() {}
+};
+
+class GameView : public VBoxContainer {
+ GDCLASS(GameView, VBoxContainer);
+
+ enum {
+ CAMERA_RESET_2D,
+ CAMERA_RESET_3D,
+ CAMERA_MODE_INGAME,
+ CAMERA_MODE_EDITORS,
+ };
+
+ Ref<GameViewDebugger> debugger;
+
+ int active_sessions = 0;
+
+ Button *suspend_button = nullptr;
+ Button *next_frame_button = nullptr;
+
+ Button *node_type_button[RuntimeNodeSelect::NODE_TYPE_MAX];
+ Button *select_mode_button[RuntimeNodeSelect::SELECT_MODE_MAX];
+
+ Button *hide_selection = nullptr;
+
+ Button *camera_override_button = nullptr;
+ MenuButton *camera_override_menu = nullptr;
+
+ Panel *panel = nullptr;
+
+ void _sessions_changed();
+
+ void _update_debugger_buttons();
+
+ void _suspend_button_toggled(bool p_pressed);
+
+ void _node_type_pressed(int p_option);
+ void _select_mode_pressed(int p_option);
+
+ void _hide_selection_toggled(bool p_pressed);
+
+ void _camera_override_button_toggled(bool p_pressed);
+ void _camera_override_menu_id_pressed(int p_id);
+
+protected:
+ void _notification(int p_what);
+
+public:
+ void set_state(const Dictionary &p_state);
+ Dictionary get_state() const;
+
+ GameView(Ref<GameViewDebugger> p_debugger);
+};
+
+class GameViewPlugin : public EditorPlugin {
+ GDCLASS(GameViewPlugin, EditorPlugin);
+
+ GameView *game_view = nullptr;
+
+ Ref<GameViewDebugger> debugger;
+
+protected:
+ void _notification(int p_what);
+
+public:
+ virtual String get_name() const override { return "Game"; }
+ bool has_main_screen() const override { return true; }
+ virtual void edit(Object *p_object) override {}
+ virtual bool handles(Object *p_object) const override { return false; }
+ virtual void make_visible(bool p_visible) override;
+
+ virtual void set_state(const Dictionary &p_state) override;
+ virtual Dictionary get_state() const override;
+
+ GameViewPlugin();
+ ~GameViewPlugin();
+};
+
+#endif // GAME_VIEW_PLUGIN_H
diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp
index 6fc73f69dd..b877c0b83e 100644
--- a/editor/plugins/node_3d_editor_plugin.cpp
+++ b/editor/plugins/node_3d_editor_plugin.cpp
@@ -1692,7 +1692,7 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) {
Ref<InputEventMouseButton> b = p_event;
if (b.is_valid()) {
- emit_signal(SNAME("clicked"), this);
+ emit_signal(SNAME("clicked"));
ViewportNavMouseButton orbit_mouse_preference = (ViewportNavMouseButton)EDITOR_GET("editors/3d/navigation/orbit_mouse_button").operator int();
ViewportNavMouseButton pan_mouse_preference = (ViewportNavMouseButton)EDITOR_GET("editors/3d/navigation/pan_mouse_button").operator int();
@@ -4210,7 +4210,7 @@ Dictionary Node3DEditorViewport::get_state() const {
void Node3DEditorViewport::_bind_methods() {
ADD_SIGNAL(MethodInfo("toggle_maximize_view", PropertyInfo(Variant::OBJECT, "viewport")));
- ADD_SIGNAL(MethodInfo("clicked", PropertyInfo(Variant::OBJECT, "viewport")));
+ ADD_SIGNAL(MethodInfo("clicked"));
}
void Node3DEditorViewport::reset() {
@@ -6572,18 +6572,6 @@ void Node3DEditor::_menu_item_toggled(bool pressed, int p_option) {
tool_option_button[TOOL_OPT_USE_SNAP]->set_pressed(pressed);
snap_enabled = pressed;
} break;
-
- case MENU_TOOL_OVERRIDE_CAMERA: {
- EditorDebuggerNode *const debugger = EditorDebuggerNode::get_singleton();
-
- using Override = EditorDebuggerNode::CameraOverride;
- if (pressed) {
- debugger->set_camera_override((Override)(Override::OVERRIDE_3D_1 + camera_override_viewport_id));
- } else {
- debugger->set_camera_override(Override::OVERRIDE_NONE);
- }
-
- } break;
}
}
@@ -6610,36 +6598,6 @@ void Node3DEditor::_menu_gizmo_toggled(int p_option) {
update_all_gizmos();
}
-void Node3DEditor::_update_camera_override_button(bool p_game_running) {
- Button *const button = tool_option_button[TOOL_OPT_OVERRIDE_CAMERA];
-
- if (p_game_running) {
- button->set_disabled(false);
- button->set_tooltip_text(TTR("Project Camera Override\nOverrides the running project's camera with the editor viewport camera."));
- } else {
- button->set_disabled(true);
- button->set_pressed(false);
- button->set_tooltip_text(TTR("Project Camera Override\nNo project instance running. Run the project from the editor to use this feature."));
- }
-}
-
-void Node3DEditor::_update_camera_override_viewport(Object *p_viewport) {
- Node3DEditorViewport *current_viewport = Object::cast_to<Node3DEditorViewport>(p_viewport);
-
- if (!current_viewport) {
- return;
- }
-
- EditorDebuggerNode *const debugger = EditorDebuggerNode::get_singleton();
-
- camera_override_viewport_id = current_viewport->index;
- if (debugger->get_camera_override() >= EditorDebuggerNode::OVERRIDE_3D_1) {
- using Override = EditorDebuggerNode::CameraOverride;
-
- debugger->set_camera_override((Override)(Override::OVERRIDE_3D_1 + camera_override_viewport_id));
- }
-}
-
void Node3DEditor::_menu_item_pressed(int p_option) {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
switch (p_option) {
@@ -6670,6 +6628,9 @@ void Node3DEditor::_menu_item_pressed(int p_option) {
} break;
case MENU_VIEW_USE_1_VIEWPORT: {
viewport_base->set_view(Node3DEditorViewportContainer::VIEW_USE_1_VIEWPORT);
+ if (last_used_viewport > 0) {
+ last_used_viewport = 0;
+ }
view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), true);
view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), false);
@@ -6681,6 +6642,9 @@ void Node3DEditor::_menu_item_pressed(int p_option) {
} break;
case MENU_VIEW_USE_2_VIEWPORTS: {
viewport_base->set_view(Node3DEditorViewportContainer::VIEW_USE_2_VIEWPORTS);
+ if (last_used_viewport > 1) {
+ last_used_viewport = 0;
+ }
view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), false);
view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), true);
@@ -6692,6 +6656,9 @@ void Node3DEditor::_menu_item_pressed(int p_option) {
} break;
case MENU_VIEW_USE_2_VIEWPORTS_ALT: {
viewport_base->set_view(Node3DEditorViewportContainer::VIEW_USE_2_VIEWPORTS_ALT);
+ if (last_used_viewport > 1) {
+ last_used_viewport = 0;
+ }
view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), false);
view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), false);
@@ -6703,6 +6670,9 @@ void Node3DEditor::_menu_item_pressed(int p_option) {
} break;
case MENU_VIEW_USE_3_VIEWPORTS: {
viewport_base->set_view(Node3DEditorViewportContainer::VIEW_USE_3_VIEWPORTS);
+ if (last_used_viewport > 2) {
+ last_used_viewport = 0;
+ }
view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), false);
view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), false);
@@ -6714,6 +6684,9 @@ void Node3DEditor::_menu_item_pressed(int p_option) {
} break;
case MENU_VIEW_USE_3_VIEWPORTS_ALT: {
viewport_base->set_view(Node3DEditorViewportContainer::VIEW_USE_3_VIEWPORTS_ALT);
+ if (last_used_viewport > 2) {
+ last_used_viewport = 0;
+ }
view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), false);
view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), false);
@@ -8033,7 +8006,6 @@ void Node3DEditor::_update_theme() {
tool_option_button[TOOL_OPT_LOCAL_COORDS]->set_button_icon(get_editor_theme_icon(SNAME("Object")));
tool_option_button[TOOL_OPT_USE_SNAP]->set_button_icon(get_editor_theme_icon(SNAME("Snap")));
- tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->set_button_icon(get_editor_theme_icon(SNAME("Camera3D")));
view_menu->get_popup()->set_item_icon(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), get_editor_theme_icon(SNAME("Panels1")));
view_menu->get_popup()->set_item_icon(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), get_editor_theme_icon(SNAME("Panels2")));
@@ -8068,9 +8040,6 @@ void Node3DEditor::_notification(int p_what) {
SceneTreeDock::get_singleton()->get_tree_editor()->connect("node_changed", callable_mp(this, &Node3DEditor::_refresh_menu_icons));
editor_selection->connect("selection_changed", callable_mp(this, &Node3DEditor::_selection_changed));
- EditorRunBar::get_singleton()->connect("stop_pressed", callable_mp(this, &Node3DEditor::_update_camera_override_button).bind(false));
- EditorRunBar::get_singleton()->connect("play_pressed", callable_mp(this, &Node3DEditor::_update_camera_override_button).bind(true));
-
_update_preview_environment();
sun_state->set_custom_minimum_size(sun_vb->get_combined_minimum_size());
@@ -8106,15 +8075,6 @@ void Node3DEditor::_notification(int p_what) {
}
} break;
- case NOTIFICATION_VISIBILITY_CHANGED: {
- if (!is_visible() && tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->is_pressed()) {
- EditorDebuggerNode *debugger = EditorDebuggerNode::get_singleton();
-
- debugger->set_camera_override(EditorDebuggerNode::OVERRIDE_NONE);
- tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->set_pressed(false);
- }
- } break;
-
case NOTIFICATION_PHYSICS_PROCESS: {
if (do_snap_selected_nodes_to_floor) {
_snap_selected_nodes_to_floor();
@@ -8216,6 +8176,10 @@ VSplitContainer *Node3DEditor::get_shader_split() {
return shader_split;
}
+Node3DEditorViewport *Node3DEditor::get_last_used_viewport() {
+ return viewports[last_used_viewport];
+}
+
void Node3DEditor::add_control_to_left_panel(Control *p_control) {
left_panel_split->add_child(p_control);
left_panel_split->move_child(p_control, 0);
@@ -8393,6 +8357,10 @@ void Node3DEditor::_toggle_maximize_view(Object *p_viewport) {
}
}
+void Node3DEditor::_viewport_clicked(int p_viewport_idx) {
+ last_used_viewport = p_viewport_idx;
+}
+
void Node3DEditor::_node_added(Node *p_node) {
if (EditorNode::get_singleton()->get_scene_root()->is_ancestor_of(p_node)) {
if (Object::cast_to<WorldEnvironment>(p_node)) {
@@ -8684,8 +8652,6 @@ Node3DEditor::Node3DEditor() {
snap_key_enabled = false;
tool_mode = TOOL_MODE_SELECT;
- camera_override_viewport_id = 0;
-
// Add some margin to the sides for better aesthetics.
// This prevents the first button's hover/pressed effect from "touching" the panel's border,
// which looks ugly.
@@ -8803,16 +8769,6 @@ Node3DEditor::Node3DEditor() {
tool_option_button[TOOL_OPT_USE_SNAP]->set_shortcut_context(this);
main_menu_hbox->add_child(memnew(VSeparator));
-
- tool_option_button[TOOL_OPT_OVERRIDE_CAMERA] = memnew(Button);
- main_menu_hbox->add_child(tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]);
- tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->set_toggle_mode(true);
- tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->set_theme_type_variation("FlatButton");
- tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->set_disabled(true);
- tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditor::_menu_item_toggled).bind(MENU_TOOL_OVERRIDE_CAMERA));
- _update_camera_override_button(false);
-
- main_menu_hbox->add_child(memnew(VSeparator));
sun_button = memnew(Button);
sun_button->set_tooltip_text(TTR("Toggle preview sunlight.\nIf a DirectionalLight3D node is added to the scene, preview sunlight is disabled."));
sun_button->set_toggle_mode(true);
@@ -8955,7 +8911,7 @@ Node3DEditor::Node3DEditor() {
for (uint32_t i = 0; i < VIEWPORTS_COUNT; i++) {
viewports[i] = memnew(Node3DEditorViewport(this, i));
viewports[i]->connect("toggle_maximize_view", callable_mp(this, &Node3DEditor::_toggle_maximize_view));
- viewports[i]->connect("clicked", callable_mp(this, &Node3DEditor::_update_camera_override_viewport));
+ viewports[i]->connect("clicked", callable_mp(this, &Node3DEditor::_viewport_clicked).bind(i));
viewports[i]->assign_pending_data_pointers(preview_node, &preview_bounds, accept);
viewport_base->add_child(viewports[i]);
}
diff --git a/editor/plugins/node_3d_editor_plugin.h b/editor/plugins/node_3d_editor_plugin.h
index 1b03362606..d35fcb7653 100644
--- a/editor/plugins/node_3d_editor_plugin.h
+++ b/editor/plugins/node_3d_editor_plugin.h
@@ -622,7 +622,6 @@ public:
enum ToolOptions {
TOOL_OPT_LOCAL_COORDS,
TOOL_OPT_USE_SNAP,
- TOOL_OPT_OVERRIDE_CAMERA,
TOOL_OPT_MAX
};
@@ -632,6 +631,8 @@ private:
Node3DEditorViewportContainer *viewport_base = nullptr;
Node3DEditorViewport *viewports[VIEWPORTS_COUNT];
+ int last_used_viewport = 0;
+
VSplitContainer *shader_split = nullptr;
HSplitContainer *left_panel_split = nullptr;
HSplitContainer *right_panel_split = nullptr;
@@ -704,7 +705,6 @@ private:
MENU_TOOL_LIST_SELECT,
MENU_TOOL_LOCAL_COORDS,
MENU_TOOL_USE_SNAP,
- MENU_TOOL_OVERRIDE_CAMERA,
MENU_TRANSFORM_CONFIGURE_SNAP,
MENU_TRANSFORM_DIALOG,
MENU_VIEW_USE_1_VIEWPORT,
@@ -759,8 +759,6 @@ private:
void _menu_item_pressed(int p_option);
void _menu_item_toggled(bool pressed, int p_option);
void _menu_gizmo_toggled(int p_option);
- void _update_camera_override_button(bool p_game_running);
- void _update_camera_override_viewport(Object *p_viewport);
// Used for secondary menu items which are displayed depending on the currently selected node
// (such as MeshInstance's "Mesh" menu).
PanelContainer *context_toolbar_panel = nullptr;
@@ -771,8 +769,6 @@ private:
void _generate_selection_boxes();
- int camera_override_viewport_id;
-
void _init_indicators();
void _update_gizmos_menu();
void _update_gizmos_menu_theme();
@@ -781,6 +777,7 @@ private:
void _finish_grid();
void _toggle_maximize_view(Object *p_viewport);
+ void _viewport_clicked(int p_viewport_idx);
Node *custom_camera = nullptr;
@@ -967,6 +964,7 @@ public:
ERR_FAIL_INDEX_V(p_idx, static_cast<int>(VIEWPORTS_COUNT), nullptr);
return viewports[p_idx];
}
+ Node3DEditorViewport *get_last_used_viewport();
void add_gizmo_plugin(Ref<EditorNode3DGizmoPlugin> p_plugin);
void remove_gizmo_plugin(Ref<EditorNode3DGizmoPlugin> p_plugin);
diff --git a/editor/themes/editor_theme_manager.cpp b/editor/themes/editor_theme_manager.cpp
index 4db43f0703..32079f3753 100644
--- a/editor/themes/editor_theme_manager.cpp
+++ b/editor/themes/editor_theme_manager.cpp
@@ -1857,6 +1857,12 @@ void EditorThemeManager::_populate_editor_styles(const Ref<EditorTheme> &p_theme
p_theme->set_stylebox("ScriptEditorPanelFloating", EditorStringName(EditorStyles), make_empty_stylebox(0, 0, 0, 0));
p_theme->set_stylebox("ScriptEditor", EditorStringName(EditorStyles), make_empty_stylebox(0, 0, 0, 0));
+ // Game view.
+ p_theme->set_type_variation("GamePanel", "Panel");
+ Ref<StyleBoxFlat> game_panel = p_theme->get_stylebox(SNAME("panel"), SNAME("Panel"))->duplicate();
+ game_panel->set_corner_radius_all(0);
+ p_theme->set_stylebox(SceneStringName(panel), "GamePanel", game_panel);
+
// Main menu.
Ref<StyleBoxFlat> menu_transparent_style = p_config.button_style->duplicate();
menu_transparent_style->set_bg_color(Color(1, 1, 1, 0));
diff --git a/misc/extension_api_validation/4.3-stable.expected b/misc/extension_api_validation/4.3-stable.expected
index 9e169d474d..3770664115 100644
--- a/misc/extension_api_validation/4.3-stable.expected
+++ b/misc/extension_api_validation/4.3-stable.expected
@@ -7,7 +7,6 @@ should instead be used to justify these changes and describe how users should wo
Add new entries at the end of the file.
## Changes between 4.3-stable and 4.4-stable
-
GH-95374
--------
Validate extension JSON: Error: Field 'classes/ShapeCast2D/properties/collision_result': getter changed value in new API, from "_get_collision_result" to &"get_collision_result".
@@ -102,3 +101,10 @@ GH-97020
Validate extension JSON: Error: Field 'classes/AnimationNode/methods/_process': is_const changed value in new API, from true to false.
`_process` virtual method fixed to be non const instead.
+
+
+GH-97257
+--------
+Validate extension JSON: Error: Field 'classes/EditorFeatureProfile/enums/Feature/values/FEATURE_MAX': value changed value in new API, from 8.0 to 9.
+
+New entry to the `EditorFeatureProfile.Feature` enum added. Those need to go before `FEATURE_MAX`, which will always cause a compatibility break.
diff --git a/scene/2d/camera_2d.cpp b/scene/2d/camera_2d.cpp
index ebed1e84e6..74f26df706 100644
--- a/scene/2d/camera_2d.cpp
+++ b/scene/2d/camera_2d.cpp
@@ -302,6 +302,7 @@ void Camera2D::_notification(int p_what) {
_interpolation_data.xform_prev = _interpolation_data.xform_curr;
} break;
+ case NOTIFICATION_SUSPENDED:
case NOTIFICATION_PAUSED: {
if (is_physics_interpolated_and_enabled()) {
_update_scroll();
diff --git a/scene/2d/gpu_particles_2d.cpp b/scene/2d/gpu_particles_2d.cpp
index cfdcbee86a..b50881cb89 100644
--- a/scene/2d/gpu_particles_2d.cpp
+++ b/scene/2d/gpu_particles_2d.cpp
@@ -696,6 +696,8 @@ void GPUParticles2D::_notification(int p_what) {
RS::get_singleton()->particles_set_subemitter(particles, RID());
} break;
+ case NOTIFICATION_SUSPENDED:
+ case NOTIFICATION_UNSUSPENDED:
case NOTIFICATION_PAUSED:
case NOTIFICATION_UNPAUSED: {
if (is_inside_tree()) {
diff --git a/scene/2d/navigation_agent_2d.cpp b/scene/2d/navigation_agent_2d.cpp
index d0fae611d8..f030473c4b 100644
--- a/scene/2d/navigation_agent_2d.cpp
+++ b/scene/2d/navigation_agent_2d.cpp
@@ -253,12 +253,20 @@ void NavigationAgent2D::_notification(int p_what) {
#endif // DEBUG_ENABLED
} break;
+ case NOTIFICATION_SUSPENDED:
case NOTIFICATION_PAUSED: {
if (agent_parent) {
NavigationServer2D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process());
}
} break;
+ case NOTIFICATION_UNSUSPENDED: {
+ if (get_tree()->is_paused()) {
+ break;
+ }
+ [[fallthrough]];
+ }
+
case NOTIFICATION_UNPAUSED: {
if (agent_parent) {
NavigationServer2D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process());
diff --git a/scene/2d/navigation_obstacle_2d.cpp b/scene/2d/navigation_obstacle_2d.cpp
index 3bf90249f8..f6502a77e9 100644
--- a/scene/2d/navigation_obstacle_2d.cpp
+++ b/scene/2d/navigation_obstacle_2d.cpp
@@ -104,6 +104,7 @@ void NavigationObstacle2D::_notification(int p_what) {
#endif // DEBUG_ENABLED
} break;
+ case NOTIFICATION_SUSPENDED:
case NOTIFICATION_PAUSED: {
if (!can_process()) {
map_before_pause = map_current;
@@ -115,6 +116,13 @@ void NavigationObstacle2D::_notification(int p_what) {
NavigationServer2D::get_singleton()->obstacle_set_paused(obstacle, !can_process());
} break;
+ case NOTIFICATION_UNSUSPENDED: {
+ if (get_tree()->is_paused()) {
+ break;
+ }
+ [[fallthrough]];
+ }
+
case NOTIFICATION_UNPAUSED: {
if (!can_process()) {
map_before_pause = map_current;
diff --git a/scene/2d/touch_screen_button.cpp b/scene/2d/touch_screen_button.cpp
index ff409272c5..cbfd7e38dc 100644
--- a/scene/2d/touch_screen_button.cpp
+++ b/scene/2d/touch_screen_button.cpp
@@ -185,6 +185,7 @@ void TouchScreenButton::_notification(int p_what) {
}
} break;
+ case NOTIFICATION_SUSPENDED:
case NOTIFICATION_PAUSED: {
if (is_pressed()) {
_release();
diff --git a/scene/3d/camera_3d.cpp b/scene/3d/camera_3d.cpp
index aafc2141af..2b841c4e0d 100644
--- a/scene/3d/camera_3d.cpp
+++ b/scene/3d/camera_3d.cpp
@@ -234,6 +234,7 @@ void Camera3D::_notification(int p_what) {
}
} break;
+ case NOTIFICATION_SUSPENDED:
case NOTIFICATION_PAUSED: {
if (is_physics_interpolated_and_enabled() && is_inside_tree() && is_visible_in_tree()) {
_physics_interpolation_ensure_transform_calculated(true);
diff --git a/scene/3d/gpu_particles_3d.cpp b/scene/3d/gpu_particles_3d.cpp
index 2cef607d29..b48a3a87c7 100644
--- a/scene/3d/gpu_particles_3d.cpp
+++ b/scene/3d/gpu_particles_3d.cpp
@@ -511,6 +511,8 @@ void GPUParticles3D::_notification(int p_what) {
RS::get_singleton()->particles_set_subemitter(particles, RID());
} break;
+ case NOTIFICATION_SUSPENDED:
+ case NOTIFICATION_UNSUSPENDED:
case NOTIFICATION_PAUSED:
case NOTIFICATION_UNPAUSED: {
if (is_inside_tree()) {
diff --git a/scene/3d/navigation_agent_3d.cpp b/scene/3d/navigation_agent_3d.cpp
index 5bbb724e2f..9242c2a2ea 100644
--- a/scene/3d/navigation_agent_3d.cpp
+++ b/scene/3d/navigation_agent_3d.cpp
@@ -272,12 +272,20 @@ void NavigationAgent3D::_notification(int p_what) {
#endif // DEBUG_ENABLED
} break;
+ case NOTIFICATION_SUSPENDED:
case NOTIFICATION_PAUSED: {
if (agent_parent) {
NavigationServer3D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process());
}
} break;
+ case NOTIFICATION_UNSUSPENDED: {
+ if (get_tree()->is_paused()) {
+ break;
+ }
+ [[fallthrough]];
+ }
+
case NOTIFICATION_UNPAUSED: {
if (agent_parent) {
NavigationServer3D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process());
diff --git a/scene/3d/navigation_obstacle_3d.cpp b/scene/3d/navigation_obstacle_3d.cpp
index f2ac8f789c..2eb04a0054 100644
--- a/scene/3d/navigation_obstacle_3d.cpp
+++ b/scene/3d/navigation_obstacle_3d.cpp
@@ -119,6 +119,7 @@ void NavigationObstacle3D::_notification(int p_what) {
#endif // DEBUG_ENABLED
} break;
+ case NOTIFICATION_SUSPENDED:
case NOTIFICATION_PAUSED: {
if (!can_process()) {
map_before_pause = map_current;
@@ -130,6 +131,13 @@ void NavigationObstacle3D::_notification(int p_what) {
NavigationServer3D::get_singleton()->obstacle_set_paused(obstacle, !can_process());
} break;
+ case NOTIFICATION_UNSUSPENDED: {
+ if (get_tree()->is_paused()) {
+ break;
+ }
+ [[fallthrough]];
+ }
+
case NOTIFICATION_UNPAUSED: {
if (!can_process()) {
map_before_pause = map_current;
diff --git a/scene/audio/audio_stream_player_internal.cpp b/scene/audio/audio_stream_player_internal.cpp
index 7d1ed56ca8..621aa31fc6 100644
--- a/scene/audio/audio_stream_player_internal.cpp
+++ b/scene/audio/audio_stream_player_internal.cpp
@@ -112,6 +112,7 @@ void AudioStreamPlayerInternal::notification(int p_what) {
stream_playbacks.clear();
} break;
+ case Node::NOTIFICATION_SUSPENDED:
case Node::NOTIFICATION_PAUSED: {
if (!node->can_process()) {
// Node can't process so we start fading out to silence
@@ -119,6 +120,13 @@ void AudioStreamPlayerInternal::notification(int p_what) {
}
} break;
+ case Node::NOTIFICATION_UNSUSPENDED: {
+ if (node->get_tree()->is_paused()) {
+ break;
+ }
+ [[fallthrough]];
+ }
+
case Node::NOTIFICATION_UNPAUSED: {
set_stream_paused(false);
} break;
diff --git a/scene/debugger/scene_debugger.cpp b/scene/debugger/scene_debugger.cpp
index 22e5238fae..b87285ed74 100644
--- a/scene/debugger/scene_debugger.cpp
+++ b/scene/debugger/scene_debugger.cpp
@@ -31,20 +31,34 @@
#include "scene_debugger.h"
#include "core/debugger/engine_debugger.h"
-#include "core/debugger/engine_profiler.h"
#include "core/io/marshalls.h"
#include "core/object/script_language.h"
#include "core/templates/local_vector.h"
+#include "scene/2d/physics/collision_object_2d.h"
+#include "scene/2d/physics/collision_polygon_2d.h"
+#include "scene/2d/physics/collision_shape_2d.h"
+#ifndef _3D_DISABLED
+#include "scene/3d/label_3d.h"
+#include "scene/3d/mesh_instance_3d.h"
+#include "scene/3d/physics/collision_object_3d.h"
+#include "scene/3d/physics/collision_shape_3d.h"
+#include "scene/3d/sprite_3d.h"
+#include "scene/resources/surface_tool.h"
+#endif // _3D_DISABLED
+#include "scene/gui/popup_menu.h"
+#include "scene/main/canvas_layer.h"
#include "scene/main/scene_tree.h"
#include "scene/main/window.h"
#include "scene/resources/packed_scene.h"
-
-SceneDebugger *SceneDebugger::singleton = nullptr;
+#include "scene/theme/theme_db.h"
SceneDebugger::SceneDebugger() {
singleton = this;
+
#ifdef DEBUG_ENABLED
LiveEditor::singleton = memnew(LiveEditor);
+ RuntimeNodeSelect::singleton = memnew(RuntimeNodeSelect);
+
EngineDebugger::register_message_capture("scene", EngineDebugger::Capture(nullptr, SceneDebugger::parse_message));
#endif
}
@@ -56,7 +70,13 @@ SceneDebugger::~SceneDebugger() {
memdelete(LiveEditor::singleton);
LiveEditor::singleton = nullptr;
}
+
+ if (RuntimeNodeSelect::singleton) {
+ memdelete(RuntimeNodeSelect::singleton);
+ RuntimeNodeSelect::singleton = nullptr;
+ }
#endif
+
singleton = nullptr;
}
@@ -78,10 +98,15 @@ Error SceneDebugger::parse_message(void *p_user, const String &p_msg, const Arra
if (!scene_tree) {
return ERR_UNCONFIGURED;
}
+
LiveEditor *live_editor = LiveEditor::get_singleton();
if (!live_editor) {
return ERR_UNCONFIGURED;
}
+ RuntimeNodeSelect *runtime_node_select = RuntimeNodeSelect::get_singleton();
+ if (!runtime_node_select) {
+ return ERR_UNCONFIGURED;
+ }
r_captured = true;
if (p_msg == "request_scene_tree") { // Scene tree
@@ -99,22 +124,34 @@ Error SceneDebugger::parse_message(void *p_user, const String &p_msg, const Arra
ObjectID id = p_args[0];
_send_object_id(id);
- } else if (p_msg == "override_camera_2D:set") { // Camera
+ } else if (p_msg == "suspend_changed") {
ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
- bool enforce = p_args[0];
- scene_tree->get_root()->enable_canvas_transform_override(enforce);
+ bool suspended = p_args[0];
+ scene_tree->set_suspend(suspended);
+ runtime_node_select->_update_input_state();
- } else if (p_msg == "override_camera_2D:transform") {
- ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
- Transform2D transform = p_args[0];
- scene_tree->get_root()->set_canvas_transform_override(transform);
-#ifndef _3D_DISABLED
- } else if (p_msg == "override_camera_3D:set") {
+ } else if (p_msg == "next_frame") {
+ _next_frame();
+
+ } else if (p_msg == "override_cameras") { // Camera
ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
bool enable = p_args[0];
+ bool from_editor = p_args[1];
+ scene_tree->get_root()->enable_canvas_transform_override(enable);
+#ifndef _3D_DISABLED
scene_tree->get_root()->enable_camera_3d_override(enable);
+#endif // _3D_DISABLED
+ runtime_node_select->_set_camera_override_enabled(enable && !from_editor);
- } else if (p_msg == "override_camera_3D:transform") {
+ } else if (p_msg == "transform_camera_2d") {
+ ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
+ Transform2D transform = p_args[0];
+ scene_tree->get_root()->set_canvas_transform_override(transform);
+
+ runtime_node_select->_queue_selection_update();
+
+#ifndef _3D_DISABLED
+ } else if (p_msg == "transform_camera_3d") {
ERR_FAIL_COND_V(p_args.size() < 5, ERR_INVALID_DATA);
Transform3D transform = p_args[0];
bool is_perspective = p_args[1];
@@ -127,95 +164,142 @@ Error SceneDebugger::parse_message(void *p_user, const String &p_msg, const Arra
scene_tree->get_root()->set_camera_3d_override_orthogonal(size_or_fov, depth_near, depth_far);
}
scene_tree->get_root()->set_camera_3d_override_transform(transform);
+
+ runtime_node_select->_queue_selection_update();
#endif // _3D_DISABLED
+
} else if (p_msg == "set_object_property") {
ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
_set_object_property(p_args[0], p_args[1], p_args[2]);
- } else if (!p_msg.begins_with("live_")) { // Live edits below.
- return ERR_SKIP;
- } else if (p_msg == "live_set_root") {
- ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
- live_editor->_root_func(p_args[0], p_args[1]);
+ runtime_node_select->_queue_selection_update();
+
+ } else if (p_msg.begins_with("live_")) { /// Live Edit
+ if (p_msg == "live_set_root") {
+ ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
+ live_editor->_root_func(p_args[0], p_args[1]);
+
+ } else if (p_msg == "live_node_path") {
+ ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
+ live_editor->_node_path_func(p_args[0], p_args[1]);
+
+ } else if (p_msg == "live_res_path") {
+ ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
+ live_editor->_res_path_func(p_args[0], p_args[1]);
+
+ } else if (p_msg == "live_node_prop_res") {
+ ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
+ live_editor->_node_set_res_func(p_args[0], p_args[1], p_args[2]);
+
+ } else if (p_msg == "live_node_prop") {
+ ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
+ live_editor->_node_set_func(p_args[0], p_args[1], p_args[2]);
+
+ } else if (p_msg == "live_res_prop_res") {
+ ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
+ live_editor->_res_set_res_func(p_args[0], p_args[1], p_args[2]);
+
+ } else if (p_msg == "live_res_prop") {
+ ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
+ live_editor->_res_set_func(p_args[0], p_args[1], p_args[2]);
+
+ } else if (p_msg == "live_node_call") {
+ ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
+ LocalVector<Variant> args;
+ LocalVector<Variant *> argptrs;
+ args.resize(p_args.size() - 2);
+ argptrs.resize(args.size());
+ for (uint32_t i = 0; i < args.size(); i++) {
+ args[i] = p_args[i + 2];
+ argptrs[i] = &args[i];
+ }
+ live_editor->_node_call_func(p_args[0], p_args[1], argptrs.size() ? (const Variant **)argptrs.ptr() : nullptr, argptrs.size());
+
+ } else if (p_msg == "live_res_call") {
+ ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
+ LocalVector<Variant> args;
+ LocalVector<Variant *> argptrs;
+ args.resize(p_args.size() - 2);
+ argptrs.resize(args.size());
+ for (uint32_t i = 0; i < args.size(); i++) {
+ args[i] = p_args[i + 2];
+ argptrs[i] = &args[i];
+ }
+ live_editor->_res_call_func(p_args[0], p_args[1], argptrs.size() ? (const Variant **)argptrs.ptr() : nullptr, argptrs.size());
- } else if (p_msg == "live_node_path") {
- ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
- live_editor->_node_path_func(p_args[0], p_args[1]);
+ } else if (p_msg == "live_create_node") {
+ ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
+ live_editor->_create_node_func(p_args[0], p_args[1], p_args[2]);
- } else if (p_msg == "live_res_path") {
- ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
- live_editor->_res_path_func(p_args[0], p_args[1]);
+ } else if (p_msg == "live_instantiate_node") {
+ ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
+ live_editor->_instance_node_func(p_args[0], p_args[1], p_args[2]);
- } else if (p_msg == "live_node_prop_res") {
- ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
- live_editor->_node_set_res_func(p_args[0], p_args[1], p_args[2]);
+ } else if (p_msg == "live_remove_node") {
+ ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
+ live_editor->_remove_node_func(p_args[0]);
- } else if (p_msg == "live_node_prop") {
- ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
- live_editor->_node_set_func(p_args[0], p_args[1], p_args[2]);
+ if (!runtime_node_select->has_selection) {
+ runtime_node_select->_clear_selection();
+ }
- } else if (p_msg == "live_res_prop_res") {
- ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
- live_editor->_res_set_res_func(p_args[0], p_args[1], p_args[2]);
+ } else if (p_msg == "live_remove_and_keep_node") {
+ ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
+ live_editor->_remove_and_keep_node_func(p_args[0], p_args[1]);
- } else if (p_msg == "live_res_prop") {
- ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
- live_editor->_res_set_func(p_args[0], p_args[1], p_args[2]);
+ if (!runtime_node_select->has_selection) {
+ runtime_node_select->_clear_selection();
+ }
- } else if (p_msg == "live_node_call") {
- ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
- LocalVector<Variant> args;
- LocalVector<Variant *> argptrs;
- args.resize(p_args.size() - 2);
- argptrs.resize(args.size());
- for (uint32_t i = 0; i < args.size(); i++) {
- args[i] = p_args[i + 2];
- argptrs[i] = &args[i];
- }
- live_editor->_node_call_func(p_args[0], p_args[1], argptrs.size() ? (const Variant **)argptrs.ptr() : nullptr, argptrs.size());
+ } else if (p_msg == "live_restore_node") {
+ ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
+ live_editor->_restore_node_func(p_args[0], p_args[1], p_args[2]);
- } else if (p_msg == "live_res_call") {
- ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
- LocalVector<Variant> args;
- LocalVector<Variant *> argptrs;
- args.resize(p_args.size() - 2);
- argptrs.resize(args.size());
- for (uint32_t i = 0; i < args.size(); i++) {
- args[i] = p_args[i + 2];
- argptrs[i] = &args[i];
+ } else if (p_msg == "live_duplicate_node") {
+ ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
+ live_editor->_duplicate_node_func(p_args[0], p_args[1]);
+
+ } else if (p_msg == "live_reparent_node") {
+ ERR_FAIL_COND_V(p_args.size() < 4, ERR_INVALID_DATA);
+ live_editor->_reparent_node_func(p_args[0], p_args[1], p_args[2], p_args[3]);
+
+ } else {
+ return ERR_SKIP;
}
- live_editor->_res_call_func(p_args[0], p_args[1], argptrs.size() ? (const Variant **)argptrs.ptr() : nullptr, argptrs.size());
- } else if (p_msg == "live_create_node") {
- ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
- live_editor->_create_node_func(p_args[0], p_args[1], p_args[2]);
+ } else if (p_msg.begins_with("runtime_node_select_")) { /// Runtime Node Selection
+ if (p_msg == "runtime_node_select_setup") {
+ runtime_node_select->_setup();
- } else if (p_msg == "live_instantiate_node") {
- ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
- live_editor->_instance_node_func(p_args[0], p_args[1], p_args[2]);
+ } else if (p_msg == "runtime_node_select_set_type") {
+ ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
+ RuntimeNodeSelect::NodeType type = (RuntimeNodeSelect::NodeType)(int)p_args[0];
+ runtime_node_select->_node_set_type(type);
- } else if (p_msg == "live_remove_node") {
- ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
- live_editor->_remove_node_func(p_args[0]);
+ } else if (p_msg == "runtime_node_select_set_mode") {
+ ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
+ RuntimeNodeSelect::SelectMode mode = (RuntimeNodeSelect::SelectMode)(int)p_args[0];
+ runtime_node_select->_select_set_mode(mode);
- } else if (p_msg == "live_remove_and_keep_node") {
- ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
- live_editor->_remove_and_keep_node_func(p_args[0], p_args[1]);
+ } else if (p_msg == "runtime_node_select_set_visible") {
+ ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
+ bool visible = p_args[0];
+ runtime_node_select->_set_selection_visible(visible);
- } else if (p_msg == "live_restore_node") {
- ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
- live_editor->_restore_node_func(p_args[0], p_args[1], p_args[2]);
+ } else if (p_msg == "runtime_node_select_reset_camera_2d") {
+ runtime_node_select->_reset_camera_2d();
- } else if (p_msg == "live_duplicate_node") {
- ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
- live_editor->_duplicate_node_func(p_args[0], p_args[1]);
+ } else if (p_msg == "runtime_node_select_reset_camera_3d") {
+ runtime_node_select->_reset_camera_3d();
+
+ } else {
+ return ERR_SKIP;
+ }
- } else if (p_msg == "live_reparent_node") {
- ERR_FAIL_COND_V(p_args.size() < 4, ERR_INVALID_DATA);
- live_editor->_reparent_node_func(p_args[0], p_args[1], p_args[2], p_args[3]);
} else {
r_captured = false;
}
+
return OK;
}
@@ -260,6 +344,9 @@ void SceneDebugger::_send_object_id(ObjectID p_id, int p_max_size) {
return;
}
+ Node *node = Object::cast_to<Node>(ObjectDB::get_instance(p_id));
+ RuntimeNodeSelect::get_singleton()->_select_node(node);
+
Array arr;
obj.serialize(arr);
EngineDebugger::get_singleton()->send_message("scene:inspect_object", arr);
@@ -280,6 +367,16 @@ void SceneDebugger::_set_object_property(ObjectID p_id, const String &p_property
obj->set(prop_name, p_value);
}
+void SceneDebugger::_next_frame() {
+ SceneTree *scene_tree = SceneTree::get_singleton();
+ if (!scene_tree->is_suspended()) {
+ return;
+ }
+
+ scene_tree->set_suspend(false);
+ RenderingServer::get_singleton()->connect("frame_post_draw", callable_mp(scene_tree, &SceneTree::set_suspend).bind(true), Object::CONNECT_ONE_SHOT);
+}
+
void SceneDebugger::add_to_cache(const String &p_filename, Node *p_node) {
LiveEditor *debugger = LiveEditor::get_singleton();
if (!debugger) {
@@ -580,7 +677,6 @@ void SceneDebuggerTree::deserialize(const Array &p_arr) {
}
/// LiveEditor
-LiveEditor *LiveEditor::singleton = nullptr;
LiveEditor *LiveEditor::get_singleton() {
return singleton;
}
@@ -1089,4 +1185,942 @@ void LiveEditor::_reparent_node_func(const NodePath &p_at, const NodePath &p_new
}
}
+/// RuntimeNodeSelect
+RuntimeNodeSelect *RuntimeNodeSelect::get_singleton() {
+ return singleton;
+}
+
+RuntimeNodeSelect::~RuntimeNodeSelect() {
+ if (selection_list && !selection_list->is_visible()) {
+ memdelete(selection_list);
+ }
+
+ if (sbox_2d_canvas.is_valid()) {
+ RS::get_singleton()->free(sbox_2d_canvas);
+ RS::get_singleton()->free(sbox_2d_ci);
+ }
+
+#ifndef _3D_DISABLED
+ if (sbox_3d_instance.is_valid()) {
+ RS::get_singleton()->free(sbox_3d_instance);
+ RS::get_singleton()->free(sbox_3d_instance_ofs);
+ RS::get_singleton()->free(sbox_3d_instance_xray);
+ RS::get_singleton()->free(sbox_3d_instance_xray_ofs);
+ }
+#endif // _3D_DISABLED
+}
+
+void RuntimeNodeSelect::_setup() {
+ Window *root = SceneTree::get_singleton()->get_root();
+ ERR_FAIL_COND(root->is_connected(SceneStringName(window_input), callable_mp(this, &RuntimeNodeSelect::_root_window_input)));
+
+ root->connect(SceneStringName(window_input), callable_mp(this, &RuntimeNodeSelect::_root_window_input));
+ root->connect("size_changed", callable_mp(this, &RuntimeNodeSelect::_queue_selection_update), CONNECT_DEFERRED);
+
+ selection_list = memnew(PopupMenu);
+ selection_list->set_theme(ThemeDB::get_singleton()->get_default_theme());
+ selection_list->set_auto_translate_mode(Node::AUTO_TRANSLATE_MODE_DISABLED);
+ selection_list->set_force_native(true);
+ selection_list->connect("index_pressed", callable_mp(this, &RuntimeNodeSelect::_items_popup_index_pressed).bind(selection_list));
+ selection_list->connect("popup_hide", callable_mp(Object::cast_to<Node>(root), &Node::remove_child).bind(selection_list));
+
+ panner.instantiate();
+ panner->set_callbacks(callable_mp(this, &RuntimeNodeSelect::_pan_callback), callable_mp(this, &RuntimeNodeSelect::_zoom_callback));
+
+ /// 2D Selection Box Generation
+
+ sbox_2d_canvas = RS::get_singleton()->canvas_create();
+ sbox_2d_ci = RS::get_singleton()->canvas_item_create();
+ RS::get_singleton()->viewport_attach_canvas(root->get_viewport_rid(), sbox_2d_canvas);
+ RS::get_singleton()->canvas_item_set_parent(sbox_2d_ci, sbox_2d_canvas);
+
+#ifndef _3D_DISABLED
+ cursor = Cursor();
+
+ /// 3D Selection Box Generation
+ // Copied from the Node3DEditor implementation.
+
+ // Use two AABBs to create the illusion of a slightly thicker line.
+ AABB aabb(Vector3(), Vector3(1, 1, 1));
+
+ // Create a x-ray (visible through solid surfaces) and standard version of the selection box.
+ // Both will be drawn at the same position, but with different opacity.
+ // This lets the user see where the selection is while still having a sense of depth.
+ Ref<SurfaceTool> st = memnew(SurfaceTool);
+ Ref<SurfaceTool> st_xray = memnew(SurfaceTool);
+
+ st->begin(Mesh::PRIMITIVE_LINES);
+ st_xray->begin(Mesh::PRIMITIVE_LINES);
+ for (int i = 0; i < 12; i++) {
+ Vector3 a, b;
+ aabb.get_edge(i, a, b);
+
+ st->add_vertex(a);
+ st->add_vertex(b);
+ st_xray->add_vertex(a);
+ st_xray->add_vertex(b);
+ }
+
+ Ref<StandardMaterial3D> mat = memnew(StandardMaterial3D);
+ mat->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
+ mat->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true);
+ // In the original Node3DEditor, this value would be fetched from the "editors/3d/selection_box_color" editor property,
+ // but since this is not accessible from here, we will just use the default value.
+ const Color selection_color_3d = Color(1, 0.5, 0);
+ mat->set_albedo(selection_color_3d);
+ mat->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA);
+ st->set_material(mat);
+ sbox_3d_mesh = st->commit();
+
+ Ref<StandardMaterial3D> mat_xray = memnew(StandardMaterial3D);
+ mat_xray->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
+ mat_xray->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true);
+ mat_xray->set_flag(StandardMaterial3D::FLAG_DISABLE_DEPTH_TEST, true);
+ mat_xray->set_albedo(selection_color_3d * Color(1, 1, 1, 0.15));
+ mat_xray->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA);
+ st_xray->set_material(mat_xray);
+ sbox_3d_mesh_xray = st_xray->commit();
+#endif // _3D_DISABLED
+
+ SceneTree::get_singleton()->connect("process_frame", callable_mp(this, &RuntimeNodeSelect::_process_frame));
+ SceneTree::get_singleton()->connect("physics_frame", callable_mp(this, &RuntimeNodeSelect::_physics_frame));
+
+ // This function will be called before the root enters the tree at first when the Game view is passing its settings to
+ // the debugger, so queue the update for after it enters.
+ root->connect(SceneStringName(tree_entered), callable_mp(this, &RuntimeNodeSelect::_update_input_state), Object::CONNECT_ONE_SHOT);
+}
+
+void RuntimeNodeSelect::_node_set_type(NodeType p_type) {
+ node_select_type = p_type;
+ _update_input_state();
+}
+
+void RuntimeNodeSelect::_select_set_mode(SelectMode p_mode) {
+ node_select_mode = p_mode;
+}
+
+void RuntimeNodeSelect::_set_camera_override_enabled(bool p_enabled) {
+ camera_override = p_enabled;
+
+ if (p_enabled) {
+ _update_view_2d();
+ }
+
+#ifndef _3D_DISABLED
+ if (camera_first_override) {
+ _reset_camera_2d();
+ _reset_camera_3d();
+
+ camera_first_override = false;
+ } else if (p_enabled) {
+ _update_view_2d();
+
+ SceneTree::get_singleton()->get_root()->set_camera_3d_override_transform(_get_cursor_transform());
+ SceneTree::get_singleton()->get_root()->set_camera_3d_override_perspective(CAMERA_BASE_FOV * cursor.fov_scale, CAMERA_ZNEAR, CAMERA_ZFAR);
+ }
+#endif // _3D_DISABLED
+}
+
+void RuntimeNodeSelect::_root_window_input(const Ref<InputEvent> &p_event) {
+ Window *root = SceneTree::get_singleton()->get_root();
+ if (node_select_type == NODE_TYPE_NONE || selection_list->is_visible()) {
+ // Workaround for platforms that don't allow subwindows.
+ if (selection_list->is_visible() && selection_list->is_embedded()) {
+ root->set_disable_input_override(false);
+ selection_list->push_input(p_event);
+ callable_mp(root->get_viewport(), &Viewport::set_disable_input_override).call_deferred(true);
+ }
+
+ return;
+ }
+
+ if (camera_override) {
+ if (node_select_type == NODE_TYPE_2D) {
+ if (panner->gui_input(p_event, Rect2(Vector2(), root->get_size()))) {
+ return;
+ }
+ } else if (node_select_type == NODE_TYPE_3D) {
+#ifndef _3D_DISABLED
+ if (root->get_camera_3d() && _handle_3d_input(p_event)) {
+ return;
+ }
+#endif // _3D_DISABLED
+ }
+ }
+
+ Ref<InputEventMouseButton> b = p_event;
+ if (!b.is_valid() || !b->is_pressed()) {
+ return;
+ }
+
+ list_shortcut_pressed = node_select_mode == SELECT_MODE_SINGLE && b->get_button_index() == MouseButton::RIGHT && b->is_alt_pressed();
+ if (list_shortcut_pressed || b->get_button_index() == MouseButton::LEFT) {
+ selection_position = b->get_position();
+ }
+}
+
+void RuntimeNodeSelect::_items_popup_index_pressed(int p_index, PopupMenu *p_popup) {
+ Object *obj = p_popup->get_item_metadata(p_index).get_validated_object();
+ if (!obj) {
+ return;
+ }
+
+ Array message;
+ message.append(obj->get_instance_id());
+ EngineDebugger::get_singleton()->send_message("remote_node_clicked", message);
+}
+
+void RuntimeNodeSelect::_update_input_state() {
+ SceneTree *scene_tree = SceneTree::get_singleton();
+ // This function can be called at the very beginning, when the root hasn't entered the tree yet.
+ // So check first to avoid a crash.
+ if (!scene_tree->get_root()->is_inside_tree()) {
+ return;
+ }
+
+ bool disable_input = scene_tree->is_suspended() || node_select_type != RuntimeNodeSelect::NODE_TYPE_NONE;
+ Input::get_singleton()->set_disable_input(disable_input);
+ Input::get_singleton()->set_mouse_mode_override_enabled(disable_input);
+ scene_tree->get_root()->set_disable_input_override(disable_input);
+}
+
+void RuntimeNodeSelect::_process_frame() {
+#ifndef _3D_DISABLED
+ if (camera_freelook) {
+ Transform3D transform = _get_cursor_transform();
+ Vector3 forward = transform.basis.xform(Vector3(0, 0, -1));
+ const Vector3 right = transform.basis.xform(Vector3(1, 0, 0));
+ Vector3 up = transform.basis.xform(Vector3(0, 1, 0));
+
+ Vector3 direction;
+
+ Input *input = Input::get_singleton();
+ bool was_input_disabled = input->is_input_disabled();
+ if (was_input_disabled) {
+ input->set_disable_input(false);
+ }
+
+ if (input->is_physical_key_pressed(Key::A)) {
+ direction -= right;
+ }
+ if (input->is_physical_key_pressed(Key::D)) {
+ direction += right;
+ }
+ if (input->is_physical_key_pressed(Key::W)) {
+ direction += forward;
+ }
+ if (input->is_physical_key_pressed(Key::S)) {
+ direction -= forward;
+ }
+ if (input->is_physical_key_pressed(Key::E)) {
+ direction += up;
+ }
+ if (input->is_physical_key_pressed(Key::Q)) {
+ direction -= up;
+ }
+
+ real_t speed = FREELOOK_BASE_SPEED;
+ if (input->is_physical_key_pressed(Key::SHIFT)) {
+ speed *= 3.0;
+ }
+ if (input->is_physical_key_pressed(Key::ALT)) {
+ speed *= 0.333333;
+ }
+
+ if (was_input_disabled) {
+ input->set_disable_input(true);
+ }
+
+ if (direction != Vector3()) {
+ // Calculate the process time manually, as the time scale is frozen.
+ const double process_time = (1.0 / Engine::get_singleton()->get_frames_per_second()) * Engine::get_singleton()->get_unfrozen_time_scale();
+ const Vector3 motion = direction * speed * process_time;
+ cursor.pos += motion;
+ cursor.eye_pos += motion;
+
+ SceneTree::get_singleton()->get_root()->set_camera_3d_override_transform(_get_cursor_transform());
+ }
+ }
+#endif // _3D_DISABLED
+
+ if (selection_update_queued || !SceneTree::get_singleton()->is_suspended()) {
+ selection_update_queued = false;
+ if (has_selection) {
+ _update_selection();
+ }
+ }
+}
+
+void RuntimeNodeSelect::_physics_frame() {
+ if (!Math::is_inf(selection_position.x) || !Math::is_inf(selection_position.y)) {
+ _click_point();
+ selection_position = Point2(INFINITY, INFINITY);
+ }
+}
+
+void RuntimeNodeSelect::_click_point() {
+ Window *root = SceneTree::get_singleton()->get_root();
+ Point2 pos = root->get_screen_transform().affine_inverse().xform(selection_position);
+ Vector<SelectResult> items;
+
+ if (node_select_type == NODE_TYPE_2D) {
+ for (int i = 0; i < root->get_child_count(); i++) {
+ _find_canvas_items_at_pos(pos, root->get_child(i), items);
+ }
+
+ // Remove possible duplicates.
+ for (int i = 0; i < items.size(); i++) {
+ Node *item = items[i].item;
+ for (int j = 0; j < i; j++) {
+ if (items[j].item == item) {
+ items.remove_at(i);
+ i--;
+
+ break;
+ }
+ }
+ }
+ } else if (node_select_type == NODE_TYPE_3D) {
+#ifndef _3D_DISABLED
+ _find_3d_items_at_pos(pos, items);
+#endif // _3D_DISABLED
+ }
+
+ if (items.is_empty()) {
+ return;
+ }
+
+ items.sort();
+
+ if ((!list_shortcut_pressed && node_select_mode == SELECT_MODE_SINGLE) || items.size() == 1) {
+ Array message;
+ message.append(items[0].item->get_instance_id());
+ EngineDebugger::get_singleton()->send_message("remote_node_clicked", message);
+ } else if (list_shortcut_pressed || node_select_mode == SELECT_MODE_LIST) {
+ if (!selection_list->is_inside_tree()) {
+ root->add_child(selection_list);
+ }
+
+ selection_list->clear();
+ for (const SelectResult &I : items) {
+ selection_list->add_item(I.item->get_name());
+ selection_list->set_item_metadata(-1, I.item);
+ }
+
+ selection_list->set_position(selection_list->is_embedded() ? pos : selection_position + root->get_position());
+ selection_list->reset_size();
+ selection_list->popup();
+ // FIXME: Ugly hack that stops the popup from hiding when the button is released.
+ selection_list->call_deferred(SNAME("set_position"), selection_list->get_position() + Point2(1, 0));
+ }
+}
+
+void RuntimeNodeSelect::_select_node(Node *p_node) {
+ if (p_node == selected_node) {
+ return;
+ }
+
+ _clear_selection();
+
+ CanvasItem *ci = Object::cast_to<CanvasItem>(p_node);
+ if (ci) {
+ selected_node = p_node;
+ } else {
+#ifndef _3D_DISABLED
+ Node3D *node_3d = Object::cast_to<Node3D>(p_node);
+ if (node_3d) {
+ if (!node_3d->is_inside_world()) {
+ return;
+ }
+
+ selected_node = p_node;
+
+ sbox_3d_instance = RS::get_singleton()->instance_create2(sbox_3d_mesh->get_rid(), node_3d->get_world_3d()->get_scenario());
+ sbox_3d_instance_ofs = RS::get_singleton()->instance_create2(sbox_3d_mesh->get_rid(), node_3d->get_world_3d()->get_scenario());
+ RS::get_singleton()->instance_geometry_set_cast_shadows_setting(sbox_3d_instance, RS::SHADOW_CASTING_SETTING_OFF);
+ RS::get_singleton()->instance_geometry_set_cast_shadows_setting(sbox_3d_instance_ofs, RS::SHADOW_CASTING_SETTING_OFF);
+ RS::get_singleton()->instance_geometry_set_flag(sbox_3d_instance, RS::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true);
+ RS::get_singleton()->instance_geometry_set_flag(sbox_3d_instance, RS::INSTANCE_FLAG_USE_BAKED_LIGHT, false);
+ RS::get_singleton()->instance_geometry_set_flag(sbox_3d_instance_ofs, RS::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true);
+ RS::get_singleton()->instance_geometry_set_flag(sbox_3d_instance_ofs, RS::INSTANCE_FLAG_USE_BAKED_LIGHT, false);
+
+ sbox_3d_instance_xray = RS::get_singleton()->instance_create2(sbox_3d_mesh_xray->get_rid(), node_3d->get_world_3d()->get_scenario());
+ sbox_3d_instance_xray_ofs = RS::get_singleton()->instance_create2(sbox_3d_mesh_xray->get_rid(), node_3d->get_world_3d()->get_scenario());
+ RS::get_singleton()->instance_geometry_set_cast_shadows_setting(sbox_3d_instance_xray, RS::SHADOW_CASTING_SETTING_OFF);
+ RS::get_singleton()->instance_geometry_set_cast_shadows_setting(sbox_3d_instance_xray_ofs, RS::SHADOW_CASTING_SETTING_OFF);
+ RS::get_singleton()->instance_geometry_set_flag(sbox_3d_instance_xray, RS::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true);
+ RS::get_singleton()->instance_geometry_set_flag(sbox_3d_instance_xray, RS::INSTANCE_FLAG_USE_BAKED_LIGHT, false);
+ RS::get_singleton()->instance_geometry_set_flag(sbox_3d_instance_xray_ofs, RS::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true);
+ RS::get_singleton()->instance_geometry_set_flag(sbox_3d_instance_xray_ofs, RS::INSTANCE_FLAG_USE_BAKED_LIGHT, false);
+ }
+#endif // _3D_DISABLED
+ }
+
+ has_selection = selected_node;
+ _queue_selection_update();
+}
+
+void RuntimeNodeSelect::_queue_selection_update() {
+ if (has_selection && selection_visible) {
+ if (SceneTree::get_singleton()->is_suspended()) {
+ _update_selection();
+ } else {
+ selection_update_queued = true;
+ }
+ }
+}
+
+void RuntimeNodeSelect::_update_selection() {
+ if (has_selection && (!selected_node || !selected_node->is_inside_tree())) {
+ _clear_selection();
+ return;
+ }
+
+ CanvasItem *ci = Object::cast_to<CanvasItem>(selected_node);
+ if (ci) {
+ Window *root = SceneTree::get_singleton()->get_root();
+ Transform2D xform;
+ if (root->is_canvas_transform_override_enabled() && !ci->get_canvas_layer_node()) {
+ RS::get_singleton()->canvas_item_set_transform(sbox_2d_ci, (root->get_canvas_transform_override()));
+ xform = ci->get_global_transform();
+ } else {
+ RS::get_singleton()->canvas_item_set_transform(sbox_2d_ci, Transform2D());
+ xform = ci->get_global_transform_with_canvas();
+ }
+
+ // Fallback.
+ Rect2 rect = Rect2(Vector2(), Vector2(10, 10));
+
+ if (ci->_edit_use_rect()) {
+ rect = ci->_edit_get_rect();
+ } else {
+ CollisionShape2D *collision_shape = Object::cast_to<CollisionShape2D>(ci);
+ if (collision_shape) {
+ Ref<Shape2D> shape = collision_shape->get_shape();
+ if (shape.is_valid()) {
+ rect = shape->get_rect();
+ }
+ }
+ }
+
+ RS::get_singleton()->canvas_item_set_visible(sbox_2d_ci, selection_visible);
+
+ if (xform == sbox_2d_xform && rect == sbox_2d_rect) {
+ return; // Nothing changed.
+ }
+ sbox_2d_xform = xform;
+ sbox_2d_rect = rect;
+
+ RS::get_singleton()->canvas_item_clear(sbox_2d_ci);
+
+ const Vector2 endpoints[4] = {
+ xform.xform(rect.position),
+ xform.xform(rect.position + Vector2(rect.size.x, 0)),
+ xform.xform(rect.position + rect.size),
+ xform.xform(rect.position + Vector2(0, rect.size.y))
+ };
+
+ const Color selection_color_2d = Color(1, 0.6, 0.4, 0.7);
+ for (int i = 0; i < 4; i++) {
+ RS::get_singleton()->canvas_item_add_line(sbox_2d_ci, endpoints[i], endpoints[(i + 1) % 4], selection_color_2d, Math::round(2.f));
+ }
+ } else {
+#ifndef _3D_DISABLED
+ Node3D *node_3d = Object::cast_to<Node3D>(selected_node);
+
+ // Fallback.
+ AABB bounds(Vector3(-0.5, -0.5, -0.5), Vector3(1, 1, 1));
+
+ VisualInstance3D *visual_instance = Object::cast_to<VisualInstance3D>(node_3d);
+ if (visual_instance) {
+ bounds = visual_instance->get_aabb();
+ } else {
+ CollisionShape3D *collision_shape = Object::cast_to<CollisionShape3D>(node_3d);
+ if (collision_shape) {
+ Ref<Shape3D> shape = collision_shape->get_shape();
+ if (shape.is_valid()) {
+ bounds = shape->get_debug_mesh()->get_aabb();
+ }
+ }
+ }
+
+ RS::get_singleton()->instance_set_visible(sbox_3d_instance, selection_visible);
+ RS::get_singleton()->instance_set_visible(sbox_3d_instance_ofs, selection_visible);
+ RS::get_singleton()->instance_set_visible(sbox_3d_instance_xray, selection_visible);
+ RS::get_singleton()->instance_set_visible(sbox_3d_instance_xray_ofs, selection_visible);
+
+ Transform3D xform_to_top_level_parent_space = node_3d->get_global_transform().affine_inverse() * node_3d->get_global_transform();
+ bounds = xform_to_top_level_parent_space.xform(bounds);
+ Transform3D t = node_3d->get_global_transform();
+
+ if (t == sbox_3d_xform && bounds == sbox_3d_bounds) {
+ return; // Nothing changed.
+ }
+ sbox_3d_xform = t;
+ sbox_3d_bounds = bounds;
+
+ Transform3D t_offset = t;
+
+ // Apply AABB scaling before item's global transform.
+ {
+ const Vector3 offset(0.005, 0.005, 0.005);
+ Basis aabb_s;
+ aabb_s.scale(bounds.size + offset);
+ t.translate_local(bounds.position - offset / 2);
+ t.basis = t.basis * aabb_s;
+ }
+ {
+ const Vector3 offset(0.01, 0.01, 0.01);
+ Basis aabb_s;
+ aabb_s.scale(bounds.size + offset);
+ t_offset.translate_local(bounds.position - offset / 2);
+ t_offset.basis = t_offset.basis * aabb_s;
+ }
+
+ RS::get_singleton()->instance_set_transform(sbox_3d_instance, t);
+ RS::get_singleton()->instance_set_transform(sbox_3d_instance_ofs, t_offset);
+ RS::get_singleton()->instance_set_transform(sbox_3d_instance_xray, t);
+ RS::get_singleton()->instance_set_transform(sbox_3d_instance_xray_ofs, t_offset);
+#endif // _3D_DISABLED
+ }
+}
+
+void RuntimeNodeSelect::_clear_selection() {
+ selected_node = nullptr;
+ has_selection = false;
+
+ if (sbox_2d_canvas.is_valid()) {
+ RS::get_singleton()->canvas_item_clear(sbox_2d_ci);
+ }
+
+#ifndef _3D_DISABLED
+ if (sbox_3d_instance.is_valid()) {
+ RS::get_singleton()->free(sbox_3d_instance);
+ RS::get_singleton()->free(sbox_3d_instance_ofs);
+ RS::get_singleton()->free(sbox_3d_instance_xray);
+ RS::get_singleton()->free(sbox_3d_instance_xray_ofs);
+ }
+#endif // _3D_DISABLED
+}
+
+void RuntimeNodeSelect::_set_selection_visible(bool p_visible) {
+ selection_visible = p_visible;
+
+ if (has_selection) {
+ _update_selection();
+ }
+}
+
+// Copied and trimmed from the CanvasItemEditor implementation.
+void RuntimeNodeSelect::_find_canvas_items_at_pos(const Point2 &p_pos, Node *p_node, Vector<SelectResult> &r_items, const Transform2D &p_parent_xform, const Transform2D &p_canvas_xform) {
+ if (!p_node || Object::cast_to<Viewport>(p_node)) {
+ return;
+ }
+
+ // In the original CanvasItemEditor, this value would be fetched from the "editors/polygon_editor/point_grab_radius" editor property,
+ // but since this is not accessible from here, we will just use the default value.
+ const real_t grab_distance = 8;
+ CanvasItem *ci = Object::cast_to<CanvasItem>(p_node);
+
+ for (int i = p_node->get_child_count() - 1; i >= 0; i--) {
+ if (ci) {
+ if (!ci->is_set_as_top_level()) {
+ _find_canvas_items_at_pos(p_pos, p_node->get_child(i), r_items, p_parent_xform * ci->get_transform(), p_canvas_xform);
+ } else {
+ _find_canvas_items_at_pos(p_pos, p_node->get_child(i), r_items, ci->get_transform(), p_canvas_xform);
+ }
+ } else {
+ CanvasLayer *cl = Object::cast_to<CanvasLayer>(p_node);
+ _find_canvas_items_at_pos(p_pos, p_node->get_child(i), r_items, Transform2D(), cl ? cl->get_transform() : p_canvas_xform);
+ }
+ }
+
+ if (ci && ci->is_visible_in_tree()) {
+ Transform2D xform = p_canvas_xform;
+ if (!ci->is_set_as_top_level()) {
+ xform *= p_parent_xform;
+ }
+
+ Vector2 pos;
+ // Cameras (overridden or not) don't affect `CanvasLayer`s.
+ if (!ci->get_canvas_layer_node()) {
+ Window *root = SceneTree::get_singleton()->get_root();
+ pos = (root->is_canvas_transform_override_enabled() ? root->get_canvas_transform_override() : root->get_canvas_transform()).affine_inverse().xform(p_pos);
+ } else {
+ pos = p_pos;
+ }
+
+ xform = (xform * ci->get_transform()).affine_inverse();
+ const real_t local_grab_distance = xform.basis_xform(Vector2(grab_distance, 0)).length() / view_2d_zoom;
+ if (ci->_edit_is_selected_on_click(xform.xform(pos), local_grab_distance)) {
+ SelectResult res;
+ res.item = ci;
+ res.order = ci->get_effective_z_index() + ci->get_canvas_layer();
+ r_items.push_back(res);
+
+ // If it's a shape, get the collision object it's from.
+ // FIXME: If the collision object has multiple shapes, only the topmost will be above it in the list.
+ if (Object::cast_to<CollisionShape2D>(ci) || Object::cast_to<CollisionPolygon2D>(ci)) {
+ CollisionObject2D *collision_object = Object::cast_to<CollisionObject2D>(ci->get_parent());
+ if (collision_object) {
+ SelectResult res_col;
+ res_col.item = ci->get_parent();
+ res_col.order = collision_object->get_z_index() + ci->get_canvas_layer();
+ r_items.push_back(res_col);
+ }
+ }
+ }
+ }
+}
+
+void RuntimeNodeSelect::_pan_callback(Vector2 p_scroll_vec, Ref<InputEvent> p_event) {
+ view_2d_offset.x -= p_scroll_vec.x / view_2d_zoom;
+ view_2d_offset.y -= p_scroll_vec.y / view_2d_zoom;
+
+ _update_view_2d();
+}
+
+// A very shallow copy of the same function inside CanvasItemEditor.
+void RuntimeNodeSelect::_zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref<InputEvent> p_event) {
+ real_t prev_zoom = view_2d_zoom;
+ view_2d_zoom = CLAMP(view_2d_zoom * p_zoom_factor, VIEW_2D_MIN_ZOOM, VIEW_2D_MAX_ZOOM);
+
+ Vector2 pos = SceneTree::get_singleton()->get_root()->get_screen_transform().affine_inverse().xform(p_origin);
+ view_2d_offset += pos / prev_zoom - pos / view_2d_zoom;
+
+ // We want to align in-scene pixels to screen pixels, this prevents blurry rendering
+ // of small details (texts, lines).
+ // This correction adds a jitter movement when zooming, so we correct only when the
+ // zoom factor is an integer. (in the other cases, all pixels won't be aligned anyway)
+ const real_t closest_zoom_factor = Math::round(view_2d_zoom);
+ if (Math::is_zero_approx(view_2d_zoom - closest_zoom_factor)) {
+ // Make sure scene pixel at view_offset is aligned on a screen pixel.
+ Vector2 view_offset_int = view_2d_offset.floor();
+ Vector2 view_offset_frac = view_2d_offset - view_offset_int;
+ view_2d_offset = view_offset_int + (view_offset_frac * closest_zoom_factor).round() / closest_zoom_factor;
+ }
+
+ _update_view_2d();
+}
+
+void RuntimeNodeSelect::_reset_camera_2d() {
+ view_2d_offset = -SceneTree::get_singleton()->get_root()->get_canvas_transform().get_origin();
+ view_2d_zoom = 1;
+
+ _update_view_2d();
+}
+
+void RuntimeNodeSelect::_update_view_2d() {
+ Transform2D transform = Transform2D();
+ transform.scale_basis(Size2(view_2d_zoom, view_2d_zoom));
+ transform.columns[2] = -view_2d_offset * view_2d_zoom;
+
+ SceneTree::get_singleton()->get_root()->set_canvas_transform_override(transform);
+
+ _queue_selection_update();
+}
+
+#ifndef _3D_DISABLED
+void RuntimeNodeSelect::_find_3d_items_at_pos(const Point2 &p_pos, Vector<SelectResult> &r_items) {
+ Window *root = SceneTree::get_singleton()->get_root();
+ Camera3D *camera = root->get_viewport()->get_camera_3d();
+ if (!camera) {
+ return;
+ }
+
+ Vector3 ray, pos, to;
+ if (root->get_viewport()->is_camera_3d_override_enabled()) {
+ Viewport *vp = root->get_viewport();
+ ray = vp->camera_3d_override_project_ray_normal(p_pos);
+ pos = vp->camera_3d_override_project_ray_origin(p_pos);
+ to = pos + ray * vp->get_camera_3d_override_properties()["z_far"];
+ } else {
+ ray = camera->project_ray_normal(p_pos);
+ pos = camera->project_ray_origin(p_pos);
+ to = pos + ray * camera->get_far();
+ }
+
+ // Start with physical objects.
+ PhysicsDirectSpaceState3D *ss = root->get_world_3d()->get_direct_space_state();
+ PhysicsDirectSpaceState3D::RayResult result;
+ HashSet<RID> excluded;
+ PhysicsDirectSpaceState3D::RayParameters ray_params;
+ ray_params.from = pos;
+ ray_params.to = to;
+ ray_params.collide_with_areas = true;
+ while (true) {
+ ray_params.exclude = excluded;
+ if (ss->intersect_ray(ray_params, result)) {
+ SelectResult res;
+ res.item = Object::cast_to<Node>(result.collider);
+ res.order = -pos.distance_to(Object::cast_to<Node3D>(res.item)->get_global_transform().xform(result.position));
+
+ // Fetch collision shapes.
+ CollisionObject3D *collision = Object::cast_to<CollisionObject3D>(result.collider);
+ if (collision) {
+ List<uint32_t> owners;
+ collision->get_shape_owners(&owners);
+ for (const uint32_t &I : owners) {
+ SelectResult res_shape;
+ res_shape.item = Object::cast_to<Node>(collision->shape_owner_get_owner(I));
+ res_shape.order = res.order;
+ r_items.push_back(res_shape);
+ }
+ }
+
+ r_items.push_back(res);
+
+ excluded.insert(result.rid);
+ } else {
+ break;
+ }
+ }
+
+ // Then go for the meshes.
+ Vector<ObjectID> items = RS::get_singleton()->instances_cull_ray(pos, to, root->get_world_3d()->get_scenario());
+ for (int i = 0; i < items.size(); i++) {
+ Object *obj = ObjectDB::get_instance(items[i]);
+ GeometryInstance3D *geo_instance = nullptr;
+ Ref<TriangleMesh> mesh_collision;
+
+ MeshInstance3D *mesh_instance = Object::cast_to<MeshInstance3D>(obj);
+ if (mesh_instance) {
+ if (mesh_instance->get_mesh().is_valid()) {
+ geo_instance = mesh_instance;
+ mesh_collision = mesh_instance->get_mesh()->generate_triangle_mesh();
+ }
+ } else {
+ Label3D *label = Object::cast_to<Label3D>(obj);
+ if (label) {
+ geo_instance = label;
+ mesh_collision = label->generate_triangle_mesh();
+ } else {
+ Sprite3D *sprite = Object::cast_to<Sprite3D>(obj);
+ if (sprite) {
+ geo_instance = sprite;
+ mesh_collision = sprite->generate_triangle_mesh();
+ }
+ }
+ }
+
+ if (mesh_collision.is_valid()) {
+ Transform3D gt = geo_instance->get_global_transform();
+ Transform3D ai = gt.affine_inverse();
+ Vector3 point, normal;
+ if (mesh_collision->intersect_ray(ai.xform(pos), ai.basis.xform(ray).normalized(), point, normal)) {
+ SelectResult res;
+ res.item = Object::cast_to<Node>(obj);
+ res.order = -pos.distance_to(gt.xform(point));
+ r_items.push_back(res);
+
+ continue;
+ }
+ }
+
+ items.remove_at(i);
+ i--;
+ }
+}
+
+bool RuntimeNodeSelect::_handle_3d_input(const Ref<InputEvent> &p_event) {
+ Ref<InputEventMouseButton> b = p_event;
+
+ if (b.is_valid()) {
+ const real_t zoom_factor = 1.08 * b->get_factor();
+ switch (b->get_button_index()) {
+ case MouseButton::WHEEL_UP: {
+ if (!camera_freelook) {
+ _cursor_scale_distance(1.0 / zoom_factor);
+ }
+
+ return true;
+ } break;
+ case MouseButton::WHEEL_DOWN: {
+ if (!camera_freelook) {
+ _cursor_scale_distance(zoom_factor);
+ }
+
+ return true;
+ } break;
+ case MouseButton::RIGHT: {
+ _set_camera_freelook_enabled(b->is_pressed());
+ return true;
+ } break;
+ default: {
+ }
+ }
+ }
+
+ Ref<InputEventMouseMotion> m = p_event;
+
+ if (m.is_valid()) {
+ if (camera_freelook) {
+ _cursor_look(m);
+ } else if (m->get_button_mask().has_flag(MouseButtonMask::MIDDLE)) {
+ if (m->is_shift_pressed()) {
+ _cursor_pan(m);
+ } else {
+ _cursor_orbit(m);
+ }
+ }
+
+ return true;
+ }
+
+ Ref<InputEventKey> k = p_event;
+
+ if (k.is_valid()) {
+ if (k->get_physical_keycode() == Key::ESCAPE) {
+ _set_camera_freelook_enabled(false);
+ return true;
+ } else if (k->is_ctrl_pressed()) {
+ switch (k->get_physical_keycode()) {
+ case Key::EQUAL: {
+ cursor.fov_scale = CLAMP(cursor.fov_scale - 0.05, CAMERA_MIN_FOV_SCALE, CAMERA_MAX_FOV_SCALE);
+ SceneTree::get_singleton()->get_root()->set_camera_3d_override_perspective(CAMERA_BASE_FOV * cursor.fov_scale, CAMERA_ZNEAR, CAMERA_ZFAR);
+
+ return true;
+ } break;
+ case Key::MINUS: {
+ cursor.fov_scale = CLAMP(cursor.fov_scale + 0.05, CAMERA_MIN_FOV_SCALE, CAMERA_MAX_FOV_SCALE);
+ SceneTree::get_singleton()->get_root()->set_camera_3d_override_perspective(CAMERA_BASE_FOV * cursor.fov_scale, CAMERA_ZNEAR, CAMERA_ZFAR);
+
+ return true;
+ } break;
+ case Key::KEY_0: {
+ cursor.fov_scale = 1;
+ SceneTree::get_singleton()->get_root()->set_camera_3d_override_perspective(CAMERA_BASE_FOV, CAMERA_ZNEAR, CAMERA_ZFAR);
+
+ return true;
+ } break;
+ default: {
+ }
+ }
+ }
+ }
+
+ // TODO: Handle magnify and pan input gestures.
+
+ return false;
+}
+
+void RuntimeNodeSelect::_set_camera_freelook_enabled(bool p_enabled) {
+ camera_freelook = p_enabled;
+
+ if (p_enabled) {
+ // Make sure eye_pos is synced, because freelook referential is eye pos rather than orbit pos
+ Vector3 forward = _get_cursor_transform().basis.xform(Vector3(0, 0, -1));
+ cursor.eye_pos = cursor.pos - cursor.distance * forward;
+
+ previous_mouse_position = SceneTree::get_singleton()->get_root()->get_mouse_position();
+
+ // Hide mouse like in an FPS (warping doesn't work).
+ Input::get_singleton()->set_mouse_mode_override(Input::MOUSE_MODE_CAPTURED);
+
+ } else {
+ // Restore mouse.
+ Input::get_singleton()->set_mouse_mode_override(Input::MOUSE_MODE_VISIBLE);
+
+ // Restore the previous mouse position when leaving freelook mode.
+ // This is done because leaving `Input.MOUSE_MODE_CAPTURED` will center the cursor
+ // due to OS limitations.
+ Input::get_singleton()->warp_mouse(previous_mouse_position);
+ }
+}
+
+void RuntimeNodeSelect::_cursor_scale_distance(real_t p_scale) {
+ real_t min_distance = MAX(CAMERA_ZNEAR * 4, VIEW_3D_MIN_ZOOM);
+ real_t max_distance = MIN(CAMERA_ZFAR / 4, VIEW_3D_MAX_ZOOM);
+ cursor.distance = CLAMP(cursor.distance * p_scale, min_distance, max_distance);
+
+ SceneTree::get_singleton()->get_root()->set_camera_3d_override_transform(_get_cursor_transform());
+}
+
+void RuntimeNodeSelect::_cursor_look(Ref<InputEventWithModifiers> p_event) {
+ Window *root = SceneTree::get_singleton()->get_root();
+ const Vector2 relative = Input::get_singleton()->warp_mouse_motion(p_event, Rect2(Vector2(), root->get_size()));
+ const Transform3D prev_camera_transform = _get_cursor_transform();
+
+ cursor.x_rot += relative.y * RADS_PER_PIXEL;
+ // Clamp the Y rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
+ cursor.x_rot = CLAMP(cursor.x_rot, -1.57, 1.57);
+
+ cursor.y_rot += relative.x * RADS_PER_PIXEL;
+
+ // Look is like the opposite of Orbit: the focus point rotates around the camera.
+ Transform3D camera_transform = _get_cursor_transform();
+ Vector3 pos = camera_transform.xform(Vector3(0, 0, 0));
+ Vector3 prev_pos = prev_camera_transform.xform(Vector3(0, 0, 0));
+ Vector3 diff = prev_pos - pos;
+ cursor.pos += diff;
+
+ SceneTree::get_singleton()->get_root()->set_camera_3d_override_transform(_get_cursor_transform());
+}
+
+void RuntimeNodeSelect::_cursor_pan(Ref<InputEventWithModifiers> p_event) {
+ Window *root = SceneTree::get_singleton()->get_root();
+ // Reduce all sides of the area by 1, so warping works when windows are maximized/fullscreen.
+ const Vector2 relative = Input::get_singleton()->warp_mouse_motion(p_event, Rect2(Vector2(1, 1), root->get_size() - Vector2(2, 2)));
+ const real_t pan_speed = 1 / 150.0;
+
+ Transform3D camera_transform;
+ camera_transform.translate_local(cursor.pos);
+ camera_transform.basis.rotate(Vector3(1, 0, 0), -cursor.x_rot);
+ camera_transform.basis.rotate(Vector3(0, 1, 0), -cursor.y_rot);
+
+ Vector3 translation(1 * -relative.x * pan_speed, relative.y * pan_speed, 0);
+ translation *= cursor.distance / 4;
+ camera_transform.translate_local(translation);
+ cursor.pos = camera_transform.origin;
+
+ SceneTree::get_singleton()->get_root()->set_camera_3d_override_transform(_get_cursor_transform());
+}
+
+void RuntimeNodeSelect::_cursor_orbit(Ref<InputEventWithModifiers> p_event) {
+ Window *root = SceneTree::get_singleton()->get_root();
+ // Reduce all sides of the area by 1, so warping works when windows are maximized/fullscreen.
+ const Vector2 relative = Input::get_singleton()->warp_mouse_motion(p_event, Rect2(Vector2(1, 1), root->get_size() - Vector2(2, 2)));
+
+ cursor.x_rot += relative.y * RADS_PER_PIXEL;
+ // Clamp the Y rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
+ cursor.x_rot = CLAMP(cursor.x_rot, -1.57, 1.57);
+
+ cursor.y_rot += relative.x * RADS_PER_PIXEL;
+
+ SceneTree::get_singleton()->get_root()->set_camera_3d_override_transform(_get_cursor_transform());
+}
+
+Transform3D RuntimeNodeSelect::_get_cursor_transform() {
+ Transform3D camera_transform;
+ camera_transform.translate_local(cursor.pos);
+ camera_transform.basis.rotate(Vector3(1, 0, 0), -cursor.x_rot);
+ camera_transform.basis.rotate(Vector3(0, 1, 0), -cursor.y_rot);
+ camera_transform.translate_local(0, 0, cursor.distance);
+
+ return camera_transform;
+}
+
+void RuntimeNodeSelect::_reset_camera_3d() {
+ camera_first_override = true;
+
+ Window *root = SceneTree::get_singleton()->get_root();
+ Camera3D *camera = root->get_camera_3d();
+ if (!camera) {
+ return;
+ }
+
+ cursor = Cursor();
+ Transform3D transform = camera->get_global_transform();
+ transform.translate_local(0, 0, -cursor.distance);
+ cursor.pos = transform.origin;
+
+ cursor.x_rot = -camera->get_global_rotation().x;
+ cursor.y_rot = -camera->get_global_rotation().y;
+
+ cursor.fov_scale = CLAMP(camera->get_fov() / CAMERA_BASE_FOV, CAMERA_MIN_FOV_SCALE, CAMERA_MAX_FOV_SCALE);
+
+ SceneTree::get_singleton()->get_root()->set_camera_3d_override_transform(_get_cursor_transform());
+ SceneTree::get_singleton()->get_root()->set_camera_3d_override_perspective(CAMERA_BASE_FOV * cursor.fov_scale, CAMERA_ZNEAR, CAMERA_ZFAR);
+}
+#endif // _3D_DISABLED
#endif
diff --git a/scene/debugger/scene_debugger.h b/scene/debugger/scene_debugger.h
index be2642a2ae..f9dd6161aa 100644
--- a/scene/debugger/scene_debugger.h
+++ b/scene/debugger/scene_debugger.h
@@ -31,19 +31,21 @@
#ifndef SCENE_DEBUGGER_H
#define SCENE_DEBUGGER_H
-#include "core/object/class_db.h"
+#include "core/input/shortcut.h"
#include "core/object/ref_counted.h"
#include "core/string/ustring.h"
#include "core/templates/pair.h"
#include "core/variant/array.h"
+#include "scene/gui/view_panner.h"
+#include "scene/resources/mesh.h"
+class PopupMenu;
class Script;
class Node;
class SceneDebugger {
-public:
private:
- static SceneDebugger *singleton;
+ inline static SceneDebugger *singleton = nullptr;
SceneDebugger();
@@ -59,6 +61,7 @@ private:
static void _set_node_owner_recursive(Node *p_node, Node *p_owner);
static void _set_object_property(ObjectID p_id, const String &p_property, const Variant &p_value);
static void _send_object_id(ObjectID p_id, int p_max_size = 1 << 20);
+ static void _next_frame();
public:
static Error parse_message(void *p_user, const String &p_msg, const Array &p_args, bool &r_captured);
@@ -160,11 +163,161 @@ private:
live_edit_root = NodePath("/root");
}
- static LiveEditor *singleton;
+ inline static LiveEditor *singleton = nullptr;
public:
static LiveEditor *get_singleton();
};
+
+class RuntimeNodeSelect : public Object {
+ GDCLASS(RuntimeNodeSelect, Object);
+
+public:
+ enum NodeType {
+ NODE_TYPE_NONE,
+ NODE_TYPE_2D,
+ NODE_TYPE_3D,
+ NODE_TYPE_MAX
+ };
+
+ enum SelectMode {
+ SELECT_MODE_SINGLE,
+ SELECT_MODE_LIST,
+ SELECT_MODE_MAX
+ };
+
+private:
+ friend class SceneDebugger;
+
+ struct SelectResult {
+ Node *item = nullptr;
+ real_t order = 0;
+ _FORCE_INLINE_ bool operator<(const SelectResult &p_rr) const { return p_rr.order < order; }
+ };
+
+ bool has_selection = false;
+ Node *selected_node = nullptr;
+ PopupMenu *selection_list = nullptr;
+ bool selection_visible = true;
+ bool selection_update_queued = false;
+
+ bool camera_override = false;
+
+ // Values taken from EditorZoomWidget.
+ const float VIEW_2D_MIN_ZOOM = 1.0 / 128;
+ const float VIEW_2D_MAX_ZOOM = 128;
+
+ Ref<ViewPanner> panner;
+ Vector2 view_2d_offset;
+ real_t view_2d_zoom = 1.0;
+
+ RID sbox_2d_canvas;
+ RID sbox_2d_ci;
+ Transform2D sbox_2d_xform;
+ Rect2 sbox_2d_rect;
+
+#ifndef _3D_DISABLED
+ struct Cursor {
+ Vector3 pos;
+ real_t x_rot, y_rot, distance, fov_scale;
+ Vector3 eye_pos; // Used in freelook mode.
+
+ Cursor() {
+ // These rotations place the camera in +X +Y +Z, aka south east, facing north west.
+ x_rot = 0.5;
+ y_rot = -0.5;
+ distance = 4;
+ fov_scale = 1.0;
+ }
+ };
+ Cursor cursor;
+
+ // Values taken from Node3DEditor.
+ const float VIEW_3D_MIN_ZOOM = 0.01;
+#ifdef REAL_T_IS_DOUBLE
+ const double VIEW_3D_MAX_ZOOM = 1'000'000'000'000;
+#else
+ const float VIEW_3D_MAX_ZOOM = 10'000;
+#endif
+ const float CAMERA_ZNEAR = 0.05;
+ const float CAMERA_ZFAR = 4'000;
+
+ const float CAMERA_BASE_FOV = 75;
+ const float CAMERA_MIN_FOV_SCALE = 0.1;
+ const float CAMERA_MAX_FOV_SCALE = 2.5;
+
+ const float FREELOOK_BASE_SPEED = 4;
+ const float RADS_PER_PIXEL = 0.004;
+
+ bool camera_first_override = true;
+ bool camera_freelook = false;
+
+ Vector2 previous_mouse_position;
+
+ Ref<ArrayMesh> sbox_3d_mesh;
+ Ref<ArrayMesh> sbox_3d_mesh_xray;
+ RID sbox_3d_instance;
+ RID sbox_3d_instance_ofs;
+ RID sbox_3d_instance_xray;
+ RID sbox_3d_instance_xray_ofs;
+ Transform3D sbox_3d_xform;
+ AABB sbox_3d_bounds;
+#endif
+
+ Point2 selection_position = Point2(INFINITY, INFINITY);
+ bool list_shortcut_pressed = false;
+
+ NodeType node_select_type = NODE_TYPE_2D;
+ SelectMode node_select_mode = SELECT_MODE_SINGLE;
+
+ void _setup();
+
+ void _node_set_type(NodeType p_type);
+ void _select_set_mode(SelectMode p_mode);
+
+ void _set_camera_override_enabled(bool p_enabled);
+
+ void _root_window_input(const Ref<InputEvent> &p_event);
+ void _items_popup_index_pressed(int p_index, PopupMenu *p_popup);
+ void _update_input_state();
+
+ void _process_frame();
+ void _physics_frame();
+
+ void _click_point();
+ void _select_node(Node *p_node);
+ void _queue_selection_update();
+ void _update_selection();
+ void _clear_selection();
+ void _set_selection_visible(bool p_visible);
+
+ void _find_canvas_items_at_pos(const Point2 &p_pos, Node *p_node, Vector<SelectResult> &r_items, const Transform2D &p_parent_xform = Transform2D(), const Transform2D &p_canvas_xform = Transform2D());
+ void _pan_callback(Vector2 p_scroll_vec, Ref<InputEvent> p_event);
+ void _zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref<InputEvent> p_event);
+ void _reset_camera_2d();
+ void _update_view_2d();
+
+#ifndef _3D_DISABLED
+ void _find_3d_items_at_pos(const Point2 &p_pos, Vector<SelectResult> &r_items);
+ bool _handle_3d_input(const Ref<InputEvent> &p_event);
+ void _set_camera_freelook_enabled(bool p_enabled);
+ void _cursor_scale_distance(real_t p_scale);
+ void _cursor_look(Ref<InputEventWithModifiers> p_event);
+ void _cursor_pan(Ref<InputEventWithModifiers> p_event);
+ void _cursor_orbit(Ref<InputEventWithModifiers> p_event);
+ Transform3D _get_cursor_transform();
+ void _reset_camera_3d();
+#endif
+
+ RuntimeNodeSelect() { singleton = this; }
+
+ inline static RuntimeNodeSelect *singleton = nullptr;
+
+public:
+ static RuntimeNodeSelect *get_singleton();
+
+ ~RuntimeNodeSelect();
+};
#endif
#endif // SCENE_DEBUGGER_H
diff --git a/scene/gui/video_stream_player.cpp b/scene/gui/video_stream_player.cpp
index 0b521f926d..878bceccbc 100644
--- a/scene/gui/video_stream_player.cpp
+++ b/scene/gui/video_stream_player.cpp
@@ -178,6 +178,7 @@ void VideoStreamPlayer::_notification(int p_notification) {
draw_texture_rect(texture, Rect2(Point2(), s), false);
} break;
+ case NOTIFICATION_SUSPENDED:
case NOTIFICATION_PAUSED: {
if (is_playing() && !is_paused()) {
paused_from_tree = true;
@@ -189,6 +190,13 @@ void VideoStreamPlayer::_notification(int p_notification) {
}
} break;
+ case NOTIFICATION_UNSUSPENDED: {
+ if (get_tree()->is_paused()) {
+ break;
+ }
+ [[fallthrough]];
+ }
+
case NOTIFICATION_UNPAUSED: {
if (paused_from_tree) {
paused_from_tree = false;
diff --git a/scene/main/node.cpp b/scene/main/node.cpp
index d921cc5b67..8dc7b4a87c 100644
--- a/scene/main/node.cpp
+++ b/scene/main/node.cpp
@@ -184,6 +184,7 @@ void Node::_notification(int p_notification) {
}
} break;
+ case NOTIFICATION_SUSPENDED:
case NOTIFICATION_PAUSED: {
if (is_physics_interpolated_and_enabled() && is_inside_tree()) {
reset_physics_interpolation();
@@ -695,6 +696,16 @@ void Node::_propagate_pause_notification(bool p_enable) {
data.blocked--;
}
+void Node::_propagate_suspend_notification(bool p_enable) {
+ notification(p_enable ? NOTIFICATION_SUSPENDED : NOTIFICATION_UNSUSPENDED);
+
+ data.blocked++;
+ for (KeyValue<StringName, Node *> &KV : data.children) {
+ KV.value->_propagate_suspend_notification(p_enable);
+ }
+ data.blocked--;
+}
+
Node::ProcessMode Node::get_process_mode() const {
return data.process_mode;
}
@@ -850,7 +861,7 @@ bool Node::can_process_notification(int p_what) const {
bool Node::can_process() const {
ERR_FAIL_COND_V(!is_inside_tree(), false);
- return _can_process(get_tree()->is_paused());
+ return !get_tree()->is_suspended() && _can_process(get_tree()->is_paused());
}
bool Node::_can_process(bool p_paused) const {
diff --git a/scene/main/node.h b/scene/main/node.h
index 799478fa35..e2f3ce9b78 100644
--- a/scene/main/node.h
+++ b/scene/main/node.h
@@ -300,6 +300,7 @@ private:
void _set_tree(SceneTree *p_tree);
void _propagate_pause_notification(bool p_enable);
+ void _propagate_suspend_notification(bool p_enable);
_FORCE_INLINE_ bool _can_process(bool p_paused) const;
_FORCE_INLINE_ bool _is_enabled() const;
@@ -439,6 +440,8 @@ public:
// Editor specific node notifications
NOTIFICATION_EDITOR_PRE_SAVE = 9001,
NOTIFICATION_EDITOR_POST_SAVE = 9002,
+ NOTIFICATION_SUSPENDED = 9003,
+ NOTIFICATION_UNSUSPENDED = 9004
};
/* NODE/TREE */
diff --git a/scene/main/scene_tree.cpp b/scene/main/scene_tree.cpp
index 71d91b970e..60cecfcfe7 100644
--- a/scene/main/scene_tree.cpp
+++ b/scene/main/scene_tree.cpp
@@ -954,11 +954,14 @@ Ref<ArrayMesh> SceneTree::get_debug_contact_mesh() {
void SceneTree::set_pause(bool p_enabled) {
ERR_FAIL_COND_MSG(!Thread::is_main_thread(), "Pause can only be set from the main thread.");
+ ERR_FAIL_COND_MSG(suspended, "Pause state cannot be modified while suspended.");
if (p_enabled == paused) {
return;
}
+
paused = p_enabled;
+
#ifndef _3D_DISABLED
PhysicsServer3D::get_singleton()->set_active(!p_enabled);
#endif // _3D_DISABLED
@@ -972,6 +975,30 @@ bool SceneTree::is_paused() const {
return paused;
}
+void SceneTree::set_suspend(bool p_enabled) {
+ ERR_FAIL_COND_MSG(!Thread::is_main_thread(), "Suspend can only be set from the main thread.");
+
+ if (p_enabled == suspended) {
+ return;
+ }
+
+ suspended = p_enabled;
+
+ Engine::get_singleton()->set_freeze_time_scale(p_enabled);
+
+#ifndef _3D_DISABLED
+ PhysicsServer3D::get_singleton()->set_active(!p_enabled && !paused);
+#endif // _3D_DISABLED
+ PhysicsServer2D::get_singleton()->set_active(!p_enabled && !paused);
+ if (get_root()) {
+ get_root()->_propagate_suspend_notification(p_enabled);
+ }
+}
+
+bool SceneTree::is_suspended() const {
+ return suspended;
+}
+
void SceneTree::_process_group(ProcessGroup *p_group, bool p_physics) {
// When reading this function, keep in mind that this code must work in a way where
// if any node is removed, this needs to continue working.
diff --git a/scene/main/scene_tree.h b/scene/main/scene_tree.h
index 7e44541105..291e4a5a0c 100644
--- a/scene/main/scene_tree.h
+++ b/scene/main/scene_tree.h
@@ -143,6 +143,7 @@ private:
bool debug_navigation_hint = false;
#endif
bool paused = false;
+ bool suspended = false;
HashMap<StringName, Group> group_map;
bool _quit = false;
@@ -343,6 +344,8 @@ public:
void set_pause(bool p_enabled);
bool is_paused() const;
+ void set_suspend(bool p_enabled);
+ bool is_suspended() const;
#ifdef DEBUG_ENABLED
void set_debug_collisions_hint(bool p_enabled);
diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp
index 54f66e8d4e..5a90eb8f3e 100644
--- a/scene/main/viewport.cpp
+++ b/scene/main/viewport.cpp
@@ -3123,7 +3123,7 @@ void Viewport::push_input(const Ref<InputEvent> &p_event, bool p_local_coords) {
ERR_FAIL_COND(!is_inside_tree());
ERR_FAIL_COND(p_event.is_null());
- if (disable_input) {
+ if (disable_input || disable_input_override) {
return;
}
@@ -3195,7 +3195,7 @@ void Viewport::push_unhandled_input(const Ref<InputEvent> &p_event, bool p_local
local_input_handled = false;
- if (disable_input || !_can_consume_input_events()) {
+ if (disable_input || disable_input_override || !_can_consume_input_events()) {
return;
}
@@ -3298,7 +3298,7 @@ void Viewport::set_disable_input(bool p_disable) {
if (p_disable == disable_input) {
return;
}
- if (p_disable) {
+ if (p_disable && !disable_input_override) {
_drop_mouse_focus();
_mouse_leave_viewport();
_gui_cancel_tooltip();
@@ -3311,6 +3311,19 @@ bool Viewport::is_input_disabled() const {
return disable_input;
}
+void Viewport::set_disable_input_override(bool p_disable) {
+ ERR_MAIN_THREAD_GUARD;
+ if (p_disable == disable_input_override) {
+ return;
+ }
+ if (p_disable && !disable_input) {
+ _drop_mouse_focus();
+ _mouse_leave_viewport();
+ _gui_cancel_tooltip();
+ }
+ disable_input_override = p_disable;
+}
+
Variant Viewport::gui_get_drag_data() const {
ERR_READ_THREAD_GUARD_V(Variant());
return get_section_root_viewport()->gui.drag_data;
@@ -4237,6 +4250,22 @@ void Viewport::set_camera_3d_override_orthogonal(real_t p_size, real_t p_z_near,
}
}
+HashMap<StringName, real_t> Viewport::get_camera_3d_override_properties() const {
+ HashMap<StringName, real_t> props;
+
+ props["size"] = 0;
+ props["fov"] = 0;
+ props["z_near"] = 0;
+ props["z_far"] = 0;
+ ERR_READ_THREAD_GUARD_V(props);
+
+ props["size"] = camera_3d_override.size;
+ props["fov"] = camera_3d_override.fov;
+ props["z_near"] = camera_3d_override.z_near;
+ props["z_far"] = camera_3d_override.z_far;
+ return props;
+}
+
void Viewport::set_disable_3d(bool p_disable) {
ERR_MAIN_THREAD_GUARD;
disable_3d = p_disable;
@@ -4270,6 +4299,54 @@ Transform3D Viewport::get_camera_3d_override_transform() const {
return Transform3D();
}
+Vector3 Viewport::camera_3d_override_project_ray_normal(const Point2 &p_pos) const {
+ ERR_READ_THREAD_GUARD_V(Vector3());
+ Vector3 ray = camera_3d_override_project_local_ray_normal(p_pos);
+ return camera_3d_override.transform.basis.xform(ray).normalized();
+}
+
+Vector3 Viewport::camera_3d_override_project_local_ray_normal(const Point2 &p_pos) const {
+ ERR_READ_THREAD_GUARD_V(Vector3());
+ Size2 viewport_size = get_camera_rect_size();
+ Vector2 cpos = get_camera_coords(p_pos);
+ Vector3 ray;
+
+ if (camera_3d_override.projection == Camera3DOverrideData::PROJECTION_ORTHOGONAL) {
+ ray = Vector3(0, 0, -1);
+ } else {
+ Projection cm;
+ cm.set_perspective(camera_3d_override.fov, get_visible_rect().size.aspect(), camera_3d_override.z_near, camera_3d_override.z_far, false);
+
+ Vector2 screen_he = cm.get_viewport_half_extents();
+ ray = Vector3(((cpos.x / viewport_size.width) * 2.0 - 1.0) * screen_he.x, ((1.0 - (cpos.y / viewport_size.height)) * 2.0 - 1.0) * screen_he.y, -camera_3d_override.z_near).normalized();
+ }
+
+ return ray;
+}
+
+Vector3 Viewport::camera_3d_override_project_ray_origin(const Point2 &p_pos) const {
+ ERR_READ_THREAD_GUARD_V(Vector3());
+ Size2 viewport_size = get_camera_rect_size();
+ Vector2 cpos = get_camera_coords(p_pos);
+ ERR_FAIL_COND_V(viewport_size.y == 0, Vector3());
+
+ if (camera_3d_override.projection == Camera3DOverrideData::PROJECTION_ORTHOGONAL) {
+ Vector2 pos = cpos / viewport_size;
+ real_t vsize, hsize;
+ hsize = camera_3d_override.size * viewport_size.aspect();
+ vsize = camera_3d_override.size;
+
+ Vector3 ray;
+ ray.x = pos.x * (hsize)-hsize / 2;
+ ray.y = (1.0 - pos.y) * (vsize)-vsize / 2;
+ ray.z = -camera_3d_override.z_near;
+ ray = camera_3d_override.transform.xform(ray);
+ return ray;
+ } else {
+ return camera_3d_override.transform.origin;
+ };
+}
+
Ref<World3D> Viewport::get_world_3d() const {
ERR_READ_THREAD_GUARD_V(Ref<World3D>());
return world_3d;
diff --git a/scene/main/viewport.h b/scene/main/viewport.h
index a18dc1f6f0..92691ccbec 100644
--- a/scene/main/viewport.h
+++ b/scene/main/viewport.h
@@ -401,6 +401,7 @@ private:
DefaultCanvasItemTextureRepeat default_canvas_item_texture_repeat = DEFAULT_CANVAS_ITEM_TEXTURE_REPEAT_DISABLED;
bool disable_input = false;
+ bool disable_input_override = false;
void _gui_call_input(Control *p_control, const Ref<InputEvent> &p_input);
void _gui_call_notification(Control *p_control, int p_what);
@@ -580,6 +581,8 @@ public:
void set_disable_input(bool p_disable);
bool is_input_disabled() const;
+ void set_disable_input_override(bool p_disable);
+
Vector2 get_mouse_position() const;
void warp_mouse(const Vector2 &p_position);
virtual void update_mouse_cursor_state();
@@ -770,6 +773,11 @@ public:
void set_camera_3d_override_perspective(real_t p_fovy_degrees, real_t p_z_near, real_t p_z_far);
void set_camera_3d_override_orthogonal(real_t p_size, real_t p_z_near, real_t p_z_far);
+ HashMap<StringName, real_t> get_camera_3d_override_properties() const;
+
+ Vector3 camera_3d_override_project_ray_normal(const Point2 &p_pos) const;
+ Vector3 camera_3d_override_project_ray_origin(const Point2 &p_pos) const;
+ Vector3 camera_3d_override_project_local_ray_normal(const Point2 &p_pos) const;
void set_disable_3d(bool p_disable);
bool is_3d_disabled() const;