diff --git a/doc/classes/SpriteFrames.xml b/doc/classes/SpriteFrames.xml index b32be8f88d..5f65d73b7e 100644 --- a/doc/classes/SpriteFrames.xml +++ b/doc/classes/SpriteFrames.xml @@ -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. - + - 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]. + + + + + + + Returns the loop mode for the [param anim] animation. @@ -124,12 +131,21 @@ Changes the [param anim] animation's name to [param newname]. - + - 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]. + + + + + + + + Sets the [param loop_mode] for the [param anim] animation. @@ -151,4 +167,16 @@ + + + The animation plays once and stops when it reaches the end, or the start if played in reverse. + + + The animation restarts from the beginning when it reaches the end, or from the end if played in reverse, repeating continuously. + + + 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). + + diff --git a/editor/scene/sprite_frames_editor_plugin.cpp b/editor/scene/sprite_frames_editor_plugin.cpp index 0ff33bdf58..b19c4679f3 100644 --- a/editor/scene/sprite_frames_editor_plugin.cpp +++ b/editor/scene/sprite_frames_editor_plugin.cpp @@ -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::from_sprite_frames(const Refname = 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) { diff --git a/editor/scene/sprite_frames_editor_plugin.h b/editor/scene/sprite_frames_editor_plugin.h index 63673b4000..e7229372e1 100644 --- a/editor/scene/sprite_frames_editor_plugin.h +++ b/editor/scene/sprite_frames_editor_plugin.h @@ -64,7 +64,7 @@ public: String name; Vector frames; float speed = 1.0f; - bool loop = false; + SpriteFrames::LoopMode loop = SpriteFrames::LOOP_LINEAR; static Ref from_sprite_frames(const Ref &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 *p_frames = nullptr); + void _update_anim_loop_button(); + StringName _find_next_animation(); String _generate_unique_animation_name(const String &p_base_name) const; diff --git a/scene/2d/animated_sprite_2d.cpp b/scene/2d/animated_sprite_2d.cpp index ebb091f58e..29dd0034ca 100644 --- a/scene/2d/animated_sprite_2d.cpp +++ b/scene/2d/animated_sprite_2d.cpp @@ -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--; } diff --git a/scene/3d/sprite_3d.cpp b/scene/3d/sprite_3d.cpp index ebe44c0469..c9216e6b61 100644 --- a/scene/3d/sprite_3d.cpp +++ b/scene/3d/sprite_3d.cpp @@ -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--; } diff --git a/scene/resources/sprite_frames.cpp b/scene/resources/sprite_frames.cpp index c327a431ed..059efdcd07 100644 --- a/scene/resources/sprite_frames.cpp +++ b/scene/resources/sprite_frames.cpp @@ -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::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::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::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((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() { diff --git a/scene/resources/sprite_frames.h b/scene/resources/sprite_frames.h index ee7675091e..da4a1e291c 100644 --- a/scene/resources/sprite_frames.h +++ b/scene/resources/sprite_frames.h @@ -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 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 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 &p_texture, float p_duration = 1.0, int p_at_pos = -1); void set_frame(const StringName &p_anim, int p_idx, const Ref &p_texture, float p_duration = 1.0); @@ -109,3 +122,5 @@ public: SpriteFrames(); }; + +VARIANT_ENUM_CAST(SpriteFrames::LoopMode); diff --git a/tests/scene/test_sprite_frames.cpp b/tests/scene/test_sprite_frames.cpp index ccdeee4dc3..a8d4a9c182 100644 --- a/tests/scene/test_sprite_frames.cpp +++ b/tests/scene/test_sprite_frames.cpp @@ -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; }