Merge pull request #114556 from jgill88/gh-11698

Add ping-pong playback support to SpriteFrames / AnimatedSprite2D / AnimatedSprite3D
This commit is contained in:
Thaddeus Crews
2026-04-07 18:21:36 -05:00
8 changed files with 179 additions and 47 deletions
+32 -4
View File
@@ -47,11 +47,18 @@
Duplicates the animation [param anim_from] to a new animation named [param anim_to]. Fails if [param anim_to] already exists, or if [param anim_from] does not exist.
</description>
</method>
<method name="get_animation_loop" qualifiers="const">
<method name="get_animation_loop" qualifiers="const" deprecated="Use [method get_animation_loop_mode] instead.">
<return type="bool" />
<param index="0" name="anim" type="StringName" />
<description>
Returns [code]true[/code] if the given animation is configured to loop when it finishes playing. Otherwise, returns [code]false[/code].
Returns [code]true[/code] if [code]get_animation_loop_mode(anim) == LOOP_LINEAR[/code]. Otherwise, returns [code]false[/code].
</description>
</method>
<method name="get_animation_loop_mode" qualifiers="const">
<return type="int" enum="SpriteFrames.LoopMode" />
<param index="0" name="anim" type="StringName" />
<description>
Returns the loop mode for the [param anim] animation.
</description>
</method>
<method name="get_animation_names" qualifiers="const">
@@ -124,12 +131,21 @@
Changes the [param anim] animation's name to [param newname].
</description>
</method>
<method name="set_animation_loop">
<method name="set_animation_loop" deprecated="Use [method set_animation_loop_mode] instead.">
<return type="void" />
<param index="0" name="anim" type="StringName" />
<param index="1" name="loop" type="bool" />
<description>
If [param loop] is [code]true[/code], the [param anim] animation will loop when it reaches the end, or the start if it is played in reverse.
If [param loop] is [code]false[/code] equivalent to [code]set_animation_loop_mode(LOOP_NONE)[/code].
If [param loop] is [code]true[/code] equivalent to [code]set_animation_loop_mode(LOOP_LINEAR)[/code].
</description>
</method>
<method name="set_animation_loop_mode">
<return type="void" />
<param index="0" name="anim" type="StringName" />
<param index="1" name="loop_mode" type="int" enum="SpriteFrames.LoopMode" />
<description>
Sets the [param loop_mode] for the [param anim] animation.
</description>
</method>
<method name="set_animation_speed">
@@ -151,4 +167,16 @@
</description>
</method>
</methods>
<constants>
<constant name="LOOP_NONE" value="0" enum="LoopMode">
The animation plays once and stops when it reaches the end, or the start if played in reverse.
</constant>
<constant name="LOOP_LINEAR" value="1" enum="LoopMode">
The animation restarts from the beginning when it reaches the end, or from the end if played in reverse, repeating continuously.
</constant>
<constant name="LOOP_PINGPONG" value="2" enum="LoopMode">
The animation alternates direction each time it reaches the end or start, playing forward and then in reverse repeatedly.
[b]Note:[/b] Both [AnimatedSprite2D] and [AnimatedSprite3D] play the first/last frame for its duration only once at each end of the animation loop (instead of twice, once per forward/backward animation direction).
</constant>
</constants>
</class>
+43 -8
View File
@@ -702,7 +702,7 @@ void SpriteFramesEditor::_notification(int p_what) {
_update_stop_icon();
autoplay->set_button_icon(get_editor_theme_icon(SNAME("AutoPlay")));
anim_loop->set_button_icon(get_editor_theme_icon(SNAME("Loop")));
_update_anim_loop_button();
play->set_button_icon(get_editor_theme_icon(SNAME("PlayStart")));
play_from->set_button_icon(get_editor_theme_icon(SNAME("Play")));
play_bw->set_button_icon(get_editor_theme_icon(SNAME("PlayStartBackwards")));
@@ -1366,18 +1366,35 @@ void SpriteFramesEditor::_animation_search_text_changed(const String &p_text) {
_update_library();
}
void SpriteFramesEditor::_animation_loop_changed() {
void SpriteFramesEditor::_animation_loop_pressed() {
if (updating) {
return;
}
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
SpriteFrames::LoopMode to_loop = SpriteFrames::LOOP_NONE;
SpriteFrames::LoopMode from_loop = frames->get_animation_loop_mode(edited_anim);
switch (from_loop) {
case SpriteFrames::LOOP_NONE: {
to_loop = SpriteFrames::LOOP_LINEAR;
} break;
case SpriteFrames::LOOP_LINEAR: {
to_loop = SpriteFrames::LOOP_PINGPONG;
} break;
case SpriteFrames::LOOP_PINGPONG: {
to_loop = SpriteFrames::LOOP_NONE;
} break;
}
undo_redo->create_action(TTR("Change Animation Loop"), UndoRedo::MERGE_DISABLE, frames.ptr());
undo_redo->add_do_method(frames.ptr(), "set_animation_loop", edited_anim, anim_loop->is_pressed());
undo_redo->add_undo_method(frames.ptr(), "set_animation_loop", edited_anim, frames->get_animation_loop(edited_anim));
undo_redo->add_do_method(frames.ptr(), "set_animation_loop_mode", edited_anim, to_loop);
undo_redo->add_undo_method(frames.ptr(), "set_animation_loop_mode", edited_anim, from_loop);
undo_redo->add_do_method(this, "_update_library", true);
undo_redo->add_undo_method(this, "_update_library", true);
undo_redo->commit_action();
_update_anim_loop_button();
}
void SpriteFramesEditor::_animation_speed_resized() {
@@ -1601,6 +1618,25 @@ void SpriteFramesEditor::_zoom_reset() {
frame_list->set_fixed_icon_size(Size2(thumbnail_default_size, thumbnail_default_size));
}
void SpriteFramesEditor::_update_anim_loop_button() {
if (frames.is_null()) {
anim_loop->set_button_icon(get_editor_theme_icon(SNAME("Loop")));
return;
}
SpriteFrames::LoopMode loop = frames->get_animation_loop_mode(edited_anim);
anim_loop->set_pressed_no_signal(loop != SpriteFrames::LOOP_NONE);
switch (loop) {
case SpriteFrames::LOOP_NONE:
case SpriteFrames::LOOP_LINEAR: {
anim_loop->set_button_icon(get_editor_theme_icon(SNAME("Loop")));
} break;
case SpriteFrames::LOOP_PINGPONG: {
anim_loop->set_button_icon(get_editor_theme_icon(SNAME("PingPongLoop")));
} break;
}
}
void SpriteFramesEditor::_update_library(bool p_skip_selector) {
if (!p_skip_selector) {
animations_dirty = true;
@@ -1755,8 +1791,7 @@ void SpriteFramesEditor::_update_library_impl() {
}
anim_speed->set_value_no_signal(frames->get_animation_speed(edited_anim));
anim_loop->set_pressed_no_signal(frames->get_animation_loop(edited_anim));
_update_anim_loop_button();
updating = false;
}
@@ -2199,7 +2234,7 @@ SpriteFramesEditor::SpriteFramesEditor() {
anim_loop->set_toggle_mode(true);
anim_loop->set_theme_type_variation(SceneStringName(FlatButton));
anim_loop->set_tooltip_text(TTRC("Animation Looping"));
anim_loop->connect(SceneStringName(pressed), callable_mp(this, &SpriteFramesEditor::_animation_loop_changed));
anim_loop->connect(SceneStringName(pressed), callable_mp(this, &SpriteFramesEditor::_animation_loop_pressed));
hbc_animlist->add_child(anim_loop);
anim_speed = memnew(SpinBox);
@@ -2804,7 +2839,7 @@ Ref<ClipboardAnimation> ClipboardAnimation::from_sprite_frames(const Ref<SpriteF
clipboard_anim.instantiate();
clipboard_anim->name = p_anim;
clipboard_anim->speed = p_frames->get_animation_speed(p_anim);
clipboard_anim->loop = p_frames->get_animation_loop(p_anim);
clipboard_anim->loop = p_frames->get_animation_loop_mode(p_anim);
int frame_count = p_frames->get_frame_count(p_anim);
for (int i = 0; i < frame_count; ++i) {
+4 -2
View File
@@ -64,7 +64,7 @@ public:
String name;
Vector<ClipboardSpriteFrames::Frame> frames;
float speed = 1.0f;
bool loop = false;
SpriteFrames::LoopMode loop = SpriteFrames::LOOP_LINEAR;
static Ref<ClipboardAnimation> from_sprite_frames(const Ref<SpriteFrames> &p_frames, const String &p_anim);
};
@@ -239,11 +239,13 @@ class SpriteFramesEditor : public EditorDock {
void _animation_remove();
void _animation_remove_confirmed();
void _animation_search_text_changed(const String &p_text);
void _animation_loop_changed();
void _animation_loop_pressed();
void _animation_speed_resized();
void _animation_speed_changed(double p_value);
void _animation_remove_undo_redo(const StringName &p_action_name, const Vector<ClipboardSpriteFrames::Frame> *p_frames = nullptr);
void _update_anim_loop_button();
StringName _find_next_animation();
String _generate_unique_animation_name(const String &p_base_name) const;
+22 -8
View File
@@ -219,15 +219,22 @@ void AnimatedSprite2D::_notification(int p_what) {
// Forwards.
if (frame_progress >= 1.0) {
if (frame >= last_frame) {
if (frames->get_animation_loop(animation)) {
frame = 0;
emit_signal("animation_looped");
} else {
SpriteFrames::LoopMode loop = frames->get_animation_loop_mode(animation);
if (loop == SpriteFrames::LOOP_NONE) {
frame = last_frame;
pause();
emit_signal(SceneStringName(animation_finished));
return;
}
if (loop == SpriteFrames::LOOP_PINGPONG) {
frame = last_frame;
custom_speed_scale *= -1;
} else {
frame = 0;
}
emit_signal("animation_looped");
} else {
frame++;
}
@@ -243,15 +250,22 @@ void AnimatedSprite2D::_notification(int p_what) {
// Backwards.
if (frame_progress <= 0) {
if (frame <= 0) {
if (frames->get_animation_loop(animation)) {
frame = last_frame;
emit_signal("animation_looped");
} else {
SpriteFrames::LoopMode loop = frames->get_animation_loop_mode(animation);
if (loop == SpriteFrames::LOOP_NONE) {
frame = 0;
pause();
emit_signal(SceneStringName(animation_finished));
return;
}
if (loop == SpriteFrames::LOOP_PINGPONG) {
frame = 0;
custom_speed_scale *= -1;
} else {
frame = last_frame;
}
emit_signal("animation_looped");
} else {
frame--;
}
+20 -8
View File
@@ -1154,15 +1154,21 @@ void AnimatedSprite3D::_notification(int p_what) {
// Forwards.
if (frame_progress >= 1.0) {
if (frame >= last_frame) {
if (frames->get_animation_loop(animation)) {
frame = 0;
emit_signal("animation_looped");
} else {
SpriteFrames::LoopMode loop = frames->get_animation_loop_mode(animation);
if (loop == SpriteFrames::LOOP_NONE) {
frame = last_frame;
pause();
emit_signal(SceneStringName(animation_finished));
return;
}
if (loop == SpriteFrames::LOOP_PINGPONG) {
frame = last_frame;
custom_speed_scale *= -1;
} else {
frame = 0;
}
emit_signal("animation_looped");
} else {
frame++;
}
@@ -1178,15 +1184,21 @@ void AnimatedSprite3D::_notification(int p_what) {
// Backwards.
if (frame_progress <= 0) {
if (frame <= 0) {
if (frames->get_animation_loop(animation)) {
frame = last_frame;
emit_signal("animation_looped");
} else {
SpriteFrames::LoopMode loop = frames->get_animation_loop_mode(animation);
if (loop == SpriteFrames::LOOP_NONE) {
frame = 0;
pause();
emit_signal(SceneStringName(animation_finished));
return;
}
if (loop == SpriteFrames::LOOP_PINGPONG) {
frame = 0;
custom_speed_scale *= -1;
} else {
frame = last_frame;
}
emit_signal("animation_looped");
} else {
frame--;
}
+25 -5
View File
@@ -154,15 +154,25 @@ double SpriteFrames::get_animation_speed(const StringName &p_anim) const {
return E->value.speed;
}
#ifndef DISABLE_DEPRECATED
void SpriteFrames::set_animation_loop(const StringName &p_anim, bool p_loop) {
HashMap<StringName, Anim>::Iterator E = animations.find(p_anim);
ERR_FAIL_COND_MSG(!E, "Animation '" + String(p_anim) + "' doesn't exist.");
E->value.loop = p_loop;
set_animation_loop_mode(p_anim, p_loop ? LOOP_LINEAR : LOOP_NONE);
}
bool SpriteFrames::get_animation_loop(const StringName &p_anim) const {
return get_animation_loop_mode(p_anim) == LOOP_LINEAR;
}
#endif
void SpriteFrames::set_animation_loop_mode(const StringName &p_anim, LoopMode p_loop_mode) {
HashMap<StringName, Anim>::Iterator E = animations.find(p_anim);
ERR_FAIL_COND_MSG(!E, "Animation '" + String(p_anim) + "' doesn't exist.");
E->value.loop = p_loop_mode;
}
SpriteFrames::LoopMode SpriteFrames::get_animation_loop_mode(const StringName &p_anim) const {
HashMap<StringName, Anim>::ConstIterator E = animations.find(p_anim);
ERR_FAIL_COND_V_MSG(!E, false, "Animation '" + String(p_anim) + "' doesn't exist.");
ERR_FAIL_COND_V_MSG(!E, LoopMode::LOOP_NONE, "Animation '" + String(p_anim) + "' doesn't exist.");
return E->value.loop;
}
@@ -205,8 +215,10 @@ void SpriteFrames::_set_animations(const Array &p_animations) {
Anim anim;
anim.speed = d["speed"];
anim.loop = d["loop"];
Array frames = d["frames"];
Variant loop = d["loop"];
anim.loop = static_cast<LoopMode>((int)loop);
for (int j = 0; j < frames.size(); j++) {
#ifndef DISABLE_DEPRECATED
// For compatibility.
@@ -262,8 +274,13 @@ void SpriteFrames::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_animation_speed", "anim", "fps"), &SpriteFrames::set_animation_speed);
ClassDB::bind_method(D_METHOD("get_animation_speed", "anim"), &SpriteFrames::get_animation_speed);
#ifndef DISABLE_DEPRECATED
ClassDB::bind_method(D_METHOD("set_animation_loop", "anim", "loop"), &SpriteFrames::set_animation_loop);
ClassDB::bind_method(D_METHOD("get_animation_loop", "anim"), &SpriteFrames::get_animation_loop);
#endif
ClassDB::bind_method(D_METHOD("set_animation_loop_mode", "anim", "loop_mode"), &SpriteFrames::set_animation_loop_mode);
ClassDB::bind_method(D_METHOD("get_animation_loop_mode", "anim"), &SpriteFrames::get_animation_loop_mode);
ClassDB::bind_method(D_METHOD("add_frame", "anim", "texture", "duration", "at_position"), &SpriteFrames::add_frame, DEFVAL(1.0), DEFVAL(-1));
ClassDB::bind_method(D_METHOD("set_frame", "anim", "idx", "texture", "duration"), &SpriteFrames::set_frame, DEFVAL(1.0));
@@ -282,6 +299,9 @@ void SpriteFrames::_bind_methods() {
ClassDB::bind_method(D_METHOD("_get_animations"), &SpriteFrames::_get_animations);
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "animations", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_animations", "_get_animations");
BIND_ENUM_CONSTANT(LoopMode::LOOP_NONE);
BIND_ENUM_CONSTANT(LoopMode::LOOP_LINEAR);
BIND_ENUM_CONSTANT(LoopMode::LOOP_PINGPONG);
}
SpriteFrames::SpriteFrames() {
+16 -1
View File
@@ -37,6 +37,14 @@ static const float SPRITE_FRAME_MINIMUM_DURATION = 0.01;
class SpriteFrames : public Resource {
GDCLASS(SpriteFrames, Resource);
public:
enum LoopMode {
LOOP_NONE,
LOOP_LINEAR,
LOOP_PINGPONG,
};
private:
struct Frame {
Ref<Texture2D> texture;
float duration = 1.0;
@@ -44,7 +52,7 @@ class SpriteFrames : public Resource {
struct Anim {
double speed = 5.0;
bool loop = true;
LoopMode loop = LoopMode::LOOP_LINEAR;
Vector<Frame> frames;
};
@@ -69,8 +77,13 @@ public:
void set_animation_speed(const StringName &p_anim, double p_fps);
double get_animation_speed(const StringName &p_anim) const;
#ifndef DISABLE_DEPRECATED
void set_animation_loop(const StringName &p_anim, bool p_loop);
bool get_animation_loop(const StringName &p_anim) const;
#endif
void set_animation_loop_mode(const StringName &p_anim, LoopMode p_loop_mode);
LoopMode get_animation_loop_mode(const StringName &p_anim) const;
void add_frame(const StringName &p_anim, const Ref<Texture2D> &p_texture, float p_duration = 1.0, int p_at_pos = -1);
void set_frame(const StringName &p_anim, int p_idx, const Ref<Texture2D> &p_texture, float p_duration = 1.0);
@@ -109,3 +122,5 @@ public:
SpriteFrames();
};
VARIANT_ENUM_CAST(SpriteFrames::LoopMode);
+17 -11
View File
@@ -169,31 +169,37 @@ TEST_CASE("[SpriteFrames] Animation Speed getter and setter") {
"Sets animation to zero");
}
TEST_CASE("[SpriteFrames] Animation Loop getter and setter") {
TEST_CASE("[SpriteFrames] Animation Loop Mode getter and setter") {
SpriteFrames frames;
frames.add_animation(test_animation_name);
CHECK_MESSAGE(
frames.get_animation_loop(test_animation_name),
"Sets new animation to default loop value.");
frames.get_animation_loop_mode(test_animation_name) == SpriteFrames::LOOP_LINEAR,
"Sets new animation to default loop mode value (linear).");
frames.set_animation_loop(test_animation_name, true);
frames.set_animation_loop_mode(test_animation_name, SpriteFrames::LOOP_LINEAR);
CHECK_MESSAGE(
frames.get_animation_loop(test_animation_name),
"Sets animation loop to true");
frames.get_animation_loop_mode(test_animation_name) == SpriteFrames::LOOP_LINEAR,
"Sets animation loop mode to linear");
frames.set_animation_loop(test_animation_name, false);
frames.set_animation_loop_mode(test_animation_name, SpriteFrames::LOOP_PINGPONG);
CHECK_MESSAGE(
!frames.get_animation_loop(test_animation_name),
"Sets animation loop to false");
frames.get_animation_loop_mode(test_animation_name) == SpriteFrames::LOOP_PINGPONG,
"Sets animation loop mode to ping pong");
frames.set_animation_loop_mode(test_animation_name, SpriteFrames::LOOP_NONE);
CHECK_MESSAGE(
frames.get_animation_loop_mode(test_animation_name) == SpriteFrames::LOOP_NONE,
"Sets animation loop mode to none");
// These error handling cases should not crash.
ERR_PRINT_OFF;
frames.get_animation_loop("This does not exist");
frames.set_animation_loop("This does not exist", false);
frames.get_animation_loop_mode("This does not exist");
frames.set_animation_loop_mode("This does not exist", SpriteFrames::LOOP_NONE);
ERR_PRINT_ON;
}