Ver Fonte

Add "Game" editor for better runtime debugging

Michael Alexsander há 10 meses atrás
pai
commit
16524a8a01
45 ficheiros alterados com 2375 adições e 271 exclusões
  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 {
+	return freeze_time_scale ? 0 : _time_scale;
+}
+
+double Engine::get_unfrozen_time_scale() const {
 	return _time_scale;
 }
 
@@ -404,6 +408,10 @@ bool Engine::notify_frame_server_synced() {
 	return server_syncs > SERVER_SYNC_FRAME_COUNT_WARNING;
 }
 
+void Engine::set_freeze_time_scale(bool p_frozen) {
+	freeze_time_scale = p_frozen;
+}
+
 Engine::Engine() {
 	singleton = this;
 }

+ 5 - 0
core/config/engine.h

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

+ 107 - 1
core/input/input.cpp

@@ -87,11 +87,50 @@ Input *Input::get_singleton() {
 
 void Input::set_mouse_mode(MouseMode p_mode) {
 	ERR_FAIL_INDEX((int)p_mode, 5);
+
+	if (p_mode == mouse_mode) {
+		return;
+	}
+
+	// Allow to be set even if overridden, to see if the platform allows the mode.
 	set_mouse_mode_func(p_mode);
+	mouse_mode = get_mouse_mode_func();
+
+	if (mouse_mode_override_enabled) {
+		set_mouse_mode_func(mouse_mode_override);
+	}
 }
 
 Input::MouseMode Input::get_mouse_mode() const {
-	return get_mouse_mode_func();
+	return mouse_mode;
+}
+
+void Input::set_mouse_mode_override_enabled(bool p_enabled) {
+	if (p_enabled == mouse_mode_override_enabled) {
+		return;
+	}
+
+	mouse_mode_override_enabled = p_enabled;
+
+	if (p_enabled) {
+		set_mouse_mode_func(mouse_mode_override);
+		mouse_mode_override = get_mouse_mode_func();
+	} else {
+		set_mouse_mode_func(mouse_mode);
+	}
+}
+
+void Input::set_mouse_mode_override(MouseMode p_mode) {
+	ERR_FAIL_INDEX((int)p_mode, 5);
+
+	if (p_mode == mouse_mode_override) {
+		return;
+	}
+
+	if (mouse_mode_override_enabled) {
+		set_mouse_mode_func(p_mode);
+		mouse_mode_override = get_mouse_mode_func();
+	}
 }
 
 void Input::_bind_methods() {
@@ -252,6 +291,10 @@ Input::VelocityTrack::VelocityTrack() {
 bool Input::is_anything_pressed() const {
 	_THREAD_SAFE_METHOD_
 
+	if (disable_input) {
+		return false;
+	}
+
 	if (!keys_pressed.is_empty() || !joy_buttons_pressed.is_empty() || !mouse_button_mask.is_empty()) {
 		return true;
 	}
@@ -267,21 +310,41 @@ bool Input::is_anything_pressed() const {
 
 bool Input::is_key_pressed(Key p_keycode) const {
 	_THREAD_SAFE_METHOD_
+
+	if (disable_input) {
+		return false;
+	}
+
 	return keys_pressed.has(p_keycode);
 }
 
 bool Input::is_physical_key_pressed(Key p_keycode) const {
 	_THREAD_SAFE_METHOD_
+
+	if (disable_input) {
+		return false;
+	}
+
 	return physical_keys_pressed.has(p_keycode);
 }
 
 bool Input::is_key_label_pressed(Key p_keycode) const {
 	_THREAD_SAFE_METHOD_
+
+	if (disable_input) {
+		return false;
+	}
+
 	return key_label_pressed.has(p_keycode);
 }
 
 bool Input::is_mouse_button_pressed(MouseButton p_button) const {
 	_THREAD_SAFE_METHOD_
+
+	if (disable_input) {
+		return false;
+	}
+
 	return mouse_button_mask.has_flag(mouse_button_to_mask(p_button));
 }
 
@@ -295,11 +358,21 @@ static JoyButton _combine_device(JoyButton p_value, int p_device) {
 
 bool Input::is_joy_button_pressed(int p_device, JoyButton p_button) const {
 	_THREAD_SAFE_METHOD_
+
+	if (disable_input) {
+		return false;
+	}
+
 	return joy_buttons_pressed.has(_combine_device(p_button, p_device));
 }
 
 bool Input::is_action_pressed(const StringName &p_action, bool p_exact) const {
 	ERR_FAIL_COND_V_MSG(!InputMap::get_singleton()->has_action(p_action), false, InputMap::get_singleton()->suggest_actions(p_action));
+
+	if (disable_input) {
+		return false;
+	}
+
 	HashMap<StringName, ActionState>::ConstIterator E = action_states.find(p_action);
 	if (!E) {
 		return false;
@@ -310,6 +383,11 @@ bool Input::is_action_pressed(const StringName &p_action, bool p_exact) const {
 
 bool Input::is_action_just_pressed(const StringName &p_action, bool p_exact) const {
 	ERR_FAIL_COND_V_MSG(!InputMap::get_singleton()->has_action(p_action), false, InputMap::get_singleton()->suggest_actions(p_action));
+
+	if (disable_input) {
+		return false;
+	}
+
 	HashMap<StringName, ActionState>::ConstIterator E = action_states.find(p_action);
 	if (!E) {
 		return false;
@@ -331,6 +409,11 @@ bool Input::is_action_just_pressed(const StringName &p_action, bool p_exact) con
 
 bool Input::is_action_just_released(const StringName &p_action, bool p_exact) const {
 	ERR_FAIL_COND_V_MSG(!InputMap::get_singleton()->has_action(p_action), false, InputMap::get_singleton()->suggest_actions(p_action));
+
+	if (disable_input) {
+		return false;
+	}
+
 	HashMap<StringName, ActionState>::ConstIterator E = action_states.find(p_action);
 	if (!E) {
 		return false;
@@ -352,6 +435,11 @@ bool Input::is_action_just_released(const StringName &p_action, bool p_exact) co
 
 float Input::get_action_strength(const StringName &p_action, bool p_exact) const {
 	ERR_FAIL_COND_V_MSG(!InputMap::get_singleton()->has_action(p_action), 0.0, InputMap::get_singleton()->suggest_actions(p_action));
+
+	if (disable_input) {
+		return 0.0f;
+	}
+
 	HashMap<StringName, ActionState>::ConstIterator E = action_states.find(p_action);
 	if (!E) {
 		return 0.0f;
@@ -366,6 +454,11 @@ float Input::get_action_strength(const StringName &p_action, bool p_exact) const
 
 float Input::get_action_raw_strength(const StringName &p_action, bool p_exact) const {
 	ERR_FAIL_COND_V_MSG(!InputMap::get_singleton()->has_action(p_action), 0.0, InputMap::get_singleton()->suggest_actions(p_action));
+
+	if (disable_input) {
+		return 0.0f;
+	}
+
 	HashMap<StringName, ActionState>::ConstIterator E = action_states.find(p_action);
 	if (!E) {
 		return 0.0f;
@@ -410,6 +503,11 @@ Vector2 Input::get_vector(const StringName &p_negative_x, const StringName &p_po
 
 float Input::get_joy_axis(int p_device, JoyAxis p_axis) const {
 	_THREAD_SAFE_METHOD_
+
+	if (disable_input) {
+		return 0;
+	}
+
 	JoyAxis c = _combine_device(p_axis, p_device);
 	if (_joy_axis.has(c)) {
 		return _joy_axis[c];
@@ -1664,6 +1762,14 @@ int Input::get_unused_joy_id() {
 	return -1;
 }
 
+void Input::set_disable_input(bool p_disable) {
+	disable_input = p_disable;
+}
+
+bool Input::is_input_disabled() const {
+	return disable_input;
+}
+
 Input::Input() {
 	singleton = this;
 

+ 10 - 0
core/input/input.h

@@ -103,6 +103,11 @@ private:
 	Vector2 mouse_pos;
 	int64_t mouse_window = 0;
 	bool legacy_just_pressed_behavior = false;
+	bool disable_input = false;
+
+	MouseMode mouse_mode = MOUSE_MODE_VISIBLE;
+	bool mouse_mode_override_enabled = false;
+	MouseMode mouse_mode_override = MOUSE_MODE_VISIBLE;
 
 	struct ActionState {
 		uint64_t pressed_physics_frame = UINT64_MAX;
@@ -279,6 +284,8 @@ protected:
 public:
 	void set_mouse_mode(MouseMode p_mode);
 	MouseMode get_mouse_mode() const;
+	void set_mouse_mode_override_enabled(bool p_enabled);
+	void set_mouse_mode_override(MouseMode p_mode);
 
 #ifdef TOOLS_ENABLED
 	void get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const override;
@@ -380,6 +387,9 @@ public:
 
 	void set_event_dispatch_function(EventDispatchFunc p_function);
 
+	void set_disable_input(bool p_disable);
+	bool is_input_disabled() const;
+
 	Input();
 	~Input();
 };

+ 4 - 1
doc/classes/EditorFeatureProfile.xml

@@ -121,7 +121,10 @@
 		<constant name="FEATURE_HISTORY_DOCK" value="7" enum="Feature">
 			The History dock. If this feature is disabled, the History dock won't be visible.
 		</constant>
-		<constant name="FEATURE_MAX" value="8" enum="Feature">
+		<constant name="FEATURE_GAME" value="8" enum="Feature">
+			The Game tab, which allows embedding the game window and selecting nodes by clicking inside of it. If this feature is disabled, the Game tab won't display.
+		</constant>
+		<constant name="FEATURE_MAX" value="9" enum="Feature">
 			Represents the size of the [enum Feature] enum.
 		</constant>
 	</constants>

+ 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("clear_execution", callable_mp(this, &EditorDebuggerNode::_clear_execution));
 	node->connect("breaked", callable_mp(this, &EditorDebuggerNode::_breaked).bind(id));
+	node->connect("remote_tree_select_requested", callable_mp(this, &EditorDebuggerNode::_remote_tree_select_requested).bind(id));
 	node->connect("remote_tree_updated", callable_mp(this, &EditorDebuggerNode::_remote_tree_updated).bind(id));
 	node->connect("remote_object_updated", callable_mp(this, &EditorDebuggerNode::_remote_object_updated).bind(id));
 	node->connect("remote_object_property_updated", callable_mp(this, &EditorDebuggerNode::_remote_object_property_updated).bind(id));
@@ -637,6 +638,13 @@ void EditorDebuggerNode::request_remote_tree() {
 	get_current_debugger()->request_remote_tree();
 }
 
+void EditorDebuggerNode::_remote_tree_select_requested(ObjectID p_id, int p_debugger) {
+	if (p_debugger != tabs->get_current_tab()) {
+		return;
+	}
+	remote_scene_tree->select_node(p_id);
+}
+
 void EditorDebuggerNode::_remote_tree_updated(int p_debugger) {
 	if (p_debugger != tabs->get_current_tab()) {
 		return;

+ 3 - 5
editor/debugger/editor_debugger_node.h

@@ -51,11 +51,8 @@ class EditorDebuggerNode : public MarginContainer {
 public:
 	enum CameraOverride {
 		OVERRIDE_NONE,
-		OVERRIDE_2D,
-		OVERRIDE_3D_1, // 3D Viewport 1
-		OVERRIDE_3D_2, // 3D Viewport 2
-		OVERRIDE_3D_3, // 3D Viewport 3
-		OVERRIDE_3D_4 // 3D Viewport 4
+		OVERRIDE_INGAME,
+		OVERRIDE_EDITORS,
 	};
 
 private:
@@ -132,6 +129,7 @@ protected:
 	void _debugger_stopped(int p_id);
 	void _debugger_wants_stop(int p_id);
 	void _debugger_changed(int p_tab);
+	void _remote_tree_select_requested(ObjectID p_id, int p_debugger);
 	void _remote_tree_updated(int p_debugger);
 	void _remote_tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button);
 	void _remote_object_updated(ObjectID p_id, int p_debugger);

+ 33 - 5
editor/debugger/editor_debugger_tree.cpp

@@ -30,6 +30,7 @@
 
 #include "editor_debugger_tree.h"
 
+#include "editor/debugger/editor_debugger_node.h"
 #include "editor/editor_node.h"
 #include "editor/editor_string_names.h"
 #include "editor/gui/editor_file_dialog.h"
@@ -148,7 +149,8 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int
 	updating_scene_tree = true;
 	const String last_path = get_selected_path();
 	const String filter = SceneTreeDock::get_singleton()->get_filter();
-	bool filter_changed = filter != last_filter;
+	bool should_scroll = scrolling_to_item || filter != last_filter;
+	scrolling_to_item = false;
 	TreeItem *scroll_item = nullptr;
 
 	// Nodes are in a flatten list, depth first. Use a stack of parents, avoid recursion.
@@ -185,8 +187,18 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int
 		// Select previously selected node.
 		if (debugger_id == p_debugger) { // Can use remote id.
 			if (node.id == inspected_object_id) {
+				if (selection_uncollapse_all) {
+					selection_uncollapse_all = false;
+
+					// Temporarily set to `false`, to allow caching the unfolds.
+					updating_scene_tree = false;
+					item->uncollapse_tree();
+					updating_scene_tree = true;
+				}
+
 				item->select(0);
-				if (filter_changed) {
+
+				if (should_scroll) {
 					scroll_item = item;
 				}
 			}
@@ -194,7 +206,7 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int
 			if (last_path == _get_path(item)) {
 				updating_scene_tree = false; // Force emission of new selection.
 				item->select(0);
-				if (filter_changed) {
+				if (should_scroll) {
 					scroll_item = item;
 				}
 				updating_scene_tree = true;
@@ -258,14 +270,30 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int
 			}
 		}
 	}
-	debugger_id = p_debugger; // Needed by hook, could be avoided if every debugger had its own tree
+
+	debugger_id = p_debugger; // Needed by hook, could be avoided if every debugger had its own tree.
 	if (scroll_item) {
-		callable_mp((Tree *)this, &Tree::scroll_to_item).call_deferred(scroll_item, false);
+		scroll_to_item(scroll_item, false);
 	}
 	last_filter = filter;
 	updating_scene_tree = false;
 }
 
+void EditorDebuggerTree::select_node(ObjectID p_id) {
+	// Manually select, as the tree control may be out-of-date for some reason (e.g. not shown yet).
+	selection_uncollapse_all = true;
+	inspected_object_id = uint64_t(p_id);
+	scrolling_to_item = true;
+	emit_signal(SNAME("object_selected"), inspected_object_id, debugger_id);
+
+	if (!updating_scene_tree) {
+		// Request a tree refresh.
+		EditorDebuggerNode::get_singleton()->request_remote_tree();
+	}
+	// Set the value immediately, so no update flooding happens and causes a crash.
+	updating_scene_tree = true;
+}
+
 Variant EditorDebuggerTree::get_drag_data(const Point2 &p_point) {
 	if (get_button_id_at_position(p_point) != -1) {
 		return Variant();

+ 3 - 0
editor/debugger/editor_debugger_tree.h

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

+ 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") {
 		emit_signal(SNAME("stop_requested"));
 		_stop_and_notify();
+	} else if (p_msg == "remote_node_clicked") {
+		if (!p_data.is_empty()) {
+			emit_signal(SNAME("remote_tree_select_requested"), p_data[0]);
+		}
 	} else if (p_msg == "performance:profile_names") {
 		Vector<StringName> monitors;
 		monitors.resize(p_data.size());
@@ -905,37 +909,42 @@ void ScriptEditorDebugger::_notification(int p_what) {
 			if (is_session_active()) {
 				peer->poll();
 
-				if (camera_override == CameraOverride::OVERRIDE_2D) {
-					Dictionary state = CanvasItemEditor::get_singleton()->get_state();
-					float zoom = state["zoom"];
-					Point2 offset = state["ofs"];
-					Transform2D transform;
-
-					transform.scale_basis(Size2(zoom, zoom));
-					transform.columns[2] = -offset * zoom;
-
-					Array msg;
-					msg.push_back(transform);
-					_put_msg("scene:override_camera_2D:transform", msg);
-
-				} else if (camera_override >= CameraOverride::OVERRIDE_3D_1) {
-					int viewport_idx = camera_override - CameraOverride::OVERRIDE_3D_1;
-					Node3DEditorViewport *viewport = Node3DEditor::get_singleton()->get_editor_viewport(viewport_idx);
-					Camera3D *const cam = viewport->get_camera_3d();
-
-					Array msg;
-					msg.push_back(cam->get_camera_transform());
-					if (cam->get_projection() == Camera3D::PROJECTION_ORTHOGONAL) {
-						msg.push_back(false);
-						msg.push_back(cam->get_size());
-					} else {
-						msg.push_back(true);
-						msg.push_back(cam->get_fov());
+				if (camera_override == CameraOverride::OVERRIDE_EDITORS) {
+					// CanvasItem Editor
+					{
+						Dictionary state = CanvasItemEditor::get_singleton()->get_state();
+						float zoom = state["zoom"];
+						Point2 offset = state["ofs"];
+						Transform2D transform;
+
+						transform.scale_basis(Size2(zoom, zoom));
+						transform.columns[2] = -offset * zoom;
+
+						Array msg;
+						msg.push_back(transform);
+						_put_msg("scene:transform_camera_2d", msg);
+					}
+
+					// Node3D Editor
+					{
+						Node3DEditorViewport *viewport = Node3DEditor::get_singleton()->get_last_used_viewport();
+						const Camera3D *cam = viewport->get_camera_3d();
+
+						Array msg;
+						msg.push_back(cam->get_camera_transform());
+						if (cam->get_projection() == Camera3D::PROJECTION_ORTHOGONAL) {
+							msg.push_back(false);
+							msg.push_back(cam->get_size());
+						} else {
+							msg.push_back(true);
+							msg.push_back(cam->get_fov());
+						}
+						msg.push_back(cam->get_near());
+						msg.push_back(cam->get_far());
+						_put_msg("scene:transform_camera_3d", msg);
 					}
-					msg.push_back(cam->get_near());
-					msg.push_back(cam->get_far());
-					_put_msg("scene:override_camera_3D:transform", msg);
 				}
+
 				if (is_breaked() && can_request_idle_draw) {
 					_put_msg("servers:draw", Array());
 					can_request_idle_draw = false;
@@ -1469,23 +1478,10 @@ CameraOverride ScriptEditorDebugger::get_camera_override() const {
 }
 
 void ScriptEditorDebugger::set_camera_override(CameraOverride p_override) {
-	if (p_override == CameraOverride::OVERRIDE_2D && camera_override != CameraOverride::OVERRIDE_2D) {
-		Array msg;
-		msg.push_back(true);
-		_put_msg("scene:override_camera_2D:set", msg);
-	} else if (p_override != CameraOverride::OVERRIDE_2D && camera_override == CameraOverride::OVERRIDE_2D) {
-		Array msg;
-		msg.push_back(false);
-		_put_msg("scene:override_camera_2D:set", msg);
-	} else if (p_override >= CameraOverride::OVERRIDE_3D_1 && camera_override < CameraOverride::OVERRIDE_3D_1) {
-		Array msg;
-		msg.push_back(true);
-		_put_msg("scene:override_camera_3D:set", msg);
-	} else if (p_override < CameraOverride::OVERRIDE_3D_1 && camera_override >= CameraOverride::OVERRIDE_3D_1) {
-		Array msg;
-		msg.push_back(false);
-		_put_msg("scene:override_camera_3D:set", msg);
-	}
+	Array msg;
+	msg.push_back(p_override != CameraOverride::OVERRIDE_NONE);
+	msg.push_back(p_override == CameraOverride::OVERRIDE_EDITORS);
+	_put_msg("scene:override_cameras", msg);
 
 	camera_override = p_override;
 }
@@ -1776,6 +1772,7 @@ void ScriptEditorDebugger::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("remote_object_updated", PropertyInfo(Variant::INT, "id")));
 	ADD_SIGNAL(MethodInfo("remote_object_property_updated", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::STRING, "property")));
 	ADD_SIGNAL(MethodInfo("remote_tree_updated"));
+	ADD_SIGNAL(MethodInfo("remote_tree_select_requested", PropertyInfo(Variant::NODE_PATH, "path")));
 	ADD_SIGNAL(MethodInfo("output", PropertyInfo(Variant::STRING, "msg"), PropertyInfo(Variant::INT, "level")));
 	ADD_SIGNAL(MethodInfo("stack_dump", PropertyInfo(Variant::ARRAY, "stack_dump")));
 	ADD_SIGNAL(MethodInfo("stack_frame_vars", PropertyInfo(Variant::INT, "num_vars")));

+ 4 - 0
editor/editor_feature_profile.cpp

@@ -43,6 +43,7 @@
 const char *EditorFeatureProfile::feature_names[FEATURE_MAX] = {
 	TTRC("3D Editor"),
 	TTRC("Script Editor"),
+	TTRC("Game View"),
 	TTRC("Asset Library"),
 	TTRC("Scene Tree Editing"),
 	TTRC("Node Dock"),
@@ -54,6 +55,7 @@ const char *EditorFeatureProfile::feature_names[FEATURE_MAX] = {
 const char *EditorFeatureProfile::feature_descriptions[FEATURE_MAX] = {
 	TTRC("Allows to view and edit 3D scenes."),
 	TTRC("Allows to edit scripts using the integrated script editor."),
+	TTRC("Provides tools for selecting and debugging nodes at runtime."),
 	TTRC("Provides built-in access to the Asset Library."),
 	TTRC("Allows editing the node hierarchy in the Scene dock."),
 	TTRC("Allows to work with signals and groups of the node selected in the Scene dock."),
@@ -65,6 +67,7 @@ const char *EditorFeatureProfile::feature_descriptions[FEATURE_MAX] = {
 const char *EditorFeatureProfile::feature_identifiers[FEATURE_MAX] = {
 	"3d",
 	"script",
+	"game",
 	"asset_lib",
 	"scene_tree",
 	"node_dock",
@@ -307,6 +310,7 @@ void EditorFeatureProfile::_bind_methods() {
 	BIND_ENUM_CONSTANT(FEATURE_FILESYSTEM_DOCK);
 	BIND_ENUM_CONSTANT(FEATURE_IMPORT_DOCK);
 	BIND_ENUM_CONSTANT(FEATURE_HISTORY_DOCK);
+	BIND_ENUM_CONSTANT(FEATURE_GAME);
 	BIND_ENUM_CONSTANT(FEATURE_MAX);
 }
 

+ 1 - 0
editor/editor_feature_profile.h

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

+ 1 - 0
editor/editor_main_screen.h

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

+ 10 - 2
editor/editor_node.cpp

@@ -145,6 +145,7 @@
 #include "editor/plugins/editor_plugin.h"
 #include "editor/plugins/editor_preview_plugins.h"
 #include "editor/plugins/editor_resource_conversion_plugin.h"
+#include "editor/plugins/game_view_plugin.h"
 #include "editor/plugins/gdextension_export_plugin.h"
 #include "editor/plugins/material_editor_plugin.h"
 #include "editor/plugins/mesh_library_editor_plugin.h"
@@ -357,6 +358,8 @@ void EditorNode::shortcut_input(const Ref<InputEvent> &p_event) {
 			editor_main_screen->select(EditorMainScreen::EDITOR_3D);
 		} else if (ED_IS_SHORTCUT("editor/editor_script", p_event)) {
 			editor_main_screen->select(EditorMainScreen::EDITOR_SCRIPT);
+		} else if (ED_IS_SHORTCUT("editor/editor_game", p_event)) {
+			editor_main_screen->select(EditorMainScreen::EDITOR_GAME);
 		} else if (ED_IS_SHORTCUT("editor/editor_help", p_event)) {
 			emit_signal(SNAME("request_help_search"), "");
 		} else if (ED_IS_SHORTCUT("editor/editor_assetlib", p_event) && AssetLibraryEditorPlugin::is_available()) {
@@ -6577,6 +6580,7 @@ void EditorNode::_feature_profile_changed() {
 
 		editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_3D, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_3D));
 		editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_SCRIPT, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_SCRIPT));
+		editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_GAME, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_GAME));
 		if (AssetLibraryEditorPlugin::is_available()) {
 			editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_ASSETLIB, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_ASSET_LIB));
 		}
@@ -6587,6 +6591,7 @@ void EditorNode::_feature_profile_changed() {
 		editor_dock_manager->set_dock_enabled(history_dock, true);
 		editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_3D, true);
 		editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_SCRIPT, true);
+		editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_GAME, true);
 		if (AssetLibraryEditorPlugin::is_available()) {
 			editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_ASSETLIB, true);
 		}
@@ -7714,6 +7719,7 @@ EditorNode::EditorNode() {
 	add_editor_plugin(memnew(CanvasItemEditorPlugin));
 	add_editor_plugin(memnew(Node3DEditorPlugin));
 	add_editor_plugin(memnew(ScriptEditorPlugin));
+	add_editor_plugin(memnew(GameViewPlugin));
 
 	EditorAudioBuses *audio_bus_editor = EditorAudioBuses::register_editor();
 
@@ -7896,12 +7902,14 @@ EditorNode::EditorNode() {
 	ED_SHORTCUT_AND_COMMAND("editor/editor_2d", TTR("Open 2D Editor"), KeyModifierMask::CTRL | Key::F1);
 	ED_SHORTCUT_AND_COMMAND("editor/editor_3d", TTR("Open 3D Editor"), KeyModifierMask::CTRL | Key::F2);
 	ED_SHORTCUT_AND_COMMAND("editor/editor_script", TTR("Open Script Editor"), KeyModifierMask::CTRL | Key::F3);
-	ED_SHORTCUT_AND_COMMAND("editor/editor_assetlib", TTR("Open Asset Library"), KeyModifierMask::CTRL | Key::F4);
+	ED_SHORTCUT_AND_COMMAND("editor/editor_game", TTR("Open Game View"), KeyModifierMask::CTRL | Key::F4);
+	ED_SHORTCUT_AND_COMMAND("editor/editor_assetlib", TTR("Open Asset Library"), KeyModifierMask::CTRL | Key::F5);
 
 	ED_SHORTCUT_OVERRIDE("editor/editor_2d", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_1);
 	ED_SHORTCUT_OVERRIDE("editor/editor_3d", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_2);
 	ED_SHORTCUT_OVERRIDE("editor/editor_script", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_3);
-	ED_SHORTCUT_OVERRIDE("editor/editor_assetlib", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_4);
+	ED_SHORTCUT_OVERRIDE("editor/editor_game", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_4);
+	ED_SHORTCUT_OVERRIDE("editor/editor_assetlib", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_5);
 
 	ED_SHORTCUT_AND_COMMAND("editor/editor_next", TTR("Open the next Editor"));
 	ED_SHORTCUT_AND_COMMAND("editor/editor_prev", TTR("Open the previous Editor"));

+ 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")));
 	snap_config_menu->set_button_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl")));
 	skeleton_menu->set_button_icon(get_editor_theme_icon(SNAME("Bone")));
-	override_camera_button->set_button_icon(get_editor_theme_icon(SNAME("Camera2D")));
 	pan_button->set_button_icon(get_editor_theme_icon(SNAME("ToolPan")));
 	ruler_button->set_button_icon(get_editor_theme_icon(SNAME("Ruler")));
 	pivot_button->set_button_icon(get_editor_theme_icon(SNAME("EditPivot")));
@@ -4016,8 +4015,6 @@ void CanvasItemEditor::_notification(int p_what) {
 		case NOTIFICATION_READY: {
 			_update_lock_and_group_button();
 
-			EditorRunBar::get_singleton()->connect("play_pressed", callable_mp(this, &CanvasItemEditor::_update_override_camera_button).bind(true));
-			EditorRunBar::get_singleton()->connect("stop_pressed", callable_mp(this, &CanvasItemEditor::_update_override_camera_button).bind(false));
 			ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &CanvasItemEditor::_project_settings_changed));
 		} break;
 
@@ -4116,15 +4113,6 @@ void CanvasItemEditor::_notification(int p_what) {
 			_update_editor_settings();
 		} break;
 
-		case NOTIFICATION_VISIBILITY_CHANGED: {
-			if (!is_visible() && override_camera_button->is_pressed()) {
-				EditorDebuggerNode *debugger = EditorDebuggerNode::get_singleton();
-
-				debugger->set_camera_override(EditorDebuggerNode::OVERRIDE_NONE);
-				override_camera_button->set_pressed(false);
-			}
-		} break;
-
 		case NOTIFICATION_APPLICATION_FOCUS_OUT:
 		case NOTIFICATION_WM_WINDOW_FOCUS_OUT: {
 			if (drag_type != DRAG_NONE) {
@@ -4282,16 +4270,6 @@ void CanvasItemEditor::_button_toggle_grid_snap(bool p_status) {
 	viewport->queue_redraw();
 }
 
-void CanvasItemEditor::_button_override_camera(bool p_pressed) {
-	EditorDebuggerNode *debugger = EditorDebuggerNode::get_singleton();
-
-	if (p_pressed) {
-		debugger->set_camera_override(EditorDebuggerNode::OVERRIDE_2D);
-	} else {
-		debugger->set_camera_override(EditorDebuggerNode::OVERRIDE_NONE);
-	}
-}
-
 void CanvasItemEditor::_button_tool_select(int p_index) {
 	Button *tb[TOOL_MAX] = { select_button, list_select_button, move_button, scale_button, rotate_button, pivot_button, pan_button, ruler_button };
 	for (int i = 0; i < TOOL_MAX; i++) {
@@ -4398,17 +4376,6 @@ void CanvasItemEditor::_insert_animation_keys(bool p_location, bool p_rotation,
 	te->commit_insert_queue();
 }
 
-void CanvasItemEditor::_update_override_camera_button(bool p_game_running) {
-	if (p_game_running) {
-		override_camera_button->set_disabled(false);
-		override_camera_button->set_tooltip_text(TTR("Project Camera Override\nOverrides the running project's camera with the editor viewport camera."));
-	} else {
-		override_camera_button->set_disabled(true);
-		override_camera_button->set_pressed(false);
-		override_camera_button->set_tooltip_text(TTR("Project Camera Override\nNo project instance running. Run the project from the editor to use this feature."));
-	}
-}
-
 void CanvasItemEditor::_popup_callback(int p_op) {
 	EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
 	last_option = MenuOption(p_op);
@@ -5514,16 +5481,6 @@ CanvasItemEditor::CanvasItemEditor() {
 
 	main_menu_hbox->add_child(memnew(VSeparator));
 
-	override_camera_button = memnew(Button);
-	override_camera_button->set_theme_type_variation("FlatButton");
-	main_menu_hbox->add_child(override_camera_button);
-	override_camera_button->connect(SceneStringName(toggled), callable_mp(this, &CanvasItemEditor::_button_override_camera));
-	override_camera_button->set_toggle_mode(true);
-	override_camera_button->set_disabled(true);
-	_update_override_camera_button(false);
-
-	main_menu_hbox->add_child(memnew(VSeparator));
-
 	view_menu = memnew(MenuButton);
 	view_menu->set_flat(false);
 	view_menu->set_theme_type_variation("FlatMenuButton");

+ 0 - 4
editor/plugins/canvas_item_editor_plugin.h

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

+ 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;
 
 	if (b.is_valid()) {
-		emit_signal(SNAME("clicked"), this);
+		emit_signal(SNAME("clicked"));
 
 		ViewportNavMouseButton orbit_mouse_preference = (ViewportNavMouseButton)EDITOR_GET("editors/3d/navigation/orbit_mouse_button").operator int();
 		ViewportNavMouseButton pan_mouse_preference = (ViewportNavMouseButton)EDITOR_GET("editors/3d/navigation/pan_mouse_button").operator int();
@@ -4210,7 +4210,7 @@ Dictionary Node3DEditorViewport::get_state() const {
 
 void Node3DEditorViewport::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("toggle_maximize_view", PropertyInfo(Variant::OBJECT, "viewport")));
-	ADD_SIGNAL(MethodInfo("clicked", PropertyInfo(Variant::OBJECT, "viewport")));
+	ADD_SIGNAL(MethodInfo("clicked"));
 }
 
 void Node3DEditorViewport::reset() {
@@ -6572,18 +6572,6 @@ void Node3DEditor::_menu_item_toggled(bool pressed, int p_option) {
 			tool_option_button[TOOL_OPT_USE_SNAP]->set_pressed(pressed);
 			snap_enabled = pressed;
 		} break;
-
-		case MENU_TOOL_OVERRIDE_CAMERA: {
-			EditorDebuggerNode *const debugger = EditorDebuggerNode::get_singleton();
-
-			using Override = EditorDebuggerNode::CameraOverride;
-			if (pressed) {
-				debugger->set_camera_override((Override)(Override::OVERRIDE_3D_1 + camera_override_viewport_id));
-			} else {
-				debugger->set_camera_override(Override::OVERRIDE_NONE);
-			}
-
-		} break;
 	}
 }
 
@@ -6610,36 +6598,6 @@ void Node3DEditor::_menu_gizmo_toggled(int p_option) {
 	update_all_gizmos();
 }
 
-void Node3DEditor::_update_camera_override_button(bool p_game_running) {
-	Button *const button = tool_option_button[TOOL_OPT_OVERRIDE_CAMERA];
-
-	if (p_game_running) {
-		button->set_disabled(false);
-		button->set_tooltip_text(TTR("Project Camera Override\nOverrides the running project's camera with the editor viewport camera."));
-	} else {
-		button->set_disabled(true);
-		button->set_pressed(false);
-		button->set_tooltip_text(TTR("Project Camera Override\nNo project instance running. Run the project from the editor to use this feature."));
-	}
-}
-
-void Node3DEditor::_update_camera_override_viewport(Object *p_viewport) {
-	Node3DEditorViewport *current_viewport = Object::cast_to<Node3DEditorViewport>(p_viewport);
-
-	if (!current_viewport) {
-		return;
-	}
-
-	EditorDebuggerNode *const debugger = EditorDebuggerNode::get_singleton();
-
-	camera_override_viewport_id = current_viewport->index;
-	if (debugger->get_camera_override() >= EditorDebuggerNode::OVERRIDE_3D_1) {
-		using Override = EditorDebuggerNode::CameraOverride;
-
-		debugger->set_camera_override((Override)(Override::OVERRIDE_3D_1 + camera_override_viewport_id));
-	}
-}
-
 void Node3DEditor::_menu_item_pressed(int p_option) {
 	EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
 	switch (p_option) {
@@ -6670,6 +6628,9 @@ void Node3DEditor::_menu_item_pressed(int p_option) {
 		} break;
 		case MENU_VIEW_USE_1_VIEWPORT: {
 			viewport_base->set_view(Node3DEditorViewportContainer::VIEW_USE_1_VIEWPORT);
+			if (last_used_viewport > 0) {
+				last_used_viewport = 0;
+			}
 
 			view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), true);
 			view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), false);
@@ -6681,6 +6642,9 @@ void Node3DEditor::_menu_item_pressed(int p_option) {
 		} break;
 		case MENU_VIEW_USE_2_VIEWPORTS: {
 			viewport_base->set_view(Node3DEditorViewportContainer::VIEW_USE_2_VIEWPORTS);
+			if (last_used_viewport > 1) {
+				last_used_viewport = 0;
+			}
 
 			view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), false);
 			view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), true);
@@ -6692,6 +6656,9 @@ void Node3DEditor::_menu_item_pressed(int p_option) {
 		} break;
 		case MENU_VIEW_USE_2_VIEWPORTS_ALT: {
 			viewport_base->set_view(Node3DEditorViewportContainer::VIEW_USE_2_VIEWPORTS_ALT);
+			if (last_used_viewport > 1) {
+				last_used_viewport = 0;
+			}
 
 			view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), false);
 			view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), false);
@@ -6703,6 +6670,9 @@ void Node3DEditor::_menu_item_pressed(int p_option) {
 		} break;
 		case MENU_VIEW_USE_3_VIEWPORTS: {
 			viewport_base->set_view(Node3DEditorViewportContainer::VIEW_USE_3_VIEWPORTS);
+			if (last_used_viewport > 2) {
+				last_used_viewport = 0;
+			}
 
 			view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), false);
 			view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), false);
@@ -6714,6 +6684,9 @@ void Node3DEditor::_menu_item_pressed(int p_option) {
 		} break;
 		case MENU_VIEW_USE_3_VIEWPORTS_ALT: {
 			viewport_base->set_view(Node3DEditorViewportContainer::VIEW_USE_3_VIEWPORTS_ALT);
+			if (last_used_viewport > 2) {
+				last_used_viewport = 0;
+			}
 
 			view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), false);
 			view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), false);
@@ -8033,7 +8006,6 @@ void Node3DEditor::_update_theme() {
 
 	tool_option_button[TOOL_OPT_LOCAL_COORDS]->set_button_icon(get_editor_theme_icon(SNAME("Object")));
 	tool_option_button[TOOL_OPT_USE_SNAP]->set_button_icon(get_editor_theme_icon(SNAME("Snap")));
-	tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->set_button_icon(get_editor_theme_icon(SNAME("Camera3D")));
 
 	view_menu->get_popup()->set_item_icon(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), get_editor_theme_icon(SNAME("Panels1")));
 	view_menu->get_popup()->set_item_icon(view_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), get_editor_theme_icon(SNAME("Panels2")));
@@ -8068,9 +8040,6 @@ void Node3DEditor::_notification(int p_what) {
 			SceneTreeDock::get_singleton()->get_tree_editor()->connect("node_changed", callable_mp(this, &Node3DEditor::_refresh_menu_icons));
 			editor_selection->connect("selection_changed", callable_mp(this, &Node3DEditor::_selection_changed));
 
-			EditorRunBar::get_singleton()->connect("stop_pressed", callable_mp(this, &Node3DEditor::_update_camera_override_button).bind(false));
-			EditorRunBar::get_singleton()->connect("play_pressed", callable_mp(this, &Node3DEditor::_update_camera_override_button).bind(true));
-
 			_update_preview_environment();
 
 			sun_state->set_custom_minimum_size(sun_vb->get_combined_minimum_size());
@@ -8106,15 +8075,6 @@ void Node3DEditor::_notification(int p_what) {
 			}
 		} break;
 
-		case NOTIFICATION_VISIBILITY_CHANGED: {
-			if (!is_visible() && tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->is_pressed()) {
-				EditorDebuggerNode *debugger = EditorDebuggerNode::get_singleton();
-
-				debugger->set_camera_override(EditorDebuggerNode::OVERRIDE_NONE);
-				tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->set_pressed(false);
-			}
-		} break;
-
 		case NOTIFICATION_PHYSICS_PROCESS: {
 			if (do_snap_selected_nodes_to_floor) {
 				_snap_selected_nodes_to_floor();
@@ -8216,6 +8176,10 @@ VSplitContainer *Node3DEditor::get_shader_split() {
 	return shader_split;
 }
 
+Node3DEditorViewport *Node3DEditor::get_last_used_viewport() {
+	return viewports[last_used_viewport];
+}
+
 void Node3DEditor::add_control_to_left_panel(Control *p_control) {
 	left_panel_split->add_child(p_control);
 	left_panel_split->move_child(p_control, 0);
@@ -8393,6 +8357,10 @@ void Node3DEditor::_toggle_maximize_view(Object *p_viewport) {
 	}
 }
 
+void Node3DEditor::_viewport_clicked(int p_viewport_idx) {
+	last_used_viewport = p_viewport_idx;
+}
+
 void Node3DEditor::_node_added(Node *p_node) {
 	if (EditorNode::get_singleton()->get_scene_root()->is_ancestor_of(p_node)) {
 		if (Object::cast_to<WorldEnvironment>(p_node)) {
@@ -8684,8 +8652,6 @@ Node3DEditor::Node3DEditor() {
 	snap_key_enabled = false;
 	tool_mode = TOOL_MODE_SELECT;
 
-	camera_override_viewport_id = 0;
-
 	// Add some margin to the sides for better aesthetics.
 	// This prevents the first button's hover/pressed effect from "touching" the panel's border,
 	// which looks ugly.
@@ -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_context(this);
 
-	main_menu_hbox->add_child(memnew(VSeparator));
-
-	tool_option_button[TOOL_OPT_OVERRIDE_CAMERA] = memnew(Button);
-	main_menu_hbox->add_child(tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]);
-	tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->set_toggle_mode(true);
-	tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->set_theme_type_variation("FlatButton");
-	tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->set_disabled(true);
-	tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditor::_menu_item_toggled).bind(MENU_TOOL_OVERRIDE_CAMERA));
-	_update_camera_override_button(false);
-
 	main_menu_hbox->add_child(memnew(VSeparator));
 	sun_button = memnew(Button);
 	sun_button->set_tooltip_text(TTR("Toggle preview sunlight.\nIf a DirectionalLight3D node is added to the scene, preview sunlight is disabled."));
@@ -8955,7 +8911,7 @@ Node3DEditor::Node3DEditor() {
 	for (uint32_t i = 0; i < VIEWPORTS_COUNT; i++) {
 		viewports[i] = memnew(Node3DEditorViewport(this, i));
 		viewports[i]->connect("toggle_maximize_view", callable_mp(this, &Node3DEditor::_toggle_maximize_view));
-		viewports[i]->connect("clicked", callable_mp(this, &Node3DEditor::_update_camera_override_viewport));
+		viewports[i]->connect("clicked", callable_mp(this, &Node3DEditor::_viewport_clicked).bind(i));
 		viewports[i]->assign_pending_data_pointers(preview_node, &preview_bounds, accept);
 		viewport_base->add_child(viewports[i]);
 	}

+ 4 - 6
editor/plugins/node_3d_editor_plugin.h

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

+ 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("ScriptEditor", EditorStringName(EditorStyles), make_empty_stylebox(0, 0, 0, 0));
 
+		// Game view.
+		p_theme->set_type_variation("GamePanel", "Panel");
+		Ref<StyleBoxFlat> game_panel = p_theme->get_stylebox(SNAME("panel"), SNAME("Panel"))->duplicate();
+		game_panel->set_corner_radius_all(0);
+		p_theme->set_stylebox(SceneStringName(panel), "GamePanel", game_panel);
+
 		// Main menu.
 		Ref<StyleBoxFlat> menu_transparent_style = p_config.button_style->duplicate();
 		menu_transparent_style->set_bg_color(Color(1, 1, 1, 0));

+ 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.
 
 ## Changes between 4.3-stable and 4.4-stable
-
 GH-95374
 --------
 Validate extension JSON: Error: Field 'classes/ShapeCast2D/properties/collision_result': getter changed value in new API, from "_get_collision_result" to &"get_collision_result".
@@ -102,3 +101,10 @@ GH-97020
 Validate extension JSON: Error: Field 'classes/AnimationNode/methods/_process': is_const changed value in new API, from true to false.
 
 `_process` virtual method fixed to be non const instead.
+
+
+GH-97257
+--------
+Validate extension JSON: Error: Field 'classes/EditorFeatureProfile/enums/Feature/values/FEATURE_MAX': value changed value in new API, from 8.0 to 9.
+
+New entry to the `EditorFeatureProfile.Feature` enum added. Those need to go before `FEATURE_MAX`, which will always cause a compatibility break.

+ 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;
 		} break;
 
+		case NOTIFICATION_SUSPENDED:
 		case NOTIFICATION_PAUSED: {
 			if (is_physics_interpolated_and_enabled()) {
 				_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());
 		} break;
 
+		case NOTIFICATION_SUSPENDED:
+		case NOTIFICATION_UNSUSPENDED:
 		case NOTIFICATION_PAUSED:
 		case NOTIFICATION_UNPAUSED: {
 			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
 		} break;
 
+		case NOTIFICATION_SUSPENDED:
 		case NOTIFICATION_PAUSED: {
 			if (agent_parent) {
 				NavigationServer2D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process());
 			}
 		} break;
 
+		case NOTIFICATION_UNSUSPENDED: {
+			if (get_tree()->is_paused()) {
+				break;
+			}
+			[[fallthrough]];
+		}
+
 		case NOTIFICATION_UNPAUSED: {
 			if (agent_parent) {
 				NavigationServer2D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process());

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

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

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

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

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

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

+ 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());
 		} break;
 
+		case NOTIFICATION_SUSPENDED:
+		case NOTIFICATION_UNSUSPENDED:
 		case NOTIFICATION_PAUSED:
 		case NOTIFICATION_UNPAUSED: {
 			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
 		} break;
 
+		case NOTIFICATION_SUSPENDED:
 		case NOTIFICATION_PAUSED: {
 			if (agent_parent) {
 				NavigationServer3D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process());
 			}
 		} break;
 
+		case NOTIFICATION_UNSUSPENDED: {
+			if (get_tree()->is_paused()) {
+				break;
+			}
+			[[fallthrough]];
+		}
+
 		case NOTIFICATION_UNPAUSED: {
 			if (agent_parent) {
 				NavigationServer3D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process());

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

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

+ 8 - 0
scene/audio/audio_stream_player_internal.cpp

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

+ 1113 - 79
scene/debugger/scene_debugger.cpp

@@ -31,20 +31,34 @@
 #include "scene_debugger.h"
 
 #include "core/debugger/engine_debugger.h"
-#include "core/debugger/engine_profiler.h"
 #include "core/io/marshalls.h"
 #include "core/object/script_language.h"
 #include "core/templates/local_vector.h"
+#include "scene/2d/physics/collision_object_2d.h"
+#include "scene/2d/physics/collision_polygon_2d.h"
+#include "scene/2d/physics/collision_shape_2d.h"
+#ifndef _3D_DISABLED
+#include "scene/3d/label_3d.h"
+#include "scene/3d/mesh_instance_3d.h"
+#include "scene/3d/physics/collision_object_3d.h"
+#include "scene/3d/physics/collision_shape_3d.h"
+#include "scene/3d/sprite_3d.h"
+#include "scene/resources/surface_tool.h"
+#endif // _3D_DISABLED
+#include "scene/gui/popup_menu.h"
+#include "scene/main/canvas_layer.h"
 #include "scene/main/scene_tree.h"
 #include "scene/main/window.h"
 #include "scene/resources/packed_scene.h"
-
-SceneDebugger *SceneDebugger::singleton = nullptr;
+#include "scene/theme/theme_db.h"
 
 SceneDebugger::SceneDebugger() {
 	singleton = this;
+
 #ifdef DEBUG_ENABLED
 	LiveEditor::singleton = memnew(LiveEditor);
+	RuntimeNodeSelect::singleton = memnew(RuntimeNodeSelect);
+
 	EngineDebugger::register_message_capture("scene", EngineDebugger::Capture(nullptr, SceneDebugger::parse_message));
 #endif
 }
@@ -56,7 +70,13 @@ SceneDebugger::~SceneDebugger() {
 		memdelete(LiveEditor::singleton);
 		LiveEditor::singleton = nullptr;
 	}
+
+	if (RuntimeNodeSelect::singleton) {
+		memdelete(RuntimeNodeSelect::singleton);
+		RuntimeNodeSelect::singleton = nullptr;
+	}
 #endif
+
 	singleton = nullptr;
 }
 
@@ -78,10 +98,15 @@ Error SceneDebugger::parse_message(void *p_user, const String &p_msg, const Arra
 	if (!scene_tree) {
 		return ERR_UNCONFIGURED;
 	}
+
 	LiveEditor *live_editor = LiveEditor::get_singleton();
 	if (!live_editor) {
 		return ERR_UNCONFIGURED;
 	}
+	RuntimeNodeSelect *runtime_node_select = RuntimeNodeSelect::get_singleton();
+	if (!runtime_node_select) {
+		return ERR_UNCONFIGURED;
+	}
 
 	r_captured = true;
 	if (p_msg == "request_scene_tree") { // Scene tree
@@ -99,22 +124,34 @@ Error SceneDebugger::parse_message(void *p_user, const String &p_msg, const Arra
 		ObjectID id = p_args[0];
 		_send_object_id(id);
 
-	} else if (p_msg == "override_camera_2D:set") { // Camera
+	} else if (p_msg == "suspend_changed") {
 		ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
-		bool enforce = p_args[0];
-		scene_tree->get_root()->enable_canvas_transform_override(enforce);
+		bool suspended = p_args[0];
+		scene_tree->set_suspend(suspended);
+		runtime_node_select->_update_input_state();
 
-	} else if (p_msg == "override_camera_2D:transform") {
-		ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
-		Transform2D transform = p_args[0];
-		scene_tree->get_root()->set_canvas_transform_override(transform);
-#ifndef _3D_DISABLED
-	} else if (p_msg == "override_camera_3D:set") {
+	} else if (p_msg == "next_frame") {
+		_next_frame();
+
+	} else if (p_msg == "override_cameras") { // Camera
 		ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
 		bool enable = p_args[0];
+		bool from_editor = p_args[1];
+		scene_tree->get_root()->enable_canvas_transform_override(enable);
+#ifndef _3D_DISABLED
 		scene_tree->get_root()->enable_camera_3d_override(enable);
+#endif // _3D_DISABLED
+		runtime_node_select->_set_camera_override_enabled(enable && !from_editor);
 
-	} else if (p_msg == "override_camera_3D:transform") {
+	} else if (p_msg == "transform_camera_2d") {
+		ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
+		Transform2D transform = p_args[0];
+		scene_tree->get_root()->set_canvas_transform_override(transform);
+
+		runtime_node_select->_queue_selection_update();
+
+#ifndef _3D_DISABLED
+	} else if (p_msg == "transform_camera_3d") {
 		ERR_FAIL_COND_V(p_args.size() < 5, ERR_INVALID_DATA);
 		Transform3D transform = p_args[0];
 		bool is_perspective = p_args[1];
@@ -127,95 +164,142 @@ Error SceneDebugger::parse_message(void *p_user, const String &p_msg, const Arra
 			scene_tree->get_root()->set_camera_3d_override_orthogonal(size_or_fov, depth_near, depth_far);
 		}
 		scene_tree->get_root()->set_camera_3d_override_transform(transform);
+
+		runtime_node_select->_queue_selection_update();
 #endif // _3D_DISABLED
+
 	} else if (p_msg == "set_object_property") {
 		ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
 		_set_object_property(p_args[0], p_args[1], p_args[2]);
 
-	} else if (!p_msg.begins_with("live_")) { // Live edits below.
-		return ERR_SKIP;
-	} else if (p_msg == "live_set_root") {
-		ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
-		live_editor->_root_func(p_args[0], p_args[1]);
+		runtime_node_select->_queue_selection_update();
+
+	} else if (p_msg.begins_with("live_")) { /// Live Edit
+		if (p_msg == "live_set_root") {
+			ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
+			live_editor->_root_func(p_args[0], p_args[1]);
+
+		} else if (p_msg == "live_node_path") {
+			ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
+			live_editor->_node_path_func(p_args[0], p_args[1]);
+
+		} else if (p_msg == "live_res_path") {
+			ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
+			live_editor->_res_path_func(p_args[0], p_args[1]);
+
+		} else if (p_msg == "live_node_prop_res") {
+			ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
+			live_editor->_node_set_res_func(p_args[0], p_args[1], p_args[2]);
+
+		} else if (p_msg == "live_node_prop") {
+			ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
+			live_editor->_node_set_func(p_args[0], p_args[1], p_args[2]);
+
+		} else if (p_msg == "live_res_prop_res") {
+			ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
+			live_editor->_res_set_res_func(p_args[0], p_args[1], p_args[2]);
+
+		} else if (p_msg == "live_res_prop") {
+			ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
+			live_editor->_res_set_func(p_args[0], p_args[1], p_args[2]);
+
+		} else if (p_msg == "live_node_call") {
+			ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
+			LocalVector<Variant> args;
+			LocalVector<Variant *> argptrs;
+			args.resize(p_args.size() - 2);
+			argptrs.resize(args.size());
+			for (uint32_t i = 0; i < args.size(); i++) {
+				args[i] = p_args[i + 2];
+				argptrs[i] = &args[i];
+			}
+			live_editor->_node_call_func(p_args[0], p_args[1], argptrs.size() ? (const Variant **)argptrs.ptr() : nullptr, argptrs.size());
+
+		} else if (p_msg == "live_res_call") {
+			ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
+			LocalVector<Variant> args;
+			LocalVector<Variant *> argptrs;
+			args.resize(p_args.size() - 2);
+			argptrs.resize(args.size());
+			for (uint32_t i = 0; i < args.size(); i++) {
+				args[i] = p_args[i + 2];
+				argptrs[i] = &args[i];
+			}
+			live_editor->_res_call_func(p_args[0], p_args[1], argptrs.size() ? (const Variant **)argptrs.ptr() : nullptr, argptrs.size());
 
-	} else if (p_msg == "live_node_path") {
-		ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
-		live_editor->_node_path_func(p_args[0], p_args[1]);
+		} else if (p_msg == "live_create_node") {
+			ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
+			live_editor->_create_node_func(p_args[0], p_args[1], p_args[2]);
 
-	} else if (p_msg == "live_res_path") {
-		ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
-		live_editor->_res_path_func(p_args[0], p_args[1]);
+		} else if (p_msg == "live_instantiate_node") {
+			ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
+			live_editor->_instance_node_func(p_args[0], p_args[1], p_args[2]);
 
-	} else if (p_msg == "live_node_prop_res") {
-		ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
-		live_editor->_node_set_res_func(p_args[0], p_args[1], p_args[2]);
+		} else if (p_msg == "live_remove_node") {
+			ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
+			live_editor->_remove_node_func(p_args[0]);
 
-	} else if (p_msg == "live_node_prop") {
-		ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
-		live_editor->_node_set_func(p_args[0], p_args[1], p_args[2]);
+			if (!runtime_node_select->has_selection) {
+				runtime_node_select->_clear_selection();
+			}
 
-	} else if (p_msg == "live_res_prop_res") {
-		ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
-		live_editor->_res_set_res_func(p_args[0], p_args[1], p_args[2]);
+		} else if (p_msg == "live_remove_and_keep_node") {
+			ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
+			live_editor->_remove_and_keep_node_func(p_args[0], p_args[1]);
 
-	} else if (p_msg == "live_res_prop") {
-		ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
-		live_editor->_res_set_func(p_args[0], p_args[1], p_args[2]);
+			if (!runtime_node_select->has_selection) {
+				runtime_node_select->_clear_selection();
+			}
 
-	} else if (p_msg == "live_node_call") {
-		ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
-		LocalVector<Variant> args;
-		LocalVector<Variant *> argptrs;
-		args.resize(p_args.size() - 2);
-		argptrs.resize(args.size());
-		for (uint32_t i = 0; i < args.size(); i++) {
-			args[i] = p_args[i + 2];
-			argptrs[i] = &args[i];
-		}
-		live_editor->_node_call_func(p_args[0], p_args[1], argptrs.size() ? (const Variant **)argptrs.ptr() : nullptr, argptrs.size());
+		} else if (p_msg == "live_restore_node") {
+			ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
+			live_editor->_restore_node_func(p_args[0], p_args[1], p_args[2]);
 
-	} else if (p_msg == "live_res_call") {
-		ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
-		LocalVector<Variant> args;
-		LocalVector<Variant *> argptrs;
-		args.resize(p_args.size() - 2);
-		argptrs.resize(args.size());
-		for (uint32_t i = 0; i < args.size(); i++) {
-			args[i] = p_args[i + 2];
-			argptrs[i] = &args[i];
+		} else if (p_msg == "live_duplicate_node") {
+			ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
+			live_editor->_duplicate_node_func(p_args[0], p_args[1]);
+
+		} else if (p_msg == "live_reparent_node") {
+			ERR_FAIL_COND_V(p_args.size() < 4, ERR_INVALID_DATA);
+			live_editor->_reparent_node_func(p_args[0], p_args[1], p_args[2], p_args[3]);
+
+		} else {
+			return ERR_SKIP;
 		}
-		live_editor->_res_call_func(p_args[0], p_args[1], argptrs.size() ? (const Variant **)argptrs.ptr() : nullptr, argptrs.size());
 
-	} else if (p_msg == "live_create_node") {
-		ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
-		live_editor->_create_node_func(p_args[0], p_args[1], p_args[2]);
+	} else if (p_msg.begins_with("runtime_node_select_")) { /// Runtime Node Selection
+		if (p_msg == "runtime_node_select_setup") {
+			runtime_node_select->_setup();
 
-	} else if (p_msg == "live_instantiate_node") {
-		ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
-		live_editor->_instance_node_func(p_args[0], p_args[1], p_args[2]);
+		} else if (p_msg == "runtime_node_select_set_type") {
+			ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
+			RuntimeNodeSelect::NodeType type = (RuntimeNodeSelect::NodeType)(int)p_args[0];
+			runtime_node_select->_node_set_type(type);
 
-	} else if (p_msg == "live_remove_node") {
-		ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
-		live_editor->_remove_node_func(p_args[0]);
+		} else if (p_msg == "runtime_node_select_set_mode") {
+			ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
+			RuntimeNodeSelect::SelectMode mode = (RuntimeNodeSelect::SelectMode)(int)p_args[0];
+			runtime_node_select->_select_set_mode(mode);
 
-	} else if (p_msg == "live_remove_and_keep_node") {
-		ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
-		live_editor->_remove_and_keep_node_func(p_args[0], p_args[1]);
+		} else if (p_msg == "runtime_node_select_set_visible") {
+			ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
+			bool visible = p_args[0];
+			runtime_node_select->_set_selection_visible(visible);
 
-	} else if (p_msg == "live_restore_node") {
-		ERR_FAIL_COND_V(p_args.size() < 3, ERR_INVALID_DATA);
-		live_editor->_restore_node_func(p_args[0], p_args[1], p_args[2]);
+		} else if (p_msg == "runtime_node_select_reset_camera_2d") {
+			runtime_node_select->_reset_camera_2d();
 
-	} else if (p_msg == "live_duplicate_node") {
-		ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA);
-		live_editor->_duplicate_node_func(p_args[0], p_args[1]);
+		} else if (p_msg == "runtime_node_select_reset_camera_3d") {
+			runtime_node_select->_reset_camera_3d();
+
+		} else {
+			return ERR_SKIP;
+		}
 
-	} else if (p_msg == "live_reparent_node") {
-		ERR_FAIL_COND_V(p_args.size() < 4, ERR_INVALID_DATA);
-		live_editor->_reparent_node_func(p_args[0], p_args[1], p_args[2], p_args[3]);
 	} else {
 		r_captured = false;
 	}
+
 	return OK;
 }
 
@@ -260,6 +344,9 @@ void SceneDebugger::_send_object_id(ObjectID p_id, int p_max_size) {
 		return;
 	}
 
+	Node *node = Object::cast_to<Node>(ObjectDB::get_instance(p_id));
+	RuntimeNodeSelect::get_singleton()->_select_node(node);
+
 	Array arr;
 	obj.serialize(arr);
 	EngineDebugger::get_singleton()->send_message("scene:inspect_object", arr);
@@ -280,6 +367,16 @@ void SceneDebugger::_set_object_property(ObjectID p_id, const String &p_property
 	obj->set(prop_name, p_value);
 }
 
+void SceneDebugger::_next_frame() {
+	SceneTree *scene_tree = SceneTree::get_singleton();
+	if (!scene_tree->is_suspended()) {
+		return;
+	}
+
+	scene_tree->set_suspend(false);
+	RenderingServer::get_singleton()->connect("frame_post_draw", callable_mp(scene_tree, &SceneTree::set_suspend).bind(true), Object::CONNECT_ONE_SHOT);
+}
+
 void SceneDebugger::add_to_cache(const String &p_filename, Node *p_node) {
 	LiveEditor *debugger = LiveEditor::get_singleton();
 	if (!debugger) {
@@ -580,7 +677,6 @@ void SceneDebuggerTree::deserialize(const Array &p_arr) {
 }
 
 /// LiveEditor
-LiveEditor *LiveEditor::singleton = nullptr;
 LiveEditor *LiveEditor::get_singleton() {
 	return singleton;
 }
@@ -1089,4 +1185,942 @@ void LiveEditor::_reparent_node_func(const NodePath &p_at, const NodePath &p_new
 	}
 }
 
+/// RuntimeNodeSelect
+RuntimeNodeSelect *RuntimeNodeSelect::get_singleton() {
+	return singleton;
+}
+
+RuntimeNodeSelect::~RuntimeNodeSelect() {
+	if (selection_list && !selection_list->is_visible()) {
+		memdelete(selection_list);
+	}
+
+	if (sbox_2d_canvas.is_valid()) {
+		RS::get_singleton()->free(sbox_2d_canvas);
+		RS::get_singleton()->free(sbox_2d_ci);
+	}
+
+#ifndef _3D_DISABLED
+	if (sbox_3d_instance.is_valid()) {
+		RS::get_singleton()->free(sbox_3d_instance);
+		RS::get_singleton()->free(sbox_3d_instance_ofs);
+		RS::get_singleton()->free(sbox_3d_instance_xray);
+		RS::get_singleton()->free(sbox_3d_instance_xray_ofs);
+	}
+#endif // _3D_DISABLED
+}
+
+void RuntimeNodeSelect::_setup() {
+	Window *root = SceneTree::get_singleton()->get_root();
+	ERR_FAIL_COND(root->is_connected(SceneStringName(window_input), callable_mp(this, &RuntimeNodeSelect::_root_window_input)));
+
+	root->connect(SceneStringName(window_input), callable_mp(this, &RuntimeNodeSelect::_root_window_input));
+	root->connect("size_changed", callable_mp(this, &RuntimeNodeSelect::_queue_selection_update), CONNECT_DEFERRED);
+
+	selection_list = memnew(PopupMenu);
+	selection_list->set_theme(ThemeDB::get_singleton()->get_default_theme());
+	selection_list->set_auto_translate_mode(Node::AUTO_TRANSLATE_MODE_DISABLED);
+	selection_list->set_force_native(true);
+	selection_list->connect("index_pressed", callable_mp(this, &RuntimeNodeSelect::_items_popup_index_pressed).bind(selection_list));
+	selection_list->connect("popup_hide", callable_mp(Object::cast_to<Node>(root), &Node::remove_child).bind(selection_list));
+
+	panner.instantiate();
+	panner->set_callbacks(callable_mp(this, &RuntimeNodeSelect::_pan_callback), callable_mp(this, &RuntimeNodeSelect::_zoom_callback));
+
+	/// 2D Selection Box Generation
+
+	sbox_2d_canvas = RS::get_singleton()->canvas_create();
+	sbox_2d_ci = RS::get_singleton()->canvas_item_create();
+	RS::get_singleton()->viewport_attach_canvas(root->get_viewport_rid(), sbox_2d_canvas);
+	RS::get_singleton()->canvas_item_set_parent(sbox_2d_ci, sbox_2d_canvas);
+
+#ifndef _3D_DISABLED
+	cursor = Cursor();
+
+	/// 3D Selection Box Generation
+	// Copied from the Node3DEditor implementation.
+
+	// Use two AABBs to create the illusion of a slightly thicker line.
+	AABB aabb(Vector3(), Vector3(1, 1, 1));
+
+	// Create a x-ray (visible through solid surfaces) and standard version of the selection box.
+	// Both will be drawn at the same position, but with different opacity.
+	// This lets the user see where the selection is while still having a sense of depth.
+	Ref<SurfaceTool> st = memnew(SurfaceTool);
+	Ref<SurfaceTool> st_xray = memnew(SurfaceTool);
+
+	st->begin(Mesh::PRIMITIVE_LINES);
+	st_xray->begin(Mesh::PRIMITIVE_LINES);
+	for (int i = 0; i < 12; i++) {
+		Vector3 a, b;
+		aabb.get_edge(i, a, b);
+
+		st->add_vertex(a);
+		st->add_vertex(b);
+		st_xray->add_vertex(a);
+		st_xray->add_vertex(b);
+	}
+
+	Ref<StandardMaterial3D> mat = memnew(StandardMaterial3D);
+	mat->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
+	mat->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true);
+	// In the original Node3DEditor, this value would be fetched from the "editors/3d/selection_box_color" editor property,
+	// but since this is not accessible from here, we will just use the default value.
+	const Color selection_color_3d = Color(1, 0.5, 0);
+	mat->set_albedo(selection_color_3d);
+	mat->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA);
+	st->set_material(mat);
+	sbox_3d_mesh = st->commit();
+
+	Ref<StandardMaterial3D> mat_xray = memnew(StandardMaterial3D);
+	mat_xray->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
+	mat_xray->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true);
+	mat_xray->set_flag(StandardMaterial3D::FLAG_DISABLE_DEPTH_TEST, true);
+	mat_xray->set_albedo(selection_color_3d * Color(1, 1, 1, 0.15));
+	mat_xray->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA);
+	st_xray->set_material(mat_xray);
+	sbox_3d_mesh_xray = st_xray->commit();
+#endif // _3D_DISABLED
+
+	SceneTree::get_singleton()->connect("process_frame", callable_mp(this, &RuntimeNodeSelect::_process_frame));
+	SceneTree::get_singleton()->connect("physics_frame", callable_mp(this, &RuntimeNodeSelect::_physics_frame));
+
+	// This function will be called before the root enters the tree at first when the Game view is passing its settings to
+	// the debugger, so queue the update for after it enters.
+	root->connect(SceneStringName(tree_entered), callable_mp(this, &RuntimeNodeSelect::_update_input_state), Object::CONNECT_ONE_SHOT);
+}
+
+void RuntimeNodeSelect::_node_set_type(NodeType p_type) {
+	node_select_type = p_type;
+	_update_input_state();
+}
+
+void RuntimeNodeSelect::_select_set_mode(SelectMode p_mode) {
+	node_select_mode = p_mode;
+}
+
+void RuntimeNodeSelect::_set_camera_override_enabled(bool p_enabled) {
+	camera_override = p_enabled;
+
+	if (p_enabled) {
+		_update_view_2d();
+	}
+
+#ifndef _3D_DISABLED
+	if (camera_first_override) {
+		_reset_camera_2d();
+		_reset_camera_3d();
+
+		camera_first_override = false;
+	} else if (p_enabled) {
+		_update_view_2d();
+
+		SceneTree::get_singleton()->get_root()->set_camera_3d_override_transform(_get_cursor_transform());
+		SceneTree::get_singleton()->get_root()->set_camera_3d_override_perspective(CAMERA_BASE_FOV * cursor.fov_scale, CAMERA_ZNEAR, CAMERA_ZFAR);
+	}
+#endif // _3D_DISABLED
+}
+
+void RuntimeNodeSelect::_root_window_input(const Ref<InputEvent> &p_event) {
+	Window *root = SceneTree::get_singleton()->get_root();
+	if (node_select_type == NODE_TYPE_NONE || selection_list->is_visible()) {
+		// Workaround for platforms that don't allow subwindows.
+		if (selection_list->is_visible() && selection_list->is_embedded()) {
+			root->set_disable_input_override(false);
+			selection_list->push_input(p_event);
+			callable_mp(root->get_viewport(), &Viewport::set_disable_input_override).call_deferred(true);
+		}
+
+		return;
+	}
+
+	if (camera_override) {
+		if (node_select_type == NODE_TYPE_2D) {
+			if (panner->gui_input(p_event, Rect2(Vector2(), root->get_size()))) {
+				return;
+			}
+		} else if (node_select_type == NODE_TYPE_3D) {
+#ifndef _3D_DISABLED
+			if (root->get_camera_3d() && _handle_3d_input(p_event)) {
+				return;
+			}
+#endif // _3D_DISABLED
+		}
+	}
+
+	Ref<InputEventMouseButton> b = p_event;
+	if (!b.is_valid() || !b->is_pressed()) {
+		return;
+	}
+
+	list_shortcut_pressed = node_select_mode == SELECT_MODE_SINGLE && b->get_button_index() == MouseButton::RIGHT && b->is_alt_pressed();
+	if (list_shortcut_pressed || b->get_button_index() == MouseButton::LEFT) {
+		selection_position = b->get_position();
+	}
+}
+
+void RuntimeNodeSelect::_items_popup_index_pressed(int p_index, PopupMenu *p_popup) {
+	Object *obj = p_popup->get_item_metadata(p_index).get_validated_object();
+	if (!obj) {
+		return;
+	}
+
+	Array message;
+	message.append(obj->get_instance_id());
+	EngineDebugger::get_singleton()->send_message("remote_node_clicked", message);
+}
+
+void RuntimeNodeSelect::_update_input_state() {
+	SceneTree *scene_tree = SceneTree::get_singleton();
+	// This function can be called at the very beginning, when the root hasn't entered the tree yet.
+	// So check first to avoid a crash.
+	if (!scene_tree->get_root()->is_inside_tree()) {
+		return;
+	}
+
+	bool disable_input = scene_tree->is_suspended() || node_select_type != RuntimeNodeSelect::NODE_TYPE_NONE;
+	Input::get_singleton()->set_disable_input(disable_input);
+	Input::get_singleton()->set_mouse_mode_override_enabled(disable_input);
+	scene_tree->get_root()->set_disable_input_override(disable_input);
+}
+
+void RuntimeNodeSelect::_process_frame() {
+#ifndef _3D_DISABLED
+	if (camera_freelook) {
+		Transform3D transform = _get_cursor_transform();
+		Vector3 forward = transform.basis.xform(Vector3(0, 0, -1));
+		const Vector3 right = transform.basis.xform(Vector3(1, 0, 0));
+		Vector3 up = transform.basis.xform(Vector3(0, 1, 0));
+
+		Vector3 direction;
+
+		Input *input = Input::get_singleton();
+		bool was_input_disabled = input->is_input_disabled();
+		if (was_input_disabled) {
+			input->set_disable_input(false);
+		}
+
+		if (input->is_physical_key_pressed(Key::A)) {
+			direction -= right;
+		}
+		if (input->is_physical_key_pressed(Key::D)) {
+			direction += right;
+		}
+		if (input->is_physical_key_pressed(Key::W)) {
+			direction += forward;
+		}
+		if (input->is_physical_key_pressed(Key::S)) {
+			direction -= forward;
+		}
+		if (input->is_physical_key_pressed(Key::E)) {
+			direction += up;
+		}
+		if (input->is_physical_key_pressed(Key::Q)) {
+			direction -= up;
+		}
+
+		real_t speed = FREELOOK_BASE_SPEED;
+		if (input->is_physical_key_pressed(Key::SHIFT)) {
+			speed *= 3.0;
+		}
+		if (input->is_physical_key_pressed(Key::ALT)) {
+			speed *= 0.333333;
+		}
+
+		if (was_input_disabled) {
+			input->set_disable_input(true);
+		}
+
+		if (direction != Vector3()) {
+			// Calculate the process time manually, as the time scale is frozen.
+			const double process_time = (1.0 / Engine::get_singleton()->get_frames_per_second()) * Engine::get_singleton()->get_unfrozen_time_scale();
+			const Vector3 motion = direction * speed * process_time;
+			cursor.pos += motion;
+			cursor.eye_pos += motion;
+
+			SceneTree::get_singleton()->get_root()->set_camera_3d_override_transform(_get_cursor_transform());
+		}
+	}
+#endif // _3D_DISABLED
+
+	if (selection_update_queued || !SceneTree::get_singleton()->is_suspended()) {
+		selection_update_queued = false;
+		if (has_selection) {
+			_update_selection();
+		}
+	}
+}
+
+void RuntimeNodeSelect::_physics_frame() {
+	if (!Math::is_inf(selection_position.x) || !Math::is_inf(selection_position.y)) {
+		_click_point();
+		selection_position = Point2(INFINITY, INFINITY);
+	}
+}
+
+void RuntimeNodeSelect::_click_point() {
+	Window *root = SceneTree::get_singleton()->get_root();
+	Point2 pos = root->get_screen_transform().affine_inverse().xform(selection_position);
+	Vector<SelectResult> items;
+
+	if (node_select_type == NODE_TYPE_2D) {
+		for (int i = 0; i < root->get_child_count(); i++) {
+			_find_canvas_items_at_pos(pos, root->get_child(i), items);
+		}
+
+		// Remove possible duplicates.
+		for (int i = 0; i < items.size(); i++) {
+			Node *item = items[i].item;
+			for (int j = 0; j < i; j++) {
+				if (items[j].item == item) {
+					items.remove_at(i);
+					i--;
+
+					break;
+				}
+			}
+		}
+	} else if (node_select_type == NODE_TYPE_3D) {
+#ifndef _3D_DISABLED
+		_find_3d_items_at_pos(pos, items);
+#endif // _3D_DISABLED
+	}
+
+	if (items.is_empty()) {
+		return;
+	}
+
+	items.sort();
+
+	if ((!list_shortcut_pressed && node_select_mode == SELECT_MODE_SINGLE) || items.size() == 1) {
+		Array message;
+		message.append(items[0].item->get_instance_id());
+		EngineDebugger::get_singleton()->send_message("remote_node_clicked", message);
+	} else if (list_shortcut_pressed || node_select_mode == SELECT_MODE_LIST) {
+		if (!selection_list->is_inside_tree()) {
+			root->add_child(selection_list);
+		}
+
+		selection_list->clear();
+		for (const SelectResult &I : items) {
+			selection_list->add_item(I.item->get_name());
+			selection_list->set_item_metadata(-1, I.item);
+		}
+
+		selection_list->set_position(selection_list->is_embedded() ? pos : selection_position + root->get_position());
+		selection_list->reset_size();
+		selection_list->popup();
+		// FIXME: Ugly hack that stops the popup from hiding when the button is released.
+		selection_list->call_deferred(SNAME("set_position"), selection_list->get_position() + Point2(1, 0));
+	}
+}
+
+void RuntimeNodeSelect::_select_node(Node *p_node) {
+	if (p_node == selected_node) {
+		return;
+	}
+
+	_clear_selection();
+
+	CanvasItem *ci = Object::cast_to<CanvasItem>(p_node);
+	if (ci) {
+		selected_node = p_node;
+	} else {
+#ifndef _3D_DISABLED
+		Node3D *node_3d = Object::cast_to<Node3D>(p_node);
+		if (node_3d) {
+			if (!node_3d->is_inside_world()) {
+				return;
+			}
+
+			selected_node = p_node;
+
+			sbox_3d_instance = RS::get_singleton()->instance_create2(sbox_3d_mesh->get_rid(), node_3d->get_world_3d()->get_scenario());
+			sbox_3d_instance_ofs = RS::get_singleton()->instance_create2(sbox_3d_mesh->get_rid(), node_3d->get_world_3d()->get_scenario());
+			RS::get_singleton()->instance_geometry_set_cast_shadows_setting(sbox_3d_instance, RS::SHADOW_CASTING_SETTING_OFF);
+			RS::get_singleton()->instance_geometry_set_cast_shadows_setting(sbox_3d_instance_ofs, RS::SHADOW_CASTING_SETTING_OFF);
+			RS::get_singleton()->instance_geometry_set_flag(sbox_3d_instance, RS::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true);
+			RS::get_singleton()->instance_geometry_set_flag(sbox_3d_instance, RS::INSTANCE_FLAG_USE_BAKED_LIGHT, false);
+			RS::get_singleton()->instance_geometry_set_flag(sbox_3d_instance_ofs, RS::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true);
+			RS::get_singleton()->instance_geometry_set_flag(sbox_3d_instance_ofs, RS::INSTANCE_FLAG_USE_BAKED_LIGHT, false);
+
+			sbox_3d_instance_xray = RS::get_singleton()->instance_create2(sbox_3d_mesh_xray->get_rid(), node_3d->get_world_3d()->get_scenario());
+			sbox_3d_instance_xray_ofs = RS::get_singleton()->instance_create2(sbox_3d_mesh_xray->get_rid(), node_3d->get_world_3d()->get_scenario());
+			RS::get_singleton()->instance_geometry_set_cast_shadows_setting(sbox_3d_instance_xray, RS::SHADOW_CASTING_SETTING_OFF);
+			RS::get_singleton()->instance_geometry_set_cast_shadows_setting(sbox_3d_instance_xray_ofs, RS::SHADOW_CASTING_SETTING_OFF);
+			RS::get_singleton()->instance_geometry_set_flag(sbox_3d_instance_xray, RS::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true);
+			RS::get_singleton()->instance_geometry_set_flag(sbox_3d_instance_xray, RS::INSTANCE_FLAG_USE_BAKED_LIGHT, false);
+			RS::get_singleton()->instance_geometry_set_flag(sbox_3d_instance_xray_ofs, RS::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true);
+			RS::get_singleton()->instance_geometry_set_flag(sbox_3d_instance_xray_ofs, RS::INSTANCE_FLAG_USE_BAKED_LIGHT, false);
+		}
+#endif // _3D_DISABLED
+	}
+
+	has_selection = selected_node;
+	_queue_selection_update();
+}
+
+void RuntimeNodeSelect::_queue_selection_update() {
+	if (has_selection && selection_visible) {
+		if (SceneTree::get_singleton()->is_suspended()) {
+			_update_selection();
+		} else {
+			selection_update_queued = true;
+		}
+	}
+}
+
+void RuntimeNodeSelect::_update_selection() {
+	if (has_selection && (!selected_node || !selected_node->is_inside_tree())) {
+		_clear_selection();
+		return;
+	}
+
+	CanvasItem *ci = Object::cast_to<CanvasItem>(selected_node);
+	if (ci) {
+		Window *root = SceneTree::get_singleton()->get_root();
+		Transform2D xform;
+		if (root->is_canvas_transform_override_enabled() && !ci->get_canvas_layer_node()) {
+			RS::get_singleton()->canvas_item_set_transform(sbox_2d_ci, (root->get_canvas_transform_override()));
+			xform = ci->get_global_transform();
+		} else {
+			RS::get_singleton()->canvas_item_set_transform(sbox_2d_ci, Transform2D());
+			xform = ci->get_global_transform_with_canvas();
+		}
+
+		// Fallback.
+		Rect2 rect = Rect2(Vector2(), Vector2(10, 10));
+
+		if (ci->_edit_use_rect()) {
+			rect = ci->_edit_get_rect();
+		} else {
+			CollisionShape2D *collision_shape = Object::cast_to<CollisionShape2D>(ci);
+			if (collision_shape) {
+				Ref<Shape2D> shape = collision_shape->get_shape();
+				if (shape.is_valid()) {
+					rect = shape->get_rect();
+				}
+			}
+		}
+
+		RS::get_singleton()->canvas_item_set_visible(sbox_2d_ci, selection_visible);
+
+		if (xform == sbox_2d_xform && rect == sbox_2d_rect) {
+			return; // Nothing changed.
+		}
+		sbox_2d_xform = xform;
+		sbox_2d_rect = rect;
+
+		RS::get_singleton()->canvas_item_clear(sbox_2d_ci);
+
+		const Vector2 endpoints[4] = {
+			xform.xform(rect.position),
+			xform.xform(rect.position + Vector2(rect.size.x, 0)),
+			xform.xform(rect.position + rect.size),
+			xform.xform(rect.position + Vector2(0, rect.size.y))
+		};
+
+		const Color selection_color_2d = Color(1, 0.6, 0.4, 0.7);
+		for (int i = 0; i < 4; i++) {
+			RS::get_singleton()->canvas_item_add_line(sbox_2d_ci, endpoints[i], endpoints[(i + 1) % 4], selection_color_2d, Math::round(2.f));
+		}
+	} else {
+#ifndef _3D_DISABLED
+		Node3D *node_3d = Object::cast_to<Node3D>(selected_node);
+
+		// Fallback.
+		AABB bounds(Vector3(-0.5, -0.5, -0.5), Vector3(1, 1, 1));
+
+		VisualInstance3D *visual_instance = Object::cast_to<VisualInstance3D>(node_3d);
+		if (visual_instance) {
+			bounds = visual_instance->get_aabb();
+		} else {
+			CollisionShape3D *collision_shape = Object::cast_to<CollisionShape3D>(node_3d);
+			if (collision_shape) {
+				Ref<Shape3D> shape = collision_shape->get_shape();
+				if (shape.is_valid()) {
+					bounds = shape->get_debug_mesh()->get_aabb();
+				}
+			}
+		}
+
+		RS::get_singleton()->instance_set_visible(sbox_3d_instance, selection_visible);
+		RS::get_singleton()->instance_set_visible(sbox_3d_instance_ofs, selection_visible);
+		RS::get_singleton()->instance_set_visible(sbox_3d_instance_xray, selection_visible);
+		RS::get_singleton()->instance_set_visible(sbox_3d_instance_xray_ofs, selection_visible);
+
+		Transform3D xform_to_top_level_parent_space = node_3d->get_global_transform().affine_inverse() * node_3d->get_global_transform();
+		bounds = xform_to_top_level_parent_space.xform(bounds);
+		Transform3D t = node_3d->get_global_transform();
+
+		if (t == sbox_3d_xform && bounds == sbox_3d_bounds) {
+			return; // Nothing changed.
+		}
+		sbox_3d_xform = t;
+		sbox_3d_bounds = bounds;
+
+		Transform3D t_offset = t;
+
+		// Apply AABB scaling before item's global transform.
+		{
+			const Vector3 offset(0.005, 0.005, 0.005);
+			Basis aabb_s;
+			aabb_s.scale(bounds.size + offset);
+			t.translate_local(bounds.position - offset / 2);
+			t.basis = t.basis * aabb_s;
+		}
+		{
+			const Vector3 offset(0.01, 0.01, 0.01);
+			Basis aabb_s;
+			aabb_s.scale(bounds.size + offset);
+			t_offset.translate_local(bounds.position - offset / 2);
+			t_offset.basis = t_offset.basis * aabb_s;
+		}
+
+		RS::get_singleton()->instance_set_transform(sbox_3d_instance, t);
+		RS::get_singleton()->instance_set_transform(sbox_3d_instance_ofs, t_offset);
+		RS::get_singleton()->instance_set_transform(sbox_3d_instance_xray, t);
+		RS::get_singleton()->instance_set_transform(sbox_3d_instance_xray_ofs, t_offset);
+#endif // _3D_DISABLED
+	}
+}
+
+void RuntimeNodeSelect::_clear_selection() {
+	selected_node = nullptr;
+	has_selection = false;
+
+	if (sbox_2d_canvas.is_valid()) {
+		RS::get_singleton()->canvas_item_clear(sbox_2d_ci);
+	}
+
+#ifndef _3D_DISABLED
+	if (sbox_3d_instance.is_valid()) {
+		RS::get_singleton()->free(sbox_3d_instance);
+		RS::get_singleton()->free(sbox_3d_instance_ofs);
+		RS::get_singleton()->free(sbox_3d_instance_xray);
+		RS::get_singleton()->free(sbox_3d_instance_xray_ofs);
+	}
+#endif // _3D_DISABLED
+}
+
+void RuntimeNodeSelect::_set_selection_visible(bool p_visible) {
+	selection_visible = p_visible;
+
+	if (has_selection) {
+		_update_selection();
+	}
+}
+
+// Copied and trimmed from the CanvasItemEditor implementation.
+void RuntimeNodeSelect::_find_canvas_items_at_pos(const Point2 &p_pos, Node *p_node, Vector<SelectResult> &r_items, const Transform2D &p_parent_xform, const Transform2D &p_canvas_xform) {
+	if (!p_node || Object::cast_to<Viewport>(p_node)) {
+		return;
+	}
+
+	// In the original CanvasItemEditor, this value would be fetched from the "editors/polygon_editor/point_grab_radius" editor property,
+	// but since this is not accessible from here, we will just use the default value.
+	const real_t grab_distance = 8;
+	CanvasItem *ci = Object::cast_to<CanvasItem>(p_node);
+
+	for (int i = p_node->get_child_count() - 1; i >= 0; i--) {
+		if (ci) {
+			if (!ci->is_set_as_top_level()) {
+				_find_canvas_items_at_pos(p_pos, p_node->get_child(i), r_items, p_parent_xform * ci->get_transform(), p_canvas_xform);
+			} else {
+				_find_canvas_items_at_pos(p_pos, p_node->get_child(i), r_items, ci->get_transform(), p_canvas_xform);
+			}
+		} else {
+			CanvasLayer *cl = Object::cast_to<CanvasLayer>(p_node);
+			_find_canvas_items_at_pos(p_pos, p_node->get_child(i), r_items, Transform2D(), cl ? cl->get_transform() : p_canvas_xform);
+		}
+	}
+
+	if (ci && ci->is_visible_in_tree()) {
+		Transform2D xform = p_canvas_xform;
+		if (!ci->is_set_as_top_level()) {
+			xform *= p_parent_xform;
+		}
+
+		Vector2 pos;
+		// Cameras (overridden or not) don't affect `CanvasLayer`s.
+		if (!ci->get_canvas_layer_node()) {
+			Window *root = SceneTree::get_singleton()->get_root();
+			pos = (root->is_canvas_transform_override_enabled() ? root->get_canvas_transform_override() : root->get_canvas_transform()).affine_inverse().xform(p_pos);
+		} else {
+			pos = p_pos;
+		}
+
+		xform = (xform * ci->get_transform()).affine_inverse();
+		const real_t local_grab_distance = xform.basis_xform(Vector2(grab_distance, 0)).length() / view_2d_zoom;
+		if (ci->_edit_is_selected_on_click(xform.xform(pos), local_grab_distance)) {
+			SelectResult res;
+			res.item = ci;
+			res.order = ci->get_effective_z_index() + ci->get_canvas_layer();
+			r_items.push_back(res);
+
+			// If it's a shape, get the collision object it's from.
+			// FIXME: If the collision object has multiple shapes, only the topmost will be above it in the list.
+			if (Object::cast_to<CollisionShape2D>(ci) || Object::cast_to<CollisionPolygon2D>(ci)) {
+				CollisionObject2D *collision_object = Object::cast_to<CollisionObject2D>(ci->get_parent());
+				if (collision_object) {
+					SelectResult res_col;
+					res_col.item = ci->get_parent();
+					res_col.order = collision_object->get_z_index() + ci->get_canvas_layer();
+					r_items.push_back(res_col);
+				}
+			}
+		}
+	}
+}
+
+void RuntimeNodeSelect::_pan_callback(Vector2 p_scroll_vec, Ref<InputEvent> p_event) {
+	view_2d_offset.x -= p_scroll_vec.x / view_2d_zoom;
+	view_2d_offset.y -= p_scroll_vec.y / view_2d_zoom;
+
+	_update_view_2d();
+}
+
+// A very shallow copy of the same function inside CanvasItemEditor.
+void RuntimeNodeSelect::_zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref<InputEvent> p_event) {
+	real_t prev_zoom = view_2d_zoom;
+	view_2d_zoom = CLAMP(view_2d_zoom * p_zoom_factor, VIEW_2D_MIN_ZOOM, VIEW_2D_MAX_ZOOM);
+
+	Vector2 pos = SceneTree::get_singleton()->get_root()->get_screen_transform().affine_inverse().xform(p_origin);
+	view_2d_offset += pos / prev_zoom - pos / view_2d_zoom;
+
+	// We want to align in-scene pixels to screen pixels, this prevents blurry rendering
+	// of small details (texts, lines).
+	// This correction adds a jitter movement when zooming, so we correct only when the
+	// zoom factor is an integer. (in the other cases, all pixels won't be aligned anyway)
+	const real_t closest_zoom_factor = Math::round(view_2d_zoom);
+	if (Math::is_zero_approx(view_2d_zoom - closest_zoom_factor)) {
+		// Make sure scene pixel at view_offset is aligned on a screen pixel.
+		Vector2 view_offset_int = view_2d_offset.floor();
+		Vector2 view_offset_frac = view_2d_offset - view_offset_int;
+		view_2d_offset = view_offset_int + (view_offset_frac * closest_zoom_factor).round() / closest_zoom_factor;
+	}
+
+	_update_view_2d();
+}
+
+void RuntimeNodeSelect::_reset_camera_2d() {
+	view_2d_offset = -SceneTree::get_singleton()->get_root()->get_canvas_transform().get_origin();
+	view_2d_zoom = 1;
+
+	_update_view_2d();
+}
+
+void RuntimeNodeSelect::_update_view_2d() {
+	Transform2D transform = Transform2D();
+	transform.scale_basis(Size2(view_2d_zoom, view_2d_zoom));
+	transform.columns[2] = -view_2d_offset * view_2d_zoom;
+
+	SceneTree::get_singleton()->get_root()->set_canvas_transform_override(transform);
+
+	_queue_selection_update();
+}
+
+#ifndef _3D_DISABLED
+void RuntimeNodeSelect::_find_3d_items_at_pos(const Point2 &p_pos, Vector<SelectResult> &r_items) {
+	Window *root = SceneTree::get_singleton()->get_root();
+	Camera3D *camera = root->get_viewport()->get_camera_3d();
+	if (!camera) {
+		return;
+	}
+
+	Vector3 ray, pos, to;
+	if (root->get_viewport()->is_camera_3d_override_enabled()) {
+		Viewport *vp = root->get_viewport();
+		ray = vp->camera_3d_override_project_ray_normal(p_pos);
+		pos = vp->camera_3d_override_project_ray_origin(p_pos);
+		to = pos + ray * vp->get_camera_3d_override_properties()["z_far"];
+	} else {
+		ray = camera->project_ray_normal(p_pos);
+		pos = camera->project_ray_origin(p_pos);
+		to = pos + ray * camera->get_far();
+	}
+
+	// Start with physical objects.
+	PhysicsDirectSpaceState3D *ss = root->get_world_3d()->get_direct_space_state();
+	PhysicsDirectSpaceState3D::RayResult result;
+	HashSet<RID> excluded;
+	PhysicsDirectSpaceState3D::RayParameters ray_params;
+	ray_params.from = pos;
+	ray_params.to = to;
+	ray_params.collide_with_areas = true;
+	while (true) {
+		ray_params.exclude = excluded;
+		if (ss->intersect_ray(ray_params, result)) {
+			SelectResult res;
+			res.item = Object::cast_to<Node>(result.collider);
+			res.order = -pos.distance_to(Object::cast_to<Node3D>(res.item)->get_global_transform().xform(result.position));
+
+			// Fetch collision shapes.
+			CollisionObject3D *collision = Object::cast_to<CollisionObject3D>(result.collider);
+			if (collision) {
+				List<uint32_t> owners;
+				collision->get_shape_owners(&owners);
+				for (const uint32_t &I : owners) {
+					SelectResult res_shape;
+					res_shape.item = Object::cast_to<Node>(collision->shape_owner_get_owner(I));
+					res_shape.order = res.order;
+					r_items.push_back(res_shape);
+				}
+			}
+
+			r_items.push_back(res);
+
+			excluded.insert(result.rid);
+		} else {
+			break;
+		}
+	}
+
+	// Then go for the meshes.
+	Vector<ObjectID> items = RS::get_singleton()->instances_cull_ray(pos, to, root->get_world_3d()->get_scenario());
+	for (int i = 0; i < items.size(); i++) {
+		Object *obj = ObjectDB::get_instance(items[i]);
+		GeometryInstance3D *geo_instance = nullptr;
+		Ref<TriangleMesh> mesh_collision;
+
+		MeshInstance3D *mesh_instance = Object::cast_to<MeshInstance3D>(obj);
+		if (mesh_instance) {
+			if (mesh_instance->get_mesh().is_valid()) {
+				geo_instance = mesh_instance;
+				mesh_collision = mesh_instance->get_mesh()->generate_triangle_mesh();
+			}
+		} else {
+			Label3D *label = Object::cast_to<Label3D>(obj);
+			if (label) {
+				geo_instance = label;
+				mesh_collision = label->generate_triangle_mesh();
+			} else {
+				Sprite3D *sprite = Object::cast_to<Sprite3D>(obj);
+				if (sprite) {
+					geo_instance = sprite;
+					mesh_collision = sprite->generate_triangle_mesh();
+				}
+			}
+		}
+
+		if (mesh_collision.is_valid()) {
+			Transform3D gt = geo_instance->get_global_transform();
+			Transform3D ai = gt.affine_inverse();
+			Vector3 point, normal;
+			if (mesh_collision->intersect_ray(ai.xform(pos), ai.basis.xform(ray).normalized(), point, normal)) {
+				SelectResult res;
+				res.item = Object::cast_to<Node>(obj);
+				res.order = -pos.distance_to(gt.xform(point));
+				r_items.push_back(res);
+
+				continue;
+			}
+		}
+
+		items.remove_at(i);
+		i--;
+	}
+}
+
+bool RuntimeNodeSelect::_handle_3d_input(const Ref<InputEvent> &p_event) {
+	Ref<InputEventMouseButton> b = p_event;
+
+	if (b.is_valid()) {
+		const real_t zoom_factor = 1.08 * b->get_factor();
+		switch (b->get_button_index()) {
+			case MouseButton::WHEEL_UP: {
+				if (!camera_freelook) {
+					_cursor_scale_distance(1.0 / zoom_factor);
+				}
+
+				return true;
+			} break;
+			case MouseButton::WHEEL_DOWN: {
+				if (!camera_freelook) {
+					_cursor_scale_distance(zoom_factor);
+				}
+
+				return true;
+			} break;
+			case MouseButton::RIGHT: {
+				_set_camera_freelook_enabled(b->is_pressed());
+				return true;
+			} break;
+			default: {
+			}
+		}
+	}
+
+	Ref<InputEventMouseMotion> m = p_event;
+
+	if (m.is_valid()) {
+		if (camera_freelook) {
+			_cursor_look(m);
+		} else if (m->get_button_mask().has_flag(MouseButtonMask::MIDDLE)) {
+			if (m->is_shift_pressed()) {
+				_cursor_pan(m);
+			} else {
+				_cursor_orbit(m);
+			}
+		}
+
+		return true;
+	}
+
+	Ref<InputEventKey> k = p_event;
+
+	if (k.is_valid()) {
+		if (k->get_physical_keycode() == Key::ESCAPE) {
+			_set_camera_freelook_enabled(false);
+			return true;
+		} else if (k->is_ctrl_pressed()) {
+			switch (k->get_physical_keycode()) {
+				case Key::EQUAL: {
+					cursor.fov_scale = CLAMP(cursor.fov_scale - 0.05, CAMERA_MIN_FOV_SCALE, CAMERA_MAX_FOV_SCALE);
+					SceneTree::get_singleton()->get_root()->set_camera_3d_override_perspective(CAMERA_BASE_FOV * cursor.fov_scale, CAMERA_ZNEAR, CAMERA_ZFAR);
+
+					return true;
+				} break;
+				case Key::MINUS: {
+					cursor.fov_scale = CLAMP(cursor.fov_scale + 0.05, CAMERA_MIN_FOV_SCALE, CAMERA_MAX_FOV_SCALE);
+					SceneTree::get_singleton()->get_root()->set_camera_3d_override_perspective(CAMERA_BASE_FOV * cursor.fov_scale, CAMERA_ZNEAR, CAMERA_ZFAR);
+
+					return true;
+				} break;
+				case Key::KEY_0: {
+					cursor.fov_scale = 1;
+					SceneTree::get_singleton()->get_root()->set_camera_3d_override_perspective(CAMERA_BASE_FOV, CAMERA_ZNEAR, CAMERA_ZFAR);
+
+					return true;
+				} break;
+				default: {
+				}
+			}
+		}
+	}
+
+	// TODO: Handle magnify and pan input gestures.
+
+	return false;
+}
+
+void RuntimeNodeSelect::_set_camera_freelook_enabled(bool p_enabled) {
+	camera_freelook = p_enabled;
+
+	if (p_enabled) {
+		// Make sure eye_pos is synced, because freelook referential is eye pos rather than orbit pos
+		Vector3 forward = _get_cursor_transform().basis.xform(Vector3(0, 0, -1));
+		cursor.eye_pos = cursor.pos - cursor.distance * forward;
+
+		previous_mouse_position = SceneTree::get_singleton()->get_root()->get_mouse_position();
+
+		// Hide mouse like in an FPS (warping doesn't work).
+		Input::get_singleton()->set_mouse_mode_override(Input::MOUSE_MODE_CAPTURED);
+
+	} else {
+		// Restore mouse.
+		Input::get_singleton()->set_mouse_mode_override(Input::MOUSE_MODE_VISIBLE);
+
+		// Restore the previous mouse position when leaving freelook mode.
+		// This is done because leaving `Input.MOUSE_MODE_CAPTURED` will center the cursor
+		// due to OS limitations.
+		Input::get_singleton()->warp_mouse(previous_mouse_position);
+	}
+}
+
+void RuntimeNodeSelect::_cursor_scale_distance(real_t p_scale) {
+	real_t min_distance = MAX(CAMERA_ZNEAR * 4, VIEW_3D_MIN_ZOOM);
+	real_t max_distance = MIN(CAMERA_ZFAR / 4, VIEW_3D_MAX_ZOOM);
+	cursor.distance = CLAMP(cursor.distance * p_scale, min_distance, max_distance);
+
+	SceneTree::get_singleton()->get_root()->set_camera_3d_override_transform(_get_cursor_transform());
+}
+
+void RuntimeNodeSelect::_cursor_look(Ref<InputEventWithModifiers> p_event) {
+	Window *root = SceneTree::get_singleton()->get_root();
+	const Vector2 relative = Input::get_singleton()->warp_mouse_motion(p_event, Rect2(Vector2(), root->get_size()));
+	const Transform3D prev_camera_transform = _get_cursor_transform();
+
+	cursor.x_rot += relative.y * RADS_PER_PIXEL;
+	// Clamp the Y rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
+	cursor.x_rot = CLAMP(cursor.x_rot, -1.57, 1.57);
+
+	cursor.y_rot += relative.x * RADS_PER_PIXEL;
+
+	// Look is like the opposite of Orbit: the focus point rotates around the camera.
+	Transform3D camera_transform = _get_cursor_transform();
+	Vector3 pos = camera_transform.xform(Vector3(0, 0, 0));
+	Vector3 prev_pos = prev_camera_transform.xform(Vector3(0, 0, 0));
+	Vector3 diff = prev_pos - pos;
+	cursor.pos += diff;
+
+	SceneTree::get_singleton()->get_root()->set_camera_3d_override_transform(_get_cursor_transform());
+}
+
+void RuntimeNodeSelect::_cursor_pan(Ref<InputEventWithModifiers> p_event) {
+	Window *root = SceneTree::get_singleton()->get_root();
+	// Reduce all sides of the area by 1, so warping works when windows are maximized/fullscreen.
+	const Vector2 relative = Input::get_singleton()->warp_mouse_motion(p_event, Rect2(Vector2(1, 1), root->get_size() - Vector2(2, 2)));
+	const real_t pan_speed = 1 / 150.0;
+
+	Transform3D camera_transform;
+	camera_transform.translate_local(cursor.pos);
+	camera_transform.basis.rotate(Vector3(1, 0, 0), -cursor.x_rot);
+	camera_transform.basis.rotate(Vector3(0, 1, 0), -cursor.y_rot);
+
+	Vector3 translation(1 * -relative.x * pan_speed, relative.y * pan_speed, 0);
+	translation *= cursor.distance / 4;
+	camera_transform.translate_local(translation);
+	cursor.pos = camera_transform.origin;
+
+	SceneTree::get_singleton()->get_root()->set_camera_3d_override_transform(_get_cursor_transform());
+}
+
+void RuntimeNodeSelect::_cursor_orbit(Ref<InputEventWithModifiers> p_event) {
+	Window *root = SceneTree::get_singleton()->get_root();
+	// Reduce all sides of the area by 1, so warping works when windows are maximized/fullscreen.
+	const Vector2 relative = Input::get_singleton()->warp_mouse_motion(p_event, Rect2(Vector2(1, 1), root->get_size() - Vector2(2, 2)));
+
+	cursor.x_rot += relative.y * RADS_PER_PIXEL;
+	// Clamp the Y rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
+	cursor.x_rot = CLAMP(cursor.x_rot, -1.57, 1.57);
+
+	cursor.y_rot += relative.x * RADS_PER_PIXEL;
+
+	SceneTree::get_singleton()->get_root()->set_camera_3d_override_transform(_get_cursor_transform());
+}
+
+Transform3D RuntimeNodeSelect::_get_cursor_transform() {
+	Transform3D camera_transform;
+	camera_transform.translate_local(cursor.pos);
+	camera_transform.basis.rotate(Vector3(1, 0, 0), -cursor.x_rot);
+	camera_transform.basis.rotate(Vector3(0, 1, 0), -cursor.y_rot);
+	camera_transform.translate_local(0, 0, cursor.distance);
+
+	return camera_transform;
+}
+
+void RuntimeNodeSelect::_reset_camera_3d() {
+	camera_first_override = true;
+
+	Window *root = SceneTree::get_singleton()->get_root();
+	Camera3D *camera = root->get_camera_3d();
+	if (!camera) {
+		return;
+	}
+
+	cursor = Cursor();
+	Transform3D transform = camera->get_global_transform();
+	transform.translate_local(0, 0, -cursor.distance);
+	cursor.pos = transform.origin;
+
+	cursor.x_rot = -camera->get_global_rotation().x;
+	cursor.y_rot = -camera->get_global_rotation().y;
+
+	cursor.fov_scale = CLAMP(camera->get_fov() / CAMERA_BASE_FOV, CAMERA_MIN_FOV_SCALE, CAMERA_MAX_FOV_SCALE);
+
+	SceneTree::get_singleton()->get_root()->set_camera_3d_override_transform(_get_cursor_transform());
+	SceneTree::get_singleton()->get_root()->set_camera_3d_override_perspective(CAMERA_BASE_FOV * cursor.fov_scale, CAMERA_ZNEAR, CAMERA_ZFAR);
+}
+#endif // _3D_DISABLED
 #endif

+ 157 - 4
scene/debugger/scene_debugger.h

@@ -31,19 +31,21 @@
 #ifndef SCENE_DEBUGGER_H
 #define SCENE_DEBUGGER_H
 
-#include "core/object/class_db.h"
+#include "core/input/shortcut.h"
 #include "core/object/ref_counted.h"
 #include "core/string/ustring.h"
 #include "core/templates/pair.h"
 #include "core/variant/array.h"
+#include "scene/gui/view_panner.h"
+#include "scene/resources/mesh.h"
 
+class PopupMenu;
 class Script;
 class Node;
 
 class SceneDebugger {
-public:
 private:
-	static SceneDebugger *singleton;
+	inline static SceneDebugger *singleton = nullptr;
 
 	SceneDebugger();
 
@@ -59,6 +61,7 @@ private:
 	static void _set_node_owner_recursive(Node *p_node, Node *p_owner);
 	static void _set_object_property(ObjectID p_id, const String &p_property, const Variant &p_value);
 	static void _send_object_id(ObjectID p_id, int p_max_size = 1 << 20);
+	static void _next_frame();
 
 public:
 	static Error parse_message(void *p_user, const String &p_msg, const Array &p_args, bool &r_captured);
@@ -160,11 +163,161 @@ private:
 		live_edit_root = NodePath("/root");
 	}
 
-	static LiveEditor *singleton;
+	inline static LiveEditor *singleton = nullptr;
 
 public:
 	static LiveEditor *get_singleton();
 };
+
+class RuntimeNodeSelect : public Object {
+	GDCLASS(RuntimeNodeSelect, Object);
+
+public:
+	enum NodeType {
+		NODE_TYPE_NONE,
+		NODE_TYPE_2D,
+		NODE_TYPE_3D,
+		NODE_TYPE_MAX
+	};
+
+	enum SelectMode {
+		SELECT_MODE_SINGLE,
+		SELECT_MODE_LIST,
+		SELECT_MODE_MAX
+	};
+
+private:
+	friend class SceneDebugger;
+
+	struct SelectResult {
+		Node *item = nullptr;
+		real_t order = 0;
+		_FORCE_INLINE_ bool operator<(const SelectResult &p_rr) const { return p_rr.order < order; }
+	};
+
+	bool has_selection = false;
+	Node *selected_node = nullptr;
+	PopupMenu *selection_list = nullptr;
+	bool selection_visible = true;
+	bool selection_update_queued = false;
+
+	bool camera_override = false;
+
+	// Values taken from EditorZoomWidget.
+	const float VIEW_2D_MIN_ZOOM = 1.0 / 128;
+	const float VIEW_2D_MAX_ZOOM = 128;
+
+	Ref<ViewPanner> panner;
+	Vector2 view_2d_offset;
+	real_t view_2d_zoom = 1.0;
+
+	RID sbox_2d_canvas;
+	RID sbox_2d_ci;
+	Transform2D sbox_2d_xform;
+	Rect2 sbox_2d_rect;
+
+#ifndef _3D_DISABLED
+	struct Cursor {
+		Vector3 pos;
+		real_t x_rot, y_rot, distance, fov_scale;
+		Vector3 eye_pos; // Used in freelook mode.
+
+		Cursor() {
+			// These rotations place the camera in +X +Y +Z, aka south east, facing north west.
+			x_rot = 0.5;
+			y_rot = -0.5;
+			distance = 4;
+			fov_scale = 1.0;
+		}
+	};
+	Cursor cursor;
+
+	// Values taken from Node3DEditor.
+	const float VIEW_3D_MIN_ZOOM = 0.01;
+#ifdef REAL_T_IS_DOUBLE
+	const double VIEW_3D_MAX_ZOOM = 1'000'000'000'000;
+#else
+	const float VIEW_3D_MAX_ZOOM = 10'000;
+#endif
+	const float CAMERA_ZNEAR = 0.05;
+	const float CAMERA_ZFAR = 4'000;
+
+	const float CAMERA_BASE_FOV = 75;
+	const float CAMERA_MIN_FOV_SCALE = 0.1;
+	const float CAMERA_MAX_FOV_SCALE = 2.5;
+
+	const float FREELOOK_BASE_SPEED = 4;
+	const float RADS_PER_PIXEL = 0.004;
+
+	bool camera_first_override = true;
+	bool camera_freelook = false;
+
+	Vector2 previous_mouse_position;
+
+	Ref<ArrayMesh> sbox_3d_mesh;
+	Ref<ArrayMesh> sbox_3d_mesh_xray;
+	RID sbox_3d_instance;
+	RID sbox_3d_instance_ofs;
+	RID sbox_3d_instance_xray;
+	RID sbox_3d_instance_xray_ofs;
+	Transform3D sbox_3d_xform;
+	AABB sbox_3d_bounds;
+#endif
+
+	Point2 selection_position = Point2(INFINITY, INFINITY);
+	bool list_shortcut_pressed = false;
+
+	NodeType node_select_type = NODE_TYPE_2D;
+	SelectMode node_select_mode = SELECT_MODE_SINGLE;
+
+	void _setup();
+
+	void _node_set_type(NodeType p_type);
+	void _select_set_mode(SelectMode p_mode);
+
+	void _set_camera_override_enabled(bool p_enabled);
+
+	void _root_window_input(const Ref<InputEvent> &p_event);
+	void _items_popup_index_pressed(int p_index, PopupMenu *p_popup);
+	void _update_input_state();
+
+	void _process_frame();
+	void _physics_frame();
+
+	void _click_point();
+	void _select_node(Node *p_node);
+	void _queue_selection_update();
+	void _update_selection();
+	void _clear_selection();
+	void _set_selection_visible(bool p_visible);
+
+	void _find_canvas_items_at_pos(const Point2 &p_pos, Node *p_node, Vector<SelectResult> &r_items, const Transform2D &p_parent_xform = Transform2D(), const Transform2D &p_canvas_xform = Transform2D());
+	void _pan_callback(Vector2 p_scroll_vec, Ref<InputEvent> p_event);
+	void _zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref<InputEvent> p_event);
+	void _reset_camera_2d();
+	void _update_view_2d();
+
+#ifndef _3D_DISABLED
+	void _find_3d_items_at_pos(const Point2 &p_pos, Vector<SelectResult> &r_items);
+	bool _handle_3d_input(const Ref<InputEvent> &p_event);
+	void _set_camera_freelook_enabled(bool p_enabled);
+	void _cursor_scale_distance(real_t p_scale);
+	void _cursor_look(Ref<InputEventWithModifiers> p_event);
+	void _cursor_pan(Ref<InputEventWithModifiers> p_event);
+	void _cursor_orbit(Ref<InputEventWithModifiers> p_event);
+	Transform3D _get_cursor_transform();
+	void _reset_camera_3d();
+#endif
+
+	RuntimeNodeSelect() { singleton = this; }
+
+	inline static RuntimeNodeSelect *singleton = nullptr;
+
+public:
+	static RuntimeNodeSelect *get_singleton();
+
+	~RuntimeNodeSelect();
+};
 #endif
 
 #endif // SCENE_DEBUGGER_H

+ 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);
 		} break;
 
+		case NOTIFICATION_SUSPENDED:
 		case NOTIFICATION_PAUSED: {
 			if (is_playing() && !is_paused()) {
 				paused_from_tree = true;
@@ -189,6 +190,13 @@ void VideoStreamPlayer::_notification(int p_notification) {
 			}
 		} break;
 
+		case NOTIFICATION_UNSUSPENDED: {
+			if (get_tree()->is_paused()) {
+				break;
+			}
+			[[fallthrough]];
+		}
+
 		case NOTIFICATION_UNPAUSED: {
 			if (paused_from_tree) {
 				paused_from_tree = false;

+ 12 - 1
scene/main/node.cpp

@@ -184,6 +184,7 @@ void Node::_notification(int p_notification) {
 			}
 		} break;
 
+		case NOTIFICATION_SUSPENDED:
 		case NOTIFICATION_PAUSED: {
 			if (is_physics_interpolated_and_enabled() && is_inside_tree()) {
 				reset_physics_interpolation();
@@ -695,6 +696,16 @@ void Node::_propagate_pause_notification(bool p_enable) {
 	data.blocked--;
 }
 
+void Node::_propagate_suspend_notification(bool p_enable) {
+	notification(p_enable ? NOTIFICATION_SUSPENDED : NOTIFICATION_UNSUSPENDED);
+
+	data.blocked++;
+	for (KeyValue<StringName, Node *> &KV : data.children) {
+		KV.value->_propagate_suspend_notification(p_enable);
+	}
+	data.blocked--;
+}
+
 Node::ProcessMode Node::get_process_mode() const {
 	return data.process_mode;
 }
@@ -850,7 +861,7 @@ bool Node::can_process_notification(int p_what) const {
 
 bool Node::can_process() const {
 	ERR_FAIL_COND_V(!is_inside_tree(), false);
-	return _can_process(get_tree()->is_paused());
+	return !get_tree()->is_suspended() && _can_process(get_tree()->is_paused());
 }
 
 bool Node::_can_process(bool p_paused) const {

+ 3 - 0
scene/main/node.h

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

+ 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) {
 	ERR_FAIL_COND_MSG(!Thread::is_main_thread(), "Pause can only be set from the main thread.");
+	ERR_FAIL_COND_MSG(suspended, "Pause state cannot be modified while suspended.");
 
 	if (p_enabled == paused) {
 		return;
 	}
+
 	paused = p_enabled;
+
 #ifndef _3D_DISABLED
 	PhysicsServer3D::get_singleton()->set_active(!p_enabled);
 #endif // _3D_DISABLED
@@ -972,6 +975,30 @@ bool SceneTree::is_paused() const {
 	return paused;
 }
 
+void SceneTree::set_suspend(bool p_enabled) {
+	ERR_FAIL_COND_MSG(!Thread::is_main_thread(), "Suspend can only be set from the main thread.");
+
+	if (p_enabled == suspended) {
+		return;
+	}
+
+	suspended = p_enabled;
+
+	Engine::get_singleton()->set_freeze_time_scale(p_enabled);
+
+#ifndef _3D_DISABLED
+	PhysicsServer3D::get_singleton()->set_active(!p_enabled && !paused);
+#endif // _3D_DISABLED
+	PhysicsServer2D::get_singleton()->set_active(!p_enabled && !paused);
+	if (get_root()) {
+		get_root()->_propagate_suspend_notification(p_enabled);
+	}
+}
+
+bool SceneTree::is_suspended() const {
+	return suspended;
+}
+
 void SceneTree::_process_group(ProcessGroup *p_group, bool p_physics) {
 	// When reading this function, keep in mind that this code must work in a way where
 	// if any node is removed, this needs to continue working.

+ 3 - 0
scene/main/scene_tree.h

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

+ 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(p_event.is_null());
 
-	if (disable_input) {
+	if (disable_input || disable_input_override) {
 		return;
 	}
 
@@ -3195,7 +3195,7 @@ void Viewport::push_unhandled_input(const Ref<InputEvent> &p_event, bool p_local
 
 	local_input_handled = false;
 
-	if (disable_input || !_can_consume_input_events()) {
+	if (disable_input || disable_input_override || !_can_consume_input_events()) {
 		return;
 	}
 
@@ -3298,7 +3298,7 @@ void Viewport::set_disable_input(bool p_disable) {
 	if (p_disable == disable_input) {
 		return;
 	}
-	if (p_disable) {
+	if (p_disable && !disable_input_override) {
 		_drop_mouse_focus();
 		_mouse_leave_viewport();
 		_gui_cancel_tooltip();
@@ -3311,6 +3311,19 @@ bool Viewport::is_input_disabled() const {
 	return disable_input;
 }
 
+void Viewport::set_disable_input_override(bool p_disable) {
+	ERR_MAIN_THREAD_GUARD;
+	if (p_disable == disable_input_override) {
+		return;
+	}
+	if (p_disable && !disable_input) {
+		_drop_mouse_focus();
+		_mouse_leave_viewport();
+		_gui_cancel_tooltip();
+	}
+	disable_input_override = p_disable;
+}
+
 Variant Viewport::gui_get_drag_data() const {
 	ERR_READ_THREAD_GUARD_V(Variant());
 	return get_section_root_viewport()->gui.drag_data;
@@ -4237,6 +4250,22 @@ void Viewport::set_camera_3d_override_orthogonal(real_t p_size, real_t p_z_near,
 	}
 }
 
+HashMap<StringName, real_t> Viewport::get_camera_3d_override_properties() const {
+	HashMap<StringName, real_t> props;
+
+	props["size"] = 0;
+	props["fov"] = 0;
+	props["z_near"] = 0;
+	props["z_far"] = 0;
+	ERR_READ_THREAD_GUARD_V(props);
+
+	props["size"] = camera_3d_override.size;
+	props["fov"] = camera_3d_override.fov;
+	props["z_near"] = camera_3d_override.z_near;
+	props["z_far"] = camera_3d_override.z_far;
+	return props;
+}
+
 void Viewport::set_disable_3d(bool p_disable) {
 	ERR_MAIN_THREAD_GUARD;
 	disable_3d = p_disable;
@@ -4270,6 +4299,54 @@ Transform3D Viewport::get_camera_3d_override_transform() const {
 	return Transform3D();
 }
 
+Vector3 Viewport::camera_3d_override_project_ray_normal(const Point2 &p_pos) const {
+	ERR_READ_THREAD_GUARD_V(Vector3());
+	Vector3 ray = camera_3d_override_project_local_ray_normal(p_pos);
+	return camera_3d_override.transform.basis.xform(ray).normalized();
+}
+
+Vector3 Viewport::camera_3d_override_project_local_ray_normal(const Point2 &p_pos) const {
+	ERR_READ_THREAD_GUARD_V(Vector3());
+	Size2 viewport_size = get_camera_rect_size();
+	Vector2 cpos = get_camera_coords(p_pos);
+	Vector3 ray;
+
+	if (camera_3d_override.projection == Camera3DOverrideData::PROJECTION_ORTHOGONAL) {
+		ray = Vector3(0, 0, -1);
+	} else {
+		Projection cm;
+		cm.set_perspective(camera_3d_override.fov, get_visible_rect().size.aspect(), camera_3d_override.z_near, camera_3d_override.z_far, false);
+
+		Vector2 screen_he = cm.get_viewport_half_extents();
+		ray = Vector3(((cpos.x / viewport_size.width) * 2.0 - 1.0) * screen_he.x, ((1.0 - (cpos.y / viewport_size.height)) * 2.0 - 1.0) * screen_he.y, -camera_3d_override.z_near).normalized();
+	}
+
+	return ray;
+}
+
+Vector3 Viewport::camera_3d_override_project_ray_origin(const Point2 &p_pos) const {
+	ERR_READ_THREAD_GUARD_V(Vector3());
+	Size2 viewport_size = get_camera_rect_size();
+	Vector2 cpos = get_camera_coords(p_pos);
+	ERR_FAIL_COND_V(viewport_size.y == 0, Vector3());
+
+	if (camera_3d_override.projection == Camera3DOverrideData::PROJECTION_ORTHOGONAL) {
+		Vector2 pos = cpos / viewport_size;
+		real_t vsize, hsize;
+		hsize = camera_3d_override.size * viewport_size.aspect();
+		vsize = camera_3d_override.size;
+
+		Vector3 ray;
+		ray.x = pos.x * (hsize)-hsize / 2;
+		ray.y = (1.0 - pos.y) * (vsize)-vsize / 2;
+		ray.z = -camera_3d_override.z_near;
+		ray = camera_3d_override.transform.xform(ray);
+		return ray;
+	} else {
+		return camera_3d_override.transform.origin;
+	};
+}
+
 Ref<World3D> Viewport::get_world_3d() const {
 	ERR_READ_THREAD_GUARD_V(Ref<World3D>());
 	return world_3d;

+ 8 - 0
scene/main/viewport.h

@@ -401,6 +401,7 @@ private:
 	DefaultCanvasItemTextureRepeat default_canvas_item_texture_repeat = DEFAULT_CANVAS_ITEM_TEXTURE_REPEAT_DISABLED;
 
 	bool disable_input = false;
+	bool disable_input_override = false;
 
 	void _gui_call_input(Control *p_control, const Ref<InputEvent> &p_input);
 	void _gui_call_notification(Control *p_control, int p_what);
@@ -580,6 +581,8 @@ public:
 	void set_disable_input(bool p_disable);
 	bool is_input_disabled() const;
 
+	void set_disable_input_override(bool p_disable);
+
 	Vector2 get_mouse_position() const;
 	void warp_mouse(const Vector2 &p_position);
 	virtual void update_mouse_cursor_state();
@@ -770,6 +773,11 @@ public:
 
 	void set_camera_3d_override_perspective(real_t p_fovy_degrees, real_t p_z_near, real_t p_z_far);
 	void set_camera_3d_override_orthogonal(real_t p_size, real_t p_z_near, real_t p_z_far);
+	HashMap<StringName, real_t> get_camera_3d_override_properties() const;
+
+	Vector3 camera_3d_override_project_ray_normal(const Point2 &p_pos) const;
+	Vector3 camera_3d_override_project_ray_origin(const Point2 &p_pos) const;
+	Vector3 camera_3d_override_project_local_ray_normal(const Point2 &p_pos) const;
 
 	void set_disable_3d(bool p_disable);
 	bool is_3d_disabled() const;