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)
]