diff --git a/config.py b/config.py index 12084aaa..947f80c6 100644 --- a/config.py +++ b/config.py @@ -51,6 +51,7 @@ def get_doc_classes(): "ImageIndexed", "ImageIndexed", "ShapeCast2D", + "VisualShape2D", ] diff --git a/doc/VisualShape2D.xml b/doc/VisualShape2D.xml new file mode 100644 index 00000000..3696d698 --- /dev/null +++ b/doc/VisualShape2D.xml @@ -0,0 +1,48 @@ + + + + Draws any [Shape2D] resource. Useful for quick prototyping and debugging. + + + This is a node with the entire purpose of rendering [Shape2D] resources just like collision shapes are drawn in the editor, yet it's also possible to override the color per each node rather than globally with this class. You may use Godot's [Polygon2D] node which is much more suitable for polygon editing and drawing with textures, as any shape can be represented with it. + + + + + + + + + Forces to update the shape from parent node. This is called automatically each [b]idle frame[/b] if [member use_parent_shape] is enabled. Updating the shape each frame may be costly, so you can disable this behavior with [code]set_process(false)[/code] on this node, and update the shape manually with this method when needed. + + + + + + The fill color used to draw the [member shape]. + + + If [code]true[/code], respects the "Visible Collision Shapes" option so that the shape is only drawn when the option is enabled while the game is running. + [b]Note:[/b] available in debug builds only. + + + If [code]true[/code], this overrides the [member color] with the color used to draw the collision shapes. + [b]Note:[/b] available in debug builds only. + + + The shape resource used as a reference to draw. The drawing method is specific for each shape and the properties must be configured per shape. + + + If [code]true[/code], the shape is fetched from the parent node to draw instead of its own [member shape]. The parent node must have either [code]shape[/code] as [Shape2D] property or [code]points[/code], [code]polygon[/code] as [PoolVector2Array] property defined, else nothing is drawn. + + + + + + Emitted when the [member shape] is changed (or cleared). + + + + + + diff --git a/editor/2d/SCsub b/editor/2d/SCsub new file mode 100644 index 00000000..3086f853 --- /dev/null +++ b/editor/2d/SCsub @@ -0,0 +1,4 @@ +#!/usr/bin/env python +Import("env_goost") + +env_goost.add_source_files(env_goost.modules_sources, "*.cpp") diff --git a/editor/2d/visual_shape_2d_editor_plugin.cpp b/editor/2d/visual_shape_2d_editor_plugin.cpp new file mode 100644 index 00000000..679c6cf3 --- /dev/null +++ b/editor/2d/visual_shape_2d_editor_plugin.cpp @@ -0,0 +1,128 @@ +#include "visual_shape_2d_editor_plugin.h" + +#include "editor/plugins/canvas_item_editor_plugin.h" + +#include "scene/resources/concave_polygon_shape_2d.h" +#include "scene/resources/convex_polygon_shape_2d.h" + +Node2D *VisualShape2DEditor::_get_node() const { + return node; +} + +void VisualShape2DEditor::_set_node(Node *p_node) { + if (node) { + node->disconnect("shape_changed", this, "_update_editing"); + } + node = Object::cast_to(p_node); + if (node) { + node->connect("shape_changed", this, "_update_editing"); + } + _update_editing(); +} + +void VisualShape2DEditor::_set_polygon(int p_idx, const Variant &p_polygon) const { + VisualShape2D *n = Object::cast_to(_get_node()); + + const Ref &shape = n->get_shape(); + Ref convex = shape; + Ref concave = shape; + + if (convex.is_valid()) { + convex->set("points", p_polygon); + + } else if (concave.is_valid()) { + const Vector &polygon = p_polygon; + + Vector segments; + segments.resize(polygon.size() * 2); + Vector2 *w = segments.ptrw(); + + for (int i = 0; i < polygon.size(); ++i) { + w[(i << 1) + 0] = polygon[i]; + w[(i << 1) + 1] = polygon[(i + 1) % polygon.size()]; + } + concave->set("segments", segments); + } +} + +Variant VisualShape2DEditor::_get_polygon(int p_idx) const { + VisualShape2D *n = Object::cast_to(_get_node()); + + const Ref &shape = n->get_shape(); + Ref convex = shape; + Ref concave = shape; + + if (convex.is_valid()) { + return convex->get("points"); + + } else if (concave.is_valid()) { + Vector polygon; + const Vector &segments = concave->get("segments"); + const Vector2 *r = segments.ptr(); + + for (int i = 0; i < segments.size(); i += 2) { + polygon.push_back(r[i]); + } + return polygon; + } + return Variant(); +} + +void VisualShape2DEditor::_update_editing() { + if (!node) { + return; + } + Ref shape = node->get_shape(); + Ref convex = shape; + Ref concave = shape; + + const String reason = TTR("No polygon-based shape resource is set."); + + if (shape.is_null()) { + disable_polygon_editing(true, reason); + } else if (convex.is_valid() || concave.is_valid()) { + disable_polygon_editing(false, ""); + } else { + disable_polygon_editing(true, reason); + } +} + +void VisualShape2DEditor::_action_set_polygon(int p_idx, const Variant &p_previous, const Variant &p_polygon) { + undo_redo->add_do_method(this, "_set_polygon", 0, p_polygon); + undo_redo->add_undo_method(this, "_set_polygon", 0, p_previous); +} + +bool VisualShape2DEditor::_has_resource() const { + if (!node) { + return false; + } + return node->get_shape().is_valid(); +} + +void VisualShape2DEditor::_create_resource() { + if (!node) { + return; + } + undo_redo->create_action(TTR("Create Convex Polygon Shape")); + undo_redo->add_do_method(node, "set_shape", Ref(memnew(ConvexPolygonShape2D))); + undo_redo->add_undo_method(node, "set_shape", Variant(REF())); + undo_redo->commit_action(); + + _menu_option(MODE_CREATE); + + EditorNode::get_singleton()->get_inspector()->refresh(); +} + +void VisualShape2DEditor::_bind_methods() { + ClassDB::bind_method(D_METHOD("_set_polygon"), &VisualShape2DEditor::_set_polygon); + ClassDB::bind_method(D_METHOD("_update_editing"), &VisualShape2DEditor::_update_editing); +} + +VisualShape2DEditor::VisualShape2DEditor(EditorNode *p_editor) : + AbstractPolygon2DEditor(p_editor) { + node = nullptr; +} + +VisualShape2DEditorPlugin::VisualShape2DEditorPlugin(EditorNode *p_node) : + AbstractPolygon2DEditorPlugin(p_node, memnew(VisualShape2DEditor(p_node)), "VisualShape2D") { +} diff --git a/editor/2d/visual_shape_2d_editor_plugin.h b/editor/2d/visual_shape_2d_editor_plugin.h new file mode 100644 index 00000000..279a6ea7 --- /dev/null +++ b/editor/2d/visual_shape_2d_editor_plugin.h @@ -0,0 +1,39 @@ +#ifndef GOOST_VISUAL_SHAPE_2D_EDITOR_PLUGIN_H +#define GOOST_VISUAL_SHAPE_2D_EDITOR_PLUGIN_H + +#include "editor/plugins/abstract_polygon_2d_editor.h" +#include "goost/scene/2d/visual_shape_2d.h" + +class VisualShape2DEditor : public AbstractPolygon2DEditor { + GDCLASS(VisualShape2DEditor, AbstractPolygon2DEditor); + + VisualShape2D *node; + + void _update_editing(); + +protected: + virtual Node2D *_get_node() const; + virtual void _set_node(Node *p_node); + + virtual void _set_polygon(int p_idx, const Variant &p_polygon) const; + virtual Variant _get_polygon(int p_idx) const; + + virtual void _action_set_polygon(int p_idx, const Variant &p_previous, const Variant &p_polygon); + + virtual bool _has_resource() const; + virtual void _create_resource(); + + static void _bind_methods(); + +public: + VisualShape2DEditor(EditorNode *p_editor); +}; + +class VisualShape2DEditorPlugin : public AbstractPolygon2DEditorPlugin { + GDCLASS(VisualShape2DEditorPlugin, AbstractPolygon2DEditorPlugin); + +public: + VisualShape2DEditorPlugin(EditorNode *p_node); +}; + +#endif // GOOST_VISUAL_SHAPE_2D_EDITOR_PLUGIN_H diff --git a/editor/3d/SCsub b/editor/3d/SCsub new file mode 100644 index 00000000..7e572db8 --- /dev/null +++ b/editor/3d/SCsub @@ -0,0 +1,3 @@ +#!/usr/bin/env python + +# Nothing here yet! diff --git a/editor/SCsub b/editor/SCsub index 42a2eafb..5970a7bf 100644 --- a/editor/SCsub +++ b/editor/SCsub @@ -1,3 +1,11 @@ #!/usr/bin/env python +Import("env") +Import("env_goost") -# Nothing here yet! +if env_goost["goost_editor_enabled"]: + SConscript("2d/SCsub", exports="env_goost") + + if not env["disable_3d"]: + SConscript("3d/SCsub", exports="env_goost") + + env_goost.add_source_files(env_goost.modules_sources, "*.cpp") diff --git a/editor/icons/README.md b/editor/icons/README.md index 758492f4..9bbb2382 100644 --- a/editor/icons/README.md +++ b/editor/icons/README.md @@ -2,6 +2,6 @@ This directory contains icons for classes provided by Goost. -See official +See official Godot Engine's [Editor icons](https://docs.godotengine.org/en/latest/development/editor/creating_icons.html) documentation page on how to create them. diff --git a/editor/icons/icon_visual_shape_2d.svg b/editor/icons/icon_visual_shape_2d.svg new file mode 100644 index 00000000..9bc89327 --- /dev/null +++ b/editor/icons/icon_visual_shape_2d.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/editor/register_editor_types.cpp b/editor/register_editor_types.cpp new file mode 100644 index 00000000..cbe133db --- /dev/null +++ b/editor/register_editor_types.cpp @@ -0,0 +1,17 @@ +#include "register_editor_types.h" + +#include "2d/visual_shape_2d_editor_plugin.h" + +namespace goost { + +void register_editor_types() { +#ifdef TOOLS_ENABLED + EditorPlugins::add_by_type(); +#endif +} + +void unregister_editor_types() { + // Nothing to do. +} + +} // namespace goost diff --git a/editor/register_editor_types.h b/editor/register_editor_types.h new file mode 100644 index 00000000..5a53fd79 --- /dev/null +++ b/editor/register_editor_types.h @@ -0,0 +1,6 @@ +namespace goost { + +void register_editor_types(); +void unregister_editor_types(); + +} // namespace goost diff --git a/goost.py b/goost.py index e5f213e0..e61a34dd 100644 --- a/goost.py +++ b/goost.py @@ -9,6 +9,7 @@ "core/image", "core/math", "scene/physics", + "editor", ] def get_components(): diff --git a/register_types.cpp b/register_types.cpp index eb625bc9..ad76ddb7 100644 --- a/register_types.cpp +++ b/register_types.cpp @@ -2,6 +2,7 @@ #include "core/register_core_types.h" #include "scene/register_scene_types.h" +#include "editor/register_editor_types.h" void register_goost_types() { #ifdef GOOST_CORE_ENABLED @@ -10,6 +11,9 @@ void register_goost_types() { #ifdef GOOST_SCENE_ENABLED goost::register_scene_types(); #endif +#ifdef GOOST_EDITOR_ENABLED + goost::register_editor_types(); +#endif } void unregister_goost_types() { diff --git a/scene/2d/SCsub b/scene/2d/SCsub new file mode 100644 index 00000000..0bdd9c11 --- /dev/null +++ b/scene/2d/SCsub @@ -0,0 +1,4 @@ +#!/usr/bin/env python +Import("env_goost") + +env_goost.add_source_files(env_goost.modules_sources, '*.cpp') diff --git a/scene/2d/visual_shape_2d.cpp b/scene/2d/visual_shape_2d.cpp new file mode 100644 index 00000000..dde053c6 --- /dev/null +++ b/scene/2d/visual_shape_2d.cpp @@ -0,0 +1,219 @@ +#include "visual_shape_2d.h" + +#include "core/core_string_names.h" +#include "core/engine.h" + +void VisualShape2D::set_shape(const Ref &p_shape) { + bool shape_changed = false; + if (shape != p_shape) { + shape_changed = true; + } + if (shape.is_valid()) { + shape->disconnect(CoreStringNames::get_singleton()->changed, this, "update"); + } + shape = p_shape; + if (shape.is_valid()) { + shape->connect(CoreStringNames::get_singleton()->changed, this, "update"); + } + update(); + + if (shape_changed) { + emit_signal("shape_changed"); + } + update_configuration_warning(); +} + +Ref VisualShape2D::get_shape() const { + return shape; +} + +void VisualShape2D::set_use_parent_shape(bool p_use_parent_shape) { + use_parent_shape = p_use_parent_shape; + update_parent_shape(); + update_configuration_warning(); + update(); +} + +bool VisualShape2D::is_using_parent_shape() const { + return use_parent_shape; +} + +bool VisualShape2D::update_parent_shape() { + bool valid = use_parent_shape; + bool got_polygon_shape = false; + + if (!is_inside_tree()) { + valid = false; + } + Node *parent = get_parent(); + if (!parent) { + valid = false; + } + if (parent_shape.is_valid()) { + parent_shape->disconnect(CoreStringNames::get_singleton()->changed, this, "update"); + } + Ref previous = parent_shape; + + if (!valid) { + parent_shape = Ref(); + return parent_shape != previous; + } + parent_shape = parent->get("shape"); + Vector points; + if (parent_shape.is_null()) { + points = parent->get("points", &valid); + if (!valid) { + points = parent->get("polygon", &valid); + } + if (valid) { + // This might be `CollisionPolygon2D` etc. + got_polygon_shape = true; + if (polygon_shape.is_null()) { + polygon_shape.instance(); + } + parent_shape = polygon_shape; + } + } + if (parent_shape.is_valid()) { + if (!parent_shape->is_connected(CoreStringNames::get_singleton()->changed, this, "update")) { + parent_shape->connect(CoreStringNames::get_singleton()->changed, this, "update"); + } + // This needs to be set after the shape `changed` signal is connected, + // so that the polygon shape can be drawn even if `_process` is disabled. + if (got_polygon_shape) { + if (points.size() >= 3) { + polygon_shape->set_points(points); + } else { + parent_shape = Ref(); + } + } + } + return parent_shape != previous; +} + +void VisualShape2D::set_color(const Color &p_color) { + color = p_color; + update(); +} + +Color VisualShape2D::get_color() const { + return color; +} + +void VisualShape2D::set_debug_use_default_color(bool p_debug_use_default_color) { + debug_use_default_color = p_debug_use_default_color; + update(); +} + +bool VisualShape2D::is_using_debug_default_color() const { + return debug_use_default_color; +} + +void VisualShape2D::set_debug_sync_visible_collision_shapes(bool p_debug_sync_visible_collision_shapes) { + debug_sync_visible_collision_shapes = p_debug_sync_visible_collision_shapes; + update(); +} + +bool VisualShape2D::is_debug_sync_visible_collision_shapes() const { + return debug_sync_visible_collision_shapes; +} + +void VisualShape2D::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + set_process(true); + } break; + case NOTIFICATION_EXIT_TREE: { + set_process(false); + } break; + case NOTIFICATION_PARENTED: + case NOTIFICATION_PATH_CHANGED: { + update_parent_shape(); + update(); + } break; + case NOTIFICATION_DRAW: { + Ref draw_shape = shape; + if (use_parent_shape) { + // May still need to update the shape if _process() is disabled. + update_parent_shape(); + draw_shape = parent_shape; + } + if (draw_shape.is_null()) { + break; + } + Color draw_color = color; +#ifdef DEBUG_ENABLED + // Avoid error messages, only in debug to save performance in release. + Ref convex = shape; + if (convex.is_valid()) { + if (convex->get_points().size() < 3) { + break; + } + } + Ref concave = shape; + if (concave.is_valid()) { + if (concave->get_segments().size() % 2) { + break; + } + } + // Only relevant in debug builds! + if (debug_use_default_color) { + draw_color = get_tree()->get_debug_collisions_color(); + } + if (debug_sync_visible_collision_shapes && !Engine::get_singleton()->is_editor_hint()) { + if (!get_tree()->is_debugging_collisions_hint()) { + break; + } + } +#endif + draw_shape->draw(get_canvas_item(), draw_color); + } break; + case NOTIFICATION_PROCESS: { + if (use_parent_shape) { + if (update_parent_shape()) { + update(); + } + } + } break; + } +} + +String VisualShape2D::get_configuration_warning() const { + String warning = Node2D::get_configuration_warning(); + + if (shape.is_null() && parent_shape.is_null()) { + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("Shape2D is required for this node to be drawn."); + } + + return warning; +} + +void VisualShape2D::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_shape", "shape"), &VisualShape2D::set_shape); + ClassDB::bind_method(D_METHOD("get_shape"), &VisualShape2D::get_shape); + + ClassDB::bind_method(D_METHOD("set_use_parent_shape", "use_parent_shape"), &VisualShape2D::set_use_parent_shape); + ClassDB::bind_method(D_METHOD("is_using_parent_shape"), &VisualShape2D::is_using_parent_shape); + ClassDB::bind_method(D_METHOD("update_parent_shape"), &VisualShape2D::update_parent_shape); + + ClassDB::bind_method(D_METHOD("set_color", "color"), &VisualShape2D::set_color); + ClassDB::bind_method(D_METHOD("get_color"), &VisualShape2D::get_color); + + ClassDB::bind_method(D_METHOD("set_debug_use_default_color", "enable"), &VisualShape2D::set_debug_use_default_color); + ClassDB::bind_method(D_METHOD("is_using_debug_default_color"), &VisualShape2D::is_using_debug_default_color); + + ClassDB::bind_method(D_METHOD("set_debug_sync_visible_collision_shapes", "debug_sync_visible_collision_shapes"), &VisualShape2D::set_debug_sync_visible_collision_shapes); + ClassDB::bind_method(D_METHOD("is_debug_sync_visible_collision_shapes"), &VisualShape2D::is_debug_sync_visible_collision_shapes); + + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "shape", PROPERTY_HINT_RESOURCE_TYPE, "Shape2D"), "set_shape", "get_shape"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_parent_shape"), "set_use_parent_shape", "is_using_parent_shape"); + ADD_PROPERTY(PropertyInfo(Variant::COLOR, "color"), "set_color", "get_color"); + ADD_GROUP("Debug", "debug_"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "debug_use_default_color"), "set_debug_use_default_color", "is_using_debug_default_color"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "debug_sync_visible_collision_shapes"), "set_debug_sync_visible_collision_shapes", "is_debug_sync_visible_collision_shapes"); + + ADD_SIGNAL(MethodInfo("shape_changed")); +} diff --git a/scene/2d/visual_shape_2d.h b/scene/2d/visual_shape_2d.h new file mode 100644 index 00000000..1f4ea4e5 --- /dev/null +++ b/scene/2d/visual_shape_2d.h @@ -0,0 +1,52 @@ +#ifndef GOOST_VISUAL_SHAPE_2D_H +#define GOOST_VISUAL_SHAPE_2D_H + +#include "scene/2d/node_2d.h" +#include "scene/resources/concave_polygon_shape_2d.h" +#include "scene/resources/convex_polygon_shape_2d.h" +#include "scene/resources/shape_2d.h" + +class VisualShape2D : public Node2D { + GDCLASS(VisualShape2D, Node2D); + + Ref shape; + + Ref parent_shape; + bool use_parent_shape = false; + + // Cache polygon-based parent geometry with this instance. + Ref polygon_shape; + + Color color = Color(1, 1, 1, 1); + + bool debug_use_default_color = false; + bool debug_sync_visible_collision_shapes = false; + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + void set_shape(const Ref &p_shape); + Ref get_shape() const; + + void set_use_parent_shape(bool p_use_parent_shape); + bool is_using_parent_shape() const; + // Returns `true` if parent shape is changed. + bool update_parent_shape(); + + void set_color(const Color &p_color); + Color get_color() const; + + void set_debug_use_default_color(bool p_debug_use_default_color); + bool is_using_debug_default_color() const; + + void set_debug_sync_visible_collision_shapes(bool p_debug_sync_visible_collision_shapes); + bool is_debug_sync_visible_collision_shapes() const; + + String get_configuration_warning() const; + + VisualShape2D(){}; +}; + +#endif // GOOST_VISUAL_SHAPE_2D_H diff --git a/scene/3d/SCsub b/scene/3d/SCsub new file mode 100644 index 00000000..7e572db8 --- /dev/null +++ b/scene/3d/SCsub @@ -0,0 +1,3 @@ +#!/usr/bin/env python + +# Nothing here yet! diff --git a/scene/SCsub b/scene/SCsub index 6a120fb1..54c057b1 100644 --- a/scene/SCsub +++ b/scene/SCsub @@ -1,6 +1,12 @@ #!/usr/bin/env python +Import("env") Import("env_goost") +SConscript("2d/SCsub", exports="env_goost") + +if not env["disable_3d"]: + SConscript("3d/SCsub", exports="env_goost") + if env_goost["goost_physics_enabled"]: SConscript("physics/SCsub", exports="env_goost") diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index 2c4964a2..a1650985 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -3,9 +3,12 @@ #include "physics/register_physics_types.h" #include "register_scene_types.h" +#include "2d/visual_shape_2d.h" + namespace goost { void register_scene_types() { + ClassDB::register_class(); #ifdef GOOST_PHYSICS_ENABLED register_physics_types(); #endif