From 0d25d8e7fcb9db82e2017de3ca5281fcd28ddcf5 Mon Sep 17 00:00:00 2001 From: Danil Alexeev Date: Sat, 26 Nov 2022 17:00:38 +0300 Subject: [PATCH] `AnimatedSprite{2D,3D}` improvements * Add support for individual frame duration to `SpriteFrames`. * Various minor improvements. --- doc/classes/AnimatedSprite2D.xml | 11 +- doc/classes/AnimatedSprite3D.xml | 11 +- doc/classes/SpriteFrames.xml | 46 +-- editor/animation_track_editor_plugins.cpp | 4 +- .../plugins/sprite_frames_editor_plugin.cpp | 263 +++++++++++------- editor/plugins/sprite_frames_editor_plugin.h | 18 +- scene/2d/animated_sprite_2d.cpp | 73 +++-- scene/2d/animated_sprite_2d.h | 6 +- scene/3d/sprite_3d.cpp | 69 +++-- scene/3d/sprite_3d.h | 6 +- scene/resources/sprite_frames.cpp | 62 ++++- scene/resources/sprite_frames.h | 32 ++- tests/scene/test_sprite_frames.h | 8 +- 13 files changed, 379 insertions(+), 230 deletions(-) diff --git a/doc/classes/AnimatedSprite2D.xml b/doc/classes/AnimatedSprite2D.xml index afbe34816ac5..e20fb71c7ea9 100644 --- a/doc/classes/AnimatedSprite2D.xml +++ b/doc/classes/AnimatedSprite2D.xml @@ -6,7 +6,7 @@ [AnimatedSprite2D] is similar to the [Sprite2D] node, except it carries multiple textures as animation frames. Animations are created using a [SpriteFrames] resource, which allows you to import image files (or a folder containing said files) to provide the animation frames for the sprite. The [SpriteFrames] resource can be configured in the editor via the SpriteFrames bottom panel. After setting up [member frames], [method play] may be called. It's also possible to select an [member animation] and toggle [member playing], even within the editor. - To pause the current animation, call [method stop] or set [member playing] to [code]false[/code]. Alternatively, setting [member speed_scale] to [code]0[/code] also preserves the current frame's elapsed time. + To pause the current animation, set [member playing] to [code]false[/code]. Alternatively, setting [member speed_scale] to [code]0[/code] also preserves the current frame's elapsed time. [b]Note:[/b] You can associate a set of normal or specular maps by creating additional [SpriteFrames] resources with a [code]_normal[/code] or [code]_specular[/code] suffix. For example, having 3 [SpriteFrames] resources [code]run[/code], [code]run_normal[/code], and [code]run_specular[/code] will make it so the [code]run[/code] animation uses normal and specular maps. @@ -20,13 +20,14 @@ Plays the animation named [param anim]. If no [param anim] is provided, the current animation is played. If [param backwards] is [code]true[/code], the animation is played in reverse. + [b]Note:[/b] If [member speed_scale] is negative, the animation direction specified by [param backwards] will be inverted. Stops the current [member animation] at the current [member frame]. - [b]Note:[/b] This method resets the current frame's elapsed time. If this behavior is undesired, consider setting [member speed_scale] to [code]0[/code], instead. + [b]Note:[/b] This method resets the current frame's elapsed time and removes the [code]backwards[/code] flag from the current [member animation] (if it was previously set by [method play]). If this behavior is undesired, set [member playing] to [code]false[/code] instead. @@ -53,7 +54,9 @@ The texture's drawing offset. - If [code]true[/code], the [member animation] is currently playing. Setting this property to [code]false[/code] is the equivalent of calling [method stop]. + If [code]true[/code], the [member animation] is currently playing. Setting this property to [code]false[/code] pauses the current animation. Use [method stop] to stop the animation at the current frame instead. + [b]Note:[/b] Unlike [method stop], changing this property to [code]false[/code] preserves the current frame's elapsed time and the [code]backwards[/code] flag of the current [member animation] (if it was previously set by [method play]). + [b]Note:[/b] After a non-looping animation finishes, the property still remains [code]true[/code]. The animation speed is multiplied by this value. If set to a negative value, the animation is played in reverse. If set to [code]0[/code], the animation is paused, preserving the current frame's elapsed time. @@ -62,7 +65,7 @@ - Emitted when the animation is finished (when it plays the last frame). If the animation is looping, this signal is emitted every time the last frame is drawn. + Emitted when the animation reaches the end, or the start if it is played in reverse. If the animation is looping, this signal is emitted at the end of each loop. diff --git a/doc/classes/AnimatedSprite3D.xml b/doc/classes/AnimatedSprite3D.xml index 09baf882fbea..4837ae715f81 100644 --- a/doc/classes/AnimatedSprite3D.xml +++ b/doc/classes/AnimatedSprite3D.xml @@ -6,7 +6,7 @@ [AnimatedSprite3D] is similar to the [Sprite3D] node, except it carries multiple textures as animation [member frames]. Animations are created using a [SpriteFrames] resource, which allows you to import image files (or a folder containing said files) to provide the animation frames for the sprite. The [SpriteFrames] resource can be configured in the editor via the SpriteFrames bottom panel. After setting up [member frames], [method play] may be called. It's also possible to select an [member animation] and toggle [member playing], even within the editor. - To pause the current animation, call [method stop] or set [member playing] to [code]false[/code]. Alternatively, setting [member speed_scale] to [code]0[/code] also preserves the current frame's elapsed time. + To pause the current animation, set [member playing] to [code]false[/code]. Alternatively, setting [member speed_scale] to [code]0[/code] also preserves the current frame's elapsed time. $DOCS_URL/tutorials/2d/2d_sprite_animation.html @@ -18,13 +18,14 @@ Plays the animation named [param anim]. If no [param anim] is provided, the current animation is played. If [param backwards] is [code]true[/code], the animation is played in reverse. + [b]Note:[/b] If [member speed_scale] is negative, the animation direction specified by [param backwards] will be inverted. Stops the current [member animation] at the current [member frame]. - [b]Note:[/b] This method resets the current frame's elapsed time. If this behavior is undesired, consider setting [member speed_scale] to [code]0[/code], instead. + [b]Note:[/b] This method resets the current frame's elapsed time and removes the [code]backwards[/code] flag from the current [member animation] (if it was previously set by [method play]). If this behavior is undesired, set [member playing] to [code]false[/code] instead. @@ -39,7 +40,9 @@ The [SpriteFrames] resource containing the animation(s). - If [code]true[/code], the [member animation] is currently playing. Setting this property to [code]false[/code] is the equivalent of calling [method stop]. + If [code]true[/code], the [member animation] is currently playing. Setting this property to [code]false[/code] pauses the current animation. Use [method stop] to stop the animation at the current frame instead. + [b]Note:[/b] Unlike [method stop], changing this property to [code]false[/code] preserves the current frame's elapsed time and the [code]backwards[/code] flag of the current [member animation] (if it was previously set by [method play]). + [b]Note:[/b] After a non-looping animation finishes, the property still remains [code]true[/code]. The animation speed is multiplied by this value. If set to a negative value, the animation is played in reverse. If set to [code]0[/code], the animation is paused, preserving the current frame's elapsed time. @@ -48,7 +51,7 @@ - Emitted when the animation is finished (when it plays the last frame). If the animation is looping, this signal is emitted every time the last frame is drawn. + Emitted when the animation reaches the end, or the start if it is played in reverse. If the animation is looping, this signal is emitted at the end of each loop. diff --git a/doc/classes/SpriteFrames.xml b/doc/classes/SpriteFrames.xml index e9721495dd2a..87b823bd2a70 100644 --- a/doc/classes/SpriteFrames.xml +++ b/doc/classes/SpriteFrames.xml @@ -20,8 +20,9 @@ - - + + + Adds a frame to the given animation. @@ -56,22 +57,34 @@ - The animation's speed in frames per second. - - - - - - - - Returns the animation's selected frame. + Returns the speed in frames per second for the [param anim] animation. - Returns the number of frames in the animation. + Returns the number of frames for the [param anim] animation. + + + + + + + + Returns a relative duration of the frame [param idx] in the [param anim] animation (defaults to [code]1.0[/code]). For example, a frame with a duration of [code]2.0[/code] is displayed twice as long as a frame with a duration of [code]1.0[/code]. You can calculate the absolute duration (in seconds) of a frame using the following formula: + [codeblock] + absolute_duration = relative_duration / (animation_fps * abs(speed_scale)) + [/codeblock] + In this example, [code]speed_scale[/code] refers to either [member AnimatedSprite2D.speed_scale] or [member AnimatedSprite3D.speed_scale]. + + + + + + + + Returns the texture of the frame [param idx] in the [param anim] animation. @@ -115,18 +128,19 @@ - + - The animation's speed in frames per second. + Sets the speed for the [param anim] animation in frames per second. - + + - Sets the texture of the given frame. + Sets the texture and the duration of the frame [param idx] in the [param anim] animation. diff --git a/editor/animation_track_editor_plugins.cpp b/editor/animation_track_editor_plugins.cpp index 0ad62710eb72..3b2ab61b4343 100644 --- a/editor/animation_track_editor_plugins.cpp +++ b/editor/animation_track_editor_plugins.cpp @@ -426,7 +426,7 @@ Rect2 AnimationTrackEditSpriteFrame::get_key_rect(int p_index, float p_pixels_se animation_name = get_animation()->track_get_key_value(animation_track, animaiton_index); } - Ref texture = sf->get_frame(animation_name, frame); + Ref texture = sf->get_frame_texture(animation_name, frame); if (!texture.is_valid()) { return AnimationTrackEdit::get_key_rect(p_index, p_pixels_sec); } @@ -518,7 +518,7 @@ void AnimationTrackEditSpriteFrame::draw_key(int p_index, float p_pixels_sec, in animation_name = get_animation()->track_get_key_value(animation_track, animaiton_index); } - texture = sf->get_frame(animation_name, frame); + texture = sf->get_frame_texture(animation_name, frame); if (!texture.is_valid()) { AnimationTrackEdit::draw_key(p_index, p_pixels_sec, p_x, p_selected, p_clip_left, p_clip_right); return; diff --git a/editor/plugins/sprite_frames_editor_plugin.cpp b/editor/plugins/sprite_frames_editor_plugin.cpp index 64e899b121cb..82f5a9a5d18b 100644 --- a/editor/plugins/sprite_frames_editor_plugin.cpp +++ b/editor/plugins/sprite_frames_editor_plugin.cpp @@ -265,7 +265,7 @@ void SpriteFramesEditor::_sheet_add_frames() { at->set_atlas(split_sheet_preview->get_texture()); at->set_region(Rect2(offset + frame_coords * (frame_size + separation), frame_size)); - undo_redo->add_do_method(frames, "add_frame", edited_anim, at, -1); + undo_redo->add_do_method(frames, "add_frame", edited_anim, at, 1.0, -1); undo_redo->add_undo_method(frames, "remove_frame", edited_anim, fc); } @@ -375,9 +375,9 @@ void SpriteFramesEditor::_sheet_spin_changed(double p_value, int p_dominant_para void SpriteFramesEditor::_prepare_sprite_sheet(const String &p_file) { Ref texture = ResourceLoader::load(p_file); - if (!texture.is_valid()) { + if (texture.is_null()) { EditorNode::get_singleton()->show_warning(TTR("Unable to load images")); - ERR_FAIL_COND(!texture.is_valid()); + ERR_FAIL_COND(texture.is_null()); } frames_selected.clear(); last_frame_selected = -1; @@ -477,7 +477,7 @@ void SpriteFramesEditor::_file_load_request(const Vector &p_path, int p_ int count = 0; for (const Ref &E : resources) { - undo_redo->add_do_method(frames, "add_frame", edited_anim, E, p_at_pos == -1 ? -1 : p_at_pos + count); + undo_redo->add_do_method(frames, "add_frame", edited_anim, E, 1.0, p_at_pos == -1 ? -1 : p_at_pos + count); undo_redo->add_undo_method(frames, "remove_frame", edited_anim, p_at_pos == -1 ? fc : p_at_pos); count++; } @@ -521,8 +521,18 @@ void SpriteFramesEditor::_load_pressed() { void SpriteFramesEditor::_paste_pressed() { ERR_FAIL_COND(!frames->has_animation(edited_anim)); - Ref r = EditorSettings::get_singleton()->get_resource_clipboard(); - if (!r.is_valid()) { + Ref texture; + float duration = 1.0; + + Ref frame = EditorSettings::get_singleton()->get_resource_clipboard(); + if (frame.is_valid()) { + texture = frame->texture; + duration = frame->duration; + } else { + texture = EditorSettings::get_singleton()->get_resource_clipboard(); + } + + if (texture.is_null()) { dialog->set_text(TTR("Resource clipboard is empty or not a texture!")); dialog->set_title(TTR("Error!")); //dialog->get_cancel()->set_text("Close"); @@ -533,7 +543,7 @@ void SpriteFramesEditor::_paste_pressed() { Ref &undo_redo = EditorNode::get_undo_redo(); undo_redo->create_action(TTR("Paste Frame")); - undo_redo->add_do_method(frames, "add_frame", edited_anim, r); + undo_redo->add_do_method(frames, "add_frame", edited_anim, texture, duration); undo_redo->add_undo_method(frames, "remove_frame", edited_anim, frames->get_frame_count(edited_anim)); undo_redo->add_do_method(this, "_update_library"); undo_redo->add_undo_method(this, "_update_library"); @@ -543,15 +553,20 @@ void SpriteFramesEditor::_paste_pressed() { void SpriteFramesEditor::_copy_pressed() { ERR_FAIL_COND(!frames->has_animation(edited_anim)); - if (tree->get_current() < 0) { - return; - } - Ref r = frames->get_frame(edited_anim, tree->get_current()); - if (!r.is_valid()) { + if (frame_list->get_current() < 0) { return; } - EditorSettings::get_singleton()->set_resource_clipboard(r); + Ref texture = frames->get_frame_texture(edited_anim, frame_list->get_current()); + if (texture.is_null()) { + return; + } + + Ref frame = memnew(EditorSpriteFramesFrame); + frame->texture = texture; + frame->duration = frames->get_frame_duration(edited_anim, frame_list->get_current()); + + EditorSettings::get_singleton()->set_resource_clipboard(frame); } void SpriteFramesEditor::_empty_pressed() { @@ -559,19 +574,19 @@ void SpriteFramesEditor::_empty_pressed() { int from = -1; - if (tree->get_current() >= 0) { - from = tree->get_current(); + if (frame_list->get_current() >= 0) { + from = frame_list->get_current(); sel = from; } else { from = frames->get_frame_count(edited_anim); } - Ref r; + Ref texture; Ref &undo_redo = EditorNode::get_undo_redo(); undo_redo->create_action(TTR("Add Empty")); - undo_redo->add_do_method(frames, "add_frame", edited_anim, r, from); + undo_redo->add_do_method(frames, "add_frame", edited_anim, texture, 1.0, from); undo_redo->add_undo_method(frames, "remove_frame", edited_anim, from); undo_redo->add_do_method(this, "_update_library"); undo_redo->add_undo_method(this, "_update_library"); @@ -583,19 +598,19 @@ void SpriteFramesEditor::_empty2_pressed() { int from = -1; - if (tree->get_current() >= 0) { - from = tree->get_current(); + if (frame_list->get_current() >= 0) { + from = frame_list->get_current(); sel = from; } else { from = frames->get_frame_count(edited_anim); } - Ref r; + Ref texture; Ref &undo_redo = EditorNode::get_undo_redo(); undo_redo->create_action(TTR("Add Empty")); - undo_redo->add_do_method(frames, "add_frame", edited_anim, r, from + 1); + undo_redo->add_do_method(frames, "add_frame", edited_anim, texture, 1.0, from + 1); undo_redo->add_undo_method(frames, "remove_frame", edited_anim, from + 1); undo_redo->add_do_method(this, "_update_library"); undo_redo->add_undo_method(this, "_update_library"); @@ -605,11 +620,11 @@ void SpriteFramesEditor::_empty2_pressed() { void SpriteFramesEditor::_up_pressed() { ERR_FAIL_COND(!frames->has_animation(edited_anim)); - if (tree->get_current() < 0) { + if (frame_list->get_current() < 0) { return; } - int to_move = tree->get_current(); + int to_move = frame_list->get_current(); if (to_move < 1) { return; } @@ -618,11 +633,11 @@ void SpriteFramesEditor::_up_pressed() { sel -= 1; Ref &undo_redo = EditorNode::get_undo_redo(); - undo_redo->create_action(TTR("Delete Resource")); - undo_redo->add_do_method(frames, "set_frame", edited_anim, to_move, frames->get_frame(edited_anim, to_move - 1)); - undo_redo->add_do_method(frames, "set_frame", edited_anim, to_move - 1, frames->get_frame(edited_anim, to_move)); - undo_redo->add_undo_method(frames, "set_frame", edited_anim, to_move, frames->get_frame(edited_anim, to_move)); - undo_redo->add_undo_method(frames, "set_frame", edited_anim, to_move - 1, frames->get_frame(edited_anim, to_move - 1)); + undo_redo->create_action(TTR("Move Frame")); + undo_redo->add_do_method(frames, "set_frame", edited_anim, to_move, frames->get_frame_texture(edited_anim, to_move - 1), frames->get_frame_duration(edited_anim, to_move - 1)); + undo_redo->add_do_method(frames, "set_frame", edited_anim, to_move - 1, frames->get_frame_texture(edited_anim, to_move), frames->get_frame_duration(edited_anim, to_move)); + undo_redo->add_undo_method(frames, "set_frame", edited_anim, to_move, frames->get_frame_texture(edited_anim, to_move), frames->get_frame_duration(edited_anim, to_move)); + undo_redo->add_undo_method(frames, "set_frame", edited_anim, to_move - 1, frames->get_frame_texture(edited_anim, to_move - 1), frames->get_frame_duration(edited_anim, to_move - 1)); undo_redo->add_do_method(this, "_update_library"); undo_redo->add_undo_method(this, "_update_library"); undo_redo->commit_action(); @@ -631,11 +646,11 @@ void SpriteFramesEditor::_up_pressed() { void SpriteFramesEditor::_down_pressed() { ERR_FAIL_COND(!frames->has_animation(edited_anim)); - if (tree->get_current() < 0) { + if (frame_list->get_current() < 0) { return; } - int to_move = tree->get_current(); + int to_move = frame_list->get_current(); if (to_move < 0 || to_move >= frames->get_frame_count(edited_anim) - 1) { return; } @@ -644,11 +659,11 @@ void SpriteFramesEditor::_down_pressed() { sel += 1; Ref &undo_redo = EditorNode::get_undo_redo(); - undo_redo->create_action(TTR("Delete Resource")); - undo_redo->add_do_method(frames, "set_frame", edited_anim, to_move, frames->get_frame(edited_anim, to_move + 1)); - undo_redo->add_do_method(frames, "set_frame", edited_anim, to_move + 1, frames->get_frame(edited_anim, to_move)); - undo_redo->add_undo_method(frames, "set_frame", edited_anim, to_move, frames->get_frame(edited_anim, to_move)); - undo_redo->add_undo_method(frames, "set_frame", edited_anim, to_move + 1, frames->get_frame(edited_anim, to_move + 1)); + undo_redo->create_action(TTR("Move Frame")); + undo_redo->add_do_method(frames, "set_frame", edited_anim, to_move, frames->get_frame_texture(edited_anim, to_move + 1), frames->get_frame_duration(edited_anim, to_move + 1)); + undo_redo->add_do_method(frames, "set_frame", edited_anim, to_move + 1, frames->get_frame_texture(edited_anim, to_move), frames->get_frame_duration(edited_anim, to_move)); + undo_redo->add_undo_method(frames, "set_frame", edited_anim, to_move, frames->get_frame_texture(edited_anim, to_move), frames->get_frame_duration(edited_anim, to_move)); + undo_redo->add_undo_method(frames, "set_frame", edited_anim, to_move + 1, frames->get_frame_texture(edited_anim, to_move + 1), frames->get_frame_duration(edited_anim, to_move + 1)); undo_redo->add_do_method(this, "_update_library"); undo_redo->add_undo_method(this, "_update_library"); undo_redo->commit_action(); @@ -657,11 +672,11 @@ void SpriteFramesEditor::_down_pressed() { void SpriteFramesEditor::_delete_pressed() { ERR_FAIL_COND(!frames->has_animation(edited_anim)); - if (tree->get_current() < 0) { + if (frame_list->get_current() < 0) { return; } - int to_delete = tree->get_current(); + int to_delete = frame_list->get_current(); if (to_delete < 0 || to_delete >= frames->get_frame_count(edited_anim)) { return; } @@ -669,7 +684,7 @@ void SpriteFramesEditor::_delete_pressed() { Ref &undo_redo = EditorNode::get_undo_redo(); undo_redo->create_action(TTR("Delete Resource")); undo_redo->add_do_method(frames, "remove_frame", edited_anim, to_delete); - undo_redo->add_undo_method(frames, "add_frame", edited_anim, frames->get_frame(edited_anim, to_delete), to_delete); + undo_redo->add_undo_method(frames, "add_frame", edited_anim, frames->get_frame_texture(edited_anim, to_delete), frames->get_frame_duration(edited_anim, to_delete), to_delete); undo_redo->add_do_method(this, "_update_library"); undo_redo->add_undo_method(this, "_update_library"); undo_redo->commit_action(); @@ -683,7 +698,7 @@ void SpriteFramesEditor::_animation_select() { if (frames->has_animation(edited_anim)) { double value = anim_speed->get_line_edit()->get_text().to_float(); if (!Math::is_equal_approx(value, (double)frames->get_animation_speed(edited_anim))) { - _animation_fps_changed(value); + _animation_speed_changed(value); } } @@ -824,8 +839,9 @@ void SpriteFramesEditor::_animation_remove_confirmed() { undo_redo->add_undo_method(frames, "set_animation_loop", edited_anim, frames->get_animation_loop(edited_anim)); int fc = frames->get_frame_count(edited_anim); for (int i = 0; i < fc; i++) { - Ref frame = frames->get_frame(edited_anim, i); - undo_redo->add_undo_method(frames, "add_frame", edited_anim, frame); + Ref texture = frames->get_frame_texture(edited_anim, i); + float duration = frames->get_frame_duration(edited_anim, i); + undo_redo->add_undo_method(frames, "add_frame", edited_anim, texture, duration); } undo_redo->add_do_method(this, "_update_library"); undo_redo->add_undo_method(this, "_update_library"); @@ -853,7 +869,7 @@ void SpriteFramesEditor::_animation_loop_changed() { undo_redo->commit_action(); } -void SpriteFramesEditor::_animation_fps_changed(double p_value) { +void SpriteFramesEditor::_animation_speed_changed(double p_value) { if (updating) { return; } @@ -864,11 +880,10 @@ void SpriteFramesEditor::_animation_fps_changed(double p_value) { undo_redo->add_undo_method(frames, "set_animation_speed", edited_anim, frames->get_animation_speed(edited_anim)); undo_redo->add_do_method(this, "_update_library", true); undo_redo->add_undo_method(this, "_update_library", true); - undo_redo->commit_action(); } -void SpriteFramesEditor::_tree_input(const Ref &p_event) { +void SpriteFramesEditor::_frame_list_gui_input(const Ref &p_event) { const Ref mb = p_event; if (mb.is_valid()) { @@ -884,6 +899,42 @@ void SpriteFramesEditor::_tree_input(const Ref &p_event) { } } +void SpriteFramesEditor::_frame_list_item_selected(int p_index) { + if (updating) { + return; + } + + sel = p_index; + + updating = true; + frame_duration->set_value(frames->get_frame_duration(edited_anim, p_index)); + updating = false; +} + +void SpriteFramesEditor::_frame_duration_changed(double p_value) { + if (updating) { + return; + } + + int index = frame_list->get_current(); + if (index < 0) { + return; + } + + sel = index; + + Ref texture = frames->get_frame_texture(edited_anim, index); + float old_duration = frames->get_frame_duration(edited_anim, index); + + Ref &undo_redo = EditorNode::get_undo_redo(); + undo_redo->create_action(TTR("Set Frame Duration")); + undo_redo->add_do_method(frames, "set_frame", edited_anim, index, texture, p_value); + undo_redo->add_undo_method(frames, "set_frame", edited_anim, index, texture, old_duration); + undo_redo->add_do_method(this, "_update_library"); + undo_redo->add_undo_method(this, "_update_library"); + undo_redo->commit_action(); +} + void SpriteFramesEditor::_zoom_in() { // Do not zoom in or out with no visible frames if (frames->get_frame_count(edited_anim) <= 0) { @@ -892,8 +943,8 @@ void SpriteFramesEditor::_zoom_in() { if (thumbnail_zoom < max_thumbnail_zoom) { thumbnail_zoom *= scale_ratio; int thumbnail_size = (int)(thumbnail_default_size * thumbnail_zoom); - tree->set_fixed_column_width(thumbnail_size * 3 / 2); - tree->set_fixed_icon_size(Size2(thumbnail_size, thumbnail_size)); + frame_list->set_fixed_column_width(thumbnail_size * 3 / 2); + frame_list->set_fixed_icon_size(Size2(thumbnail_size, thumbnail_size)); } } @@ -905,20 +956,22 @@ void SpriteFramesEditor::_zoom_out() { if (thumbnail_zoom > min_thumbnail_zoom) { thumbnail_zoom /= scale_ratio; int thumbnail_size = (int)(thumbnail_default_size * thumbnail_zoom); - tree->set_fixed_column_width(thumbnail_size * 3 / 2); - tree->set_fixed_icon_size(Size2(thumbnail_size, thumbnail_size)); + frame_list->set_fixed_column_width(thumbnail_size * 3 / 2); + frame_list->set_fixed_icon_size(Size2(thumbnail_size, thumbnail_size)); } } void SpriteFramesEditor::_zoom_reset() { thumbnail_zoom = MAX(1.0f, EDSCALE); - tree->set_fixed_column_width(thumbnail_default_size * 3 / 2); - tree->set_fixed_icon_size(Size2(thumbnail_default_size, thumbnail_default_size)); + frame_list->set_fixed_column_width(thumbnail_default_size * 3 / 2); + frame_list->set_fixed_icon_size(Size2(thumbnail_default_size, thumbnail_default_size)); } void SpriteFramesEditor::_update_library(bool p_skip_selector) { updating = true; + frame_duration->set_value(1.0); // Default. + if (!p_skip_selector) { animations->clear(); @@ -951,7 +1004,7 @@ void SpriteFramesEditor::_update_library(bool p_skip_selector) { } } - tree->clear(); + frame_list->clear(); if (!frames->has_animation(edited_anim)) { updating = false; @@ -966,33 +1019,39 @@ void SpriteFramesEditor::_update_library(bool p_skip_selector) { for (int i = 0; i < frames->get_frame_count(edited_anim); i++) { String name; - Ref frame = frames->get_frame(edited_anim, i); - - if (frame.is_null()) { - name = itos(i) + ": " + TTR("(empty)"); - } else { - name = itos(i) + ": " + frame->get_name(); + Ref texture = frames->get_frame_texture(edited_anim, i); + float duration = frames->get_frame_duration(edited_anim, i); + String duration_string; + if (duration != 1.0f) { + duration_string = String::utf8(" [ ×") + String::num_real(frames->get_frame_duration(edited_anim, i)) + " ]"; } - tree->add_item(name, frame); - if (frame.is_valid()) { - String tooltip = frame->get_path(); + if (texture.is_null()) { + name = itos(i) + ": " + TTR("(empty)") + duration_string; + } else { + name = itos(i) + ": " + texture->get_name() + duration_string; + } + + frame_list->add_item(name, texture); + if (texture.is_valid()) { + String tooltip = texture->get_path(); // Frame is often saved as an AtlasTexture subresource within a scene/resource file, // thus its path might be not what the user is looking for. So we're also showing // subsequent source texture paths. String prefix = String::utf8("┖╴"); - Ref at = frame; + Ref at = texture; while (at.is_valid() && at->get_atlas().is_valid()) { tooltip += "\n" + prefix + at->get_atlas()->get_path(); prefix = " " + prefix; at = at->get_atlas(); } - tree->set_item_tooltip(-1, tooltip); + frame_list->set_item_tooltip(-1, tooltip); } if (sel == i) { - tree->select(tree->get_item_count() - 1); + frame_list->select(frame_list->get_item_count() - 1); + frame_duration->set_value(frames->get_frame_duration(edited_anim, i)); } } @@ -1059,13 +1118,13 @@ Variant SpriteFramesEditor::get_drag_data_fw(const Point2 &p_point, Control *p_f return false; } - int idx = tree->get_item_at_position(p_point, true); + int idx = frame_list->get_item_at_position(p_point, true); if (idx < 0 || idx >= frames->get_frame_count(edited_anim)) { return Variant(); } - Ref frame = frames->get_frame(edited_anim, idx); + Ref frame = frames->get_frame_texture(edited_anim, idx); if (frame.is_null()) { return Variant(); @@ -1088,7 +1147,7 @@ bool SpriteFramesEditor::can_drop_data_fw(const Point2 &p_point, const Variant & } // reordering frames - if (d.has("from") && (Object *)(d["from"]) == tree) { + if (d.has("from") && (Object *)(d["from"]) == frame_list) { return true; } @@ -1134,7 +1193,7 @@ void SpriteFramesEditor::drop_data_fw(const Point2 &p_point, const Variant &p_da return; } - int at_pos = tree->get_item_at_position(p_point, true); + int at_pos = frame_list->get_item_at_position(p_point, true); if (String(d["type"]) == "resource" && d.has("resource")) { Ref r = d["resource"]; @@ -1143,28 +1202,30 @@ void SpriteFramesEditor::drop_data_fw(const Point2 &p_point, const Variant &p_da if (texture.is_valid()) { bool reorder = false; - if (d.has("from") && (Object *)(d["from"]) == tree) { + if (d.has("from") && (Object *)(d["from"]) == frame_list) { reorder = true; } Ref &undo_redo = EditorNode::get_undo_redo(); if (reorder) { //drop is from reordering frames int from_frame = -1; + float duration = 1.0; if (d.has("frame")) { from_frame = d["frame"]; + duration = frames->get_frame_duration(edited_anim, from_frame); } undo_redo->create_action(TTR("Move Frame")); undo_redo->add_do_method(frames, "remove_frame", edited_anim, from_frame == -1 ? frames->get_frame_count(edited_anim) : from_frame); - undo_redo->add_do_method(frames, "add_frame", edited_anim, texture, at_pos == -1 ? -1 : at_pos); + undo_redo->add_do_method(frames, "add_frame", edited_anim, texture, duration, at_pos == -1 ? -1 : at_pos); undo_redo->add_undo_method(frames, "remove_frame", edited_anim, at_pos == -1 ? frames->get_frame_count(edited_anim) - 1 : at_pos); - undo_redo->add_undo_method(frames, "add_frame", edited_anim, texture, from_frame); + undo_redo->add_undo_method(frames, "add_frame", edited_anim, texture, duration, from_frame); undo_redo->add_do_method(this, "_update_library"); undo_redo->add_undo_method(this, "_update_library"); undo_redo->commit_action(); } else { undo_redo->create_action(TTR("Add Frame")); - undo_redo->add_do_method(frames, "add_frame", edited_anim, texture, at_pos == -1 ? -1 : at_pos); + undo_redo->add_do_method(frames, "add_frame", edited_anim, texture, 1.0, at_pos == -1 ? -1 : at_pos); undo_redo->add_undo_method(frames, "remove_frame", edited_anim, at_pos == -1 ? frames->get_frame_count(edited_anim) : at_pos); undo_redo->add_do_method(this, "_update_library"); undo_redo->add_undo_method(this, "_update_library"); @@ -1240,11 +1301,11 @@ SpriteFramesEditor::SpriteFramesEditor() { anim_speed = memnew(SpinBox); anim_speed->set_suffix(TTR("FPS")); anim_speed->set_min(0); - anim_speed->set_max(100); + anim_speed->set_max(120); anim_speed->set_step(0.01); anim_speed->set_h_size_flags(SIZE_EXPAND_FILL); hbc_anim_speed->add_child(anim_speed); - anim_speed->connect("value_changed", callable_mp(this, &SpriteFramesEditor::_animation_fps_changed)); + anim_speed->connect("value_changed", callable_mp(this, &SpriteFramesEditor::_animation_speed_changed)); anim_loop = memnew(CheckButton); anim_loop->set_text(TTR("Loop")); @@ -1303,6 +1364,20 @@ SpriteFramesEditor::SpriteFramesEditor() { delete_frame->set_flat(true); hbc->add_child(delete_frame); + hbc->add_child(memnew(VSeparator)); + + Label *label = memnew(Label); + label->set_text(TTR("Frame Duration:")); + hbc->add_child(label); + + frame_duration = memnew(SpinBox); + frame_duration->set_prefix(String::utf8("×")); + frame_duration->set_min(0); + frame_duration->set_max(10); + frame_duration->set_step(0.01); + frame_duration->set_allow_greater(true); + hbc->add_child(frame_duration); + hbc->add_spacer(); zoom_out = memnew(Button); @@ -1326,17 +1401,18 @@ SpriteFramesEditor::SpriteFramesEditor() { file = memnew(EditorFileDialog); add_child(file); - tree = memnew(ItemList); - tree->set_v_size_flags(SIZE_EXPAND_FILL); - tree->set_icon_mode(ItemList::ICON_MODE_TOP); + frame_list = memnew(ItemList); + frame_list->set_v_size_flags(SIZE_EXPAND_FILL); + frame_list->set_icon_mode(ItemList::ICON_MODE_TOP); - tree->set_max_columns(0); - tree->set_icon_mode(ItemList::ICON_MODE_TOP); - tree->set_max_text_lines(2); - tree->set_drag_forwarding(this); - tree->connect("gui_input", callable_mp(this, &SpriteFramesEditor::_tree_input)); + frame_list->set_max_columns(0); + frame_list->set_icon_mode(ItemList::ICON_MODE_TOP); + frame_list->set_max_text_lines(2); + frame_list->set_drag_forwarding(this); + frame_list->connect("gui_input", callable_mp(this, &SpriteFramesEditor::_frame_list_gui_input)); + frame_list->connect("item_selected", callable_mp(this, &SpriteFramesEditor::_frame_list_item_selected)); - sub_vb->add_child(tree); + sub_vb->add_child(frame_list); dialog = memnew(AcceptDialog); add_child(dialog); @@ -1351,33 +1427,34 @@ SpriteFramesEditor::SpriteFramesEditor() { move_up->connect("pressed", callable_mp(this, &SpriteFramesEditor::_up_pressed)); move_down->connect("pressed", callable_mp(this, &SpriteFramesEditor::_down_pressed)); - load->set_shortcut_context(tree); + load->set_shortcut_context(frame_list); load->set_shortcut(ED_SHORTCUT("sprite_frames/load_from_file", TTR("Add frame from file"), KeyModifierMask::CMD_OR_CTRL | Key::O)); - load_sheet->set_shortcut_context(tree); + load_sheet->set_shortcut_context(frame_list); load_sheet->set_shortcut(ED_SHORTCUT("sprite_frames/load_from_sheet", TTR("Add frames from sprite sheet"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::O)); - delete_frame->set_shortcut_context(tree); + delete_frame->set_shortcut_context(frame_list); delete_frame->set_shortcut(ED_SHORTCUT("sprite_frames/delete", TTR("Delete Frame"), Key::KEY_DELETE)); - copy->set_shortcut_context(tree); + copy->set_shortcut_context(frame_list); copy->set_shortcut(ED_SHORTCUT("sprite_frames/copy", TTR("Copy Frame"), KeyModifierMask::CMD_OR_CTRL | Key::C)); - paste->set_shortcut_context(tree); + paste->set_shortcut_context(frame_list); paste->set_shortcut(ED_SHORTCUT("sprite_frames/paste", TTR("Paste Frame"), KeyModifierMask::CMD_OR_CTRL | Key::V)); - empty_before->set_shortcut_context(tree); + empty_before->set_shortcut_context(frame_list); empty_before->set_shortcut(ED_SHORTCUT("sprite_frames/empty_before", TTR("Insert Empty (Before Selected)"), KeyModifierMask::ALT | Key::LEFT)); - empty_after->set_shortcut_context(tree); + empty_after->set_shortcut_context(frame_list); empty_after->set_shortcut(ED_SHORTCUT("sprite_frames/empty_after", TTR("Insert Empty (After Selected)"), KeyModifierMask::ALT | Key::RIGHT)); - move_up->set_shortcut_context(tree); + move_up->set_shortcut_context(frame_list); move_up->set_shortcut(ED_SHORTCUT("sprite_frames/move_left", TTR("Move Frame Left"), KeyModifierMask::CMD_OR_CTRL | Key::LEFT)); - move_down->set_shortcut_context(tree); + move_down->set_shortcut_context(frame_list); move_down->set_shortcut(ED_SHORTCUT("sprite_frames/move_right", TTR("Move Frame Right"), KeyModifierMask::CMD_OR_CTRL | Key::RIGHT)); - zoom_out->set_shortcut_context(tree); + zoom_out->set_shortcut_context(frame_list); zoom_out->set_shortcut(ED_SHORTCUT_ARRAY("sprite_frames/zoom_out", TTR("Zoom Out"), { int32_t(KeyModifierMask::CMD_OR_CTRL | Key::MINUS), int32_t(KeyModifierMask::CMD_OR_CTRL | Key::KP_SUBTRACT) })); - zoom_in->set_shortcut_context(tree); + zoom_in->set_shortcut_context(frame_list); zoom_in->set_shortcut(ED_SHORTCUT_ARRAY("sprite_frames/zoom_in", TTR("Zoom In"), { int32_t(KeyModifierMask::CMD_OR_CTRL | Key::EQUAL), int32_t(KeyModifierMask::CMD_OR_CTRL | Key::KP_ADD) })); file->connect("files_selected", callable_mp(this, &SpriteFramesEditor::_file_load_request).bind(-1)); + frame_duration->connect("value_changed", callable_mp(this, &SpriteFramesEditor::_frame_duration_changed)); loading_scene = false; sel = -1; diff --git a/editor/plugins/sprite_frames_editor_plugin.h b/editor/plugins/sprite_frames_editor_plugin.h index 64245ee1e0b1..214ae3a1580f 100644 --- a/editor/plugins/sprite_frames_editor_plugin.h +++ b/editor/plugins/sprite_frames_editor_plugin.h @@ -46,6 +46,14 @@ class EditorFileDialog; +class EditorSpriteFramesFrame : public Resource { + GDCLASS(EditorSpriteFramesFrame, Resource); + +public: + Ref texture; + float duration; +}; + class SpriteFramesEditor : public HSplitContainer { GDCLASS(SpriteFramesEditor, HSplitContainer); @@ -70,7 +78,8 @@ class SpriteFramesEditor : public HSplitContainer { Button *zoom_out = nullptr; Button *zoom_reset = nullptr; Button *zoom_in = nullptr; - ItemList *tree = nullptr; + SpinBox *frame_duration = nullptr; + ItemList *frame_list = nullptr; bool loading_scene; int sel; @@ -134,6 +143,7 @@ class SpriteFramesEditor : public HSplitContainer { void _delete_pressed(); void _up_pressed(); void _down_pressed(); + void _frame_duration_changed(double p_value); void _update_library(bool p_skip_selector = false); void _animation_select(); @@ -143,9 +153,11 @@ class SpriteFramesEditor : public HSplitContainer { void _animation_remove_confirmed(); void _animation_search_text_changed(const String &p_text); void _animation_loop_changed(); - void _animation_fps_changed(double p_value); + void _animation_speed_changed(double p_value); + + void _frame_list_gui_input(const Ref &p_event); + void _frame_list_item_selected(int p_index); - void _tree_input(const Ref &p_event); void _zoom_in(); void _zoom_out(); void _zoom_reset(); diff --git a/scene/2d/animated_sprite_2d.cpp b/scene/2d/animated_sprite_2d.cpp index 7ee9861d3fb1..5d8daa0c5cb9 100644 --- a/scene/2d/animated_sprite_2d.cpp +++ b/scene/2d/animated_sprite_2d.cpp @@ -72,7 +72,7 @@ bool AnimatedSprite2D::_edit_use_rect() const { Ref t; if (animation) { - t = frames->get_frame(animation, frame); + t = frames->get_frame_texture(animation, frame); } return t.is_valid(); } @@ -92,7 +92,7 @@ Rect2 AnimatedSprite2D::_get_rect() const { Ref t; if (animation) { - t = frames->get_frame(animation, frame); + t = frames->get_frame_texture(animation, frame); } if (t.is_null()) { return Rect2(); @@ -172,17 +172,21 @@ void AnimatedSprite2D::_notification(int p_what) { return; } - double speed = frames->get_animation_speed(animation) * Math::abs(speed_scale); - if (speed == 0) { - return; // Do nothing. - } - int last_frame = frames->get_frame_count(animation) - 1; - double remaining = get_process_delta_time(); + int i = 0; while (remaining) { - if (timeout <= 0) { - timeout = _get_frame_duration(); + // Animation speed may be changed by animation_finished or frame_changed signals. + double speed = frames->get_animation_speed(animation) * Math::abs(speed_scale); + if (speed == 0) { + return; // Do nothing. + } + + // Frame count may be changed by animation_finished or frame_changed signals. + int fc = frames->get_frame_count(animation); + + if (timeout <= 0) { + int last_frame = fc - 1; if (!playing_backwards) { // Forward. if (frame >= last_frame) { @@ -217,14 +221,21 @@ void AnimatedSprite2D::_notification(int p_what) { } } + timeout = _get_frame_duration(); + queue_redraw(); emit_signal(SceneStringNames::get_singleton()->frame_changed); } - double to_process = MIN(timeout, remaining); + double to_process = MIN(timeout / speed, remaining); + timeout -= to_process * speed; remaining -= to_process; - timeout -= to_process; + + i++; + if (i > fc) { + return; // Prevents freezing if to_process is each time much less than remaining. + } } } break; @@ -233,7 +244,7 @@ void AnimatedSprite2D::_notification(int p_what) { return; } - Ref texture = frames->get_frame(animation, frame); + Ref texture = frames->get_frame_texture(animation, frame); if (texture.is_null()) { return; } @@ -312,7 +323,6 @@ void AnimatedSprite2D::set_frame(int p_frame) { frame = p_frame; _reset_timeout(); queue_redraw(); - emit_signal(SceneStringNames::get_singleton()->frame_changed); } @@ -320,22 +330,12 @@ int AnimatedSprite2D::get_frame() const { return frame; } -void AnimatedSprite2D::set_speed_scale(double p_speed_scale) { - if (speed_scale == p_speed_scale) { - return; - } - - double elapsed = _get_frame_duration() - timeout; - +void AnimatedSprite2D::set_speed_scale(float p_speed_scale) { speed_scale = p_speed_scale; playing_backwards = signbit(speed_scale) != backwards; - - // We adapt the timeout so that the animation speed adapts as soon as the speed scale is changed. - _reset_timeout(); - timeout -= elapsed; } -double AnimatedSprite2D::get_speed_scale() const { +float AnimatedSprite2D::get_speed_scale() const { return speed_scale; } @@ -379,8 +379,8 @@ bool AnimatedSprite2D::is_flipped_v() const { void AnimatedSprite2D::_res_changed() { set_frame(frame); - queue_redraw(); + notify_property_list_changed(); } void AnimatedSprite2D::set_playing(bool p_playing) { @@ -388,7 +388,7 @@ void AnimatedSprite2D::set_playing(bool p_playing) { return; } playing = p_playing; - _reset_timeout(); + playing_backwards = signbit(speed_scale) != backwards; set_process_internal(playing); notify_property_list_changed(); } @@ -414,23 +414,18 @@ void AnimatedSprite2D::play(const StringName &p_animation, bool p_backwards) { void AnimatedSprite2D::stop() { set_playing(false); + backwards = false; + _reset_timeout(); } double AnimatedSprite2D::_get_frame_duration() { if (frames.is_valid() && frames->has_animation(animation)) { - double speed = frames->get_animation_speed(animation) * Math::abs(speed_scale); - if (speed > 0) { - return 1.0 / speed; - } + return frames->get_frame_duration(animation, frame); } return 0.0; } void AnimatedSprite2D::_reset_timeout() { - if (!playing) { - return; - } - timeout = _get_frame_duration(); is_over = false; } @@ -444,8 +439,8 @@ void AnimatedSprite2D::set_animation(const StringName &p_animation) { } animation = p_animation; - _reset_timeout(); set_frame(0); + _reset_timeout(); notify_property_list_changed(); queue_redraw(); } @@ -455,12 +450,10 @@ StringName AnimatedSprite2D::get_animation() const { } PackedStringArray AnimatedSprite2D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); - + PackedStringArray warnings = Node2D::get_configuration_warnings(); if (frames.is_null()) { warnings.push_back(RTR("A SpriteFrames resource must be created or set in the \"Frames\" property in order for AnimatedSprite2D to display frames.")); } - return warnings; } diff --git a/scene/2d/animated_sprite_2d.h b/scene/2d/animated_sprite_2d.h index 11c4adb816af..3195bd71618e 100644 --- a/scene/2d/animated_sprite_2d.h +++ b/scene/2d/animated_sprite_2d.h @@ -43,7 +43,7 @@ class AnimatedSprite2D : public Node2D { bool backwards = false; StringName animation = "default"; int frame = 0; - float speed_scale = 1.0f; + float speed_scale = 1.0; bool centered = true; Point2 offset; @@ -94,8 +94,8 @@ public: void set_frame(int p_frame); int get_frame() const; - void set_speed_scale(double p_speed_scale); - double get_speed_scale() const; + void set_speed_scale(float p_speed_scale); + float get_speed_scale() const; void set_centered(bool p_center); bool is_centered() const; diff --git a/scene/3d/sprite_3d.cpp b/scene/3d/sprite_3d.cpp index 3629464cd15c..e39a165b0d5f 100644 --- a/scene/3d/sprite_3d.cpp +++ b/scene/3d/sprite_3d.cpp @@ -837,7 +837,7 @@ void AnimatedSprite3D::_draw() { return; } - Ref texture = frames->get_frame(animation, frame); + Ref texture = frames->get_frame_texture(animation, frame); if (texture.is_null()) { set_base(RID()); return; @@ -921,17 +921,21 @@ void AnimatedSprite3D::_notification(int p_what) { return; } - double speed = frames->get_animation_speed(animation) * Math::abs(speed_scale); - if (speed == 0) { - return; // Do nothing. - } - int last_frame = frames->get_frame_count(animation) - 1; - double remaining = get_process_delta_time(); + int i = 0; while (remaining) { - if (timeout <= 0) { - timeout = _get_frame_duration(); + // Animation speed may be changed by animation_finished or frame_changed signals. + double speed = frames->get_animation_speed(animation) * Math::abs(speed_scale); + if (speed == 0) { + return; // Do nothing. + } + + // Frame count may be changed by animation_finished or frame_changed signals. + int fc = frames->get_frame_count(animation); + + if (timeout <= 0) { + int last_frame = fc - 1; if (!playing_backwards) { // Forward. if (frame >= last_frame) { @@ -966,14 +970,21 @@ void AnimatedSprite3D::_notification(int p_what) { } } + timeout = _get_frame_duration(); + _queue_redraw(); emit_signal(SceneStringNames::get_singleton()->frame_changed); } - double to_process = MIN(timeout, remaining); + double to_process = MIN(timeout / speed, remaining); + timeout -= to_process * speed; remaining -= to_process; - timeout -= to_process; + + i++; + if (i > fc) { + return; // Prevents freezing if to_process is each time much less than remaining. + } } } break; } @@ -1028,7 +1039,6 @@ void AnimatedSprite3D::set_frame(int p_frame) { frame = p_frame; _reset_timeout(); _queue_redraw(); - emit_signal(SceneStringNames::get_singleton()->frame_changed); } @@ -1036,22 +1046,12 @@ int AnimatedSprite3D::get_frame() const { return frame; } -void AnimatedSprite3D::set_speed_scale(double p_speed_scale) { - if (speed_scale == p_speed_scale) { - return; - } - - double elapsed = _get_frame_duration() - timeout; - +void AnimatedSprite3D::set_speed_scale(float p_speed_scale) { speed_scale = p_speed_scale; playing_backwards = signbit(speed_scale) != backwards; - - // We adapt the timeout so that the animation speed adapts as soon as the speed scale is changed. - _reset_timeout(); - timeout -= elapsed; } -double AnimatedSprite3D::get_speed_scale() const { +float AnimatedSprite3D::get_speed_scale() const { return speed_scale; } @@ -1065,7 +1065,7 @@ Rect2 AnimatedSprite3D::get_item_rect() const { Ref t; if (animation) { - t = frames->get_frame(animation, frame); + t = frames->get_frame_texture(animation, frame); } if (t.is_null()) { return Rect2(0, 0, 1, 1); @@ -1086,8 +1086,8 @@ Rect2 AnimatedSprite3D::get_item_rect() const { void AnimatedSprite3D::_res_changed() { set_frame(frame); - _queue_redraw(); + notify_property_list_changed(); } void AnimatedSprite3D::set_playing(bool p_playing) { @@ -1095,7 +1095,7 @@ void AnimatedSprite3D::set_playing(bool p_playing) { return; } playing = p_playing; - _reset_timeout(); + playing_backwards = signbit(speed_scale) != backwards; set_process_internal(playing); notify_property_list_changed(); } @@ -1121,23 +1121,18 @@ void AnimatedSprite3D::play(const StringName &p_animation, bool p_backwards) { void AnimatedSprite3D::stop() { set_playing(false); + backwards = false; + _reset_timeout(); } double AnimatedSprite3D::_get_frame_duration() { if (frames.is_valid() && frames->has_animation(animation)) { - double speed = frames->get_animation_speed(animation) * Math::abs(speed_scale); - if (speed > 0) { - return 1.0 / speed; - } + return frames->get_frame_duration(animation, frame); } return 0.0; } void AnimatedSprite3D::_reset_timeout() { - if (!playing) { - return; - } - timeout = _get_frame_duration(); is_over = false; } @@ -1145,13 +1140,14 @@ void AnimatedSprite3D::_reset_timeout() { void AnimatedSprite3D::set_animation(const StringName &p_animation) { ERR_FAIL_COND_MSG(frames == nullptr, vformat("There is no animation with name '%s'.", p_animation)); ERR_FAIL_COND_MSG(!frames->get_animation_names().has(p_animation), vformat("There is no animation with name '%s'.", p_animation)); + if (animation == p_animation) { return; } animation = p_animation; - _reset_timeout(); set_frame(0); + _reset_timeout(); notify_property_list_changed(); _queue_redraw(); } @@ -1165,7 +1161,6 @@ PackedStringArray AnimatedSprite3D::get_configuration_warnings() const { if (frames.is_null()) { warnings.push_back(RTR("A SpriteFrames resource must be created or set in the \"Frames\" property in order for AnimatedSprite3D to display frames.")); } - return warnings; } diff --git a/scene/3d/sprite_3d.h b/scene/3d/sprite_3d.h index edc48c7b7101..4237f820193c 100644 --- a/scene/3d/sprite_3d.h +++ b/scene/3d/sprite_3d.h @@ -214,7 +214,7 @@ class AnimatedSprite3D : public SpriteBase3D { bool backwards = false; StringName animation = "default"; int frame = 0; - float speed_scale = 1.0f; + float speed_scale = 1.0; bool centered = false; @@ -248,8 +248,8 @@ public: void set_frame(int p_frame); int get_frame() const; - void set_speed_scale(double p_speed_scale); - double get_speed_scale() const; + void set_speed_scale(float p_speed_scale); + float get_speed_scale() const; virtual Rect2 get_item_rect() const override; diff --git a/scene/resources/sprite_frames.cpp b/scene/resources/sprite_frames.cpp index 838566f69655..1674e4b667df 100644 --- a/scene/resources/sprite_frames.cpp +++ b/scene/resources/sprite_frames.cpp @@ -32,19 +32,40 @@ #include "scene/scene_string_names.h" -void SpriteFrames::add_frame(const StringName &p_anim, const Ref &p_frame, int p_at_pos) { +void SpriteFrames::add_frame(const StringName &p_anim, const Ref &p_texture, float p_duration, int p_at_pos) { HashMap::Iterator E = animations.find(p_anim); ERR_FAIL_COND_MSG(!E, "Animation '" + String(p_anim) + "' doesn't exist."); + p_duration = MAX(0.0, p_duration); + + Frame frame = { p_texture, p_duration }; + if (p_at_pos >= 0 && p_at_pos < E->value.frames.size()) { - E->value.frames.insert(p_at_pos, p_frame); + E->value.frames.insert(p_at_pos, frame); } else { - E->value.frames.push_back(p_frame); + E->value.frames.push_back(frame); } emit_changed(); } +void SpriteFrames::set_frame(const StringName &p_anim, int p_idx, const Ref &p_texture, float p_duration) { + HashMap::Iterator E = animations.find(p_anim); + ERR_FAIL_COND_MSG(!E, "Animation '" + String(p_anim) + "' doesn't exist."); + ERR_FAIL_COND(p_idx < 0); + if (p_idx >= E->value.frames.size()) { + return; + } + + p_duration = MAX(0.0, p_duration); + + Frame frame = { p_texture, p_duration }; + + E->value.frames.write[p_idx] = frame; + + emit_changed(); +} + int SpriteFrames::get_frame_count(const StringName &p_anim) const { HashMap::ConstIterator E = animations.find(p_anim); ERR_FAIL_COND_V_MSG(!E, 0, "Animation '" + String(p_anim) + "' doesn't exist."); @@ -57,6 +78,7 @@ void SpriteFrames::remove_frame(const StringName &p_anim, int p_idx) { ERR_FAIL_COND_MSG(!E, "Animation '" + String(p_anim) + "' doesn't exist."); E->value.frames.remove_at(p_idx); + emit_changed(); } @@ -65,6 +87,7 @@ void SpriteFrames::clear(const StringName &p_anim) { ERR_FAIL_COND_MSG(!E, "Animation '" + String(p_anim) + "' doesn't exist."); E->value.frames.clear(); + emit_changed(); } @@ -151,7 +174,10 @@ Array SpriteFrames::_get_animations() const { d["loop"] = anim.loop; Array frames; for (int i = 0; i < anim.frames.size(); i++) { - frames.push_back(anim.frames[i]); + Dictionary f; + f["texture"] = anim.frames[i].texture; + f["duration"] = anim.frames[i].duration; + frames.push_back(f); } d["frames"] = frames; anims.push_back(d); @@ -175,8 +201,21 @@ void SpriteFrames::_set_animations(const Array &p_animations) { anim.loop = d["loop"]; Array frames = d["frames"]; for (int j = 0; j < frames.size(); j++) { + // For compatibility. Ref res = frames[j]; - anim.frames.push_back(res); + if (res.is_valid()) { + Frame frame = { res, 1.0 }; + anim.frames.push_back(frame); + continue; + } + + Dictionary f = frames[j]; + + ERR_CONTINUE(!f.has("texture")); + ERR_CONTINUE(!f.has("duration")); + + Frame frame = { f["texture"], f["duration"] }; + anim.frames.push_back(frame); } animations[d["name"]] = anim; @@ -191,17 +230,20 @@ void SpriteFrames::_bind_methods() { ClassDB::bind_method(D_METHOD("get_animation_names"), &SpriteFrames::get_animation_names); - ClassDB::bind_method(D_METHOD("set_animation_speed", "anim", "speed"), &SpriteFrames::set_animation_speed); + 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); 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); - ClassDB::bind_method(D_METHOD("add_frame", "anim", "frame", "at_position"), &SpriteFrames::add_frame, DEFVAL(-1)); - ClassDB::bind_method(D_METHOD("get_frame_count", "anim"), &SpriteFrames::get_frame_count); - ClassDB::bind_method(D_METHOD("get_frame", "anim", "idx"), &SpriteFrames::get_frame); - ClassDB::bind_method(D_METHOD("set_frame", "anim", "idx", "txt"), &SpriteFrames::set_frame); + 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)); ClassDB::bind_method(D_METHOD("remove_frame", "anim", "idx"), &SpriteFrames::remove_frame); + + ClassDB::bind_method(D_METHOD("get_frame_count", "anim"), &SpriteFrames::get_frame_count); + ClassDB::bind_method(D_METHOD("get_frame_texture", "anim", "idx"), &SpriteFrames::get_frame_texture); + ClassDB::bind_method(D_METHOD("get_frame_duration", "anim", "idx"), &SpriteFrames::get_frame_duration); + ClassDB::bind_method(D_METHOD("clear", "anim"), &SpriteFrames::clear); ClassDB::bind_method(D_METHOD("clear_all"), &SpriteFrames::clear_all); diff --git a/scene/resources/sprite_frames.h b/scene/resources/sprite_frames.h index 87d84b70c0b5..22f5393f34dd 100644 --- a/scene/resources/sprite_frames.h +++ b/scene/resources/sprite_frames.h @@ -36,10 +36,15 @@ class SpriteFrames : public Resource { GDCLASS(SpriteFrames, Resource); + struct Frame { + Ref texture; + float duration = 1.0; + }; + struct Anim { double speed = 5.0; bool loop = true; - Vector> frames; + Vector frames; }; HashMap animations; @@ -65,9 +70,13 @@ public: void set_animation_loop(const StringName &p_anim, bool p_loop); bool get_animation_loop(const StringName &p_anim) const; - void add_frame(const StringName &p_anim, const Ref &p_frame, int p_at_pos = -1); + 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); + void remove_frame(const StringName &p_anim, int p_idx); + int get_frame_count(const StringName &p_anim) const; - _FORCE_INLINE_ Ref get_frame(const StringName &p_anim, int p_idx) const { + + _FORCE_INLINE_ Ref get_frame_texture(const StringName &p_anim, int p_idx) const { HashMap::ConstIterator E = animations.find(p_anim); ERR_FAIL_COND_V_MSG(!E, Ref(), "Animation '" + String(p_anim) + "' doesn't exist."); ERR_FAIL_COND_V(p_idx < 0, Ref()); @@ -75,19 +84,20 @@ public: return Ref(); } - return E->value.frames[p_idx]; + return E->value.frames[p_idx].texture; } - void set_frame(const StringName &p_anim, int p_idx, const Ref &p_frame) { - HashMap::Iterator E = animations.find(p_anim); - ERR_FAIL_COND_MSG(!E, "Animation '" + String(p_anim) + "' doesn't exist."); - ERR_FAIL_COND(p_idx < 0); + _FORCE_INLINE_ float get_frame_duration(const StringName &p_anim, int p_idx) const { + HashMap::ConstIterator E = animations.find(p_anim); + ERR_FAIL_COND_V_MSG(!E, 0.0, "Animation '" + String(p_anim) + "' doesn't exist."); + ERR_FAIL_COND_V(p_idx < 0, 0.0); if (p_idx >= E->value.frames.size()) { - return; + return 0.0; } - E->value.frames.write[p_idx] = p_frame; + + return E->value.frames[p_idx].duration; } - void remove_frame(const StringName &p_anim, int p_idx); + void clear(const StringName &p_anim); void clear_all(); diff --git a/tests/scene/test_sprite_frames.h b/tests/scene/test_sprite_frames.h index 61bbd164933e..ccf18bc5568d 100644 --- a/tests/scene/test_sprite_frames.h +++ b/tests/scene/test_sprite_frames.h @@ -210,9 +210,9 @@ TEST_CASE("[SpriteFrames] Frame addition, removal, and retrieval") { frames.get_frame_count(test_animation_name) == 0, "Animation has a default frame count of 0"); - frames.add_frame(test_animation_name, dummy_frame1, 0); - frames.add_frame(test_animation_name, dummy_frame1, 1); - frames.add_frame(test_animation_name, dummy_frame1, 2); + frames.add_frame(test_animation_name, dummy_frame1, 1.0, 0); + frames.add_frame(test_animation_name, dummy_frame1, 1.0, 1); + frames.add_frame(test_animation_name, dummy_frame1, 1.0, 2); CHECK_MESSAGE( frames.get_frame_count(test_animation_name) == 3, @@ -227,7 +227,7 @@ TEST_CASE("[SpriteFrames] Frame addition, removal, and retrieval") { // These error handling cases should not crash. ERR_PRINT_OFF; - frames.add_frame("does not exist", dummy_frame1, 0); + frames.add_frame("does not exist", dummy_frame1, 1.0, 0); frames.remove_frame(test_animation_name, -99); frames.remove_frame("does not exist", 0); ERR_PRINT_ON;