diff --git a/doc/classes/Camera2D.xml b/doc/classes/Camera2D.xml index da98e6c26a0..714288de845 100644 --- a/doc/classes/Camera2D.xml +++ b/doc/classes/Camera2D.xml @@ -142,6 +142,9 @@ Bottom scroll limit in pixels. The camera stops moving when reaching this value, but [member offset] can push the view past the limit. + + If [code]true[/code], the limits will be enabled. Disabling this will allow the camera to focus anywhere, when the four [code]limit_*[/code] properties will not work. + Left scroll limit in pixels. The camera stops moving when reaching this value, but [member offset] can push the view past the limit. diff --git a/editor/plugins/camera_2d_editor_plugin.cpp b/editor/plugins/camera_2d_editor_plugin.cpp new file mode 100644 index 00000000000..fc075f62217 --- /dev/null +++ b/editor/plugins/camera_2d_editor_plugin.cpp @@ -0,0 +1,167 @@ +/**************************************************************************/ +/* camera_2d_editor_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 "camera_2d_editor_plugin.h" + +#include "canvas_item_editor_plugin.h" +#include "core/config/project_settings.h" +#include "editor/editor_node.h" +#include "editor/editor_undo_redo_manager.h" +#include "editor/themes/editor_scale.h" +#include "scene/2d/camera_2d.h" +#include "scene/gui/label.h" +#include "scene/gui/menu_button.h" + +void Camera2DEditor::edit(Camera2D *p_camera) { + if (p_camera == selected_camera) { + return; + } + selected_camera = p_camera; +} + +void Camera2DEditor::_menu_option(int p_option) { + switch (p_option) { + case MENU_SNAP_LIMITS_TO_VIEWPORT: { + EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton(); + Rect2 prev_rect = selected_camera->get_limit_rect(); + ur->create_action(TTR("Snap the Limits to the Viewport"), UndoRedo::MERGE_DISABLE, selected_camera); + ur->add_do_method(this, "_snap_limits_to_viewport"); + ur->add_do_reference(selected_camera); + ur->add_undo_method(this, "_undo_snap_limits_to_viewport", prev_rect); + ur->commit_action(); + } break; + } +} + +void Camera2DEditor::_snap_limits_to_viewport() { + selected_camera->set_limit(SIDE_LEFT, 0); + selected_camera->set_limit(SIDE_TOP, 0); + selected_camera->set_limit(SIDE_RIGHT, GLOBAL_GET("display/window/size/viewport_width")); + selected_camera->set_limit(SIDE_BOTTOM, GLOBAL_GET("display/window/size/viewport_height")); +} + +void Camera2DEditor::_undo_snap_limits_to_viewport(const Rect2 &p_prev_rect) { + Point2 end = p_prev_rect.get_end(); + selected_camera->set_limit(SIDE_LEFT, p_prev_rect.position.x); + selected_camera->set_limit(SIDE_TOP, p_prev_rect.position.y); + selected_camera->set_limit(SIDE_RIGHT, end.x); + selected_camera->set_limit(SIDE_BOTTOM, end.y); +} + +void Camera2DEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_THEME_CHANGED: { + options->set_button_icon(get_editor_theme_icon(SNAME("Camera2D"))); + } break; + } +} + +void Camera2DEditor::_bind_methods() { + ClassDB::bind_method(D_METHOD("_snap_limits_to_viewport"), &Camera2DEditor::_snap_limits_to_viewport); + ClassDB::bind_method(D_METHOD("_undo_snap_limits_to_viewport", "prev_rect"), &Camera2DEditor::_undo_snap_limits_to_viewport); +} + +Camera2DEditor::Camera2DEditor() { + options = memnew(MenuButton); + + CanvasItemEditor::get_singleton()->add_control_to_menu_panel(options); + + options->set_text(TTRC("Camera2D")); + + options->get_popup()->add_item(TTRC("Snap the Limits to the Viewport"), MENU_SNAP_LIMITS_TO_VIEWPORT); + options->set_switch_on_hover(true); + + options->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &Camera2DEditor::_menu_option)); + +#ifdef TOOLS_ENABLED + add_user_signal(MethodInfo("_editor_theme_changed")); +#endif +} + +void Camera2DEditorPlugin::_update_approach_text_visibility() { + if (camera_2d_editor->selected_camera == nullptr) { + return; + } + approach_to_move_rect->set_visible(camera_2d_editor->selected_camera->is_limit_enabled()); +} + +void Camera2DEditorPlugin::_editor_theme_changed() { + approach_to_move_rect->remove_theme_color_override(SceneStringName(font_color)); + approach_to_move_rect->add_theme_color_override(SceneStringName(font_color), Color(0.6f, 0.6f, 0.6f, 1)); + approach_to_move_rect->add_theme_color_override("font_shadow_color", Color(0.2f, 0.2f, 0.2f, 1)); + approach_to_move_rect->add_theme_constant_override("shadow_outline_size", 1 * EDSCALE); + approach_to_move_rect->add_theme_constant_override("line_spacing", 0); +} + +void Camera2DEditorPlugin::edit(Object *p_object) { + Callable update_text = callable_mp(this, &Camera2DEditorPlugin::_update_approach_text_visibility); + StringName update_signal = SNAME("_camera_limit_enabled_updated"); + + Camera2D *prev_cam = camera_2d_editor->selected_camera; + if (prev_cam != nullptr && prev_cam->is_connected(update_signal, update_text)) { + prev_cam->disconnect(update_signal, update_text); + } + Camera2D *cam = Object::cast_to(p_object); + if (cam != nullptr) { + camera_2d_editor->edit(cam); + _update_approach_text_visibility(); + if (!cam->is_connected(update_signal, update_text)) { + cam->connect(update_signal, update_text); + } + } +} + +bool Camera2DEditorPlugin::handles(Object *p_object) const { + return p_object->is_class("Camera2D"); +} + +void Camera2DEditorPlugin::make_visible(bool p_visible) { + if (p_visible) { + camera_2d_editor->options->show(); + approach_to_move_rect->show(); + } else { + camera_2d_editor->options->hide(); + approach_to_move_rect->hide(); + } +} + +Camera2DEditorPlugin::Camera2DEditorPlugin() { + camera_2d_editor = memnew(Camera2DEditor); + EditorNode::get_singleton()->get_gui_base()->add_child(camera_2d_editor); + camera_2d_editor->connect(SNAME("_editor_theme_changed"), callable_mp(this, &Camera2DEditorPlugin::_editor_theme_changed)); + + approach_to_move_rect = memnew(Label); + approach_to_move_rect->set_text(TTRC("In Move Mode: \nHold Ctrl + left mouse button to move the limit rectangle.\nHold left mouse button to move the camera only.")); + approach_to_move_rect->hide(); + _editor_theme_changed(); + CanvasItemEditor::get_singleton()->get_controls_container()->add_child(approach_to_move_rect); + + make_visible(false); +} diff --git a/editor/plugins/camera_2d_editor_plugin.h b/editor/plugins/camera_2d_editor_plugin.h new file mode 100644 index 00000000000..3e72329a285 --- /dev/null +++ b/editor/plugins/camera_2d_editor_plugin.h @@ -0,0 +1,80 @@ +/**************************************************************************/ +/* camera_2d_editor_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. */ +/**************************************************************************/ + +#pragma once + +#include "editor/plugins/editor_plugin.h" + +class Camera2D; +class Label; +class MenuButton; + +class Camera2DEditor : public Control { + GDCLASS(Camera2DEditor, Control); + + enum Menu { + MENU_SNAP_LIMITS_TO_VIEWPORT, + }; + + Camera2D *selected_camera = nullptr; + + friend class Camera2DEditorPlugin; + MenuButton *options = nullptr; + + void _menu_option(int p_option); + void _snap_limits_to_viewport(); + void _undo_snap_limits_to_viewport(const Rect2 &p_prev_rect); + +protected: + static void _bind_methods(); + void _notification(int p_what); + +public: + void edit(Camera2D *p_camera); + Camera2DEditor(); +}; + +class Camera2DEditorPlugin : public EditorPlugin { + GDCLASS(Camera2DEditorPlugin, EditorPlugin); + + Camera2DEditor *camera_2d_editor = nullptr; + + Label *approach_to_move_rect = nullptr; + + void _editor_theme_changed(); + void _update_approach_text_visibility(); + +public: + virtual void edit(Object *p_object) override; + virtual bool handles(Object *p_object) const override; + virtual void make_visible(bool p_visible) override; + + Camera2DEditorPlugin(); +}; diff --git a/editor/register_editor_types.cpp b/editor/register_editor_types.cpp index 1dfe399a24a..2edba72e79e 100644 --- a/editor/register_editor_types.cpp +++ b/editor/register_editor_types.cpp @@ -73,6 +73,7 @@ #include "editor/plugins/audio_stream_randomizer_editor_plugin.h" #include "editor/plugins/bit_map_editor_plugin.h" #include "editor/plugins/bone_map_editor_plugin.h" +#include "editor/plugins/camera_2d_editor_plugin.h" #include "editor/plugins/camera_3d_editor_plugin.h" #include "editor/plugins/cast_2d_editor_plugin.h" #include "editor/plugins/collision_polygon_2d_editor_plugin.h" @@ -252,6 +253,7 @@ void register_editor_types() { EditorPlugins::add_by_type(); // 2D + EditorPlugins::add_by_type(); EditorPlugins::add_by_type(); EditorPlugins::add_by_type(); EditorPlugins::add_by_type(); diff --git a/scene/2d/camera_2d.cpp b/scene/2d/camera_2d.cpp index cad9e41c1c5..25a7f8900fd 100644 --- a/scene/2d/camera_2d.cpp +++ b/scene/2d/camera_2d.cpp @@ -31,8 +31,61 @@ #include "camera_2d.h" #include "core/config/project_settings.h" +#include "core/input/input.h" #include "scene/main/viewport.h" +#ifdef TOOLS_ENABLED +Dictionary Camera2D::_edit_get_state() const { + Dictionary state = Node2D::_edit_get_state(); + state["limit_rect"] = get_limit_rect(); + return state; +} + +void Camera2D::_edit_set_state(const Dictionary &p_state) { + if (p_state.has("limit_rect")) { + _set_limit_rect(p_state["limit_rect"]); + } + Node2D::_edit_set_state(p_state); +} + +void Camera2D::_edit_set_position(const Point2 &p_position) { + if (_is_dragging_limit_rect()) { + Rect2 rect = get_limit_rect(); + rect.position = p_position; + _set_limit_rect(rect); + } else { + Node2D::_edit_set_position(p_position); + } +} + +Point2 Camera2D::_edit_get_position() const { + return _is_dragging_limit_rect() ? get_limit_rect().position : Node2D::_edit_get_position(); +} + +void Camera2D::_edit_set_rect(const Rect2 &p_rect) { + ERR_FAIL_COND(limit_enabled && !_edit_use_rect()); + Rect2 rect = p_rect; + Vector2 scl = get_global_scale().abs(); + rect.size *= scl; + rect.position = (rect.position + get_global_position()) * scl; + _set_limit_rect(rect); +} +#endif // TOOLS_ENABLED + +#ifdef DEBUG_ENABLED +Rect2 Camera2D::_edit_get_rect() const { + Rect2 rect = get_limit_rect(); + Vector2 scl = get_global_scale().abs(); + rect.size /= scl; + rect.position = (rect.position - get_global_position()) / scl; + return rect; +} + +bool Camera2D::_edit_use_rect() const { + return limit_enabled; +} +#endif // DEBUG_ENABLED + bool Camera2D::_is_editing_in_editor() const { #ifdef TOOLS_ENABLED return is_part_of_edited_scene(); @@ -75,6 +128,10 @@ void Camera2D::_update_scroll() { } #ifdef TOOLS_ENABLED +bool Camera2D::_is_dragging_limit_rect() const { + return _edit_use_rect() && Input::get_singleton()->is_key_pressed(Key::CTRL); +} + void Camera2D::_project_settings_changed() { if (screen_drawing_enabled) { queue_redraw(); @@ -169,7 +226,7 @@ Transform2D Camera2D::get_camera_transform() { Point2 screen_offset = (anchor_mode == ANCHOR_MODE_DRAG_CENTER ? (screen_size * 0.5 * zoom_scale) : Point2()); Rect2 screen_rect(-screen_offset + camera_pos, screen_size * zoom_scale); - if (limit_smoothing_enabled) { + if (limit_enabled && limit_smoothing_enabled) { if (screen_rect.position.x < limit[SIDE_LEFT]) { camera_pos.x -= screen_rect.position.x - limit[SIDE_LEFT]; } @@ -222,7 +279,7 @@ Transform2D Camera2D::get_camera_transform() { Rect2 screen_rect(-screen_offset + ret_camera_pos, screen_size * zoom_scale); - if (!position_smoothing_enabled || !limit_smoothing_enabled) { + if (limit_enabled && (!position_smoothing_enabled || !limit_smoothing_enabled)) { if (screen_rect.position.x < limit[SIDE_LEFT]) { screen_rect.position.x = limit[SIDE_LEFT]; } @@ -398,8 +455,7 @@ void Camera2D::_notification(int p_what) { } } - if (limit_drawing_enabled) { - Color limit_drawing_color(1, 1, 0.25, 0.63); + if (limit_enabled && limit_drawing_enabled) { real_t limit_drawing_width = -1; if (is_current()) { limit_drawing_width = 3; @@ -415,7 +471,7 @@ void Camera2D::_notification(int p_what) { }; for (int i = 0; i < 4; i++) { - draw_line(limit_points[i], limit_points[(i + 1) % 4], limit_drawing_color, limit_drawing_width); + draw_line(limit_points[i], limit_points[(i + 1) % 4], Color(1, 1, 0.25, 0.63), limit_drawing_width); } } @@ -484,6 +540,22 @@ bool Camera2D::is_ignoring_rotation() const { return ignore_rotation; } +void Camera2D::set_limit_enabled(bool p_limit_enabled) { + if (p_limit_enabled == limit_enabled) { + return; + } + limit_enabled = p_limit_enabled; + _update_scroll(); +#ifdef TOOLS_ENABLED + emit_signal("_camera_limit_enabled_updated"); // Used for Camera2DEditorPlugin +#endif + notify_property_list_changed(); +} + +bool Camera2D::is_limit_enabled() const { + return limit_enabled; +} + void Camera2D::set_process_callback(Camera2DProcessCallback p_mode) { if (process_callback == p_mode) { return; @@ -548,6 +620,18 @@ void Camera2D::_update_process_internal_for_smoothing() { set_process_internal(enable); } +void Camera2D::_set_limit_rect(const Rect2 &p_limit_rect) { + Point2 limit_rect_end = p_limit_rect.get_end(); + set_limit(SIDE_LEFT, p_limit_rect.position.x); + set_limit(SIDE_TOP, p_limit_rect.position.y); + set_limit(SIDE_RIGHT, limit_rect_end.x); + set_limit(SIDE_BOTTOM, limit_rect_end.y); +} + +Rect2 Camera2D::get_limit_rect() const { + return Rect2(limit[SIDE_LEFT], limit[SIDE_TOP], limit[SIDE_RIGHT] - limit[SIDE_LEFT], limit[SIDE_BOTTOM] - limit[SIDE_TOP]); +} + void Camera2D::make_current() { ERR_FAIL_COND(!enabled || !is_inside_tree()); get_tree()->call_group(group_name, "_make_current", this); @@ -817,6 +901,9 @@ bool Camera2D::is_margin_drawing_enabled() const { } void Camera2D::_validate_property(PropertyInfo &p_property) const { + if (!limit_enabled && (p_property.name == "limit_smoothed" || p_property.name == "limit_left" || p_property.name == "limit_top" || p_property.name == "limit_right" || p_property.name == "limit_bottom")) { + p_property.usage = PROPERTY_USAGE_NO_EDITOR; + } if (!position_smoothing_enabled && p_property.name == "position_smoothing_speed") { p_property.usage = PROPERTY_USAGE_NO_EDITOR; } @@ -847,6 +934,9 @@ void Camera2D::_bind_methods() { ClassDB::bind_method(D_METHOD("is_current"), &Camera2D::is_current); ClassDB::bind_method(D_METHOD("_make_current"), &Camera2D::_make_current); + ClassDB::bind_method(D_METHOD("set_limit_enabled", "limit_enabled"), &Camera2D::set_limit_enabled); + ClassDB::bind_method(D_METHOD("is_limit_enabled"), &Camera2D::is_limit_enabled); + ClassDB::bind_method(D_METHOD("set_limit", "margin", "limit"), &Camera2D::set_limit); ClassDB::bind_method(D_METHOD("get_limit", "margin"), &Camera2D::get_limit); @@ -913,6 +1003,7 @@ void Camera2D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "process_callback", PROPERTY_HINT_ENUM, "Physics,Idle"), "set_process_callback", "get_process_callback"); ADD_GROUP("Limit", "limit_"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "limit_enabled"), "set_limit_enabled", "is_limit_enabled"); ADD_PROPERTYI(PropertyInfo(Variant::INT, "limit_left", PROPERTY_HINT_NONE, "suffix:px"), "set_limit", "get_limit", SIDE_LEFT); ADD_PROPERTYI(PropertyInfo(Variant::INT, "limit_top", PROPERTY_HINT_NONE, "suffix:px"), "set_limit", "get_limit", SIDE_TOP); ADD_PROPERTYI(PropertyInfo(Variant::INT, "limit_right", PROPERTY_HINT_NONE, "suffix:px"), "set_limit", "get_limit", SIDE_RIGHT); @@ -961,4 +1052,8 @@ Camera2D::Camera2D() { set_notify_transform(true); set_hide_clip_children(true); + +#ifdef TOOLS_ENABLED + add_user_signal(MethodInfo("_camera_limit_enabled_updated")); // Camera2DEditorPlugin listens to this +#endif } diff --git a/scene/2d/camera_2d.h b/scene/2d/camera_2d.h index 2b5cc281d41..b3f90b211b1 100644 --- a/scene/2d/camera_2d.h +++ b/scene/2d/camera_2d.h @@ -72,6 +72,7 @@ protected: real_t rotation_smoothing_speed = 5.0; bool rotation_smoothing_enabled = false; + bool limit_enabled = true; int limit[4]; bool limit_smoothing_enabled = false; @@ -87,7 +88,9 @@ protected: bool _is_editing_in_editor() const; void _update_process_callback(); void _update_scroll(); + #ifdef TOOLS_ENABLED + bool _is_dragging_limit_rect() const; void _project_settings_changed(); #endif @@ -98,6 +101,8 @@ protected: void _update_process_internal_for_smoothing(); + void _set_limit_rect(const Rect2 &p_limit_rect); + bool screen_drawing_enabled = true; bool limit_drawing_enabled = false; bool margin_drawing_enabled = false; @@ -122,6 +127,24 @@ protected: void _validate_property(PropertyInfo &p_property) const; public: +#ifdef TOOLS_ENABLED + virtual Dictionary _edit_get_state() const override; + virtual void _edit_set_state(const Dictionary &p_state) override; + + virtual void _edit_set_position(const Point2 &p_position) override; + virtual Point2 _edit_get_position() const override; + + virtual void _edit_set_rect(const Rect2 &p_rect) override; + virtual Size2 _edit_get_minimum_size() const override { return Size2(); } +#endif // TOOLS_ENABLED + +#ifdef DEBUG_ENABLED + virtual Rect2 _edit_get_rect() const override; + virtual bool _edit_use_rect() const override; +#endif // DEBUG_ENABLED + + Rect2 get_limit_rect() const; + void set_offset(const Vector2 &p_offset); Vector2 get_offset() const; @@ -131,6 +154,9 @@ public: void set_ignore_rotation(bool p_ignore); bool is_ignoring_rotation() const; + void set_limit_enabled(bool p_limit_enabled); + bool is_limit_enabled() const; + void set_limit(Side p_side, int p_limit); int get_limit(Side p_side) const;