diff --git a/core/config/engine.cpp b/core/config/engine.cpp index 12ada98d43d..aac048e93f7 100644 --- a/core/config/engine.cpp +++ b/core/config/engine.cpp @@ -116,6 +116,10 @@ void Engine::set_time_scale(double p_scale) { } double Engine::get_time_scale() const { + return freeze_time_scale ? 0 : _time_scale; +} + +double Engine::get_unfrozen_time_scale() const { return _time_scale; } @@ -404,6 +408,10 @@ bool Engine::notify_frame_server_synced() { return server_syncs > SERVER_SYNC_FRAME_COUNT_WARNING; } +void Engine::set_freeze_time_scale(bool p_frozen) { + freeze_time_scale = p_frozen; +} + Engine::Engine() { singleton = this; } diff --git a/core/config/engine.h b/core/config/engine.h index fd7fbd717eb..b38412308ae 100644 --- a/core/config/engine.h +++ b/core/config/engine.h @@ -99,6 +99,8 @@ private: int server_syncs = 0; bool frame_server_synced = false; + bool freeze_time_scale = false; + public: static Engine *get_singleton(); @@ -130,6 +132,7 @@ public: void set_time_scale(double p_scale); double get_time_scale() const; + double get_unfrozen_time_scale() const; void set_print_to_stdout(bool p_enabled); bool is_printing_to_stdout() const; @@ -197,6 +200,8 @@ public: void increment_frames_drawn(); bool notify_frame_server_synced(); + void set_freeze_time_scale(bool p_frozen); + Engine(); virtual ~Engine(); }; diff --git a/core/input/input.cpp b/core/input/input.cpp index 6261a435fa5..4117193c8b7 100644 --- a/core/input/input.cpp +++ b/core/input/input.cpp @@ -87,11 +87,50 @@ Input *Input::get_singleton() { void Input::set_mouse_mode(MouseMode p_mode) { ERR_FAIL_INDEX((int)p_mode, 5); + + if (p_mode == mouse_mode) { + return; + } + + // Allow to be set even if overridden, to see if the platform allows the mode. set_mouse_mode_func(p_mode); + mouse_mode = get_mouse_mode_func(); + + if (mouse_mode_override_enabled) { + set_mouse_mode_func(mouse_mode_override); + } } Input::MouseMode Input::get_mouse_mode() const { - return get_mouse_mode_func(); + return mouse_mode; +} + +void Input::set_mouse_mode_override_enabled(bool p_enabled) { + if (p_enabled == mouse_mode_override_enabled) { + return; + } + + mouse_mode_override_enabled = p_enabled; + + if (p_enabled) { + set_mouse_mode_func(mouse_mode_override); + mouse_mode_override = get_mouse_mode_func(); + } else { + set_mouse_mode_func(mouse_mode); + } +} + +void Input::set_mouse_mode_override(MouseMode p_mode) { + ERR_FAIL_INDEX((int)p_mode, 5); + + if (p_mode == mouse_mode_override) { + return; + } + + if (mouse_mode_override_enabled) { + set_mouse_mode_func(p_mode); + mouse_mode_override = get_mouse_mode_func(); + } } void Input::_bind_methods() { @@ -252,6 +291,10 @@ Input::VelocityTrack::VelocityTrack() { bool Input::is_anything_pressed() const { _THREAD_SAFE_METHOD_ + if (disable_input) { + return false; + } + if (!keys_pressed.is_empty() || !joy_buttons_pressed.is_empty() || !mouse_button_mask.is_empty()) { return true; } @@ -267,21 +310,41 @@ bool Input::is_anything_pressed() const { bool Input::is_key_pressed(Key p_keycode) const { _THREAD_SAFE_METHOD_ + + if (disable_input) { + return false; + } + return keys_pressed.has(p_keycode); } bool Input::is_physical_key_pressed(Key p_keycode) const { _THREAD_SAFE_METHOD_ + + if (disable_input) { + return false; + } + return physical_keys_pressed.has(p_keycode); } bool Input::is_key_label_pressed(Key p_keycode) const { _THREAD_SAFE_METHOD_ + + if (disable_input) { + return false; + } + return key_label_pressed.has(p_keycode); } bool Input::is_mouse_button_pressed(MouseButton p_button) const { _THREAD_SAFE_METHOD_ + + if (disable_input) { + return false; + } + return mouse_button_mask.has_flag(mouse_button_to_mask(p_button)); } @@ -295,11 +358,21 @@ static JoyButton _combine_device(JoyButton p_value, int p_device) { bool Input::is_joy_button_pressed(int p_device, JoyButton p_button) const { _THREAD_SAFE_METHOD_ + + if (disable_input) { + return false; + } + return joy_buttons_pressed.has(_combine_device(p_button, p_device)); } bool Input::is_action_pressed(const StringName &p_action, bool p_exact) const { ERR_FAIL_COND_V_MSG(!InputMap::get_singleton()->has_action(p_action), false, InputMap::get_singleton()->suggest_actions(p_action)); + + if (disable_input) { + return false; + } + HashMap::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::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::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::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::ConstIterator E = action_states.find(p_action); if (!E) { return 0.0f; @@ -410,6 +503,11 @@ Vector2 Input::get_vector(const StringName &p_negative_x, const StringName &p_po float Input::get_joy_axis(int p_device, JoyAxis p_axis) const { _THREAD_SAFE_METHOD_ + + if (disable_input) { + return 0; + } + JoyAxis c = _combine_device(p_axis, p_device); if (_joy_axis.has(c)) { return _joy_axis[c]; @@ -1664,6 +1762,14 @@ int Input::get_unused_joy_id() { return -1; } +void Input::set_disable_input(bool p_disable) { + disable_input = p_disable; +} + +bool Input::is_input_disabled() const { + return disable_input; +} + Input::Input() { singleton = this; diff --git a/core/input/input.h b/core/input/input.h index 95dd623cc03..a189ae7d9ad 100644 --- a/core/input/input.h +++ b/core/input/input.h @@ -103,6 +103,11 @@ private: Vector2 mouse_pos; int64_t mouse_window = 0; bool legacy_just_pressed_behavior = false; + bool disable_input = false; + + MouseMode mouse_mode = MOUSE_MODE_VISIBLE; + bool mouse_mode_override_enabled = false; + MouseMode mouse_mode_override = MOUSE_MODE_VISIBLE; struct ActionState { uint64_t pressed_physics_frame = UINT64_MAX; @@ -279,6 +284,8 @@ protected: public: void set_mouse_mode(MouseMode p_mode); MouseMode get_mouse_mode() const; + void set_mouse_mode_override_enabled(bool p_enabled); + void set_mouse_mode_override(MouseMode p_mode); #ifdef TOOLS_ENABLED void get_argument_options(const StringName &p_function, int p_idx, List *r_options) const override; @@ -380,6 +387,9 @@ public: void set_event_dispatch_function(EventDispatchFunc p_function); + void set_disable_input(bool p_disable); + bool is_input_disabled() const; + Input(); ~Input(); }; diff --git a/doc/classes/EditorFeatureProfile.xml b/doc/classes/EditorFeatureProfile.xml index 3aa1e63aac1..c125c923efb 100644 --- a/doc/classes/EditorFeatureProfile.xml +++ b/doc/classes/EditorFeatureProfile.xml @@ -121,7 +121,10 @@ The History dock. If this feature is disabled, the History dock won't be visible. - + + 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. + + Represents the size of the [enum Feature] enum. diff --git a/editor/debugger/editor_debugger_node.cpp b/editor/debugger/editor_debugger_node.cpp index ee8aae661b4..0f948b4ed59 100644 --- a/editor/debugger/editor_debugger_node.cpp +++ b/editor/debugger/editor_debugger_node.cpp @@ -105,6 +105,7 @@ ScriptEditorDebugger *EditorDebuggerNode::_add_debugger() { node->connect("breakpoint_selected", callable_mp(this, &EditorDebuggerNode::_error_selected).bind(id)); node->connect("clear_execution", callable_mp(this, &EditorDebuggerNode::_clear_execution)); node->connect("breaked", callable_mp(this, &EditorDebuggerNode::_breaked).bind(id)); + node->connect("remote_tree_select_requested", callable_mp(this, &EditorDebuggerNode::_remote_tree_select_requested).bind(id)); node->connect("remote_tree_updated", callable_mp(this, &EditorDebuggerNode::_remote_tree_updated).bind(id)); node->connect("remote_object_updated", callable_mp(this, &EditorDebuggerNode::_remote_object_updated).bind(id)); node->connect("remote_object_property_updated", callable_mp(this, &EditorDebuggerNode::_remote_object_property_updated).bind(id)); @@ -637,6 +638,13 @@ void EditorDebuggerNode::request_remote_tree() { get_current_debugger()->request_remote_tree(); } +void EditorDebuggerNode::_remote_tree_select_requested(ObjectID p_id, int p_debugger) { + if (p_debugger != tabs->get_current_tab()) { + return; + } + remote_scene_tree->select_node(p_id); +} + void EditorDebuggerNode::_remote_tree_updated(int p_debugger) { if (p_debugger != tabs->get_current_tab()) { return; diff --git a/editor/debugger/editor_debugger_node.h b/editor/debugger/editor_debugger_node.h index 12e097f6522..12c0d30c426 100644 --- a/editor/debugger/editor_debugger_node.h +++ b/editor/debugger/editor_debugger_node.h @@ -51,11 +51,8 @@ class EditorDebuggerNode : public MarginContainer { public: enum CameraOverride { OVERRIDE_NONE, - OVERRIDE_2D, - OVERRIDE_3D_1, // 3D Viewport 1 - OVERRIDE_3D_2, // 3D Viewport 2 - OVERRIDE_3D_3, // 3D Viewport 3 - OVERRIDE_3D_4 // 3D Viewport 4 + OVERRIDE_INGAME, + OVERRIDE_EDITORS, }; private: @@ -132,6 +129,7 @@ protected: void _debugger_stopped(int p_id); void _debugger_wants_stop(int p_id); void _debugger_changed(int p_tab); + void _remote_tree_select_requested(ObjectID p_id, int p_debugger); void _remote_tree_updated(int p_debugger); void _remote_tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button); void _remote_object_updated(ObjectID p_id, int p_debugger); diff --git a/editor/debugger/editor_debugger_tree.cpp b/editor/debugger/editor_debugger_tree.cpp index a9008426514..4d67800e6ec 100644 --- a/editor/debugger/editor_debugger_tree.cpp +++ b/editor/debugger/editor_debugger_tree.cpp @@ -30,6 +30,7 @@ #include "editor_debugger_tree.h" +#include "editor/debugger/editor_debugger_node.h" #include "editor/editor_node.h" #include "editor/editor_string_names.h" #include "editor/gui/editor_file_dialog.h" @@ -148,7 +149,8 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int updating_scene_tree = true; const String last_path = get_selected_path(); const String filter = SceneTreeDock::get_singleton()->get_filter(); - bool filter_changed = filter != last_filter; + bool should_scroll = scrolling_to_item || filter != last_filter; + scrolling_to_item = false; TreeItem *scroll_item = nullptr; // Nodes are in a flatten list, depth first. Use a stack of parents, avoid recursion. @@ -185,8 +187,18 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int // Select previously selected node. if (debugger_id == p_debugger) { // Can use remote id. if (node.id == inspected_object_id) { + if (selection_uncollapse_all) { + selection_uncollapse_all = false; + + // Temporarily set to `false`, to allow caching the unfolds. + updating_scene_tree = false; + item->uncollapse_tree(); + updating_scene_tree = true; + } + item->select(0); - if (filter_changed) { + + if (should_scroll) { scroll_item = item; } } @@ -194,7 +206,7 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int if (last_path == _get_path(item)) { updating_scene_tree = false; // Force emission of new selection. item->select(0); - if (filter_changed) { + if (should_scroll) { scroll_item = item; } updating_scene_tree = true; @@ -258,14 +270,30 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int } } } - debugger_id = p_debugger; // Needed by hook, could be avoided if every debugger had its own tree + + debugger_id = p_debugger; // Needed by hook, could be avoided if every debugger had its own tree. if (scroll_item) { - callable_mp((Tree *)this, &Tree::scroll_to_item).call_deferred(scroll_item, false); + scroll_to_item(scroll_item, false); } last_filter = filter; updating_scene_tree = false; } +void EditorDebuggerTree::select_node(ObjectID p_id) { + // Manually select, as the tree control may be out-of-date for some reason (e.g. not shown yet). + selection_uncollapse_all = true; + inspected_object_id = uint64_t(p_id); + scrolling_to_item = true; + emit_signal(SNAME("object_selected"), inspected_object_id, debugger_id); + + if (!updating_scene_tree) { + // Request a tree refresh. + EditorDebuggerNode::get_singleton()->request_remote_tree(); + } + // Set the value immediately, so no update flooding happens and causes a crash. + updating_scene_tree = true; +} + Variant EditorDebuggerTree::get_drag_data(const Point2 &p_point) { if (get_button_id_at_position(p_point) != -1) { return Variant(); diff --git a/editor/debugger/editor_debugger_tree.h b/editor/debugger/editor_debugger_tree.h index 705df17baf1..d048688cad2 100644 --- a/editor/debugger/editor_debugger_tree.h +++ b/editor/debugger/editor_debugger_tree.h @@ -49,6 +49,8 @@ private: ObjectID inspected_object_id; int debugger_id = 0; bool updating_scene_tree = false; + bool scrolling_to_item = false; + bool selection_uncollapse_all = false; HashSet unfold_cache; PopupMenu *item_menu = nullptr; EditorFileDialog *file_dialog = nullptr; @@ -78,6 +80,7 @@ public: ObjectID get_selected_object(); int get_current_debugger(); // Would love to have one tree for every debugger. void update_scene_tree(const SceneDebuggerTree *p_tree, int p_debugger); + void select_node(ObjectID p_id); EditorDebuggerTree(); }; diff --git a/editor/debugger/script_editor_debugger.cpp b/editor/debugger/script_editor_debugger.cpp index c42740de504..b78aad17218 100644 --- a/editor/debugger/script_editor_debugger.cpp +++ b/editor/debugger/script_editor_debugger.cpp @@ -806,6 +806,10 @@ void ScriptEditorDebugger::_parse_message(const String &p_msg, uint64_t p_thread } else if (p_msg == "request_quit") { emit_signal(SNAME("stop_requested")); _stop_and_notify(); + } else if (p_msg == "remote_node_clicked") { + if (!p_data.is_empty()) { + emit_signal(SNAME("remote_tree_select_requested"), p_data[0]); + } } else if (p_msg == "performance:profile_names") { Vector 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; + 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; + 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()); + Array msg; + msg.push_back(transform); + _put_msg("scene:transform_camera_2d", msg); + } + + // Node3D Editor + { + Node3DEditorViewport *viewport = Node3DEditor::get_singleton()->get_last_used_viewport(); + const Camera3D *cam = viewport->get_camera_3d(); + + Array msg; + msg.push_back(cam->get_camera_transform()); + if (cam->get_projection() == Camera3D::PROJECTION_ORTHOGONAL) { + msg.push_back(false); + msg.push_back(cam->get_size()); + } else { + msg.push_back(true); + msg.push_back(cam->get_fov()); + } + msg.push_back(cam->get_near()); + msg.push_back(cam->get_far()); + _put_msg("scene:transform_camera_3d", msg); } - msg.push_back(cam->get_near()); - msg.push_back(cam->get_far()); - _put_msg("scene:override_camera_3D:transform", msg); } + if (is_breaked() && can_request_idle_draw) { _put_msg("servers:draw", Array()); can_request_idle_draw = false; @@ -1469,23 +1478,10 @@ CameraOverride ScriptEditorDebugger::get_camera_override() const { } void ScriptEditorDebugger::set_camera_override(CameraOverride p_override) { - if (p_override == CameraOverride::OVERRIDE_2D && camera_override != CameraOverride::OVERRIDE_2D) { - Array msg; - msg.push_back(true); - _put_msg("scene:override_camera_2D:set", msg); - } else if (p_override != CameraOverride::OVERRIDE_2D && camera_override == CameraOverride::OVERRIDE_2D) { - Array msg; - msg.push_back(false); - _put_msg("scene:override_camera_2D:set", msg); - } else if (p_override >= CameraOverride::OVERRIDE_3D_1 && camera_override < CameraOverride::OVERRIDE_3D_1) { - Array msg; - msg.push_back(true); - _put_msg("scene:override_camera_3D:set", msg); - } else if (p_override < CameraOverride::OVERRIDE_3D_1 && camera_override >= CameraOverride::OVERRIDE_3D_1) { - Array msg; - msg.push_back(false); - _put_msg("scene:override_camera_3D:set", msg); - } + Array msg; + msg.push_back(p_override != CameraOverride::OVERRIDE_NONE); + msg.push_back(p_override == CameraOverride::OVERRIDE_EDITORS); + _put_msg("scene:override_cameras", msg); camera_override = p_override; } @@ -1776,6 +1772,7 @@ void ScriptEditorDebugger::_bind_methods() { ADD_SIGNAL(MethodInfo("remote_object_updated", PropertyInfo(Variant::INT, "id"))); ADD_SIGNAL(MethodInfo("remote_object_property_updated", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::STRING, "property"))); ADD_SIGNAL(MethodInfo("remote_tree_updated")); + ADD_SIGNAL(MethodInfo("remote_tree_select_requested", PropertyInfo(Variant::NODE_PATH, "path"))); ADD_SIGNAL(MethodInfo("output", PropertyInfo(Variant::STRING, "msg"), PropertyInfo(Variant::INT, "level"))); ADD_SIGNAL(MethodInfo("stack_dump", PropertyInfo(Variant::ARRAY, "stack_dump"))); ADD_SIGNAL(MethodInfo("stack_frame_vars", PropertyInfo(Variant::INT, "num_vars"))); diff --git a/editor/editor_feature_profile.cpp b/editor/editor_feature_profile.cpp index 44fc9e37020..7ffbc39d751 100644 --- a/editor/editor_feature_profile.cpp +++ b/editor/editor_feature_profile.cpp @@ -43,6 +43,7 @@ const char *EditorFeatureProfile::feature_names[FEATURE_MAX] = { TTRC("3D Editor"), TTRC("Script Editor"), + TTRC("Game View"), TTRC("Asset Library"), TTRC("Scene Tree Editing"), TTRC("Node Dock"), @@ -54,6 +55,7 @@ const char *EditorFeatureProfile::feature_names[FEATURE_MAX] = { const char *EditorFeatureProfile::feature_descriptions[FEATURE_MAX] = { TTRC("Allows to view and edit 3D scenes."), TTRC("Allows to edit scripts using the integrated script editor."), + TTRC("Provides tools for selecting and debugging nodes at runtime."), TTRC("Provides built-in access to the Asset Library."), TTRC("Allows editing the node hierarchy in the Scene dock."), TTRC("Allows to work with signals and groups of the node selected in the Scene dock."), @@ -65,6 +67,7 @@ const char *EditorFeatureProfile::feature_descriptions[FEATURE_MAX] = { const char *EditorFeatureProfile::feature_identifiers[FEATURE_MAX] = { "3d", "script", + "game", "asset_lib", "scene_tree", "node_dock", @@ -307,6 +310,7 @@ void EditorFeatureProfile::_bind_methods() { BIND_ENUM_CONSTANT(FEATURE_FILESYSTEM_DOCK); BIND_ENUM_CONSTANT(FEATURE_IMPORT_DOCK); BIND_ENUM_CONSTANT(FEATURE_HISTORY_DOCK); + BIND_ENUM_CONSTANT(FEATURE_GAME); BIND_ENUM_CONSTANT(FEATURE_MAX); } diff --git a/editor/editor_feature_profile.h b/editor/editor_feature_profile.h index 7458a04e195..e84936dd348 100644 --- a/editor/editor_feature_profile.h +++ b/editor/editor_feature_profile.h @@ -55,6 +55,7 @@ public: FEATURE_FILESYSTEM_DOCK, FEATURE_IMPORT_DOCK, FEATURE_HISTORY_DOCK, + FEATURE_GAME, FEATURE_MAX }; diff --git a/editor/editor_main_screen.h b/editor/editor_main_screen.h index 153a182bc21..ca78ceaa885 100644 --- a/editor/editor_main_screen.h +++ b/editor/editor_main_screen.h @@ -47,6 +47,7 @@ public: EDITOR_2D = 0, EDITOR_3D, EDITOR_SCRIPT, + EDITOR_GAME, EDITOR_ASSETLIB, }; diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 161e518fc6a..36b43b7e9b7 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -145,6 +145,7 @@ #include "editor/plugins/editor_plugin.h" #include "editor/plugins/editor_preview_plugins.h" #include "editor/plugins/editor_resource_conversion_plugin.h" +#include "editor/plugins/game_view_plugin.h" #include "editor/plugins/gdextension_export_plugin.h" #include "editor/plugins/material_editor_plugin.h" #include "editor/plugins/mesh_library_editor_plugin.h" @@ -357,6 +358,8 @@ void EditorNode::shortcut_input(const Ref &p_event) { editor_main_screen->select(EditorMainScreen::EDITOR_3D); } else if (ED_IS_SHORTCUT("editor/editor_script", p_event)) { editor_main_screen->select(EditorMainScreen::EDITOR_SCRIPT); + } else if (ED_IS_SHORTCUT("editor/editor_game", p_event)) { + editor_main_screen->select(EditorMainScreen::EDITOR_GAME); } else if (ED_IS_SHORTCUT("editor/editor_help", p_event)) { emit_signal(SNAME("request_help_search"), ""); } else if (ED_IS_SHORTCUT("editor/editor_assetlib", p_event) && AssetLibraryEditorPlugin::is_available()) { @@ -6577,6 +6580,7 @@ void EditorNode::_feature_profile_changed() { editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_3D, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_3D)); editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_SCRIPT, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_SCRIPT)); + editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_GAME, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_GAME)); if (AssetLibraryEditorPlugin::is_available()) { editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_ASSETLIB, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_ASSET_LIB)); } @@ -6587,6 +6591,7 @@ void EditorNode::_feature_profile_changed() { editor_dock_manager->set_dock_enabled(history_dock, true); editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_3D, true); editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_SCRIPT, true); + editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_GAME, true); if (AssetLibraryEditorPlugin::is_available()) { editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_ASSETLIB, true); } @@ -7714,6 +7719,7 @@ EditorNode::EditorNode() { add_editor_plugin(memnew(CanvasItemEditorPlugin)); add_editor_plugin(memnew(Node3DEditorPlugin)); add_editor_plugin(memnew(ScriptEditorPlugin)); + add_editor_plugin(memnew(GameViewPlugin)); EditorAudioBuses *audio_bus_editor = EditorAudioBuses::register_editor(); @@ -7896,12 +7902,14 @@ EditorNode::EditorNode() { ED_SHORTCUT_AND_COMMAND("editor/editor_2d", TTR("Open 2D Editor"), KeyModifierMask::CTRL | Key::F1); ED_SHORTCUT_AND_COMMAND("editor/editor_3d", TTR("Open 3D Editor"), KeyModifierMask::CTRL | Key::F2); ED_SHORTCUT_AND_COMMAND("editor/editor_script", TTR("Open Script Editor"), KeyModifierMask::CTRL | Key::F3); - ED_SHORTCUT_AND_COMMAND("editor/editor_assetlib", TTR("Open Asset Library"), KeyModifierMask::CTRL | Key::F4); + ED_SHORTCUT_AND_COMMAND("editor/editor_game", TTR("Open Game View"), KeyModifierMask::CTRL | Key::F4); + ED_SHORTCUT_AND_COMMAND("editor/editor_assetlib", TTR("Open Asset Library"), KeyModifierMask::CTRL | Key::F5); ED_SHORTCUT_OVERRIDE("editor/editor_2d", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_1); ED_SHORTCUT_OVERRIDE("editor/editor_3d", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_2); ED_SHORTCUT_OVERRIDE("editor/editor_script", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_3); - ED_SHORTCUT_OVERRIDE("editor/editor_assetlib", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_4); + ED_SHORTCUT_OVERRIDE("editor/editor_game", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_4); + ED_SHORTCUT_OVERRIDE("editor/editor_assetlib", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::KEY_5); ED_SHORTCUT_AND_COMMAND("editor/editor_next", TTR("Open the next Editor")); ED_SHORTCUT_AND_COMMAND("editor/editor_prev", TTR("Open the previous Editor")); diff --git a/editor/icons/2DNodes.svg b/editor/icons/2DNodes.svg new file mode 100644 index 00000000000..90b92a4bc7b --- /dev/null +++ b/editor/icons/2DNodes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/icons/Camera.svg b/editor/icons/Camera.svg new file mode 100644 index 00000000000..8612d458a73 --- /dev/null +++ b/editor/icons/Camera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/icons/Game.svg b/editor/icons/Game.svg new file mode 100644 index 00000000000..e75e5c53129 --- /dev/null +++ b/editor/icons/Game.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/icons/NextFrame.svg b/editor/icons/NextFrame.svg new file mode 100644 index 00000000000..9609b2538b5 --- /dev/null +++ b/editor/icons/NextFrame.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp index 51992a0b21d..e3cf3dbbf21 100644 --- a/editor/plugins/canvas_item_editor_plugin.cpp +++ b/editor/plugins/canvas_item_editor_plugin.cpp @@ -3977,7 +3977,6 @@ void CanvasItemEditor::_update_editor_settings() { grid_snap_button->set_button_icon(get_editor_theme_icon(SNAME("SnapGrid"))); snap_config_menu->set_button_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl"))); skeleton_menu->set_button_icon(get_editor_theme_icon(SNAME("Bone"))); - override_camera_button->set_button_icon(get_editor_theme_icon(SNAME("Camera2D"))); pan_button->set_button_icon(get_editor_theme_icon(SNAME("ToolPan"))); ruler_button->set_button_icon(get_editor_theme_icon(SNAME("Ruler"))); pivot_button->set_button_icon(get_editor_theme_icon(SNAME("EditPivot"))); @@ -4016,8 +4015,6 @@ void CanvasItemEditor::_notification(int p_what) { case NOTIFICATION_READY: { _update_lock_and_group_button(); - EditorRunBar::get_singleton()->connect("play_pressed", callable_mp(this, &CanvasItemEditor::_update_override_camera_button).bind(true)); - EditorRunBar::get_singleton()->connect("stop_pressed", callable_mp(this, &CanvasItemEditor::_update_override_camera_button).bind(false)); ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &CanvasItemEditor::_project_settings_changed)); } break; @@ -4116,15 +4113,6 @@ void CanvasItemEditor::_notification(int p_what) { _update_editor_settings(); } break; - case NOTIFICATION_VISIBILITY_CHANGED: { - if (!is_visible() && override_camera_button->is_pressed()) { - EditorDebuggerNode *debugger = EditorDebuggerNode::get_singleton(); - - debugger->set_camera_override(EditorDebuggerNode::OVERRIDE_NONE); - override_camera_button->set_pressed(false); - } - } break; - case NOTIFICATION_APPLICATION_FOCUS_OUT: case NOTIFICATION_WM_WINDOW_FOCUS_OUT: { if (drag_type != DRAG_NONE) { @@ -4282,16 +4270,6 @@ void CanvasItemEditor::_button_toggle_grid_snap(bool p_status) { viewport->queue_redraw(); } -void CanvasItemEditor::_button_override_camera(bool p_pressed) { - EditorDebuggerNode *debugger = EditorDebuggerNode::get_singleton(); - - if (p_pressed) { - debugger->set_camera_override(EditorDebuggerNode::OVERRIDE_2D); - } else { - debugger->set_camera_override(EditorDebuggerNode::OVERRIDE_NONE); - } -} - void CanvasItemEditor::_button_tool_select(int p_index) { Button *tb[TOOL_MAX] = { select_button, list_select_button, move_button, scale_button, rotate_button, pivot_button, pan_button, ruler_button }; for (int i = 0; i < TOOL_MAX; i++) { @@ -4398,17 +4376,6 @@ void CanvasItemEditor::_insert_animation_keys(bool p_location, bool p_rotation, te->commit_insert_queue(); } -void CanvasItemEditor::_update_override_camera_button(bool p_game_running) { - if (p_game_running) { - override_camera_button->set_disabled(false); - override_camera_button->set_tooltip_text(TTR("Project Camera Override\nOverrides the running project's camera with the editor viewport camera.")); - } else { - override_camera_button->set_disabled(true); - override_camera_button->set_pressed(false); - override_camera_button->set_tooltip_text(TTR("Project Camera Override\nNo project instance running. Run the project from the editor to use this feature.")); - } -} - void CanvasItemEditor::_popup_callback(int p_op) { EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); last_option = MenuOption(p_op); @@ -5514,16 +5481,6 @@ CanvasItemEditor::CanvasItemEditor() { main_menu_hbox->add_child(memnew(VSeparator)); - override_camera_button = memnew(Button); - override_camera_button->set_theme_type_variation("FlatButton"); - main_menu_hbox->add_child(override_camera_button); - override_camera_button->connect(SceneStringName(toggled), callable_mp(this, &CanvasItemEditor::_button_override_camera)); - override_camera_button->set_toggle_mode(true); - override_camera_button->set_disabled(true); - _update_override_camera_button(false); - - main_menu_hbox->add_child(memnew(VSeparator)); - view_menu = memnew(MenuButton); view_menu->set_flat(false); view_menu->set_theme_type_variation("FlatMenuButton"); diff --git a/editor/plugins/canvas_item_editor_plugin.h b/editor/plugins/canvas_item_editor_plugin.h index bae9efebc91..c5335bf9c19 100644 --- a/editor/plugins/canvas_item_editor_plugin.h +++ b/editor/plugins/canvas_item_editor_plugin.h @@ -335,7 +335,6 @@ private: Button *group_button = nullptr; Button *ungroup_button = nullptr; - Button *override_camera_button = nullptr; MenuButton *view_menu = nullptr; PopupMenu *grid_menu = nullptr; PopupMenu *theme_menu = nullptr; @@ -518,11 +517,8 @@ private: void _zoom_on_position(real_t p_zoom, Point2 p_position = Point2()); void _button_toggle_smart_snap(bool p_status); void _button_toggle_grid_snap(bool p_status); - void _button_override_camera(bool p_pressed); void _button_tool_select(int p_index); - void _update_override_camera_button(bool p_game_running); - HSplitContainer *left_panel_split = nullptr; HSplitContainer *right_panel_split = nullptr; VSplitContainer *bottom_split = nullptr; diff --git a/editor/plugins/game_view_plugin.cpp b/editor/plugins/game_view_plugin.cpp new file mode 100644 index 00000000000..f45af72e909 --- /dev/null +++ b/editor/plugins/game_view_plugin.cpp @@ -0,0 +1,478 @@ +/**************************************************************************/ +/* game_view_plugin.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "game_view_plugin.h" + +#include "editor/editor_main_screen.h" +#include "editor/editor_node.h" +#include "editor/editor_settings.h" +#include "editor/themes/editor_scale.h" +#include "scene/gui/button.h" +#include "scene/gui/menu_button.h" +#include "scene/gui/panel.h" +#include "scene/gui/separator.h" + +void GameViewDebugger::_session_started(Ref 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 &I : sessions) { + if (I->is_active()) { + I->send_message("scene:suspend_changed", message); + } + } +} + +void GameViewDebugger::next_frame() { + for (Ref &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 &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 &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 &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 &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 &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 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(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 p_debugger) { + debugger = p_debugger; + + // Add some margin to the sides for better aesthetics. + // This prevents the first button's hover/pressed effect from "touching" the panel's border, + // which looks ugly. + MarginContainer *toolbar_margin = memnew(MarginContainer); + toolbar_margin->add_theme_constant_override("margin_left", 4 * EDSCALE); + toolbar_margin->add_theme_constant_override("margin_right", 4 * EDSCALE); + add_child(toolbar_margin); + + HBoxContainer *main_menu_hbox = memnew(HBoxContainer); + toolbar_margin->add_child(main_menu_hbox); + + suspend_button = memnew(Button); + main_menu_hbox->add_child(suspend_button); + suspend_button->set_toggle_mode(true); + suspend_button->set_theme_type_variation("FlatButton"); + suspend_button->connect(SceneStringName(toggled), callable_mp(this, &GameView::_suspend_button_toggled)); + suspend_button->set_tooltip_text(TTR("Suspend")); + + next_frame_button = memnew(Button); + main_menu_hbox->add_child(next_frame_button); + next_frame_button->set_theme_type_variation("FlatButton"); + next_frame_button->connect(SceneStringName(pressed), callable_mp(*debugger, &GameViewDebugger::next_frame)); + next_frame_button->set_tooltip_text(TTR("Next Frame")); + + main_menu_hbox->add_child(memnew(VSeparator)); + + node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE] = memnew(Button); + main_menu_hbox->add_child(node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE]); + node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE]->set_text(TTR("Input")); + node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE]->set_toggle_mode(true); + node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE]->set_pressed(true); + node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE]->set_theme_type_variation("FlatButton"); + node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE]->connect(SceneStringName(pressed), callable_mp(this, &GameView::_node_type_pressed).bind(RuntimeNodeSelect::NODE_TYPE_NONE)); + node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE]->set_tooltip_text(TTR("Allow game input.")); + + node_type_button[RuntimeNodeSelect::NODE_TYPE_2D] = memnew(Button); + main_menu_hbox->add_child(node_type_button[RuntimeNodeSelect::NODE_TYPE_2D]); + node_type_button[RuntimeNodeSelect::NODE_TYPE_2D]->set_text(TTR("2D")); + node_type_button[RuntimeNodeSelect::NODE_TYPE_2D]->set_toggle_mode(true); + node_type_button[RuntimeNodeSelect::NODE_TYPE_2D]->set_theme_type_variation("FlatButton"); + node_type_button[RuntimeNodeSelect::NODE_TYPE_2D]->connect(SceneStringName(pressed), callable_mp(this, &GameView::_node_type_pressed).bind(RuntimeNodeSelect::NODE_TYPE_2D)); + node_type_button[RuntimeNodeSelect::NODE_TYPE_2D]->set_tooltip_text(TTR("Disable game input and allow to select Node2Ds, Controls, and manipulate the 2D camera.")); + +#ifndef _3D_DISABLED + node_type_button[RuntimeNodeSelect::NODE_TYPE_3D] = memnew(Button); + main_menu_hbox->add_child(node_type_button[RuntimeNodeSelect::NODE_TYPE_3D]); + node_type_button[RuntimeNodeSelect::NODE_TYPE_3D]->set_text(TTR("3D")); + node_type_button[RuntimeNodeSelect::NODE_TYPE_3D]->set_toggle_mode(true); + node_type_button[RuntimeNodeSelect::NODE_TYPE_3D]->set_theme_type_variation("FlatButton"); + node_type_button[RuntimeNodeSelect::NODE_TYPE_3D]->connect(SceneStringName(pressed), callable_mp(this, &GameView::_node_type_pressed).bind(RuntimeNodeSelect::NODE_TYPE_3D)); + node_type_button[RuntimeNodeSelect::NODE_TYPE_3D]->set_tooltip_text(TTR("Disable game input and allow to select Node3Ds and manipulate the 3D camera.")); +#endif // _3D_DISABLED + + main_menu_hbox->add_child(memnew(VSeparator)); + + hide_selection = memnew(Button); + main_menu_hbox->add_child(hide_selection); + hide_selection->set_toggle_mode(true); + hide_selection->set_theme_type_variation("FlatButton"); + hide_selection->connect(SceneStringName(toggled), callable_mp(this, &GameView::_hide_selection_toggled)); + hide_selection->set_tooltip_text(TTR("Toggle Selection Visibility")); + + main_menu_hbox->add_child(memnew(VSeparator)); + + select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE] = memnew(Button); + main_menu_hbox->add_child(select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE]); + select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE]->set_toggle_mode(true); + select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE]->set_pressed(true); + select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE]->set_theme_type_variation("FlatButton"); + select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE]->connect(SceneStringName(pressed), callable_mp(this, &GameView::_select_mode_pressed).bind(RuntimeNodeSelect::SELECT_MODE_SINGLE)); + select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE]->set_shortcut(ED_SHORTCUT("spatial_editor/tool_select", TTR("Select Mode"), Key::Q)); + select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE]->set_shortcut_context(this); + select_mode_button[RuntimeNodeSelect::SELECT_MODE_SINGLE]->set_tooltip_text(keycode_get_string((Key)KeyModifierMask::CMD_OR_CTRL) + TTR("Alt+RMB: Show list of all nodes at position clicked.")); + + select_mode_button[RuntimeNodeSelect::SELECT_MODE_LIST] = memnew(Button); + main_menu_hbox->add_child(select_mode_button[RuntimeNodeSelect::SELECT_MODE_LIST]); + select_mode_button[RuntimeNodeSelect::SELECT_MODE_LIST]->set_toggle_mode(true); + select_mode_button[RuntimeNodeSelect::SELECT_MODE_LIST]->set_theme_type_variation("FlatButton"); + select_mode_button[RuntimeNodeSelect::SELECT_MODE_LIST]->connect(SceneStringName(pressed), callable_mp(this, &GameView::_select_mode_pressed).bind(RuntimeNodeSelect::SELECT_MODE_LIST)); + select_mode_button[RuntimeNodeSelect::SELECT_MODE_LIST]->set_tooltip_text(TTR("Show list of selectable nodes at position clicked.")); + + main_menu_hbox->add_child(memnew(VSeparator)); + + camera_override_button = memnew(Button); + main_menu_hbox->add_child(camera_override_button); + camera_override_button->set_toggle_mode(true); + camera_override_button->set_theme_type_variation("FlatButton"); + camera_override_button->connect(SceneStringName(toggled), callable_mp(this, &GameView::_camera_override_button_toggled)); + camera_override_button->set_tooltip_text(TTR("Override the in-game camera.")); + + camera_override_menu = memnew(MenuButton); + main_menu_hbox->add_child(camera_override_menu); + camera_override_menu->set_flat(false); + camera_override_menu->set_theme_type_variation("FlatMenuButton"); + camera_override_menu->set_h_size_flags(SIZE_SHRINK_END); + camera_override_menu->set_tooltip_text(TTR("Camera Override Options")); + + PopupMenu *menu = camera_override_menu->get_popup(); + menu->connect(SceneStringName(id_pressed), callable_mp(this, &GameView::_camera_override_menu_id_pressed)); + menu->add_item(TTR("Reset 2D Camera"), CAMERA_RESET_2D); + menu->add_item(TTR("Reset 3D Camera"), CAMERA_RESET_3D); + menu->add_separator(); + menu->add_radio_check_item(TTR("Manipulate In-Game"), CAMERA_MODE_INGAME); + menu->set_item_checked(menu->get_item_index(CAMERA_MODE_INGAME), true); + menu->add_radio_check_item(TTR("Manipulate From Editors"), CAMERA_MODE_EDITORS); + + _update_debugger_buttons(); + + panel = memnew(Panel); + add_child(panel); + panel->set_theme_type_variation("GamePanel"); + panel->set_v_size_flags(SIZE_EXPAND_FILL); + + p_debugger->connect("session_started", callable_mp(this, &GameView::_sessions_changed)); + p_debugger->connect("session_stopped", callable_mp(this, &GameView::_sessions_changed)); +} + +/////// + +void GameViewPlugin::make_visible(bool p_visible) { + game_view->set_visible(p_visible); +} + +void GameViewPlugin::set_state(const Dictionary &p_state) { + game_view->set_state(p_state); +} + +Dictionary GameViewPlugin::get_state() const { + return game_view->get_state(); +} + +void GameViewPlugin::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + add_debugger_plugin(debugger); + } break; + case NOTIFICATION_EXIT_TREE: { + remove_debugger_plugin(debugger); + } break; + } +} + +GameViewPlugin::GameViewPlugin() { + debugger.instantiate(); + + game_view = memnew(GameView(debugger)); + game_view->set_v_size_flags(Control::SIZE_EXPAND_FILL); + EditorNode::get_singleton()->get_editor_main_screen()->get_control()->add_child(game_view); + game_view->hide(); +} + +GameViewPlugin::~GameViewPlugin() { +} diff --git a/editor/plugins/game_view_plugin.h b/editor/plugins/game_view_plugin.h new file mode 100644 index 00000000000..f8701c3e76f --- /dev/null +++ b/editor/plugins/game_view_plugin.h @@ -0,0 +1,152 @@ +/**************************************************************************/ +/* game_view_plugin.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef GAME_VIEW_PLUGIN_H +#define GAME_VIEW_PLUGIN_H + +#include "editor/debugger/editor_debugger_node.h" +#include "editor/plugins/editor_debugger_plugin.h" +#include "editor/plugins/editor_plugin.h" +#include "scene/debugger/scene_debugger.h" +#include "scene/gui/box_container.h" + +class GameViewDebugger : public EditorDebuggerPlugin { + GDCLASS(GameViewDebugger, EditorDebuggerPlugin); + +private: + Vector> 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 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 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 p_debugger); +}; + +class GameViewPlugin : public EditorPlugin { + GDCLASS(GameViewPlugin, EditorPlugin); + + GameView *game_view = nullptr; + + Ref debugger; + +protected: + void _notification(int p_what); + +public: + virtual String get_name() const override { return "Game"; } + bool has_main_screen() const override { return true; } + virtual void edit(Object *p_object) override {} + virtual bool handles(Object *p_object) const override { return false; } + virtual void make_visible(bool p_visible) override; + + virtual void set_state(const Dictionary &p_state) override; + virtual Dictionary get_state() const override; + + GameViewPlugin(); + ~GameViewPlugin(); +}; + +#endif // GAME_VIEW_PLUGIN_H diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp index 6fc73f69ddf..b877c0b83ef 100644 --- a/editor/plugins/node_3d_editor_plugin.cpp +++ b/editor/plugins/node_3d_editor_plugin.cpp @@ -1692,7 +1692,7 @@ void Node3DEditorViewport::_sinput(const Ref &p_event) { Ref 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(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(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]); } diff --git a/editor/plugins/node_3d_editor_plugin.h b/editor/plugins/node_3d_editor_plugin.h index 1b03362606a..d35fcb76537 100644 --- a/editor/plugins/node_3d_editor_plugin.h +++ b/editor/plugins/node_3d_editor_plugin.h @@ -622,7 +622,6 @@ public: enum ToolOptions { TOOL_OPT_LOCAL_COORDS, TOOL_OPT_USE_SNAP, - TOOL_OPT_OVERRIDE_CAMERA, TOOL_OPT_MAX }; @@ -632,6 +631,8 @@ private: Node3DEditorViewportContainer *viewport_base = nullptr; Node3DEditorViewport *viewports[VIEWPORTS_COUNT]; + int last_used_viewport = 0; + VSplitContainer *shader_split = nullptr; HSplitContainer *left_panel_split = nullptr; HSplitContainer *right_panel_split = nullptr; @@ -704,7 +705,6 @@ private: MENU_TOOL_LIST_SELECT, MENU_TOOL_LOCAL_COORDS, MENU_TOOL_USE_SNAP, - MENU_TOOL_OVERRIDE_CAMERA, MENU_TRANSFORM_CONFIGURE_SNAP, MENU_TRANSFORM_DIALOG, MENU_VIEW_USE_1_VIEWPORT, @@ -759,8 +759,6 @@ private: void _menu_item_pressed(int p_option); void _menu_item_toggled(bool pressed, int p_option); void _menu_gizmo_toggled(int p_option); - void _update_camera_override_button(bool p_game_running); - void _update_camera_override_viewport(Object *p_viewport); // Used for secondary menu items which are displayed depending on the currently selected node // (such as MeshInstance's "Mesh" menu). PanelContainer *context_toolbar_panel = nullptr; @@ -771,8 +769,6 @@ private: void _generate_selection_boxes(); - int camera_override_viewport_id; - void _init_indicators(); void _update_gizmos_menu(); void _update_gizmos_menu_theme(); @@ -781,6 +777,7 @@ private: void _finish_grid(); void _toggle_maximize_view(Object *p_viewport); + void _viewport_clicked(int p_viewport_idx); Node *custom_camera = nullptr; @@ -967,6 +964,7 @@ public: ERR_FAIL_INDEX_V(p_idx, static_cast(VIEWPORTS_COUNT), nullptr); return viewports[p_idx]; } + Node3DEditorViewport *get_last_used_viewport(); void add_gizmo_plugin(Ref p_plugin); void remove_gizmo_plugin(Ref p_plugin); diff --git a/editor/themes/editor_theme_manager.cpp b/editor/themes/editor_theme_manager.cpp index 4db43f07036..32079f3753f 100644 --- a/editor/themes/editor_theme_manager.cpp +++ b/editor/themes/editor_theme_manager.cpp @@ -1857,6 +1857,12 @@ void EditorThemeManager::_populate_editor_styles(const Ref &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 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 menu_transparent_style = p_config.button_style->duplicate(); menu_transparent_style->set_bg_color(Color(1, 1, 1, 0)); diff --git a/misc/extension_api_validation/4.3-stable.expected b/misc/extension_api_validation/4.3-stable.expected index 9e169d474d7..37706641152 100644 --- a/misc/extension_api_validation/4.3-stable.expected +++ b/misc/extension_api_validation/4.3-stable.expected @@ -7,7 +7,6 @@ should instead be used to justify these changes and describe how users should wo Add new entries at the end of the file. ## Changes between 4.3-stable and 4.4-stable - GH-95374 -------- Validate extension JSON: Error: Field 'classes/ShapeCast2D/properties/collision_result': getter changed value in new API, from "_get_collision_result" to &"get_collision_result". @@ -102,3 +101,10 @@ GH-97020 Validate extension JSON: Error: Field 'classes/AnimationNode/methods/_process': is_const changed value in new API, from true to false. `_process` virtual method fixed to be non const instead. + + +GH-97257 +-------- +Validate extension JSON: Error: Field 'classes/EditorFeatureProfile/enums/Feature/values/FEATURE_MAX': value changed value in new API, from 8.0 to 9. + +New entry to the `EditorFeatureProfile.Feature` enum added. Those need to go before `FEATURE_MAX`, which will always cause a compatibility break. diff --git a/scene/2d/camera_2d.cpp b/scene/2d/camera_2d.cpp index ebed1e84e6a..74f26df7062 100644 --- a/scene/2d/camera_2d.cpp +++ b/scene/2d/camera_2d.cpp @@ -302,6 +302,7 @@ void Camera2D::_notification(int p_what) { _interpolation_data.xform_prev = _interpolation_data.xform_curr; } break; + case NOTIFICATION_SUSPENDED: case NOTIFICATION_PAUSED: { if (is_physics_interpolated_and_enabled()) { _update_scroll(); diff --git a/scene/2d/gpu_particles_2d.cpp b/scene/2d/gpu_particles_2d.cpp index cfdcbee86a7..b50881cb893 100644 --- a/scene/2d/gpu_particles_2d.cpp +++ b/scene/2d/gpu_particles_2d.cpp @@ -696,6 +696,8 @@ void GPUParticles2D::_notification(int p_what) { RS::get_singleton()->particles_set_subemitter(particles, RID()); } break; + case NOTIFICATION_SUSPENDED: + case NOTIFICATION_UNSUSPENDED: case NOTIFICATION_PAUSED: case NOTIFICATION_UNPAUSED: { if (is_inside_tree()) { diff --git a/scene/2d/navigation_agent_2d.cpp b/scene/2d/navigation_agent_2d.cpp index d0fae611d88..f030473c4b7 100644 --- a/scene/2d/navigation_agent_2d.cpp +++ b/scene/2d/navigation_agent_2d.cpp @@ -253,12 +253,20 @@ void NavigationAgent2D::_notification(int p_what) { #endif // DEBUG_ENABLED } break; + case NOTIFICATION_SUSPENDED: case NOTIFICATION_PAUSED: { if (agent_parent) { NavigationServer2D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process()); } } break; + case NOTIFICATION_UNSUSPENDED: { + if (get_tree()->is_paused()) { + break; + } + [[fallthrough]]; + } + case NOTIFICATION_UNPAUSED: { if (agent_parent) { NavigationServer2D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process()); diff --git a/scene/2d/navigation_obstacle_2d.cpp b/scene/2d/navigation_obstacle_2d.cpp index 3bf90249f8b..f6502a77e94 100644 --- a/scene/2d/navigation_obstacle_2d.cpp +++ b/scene/2d/navigation_obstacle_2d.cpp @@ -104,6 +104,7 @@ void NavigationObstacle2D::_notification(int p_what) { #endif // DEBUG_ENABLED } break; + case NOTIFICATION_SUSPENDED: case NOTIFICATION_PAUSED: { if (!can_process()) { map_before_pause = map_current; @@ -115,6 +116,13 @@ void NavigationObstacle2D::_notification(int p_what) { NavigationServer2D::get_singleton()->obstacle_set_paused(obstacle, !can_process()); } break; + case NOTIFICATION_UNSUSPENDED: { + if (get_tree()->is_paused()) { + break; + } + [[fallthrough]]; + } + case NOTIFICATION_UNPAUSED: { if (!can_process()) { map_before_pause = map_current; diff --git a/scene/2d/touch_screen_button.cpp b/scene/2d/touch_screen_button.cpp index ff409272c54..cbfd7e38dc2 100644 --- a/scene/2d/touch_screen_button.cpp +++ b/scene/2d/touch_screen_button.cpp @@ -185,6 +185,7 @@ void TouchScreenButton::_notification(int p_what) { } } break; + case NOTIFICATION_SUSPENDED: case NOTIFICATION_PAUSED: { if (is_pressed()) { _release(); diff --git a/scene/3d/camera_3d.cpp b/scene/3d/camera_3d.cpp index aafc2141afb..2b841c4e0de 100644 --- a/scene/3d/camera_3d.cpp +++ b/scene/3d/camera_3d.cpp @@ -234,6 +234,7 @@ void Camera3D::_notification(int p_what) { } } break; + case NOTIFICATION_SUSPENDED: case NOTIFICATION_PAUSED: { if (is_physics_interpolated_and_enabled() && is_inside_tree() && is_visible_in_tree()) { _physics_interpolation_ensure_transform_calculated(true); diff --git a/scene/3d/gpu_particles_3d.cpp b/scene/3d/gpu_particles_3d.cpp index 2cef607d296..b48a3a87c70 100644 --- a/scene/3d/gpu_particles_3d.cpp +++ b/scene/3d/gpu_particles_3d.cpp @@ -511,6 +511,8 @@ void GPUParticles3D::_notification(int p_what) { RS::get_singleton()->particles_set_subemitter(particles, RID()); } break; + case NOTIFICATION_SUSPENDED: + case NOTIFICATION_UNSUSPENDED: case NOTIFICATION_PAUSED: case NOTIFICATION_UNPAUSED: { if (is_inside_tree()) { diff --git a/scene/3d/navigation_agent_3d.cpp b/scene/3d/navigation_agent_3d.cpp index 5bbb724e2fa..9242c2a2ea3 100644 --- a/scene/3d/navigation_agent_3d.cpp +++ b/scene/3d/navigation_agent_3d.cpp @@ -272,12 +272,20 @@ void NavigationAgent3D::_notification(int p_what) { #endif // DEBUG_ENABLED } break; + case NOTIFICATION_SUSPENDED: case NOTIFICATION_PAUSED: { if (agent_parent) { NavigationServer3D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process()); } } break; + case NOTIFICATION_UNSUSPENDED: { + if (get_tree()->is_paused()) { + break; + } + [[fallthrough]]; + } + case NOTIFICATION_UNPAUSED: { if (agent_parent) { NavigationServer3D::get_singleton()->agent_set_paused(get_rid(), !agent_parent->can_process()); diff --git a/scene/3d/navigation_obstacle_3d.cpp b/scene/3d/navigation_obstacle_3d.cpp index f2ac8f789c9..2eb04a0054e 100644 --- a/scene/3d/navigation_obstacle_3d.cpp +++ b/scene/3d/navigation_obstacle_3d.cpp @@ -119,6 +119,7 @@ void NavigationObstacle3D::_notification(int p_what) { #endif // DEBUG_ENABLED } break; + case NOTIFICATION_SUSPENDED: case NOTIFICATION_PAUSED: { if (!can_process()) { map_before_pause = map_current; @@ -130,6 +131,13 @@ void NavigationObstacle3D::_notification(int p_what) { NavigationServer3D::get_singleton()->obstacle_set_paused(obstacle, !can_process()); } break; + case NOTIFICATION_UNSUSPENDED: { + if (get_tree()->is_paused()) { + break; + } + [[fallthrough]]; + } + case NOTIFICATION_UNPAUSED: { if (!can_process()) { map_before_pause = map_current; diff --git a/scene/audio/audio_stream_player_internal.cpp b/scene/audio/audio_stream_player_internal.cpp index 7d1ed56ca8e..621aa31fc61 100644 --- a/scene/audio/audio_stream_player_internal.cpp +++ b/scene/audio/audio_stream_player_internal.cpp @@ -112,6 +112,7 @@ void AudioStreamPlayerInternal::notification(int p_what) { stream_playbacks.clear(); } break; + case Node::NOTIFICATION_SUSPENDED: case Node::NOTIFICATION_PAUSED: { if (!node->can_process()) { // Node can't process so we start fading out to silence @@ -119,6 +120,13 @@ void AudioStreamPlayerInternal::notification(int p_what) { } } break; + case Node::NOTIFICATION_UNSUSPENDED: { + if (node->get_tree()->is_paused()) { + break; + } + [[fallthrough]]; + } + case Node::NOTIFICATION_UNPAUSED: { set_stream_paused(false); } break; diff --git a/scene/debugger/scene_debugger.cpp b/scene/debugger/scene_debugger.cpp index 22e5238faef..b87285ed74f 100644 --- a/scene/debugger/scene_debugger.cpp +++ b/scene/debugger/scene_debugger.cpp @@ -31,20 +31,34 @@ #include "scene_debugger.h" #include "core/debugger/engine_debugger.h" -#include "core/debugger/engine_profiler.h" #include "core/io/marshalls.h" #include "core/object/script_language.h" #include "core/templates/local_vector.h" +#include "scene/2d/physics/collision_object_2d.h" +#include "scene/2d/physics/collision_polygon_2d.h" +#include "scene/2d/physics/collision_shape_2d.h" +#ifndef _3D_DISABLED +#include "scene/3d/label_3d.h" +#include "scene/3d/mesh_instance_3d.h" +#include "scene/3d/physics/collision_object_3d.h" +#include "scene/3d/physics/collision_shape_3d.h" +#include "scene/3d/sprite_3d.h" +#include "scene/resources/surface_tool.h" +#endif // _3D_DISABLED +#include "scene/gui/popup_menu.h" +#include "scene/main/canvas_layer.h" #include "scene/main/scene_tree.h" #include "scene/main/window.h" #include "scene/resources/packed_scene.h" - -SceneDebugger *SceneDebugger::singleton = nullptr; +#include "scene/theme/theme_db.h" SceneDebugger::SceneDebugger() { singleton = this; + #ifdef DEBUG_ENABLED LiveEditor::singleton = memnew(LiveEditor); + RuntimeNodeSelect::singleton = memnew(RuntimeNodeSelect); + EngineDebugger::register_message_capture("scene", EngineDebugger::Capture(nullptr, SceneDebugger::parse_message)); #endif } @@ -56,7 +70,13 @@ SceneDebugger::~SceneDebugger() { memdelete(LiveEditor::singleton); LiveEditor::singleton = nullptr; } + + if (RuntimeNodeSelect::singleton) { + memdelete(RuntimeNodeSelect::singleton); + RuntimeNodeSelect::singleton = nullptr; + } #endif + singleton = nullptr; } @@ -78,10 +98,15 @@ Error SceneDebugger::parse_message(void *p_user, const String &p_msg, const Arra if (!scene_tree) { return ERR_UNCONFIGURED; } + LiveEditor *live_editor = LiveEditor::get_singleton(); if (!live_editor) { return ERR_UNCONFIGURED; } + RuntimeNodeSelect *runtime_node_select = RuntimeNodeSelect::get_singleton(); + if (!runtime_node_select) { + return ERR_UNCONFIGURED; + } r_captured = true; if (p_msg == "request_scene_tree") { // Scene tree @@ -99,22 +124,34 @@ Error SceneDebugger::parse_message(void *p_user, const String &p_msg, const Arra ObjectID id = p_args[0]; _send_object_id(id); - } else if (p_msg == "override_camera_2D:set") { // Camera + } else if (p_msg == "suspend_changed") { ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA); - bool enforce = p_args[0]; - scene_tree->get_root()->enable_canvas_transform_override(enforce); + bool suspended = p_args[0]; + scene_tree->set_suspend(suspended); + runtime_node_select->_update_input_state(); - } else if (p_msg == "override_camera_2D:transform") { + } 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 == "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); -#ifndef _3D_DISABLED - } else if (p_msg == "override_camera_3D:set") { - ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA); - bool enable = p_args[0]; - scene_tree->get_root()->enable_camera_3d_override(enable); - } else if (p_msg == "override_camera_3D: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 == "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.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_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_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_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_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") { - 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_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_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_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") { - 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_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_node_call") { - ERR_FAIL_COND_V(p_args.size() < 2, ERR_INVALID_DATA); - LocalVector args; - LocalVector 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_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 args; + LocalVector 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 args; + LocalVector 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_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_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_remove_node") { + ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA); + live_editor->_remove_node_func(p_args[0]); + + if (!runtime_node_select->has_selection) { + runtime_node_select->_clear_selection(); + } + + } 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]); + + if (!runtime_node_select->has_selection) { + runtime_node_select->_clear_selection(); + } + + } 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_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->_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 args; - LocalVector 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.begins_with("runtime_node_select_")) { /// Runtime Node Selection + if (p_msg == "runtime_node_select_setup") { + runtime_node_select->_setup(); + + } 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 == "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 == "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 == "runtime_node_select_reset_camera_2d") { + runtime_node_select->_reset_camera_2d(); + + } else if (p_msg == "runtime_node_select_reset_camera_3d") { + runtime_node_select->_reset_camera_3d(); + + } 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 == "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_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_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_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_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 { 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(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(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 st = memnew(SurfaceTool); + Ref 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 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 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 &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 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 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(p_node); + if (ci) { + selected_node = p_node; + } else { +#ifndef _3D_DISABLED + Node3D *node_3d = Object::cast_to(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(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(ci); + if (collision_shape) { + Ref 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(selected_node); + + // Fallback. + AABB bounds(Vector3(-0.5, -0.5, -0.5), Vector3(1, 1, 1)); + + VisualInstance3D *visual_instance = Object::cast_to(node_3d); + if (visual_instance) { + bounds = visual_instance->get_aabb(); + } else { + CollisionShape3D *collision_shape = Object::cast_to(node_3d); + if (collision_shape) { + Ref 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 &r_items, const Transform2D &p_parent_xform, const Transform2D &p_canvas_xform) { + if (!p_node || Object::cast_to(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(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(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(ci) || Object::cast_to(ci)) { + CollisionObject2D *collision_object = Object::cast_to(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 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 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 &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 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(result.collider); + res.order = -pos.distance_to(Object::cast_to(res.item)->get_global_transform().xform(result.position)); + + // Fetch collision shapes. + CollisionObject3D *collision = Object::cast_to(result.collider); + if (collision) { + List owners; + collision->get_shape_owners(&owners); + for (const uint32_t &I : owners) { + SelectResult res_shape; + res_shape.item = Object::cast_to(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 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 mesh_collision; + + MeshInstance3D *mesh_instance = Object::cast_to(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(obj); + if (label) { + geo_instance = label; + mesh_collision = label->generate_triangle_mesh(); + } else { + Sprite3D *sprite = Object::cast_to(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(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 &p_event) { + Ref 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 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 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 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 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 p_event) { + Window *root = SceneTree::get_singleton()->get_root(); + // Reduce all sides of the area by 1, so warping works when windows are maximized/fullscreen. + const Vector2 relative = Input::get_singleton()->warp_mouse_motion(p_event, Rect2(Vector2(1, 1), root->get_size() - Vector2(2, 2))); + + cursor.x_rot += relative.y * RADS_PER_PIXEL; + // Clamp the Y rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented. + cursor.x_rot = CLAMP(cursor.x_rot, -1.57, 1.57); + + cursor.y_rot += relative.x * RADS_PER_PIXEL; + + SceneTree::get_singleton()->get_root()->set_camera_3d_override_transform(_get_cursor_transform()); +} + +Transform3D RuntimeNodeSelect::_get_cursor_transform() { + Transform3D camera_transform; + camera_transform.translate_local(cursor.pos); + camera_transform.basis.rotate(Vector3(1, 0, 0), -cursor.x_rot); + camera_transform.basis.rotate(Vector3(0, 1, 0), -cursor.y_rot); + camera_transform.translate_local(0, 0, cursor.distance); + + return camera_transform; +} + +void RuntimeNodeSelect::_reset_camera_3d() { + camera_first_override = true; + + Window *root = SceneTree::get_singleton()->get_root(); + Camera3D *camera = root->get_camera_3d(); + if (!camera) { + return; + } + + cursor = Cursor(); + Transform3D transform = camera->get_global_transform(); + transform.translate_local(0, 0, -cursor.distance); + cursor.pos = transform.origin; + + cursor.x_rot = -camera->get_global_rotation().x; + cursor.y_rot = -camera->get_global_rotation().y; + + cursor.fov_scale = CLAMP(camera->get_fov() / CAMERA_BASE_FOV, CAMERA_MIN_FOV_SCALE, CAMERA_MAX_FOV_SCALE); + + SceneTree::get_singleton()->get_root()->set_camera_3d_override_transform(_get_cursor_transform()); + SceneTree::get_singleton()->get_root()->set_camera_3d_override_perspective(CAMERA_BASE_FOV * cursor.fov_scale, CAMERA_ZNEAR, CAMERA_ZFAR); +} +#endif // _3D_DISABLED #endif diff --git a/scene/debugger/scene_debugger.h b/scene/debugger/scene_debugger.h index be2642a2aee..f9dd6161aa3 100644 --- a/scene/debugger/scene_debugger.h +++ b/scene/debugger/scene_debugger.h @@ -31,19 +31,21 @@ #ifndef SCENE_DEBUGGER_H #define SCENE_DEBUGGER_H -#include "core/object/class_db.h" +#include "core/input/shortcut.h" #include "core/object/ref_counted.h" #include "core/string/ustring.h" #include "core/templates/pair.h" #include "core/variant/array.h" +#include "scene/gui/view_panner.h" +#include "scene/resources/mesh.h" +class PopupMenu; class Script; class Node; class SceneDebugger { -public: private: - static SceneDebugger *singleton; + inline static SceneDebugger *singleton = nullptr; SceneDebugger(); @@ -59,6 +61,7 @@ private: static void _set_node_owner_recursive(Node *p_node, Node *p_owner); static void _set_object_property(ObjectID p_id, const String &p_property, const Variant &p_value); static void _send_object_id(ObjectID p_id, int p_max_size = 1 << 20); + static void _next_frame(); public: static Error parse_message(void *p_user, const String &p_msg, const Array &p_args, bool &r_captured); @@ -160,11 +163,161 @@ private: live_edit_root = NodePath("/root"); } - static LiveEditor *singleton; + inline static LiveEditor *singleton = nullptr; public: static LiveEditor *get_singleton(); }; + +class RuntimeNodeSelect : public Object { + GDCLASS(RuntimeNodeSelect, Object); + +public: + enum NodeType { + NODE_TYPE_NONE, + NODE_TYPE_2D, + NODE_TYPE_3D, + NODE_TYPE_MAX + }; + + enum SelectMode { + SELECT_MODE_SINGLE, + SELECT_MODE_LIST, + SELECT_MODE_MAX + }; + +private: + friend class SceneDebugger; + + struct SelectResult { + Node *item = nullptr; + real_t order = 0; + _FORCE_INLINE_ bool operator<(const SelectResult &p_rr) const { return p_rr.order < order; } + }; + + bool has_selection = false; + Node *selected_node = nullptr; + PopupMenu *selection_list = nullptr; + bool selection_visible = true; + bool selection_update_queued = false; + + bool camera_override = false; + + // Values taken from EditorZoomWidget. + const float VIEW_2D_MIN_ZOOM = 1.0 / 128; + const float VIEW_2D_MAX_ZOOM = 128; + + Ref 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 sbox_3d_mesh; + Ref 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 &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 &r_items, const Transform2D &p_parent_xform = Transform2D(), const Transform2D &p_canvas_xform = Transform2D()); + void _pan_callback(Vector2 p_scroll_vec, Ref p_event); + void _zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref p_event); + void _reset_camera_2d(); + void _update_view_2d(); + +#ifndef _3D_DISABLED + void _find_3d_items_at_pos(const Point2 &p_pos, Vector &r_items); + bool _handle_3d_input(const Ref &p_event); + void _set_camera_freelook_enabled(bool p_enabled); + void _cursor_scale_distance(real_t p_scale); + void _cursor_look(Ref p_event); + void _cursor_pan(Ref p_event); + void _cursor_orbit(Ref p_event); + Transform3D _get_cursor_transform(); + void _reset_camera_3d(); +#endif + + RuntimeNodeSelect() { singleton = this; } + + inline static RuntimeNodeSelect *singleton = nullptr; + +public: + static RuntimeNodeSelect *get_singleton(); + + ~RuntimeNodeSelect(); +}; #endif #endif // SCENE_DEBUGGER_H diff --git a/scene/gui/video_stream_player.cpp b/scene/gui/video_stream_player.cpp index 0b521f926df..878bceccbce 100644 --- a/scene/gui/video_stream_player.cpp +++ b/scene/gui/video_stream_player.cpp @@ -178,6 +178,7 @@ void VideoStreamPlayer::_notification(int p_notification) { draw_texture_rect(texture, Rect2(Point2(), s), false); } break; + case NOTIFICATION_SUSPENDED: case NOTIFICATION_PAUSED: { if (is_playing() && !is_paused()) { paused_from_tree = true; @@ -189,6 +190,13 @@ void VideoStreamPlayer::_notification(int p_notification) { } } break; + case NOTIFICATION_UNSUSPENDED: { + if (get_tree()->is_paused()) { + break; + } + [[fallthrough]]; + } + case NOTIFICATION_UNPAUSED: { if (paused_from_tree) { paused_from_tree = false; diff --git a/scene/main/node.cpp b/scene/main/node.cpp index d921cc5b676..8dc7b4a87cc 100644 --- a/scene/main/node.cpp +++ b/scene/main/node.cpp @@ -184,6 +184,7 @@ void Node::_notification(int p_notification) { } } break; + case NOTIFICATION_SUSPENDED: case NOTIFICATION_PAUSED: { if (is_physics_interpolated_and_enabled() && is_inside_tree()) { reset_physics_interpolation(); @@ -695,6 +696,16 @@ void Node::_propagate_pause_notification(bool p_enable) { data.blocked--; } +void Node::_propagate_suspend_notification(bool p_enable) { + notification(p_enable ? NOTIFICATION_SUSPENDED : NOTIFICATION_UNSUSPENDED); + + data.blocked++; + for (KeyValue &KV : data.children) { + KV.value->_propagate_suspend_notification(p_enable); + } + data.blocked--; +} + Node::ProcessMode Node::get_process_mode() const { return data.process_mode; } @@ -850,7 +861,7 @@ bool Node::can_process_notification(int p_what) const { bool Node::can_process() const { ERR_FAIL_COND_V(!is_inside_tree(), false); - return _can_process(get_tree()->is_paused()); + return !get_tree()->is_suspended() && _can_process(get_tree()->is_paused()); } bool Node::_can_process(bool p_paused) const { diff --git a/scene/main/node.h b/scene/main/node.h index 799478fa356..e2f3ce9b780 100644 --- a/scene/main/node.h +++ b/scene/main/node.h @@ -300,6 +300,7 @@ private: void _set_tree(SceneTree *p_tree); void _propagate_pause_notification(bool p_enable); + void _propagate_suspend_notification(bool p_enable); _FORCE_INLINE_ bool _can_process(bool p_paused) const; _FORCE_INLINE_ bool _is_enabled() const; @@ -439,6 +440,8 @@ public: // Editor specific node notifications NOTIFICATION_EDITOR_PRE_SAVE = 9001, NOTIFICATION_EDITOR_POST_SAVE = 9002, + NOTIFICATION_SUSPENDED = 9003, + NOTIFICATION_UNSUSPENDED = 9004 }; /* NODE/TREE */ diff --git a/scene/main/scene_tree.cpp b/scene/main/scene_tree.cpp index 71d91b970ee..60cecfcfe70 100644 --- a/scene/main/scene_tree.cpp +++ b/scene/main/scene_tree.cpp @@ -954,11 +954,14 @@ Ref SceneTree::get_debug_contact_mesh() { void SceneTree::set_pause(bool p_enabled) { ERR_FAIL_COND_MSG(!Thread::is_main_thread(), "Pause can only be set from the main thread."); + ERR_FAIL_COND_MSG(suspended, "Pause state cannot be modified while suspended."); if (p_enabled == paused) { return; } + paused = p_enabled; + #ifndef _3D_DISABLED PhysicsServer3D::get_singleton()->set_active(!p_enabled); #endif // _3D_DISABLED @@ -972,6 +975,30 @@ bool SceneTree::is_paused() const { return paused; } +void SceneTree::set_suspend(bool p_enabled) { + ERR_FAIL_COND_MSG(!Thread::is_main_thread(), "Suspend can only be set from the main thread."); + + if (p_enabled == suspended) { + return; + } + + suspended = p_enabled; + + Engine::get_singleton()->set_freeze_time_scale(p_enabled); + +#ifndef _3D_DISABLED + PhysicsServer3D::get_singleton()->set_active(!p_enabled && !paused); +#endif // _3D_DISABLED + PhysicsServer2D::get_singleton()->set_active(!p_enabled && !paused); + if (get_root()) { + get_root()->_propagate_suspend_notification(p_enabled); + } +} + +bool SceneTree::is_suspended() const { + return suspended; +} + void SceneTree::_process_group(ProcessGroup *p_group, bool p_physics) { // When reading this function, keep in mind that this code must work in a way where // if any node is removed, this needs to continue working. diff --git a/scene/main/scene_tree.h b/scene/main/scene_tree.h index 7e445411050..291e4a5a0cf 100644 --- a/scene/main/scene_tree.h +++ b/scene/main/scene_tree.h @@ -143,6 +143,7 @@ private: bool debug_navigation_hint = false; #endif bool paused = false; + bool suspended = false; HashMap group_map; bool _quit = false; @@ -343,6 +344,8 @@ public: void set_pause(bool p_enabled); bool is_paused() const; + void set_suspend(bool p_enabled); + bool is_suspended() const; #ifdef DEBUG_ENABLED void set_debug_collisions_hint(bool p_enabled); diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index 54f66e8d4e6..5a90eb8f3e3 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -3123,7 +3123,7 @@ void Viewport::push_input(const Ref &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 &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 Viewport::get_camera_3d_override_properties() const { + HashMap 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 Viewport::get_world_3d() const { ERR_READ_THREAD_GUARD_V(Ref()); return world_3d; diff --git a/scene/main/viewport.h b/scene/main/viewport.h index a18dc1f6f0f..92691ccbec3 100644 --- a/scene/main/viewport.h +++ b/scene/main/viewport.h @@ -401,6 +401,7 @@ private: DefaultCanvasItemTextureRepeat default_canvas_item_texture_repeat = DEFAULT_CANVAS_ITEM_TEXTURE_REPEAT_DISABLED; bool disable_input = false; + bool disable_input_override = false; void _gui_call_input(Control *p_control, const Ref &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 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;