diff --git a/doc/classes/AnimationNodeBlendSpace1D.xml b/doc/classes/AnimationNodeBlendSpace1D.xml index 24f05213c2..64afe791b5 100644 --- a/doc/classes/AnimationNodeBlendSpace1D.xml +++ b/doc/classes/AnimationNodeBlendSpace1D.xml @@ -101,6 +101,9 @@ Controls the interpolation between animations. + + The cycle length in seconds used by [constant SYNC_MODE_CYCLIC_CONSTANT]. All animations are time-scaled so they complete one full cycle in this duration. Must be greater than [code]0[/code] for cyclic sync to take effect. + The blend space's axis's upper limit for the points' position. See [method add_blend_point]. @@ -110,9 +113,11 @@ Position increment to snap to when moving a point on the axis. - - If [code]false[/code], the blended animations' frame are stopped when the blend value is [code]0[/code]. - If [code]true[/code], forcing the blended animations to advance frame. + + If [code]true[/code], sync mode is enabled (equivalent to [constant SYNC_MODE_INDEPENDENT]). This property is kept for backward compatibility. + + + Controls how animations are synced when blended. See [enum SyncMode] for available options. Label of the virtual axis of the blend space. @@ -128,5 +133,19 @@ Similar to [constant BLEND_MODE_DISCRETE], but starts the new animation at the last animation's playback position. + + Inactive animations are frozen and do not advance. + + + Inactive animations advance with a weight of [code]0[/code]. This is equivalent to the previous [code]sync = true[/code] behavior. + + + All animations are time-scaled so they stay in sync, with the cycle length dynamically computed from active blend weights. This is self-normalizing: a solo animation plays at normal speed. + [b]Note:[/b] If you apply [AnimationNodeTimeSeek] to the result when handling animations of different lengths, synchronization will be broken. In such cases, it is recommended to use [member AnimationNodeAnimation.use_custom_timeline] to align the animation lengths. + + + All animations are time-scaled so they complete one cycle in [member cyclic_length] seconds, keeping them in sync regardless of their individual lengths. + [b]Note:[/b] If you apply [AnimationNodeTimeSeek] to the result when handling animations of different lengths, synchronization will be broken. In such cases, it is recommended to use [member AnimationNodeAnimation.use_custom_timeline] to align the animation lengths. + diff --git a/doc/classes/AnimationNodeBlendSpace2D.xml b/doc/classes/AnimationNodeBlendSpace2D.xml index 77a4c96caf..51ee5f7eed 100644 --- a/doc/classes/AnimationNodeBlendSpace2D.xml +++ b/doc/classes/AnimationNodeBlendSpace2D.xml @@ -136,6 +136,9 @@ Controls the interpolation between animations. + + The cycle length in seconds used by [constant SYNC_MODE_CYCLIC_CONSTANT]. All animations are time-scaled so they complete one full cycle in this duration. Must be greater than [code]0[/code] for cyclic sync to take effect. + The blend space's X and Y axes' upper limit for the points' position. See [method add_blend_point]. @@ -145,9 +148,11 @@ Position increment to snap to when moving a point. - - If [code]false[/code], the blended animations' frame are stopped when the blend value is [code]0[/code]. - If [code]true[/code], forcing the blended animations to advance frame. + + If [code]true[/code], sync mode is enabled (equivalent to [constant SYNC_MODE_INDEPENDENT]). This property is kept for backward compatibility. + + + Controls how animations are synced when blended. See [enum SyncMode] for available options. Name of the blend space's X axis. @@ -173,5 +178,19 @@ Similar to [constant BLEND_MODE_DISCRETE], but starts the new animation at the last animation's playback position. + + Inactive animations are frozen and do not advance. + + + Inactive animations advance with a weight of [code]0[/code]. This is equivalent to the previous [code]sync = true[/code] behavior. + + + All animations are time-scaled so they stay in sync, with the cycle length dynamically computed from active blend weights. This is self-normalizing: a solo animation plays at normal speed. + [b]Note:[/b] If you apply [AnimationNodeTimeSeek] to the result when handling animations of different lengths, synchronization will be broken. In such cases, it is recommended to use [member AnimationNodeAnimation.use_custom_timeline] to align the animation lengths. + + + All animations are time-scaled so they complete one cycle in [member cyclic_length] seconds, keeping them in sync regardless of their individual lengths. + [b]Note:[/b] If you apply [AnimationNodeTimeSeek] to the result when handling animations of different lengths, synchronization will be broken. In such cases, it is recommended to use [member AnimationNodeAnimation.use_custom_timeline] to align the animation lengths. + diff --git a/editor/animation/animation_blend_space_1d_editor.cpp b/editor/animation/animation_blend_space_1d_editor.cpp index b0f7fdd973..3a9b0971d6 100644 --- a/editor/animation/animation_blend_space_1d_editor.cpp +++ b/editor/animation/animation_blend_space_1d_editor.cpp @@ -42,7 +42,6 @@ #include "editor/themes/editor_scale.h" #include "scene/animation/animation_blend_tree.h" #include "scene/gui/button.h" -#include "scene/gui/check_box.h" #include "scene/gui/line_edit.h" #include "scene/gui/option_button.h" #include "scene/gui/panel_container.h" @@ -294,6 +293,7 @@ void AnimationNodeBlendSpace1DEditor::_blend_space_draw() { int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label")); Ref icon = get_editor_theme_icon(SNAME("KeyValue")); Ref icon_selected = get_editor_theme_icon(SNAME("KeySelected")); + Ref icon_invalid = get_editor_theme_icon(SNAME("KeyInvalid")); const float pm = POINT_MARGIN * EDSCALE; @@ -331,8 +331,8 @@ void AnimationNodeBlendSpace1DEditor::_blend_space_draw() { } } + bool does_include_invalid_key = false; points.clear(); - for (int i = 0; i < blend_space->get_blend_point_count(); i++) { float point = blend_space->get_blend_point_position(i); if (!read_only && dragging_selected && selected_point == i) { @@ -348,8 +348,19 @@ void AnimationNodeBlendSpace1DEditor::_blend_space_draw() { points.push_back(point + ofs.x); + // Draw × marker on non-AnimationNodeAnimation points when in cyclic mode. + bool is_key_valid = true; + AnimationNodeBlendSpace1D::SyncMode sync_mode = blend_space->get_sync_mode(); + if (sync_mode == AnimationNodeBlendSpace1D::SYNC_MODE_CYCLIC_MUTABLE || sync_mode == AnimationNodeBlendSpace1D::SYNC_MODE_CYCLIC_CONSTANT) { + Ref node = blend_space->get_blend_point_node(i); + Ref anim_node = node; + if (anim_node.is_null()) { + is_key_valid = false; + does_include_invalid_key = true; + } + } Vector2 gui_point = (ofs + Vector2(point, s.height / 2.0) - icon->get_size() / 2.0).floor(); - blend_space_draw->draw_texture(i == selected_point ? icon_selected : icon, gui_point); + blend_space_draw->draw_texture(is_key_valid ? (i == selected_point ? icon_selected : icon) : icon_invalid, gui_point); if (point >= 0.0 && point <= s.width && editing_point != i) { String name_text = show_indices ? itos(i) : String(blend_space->get_blend_point_name(i)); @@ -366,6 +377,11 @@ void AnimationNodeBlendSpace1DEditor::_blend_space_draw() { text_rects.write[i] = Rect2(Vector2(text_pos.x, text_pos.y - font->get_ascent(font_size)), text_size); } } + if (does_include_invalid_key) { + invalid_point_warning_hb->show(); + } else { + invalid_point_warning_hb->hide(); + } // blend position { @@ -403,7 +419,10 @@ void AnimationNodeBlendSpace1DEditor::_update_space() { max_value->set_value(blend_space->get_max_space()); min_value->set_value(blend_space->get_min_space()); - sync->set_pressed(blend_space->is_using_sync()); + sync->select(blend_space->get_sync_mode()); + cyclic_length_value->set_value(blend_space->get_cyclic_length()); + cyclic_length_value->set_visible(blend_space->get_sync_mode() == AnimationNodeBlendSpace1D::SYNC_MODE_CYCLIC_CONSTANT); + interpolation->select(blend_space->get_blend_mode()); label_value->set_text(blend_space->get_value_label()); @@ -433,8 +452,10 @@ void AnimationNodeBlendSpace1DEditor::_config_changed(double) { undo_redo->add_undo_method(blend_space.ptr(), "set_min_space", blend_space->get_min_space()); undo_redo->add_do_method(blend_space.ptr(), "set_snap", snap_value->get_value()); undo_redo->add_undo_method(blend_space.ptr(), "set_snap", blend_space->get_snap()); - undo_redo->add_do_method(blend_space.ptr(), "set_use_sync", sync->is_pressed()); - undo_redo->add_undo_method(blend_space.ptr(), "set_use_sync", blend_space->is_using_sync()); + undo_redo->add_do_method(blend_space.ptr(), "set_sync_mode", sync->get_selected()); + undo_redo->add_undo_method(blend_space.ptr(), "set_sync_mode", blend_space->get_sync_mode()); + undo_redo->add_do_method(blend_space.ptr(), "set_cyclic_length", cyclic_length_value->get_value()); + undo_redo->add_undo_method(blend_space.ptr(), "set_cyclic_length", blend_space->get_cyclic_length()); undo_redo->add_do_method(blend_space.ptr(), "set_blend_mode", interpolation->get_selected()); undo_redo->add_undo_method(blend_space.ptr(), "set_blend_mode", blend_space->get_blend_mode()); undo_redo->add_do_method(this, "_update_space"); @@ -442,6 +463,9 @@ void AnimationNodeBlendSpace1DEditor::_config_changed(double) { undo_redo->commit_action(); updating = false; + // Update cyclic_length visibility immediately (undo/redo calls _update_space while updating=true). + cyclic_length_value->set_visible(sync->get_selected() == AnimationNodeBlendSpace1D::SYNC_MODE_CYCLIC_CONSTANT); + blend_space_draw->queue_redraw(); } @@ -755,6 +779,7 @@ void AnimationNodeBlendSpace1DEditor::_notification(int p_what) { interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackContinuous")), TTR("Continuous"), 0); interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackDiscrete")), TTR("Discrete"), 1); interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackCapture")), TTR("Capture"), 2); + invalid_point_warning->set_button_icon(get_editor_theme_icon(SNAME("NodeWarning"))); } break; case NOTIFICATION_PROCESS: { @@ -772,6 +797,11 @@ void AnimationNodeBlendSpace1DEditor::_notification(int p_what) { } } +void AnimationNodeBlendSpace1DEditor::_show_invalid_point_warning() { + EditorNode::get_singleton()->show_warning( + TTR("BlendSpace contains points that are not AnimationNodeAnimation.\nCyclic sync modes require that all blend points use AnimationNodeAnimation with a finite, immutable length.")); +} + void AnimationNodeBlendSpace1DEditor::_bind_methods() { ClassDB::bind_method("_update_space", &AnimationNodeBlendSpace1DEditor::_update_space); ClassDB::bind_method("_update_tool_erase", &AnimationNodeBlendSpace1DEditor::_update_tool_erase); @@ -804,6 +834,7 @@ void AnimationNodeBlendSpace1DEditor::edit(const Ref &p_node) { min_value->set_editable(!read_only); max_value->set_editable(!read_only); sync->set_disabled(read_only); + cyclic_length_value->set_editable(!read_only); interpolation->set_disabled(read_only); } @@ -999,9 +1030,24 @@ AnimationNodeBlendSpace1DEditor::AnimationNodeBlendSpace1DEditor() { top_hb->add_child(memnew(VSeparator)); top_hb->add_child(memnew(Label(TTR("Sync")))); - sync = memnew(CheckBox); + sync = memnew(OptionButton); + sync->add_item(TTR("None")); + sync->add_item(TTR("Independent")); + sync->add_item(TTR("Cyclic Mutable")); + sync->add_item(TTR("Cyclic Constant")); top_hb->add_child(sync); - sync->connect(SceneStringName(toggled), callable_mp(this, &AnimationNodeBlendSpace1DEditor::_config_changed)); + sync->connect(SceneStringName(item_selected), callable_mp(this, &AnimationNodeBlendSpace1DEditor::_config_changed)); + + cyclic_length_value = memnew(SpinBox); + cyclic_length_value->set_min(0.0); + cyclic_length_value->set_max(99.0); + cyclic_length_value->set_step(0.001); + cyclic_length_value->set_allow_greater(true); + cyclic_length_value->set_suffix("s"); + cyclic_length_value->set_accessibility_name(TTRC("Cyclic Length")); + cyclic_length_value->set_tooltip_text(TTR("Cycle length in seconds for cyclic sync. All animations are time-scaled to complete one cycle in this duration.")); + top_hb->add_child(cyclic_length_value); + cyclic_length_value->connect(SceneStringName(value_changed), callable_mp(this, &AnimationNodeBlendSpace1DEditor::_config_changed)); top_hb->add_child(memnew(VSeparator)); @@ -1115,6 +1161,16 @@ AnimationNodeBlendSpace1DEditor::AnimationNodeBlendSpace1DEditor() { error_panel->add_child(error_label); error_panel->hide(); + invalid_point_warning_hb = memnew(HBoxContainer); + add_child(invalid_point_warning_hb); + invalid_point_warning_hb->hide(); + + invalid_point_warning = memnew(Button); + invalid_point_warning->set_text(TTRC("Contains Invalid Point")); + invalid_point_warning->set_tooltip_text(TTRC("Warning: BlendSpace contains invalid points for cyclic sync")); + invalid_point_warning->connect(SceneStringName(pressed), callable_mp(this, &AnimationNodeBlendSpace1DEditor::_show_invalid_point_warning)); + invalid_point_warning_hb->add_child(invalid_point_warning); + menu = memnew(PopupMenu); add_child(menu); menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationNodeBlendSpace1DEditor::_add_menu_type)); diff --git a/editor/animation/animation_blend_space_1d_editor.h b/editor/animation/animation_blend_space_1d_editor.h index 9b851ee6f7..88cb8ea972 100644 --- a/editor/animation/animation_blend_space_1d_editor.h +++ b/editor/animation/animation_blend_space_1d_editor.h @@ -67,7 +67,8 @@ class AnimationNodeBlendSpace1DEditor : public AnimationTreeNodeEditorPlugin { SpinBox *max_value = nullptr; SpinBox *min_value = nullptr; - CheckBox *sync = nullptr; + OptionButton *sync = nullptr; + SpinBox *cyclic_length_value = nullptr; OptionButton *interpolation = nullptr; HBoxContainer *edit_hb = nullptr; @@ -83,6 +84,10 @@ class AnimationNodeBlendSpace1DEditor : public AnimationTreeNodeEditorPlugin { PanelContainer *error_panel = nullptr; RichTextLabel *error_label = nullptr; + HBoxContainer *invalid_point_warning_hb = nullptr; + Button *invalid_point_warning = nullptr; + void _show_invalid_point_warning(); + bool updating = false; static AnimationNodeBlendSpace1DEditor *singleton; diff --git a/editor/animation/animation_blend_space_2d_editor.cpp b/editor/animation/animation_blend_space_2d_editor.cpp index 1d46aa4eba..3e3635c76d 100644 --- a/editor/animation/animation_blend_space_2d_editor.cpp +++ b/editor/animation/animation_blend_space_2d_editor.cpp @@ -43,7 +43,6 @@ #include "editor/themes/editor_scale.h" #include "scene/animation/animation_blend_tree.h" #include "scene/gui/button.h" -#include "scene/gui/check_box.h" #include "scene/gui/grid_container.h" #include "scene/gui/line_edit.h" #include "scene/gui/option_button.h" @@ -90,6 +89,7 @@ void AnimationNodeBlendSpace2DEditor::edit(const Ref &p_node) { tool_triangle->set_disabled(read_only); auto_triangles->set_disabled(read_only); sync->set_disabled(read_only); + cyclic_length_value->set_editable(!read_only); interpolation->set_disabled(read_only); } @@ -535,6 +535,7 @@ void AnimationNodeBlendSpace2DEditor::_blend_space_draw() { int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label")); Ref icon = get_editor_theme_icon(SNAME("KeyValue")); Ref icon_selected = get_editor_theme_icon(SNAME("KeySelected")); + Ref icon_invalid = get_editor_theme_icon(SNAME("KeyInvalid")); const float pm = POINT_MARGIN * EDSCALE; @@ -629,6 +630,7 @@ void AnimationNodeBlendSpace2DEditor::_blend_space_draw() { blend_space_draw->draw_primitive(bl_points, colors, Vector()); } + bool does_include_invalid_key = false; points.clear(); for (int i = 0; i < blend_space->get_blend_point_count(); i++) { Vector2 point = blend_space->get_blend_point_position(i); @@ -646,8 +648,19 @@ void AnimationNodeBlendSpace2DEditor::_blend_space_draw() { points.push_back(ofs + point); + // Draw × marker on non-AnimationNodeAnimation points when in cyclic mode. + bool is_key_valid = true; + AnimationNodeBlendSpace2D::SyncMode sync_mode = blend_space->get_sync_mode(); + if (sync_mode == AnimationNodeBlendSpace2D::SYNC_MODE_CYCLIC_MUTABLE || sync_mode == AnimationNodeBlendSpace2D::SYNC_MODE_CYCLIC_CONSTANT) { + Ref node = blend_space->get_blend_point_node(i); + Ref anim_node = node; + if (anim_node.is_null()) { + is_key_valid = false; + does_include_invalid_key = true; + } + } Vector2 gui_point = (ofs + point - icon->get_size() / 2).floor(); - blend_space_draw->draw_texture(i == selected_point ? icon_selected : icon, gui_point); + blend_space_draw->draw_texture(is_key_valid ? (i == selected_point ? icon_selected : icon) : icon_invalid, gui_point); if (point.x >= 0.0 && point.x <= s.width && point.y >= 0.0 && point.y <= s.height && editing_point != i) { String name_text = show_indices ? itos(i) : String(blend_space->get_blend_point_name(i)); @@ -669,6 +682,11 @@ void AnimationNodeBlendSpace2DEditor::_blend_space_draw() { text_rects.write[i] = Rect2(Vector2(text_pos.x, text_pos.y - font->get_ascent(font_size)), text_size); } } + if (does_include_invalid_key) { + invalid_point_warning_hb->show(); + } else { + invalid_point_warning_hb->hide(); + } if (making_triangle.size()) { Vector bl_points; @@ -745,7 +763,10 @@ void AnimationNodeBlendSpace2DEditor::_update_space() { auto_triangles->set_pressed(blend_space->get_auto_triangles()); - sync->set_pressed(blend_space->is_using_sync()); + sync->select(blend_space->get_sync_mode()); + cyclic_length_value->set_value(blend_space->get_cyclic_length()); + cyclic_length_value->set_visible(blend_space->get_sync_mode() == AnimationNodeBlendSpace2D::SYNC_MODE_CYCLIC_CONSTANT); + interpolation->select(blend_space->get_blend_mode()); max_x_value->set_value(blend_space->get_max_space().x); @@ -785,8 +806,10 @@ void AnimationNodeBlendSpace2DEditor::_config_changed(double) { undo_redo->add_undo_method(blend_space.ptr(), "set_min_space", blend_space->get_min_space()); undo_redo->add_do_method(blend_space.ptr(), "set_snap", Vector2(snap_x->get_value(), snap_y->get_value())); undo_redo->add_undo_method(blend_space.ptr(), "set_snap", blend_space->get_snap()); - undo_redo->add_do_method(blend_space.ptr(), "set_use_sync", sync->is_pressed()); - undo_redo->add_undo_method(blend_space.ptr(), "set_use_sync", blend_space->is_using_sync()); + undo_redo->add_do_method(blend_space.ptr(), "set_sync_mode", sync->get_selected()); + undo_redo->add_undo_method(blend_space.ptr(), "set_sync_mode", blend_space->get_sync_mode()); + undo_redo->add_do_method(blend_space.ptr(), "set_cyclic_length", cyclic_length_value->get_value()); + undo_redo->add_undo_method(blend_space.ptr(), "set_cyclic_length", blend_space->get_cyclic_length()); undo_redo->add_do_method(blend_space.ptr(), "set_blend_mode", interpolation->get_selected()); undo_redo->add_undo_method(blend_space.ptr(), "set_blend_mode", blend_space->get_blend_mode()); undo_redo->add_do_method(this, "_update_space"); @@ -794,6 +817,9 @@ void AnimationNodeBlendSpace2DEditor::_config_changed(double) { undo_redo->commit_action(); updating = false; + // Update cyclic_length visibility immediately (undo/redo calls _update_space while updating=true). + cyclic_length_value->set_visible(sync->get_selected() == AnimationNodeBlendSpace2D::SYNC_MODE_CYCLIC_CONSTANT); + blend_space_draw->queue_redraw(); } @@ -998,6 +1024,7 @@ void AnimationNodeBlendSpace2DEditor::_notification(int p_what) { interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackContinuous")), TTR("Continuous"), 0); interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackDiscrete")), TTR("Discrete"), 1); interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackCapture")), TTR("Capture"), 2); + invalid_point_warning->set_button_icon(get_editor_theme_icon(SNAME("NodeWarning"))); } break; case NOTIFICATION_PROCESS: { @@ -1015,6 +1042,11 @@ void AnimationNodeBlendSpace2DEditor::_notification(int p_what) { } } +void AnimationNodeBlendSpace2DEditor::_show_invalid_point_warning() { + EditorNode::get_singleton()->show_warning( + TTR("BlendSpace contains points that are not AnimationNodeAnimation.\nCyclic sync modes require that all blend points use AnimationNodeAnimation with a finite, immutable length.")); +} + void AnimationNodeBlendSpace2DEditor::_open_editor() { if (selected_point >= 0 && selected_point < blend_space->get_blend_point_count()) { Ref an = blend_space->get_blend_point_node(selected_point); @@ -1265,9 +1297,24 @@ AnimationNodeBlendSpace2DEditor::AnimationNodeBlendSpace2DEditor() { top_hb->add_child(memnew(VSeparator)); top_hb->add_child(memnew(Label(TTR("Sync")))); - sync = memnew(CheckBox); + sync = memnew(OptionButton); + sync->add_item(TTR("None")); + sync->add_item(TTR("Independent")); + sync->add_item(TTR("Cyclic Mutable")); + sync->add_item(TTR("Cyclic Constant")); top_hb->add_child(sync); - sync->connect(SceneStringName(toggled), callable_mp(this, &AnimationNodeBlendSpace2DEditor::_config_changed)); + sync->connect(SceneStringName(item_selected), callable_mp(this, &AnimationNodeBlendSpace2DEditor::_config_changed)); + + cyclic_length_value = memnew(SpinBox); + cyclic_length_value->set_min(0.0); + cyclic_length_value->set_max(99.0); + cyclic_length_value->set_step(0.001); + cyclic_length_value->set_allow_greater(true); + cyclic_length_value->set_suffix("s"); + cyclic_length_value->set_accessibility_name(TTRC("Cyclic Length")); + cyclic_length_value->set_tooltip_text(TTR("Cycle length in seconds for cyclic sync. All animations are time-scaled to complete one cycle in this duration.")); + top_hb->add_child(cyclic_length_value); + cyclic_length_value->connect(SceneStringName(value_changed), callable_mp(this, &AnimationNodeBlendSpace2DEditor::_config_changed)); top_hb->add_child(memnew(VSeparator)); @@ -1423,6 +1470,16 @@ AnimationNodeBlendSpace2DEditor::AnimationNodeBlendSpace2DEditor() { error_panel->add_child(error_label); error_panel->hide(); + invalid_point_warning_hb = memnew(HBoxContainer); + add_child(invalid_point_warning_hb); + invalid_point_warning_hb->hide(); + + invalid_point_warning = memnew(Button); + invalid_point_warning->set_text(TTRC("Contains Invalid Point")); + invalid_point_warning->set_tooltip_text(TTRC("Warning: BlendSpace contains invalid points for cyclic sync")); + invalid_point_warning->connect(SceneStringName(pressed), callable_mp(this, &AnimationNodeBlendSpace2DEditor::_show_invalid_point_warning)); + invalid_point_warning_hb->add_child(invalid_point_warning); + set_custom_minimum_size(Size2(0, 300 * EDSCALE)); menu = memnew(PopupMenu); diff --git a/editor/animation/animation_blend_space_2d_editor.h b/editor/animation/animation_blend_space_2d_editor.h index e0e59130d4..ec6749c5a6 100644 --- a/editor/animation/animation_blend_space_2d_editor.h +++ b/editor/animation/animation_blend_space_2d_editor.h @@ -64,7 +64,8 @@ class AnimationNodeBlendSpace2DEditor : public AnimationTreeNodeEditorPlugin { Button *snap = nullptr; SpinBox *snap_x = nullptr; SpinBox *snap_y = nullptr; - CheckBox *sync = nullptr; + OptionButton *sync = nullptr; + SpinBox *cyclic_length_value = nullptr; OptionButton *interpolation = nullptr; Button *auto_triangles = nullptr; @@ -91,6 +92,10 @@ class AnimationNodeBlendSpace2DEditor : public AnimationTreeNodeEditorPlugin { PanelContainer *error_panel = nullptr; RichTextLabel *error_label = nullptr; + HBoxContainer *invalid_point_warning_hb = nullptr; + Button *invalid_point_warning = nullptr; + void _show_invalid_point_warning(); + bool updating; static AnimationNodeBlendSpace2DEditor *singleton; diff --git a/scene/animation/animation_blend_space_1d.cpp b/scene/animation/animation_blend_space_1d.cpp index d3ac347dae..6128ecb789 100644 --- a/scene/animation/animation_blend_space_1d.cpp +++ b/scene/animation/animation_blend_space_1d.cpp @@ -63,6 +63,7 @@ Ref AnimationNodeBlendSpace1D::get_child_by_name(const StringName void AnimationNodeBlendSpace1D::_tree_changed() { AnimationRootNode::_tree_changed(); + _check_can_sync(); } void AnimationNodeBlendSpace1D::_animation_node_renamed(const ObjectID &p_oid, const String &p_old_name, const String &p_new_name) { @@ -109,19 +110,38 @@ void AnimationNodeBlendSpace1D::_bind_methods() { ClassDB::bind_method(D_METHOD("set_blend_mode", "mode"), &AnimationNodeBlendSpace1D::set_blend_mode); ClassDB::bind_method(D_METHOD("get_blend_mode"), &AnimationNodeBlendSpace1D::get_blend_mode); +#ifndef DISABLE_DEPRECATED ClassDB::bind_method(D_METHOD("set_use_sync", "enable"), &AnimationNodeBlendSpace1D::set_use_sync); ClassDB::bind_method(D_METHOD("is_using_sync"), &AnimationNodeBlendSpace1D::is_using_sync); +#endif // DISABLE_DEPRECATED + + ClassDB::bind_method(D_METHOD("set_sync_mode", "sync_mode"), &AnimationNodeBlendSpace1D::set_sync_mode); + ClassDB::bind_method(D_METHOD("get_sync_mode"), &AnimationNodeBlendSpace1D::get_sync_mode); + + ClassDB::bind_method(D_METHOD("set_cyclic_length", "length"), &AnimationNodeBlendSpace1D::set_cyclic_length); + ClassDB::bind_method(D_METHOD("get_cyclic_length"), &AnimationNodeBlendSpace1D::get_cyclic_length); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "min_space", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_min_space", "get_min_space"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "max_space", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_max_space", "get_max_space"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "snap", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_snap", "get_snap"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "value_label", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_value_label", "get_value_label"); ADD_PROPERTY(PropertyInfo(Variant::INT, "blend_mode", PROPERTY_HINT_ENUM, "Interpolated,Discrete,Carry", PROPERTY_USAGE_NO_EDITOR), "set_blend_mode", "get_blend_mode"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "sync", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_use_sync", "is_using_sync"); +#ifndef DISABLE_DEPRECATED + // Keep "sync" as bool with old getter/setter for GDExtension API backward compatibility (4.0-4.6). + // Old scenes with "sync = true" still load via set_use_sync -> SYNC_MODE_INDEPENDENT. + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "sync", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_use_sync", "is_using_sync"); +#endif // DISABLE_DEPRECATED + ADD_PROPERTY(PropertyInfo(Variant::INT, "sync_mode", PROPERTY_HINT_ENUM, "None,Independent,Cyclic Mutable,Cyclic Constant", PROPERTY_USAGE_NO_EDITOR), "set_sync_mode", "get_sync_mode"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "cyclic_length", PROPERTY_HINT_RANGE, "0.0,99.0,0.001,or_greater", PROPERTY_USAGE_NO_EDITOR), "set_cyclic_length", "get_cyclic_length"); BIND_ENUM_CONSTANT(BLEND_MODE_INTERPOLATED); BIND_ENUM_CONSTANT(BLEND_MODE_DISCRETE); BIND_ENUM_CONSTANT(BLEND_MODE_DISCRETE_CARRY); + + BIND_ENUM_CONSTANT(SYNC_MODE_NONE); + BIND_ENUM_CONSTANT(SYNC_MODE_INDEPENDENT); + BIND_ENUM_CONSTANT(SYNC_MODE_CYCLIC_MUTABLE); + BIND_ENUM_CONSTANT(SYNC_MODE_CYCLIC_CONSTANT); } void AnimationNodeBlendSpace1D::get_child_nodes(LocalVector *r_child_nodes) { @@ -161,6 +181,7 @@ void AnimationNodeBlendSpace1D::add_blend_point(const Ref &p_ _add_node(blend_points[p_at_index].node); blend_points_used++; + lengths_dirty = true; _tree_changed(); } @@ -181,6 +202,7 @@ void AnimationNodeBlendSpace1D::set_blend_point_node(int p_point, const Ref CMP_EPSILON) ? (1.0 / p_length) : 0.0; +} + +double AnimationNodeBlendSpace1D::get_cyclic_length() const { + return cyclic_length; } bool AnimationNodeBlendSpace1D::_set(const StringName &p_name, const Variant &p_value) { @@ -365,8 +408,22 @@ void AnimationNodeBlendSpace1D::_get_property_list(List *p_list) c } } +void AnimationNodeBlendSpace1D::_check_can_sync() { + is_contain_invalid_point = false; + if (sync_mode < SYNC_MODE_CYCLIC_MUTABLE) { + return; + } + for (int i = 0; i < blend_points_used; i++) { + Ref na = static_cast>(blend_points[i].node); + if (na.is_null()) { + is_contain_invalid_point = true; + break; + } + } +} + AnimationNode::NodeTimeInfo AnimationNodeBlendSpace1D::_process(ProcessState &p_process_state, AnimationNodeInstance &p_instance, const AnimationMixer::PlaybackInfo &p_playback_info, bool p_test_only) { - if (!blend_points_used) { + if (!blend_points_used || is_contain_invalid_point) { return NodeTimeInfo(); } @@ -383,6 +440,18 @@ AnimationNode::NodeTimeInfo AnimationNodeBlendSpace1D::_process(ProcessState &p_ int cur_closest = p_instance.get_parameter_closest(); NodeTimeInfo mind; + // Build weights array for all blend points. + LocalVector weights; + LocalVector deltas; + weights.resize_initialized(blend_points_used); + deltas.resize_initialized(blend_points_used); + if (sync_mode == SYNC_MODE_NONE) { + for (int i = 0; i < blend_points_used; i++) { + deltas[i] = -1; // Not process. + } + } + + int new_closest = -1; if (blend_mode == BLEND_MODE_INTERPOLATED) { int point_lower = -1; float pos_lower = 0.0; @@ -405,57 +474,28 @@ AnimationNode::NodeTimeInfo AnimationNodeBlendSpace1D::_process(ProcessState &p_ } // fill in weights - float weights[MAX_BLEND_POINTS] = {}; if (point_lower == -1 && point_higher != -1) { - // we are on the left side, no other point to the left - // we just play the next point. - weights[point_higher] = 1.0; } else if (point_higher == -1) { - // we are on the right side, no other point to the right - // we just play the previous point - weights[point_lower] = 1.0; } else { - // we are between two points. - // figure out weights, then blend the animations - float distance_between_points = pos_higher - pos_lower; - float current_pos_inbetween = blend_pos - pos_lower; - float blend_percentage = current_pos_inbetween / distance_between_points; - - float blend_lower = 1.0 - blend_percentage; - float blend_higher = blend_percentage; - - weights[point_lower] = blend_lower; - weights[point_higher] = blend_higher; + weights[point_lower] = 1.0 - blend_percentage; + weights[point_higher] = blend_percentage; } - // actually blend the animations now - bool first = true; - double max_weight = 0.0; + // Determine closest for return info. + float max_weight = 0.0; for (int i = 0; i < blend_points_used; i++) { - if (i == point_lower || i == point_higher) { - pi.weight = weights[i]; - AnimationNodeInstance &other_instance = p_instance.get_child_instance_by_path(get_blend_point_name(i)); - NodeTimeInfo t = blend_node(p_process_state, p_instance, &other_instance, pi, FILTER_IGNORE, true, p_test_only); - if (first || pi.weight > max_weight) { - max_weight = pi.weight; - mind = t; - first = false; - } - } else if (sync) { - pi.weight = 0; - AnimationNodeInstance &other_instance = p_instance.get_child_instance_by_path(get_blend_point_name(i)); - blend_node(p_process_state, p_instance, &other_instance, pi, FILTER_IGNORE, true, p_test_only); + if (weights[i] >= max_weight) { + max_weight = weights[i]; + new_closest = i; } } } else { - int new_closest = -1; double new_closest_dist = 1e20; - for (int i = 0; i < blend_points_used; i++) { double d = std::abs(blend_points[i].position - blend_pos); if (d < new_closest_dist) { @@ -463,49 +503,98 @@ AnimationNode::NodeTimeInfo AnimationNodeBlendSpace1D::_process(ProcessState &p_ new_closest_dist = d; } } + weights[new_closest] = 1.0; + } - AnimationNodeInstance *instance_current_closest = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(cur_closest)); - if (new_closest != cur_closest && new_closest != -1) { - AnimationNodeInstance *instance_new_closest = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(new_closest)); - - if (blend_mode == BLEND_MODE_DISCRETE_CARRY && cur_closest != -1) { - NodeTimeInfo from; - // For ping-pong loop. - Ref na_c = static_cast>(blend_points[cur_closest].node); - Ref na_n = static_cast>(blend_points[new_closest].node); - if (na_c.is_valid() && na_n.is_valid() && instance_current_closest && instance_new_closest) { - na_n->set_backward(*instance_new_closest, p_process_state, na_c->is_backward(*instance_current_closest, p_process_state)); - na_n = nullptr; - na_c = nullptr; - } - // See how much animation remains. - pi.seeked = false; - pi.weight = 0; - from = blend_node(p_process_state, p_instance, instance_current_closest, pi, FILTER_IGNORE, true, true); - pi.time = from.position; - } - pi.seeked = true; - pi.weight = 1.0; - mind = blend_node(p_process_state, p_instance, instance_new_closest, pi, FILTER_IGNORE, true, p_test_only); - cur_closest = new_closest; - } else { - pi.weight = 1.0; - mind = blend_node(p_process_state, p_instance, instance_current_closest, pi, FILTER_IGNORE, true, p_test_only); - } - - if (sync) { - pi = p_playback_info; - pi.weight = 0; + // Compute all deltas. + if (sync_mode >= SYNC_MODE_CYCLIC_MUTABLE) { + // Refresh cached lengths when blend points have changed. + if (lengths_dirty) { + cached_lengths.resize(blend_points_used); for (int i = 0; i < blend_points_used; i++) { - if (i != cur_closest) { - AnimationNodeInstance &other_instance = p_instance.get_child_instance_by_path(get_blend_point_name(i)); - blend_node(p_process_state, p_instance, &other_instance, pi, FILTER_IGNORE, true, p_test_only); + AnimationMixer::PlaybackInfo test_pi = p_playback_info; + test_pi.weight = 0; + AnimationNodeInstance &other_instance = p_instance.get_child_instance_by_path(get_blend_point_name(i)); + NodeTimeInfo info = blend_node(p_process_state, p_instance, &other_instance, test_pi, FILTER_IGNORE, true, true); + cached_lengths[i] = (info.length > CMP_EPSILON) ? info.length : 0.0; + } + lengths_dirty = false; + } + double inv_target_length = 0.0; + if (sync_mode == SYNC_MODE_CYCLIC_MUTABLE) { + // Compute blended_len from active weights. + double target_length = 0.0; + double total_weight = 0.0; + for (int i = 0; i < blend_points_used; i++) { + if (weights[i] > 0.0f && cached_lengths[i] > CMP_EPSILON) { + target_length += weights[i] * cached_lengths[i]; + total_weight += weights[i]; } } + if (total_weight > CMP_EPSILON) { + target_length /= total_weight; + } + inv_target_length = (target_length > CMP_EPSILON) ? (1.0 / target_length) : 0.0; + } else { + // Use cached inverse of user-specified cyclic_length. + inv_target_length = inverted_cycle_length; + } + for (int i = 0; i < blend_points_used; i++) { + deltas[i] = pi.delta * cached_lengths[i] * inv_target_length; + } + } else if (sync_mode == SYNC_MODE_INDEPENDENT) { + for (int i = 0; i < blend_points_used; i++) { + deltas[i] = pi.delta; + } + } else { + // SYNC_MODE_NONE: only active points get delta. + for (int i = 0; i < blend_points_used; i++) { + if (weights[i] >= CMP_EPSILON) { + deltas[i] = pi.delta; + } } } - p_instance.set_parameter_closest(cur_closest, p_process_state.is_testing); + // Special case for discrete carry. + AnimationNodeInstance *instance_current_closest = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(cur_closest)); + if (new_closest != cur_closest && new_closest != -1 && cur_closest != -1 && blend_mode == BLEND_MODE_DISCRETE_CARRY && sync_mode < SYNC_MODE_CYCLIC_MUTABLE) { + AnimationNodeInstance *instance_new_closest = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(new_closest)); + NodeTimeInfo from; + // For ping-pong loop. + Ref na_c = static_cast>(blend_points[cur_closest].node); + Ref na_n = static_cast>(blend_points[new_closest].node); + if (na_c.is_valid() && na_n.is_valid() && instance_current_closest && instance_new_closest) { + na_n->set_backward(*instance_new_closest, p_process_state, na_c->is_backward(*instance_current_closest, p_process_state)); + na_n = nullptr; + na_c = nullptr; + } + // See how much animation remains. + pi.seeked = false; + pi.weight = 0; + from = blend_node(p_process_state, p_instance, instance_current_closest, pi, FILTER_IGNORE, true, true); + pi.time = from.position; + pi.seeked = true; + pi.weight = 1.0; + mind = blend_node(p_process_state, p_instance, instance_new_closest, pi, FILTER_IGNORE, true, p_test_only); + deltas[new_closest] = -1; // No more blend new_closest point. + } + + // Normal case, blend all points. + for (int i = 0; i < blend_points_used; i++) { + if (std::signbit(deltas[i])) { + continue; + } + pi = p_playback_info; + pi.weight = weights[i]; + pi.delta = deltas[i]; + AnimationNodeInstance &other_instance = p_instance.get_child_instance_by_path(get_blend_point_name(i)); + NodeTimeInfo t = blend_node(p_process_state, p_instance, &other_instance, pi, FILTER_IGNORE, true, p_test_only); + if (i == new_closest) { + mind = t; + } + } + + p_instance.set_parameter_closest(new_closest, p_process_state.is_testing); return mind; } diff --git a/scene/animation/animation_blend_space_1d.h b/scene/animation/animation_blend_space_1d.h index 701e76a7a2..8329b753f0 100644 --- a/scene/animation/animation_blend_space_1d.h +++ b/scene/animation/animation_blend_space_1d.h @@ -42,6 +42,13 @@ public: BLEND_MODE_DISCRETE_CARRY, }; + enum SyncMode { + SYNC_MODE_NONE, // Inactive animations are frozen (not advanced). + SYNC_MODE_INDEPENDENT, // Inactive animations advance with weight=0 (previous "sync" behavior). + SYNC_MODE_CYCLIC_MUTABLE, // Time-scaled with blend-weight-dependent cycle length (self-normalizing). + SYNC_MODE_CYCLIC_CONSTANT, // Time-scaled to complete one cycle in cyclic_length seconds. + }; + protected: enum { MAX_BLEND_POINTS = 64 @@ -72,7 +79,11 @@ protected: BlendMode blend_mode = BLEND_MODE_INTERPOLATED; - bool sync = false; + SyncMode sync_mode = SYNC_MODE_NONE; + double cyclic_length = 0.0; + double inverted_cycle_length = 0.0; // Cached 1/cyclic_length. + LocalVector cached_lengths; + bool lengths_dirty = true; static void _bind_methods(); @@ -80,6 +91,9 @@ protected: virtual void _animation_node_renamed(const ObjectID &p_oid, const String &p_old_name, const String &p_new_name) override; virtual void _animation_node_removed(const ObjectID &p_oid, const StringName &p_node) override; + bool is_contain_invalid_point = false; + void _check_can_sync(); + #ifndef DISABLE_DEPRECATED void _add_blend_point_bind_compat_110369(const Ref &p_node, float p_position, int p_at_index = -1); static void _bind_compatibility_methods(); @@ -122,8 +136,16 @@ public: void set_blend_mode(BlendMode p_blend_mode); BlendMode get_blend_mode() const; - void set_use_sync(bool p_sync); - bool is_using_sync() const; + void set_cyclic_length(double p_length); + double get_cyclic_length() const; + +#ifndef DISABLE_DEPRECATED + void set_use_sync(bool p_sync); // Compat: maps to SYNC_MODE_INDEPENDENT or SYNC_MODE_NONE. + bool is_using_sync() const; // Compat: returns sync_mode != SYNC_MODE_NONE. +#endif // DISABLE_DEPRECATED + + void set_sync_mode(SyncMode p_sync_mode); + SyncMode get_sync_mode() const; virtual NodeTimeInfo _process(ProcessState &p_process_state, AnimationNodeInstance &p_instance, const AnimationMixer::PlaybackInfo &p_playback_info, bool p_test_only = false) override; String get_caption() const override; @@ -132,3 +154,4 @@ public: }; VARIANT_ENUM_CAST(AnimationNodeBlendSpace1D::BlendMode) +VARIANT_ENUM_CAST(AnimationNodeBlendSpace1D::SyncMode) diff --git a/scene/animation/animation_blend_space_2d.cpp b/scene/animation/animation_blend_space_2d.cpp index 71382c6248..befe42ad0b 100644 --- a/scene/animation/animation_blend_space_2d.cpp +++ b/scene/animation/animation_blend_space_2d.cpp @@ -99,6 +99,7 @@ void AnimationNodeBlendSpace2D::add_blend_point(const Ref &p_ _add_node(blend_points[p_at_index].node); blend_points_used++; + lengths_dirty = true; _queue_auto_triangles(); _tree_changed(); @@ -120,6 +121,7 @@ void AnimationNodeBlendSpace2D::set_blend_point_node(int p_point, const Ref &p_points, LocalVector &r_weights) { if (p_pos.is_equal_approx(p_points[0])) { r_weights[0] = 1; r_weights[1] = 0; @@ -551,32 +554,61 @@ void AnimationNodeBlendSpace2D::_blend_triangle(const Vector2 &p_pos, const Vect r_weights[2] = w; } +void AnimationNodeBlendSpace2D::_check_can_sync() { + is_contain_invalid_point = false; + if (sync_mode < SYNC_MODE_CYCLIC_MUTABLE) { + return; + } + for (int i = 0; i < blend_points_used; i++) { + Ref na = static_cast>(blend_points[i].node); + if (na.is_null()) { + is_contain_invalid_point = true; + break; + } + } +} + AnimationNode::NodeTimeInfo AnimationNodeBlendSpace2D::_process(ProcessState &p_process_state, AnimationNodeInstance &p_instance, const AnimationMixer::PlaybackInfo &p_playback_info, bool p_test_only) { _update_triangles(); - if (!blend_points_used) { + if (!blend_points_used || is_contain_invalid_point) { return NodeTimeInfo(); } - Vector2 blend_pos = p_instance.get_parameter(blend_position); - int cur_closest = p_instance.get_parameter_closest(); - NodeTimeInfo mind; //time of min distance point + constexpr int TRIANGLE_VERTS = 3; AnimationMixer::PlaybackInfo pi = p_playback_info; + Vector2 blend_pos = p_instance.get_parameter(blend_position); + int cur_closest = p_instance.get_parameter_closest(); + NodeTimeInfo mind; // Time of closest point. + // Build weights array for all blend points. + LocalVector weights; + LocalVector deltas; + weights.resize_initialized(blend_points_used); + deltas.resize_initialized(blend_points_used); + if (sync_mode == SYNC_MODE_NONE) { + for (int i = 0; i < blend_points_used; i++) { + deltas[i] = -1; // Not process. + } + } + + int new_closest = -1; + LocalVector triangle_points; + LocalVector blend_weights; + triangle_points.resize_initialized(TRIANGLE_VERTS); + blend_weights.resize_initialized(TRIANGLE_VERTS); if (blend_mode == BLEND_MODE_INTERPOLATED) { if (triangles.is_empty()) { return NodeTimeInfo(); } - Vector2 best_point; - bool first = true; int blend_triangle = -1; - float blend_weights[3] = { 0, 0, 0 }; - + bool first = true; + LocalVector points; for (int i = 0; i < triangles.size(); i++) { - Vector2 points[3]; - for (int j = 0; j < 3; j++) { + points.resize_initialized(TRIANGLE_VERTS); + for (int j = 0; j < TRIANGLE_VERTS; j++) { points[j] = get_blend_point_position(get_triangle_point(i, j)); } @@ -586,9 +618,9 @@ AnimationNode::NodeTimeInfo AnimationNodeBlendSpace2D::_process(ProcessState &p_ break; } - for (int j = 0; j < 3; j++) { + for (int j = 0; j < TRIANGLE_VERTS; j++) { const Vector2 segment_a = points[j]; - const Vector2 segment_b = points[(j + 1) % 3]; + const Vector2 segment_b = points[(j + 1) % TRIANGLE_VERTS]; Vector2 closest2 = Geometry2D::get_closest_point_to_segment(blend_pos, segment_a, segment_b); if (first || closest2.distance_to(blend_pos) < best_point.distance_to(blend_pos)) { best_point = closest2; @@ -597,57 +629,33 @@ AnimationNode::NodeTimeInfo AnimationNodeBlendSpace2D::_process(ProcessState &p_ const real_t d = segment_a.distance_to(segment_b); if (d == 0.0) { blend_weights[j] = 1.0; - blend_weights[(j + 1) % 3] = 0.0; - blend_weights[(j + 2) % 3] = 0.0; + blend_weights[(j + 1) % TRIANGLE_VERTS] = 0.0; + blend_weights[(j + 2) % TRIANGLE_VERTS] = 0.0; } else { const real_t c = segment_a.distance_to(closest2) / d; - blend_weights[j] = 1.0 - c; - blend_weights[(j + 1) % 3] = c; - blend_weights[(j + 2) % 3] = 0.0; + blend_weights[(j + 1) % TRIANGLE_VERTS] = c; + blend_weights[(j + 2) % TRIANGLE_VERTS] = 0.0; } } } } + ERR_FAIL_COND_V(blend_triangle == -1, NodeTimeInfo()); // Should never reach here. - ERR_FAIL_COND_V(blend_triangle == -1, NodeTimeInfo()); //should never reach here - - int triangle_points[3]; - for (int j = 0; j < 3; j++) { - triangle_points[j] = get_triangle_point(blend_triangle, j); + for (int i = 0; i < TRIANGLE_VERTS; i++) { + triangle_points[i] = get_triangle_point(blend_triangle, i); } - first = true; - - double max_weight = 0.0; - for (int i = 0; i < blend_points_used; i++) { - bool found = false; - for (int j = 0; j < 3; j++) { - if (i == triangle_points[j]) { - //blend with the given weight - pi.weight = blend_weights[j]; - AnimationNodeInstance *other_instance = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(i)); - NodeTimeInfo t = blend_node(p_process_state, p_instance, other_instance, pi, FILTER_IGNORE, true, p_test_only); - if (first || pi.weight > max_weight) { - mind = t; - max_weight = pi.weight; - first = false; - } - found = true; - break; - } - } - - if (sync && !found) { - pi.weight = 0; - AnimationNodeInstance *other_instance = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(i)); - blend_node(p_process_state, p_instance, other_instance, pi, FILTER_IGNORE, true, p_test_only); + float max_weight = 0.0; + for (int i = 0; i < TRIANGLE_VERTS; i++) { + weights[triangle_points[i]] = blend_weights[i]; + if (blend_weights[i] >= max_weight) { + max_weight = blend_weights[i]; + new_closest = triangle_points[i]; } } } else { - int new_closest = -1; float new_closest_dist = 1e20; - for (int i = 0; i < blend_points_used; i++) { float d = blend_points[i].position.distance_squared_to(blend_pos); if (d < new_closest_dist) { @@ -655,49 +663,98 @@ AnimationNode::NodeTimeInfo AnimationNodeBlendSpace2D::_process(ProcessState &p_ new_closest_dist = d; } } + weights[new_closest] = 1.0; + } - AnimationNodeInstance *instance_current_closest = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(cur_closest)); - if (new_closest != cur_closest && new_closest != -1) { - AnimationNodeInstance *instance_new_closest = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(new_closest)); - - if (blend_mode == BLEND_MODE_DISCRETE_CARRY && cur_closest != -1) { - NodeTimeInfo from; - // For ping-pong loop. - Ref na_c = static_cast>(blend_points[cur_closest].node); - Ref na_n = static_cast>(blend_points[new_closest].node); - if (na_c.is_valid() && na_n.is_valid() && instance_current_closest && instance_new_closest) { - na_n->set_backward(*instance_new_closest, p_process_state, na_c->is_backward(*instance_current_closest, p_process_state)); - na_n = nullptr; - na_c = nullptr; - } - // See how much animation remains. - pi.seeked = false; - pi.weight = 0; - from = blend_node(p_process_state, p_instance, instance_current_closest, pi, FILTER_IGNORE, true, true); - pi.time = from.position; - } - pi.seeked = true; - pi.weight = 1.0; - mind = blend_node(p_process_state, p_instance, instance_new_closest, pi, FILTER_IGNORE, true, p_test_only); - cur_closest = new_closest; - } else { - pi.weight = 1.0; - mind = blend_node(p_process_state, p_instance, instance_current_closest, pi, FILTER_IGNORE, true, p_test_only); - } - - if (sync) { - pi = p_playback_info; - pi.weight = 0; + // Compute all deltas. + if (sync_mode >= SYNC_MODE_CYCLIC_MUTABLE) { + // Refresh cached lengths when blend points have changed. + if (lengths_dirty) { + cached_lengths.resize(blend_points_used); for (int i = 0; i < blend_points_used; i++) { - if (i != cur_closest) { - AnimationNodeInstance &other_instance = p_instance.get_child_instance_by_path(get_blend_point_name(i)); - blend_node(p_process_state, p_instance, &other_instance, pi, FILTER_IGNORE, true, p_test_only); + AnimationMixer::PlaybackInfo test_pi = p_playback_info; + test_pi.weight = 0; + AnimationNodeInstance &other_instance = p_instance.get_child_instance_by_path(get_blend_point_name(i)); + NodeTimeInfo info = blend_node(p_process_state, p_instance, &other_instance, test_pi, FILTER_IGNORE, true, true); + cached_lengths[i] = (info.length > CMP_EPSILON) ? info.length : 0.0; + } + lengths_dirty = false; + } + double inv_target_length = 0.0; + if (sync_mode == SYNC_MODE_CYCLIC_MUTABLE) { + // Compute blended_len from active weights. + double target_length = 0.0; + double total_weight = 0.0; + for (int i = 0; i < blend_points_used; i++) { + if (weights[i] > 0.0f && cached_lengths[i] > CMP_EPSILON) { + target_length += weights[i] * cached_lengths[i]; + total_weight += weights[i]; } } + if (total_weight > CMP_EPSILON) { + target_length /= total_weight; + } + inv_target_length = (target_length > CMP_EPSILON) ? (1.0 / target_length) : 0.0; + } else { + // Use cached inverse of user-specified cyclic_length. + inv_target_length = inverted_cycle_length; + } + for (int i = 0; i < blend_points_used; i++) { + deltas[i] = pi.delta * cached_lengths[i] * inv_target_length; + } + } else if (sync_mode == SYNC_MODE_INDEPENDENT) { + for (int i = 0; i < blend_points_used; i++) { + deltas[i] = pi.delta; + } + } else { + // SYNC_MODE_NONE: only active points get delta. + for (int i = 0; i < blend_points_used; i++) { + if (weights[i] >= CMP_EPSILON) { + deltas[i] = pi.delta; + } } } - p_instance.set_parameter_closest(cur_closest, p_process_state.is_testing); + // Special case for discrete carry. + if (new_closest != cur_closest && new_closest != -1 && cur_closest != -1 && blend_mode == BLEND_MODE_DISCRETE_CARRY && sync_mode < SYNC_MODE_CYCLIC_MUTABLE) { + AnimationNodeInstance *instance_current_closest = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(cur_closest)); + AnimationNodeInstance *instance_new_closest = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(new_closest)); + NodeTimeInfo from; + // For ping-pong loop. + Ref na_c = static_cast>(blend_points[cur_closest].node); + Ref na_n = static_cast>(blend_points[new_closest].node); + if (na_c.is_valid() && na_n.is_valid() && instance_current_closest && instance_new_closest) { + na_n->set_backward(*instance_new_closest, p_process_state, na_c->is_backward(*instance_current_closest, p_process_state)); + na_n = nullptr; + na_c = nullptr; + } + // See how much animation remains. + pi.seeked = false; + pi.weight = 0; + from = blend_node(p_process_state, p_instance, instance_current_closest, pi, FILTER_IGNORE, true, true); + pi.time = from.position; + pi.seeked = true; + pi.weight = 1.0; + mind = blend_node(p_process_state, p_instance, instance_new_closest, pi, FILTER_IGNORE, true, p_test_only); + deltas[new_closest] = -1; // No more blend new_closest point. + } + + // Normal case, blend all points. + for (int i = 0; i < blend_points_used; i++) { + if (std::signbit(deltas[i])) { + continue; + } + pi = p_playback_info; + pi.weight = weights[i]; + pi.delta = deltas[i]; + AnimationNodeInstance *other_instance = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(i)); + NodeTimeInfo t = blend_node(p_process_state, p_instance, other_instance, pi, FILTER_IGNORE, true, p_test_only); + if (i == new_closest) { + mind = t; + } + } + + p_instance.set_parameter_closest(new_closest, p_process_state.is_testing); return mind; } @@ -740,16 +797,37 @@ AnimationNodeBlendSpace2D::BlendMode AnimationNodeBlendSpace2D::get_blend_mode() return blend_mode; } +#ifndef DISABLE_DEPRECATED void AnimationNodeBlendSpace2D::set_use_sync(bool p_sync) { - sync = p_sync; + sync_mode = p_sync ? SYNC_MODE_INDEPENDENT : SYNC_MODE_NONE; } bool AnimationNodeBlendSpace2D::is_using_sync() const { - return sync; + return sync_mode != SYNC_MODE_NONE; +} +#endif // DISABLE_DEPRECATED + +void AnimationNodeBlendSpace2D::set_sync_mode(SyncMode p_sync_mode) { + sync_mode = p_sync_mode; + _check_can_sync(); +} + +AnimationNodeBlendSpace2D::SyncMode AnimationNodeBlendSpace2D::get_sync_mode() const { + return sync_mode; +} + +void AnimationNodeBlendSpace2D::set_cyclic_length(double p_length) { + cyclic_length = p_length; + inverted_cycle_length = (p_length > CMP_EPSILON) ? (1.0 / p_length) : 0.0; +} + +double AnimationNodeBlendSpace2D::get_cyclic_length() const { + return cyclic_length; } void AnimationNodeBlendSpace2D::_tree_changed() { AnimationRootNode::_tree_changed(); + _check_can_sync(); } void AnimationNodeBlendSpace2D::_animation_node_renamed(const ObjectID &p_oid, const String &p_old_name, const String &p_new_name) { @@ -811,8 +889,16 @@ void AnimationNodeBlendSpace2D::_bind_methods() { ClassDB::bind_method(D_METHOD("set_blend_mode", "mode"), &AnimationNodeBlendSpace2D::set_blend_mode); ClassDB::bind_method(D_METHOD("get_blend_mode"), &AnimationNodeBlendSpace2D::get_blend_mode); +#ifndef DISABLE_DEPRECATED ClassDB::bind_method(D_METHOD("set_use_sync", "enable"), &AnimationNodeBlendSpace2D::set_use_sync); ClassDB::bind_method(D_METHOD("is_using_sync"), &AnimationNodeBlendSpace2D::is_using_sync); +#endif // DISABLE_DEPRECATED + + ClassDB::bind_method(D_METHOD("set_sync_mode", "sync_mode"), &AnimationNodeBlendSpace2D::set_sync_mode); + ClassDB::bind_method(D_METHOD("get_sync_mode"), &AnimationNodeBlendSpace2D::get_sync_mode); + + ClassDB::bind_method(D_METHOD("set_cyclic_length", "length"), &AnimationNodeBlendSpace2D::set_cyclic_length); + ClassDB::bind_method(D_METHOD("get_cyclic_length"), &AnimationNodeBlendSpace2D::get_cyclic_length); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_triangles", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_auto_triangles", "get_auto_triangles"); ADD_PROPERTY(PropertyInfo(Variant::PACKED_INT32_ARRAY, "triangles", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_triangles", "_get_triangles"); @@ -823,10 +909,19 @@ void AnimationNodeBlendSpace2D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::STRING, "x_label", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_x_label", "get_x_label"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "y_label", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_y_label", "get_y_label"); ADD_PROPERTY(PropertyInfo(Variant::INT, "blend_mode", PROPERTY_HINT_ENUM, "Interpolated,Discrete,Carry", PROPERTY_USAGE_NO_EDITOR), "set_blend_mode", "get_blend_mode"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "sync", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_use_sync", "is_using_sync"); +#ifndef DISABLE_DEPRECATED + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "sync", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_use_sync", "is_using_sync"); +#endif // DISABLE_DEPRECATED + ADD_PROPERTY(PropertyInfo(Variant::INT, "sync_mode", PROPERTY_HINT_ENUM, "None,Independent,Cyclic Mutable,Cyclic Constant", PROPERTY_USAGE_NO_EDITOR), "set_sync_mode", "get_sync_mode"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "cyclic_length", PROPERTY_HINT_RANGE, "0.0,99.0,0.001,or_greater", PROPERTY_USAGE_NO_EDITOR), "set_cyclic_length", "get_cyclic_length"); ADD_SIGNAL(MethodInfo("triangles_updated")); BIND_ENUM_CONSTANT(BLEND_MODE_INTERPOLATED); BIND_ENUM_CONSTANT(BLEND_MODE_DISCRETE); BIND_ENUM_CONSTANT(BLEND_MODE_DISCRETE_CARRY); + + BIND_ENUM_CONSTANT(SYNC_MODE_NONE); + BIND_ENUM_CONSTANT(SYNC_MODE_INDEPENDENT); + BIND_ENUM_CONSTANT(SYNC_MODE_CYCLIC_MUTABLE); + BIND_ENUM_CONSTANT(SYNC_MODE_CYCLIC_CONSTANT); } diff --git a/scene/animation/animation_blend_space_2d.h b/scene/animation/animation_blend_space_2d.h index 73a1bbbbb6..f7f9839004 100644 --- a/scene/animation/animation_blend_space_2d.h +++ b/scene/animation/animation_blend_space_2d.h @@ -42,6 +42,13 @@ public: BLEND_MODE_DISCRETE_CARRY, }; + enum SyncMode { + SYNC_MODE_NONE, // Inactive animations are frozen (not advanced). + SYNC_MODE_INDEPENDENT, // Inactive animations advance with weight=0 (previous "sync" behavior). + SYNC_MODE_CYCLIC_MUTABLE, // Time-scaled with blend-weight-dependent cycle length (self-normalizing). + SYNC_MODE_CYCLIC_CONSTANT, // Time-scaled to complete one cycle in cyclic_length seconds. + }; + protected: enum { MAX_BLEND_POINTS = 64 @@ -78,7 +85,7 @@ protected: void _set_triangles(const Vector &p_triangles); Vector _get_triangles() const; - void _blend_triangle(const Vector2 &p_pos, const Vector2 *p_points, float *r_weights); + void _blend_triangle(const Vector2 &p_pos, const LocalVector &p_points, LocalVector &r_weights); bool auto_triangles = true; bool triangles_dirty = false; @@ -86,7 +93,11 @@ protected: void _update_triangles(); void _queue_auto_triangles(); - bool sync = false; + SyncMode sync_mode = SYNC_MODE_NONE; + double cyclic_length = 0.0; + double inverted_cycle_length = 0.0; // Cached 1/cyclic_length. + LocalVector cached_lengths; + bool lengths_dirty = true; void _validate_property(PropertyInfo &p_property) const; static void _bind_methods(); @@ -95,6 +106,9 @@ protected: virtual void _animation_node_renamed(const ObjectID &p_oid, const String &p_old_name, const String &p_new_name) override; virtual void _animation_node_removed(const ObjectID &p_oid, const StringName &p_node) override; + bool is_contain_invalid_point = false; + void _check_can_sync(); + #ifndef DISABLE_DEPRECATED void _add_blend_point_bind_compat_110369(const Ref &p_node, const Vector2 &p_position, int p_at_index = -1); static void _bind_compatibility_methods(); @@ -153,10 +167,19 @@ public: void set_blend_mode(BlendMode p_blend_mode); BlendMode get_blend_mode() const; - void set_use_sync(bool p_sync); - bool is_using_sync() const; + void set_cyclic_length(double p_length); + double get_cyclic_length() const; + +#ifndef DISABLE_DEPRECATED + void set_use_sync(bool p_sync); // Compat: maps to SYNC_MODE_INDEPENDENT or SYNC_MODE_NONE. + bool is_using_sync() const; // Compat: returns sync_mode != SYNC_MODE_NONE. +#endif // DISABLE_DEPRECATED + + void set_sync_mode(SyncMode p_sync_mode); + SyncMode get_sync_mode() const; virtual Ref get_child_by_name(const StringName &p_name) const override; }; VARIANT_ENUM_CAST(AnimationNodeBlendSpace2D::BlendMode) +VARIANT_ENUM_CAST(AnimationNodeBlendSpace2D::SyncMode)