瀏覽代碼

Add "Game" editor for better runtime debugging

Michael Alexsander 10 月之前
父節點
當前提交
16524a8a01
共有 45 個文件被更改,包括 2375 次插入271 次删除
  1. 8 0
      core/config/engine.cpp
  2. 5 0
      core/config/engine.h
  3. 107 1
      core/input/input.cpp
  4. 10 0
      core/input/input.h
  5. 4 1
      doc/classes/EditorFeatureProfile.xml
  6. 8 0
      editor/debugger/editor_debugger_node.cpp
  7. 3 5
      editor/debugger/editor_debugger_node.h
  8. 33 5
      editor/debugger/editor_debugger_tree.cpp
  9. 3 0
      editor/debugger/editor_debugger_tree.h
  10. 43 46
      editor/debugger/script_editor_debugger.cpp
  11. 4 0
      editor/editor_feature_profile.cpp
  12. 1 0
      editor/editor_feature_profile.h
  13. 1 0
      editor/editor_main_screen.h
  14. 10 2
      editor/editor_node.cpp
  15. 1 0
      editor/icons/2DNodes.svg
  16. 1 0
      editor/icons/Camera.svg
  17. 1 0
      editor/icons/Game.svg
  18. 1 0
      editor/icons/NextFrame.svg
  19. 0 43
      editor/plugins/canvas_item_editor_plugin.cpp
  20. 0 4
      editor/plugins/canvas_item_editor_plugin.h
  21. 478 0
      editor/plugins/game_view_plugin.cpp
  22. 152 0
      editor/plugins/game_view_plugin.h
  23. 26 70
      editor/plugins/node_3d_editor_plugin.cpp
  24. 4 6
      editor/plugins/node_3d_editor_plugin.h
  25. 6 0
      editor/themes/editor_theme_manager.cpp
  26. 7 1
      misc/extension_api_validation/4.3-stable.expected
  27. 1 0
      scene/2d/camera_2d.cpp
  28. 2 0
      scene/2d/gpu_particles_2d.cpp
  29. 8 0
      scene/2d/navigation_agent_2d.cpp
  30. 8 0
      scene/2d/navigation_obstacle_2d.cpp
  31. 1 0
      scene/2d/touch_screen_button.cpp
  32. 1 0
      scene/3d/camera_3d.cpp
  33. 2 0
      scene/3d/gpu_particles_3d.cpp
  34. 8 0
      scene/3d/navigation_agent_3d.cpp
  35. 8 0
      scene/3d/navigation_obstacle_3d.cpp
  36. 8 0
      scene/audio/audio_stream_player_internal.cpp
  37. 1113 79
      scene/debugger/scene_debugger.cpp
  38. 157 4
      scene/debugger/scene_debugger.h
  39. 8 0
      scene/gui/video_stream_player.cpp
  40. 12 1
      scene/main/node.cpp
  41. 3 0
      scene/main/node.h
  42. 27 0
      scene/main/scene_tree.cpp
  43. 3 0
      scene/main/scene_tree.h
  44. 80 3
      scene/main/viewport.cpp
  45. 8 0
      scene/main/viewport.h

+ 8 - 0
core/config/engine.cpp

@@ -116,6 +116,10 @@ void Engine::set_time_scale(double p_scale) {
 }
 }
 
 
 double Engine::get_time_scale() const {
 double Engine::get_time_scale() const {
+	return freeze_time_scale ? 0 : _time_scale;
+}
+
+double Engine::get_unfrozen_time_scale() const {
 	return _time_scale;
 	return _time_scale;
 }
 }
 
 
@@ -404,6 +408,10 @@ bool Engine::notify_frame_server_synced() {
 	return server_syncs > SERVER_SYNC_FRAME_COUNT_WARNING;
 	return server_syncs > SERVER_SYNC_FRAME_COUNT_WARNING;
 }
 }
 
 
+void Engine::set_freeze_time_scale(bool p_frozen) {
+	freeze_time_scale = p_frozen;
+}
+
 Engine::Engine() {
 Engine::Engine() {
 	singleton = this;
 	singleton = this;
 }
 }

+ 5 - 0
core/config/engine.h

@@ -99,6 +99,8 @@ private:
 	int server_syncs = 0;
 	int server_syncs = 0;
 	bool frame_server_synced = false;
 	bool frame_server_synced = false;
 
 
+	bool freeze_time_scale = false;
+
 public:
 public:
 	static Engine *get_singleton();
 	static Engine *get_singleton();
 
 
@@ -130,6 +132,7 @@ public:
 
 
 	void set_time_scale(double p_scale);
 	void set_time_scale(double p_scale);
 	double get_time_scale() const;
 	double get_time_scale() const;
+	double get_unfrozen_time_scale() const;
 
 
 	void set_print_to_stdout(bool p_enabled);
 	void set_print_to_stdout(bool p_enabled);
 	bool is_printing_to_stdout() const;
 	bool is_printing_to_stdout() const;
@@ -197,6 +200,8 @@ public:
 	void increment_frames_drawn();
 	void increment_frames_drawn();
 	bool notify_frame_server_synced();
 	bool notify_frame_server_synced();
 
 
+	void set_freeze_time_scale(bool p_frozen);
+
 	Engine();
 	Engine();
 	virtual ~Engine();
 	virtual ~Engine();
 };
 };

+ 107 - 1
core/input/input.cpp

@@ -87,11 +87,50 @@ Input *Input::get_singleton() {
 
 
 void Input::set_mouse_mode(MouseMode p_mode) {
 void Input::set_mouse_mode(MouseMode p_mode) {
 	ERR_FAIL_INDEX((int)p_mode, 5);
 	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);
 	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 {
 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() {
 void Input::_bind_methods() {
@@ -252,6 +291,10 @@ Input::VelocityTrack::VelocityTrack() {
 bool Input::is_anything_pressed() const {
 bool Input::is_anything_pressed() const {
 	_THREAD_SAFE_METHOD_
 	_THREAD_SAFE_METHOD_
 
 
+	if (disable_input) {
+		return false;
+	}
+
 	if (!keys_pressed.is_empty() || !joy_buttons_pressed.is_empty() || !mouse_button_mask.is_empty()) {
 	if (!keys_pressed.is_empty() || !joy_buttons_pressed.is_empty() || !mouse_button_mask.is_empty()) {
 		return true;
 		return true;
 	}
 	}
@@ -267,21 +310,41 @@ bool Input::is_anything_pressed() const {
 
 
 bool Input::is_key_pressed(Key p_keycode) const {
 bool Input::is_key_pressed(Key p_keycode) const {
 	_THREAD_SAFE_METHOD_
 	_THREAD_SAFE_METHOD_
+
+	if (disable_input) {
+		return false;
+	}
+
 	return keys_pressed.has(p_keycode);
 	return keys_pressed.has(p_keycode);
 }
 }
 
 
 bool Input::is_physical_key_pressed(Key p_keycode) const {
 bool Input::is_physical_key_pressed(Key p_keycode) const {
 	_THREAD_SAFE_METHOD_
 	_THREAD_SAFE_METHOD_
+
+	if (disable_input) {
+		return false;
+	}
+
 	return physical_keys_pressed.has(p_keycode);
 	return physical_keys_pressed.has(p_keycode);
 }
 }
 
 
 bool Input::is_key_label_pressed(Key p_keycode) const {
 bool Input::is_key_label_pressed(Key p_keycode) const {
 	_THREAD_SAFE_METHOD_
 	_THREAD_SAFE_METHOD_
+
+	if (disable_input) {
+		return false;
+	}
+
 	return key_label_pressed.has(p_keycode);
 	return key_label_pressed.has(p_keycode);
 }
 }
 
 
 bool Input::is_mouse_button_pressed(MouseButton p_button) const {
 bool Input::is_mouse_button_pressed(MouseButton p_button) const {
 	_THREAD_SAFE_METHOD_
 	_THREAD_SAFE_METHOD_
+
+	if (disable_input) {
+		return false;
+	}
+
 	return mouse_button_mask.has_flag(mouse_button_to_mask(p_button));
 	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 {
 bool Input::is_joy_button_pressed(int p_device, JoyButton p_button) const {
 	_THREAD_SAFE_METHOD_
 	_THREAD_SAFE_METHOD_
+
+	if (disable_input) {
+		return false;
+	}
+
 	return joy_buttons_pressed.has(_combine_device(p_button, p_device));
 	return joy_buttons_pressed.has(_combine_device(p_button, p_device));
 }
 }
 
 
 bool Input::is_action_pressed(const StringName &p_action, bool p_exact) const {
 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));
 	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);
 	HashMap<StringName, ActionState>::ConstIterator E = action_states.find(p_action);
 	if (!E) {
 	if (!E) {
 		return false;
 		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 {
 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));
 	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);
 	HashMap<StringName, ActionState>::ConstIterator E = action_states.find(p_action);
 	if (!E) {
 	if (!E) {
 		return false;
 		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 {
 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));
 	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);
 	HashMap<StringName, ActionState>::ConstIterator E = action_states.find(p_action);
 	if (!E) {
 	if (!E) {
 		return false;
 		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 {
 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));
 	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);
 	HashMap<StringName, ActionState>::ConstIterator E = action_states.find(p_action);
 	if (!E) {
 	if (!E) {
 		return 0.0f;
 		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 {
 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));
 	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);
 	HashMap<StringName, ActionState>::ConstIterator E = action_states.find(p_action);
 	if (!E) {
 	if (!E) {
 		return 0.0f;
 		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 {
 float Input::get_joy_axis(int p_device, JoyAxis p_axis) const {
 	_THREAD_SAFE_METHOD_
 	_THREAD_SAFE_METHOD_
+
+	if (disable_input) {
+		return 0;
+	}
+
 	JoyAxis c = _combine_device(p_axis, p_device);
 	JoyAxis c = _combine_device(p_axis, p_device);
 	if (_joy_axis.has(c)) {
 	if (_joy_axis.has(c)) {
 		return _joy_axis[c];
 		return _joy_axis[c];
@@ -1664,6 +1762,14 @@ int Input::get_unused_joy_id() {
 	return -1;
 	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() {
 Input::Input() {
 	singleton = this;
 	singleton = this;
 
 

+ 10 - 0
core/input/input.h

@@ -103,6 +103,11 @@ private:
 	Vector2 mouse_pos;
 	Vector2 mouse_pos;
 	int64_t mouse_window = 0;
 	int64_t mouse_window = 0;
 	bool legacy_just_pressed_behavior = false;
 	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 {
 	struct ActionState {
 		uint64_t pressed_physics_frame = UINT64_MAX;
 		uint64_t pressed_physics_frame = UINT64_MAX;
@@ -279,6 +284,8 @@ protected:
 public:
 public:
 	void set_mouse_mode(MouseMode p_mode);
 	void set_mouse_mode(MouseMode p_mode);
 	MouseMode get_mouse_mode() const;
 	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
 #ifdef TOOLS_ENABLED
 	void get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const override;
 	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_event_dispatch_function(EventDispatchFunc p_function);
 
 
+	void set_disable_input(bool p_disable);
+	bool is_input_disabled() const;
+
 	Input();
 	Input();
 	~Input();
 	~Input();
 };
 };

+ 4 - 1
doc/classes/EditorFeatureProfile.xml

@@ -121,7 +121,10 @@
 		<constant name="FEATURE_HISTORY_DOCK" value="7" enum="Feature">
 		<constant name="FEATURE_HISTORY_DOCK" value="7" enum="Feature">
 			The History dock. If this feature is disabled, the History dock won't be visible.
 			The History dock. If this feature is disabled, the History dock won't be visible.
 		</constant>
 		</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.
 			Represents the size of the [enum Feature] enum.
 		</constant>
 		</constant>
 	</constants>
 	</constants>

+ 8 - 0
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("breakpoint_selected", callable_mp(this, &EditorDebuggerNode::_error_selected).bind(id));
 	node->connect("clear_execution", callable_mp(this, &EditorDebuggerNode::_clear_execution));
 	node->connect("clear_execution", callable_mp(this, &EditorDebuggerNode::_clear_execution));
 	node->connect("breaked", callable_mp(this, &EditorDebuggerNode::_breaked).bind(id));
 	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_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_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));
 	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();
 	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) {
 void EditorDebuggerNode::_remote_tree_updated(int p_debugger) {
 	if (p_debugger != tabs->get_current_tab()) {
 	if (p_debugger != tabs->get_current_tab()) {
 		return;
 		return;

+ 3 - 5
editor/debugger/editor_debugger_node.h

@@ -51,11 +51,8 @@ class EditorDebuggerNode : public MarginContainer {
 public:
 public:
 	enum CameraOverride {
 	enum CameraOverride {
 		OVERRIDE_NONE,
 		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:
 private:
@@ -132,6 +129,7 @@ protected:
 	void _debugger_stopped(int p_id);
 	void _debugger_stopped(int p_id);
 	void _debugger_wants_stop(int p_id);
 	void _debugger_wants_stop(int p_id);
 	void _debugger_changed(int p_tab);
 	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_updated(int p_debugger);
 	void _remote_tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button);
 	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);
 	void _remote_object_updated(ObjectID p_id, int p_debugger);

+ 33 - 5
editor/debugger/editor_debugger_tree.cpp

@@ -30,6 +30,7 @@
 
 
 #include "editor_debugger_tree.h"
 #include "editor_debugger_tree.h"
 
 
+#include "editor/debugger/editor_debugger_node.h"
 #include "editor/editor_node.h"
 #include "editor/editor_node.h"
 #include "editor/editor_string_names.h"
 #include "editor/editor_string_names.h"
 #include "editor/gui/editor_file_dialog.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;
 	updating_scene_tree = true;
 	const String last_path = get_selected_path();
 	const String last_path = get_selected_path();
 	const String filter = SceneTreeDock::get_singleton()->get_filter();
 	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;
 	TreeItem *scroll_item = nullptr;
 
 
 	// Nodes are in a flatten list, depth first. Use a stack of parents, avoid recursion.
 	// 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.
 		// Select previously selected node.
 		if (debugger_id == p_debugger) { // Can use remote id.
 		if (debugger_id == p_debugger) { // Can use remote id.
 			if (node.id == inspected_object_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);
 				item->select(0);
-				if (filter_changed) {
+
+				if (should_scroll) {
 					scroll_item = item;
 					scroll_item = item;
 				}
 				}
 			}
 			}
@@ -194,7 +206,7 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int
 			if (last_path == _get_path(item)) {
 			if (last_path == _get_path(item)) {
 				updating_scene_tree = false; // Force emission of new selection.
 				updating_scene_tree = false; // Force emission of new selection.
 				item->select(0);
 				item->select(0);
-				if (filter_changed) {
+				if (should_scroll) {
 					scroll_item = item;
 					scroll_item = item;
 				}
 				}
 				updating_scene_tree = true;
 				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) {
 	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;
 	last_filter = filter;
 	updating_scene_tree = false;
 	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) {
 Variant EditorDebuggerTree::get_drag_data(const Point2 &p_point) {
 	if (get_button_id_at_position(p_point) != -1) {
 	if (get_button_id_at_position(p_point) != -1) {
 		return Variant();
 		return Variant();

+ 3 - 0
editor/debugger/editor_debugger_tree.h

@@ -49,6 +49,8 @@ private:
 	ObjectID inspected_object_id;
 	ObjectID inspected_object_id;
 	int debugger_id = 0;
 	int debugger_id = 0;
 	bool updating_scene_tree = false;
 	bool updating_scene_tree = false;
+	bool scrolling_to_item = false;
+	bool selection_uncollapse_all = false;
 	HashSet<ObjectID> unfold_cache;
 	HashSet<ObjectID> unfold_cache;
 	PopupMenu *item_menu = nullptr;
 	PopupMenu *item_menu = nullptr;
 	EditorFileDialog *file_dialog = nullptr;
 	EditorFileDialog *file_dialog = nullptr;
@@ -78,6 +80,7 @@ public:
 	ObjectID get_selected_object();
 	ObjectID get_selected_object();
 	int get_current_debugger(); // Would love to have one tree for every debugger.
 	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 update_scene_tree(const SceneDebuggerTree *p_tree, int p_debugger);
+	void select_node(ObjectID p_id);
 	EditorDebuggerTree();
 	EditorDebuggerTree();
 };
 };
 
 

+ 43 - 46
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") {
 	} else if (p_msg == "request_quit") {
 		emit_signal(SNAME("stop_requested"));
 		emit_signal(SNAME("stop_requested"));
 		_stop_and_notify();
 		_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") {
 	} else if (p_msg == "performance:profile_names") {
 		Vector<StringName> monitors;
 		Vector<StringName> monitors;
 		monitors.resize(p_data.size());
 		monitors.resize(p_data.size());
@@ -905,37 +909,42 @@ void ScriptEditorDebugger::_notification(int p_what) {
 			if (is_session_active()) {
 			if (is_session_active()) {
 				peer->poll();
 				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) {
 				if (is_breaked() && can_request_idle_draw) {
 					_put_msg("servers:draw", Array());
 					_put_msg("servers:draw", Array());
 					can_request_idle_draw = false;
 					can_request_idle_draw = false;
@@ -1469,23 +1478,10 @@ CameraOverride ScriptEditorDebugger::get_camera_override() const {
 }
 }
 
 
 void ScriptEditorDebugger::set_camera_override(CameraOverride p_override) {
 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;
 	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_updated", PropertyInfo(Variant::INT, "id")));
 	ADD_SIGNAL(MethodInfo("remote_object_property_updated", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::STRING, "property")));
 	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_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("output", PropertyInfo(Variant::STRING, "msg"), PropertyInfo(Variant::INT, "level")));
 	ADD_SIGNAL(MethodInfo("stack_dump", PropertyInfo(Variant::ARRAY, "stack_dump")));
 	ADD_SIGNAL(MethodInfo("stack_dump", PropertyInfo(Variant::ARRAY, "stack_dump")));
 	ADD_SIGNAL(MethodInfo("stack_frame_vars", PropertyInfo(Variant::INT, "num_vars")));
 	ADD_SIGNAL(MethodInfo("stack_frame_vars", PropertyInfo(Variant::INT, "num_vars")));

+ 4 - 0
editor/editor_feature_profile.cpp

@@ -43,6 +43,7 @@
 const char *EditorFeatureProfile::feature_names[FEATURE_MAX] = {
 const char *EditorFeatureProfile::feature_names[FEATURE_MAX] = {
 	TTRC("3D Editor"),
 	TTRC("3D Editor"),
 	TTRC("Script Editor"),
 	TTRC("Script Editor"),
+	TTRC("Game View"),
 	TTRC("Asset Library"),
 	TTRC("Asset Library"),
 	TTRC("Scene Tree Editing"),
 	TTRC("Scene Tree Editing"),
 	TTRC("Node Dock"),
 	TTRC("Node Dock"),
@@ -54,6 +55,7 @@ const char *EditorFeatureProfile::feature_names[FEATURE_MAX] = {
 const char *EditorFeatureProfile::feature_descriptions[FEATURE_MAX] = {
 const char *EditorFeatureProfile::feature_descriptions[FEATURE_MAX] = {
 	TTRC("Allows to view and edit 3D scenes."),
 	TTRC("Allows to view and edit 3D scenes."),
 	TTRC("Allows to edit scripts using the integrated script editor."),
 	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("Provides built-in access to the Asset Library."),
 	TTRC("Allows editing the node hierarchy in the Scene dock."),
 	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."),
 	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] = {
 const char *EditorFeatureProfile::feature_identifiers[FEATURE_MAX] = {
 	"3d",
 	"3d",
 	"script",
 	"script",
+	"game",
 	"asset_lib",
 	"asset_lib",
 	"scene_tree",
 	"scene_tree",
 	"node_dock",
 	"node_dock",
@@ -307,6 +310,7 @@ void EditorFeatureProfile::_bind_methods() {
 	BIND_ENUM_CONSTANT(FEATURE_FILESYSTEM_DOCK);
 	BIND_ENUM_CONSTANT(FEATURE_FILESYSTEM_DOCK);
 	BIND_ENUM_CONSTANT(FEATURE_IMPORT_DOCK);
 	BIND_ENUM_CONSTANT(FEATURE_IMPORT_DOCK);
 	BIND_ENUM_CONSTANT(FEATURE_HISTORY_DOCK);
 	BIND_ENUM_CONSTANT(FEATURE_HISTORY_DOCK);
+	BIND_ENUM_CONSTANT(FEATURE_GAME);
 	BIND_ENUM_CONSTANT(FEATURE_MAX);
 	BIND_ENUM_CONSTANT(FEATURE_MAX);
 }
 }
 
 

+ 1 - 0
editor/editor_feature_profile.h

@@ -55,6 +55,7 @@ public:
 		FEATURE_FILESYSTEM_DOCK,
 		FEATURE_FILESYSTEM_DOCK,
 		FEATURE_IMPORT_DOCK,
 		FEATURE_IMPORT_DOCK,
 		FEATURE_HISTORY_DOCK,
 		FEATURE_HISTORY_DOCK,
+		FEATURE_GAME,
 		FEATURE_MAX
 		FEATURE_MAX
 	};
 	};
 
 

+ 1 - 0
editor/editor_main_screen.h

@@ -47,6 +47,7 @@ public:
 		EDITOR_2D = 0,
 		EDITOR_2D = 0,
 		EDITOR_3D,
 		EDITOR_3D,
 		EDITOR_SCRIPT,
 		EDITOR_SCRIPT,
+		EDITOR_GAME,
 		EDITOR_ASSETLIB,
 		EDITOR_ASSETLIB,
 	};
 	};
 
 

+ 10 - 2
editor/editor_node.cpp

@@ -145,6 +145,7 @@
 #include "editor/plugins/editor_plugin.h"
 #include "editor/plugins/editor_plugin.h"
 #include "editor/plugins/editor_preview_plugins.h"
 #include "editor/plugins/editor_preview_plugins.h"
 #include "editor/plugins/editor_resource_conversion_plugin.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/gdextension_export_plugin.h"
 #include "editor/plugins/material_editor_plugin.h"
 #include "editor/plugins/material_editor_plugin.h"
 #include "editor/plugins/mesh_library_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);
 			editor_main_screen->select(EditorMainScreen::EDITOR_3D);
 		} else if (ED_IS_SHORTCUT("editor/editor_script", p_event)) {
 		} else if (ED_IS_SHORTCUT("editor/editor_script", p_event)) {
 			editor_main_screen->select(EditorMainScreen::EDITOR_SCRIPT);
 			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)) {
 		} else if (ED_IS_SHORTCUT("editor/editor_help", p_event)) {
 			emit_signal(SNAME("request_help_search"), "");
 			emit_signal(SNAME("request_help_search"), "");
 		} else if (ED_IS_SHORTCUT("editor/editor_assetlib", p_event) && AssetLibraryEditorPlugin::is_available()) {
 		} 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_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_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()) {
 		if (AssetLibraryEditorPlugin::is_available()) {
 			editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_ASSETLIB, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_ASSET_LIB));
 			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_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_3D, true);
 		editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_SCRIPT, 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()) {
 		if (AssetLibraryEditorPlugin::is_available()) {
 			editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_ASSETLIB, true);
 			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(CanvasItemEditorPlugin));
 	add_editor_plugin(memnew(Node3DEditorPlugin));
 	add_editor_plugin(memnew(Node3DEditorPlugin));
 	add_editor_plugin(memnew(ScriptEditorPlugin));
 	add_editor_plugin(memnew(ScriptEditorPlugin));
+	add_editor_plugin(memnew(GameViewPlugin));
 
 
 	EditorAudioBuses *audio_bus_editor = EditorAudioBuses::register_editor();
 	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_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_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_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_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_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_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_next", TTR("Open the next Editor"));
 	ED_SHORTCUT_AND_COMMAND("editor/editor_prev", TTR("Open the previous Editor"));
 	ED_SHORTCUT_AND_COMMAND("editor/editor_prev", TTR("Open the previous Editor"));

+ 1 - 0
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>

+ 1 - 0
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>

+ 1 - 0
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>

+ 1 - 0
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>

+ 0 - 43
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")));
 	grid_snap_button->set_button_icon(get_editor_theme_icon(SNAME("SnapGrid")));
 	snap_config_menu->set_button_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl")));
 	snap_config_menu->set_button_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl")));
 	skeleton_menu->set_button_icon(get_editor_theme_icon(SNAME("Bone")));
 	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")));
 	pan_button->set_button_icon(get_editor_theme_icon(SNAME("ToolPan")));
 	ruler_button->set_button_icon(get_editor_theme_icon(SNAME("Ruler")));
 	ruler_button->set_button_icon(get_editor_theme_icon(SNAME("Ruler")));
 	pivot_button->set_button_icon(get_editor_theme_icon(SNAME("EditPivot")));
 	pivot_button->set_button_icon(get_editor_theme_icon(SNAME("EditPivot")));
@@ -4016,8 +4015,6 @@ void CanvasItemEditor::_notification(int p_what) {
 		case NOTIFICATION_READY: {
 		case NOTIFICATION_READY: {
 			_update_lock_and_group_button();
 			_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));
 			ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &CanvasItemEditor::_project_settings_changed));
 		} break;
 		} break;
 
 
@@ -4116,15 +4113,6 @@ void CanvasItemEditor::_notification(int p_what) {
 			_update_editor_settings();
 			_update_editor_settings();
 		} break;
 		} 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_APPLICATION_FOCUS_OUT:
 		case NOTIFICATION_WM_WINDOW_FOCUS_OUT: {
 		case NOTIFICATION_WM_WINDOW_FOCUS_OUT: {
 			if (drag_type != DRAG_NONE) {
 			if (drag_type != DRAG_NONE) {
@@ -4282,16 +4270,6 @@ void CanvasItemEditor::_button_toggle_grid_snap(bool p_status) {
 	viewport->queue_redraw();
 	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) {
 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 };
 	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++) {
 	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();
 	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) {
 void CanvasItemEditor::_popup_callback(int p_op) {
 	EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
 	EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
 	last_option = MenuOption(p_op);
 	last_option = MenuOption(p_op);
@@ -5514,16 +5481,6 @@ CanvasItemEditor::CanvasItemEditor() {
 
 
 	main_menu_hbox->add_child(memnew(VSeparator));
 	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 = memnew(MenuButton);
 	view_menu->set_flat(false);
 	view_menu->set_flat(false);
 	view_menu->set_theme_type_variation("FlatMenuButton");
 	view_menu->set_theme_type_variation("FlatMenuButton");

+ 0 - 4
editor/plugins/canvas_item_editor_plugin.h

@@ -335,7 +335,6 @@ private:
 	Button *group_button = nullptr;
 	Button *group_button = nullptr;
 	Button *ungroup_button = nullptr;
 	Button *ungroup_button = nullptr;
 
 
-	Button *override_camera_button = nullptr;
 	MenuButton *view_menu = nullptr;
 	MenuButton *view_menu = nullptr;
 	PopupMenu *grid_menu = nullptr;
 	PopupMenu *grid_menu = nullptr;
 	PopupMenu *theme_menu = nullptr;
 	PopupMenu *theme_menu = nullptr;
@@ -518,11 +517,8 @@ private:
 	void _zoom_on_position(real_t p_zoom, Point2 p_position = Point2());
 	void _zoom_on_position(real_t p_zoom, Point2 p_position = Point2());
 	void _button_toggle_smart_snap(bool p_status);
 	void _button_toggle_smart_snap(bool p_status);
 	void _button_toggle_grid_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 _button_tool_select(int p_index);
 
 
-	void _update_override_camera_button(bool p_game_running);
-
 	HSplitContainer *left_panel_split = nullptr;
 	HSplitContainer *left_panel_split = nullptr;
 	HSplitContainer *right_panel_split = nullptr;
 	HSplitContainer *right_panel_split = nullptr;
 	VSplitContainer *bottom_split = nullptr;
 	VSplitContainer *bottom_split = nullptr;

+ 478 - 0
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() {
+}

+ 152 - 0
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

+ 26 - 70
editor/plugins/node_3d_editor_plugin.cpp

@@ -1692,7 +1692,7 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) {
 	Ref<InputEventMouseButton> b = p_event;
 	Ref<InputEventMouseButton> b = p_event;
 
 
 	if (b.is_valid()) {
 	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 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();
 		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() {
 void Node3DEditorViewport::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("toggle_maximize_view", PropertyInfo(Variant::OBJECT, "viewport")));
 	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() {
 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);
 			tool_option_button[TOOL_OPT_USE_SNAP]->set_pressed(pressed);
 			snap_enabled = pressed;
 			snap_enabled = pressed;
 		} break;
 		} 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();
 	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) {
 void Node3DEditor::_menu_item_pressed(int p_option) {
 	EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
 	EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
 	switch (p_option) {
 	switch (p_option) {
@@ -6670,6 +6628,9 @@ void Node3DEditor::_menu_item_pressed(int p_option) {
 		} break;
 		} break;
 		case MENU_VIEW_USE_1_VIEWPORT: {
 		case MENU_VIEW_USE_1_VIEWPORT: {
 			viewport_base->set_view(Node3DEditorViewportContainer::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_1_VIEWPORT), true);
 			view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), false);
 			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;
 		} break;
 		case MENU_VIEW_USE_2_VIEWPORTS: {
 		case MENU_VIEW_USE_2_VIEWPORTS: {
 			viewport_base->set_view(Node3DEditorViewportContainer::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_1_VIEWPORT), false);
 			view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), true);
 			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;
 		} break;
 		case MENU_VIEW_USE_2_VIEWPORTS_ALT: {
 		case MENU_VIEW_USE_2_VIEWPORTS_ALT: {
 			viewport_base->set_view(Node3DEditorViewportContainer::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_1_VIEWPORT), false);
 			view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), 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;
 		} break;
 		case MENU_VIEW_USE_3_VIEWPORTS: {
 		case MENU_VIEW_USE_3_VIEWPORTS: {
 			viewport_base->set_view(Node3DEditorViewportContainer::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_1_VIEWPORT), false);
 			view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), 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;
 		} break;
 		case MENU_VIEW_USE_3_VIEWPORTS_ALT: {
 		case MENU_VIEW_USE_3_VIEWPORTS_ALT: {
 			viewport_base->set_view(Node3DEditorViewportContainer::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_1_VIEWPORT), false);
 			view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), 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_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_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_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")));
 	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));
 			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));
 			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();
 			_update_preview_environment();
 
 
 			sun_state->set_custom_minimum_size(sun_vb->get_combined_minimum_size());
 			sun_state->set_custom_minimum_size(sun_vb->get_combined_minimum_size());
@@ -8106,15 +8075,6 @@ void Node3DEditor::_notification(int p_what) {
 			}
 			}
 		} break;
 		} 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: {
 		case NOTIFICATION_PHYSICS_PROCESS: {
 			if (do_snap_selected_nodes_to_floor) {
 			if (do_snap_selected_nodes_to_floor) {
 				_snap_selected_nodes_to_floor();
 				_snap_selected_nodes_to_floor();
@@ -8216,6 +8176,10 @@ VSplitContainer *Node3DEditor::get_shader_split() {
 	return 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) {
 void Node3DEditor::add_control_to_left_panel(Control *p_control) {
 	left_panel_split->add_child(p_control);
 	left_panel_split->add_child(p_control);
 	left_panel_split->move_child(p_control, 0);
 	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) {
 void Node3DEditor::_node_added(Node *p_node) {
 	if (EditorNode::get_singleton()->get_scene_root()->is_ancestor_of(p_node)) {
 	if (EditorNode::get_singleton()->get_scene_root()->is_ancestor_of(p_node)) {
 		if (Object::cast_to<WorldEnvironment>(p_node)) {
 		if (Object::cast_to<WorldEnvironment>(p_node)) {
@@ -8684,8 +8652,6 @@ Node3DEditor::Node3DEditor() {
 	snap_key_enabled = false;
 	snap_key_enabled = false;
 	tool_mode = TOOL_MODE_SELECT;
 	tool_mode = TOOL_MODE_SELECT;
 
 
-	camera_override_viewport_id = 0;
-
 	// Add some margin to the sides for better aesthetics.
 	// Add some margin to the sides for better aesthetics.
 	// This prevents the first button's hover/pressed effect from "touching" the panel's border,
 	// This prevents the first button's hover/pressed effect from "touching" the panel's border,
 	// which looks ugly.
 	// which looks ugly.
@@ -8802,16 +8768,6 @@ Node3DEditor::Node3DEditor() {
 	tool_option_button[TOOL_OPT_USE_SNAP]->set_shortcut(ED_SHORTCUT("spatial_editor/snap", TTR("Use Snap"), Key::Y));
 	tool_option_button[TOOL_OPT_USE_SNAP]->set_shortcut(ED_SHORTCUT("spatial_editor/snap", TTR("Use Snap"), Key::Y));
 	tool_option_button[TOOL_OPT_USE_SNAP]->set_shortcut_context(this);
 	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));
 	main_menu_hbox->add_child(memnew(VSeparator));
 	sun_button = memnew(Button);
 	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_tooltip_text(TTR("Toggle preview sunlight.\nIf a DirectionalLight3D node is added to the scene, preview sunlight is disabled."));
@@ -8955,7 +8911,7 @@ Node3DEditor::Node3DEditor() {
 	for (uint32_t i = 0; i < VIEWPORTS_COUNT; i++) {
 	for (uint32_t i = 0; i < VIEWPORTS_COUNT; i++) {
 		viewports[i] = memnew(Node3DEditorViewport(this, i));
 		viewports[i] = memnew(Node3DEditorViewport(this, i));
 		viewports[i]->connect("toggle_maximize_view", callable_mp(this, &Node3DEditor::_toggle_maximize_view));
 		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);
 		viewports[i]->assign_pending_data_pointers(preview_node, &preview_bounds, accept);
 		viewport_base->add_child(viewports[i]);
 		viewport_base->add_child(viewports[i]);
 	}
 	}

+ 4 - 6
editor/plugins/node_3d_editor_plugin.h

@@ -622,7 +622,6 @@ public:
 	enum ToolOptions {
 	enum ToolOptions {
 		TOOL_OPT_LOCAL_COORDS,
 		TOOL_OPT_LOCAL_COORDS,
 		TOOL_OPT_USE_SNAP,
 		TOOL_OPT_USE_SNAP,
-		TOOL_OPT_OVERRIDE_CAMERA,
 		TOOL_OPT_MAX
 		TOOL_OPT_MAX
 
 
 	};
 	};
@@ -632,6 +631,8 @@ private:
 
 
 	Node3DEditorViewportContainer *viewport_base = nullptr;
 	Node3DEditorViewportContainer *viewport_base = nullptr;
 	Node3DEditorViewport *viewports[VIEWPORTS_COUNT];
 	Node3DEditorViewport *viewports[VIEWPORTS_COUNT];
+	int last_used_viewport = 0;
+
 	VSplitContainer *shader_split = nullptr;
 	VSplitContainer *shader_split = nullptr;
 	HSplitContainer *left_panel_split = nullptr;
 	HSplitContainer *left_panel_split = nullptr;
 	HSplitContainer *right_panel_split = nullptr;
 	HSplitContainer *right_panel_split = nullptr;
@@ -704,7 +705,6 @@ private:
 		MENU_TOOL_LIST_SELECT,
 		MENU_TOOL_LIST_SELECT,
 		MENU_TOOL_LOCAL_COORDS,
 		MENU_TOOL_LOCAL_COORDS,
 		MENU_TOOL_USE_SNAP,
 		MENU_TOOL_USE_SNAP,
-		MENU_TOOL_OVERRIDE_CAMERA,
 		MENU_TRANSFORM_CONFIGURE_SNAP,
 		MENU_TRANSFORM_CONFIGURE_SNAP,
 		MENU_TRANSFORM_DIALOG,
 		MENU_TRANSFORM_DIALOG,
 		MENU_VIEW_USE_1_VIEWPORT,
 		MENU_VIEW_USE_1_VIEWPORT,
@@ -759,8 +759,6 @@ private:
 	void _menu_item_pressed(int p_option);
 	void _menu_item_pressed(int p_option);
 	void _menu_item_toggled(bool pressed, int p_option);
 	void _menu_item_toggled(bool pressed, int p_option);
 	void _menu_gizmo_toggled(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
 	// Used for secondary menu items which are displayed depending on the currently selected node
 	// (such as MeshInstance's "Mesh" menu).
 	// (such as MeshInstance's "Mesh" menu).
 	PanelContainer *context_toolbar_panel = nullptr;
 	PanelContainer *context_toolbar_panel = nullptr;
@@ -771,8 +769,6 @@ private:
 
 
 	void _generate_selection_boxes();
 	void _generate_selection_boxes();
 
 
-	int camera_override_viewport_id;
-
 	void _init_indicators();
 	void _init_indicators();
 	void _update_gizmos_menu();
 	void _update_gizmos_menu();
 	void _update_gizmos_menu_theme();
 	void _update_gizmos_menu_theme();
@@ -781,6 +777,7 @@ private:
 	void _finish_grid();
 	void _finish_grid();
 
 
 	void _toggle_maximize_view(Object *p_viewport);
 	void _toggle_maximize_view(Object *p_viewport);
+	void _viewport_clicked(int p_viewport_idx);
 
 
 	Node *custom_camera = nullptr;
 	Node *custom_camera = nullptr;
 
 
@@ -967,6 +964,7 @@ public:
 		ERR_FAIL_INDEX_V(p_idx, static_cast<int>(VIEWPORTS_COUNT), nullptr);
 		ERR_FAIL_INDEX_V(p_idx, static_cast<int>(VIEWPORTS_COUNT), nullptr);
 		return viewports[p_idx];
 		return viewports[p_idx];
 	}
 	}
+	Node3DEditorViewport *get_last_used_viewport();
 
 
 	void add_gizmo_plugin(Ref<EditorNode3DGizmoPlugin> p_plugin);
 	void add_gizmo_plugin(Ref<EditorNode3DGizmoPlugin> p_plugin);
 	void remove_gizmo_plugin(Ref<EditorNode3DGizmoPlugin> p_plugin);
 	void remove_gizmo_plugin(Ref<EditorNode3DGizmoPlugin> p_plugin);

+ 6 - 0
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("ScriptEditorPanelFloating", EditorStringName(EditorStyles), make_empty_stylebox(0, 0, 0, 0));
 		p_theme->set_stylebox("ScriptEditor", 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.
 		// Main menu.
 		Ref<StyleBoxFlat> menu_transparent_style = p_config.button_style->duplicate();
 		Ref<StyleBoxFlat> menu_transparent_style = p_config.button_style->duplicate();
 		menu_transparent_style->set_bg_color(Color(1, 1, 1, 0));
 		menu_transparent_style->set_bg_color(Color(1, 1, 1, 0));

+ 7 - 1
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.
 Add new entries at the end of the file.
 
 
 ## Changes between 4.3-stable and 4.4-stable
 ## Changes between 4.3-stable and 4.4-stable
-
 GH-95374
 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".
 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.
 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.
 `_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.

+ 1 - 0
scene/2d/camera_2d.cpp

@@ -302,6 +302,7 @@ void Camera2D::_notification(int p_what) {
 			_interpolation_data.xform_prev = _interpolation_data.xform_curr;
 			_interpolation_data.xform_prev = _interpolation_data.xform_curr;
 		} break;
 		} break;
 
 
+		case NOTIFICATION_SUSPENDED:
 		case NOTIFICATION_PAUSED: {
 		case NOTIFICATION_PAUSED: {
 			if (is_physics_interpolated_and_enabled()) {
 			if (is_physics_interpolated_and_enabled()) {
 				_update_scroll();
 				_update_scroll();

+ 2 - 0
scene/2d/gpu_particles_2d.cpp

@@ -696,6 +696,8 @@ void GPUParticles2D::_notification(int p_what) {
 			RS::get_singleton()->particles_set_subemitter(particles, RID());
 			RS::get_singleton()->particles_set_subemitter(particles, RID());
 		} break;
 		} break;
 
 
+		case NOTIFICATION_SUSPENDED:
+		case NOTIFICATION_UNSUSPENDED:
 		case NOTIFICATION_PAUSED:
 		case NOTIFICATION_PAUSED:
 		case NOTIFICATION_UNPAUSED: {
 		case NOTIFICATION_UNPAUSED: {
 			if (is_inside_tree()) {
 			if (is_inside_tree()) {

+ 8 - 0
scene/2d/navigation_agent_2d.cpp

@@ -253,12 +253,20 @@ void NavigationAgent2D::_notification(int p_what) {
 #endif // DEBUG_ENABLED
 #endif // DEBUG_ENABLED
 		} break;
 		} break;
 
 
+		case NOTIFICATION_SUSPENDED:
 		case NOTIFICATION_PAUSED: {
 		case NOTIFICATION_PAUSED: {
 			if (agent_parent) {
 			if (agent_parent) {
 				NavigationServer2D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process());
 				NavigationServer2D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process());
 			}
 			}
 		} break;
 		} break;
 
 
+		case NOTIFICATION_UNSUSPENDED: {
+			if (get_tree()->is_paused()) {
+				break;
+			}
+			[[fallthrough]];
+		}
+
 		case NOTIFICATION_UNPAUSED: {
 		case NOTIFICATION_UNPAUSED: {
 			if (agent_parent) {
 			if (agent_parent) {
 				NavigationServer2D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process());
 				NavigationServer2D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process());

+ 8 - 0
scene/2d/navigation_obstacle_2d.cpp

@@ -104,6 +104,7 @@ void NavigationObstacle2D::_notification(int p_what) {
 #endif // DEBUG_ENABLED
 #endif // DEBUG_ENABLED
 		} break;
 		} break;
 
 
+		case NOTIFICATION_SUSPENDED:
 		case NOTIFICATION_PAUSED: {
 		case NOTIFICATION_PAUSED: {
 			if (!can_process()) {
 			if (!can_process()) {
 				map_before_pause = map_current;
 				map_before_pause = map_current;
@@ -115,6 +116,13 @@ void NavigationObstacle2D::_notification(int p_what) {
 			NavigationServer2D::get_singleton()->obstacle_set_paused(obstacle, !can_process());
 			NavigationServer2D::get_singleton()->obstacle_set_paused(obstacle, !can_process());
 		} break;
 		} break;
 
 
+		case NOTIFICATION_UNSUSPENDED: {
+			if (get_tree()->is_paused()) {
+				break;
+			}
+			[[fallthrough]];
+		}
+
 		case NOTIFICATION_UNPAUSED: {
 		case NOTIFICATION_UNPAUSED: {
 			if (!can_process()) {
 			if (!can_process()) {
 				map_before_pause = map_current;
 				map_before_pause = map_current;

+ 1 - 0
scene/2d/touch_screen_button.cpp

@@ -185,6 +185,7 @@ void TouchScreenButton::_notification(int p_what) {
 			}
 			}
 		} break;
 		} break;
 
 
+		case NOTIFICATION_SUSPENDED:
 		case NOTIFICATION_PAUSED: {
 		case NOTIFICATION_PAUSED: {
 			if (is_pressed()) {
 			if (is_pressed()) {
 				_release();
 				_release();

+ 1 - 0
scene/3d/camera_3d.cpp

@@ -234,6 +234,7 @@ void Camera3D::_notification(int p_what) {
 			}
 			}
 		} break;
 		} break;
 
 
+		case NOTIFICATION_SUSPENDED:
 		case NOTIFICATION_PAUSED: {
 		case NOTIFICATION_PAUSED: {
 			if (is_physics_interpolated_and_enabled() && is_inside_tree() && is_visible_in_tree()) {
 			if (is_physics_interpolated_and_enabled() && is_inside_tree() && is_visible_in_tree()) {
 				_physics_interpolation_ensure_transform_calculated(true);
 				_physics_interpolation_ensure_transform_calculated(true);

+ 2 - 0
scene/3d/gpu_particles_3d.cpp

@@ -511,6 +511,8 @@ void GPUParticles3D::_notification(int p_what) {
 			RS::get_singleton()->particles_set_subemitter(particles, RID());
 			RS::get_singleton()->particles_set_subemitter(particles, RID());
 		} break;
 		} break;
 
 
+		case NOTIFICATION_SUSPENDED:
+		case NOTIFICATION_UNSUSPENDED:
 		case NOTIFICATION_PAUSED:
 		case NOTIFICATION_PAUSED:
 		case NOTIFICATION_UNPAUSED: {
 		case NOTIFICATION_UNPAUSED: {
 			if (is_inside_tree()) {
 			if (is_inside_tree()) {

+ 8 - 0
scene/3d/navigation_agent_3d.cpp

@@ -272,12 +272,20 @@ void NavigationAgent3D::_notification(int p_what) {
 #endif // DEBUG_ENABLED
 #endif // DEBUG_ENABLED
 		} break;
 		} break;
 
 
+		case NOTIFICATION_SUSPENDED:
 		case NOTIFICATION_PAUSED: {
 		case NOTIFICATION_PAUSED: {
 			if (agent_parent) {
 			if (agent_parent) {
 				NavigationServer3D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process());
 				NavigationServer3D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process());
 			}
 			}
 		} break;
 		} break;
 
 
+		case NOTIFICATION_UNSUSPENDED: {
+			if (get_tree()->is_paused()) {
+				break;
+			}
+			[[fallthrough]];
+		}
+
 		case NOTIFICATION_UNPAUSED: {
 		case NOTIFICATION_UNPAUSED: {
 			if (agent_parent) {
 			if (agent_parent) {
 				NavigationServer3D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process());
 				NavigationServer3D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process());

+ 8 - 0
scene/3d/navigation_obstacle_3d.cpp

@@ -119,6 +119,7 @@ void NavigationObstacle3D::_notification(int p_what) {
 #endif // DEBUG_ENABLED
 #endif // DEBUG_ENABLED
 		} break;
 		} break;
 
 
+		case NOTIFICATION_SUSPENDED:
 		case NOTIFICATION_PAUSED: {
 		case NOTIFICATION_PAUSED: {
 			if (!can_process()) {
 			if (!can_process()) {
 				map_before_pause = map_current;
 				map_before_pause = map_current;
@@ -130,6 +131,13 @@ void NavigationObstacle3D::_notification(int p_what) {
 			NavigationServer3D::get_singleton()->obstacle_set_paused(obstacle, !can_process());
 			NavigationServer3D::get_singleton()->obstacle_set_paused(obstacle, !can_process());
 		} break;
 		} break;
 
 
+		case NOTIFICATION_UNSUSPENDED: {
+			if (get_tree()->is_paused()) {
+				break;
+			}
+			[[fallthrough]];
+		}
+
 		case NOTIFICATION_UNPAUSED: {
 		case NOTIFICATION_UNPAUSED: {
 			if (!can_process()) {
 			if (!can_process()) {
 				map_before_pause = map_current;
 				map_before_pause = map_current;

+ 8 - 0
scene/audio/audio_stream_player_internal.cpp

@@ -112,6 +112,7 @@ void AudioStreamPlayerInternal::notification(int p_what) {
 			stream_playbacks.clear();
 			stream_playbacks.clear();
 		} break;
 		} break;
 
 
+		case Node::NOTIFICATION_SUSPENDED:
 		case Node::NOTIFICATION_PAUSED: {
 		case Node::NOTIFICATION_PAUSED: {
 			if (!node->can_process()) {
 			if (!node->can_process()) {
 				// Node can't process so we start fading out to silence
 				// Node can't process so we start fading out to silence
@@ -119,6 +120,13 @@ void AudioStreamPlayerInternal::notification(int p_what) {
 			}
 			}
 		} break;
 		} break;
 
 
+		case Node::NOTIFICATION_UNSUSPENDED: {
+			if (node->get_tree()->is_paused()) {
+				break;
+			}
+			[[fallthrough]];
+		}
+
 		case Node::NOTIFICATION_UNPAUSED: {
 		case Node::NOTIFICATION_UNPAUSED: {
 			set_stream_paused(false);
 			set_stream_paused(false);
 		} break;
 		} break;

+ 1113 - 79
scene/debugger/scene_debugger.cpp

@@ -31,20 +31,34 @@
 #include "scene_debugger.h"
 #include "scene_debugger.h"
 
 
 #include "core/debugger/engine_debugger.h"
 #include "core/debugger/engine_debugger.h"
-#include "core/debugger/engine_profiler.h"
 #include "core/io/marshalls.h"
 #include "core/io/marshalls.h"
 #include "core/object/script_language.h"
 #include "core/object/script_language.h"
 #include "core/templates/local_vector.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/scene_tree.h"
 #include "scene/main/window.h"
 #include "scene/main/window.h"
 #include "scene/resources/packed_scene.h"
 #include "scene/resources/packed_scene.h"
-
-SceneDebugger *SceneDebugger::singleton = nullptr;
+#include "scene/theme/theme_db.h"
 
 
 SceneDebugger::SceneDebugger() {
 SceneDebugger::SceneDebugger() {
 	singleton = this;
 	singleton = this;
+
 #ifdef DEBUG_ENABLED
 #ifdef DEBUG_ENABLED
 	LiveEditor::singleton = memnew(LiveEditor);
 	LiveEditor::singleton = memnew(LiveEditor);
+	RuntimeNodeSelect::singleton = memnew(RuntimeNodeSelect);
+
 	EngineDebugger::register_message_capture("scene", EngineDebugger::Capture(nullptr, SceneDebugger::parse_message));
 	EngineDebugger::register_message_capture("scene", EngineDebugger::Capture(nullptr, SceneDebugger::parse_message));
 #endif
 #endif
 }
 }
@@ -56,7 +70,13 @@ SceneDebugger::~SceneDebugger() {
 		memdelete(LiveEditor::singleton);
 		memdelete(LiveEditor::singleton);
 		LiveEditor::singleton = nullptr;
 		LiveEditor::singleton = nullptr;
 	}
 	}
+
+	if (RuntimeNodeSelect::singleton) {
+		memdelete(RuntimeNodeSelect::singleton);
+		RuntimeNodeSelect::singleton = nullptr;
+	}
 #endif
 #endif
+
 	singleton = nullptr;
 	singleton = nullptr;
 }
 }
 
 
@@ -78,10 +98,15 @@ Error SceneDebugger::parse_message(void *p_user, const String &p_msg, const Arra
 	if (!scene_tree) {
 	if (!scene_tree) {
 		return ERR_UNCONFIGURED;
 		return ERR_UNCONFIGURED;
 	}
 	}
+
 	LiveEditor *live_editor = LiveEditor::get_singleton();
 	LiveEditor *live_editor = LiveEditor::get_singleton();
 	if (!live_editor) {
 	if (!live_editor) {
 		return ERR_UNCONFIGURED;
 		return ERR_UNCONFIGURED;
 	}
 	}
+	RuntimeNodeSelect *runtime_node_select = RuntimeNodeSelect::get_singleton();
+	if (!runtime_node_select) {
+		return ERR_UNCONFIGURED;
+	}
 
 
 	r_captured = true;
 	r_captured = true;
 	if (p_msg == "request_scene_tree") { // Scene tree
 	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];
 		ObjectID id = p_args[0];
 		_send_object_id(id);
 		_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);
 		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);
 		ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
 		bool enable = p_args[0];
 		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);
 		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);
 		ERR_FAIL_COND_V(p_args.size() < 5, ERR_INVALID_DATA);
 		Transform3D transform = p_args[0];
 		Transform3D transform = p_args[0];
 		bool is_perspective = p_args[1];
 		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_orthogonal(size_or_fov, depth_near, depth_far);
 		}
 		}
 		scene_tree->get_root()->set_camera_3d_override_transform(transform);
 		scene_tree->get_root()->set_camera_3d_override_transform(transform);
+
+		runtime_node_select->_queue_selection_update();
 #endif // _3D_DISABLED
 #endif // _3D_DISABLED
+
 	} else if (p_msg == "set_object_property") {
 	} else if (p_msg == "set_object_property") {
 		ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
 		ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
 		_set_object_property(p_args[0], p_args[1], p_args[2]);
 		_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 {
 	} else {
 		r_captured = false;
 		r_captured = false;
 	}
 	}
+
 	return OK;
 	return OK;
 }
 }
 
 
@@ -260,6 +344,9 @@ void SceneDebugger::_send_object_id(ObjectID p_id, int p_max_size) {
 		return;
 		return;
 	}
 	}
 
 
+	Node *node = Object::cast_to<Node>(ObjectDB::get_instance(p_id));
+	RuntimeNodeSelect::get_singleton()->_select_node(node);
+
 	Array arr;
 	Array arr;
 	obj.serialize(arr);
 	obj.serialize(arr);
 	EngineDebugger::get_singleton()->send_message("scene:inspect_object", 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);
 	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) {
 void SceneDebugger::add_to_cache(const String &p_filename, Node *p_node) {
 	LiveEditor *debugger = LiveEditor::get_singleton();
 	LiveEditor *debugger = LiveEditor::get_singleton();
 	if (!debugger) {
 	if (!debugger) {
@@ -580,7 +677,6 @@ void SceneDebuggerTree::deserialize(const Array &p_arr) {
 }
 }
 
 
 /// LiveEditor
 /// LiveEditor
-LiveEditor *LiveEditor::singleton = nullptr;
 LiveEditor *LiveEditor::get_singleton() {
 LiveEditor *LiveEditor::get_singleton() {
 	return 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
 #endif

+ 157 - 4
scene/debugger/scene_debugger.h

@@ -31,19 +31,21 @@
 #ifndef SCENE_DEBUGGER_H
 #ifndef SCENE_DEBUGGER_H
 #define 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/object/ref_counted.h"
 #include "core/string/ustring.h"
 #include "core/string/ustring.h"
 #include "core/templates/pair.h"
 #include "core/templates/pair.h"
 #include "core/variant/array.h"
 #include "core/variant/array.h"
+#include "scene/gui/view_panner.h"
+#include "scene/resources/mesh.h"
 
 
+class PopupMenu;
 class Script;
 class Script;
 class Node;
 class Node;
 
 
 class SceneDebugger {
 class SceneDebugger {
-public:
 private:
 private:
-	static SceneDebugger *singleton;
+	inline static SceneDebugger *singleton = nullptr;
 
 
 	SceneDebugger();
 	SceneDebugger();
 
 
@@ -59,6 +61,7 @@ private:
 	static void _set_node_owner_recursive(Node *p_node, Node *p_owner);
 	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 _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 _send_object_id(ObjectID p_id, int p_max_size = 1 << 20);
+	static void _next_frame();
 
 
 public:
 public:
 	static Error parse_message(void *p_user, const String &p_msg, const Array &p_args, bool &r_captured);
 	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");
 		live_edit_root = NodePath("/root");
 	}
 	}
 
 
-	static LiveEditor *singleton;
+	inline static LiveEditor *singleton = nullptr;
 
 
 public:
 public:
 	static LiveEditor *get_singleton();
 	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
 
 
 #endif // SCENE_DEBUGGER_H
 #endif // SCENE_DEBUGGER_H

+ 8 - 0
scene/gui/video_stream_player.cpp

@@ -178,6 +178,7 @@ void VideoStreamPlayer::_notification(int p_notification) {
 			draw_texture_rect(texture, Rect2(Point2(), s), false);
 			draw_texture_rect(texture, Rect2(Point2(), s), false);
 		} break;
 		} break;
 
 
+		case NOTIFICATION_SUSPENDED:
 		case NOTIFICATION_PAUSED: {
 		case NOTIFICATION_PAUSED: {
 			if (is_playing() && !is_paused()) {
 			if (is_playing() && !is_paused()) {
 				paused_from_tree = true;
 				paused_from_tree = true;
@@ -189,6 +190,13 @@ void VideoStreamPlayer::_notification(int p_notification) {
 			}
 			}
 		} break;
 		} break;
 
 
+		case NOTIFICATION_UNSUSPENDED: {
+			if (get_tree()->is_paused()) {
+				break;
+			}
+			[[fallthrough]];
+		}
+
 		case NOTIFICATION_UNPAUSED: {
 		case NOTIFICATION_UNPAUSED: {
 			if (paused_from_tree) {
 			if (paused_from_tree) {
 				paused_from_tree = false;
 				paused_from_tree = false;

+ 12 - 1
scene/main/node.cpp

@@ -184,6 +184,7 @@ void Node::_notification(int p_notification) {
 			}
 			}
 		} break;
 		} break;
 
 
+		case NOTIFICATION_SUSPENDED:
 		case NOTIFICATION_PAUSED: {
 		case NOTIFICATION_PAUSED: {
 			if (is_physics_interpolated_and_enabled() && is_inside_tree()) {
 			if (is_physics_interpolated_and_enabled() && is_inside_tree()) {
 				reset_physics_interpolation();
 				reset_physics_interpolation();
@@ -695,6 +696,16 @@ void Node::_propagate_pause_notification(bool p_enable) {
 	data.blocked--;
 	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 {
 Node::ProcessMode Node::get_process_mode() const {
 	return data.process_mode;
 	return data.process_mode;
 }
 }
@@ -850,7 +861,7 @@ bool Node::can_process_notification(int p_what) const {
 
 
 bool Node::can_process() const {
 bool Node::can_process() const {
 	ERR_FAIL_COND_V(!is_inside_tree(), false);
 	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 {
 bool Node::_can_process(bool p_paused) const {

+ 3 - 0
scene/main/node.h

@@ -300,6 +300,7 @@ private:
 
 
 	void _set_tree(SceneTree *p_tree);
 	void _set_tree(SceneTree *p_tree);
 	void _propagate_pause_notification(bool p_enable);
 	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 _can_process(bool p_paused) const;
 	_FORCE_INLINE_ bool _is_enabled() const;
 	_FORCE_INLINE_ bool _is_enabled() const;
@@ -439,6 +440,8 @@ public:
 		// Editor specific node notifications
 		// Editor specific node notifications
 		NOTIFICATION_EDITOR_PRE_SAVE = 9001,
 		NOTIFICATION_EDITOR_PRE_SAVE = 9001,
 		NOTIFICATION_EDITOR_POST_SAVE = 9002,
 		NOTIFICATION_EDITOR_POST_SAVE = 9002,
+		NOTIFICATION_SUSPENDED = 9003,
+		NOTIFICATION_UNSUSPENDED = 9004
 	};
 	};
 
 
 	/* NODE/TREE */
 	/* NODE/TREE */

+ 27 - 0
scene/main/scene_tree.cpp

@@ -954,11 +954,14 @@ Ref<ArrayMesh> SceneTree::get_debug_contact_mesh() {
 
 
 void SceneTree::set_pause(bool p_enabled) {
 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(!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) {
 	if (p_enabled == paused) {
 		return;
 		return;
 	}
 	}
+
 	paused = p_enabled;
 	paused = p_enabled;
+
 #ifndef _3D_DISABLED
 #ifndef _3D_DISABLED
 	PhysicsServer3D::get_singleton()->set_active(!p_enabled);
 	PhysicsServer3D::get_singleton()->set_active(!p_enabled);
 #endif // _3D_DISABLED
 #endif // _3D_DISABLED
@@ -972,6 +975,30 @@ bool SceneTree::is_paused() const {
 	return paused;
 	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) {
 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
 	// 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.
 	// if any node is removed, this needs to continue working.

+ 3 - 0
scene/main/scene_tree.h

@@ -143,6 +143,7 @@ private:
 	bool debug_navigation_hint = false;
 	bool debug_navigation_hint = false;
 #endif
 #endif
 	bool paused = false;
 	bool paused = false;
+	bool suspended = false;
 
 
 	HashMap<StringName, Group> group_map;
 	HashMap<StringName, Group> group_map;
 	bool _quit = false;
 	bool _quit = false;
@@ -343,6 +344,8 @@ public:
 
 
 	void set_pause(bool p_enabled);
 	void set_pause(bool p_enabled);
 	bool is_paused() const;
 	bool is_paused() const;
+	void set_suspend(bool p_enabled);
+	bool is_suspended() const;
 
 
 #ifdef DEBUG_ENABLED
 #ifdef DEBUG_ENABLED
 	void set_debug_collisions_hint(bool p_enabled);
 	void set_debug_collisions_hint(bool p_enabled);

+ 80 - 3
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(!is_inside_tree());
 	ERR_FAIL_COND(p_event.is_null());
 	ERR_FAIL_COND(p_event.is_null());
 
 
-	if (disable_input) {
+	if (disable_input || disable_input_override) {
 		return;
 		return;
 	}
 	}
 
 
@@ -3195,7 +3195,7 @@ void Viewport::push_unhandled_input(const Ref<InputEvent> &p_event, bool p_local
 
 
 	local_input_handled = false;
 	local_input_handled = false;
 
 
-	if (disable_input || !_can_consume_input_events()) {
+	if (disable_input || disable_input_override || !_can_consume_input_events()) {
 		return;
 		return;
 	}
 	}
 
 
@@ -3298,7 +3298,7 @@ void Viewport::set_disable_input(bool p_disable) {
 	if (p_disable == disable_input) {
 	if (p_disable == disable_input) {
 		return;
 		return;
 	}
 	}
-	if (p_disable) {
+	if (p_disable && !disable_input_override) {
 		_drop_mouse_focus();
 		_drop_mouse_focus();
 		_mouse_leave_viewport();
 		_mouse_leave_viewport();
 		_gui_cancel_tooltip();
 		_gui_cancel_tooltip();
@@ -3311,6 +3311,19 @@ bool Viewport::is_input_disabled() const {
 	return disable_input;
 	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 {
 Variant Viewport::gui_get_drag_data() const {
 	ERR_READ_THREAD_GUARD_V(Variant());
 	ERR_READ_THREAD_GUARD_V(Variant());
 	return get_section_root_viewport()->gui.drag_data;
 	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) {
 void Viewport::set_disable_3d(bool p_disable) {
 	ERR_MAIN_THREAD_GUARD;
 	ERR_MAIN_THREAD_GUARD;
 	disable_3d = p_disable;
 	disable_3d = p_disable;
@@ -4270,6 +4299,54 @@ Transform3D Viewport::get_camera_3d_override_transform() const {
 	return Transform3D();
 	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 {
 Ref<World3D> Viewport::get_world_3d() const {
 	ERR_READ_THREAD_GUARD_V(Ref<World3D>());
 	ERR_READ_THREAD_GUARD_V(Ref<World3D>());
 	return world_3d;
 	return world_3d;

+ 8 - 0
scene/main/viewport.h

@@ -401,6 +401,7 @@ private:
 	DefaultCanvasItemTextureRepeat default_canvas_item_texture_repeat = DEFAULT_CANVAS_ITEM_TEXTURE_REPEAT_DISABLED;
 	DefaultCanvasItemTextureRepeat default_canvas_item_texture_repeat = DEFAULT_CANVAS_ITEM_TEXTURE_REPEAT_DISABLED;
 
 
 	bool disable_input = false;
 	bool disable_input = false;
+	bool disable_input_override = false;
 
 
 	void _gui_call_input(Control *p_control, const Ref<InputEvent> &p_input);
 	void _gui_call_input(Control *p_control, const Ref<InputEvent> &p_input);
 	void _gui_call_notification(Control *p_control, int p_what);
 	void _gui_call_notification(Control *p_control, int p_what);
@@ -580,6 +581,8 @@ public:
 	void set_disable_input(bool p_disable);
 	void set_disable_input(bool p_disable);
 	bool is_input_disabled() const;
 	bool is_input_disabled() const;
 
 
+	void set_disable_input_override(bool p_disable);
+
 	Vector2 get_mouse_position() const;
 	Vector2 get_mouse_position() const;
 	void warp_mouse(const Vector2 &p_position);
 	void warp_mouse(const Vector2 &p_position);
 	virtual void update_mouse_cursor_state();
 	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_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);
 	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);
 	void set_disable_3d(bool p_disable);
 	bool is_3d_disabled() const;
 	bool is_3d_disabled() const;