AnimatedSprite{2D,3D} improvements

* Add support for individual frame duration to `SpriteFrames`.
* Various minor improvements.
This commit is contained in:
Danil Alexeev 2022-11-26 17:00:38 +03:00
parent dc3175e4cd
commit 0d25d8e7fc
No known key found for this signature in database
GPG Key ID: 124453E157DA8DC7
13 changed files with 379 additions and 230 deletions

View File

@ -6,7 +6,7 @@
<description> <description>
[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. [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. 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. [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.
</description> </description>
<tutorials> <tutorials>
@ -20,13 +20,14 @@
<param index="1" name="backwards" type="bool" default="false" /> <param index="1" name="backwards" type="bool" default="false" />
<description> <description>
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. 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.
</description> </description>
</method> </method>
<method name="stop"> <method name="stop">
<return type="void" /> <return type="void" />
<description> <description>
Stops the current [member animation] at the current [member frame]. 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.
</description> </description>
</method> </method>
</methods> </methods>
@ -53,7 +54,9 @@
The texture's drawing offset. The texture's drawing offset.
</member> </member>
<member name="playing" type="bool" setter="set_playing" getter="is_playing" default="false"> <member name="playing" type="bool" setter="set_playing" getter="is_playing" default="false">
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].
</member> </member>
<member name="speed_scale" type="float" setter="set_speed_scale" getter="get_speed_scale" default="1.0"> <member name="speed_scale" type="float" setter="set_speed_scale" getter="get_speed_scale" default="1.0">
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. 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 @@
<signals> <signals>
<signal name="animation_finished"> <signal name="animation_finished">
<description> <description>
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.
</description> </description>
</signal> </signal>
<signal name="frame_changed"> <signal name="frame_changed">

View File

@ -6,7 +6,7 @@
<description> <description>
[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. [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. 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.
</description> </description>
<tutorials> <tutorials>
<link title="2D Sprite animation (also applies to 3D)">$DOCS_URL/tutorials/2d/2d_sprite_animation.html</link> <link title="2D Sprite animation (also applies to 3D)">$DOCS_URL/tutorials/2d/2d_sprite_animation.html</link>
@ -18,13 +18,14 @@
<param index="1" name="backwards" type="bool" default="false" /> <param index="1" name="backwards" type="bool" default="false" />
<description> <description>
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. 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.
</description> </description>
</method> </method>
<method name="stop"> <method name="stop">
<return type="void" /> <return type="void" />
<description> <description>
Stops the current [member animation] at the current [member frame]. 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.
</description> </description>
</method> </method>
</methods> </methods>
@ -39,7 +40,9 @@
The [SpriteFrames] resource containing the animation(s). The [SpriteFrames] resource containing the animation(s).
</member> </member>
<member name="playing" type="bool" setter="set_playing" getter="is_playing" default="false"> <member name="playing" type="bool" setter="set_playing" getter="is_playing" default="false">
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].
</member> </member>
<member name="speed_scale" type="float" setter="set_speed_scale" getter="get_speed_scale" default="1.0"> <member name="speed_scale" type="float" setter="set_speed_scale" getter="get_speed_scale" default="1.0">
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. 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 @@
<signals> <signals>
<signal name="animation_finished"> <signal name="animation_finished">
<description> <description>
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.
</description> </description>
</signal> </signal>
<signal name="frame_changed"> <signal name="frame_changed">

View File

@ -20,8 +20,9 @@
<method name="add_frame"> <method name="add_frame">
<return type="void" /> <return type="void" />
<param index="0" name="anim" type="StringName" /> <param index="0" name="anim" type="StringName" />
<param index="1" name="frame" type="Texture2D" /> <param index="1" name="texture" type="Texture2D" />
<param index="2" name="at_position" type="int" default="-1" /> <param index="2" name="duration" type="float" default="1.0" />
<param index="3" name="at_position" type="int" default="-1" />
<description> <description>
Adds a frame to the given animation. Adds a frame to the given animation.
</description> </description>
@ -56,22 +57,34 @@
<return type="float" /> <return type="float" />
<param index="0" name="anim" type="StringName" /> <param index="0" name="anim" type="StringName" />
<description> <description>
The animation's speed in frames per second. Returns the speed in frames per second for the [param anim] animation.
</description>
</method>
<method name="get_frame" qualifiers="const">
<return type="Texture2D" />
<param index="0" name="anim" type="StringName" />
<param index="1" name="idx" type="int" />
<description>
Returns the animation's selected frame.
</description> </description>
</method> </method>
<method name="get_frame_count" qualifiers="const"> <method name="get_frame_count" qualifiers="const">
<return type="int" /> <return type="int" />
<param index="0" name="anim" type="StringName" /> <param index="0" name="anim" type="StringName" />
<description> <description>
Returns the number of frames in the animation. Returns the number of frames for the [param anim] animation.
</description>
</method>
<method name="get_frame_duration" qualifiers="const">
<return type="float" />
<param index="0" name="anim" type="StringName" />
<param index="1" name="idx" type="int" />
<description>
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].
</description>
</method>
<method name="get_frame_texture" qualifiers="const">
<return type="Texture2D" />
<param index="0" name="anim" type="StringName" />
<param index="1" name="idx" type="int" />
<description>
Returns the texture of the frame [param idx] in the [param anim] animation.
</description> </description>
</method> </method>
<method name="has_animation" qualifiers="const"> <method name="has_animation" qualifiers="const">
@ -115,18 +128,19 @@
<method name="set_animation_speed"> <method name="set_animation_speed">
<return type="void" /> <return type="void" />
<param index="0" name="anim" type="StringName" /> <param index="0" name="anim" type="StringName" />
<param index="1" name="speed" type="float" /> <param index="1" name="fps" type="float" />
<description> <description>
The animation's speed in frames per second. Sets the speed for the [param anim] animation in frames per second.
</description> </description>
</method> </method>
<method name="set_frame"> <method name="set_frame">
<return type="void" /> <return type="void" />
<param index="0" name="anim" type="StringName" /> <param index="0" name="anim" type="StringName" />
<param index="1" name="idx" type="int" /> <param index="1" name="idx" type="int" />
<param index="2" name="txt" type="Texture2D" /> <param index="2" name="texture" type="Texture2D" />
<param index="3" name="duration" type="float" default="1.0" />
<description> <description>
Sets the texture of the given frame. Sets the texture and the duration of the frame [param idx] in the [param anim] animation.
</description> </description>
</method> </method>
</methods> </methods>

View File

@ -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); animation_name = get_animation()->track_get_key_value(animation_track, animaiton_index);
} }
Ref<Texture2D> texture = sf->get_frame(animation_name, frame); Ref<Texture2D> texture = sf->get_frame_texture(animation_name, frame);
if (!texture.is_valid()) { if (!texture.is_valid()) {
return AnimationTrackEdit::get_key_rect(p_index, p_pixels_sec); 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); 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()) { if (!texture.is_valid()) {
AnimationTrackEdit::draw_key(p_index, p_pixels_sec, p_x, p_selected, p_clip_left, p_clip_right); AnimationTrackEdit::draw_key(p_index, p_pixels_sec, p_x, p_selected, p_clip_left, p_clip_right);
return; return;

View File

@ -265,7 +265,7 @@ void SpriteFramesEditor::_sheet_add_frames() {
at->set_atlas(split_sheet_preview->get_texture()); at->set_atlas(split_sheet_preview->get_texture());
at->set_region(Rect2(offset + frame_coords * (frame_size + separation), frame_size)); 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); 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) { void SpriteFramesEditor::_prepare_sprite_sheet(const String &p_file) {
Ref<Texture2D> texture = ResourceLoader::load(p_file); Ref<Texture2D> texture = ResourceLoader::load(p_file);
if (!texture.is_valid()) { if (texture.is_null()) {
EditorNode::get_singleton()->show_warning(TTR("Unable to load images")); 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(); frames_selected.clear();
last_frame_selected = -1; last_frame_selected = -1;
@ -477,7 +477,7 @@ void SpriteFramesEditor::_file_load_request(const Vector<String> &p_path, int p_
int count = 0; int count = 0;
for (const Ref<Texture2D> &E : resources) { for (const Ref<Texture2D> &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); undo_redo->add_undo_method(frames, "remove_frame", edited_anim, p_at_pos == -1 ? fc : p_at_pos);
count++; count++;
} }
@ -521,8 +521,18 @@ void SpriteFramesEditor::_load_pressed() {
void SpriteFramesEditor::_paste_pressed() { void SpriteFramesEditor::_paste_pressed() {
ERR_FAIL_COND(!frames->has_animation(edited_anim)); ERR_FAIL_COND(!frames->has_animation(edited_anim));
Ref<Texture2D> r = EditorSettings::get_singleton()->get_resource_clipboard(); Ref<Texture2D> texture;
if (!r.is_valid()) { float duration = 1.0;
Ref<EditorSpriteFramesFrame> 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_text(TTR("Resource clipboard is empty or not a texture!"));
dialog->set_title(TTR("Error!")); dialog->set_title(TTR("Error!"));
//dialog->get_cancel()->set_text("Close"); //dialog->get_cancel()->set_text("Close");
@ -533,7 +543,7 @@ void SpriteFramesEditor::_paste_pressed() {
Ref<EditorUndoRedoManager> &undo_redo = EditorNode::get_undo_redo(); Ref<EditorUndoRedoManager> &undo_redo = EditorNode::get_undo_redo();
undo_redo->create_action(TTR("Paste Frame")); 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_undo_method(frames, "remove_frame", edited_anim, frames->get_frame_count(edited_anim));
undo_redo->add_do_method(this, "_update_library"); undo_redo->add_do_method(this, "_update_library");
undo_redo->add_undo_method(this, "_update_library"); undo_redo->add_undo_method(this, "_update_library");
@ -543,15 +553,20 @@ void SpriteFramesEditor::_paste_pressed() {
void SpriteFramesEditor::_copy_pressed() { void SpriteFramesEditor::_copy_pressed() {
ERR_FAIL_COND(!frames->has_animation(edited_anim)); ERR_FAIL_COND(!frames->has_animation(edited_anim));
if (tree->get_current() < 0) { if (frame_list->get_current() < 0) {
return;
}
Ref<Texture2D> r = frames->get_frame(edited_anim, tree->get_current());
if (!r.is_valid()) {
return; return;
} }
EditorSettings::get_singleton()->set_resource_clipboard(r); Ref<Texture2D> texture = frames->get_frame_texture(edited_anim, frame_list->get_current());
if (texture.is_null()) {
return;
}
Ref<EditorSpriteFramesFrame> 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() { void SpriteFramesEditor::_empty_pressed() {
@ -559,19 +574,19 @@ void SpriteFramesEditor::_empty_pressed() {
int from = -1; int from = -1;
if (tree->get_current() >= 0) { if (frame_list->get_current() >= 0) {
from = tree->get_current(); from = frame_list->get_current();
sel = from; sel = from;
} else { } else {
from = frames->get_frame_count(edited_anim); from = frames->get_frame_count(edited_anim);
} }
Ref<Texture2D> r; Ref<Texture2D> texture;
Ref<EditorUndoRedoManager> &undo_redo = EditorNode::get_undo_redo(); Ref<EditorUndoRedoManager> &undo_redo = EditorNode::get_undo_redo();
undo_redo->create_action(TTR("Add Empty")); 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_undo_method(frames, "remove_frame", edited_anim, from);
undo_redo->add_do_method(this, "_update_library"); undo_redo->add_do_method(this, "_update_library");
undo_redo->add_undo_method(this, "_update_library"); undo_redo->add_undo_method(this, "_update_library");
@ -583,19 +598,19 @@ void SpriteFramesEditor::_empty2_pressed() {
int from = -1; int from = -1;
if (tree->get_current() >= 0) { if (frame_list->get_current() >= 0) {
from = tree->get_current(); from = frame_list->get_current();
sel = from; sel = from;
} else { } else {
from = frames->get_frame_count(edited_anim); from = frames->get_frame_count(edited_anim);
} }
Ref<Texture2D> r; Ref<Texture2D> texture;
Ref<EditorUndoRedoManager> &undo_redo = EditorNode::get_undo_redo(); Ref<EditorUndoRedoManager> &undo_redo = EditorNode::get_undo_redo();
undo_redo->create_action(TTR("Add Empty")); 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_undo_method(frames, "remove_frame", edited_anim, from + 1);
undo_redo->add_do_method(this, "_update_library"); undo_redo->add_do_method(this, "_update_library");
undo_redo->add_undo_method(this, "_update_library"); undo_redo->add_undo_method(this, "_update_library");
@ -605,11 +620,11 @@ void SpriteFramesEditor::_empty2_pressed() {
void SpriteFramesEditor::_up_pressed() { void SpriteFramesEditor::_up_pressed() {
ERR_FAIL_COND(!frames->has_animation(edited_anim)); ERR_FAIL_COND(!frames->has_animation(edited_anim));
if (tree->get_current() < 0) { if (frame_list->get_current() < 0) {
return; return;
} }
int to_move = tree->get_current(); int to_move = frame_list->get_current();
if (to_move < 1) { if (to_move < 1) {
return; return;
} }
@ -618,11 +633,11 @@ void SpriteFramesEditor::_up_pressed() {
sel -= 1; sel -= 1;
Ref<EditorUndoRedoManager> &undo_redo = EditorNode::get_undo_redo(); Ref<EditorUndoRedoManager> &undo_redo = EditorNode::get_undo_redo();
undo_redo->create_action(TTR("Delete Resource")); undo_redo->create_action(TTR("Move Frame"));
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, 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(edited_anim, to_move)); 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(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(edited_anim, to_move - 1)); 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_do_method(this, "_update_library");
undo_redo->add_undo_method(this, "_update_library"); undo_redo->add_undo_method(this, "_update_library");
undo_redo->commit_action(); undo_redo->commit_action();
@ -631,11 +646,11 @@ void SpriteFramesEditor::_up_pressed() {
void SpriteFramesEditor::_down_pressed() { void SpriteFramesEditor::_down_pressed() {
ERR_FAIL_COND(!frames->has_animation(edited_anim)); ERR_FAIL_COND(!frames->has_animation(edited_anim));
if (tree->get_current() < 0) { if (frame_list->get_current() < 0) {
return; 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) { if (to_move < 0 || to_move >= frames->get_frame_count(edited_anim) - 1) {
return; return;
} }
@ -644,11 +659,11 @@ void SpriteFramesEditor::_down_pressed() {
sel += 1; sel += 1;
Ref<EditorUndoRedoManager> &undo_redo = EditorNode::get_undo_redo(); Ref<EditorUndoRedoManager> &undo_redo = EditorNode::get_undo_redo();
undo_redo->create_action(TTR("Delete Resource")); undo_redo->create_action(TTR("Move Frame"));
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, 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(edited_anim, to_move)); 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(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(edited_anim, to_move + 1)); 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_do_method(this, "_update_library");
undo_redo->add_undo_method(this, "_update_library"); undo_redo->add_undo_method(this, "_update_library");
undo_redo->commit_action(); undo_redo->commit_action();
@ -657,11 +672,11 @@ void SpriteFramesEditor::_down_pressed() {
void SpriteFramesEditor::_delete_pressed() { void SpriteFramesEditor::_delete_pressed() {
ERR_FAIL_COND(!frames->has_animation(edited_anim)); ERR_FAIL_COND(!frames->has_animation(edited_anim));
if (tree->get_current() < 0) { if (frame_list->get_current() < 0) {
return; 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)) { if (to_delete < 0 || to_delete >= frames->get_frame_count(edited_anim)) {
return; return;
} }
@ -669,7 +684,7 @@ void SpriteFramesEditor::_delete_pressed() {
Ref<EditorUndoRedoManager> &undo_redo = EditorNode::get_undo_redo(); Ref<EditorUndoRedoManager> &undo_redo = EditorNode::get_undo_redo();
undo_redo->create_action(TTR("Delete Resource")); undo_redo->create_action(TTR("Delete Resource"));
undo_redo->add_do_method(frames, "remove_frame", edited_anim, to_delete); 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_do_method(this, "_update_library");
undo_redo->add_undo_method(this, "_update_library"); undo_redo->add_undo_method(this, "_update_library");
undo_redo->commit_action(); undo_redo->commit_action();
@ -683,7 +698,7 @@ void SpriteFramesEditor::_animation_select() {
if (frames->has_animation(edited_anim)) { if (frames->has_animation(edited_anim)) {
double value = anim_speed->get_line_edit()->get_text().to_float(); double value = anim_speed->get_line_edit()->get_text().to_float();
if (!Math::is_equal_approx(value, (double)frames->get_animation_speed(edited_anim))) { 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)); 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); int fc = frames->get_frame_count(edited_anim);
for (int i = 0; i < fc; i++) { for (int i = 0; i < fc; i++) {
Ref<Texture2D> frame = frames->get_frame(edited_anim, i); Ref<Texture2D> texture = frames->get_frame_texture(edited_anim, i);
undo_redo->add_undo_method(frames, "add_frame", edited_anim, frame); 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_do_method(this, "_update_library");
undo_redo->add_undo_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(); undo_redo->commit_action();
} }
void SpriteFramesEditor::_animation_fps_changed(double p_value) { void SpriteFramesEditor::_animation_speed_changed(double p_value) {
if (updating) { if (updating) {
return; 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_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_do_method(this, "_update_library", true);
undo_redo->add_undo_method(this, "_update_library", true); undo_redo->add_undo_method(this, "_update_library", true);
undo_redo->commit_action(); undo_redo->commit_action();
} }
void SpriteFramesEditor::_tree_input(const Ref<InputEvent> &p_event) { void SpriteFramesEditor::_frame_list_gui_input(const Ref<InputEvent> &p_event) {
const Ref<InputEventMouseButton> mb = p_event; const Ref<InputEventMouseButton> mb = p_event;
if (mb.is_valid()) { if (mb.is_valid()) {
@ -884,6 +899,42 @@ void SpriteFramesEditor::_tree_input(const Ref<InputEvent> &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<Texture2D> texture = frames->get_frame_texture(edited_anim, index);
float old_duration = frames->get_frame_duration(edited_anim, index);
Ref<EditorUndoRedoManager> &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() { void SpriteFramesEditor::_zoom_in() {
// Do not zoom in or out with no visible frames // Do not zoom in or out with no visible frames
if (frames->get_frame_count(edited_anim) <= 0) { if (frames->get_frame_count(edited_anim) <= 0) {
@ -892,8 +943,8 @@ void SpriteFramesEditor::_zoom_in() {
if (thumbnail_zoom < max_thumbnail_zoom) { if (thumbnail_zoom < max_thumbnail_zoom) {
thumbnail_zoom *= scale_ratio; thumbnail_zoom *= scale_ratio;
int thumbnail_size = (int)(thumbnail_default_size * thumbnail_zoom); int thumbnail_size = (int)(thumbnail_default_size * thumbnail_zoom);
tree->set_fixed_column_width(thumbnail_size * 3 / 2); frame_list->set_fixed_column_width(thumbnail_size * 3 / 2);
tree->set_fixed_icon_size(Size2(thumbnail_size, thumbnail_size)); 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) { if (thumbnail_zoom > min_thumbnail_zoom) {
thumbnail_zoom /= scale_ratio; thumbnail_zoom /= scale_ratio;
int thumbnail_size = (int)(thumbnail_default_size * thumbnail_zoom); int thumbnail_size = (int)(thumbnail_default_size * thumbnail_zoom);
tree->set_fixed_column_width(thumbnail_size * 3 / 2); frame_list->set_fixed_column_width(thumbnail_size * 3 / 2);
tree->set_fixed_icon_size(Size2(thumbnail_size, thumbnail_size)); frame_list->set_fixed_icon_size(Size2(thumbnail_size, thumbnail_size));
} }
} }
void SpriteFramesEditor::_zoom_reset() { void SpriteFramesEditor::_zoom_reset() {
thumbnail_zoom = MAX(1.0f, EDSCALE); thumbnail_zoom = MAX(1.0f, EDSCALE);
tree->set_fixed_column_width(thumbnail_default_size * 3 / 2); frame_list->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_icon_size(Size2(thumbnail_default_size, thumbnail_default_size));
} }
void SpriteFramesEditor::_update_library(bool p_skip_selector) { void SpriteFramesEditor::_update_library(bool p_skip_selector) {
updating = true; updating = true;
frame_duration->set_value(1.0); // Default.
if (!p_skip_selector) { if (!p_skip_selector) {
animations->clear(); 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)) { if (!frames->has_animation(edited_anim)) {
updating = false; 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++) { for (int i = 0; i < frames->get_frame_count(edited_anim); i++) {
String name; String name;
Ref<Texture2D> frame = frames->get_frame(edited_anim, i); Ref<Texture2D> texture = frames->get_frame_texture(edited_anim, i);
float duration = frames->get_frame_duration(edited_anim, i);
if (frame.is_null()) { String duration_string;
name = itos(i) + ": " + TTR("(empty)"); if (duration != 1.0f) {
} else { duration_string = String::utf8(" [ ×") + String::num_real(frames->get_frame_duration(edited_anim, i)) + " ]";
name = itos(i) + ": " + frame->get_name();
} }
tree->add_item(name, frame); if (texture.is_null()) {
if (frame.is_valid()) { name = itos(i) + ": " + TTR("(empty)") + duration_string;
String tooltip = frame->get_path(); } 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, // 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 // thus its path might be not what the user is looking for. So we're also showing
// subsequent source texture paths. // subsequent source texture paths.
String prefix = String::utf8("┖╴"); String prefix = String::utf8("┖╴");
Ref<AtlasTexture> at = frame; Ref<AtlasTexture> at = texture;
while (at.is_valid() && at->get_atlas().is_valid()) { while (at.is_valid() && at->get_atlas().is_valid()) {
tooltip += "\n" + prefix + at->get_atlas()->get_path(); tooltip += "\n" + prefix + at->get_atlas()->get_path();
prefix = " " + prefix; prefix = " " + prefix;
at = at->get_atlas(); at = at->get_atlas();
} }
tree->set_item_tooltip(-1, tooltip); frame_list->set_item_tooltip(-1, tooltip);
} }
if (sel == i) { 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; 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)) { if (idx < 0 || idx >= frames->get_frame_count(edited_anim)) {
return Variant(); return Variant();
} }
Ref<Resource> frame = frames->get_frame(edited_anim, idx); Ref<Resource> frame = frames->get_frame_texture(edited_anim, idx);
if (frame.is_null()) { if (frame.is_null()) {
return Variant(); return Variant();
@ -1088,7 +1147,7 @@ bool SpriteFramesEditor::can_drop_data_fw(const Point2 &p_point, const Variant &
} }
// reordering frames // reordering frames
if (d.has("from") && (Object *)(d["from"]) == tree) { if (d.has("from") && (Object *)(d["from"]) == frame_list) {
return true; return true;
} }
@ -1134,7 +1193,7 @@ void SpriteFramesEditor::drop_data_fw(const Point2 &p_point, const Variant &p_da
return; 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")) { if (String(d["type"]) == "resource" && d.has("resource")) {
Ref<Resource> r = d["resource"]; Ref<Resource> r = d["resource"];
@ -1143,28 +1202,30 @@ void SpriteFramesEditor::drop_data_fw(const Point2 &p_point, const Variant &p_da
if (texture.is_valid()) { if (texture.is_valid()) {
bool reorder = false; bool reorder = false;
if (d.has("from") && (Object *)(d["from"]) == tree) { if (d.has("from") && (Object *)(d["from"]) == frame_list) {
reorder = true; reorder = true;
} }
Ref<EditorUndoRedoManager> &undo_redo = EditorNode::get_undo_redo(); Ref<EditorUndoRedoManager> &undo_redo = EditorNode::get_undo_redo();
if (reorder) { //drop is from reordering frames if (reorder) { //drop is from reordering frames
int from_frame = -1; int from_frame = -1;
float duration = 1.0;
if (d.has("frame")) { if (d.has("frame")) {
from_frame = d["frame"]; from_frame = d["frame"];
duration = frames->get_frame_duration(edited_anim, from_frame);
} }
undo_redo->create_action(TTR("Move 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, "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, "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_do_method(this, "_update_library");
undo_redo->add_undo_method(this, "_update_library"); undo_redo->add_undo_method(this, "_update_library");
undo_redo->commit_action(); undo_redo->commit_action();
} else { } else {
undo_redo->create_action(TTR("Add Frame")); 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_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_do_method(this, "_update_library");
undo_redo->add_undo_method(this, "_update_library"); undo_redo->add_undo_method(this, "_update_library");
@ -1240,11 +1301,11 @@ SpriteFramesEditor::SpriteFramesEditor() {
anim_speed = memnew(SpinBox); anim_speed = memnew(SpinBox);
anim_speed->set_suffix(TTR("FPS")); anim_speed->set_suffix(TTR("FPS"));
anim_speed->set_min(0); anim_speed->set_min(0);
anim_speed->set_max(100); anim_speed->set_max(120);
anim_speed->set_step(0.01); anim_speed->set_step(0.01);
anim_speed->set_h_size_flags(SIZE_EXPAND_FILL); anim_speed->set_h_size_flags(SIZE_EXPAND_FILL);
hbc_anim_speed->add_child(anim_speed); 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 = memnew(CheckButton);
anim_loop->set_text(TTR("Loop")); anim_loop->set_text(TTR("Loop"));
@ -1303,6 +1364,20 @@ SpriteFramesEditor::SpriteFramesEditor() {
delete_frame->set_flat(true); delete_frame->set_flat(true);
hbc->add_child(delete_frame); 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(); hbc->add_spacer();
zoom_out = memnew(Button); zoom_out = memnew(Button);
@ -1326,17 +1401,18 @@ SpriteFramesEditor::SpriteFramesEditor() {
file = memnew(EditorFileDialog); file = memnew(EditorFileDialog);
add_child(file); add_child(file);
tree = memnew(ItemList); frame_list = memnew(ItemList);
tree->set_v_size_flags(SIZE_EXPAND_FILL); frame_list->set_v_size_flags(SIZE_EXPAND_FILL);
tree->set_icon_mode(ItemList::ICON_MODE_TOP); frame_list->set_icon_mode(ItemList::ICON_MODE_TOP);
tree->set_max_columns(0); frame_list->set_max_columns(0);
tree->set_icon_mode(ItemList::ICON_MODE_TOP); frame_list->set_icon_mode(ItemList::ICON_MODE_TOP);
tree->set_max_text_lines(2); frame_list->set_max_text_lines(2);
tree->set_drag_forwarding(this); frame_list->set_drag_forwarding(this);
tree->connect("gui_input", callable_mp(this, &SpriteFramesEditor::_tree_input)); 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); dialog = memnew(AcceptDialog);
add_child(dialog); add_child(dialog);
@ -1351,33 +1427,34 @@ SpriteFramesEditor::SpriteFramesEditor() {
move_up->connect("pressed", callable_mp(this, &SpriteFramesEditor::_up_pressed)); move_up->connect("pressed", callable_mp(this, &SpriteFramesEditor::_up_pressed));
move_down->connect("pressed", callable_mp(this, &SpriteFramesEditor::_down_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->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)); 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)); 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)); 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)); 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_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)); 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_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)); 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"), 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) })); { 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"), 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) })); { 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)); 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; loading_scene = false;
sel = -1; sel = -1;

View File

@ -46,6 +46,14 @@
class EditorFileDialog; class EditorFileDialog;
class EditorSpriteFramesFrame : public Resource {
GDCLASS(EditorSpriteFramesFrame, Resource);
public:
Ref<Texture2D> texture;
float duration;
};
class SpriteFramesEditor : public HSplitContainer { class SpriteFramesEditor : public HSplitContainer {
GDCLASS(SpriteFramesEditor, HSplitContainer); GDCLASS(SpriteFramesEditor, HSplitContainer);
@ -70,7 +78,8 @@ class SpriteFramesEditor : public HSplitContainer {
Button *zoom_out = nullptr; Button *zoom_out = nullptr;
Button *zoom_reset = nullptr; Button *zoom_reset = nullptr;
Button *zoom_in = nullptr; Button *zoom_in = nullptr;
ItemList *tree = nullptr; SpinBox *frame_duration = nullptr;
ItemList *frame_list = nullptr;
bool loading_scene; bool loading_scene;
int sel; int sel;
@ -134,6 +143,7 @@ class SpriteFramesEditor : public HSplitContainer {
void _delete_pressed(); void _delete_pressed();
void _up_pressed(); void _up_pressed();
void _down_pressed(); void _down_pressed();
void _frame_duration_changed(double p_value);
void _update_library(bool p_skip_selector = false); void _update_library(bool p_skip_selector = false);
void _animation_select(); void _animation_select();
@ -143,9 +153,11 @@ class SpriteFramesEditor : public HSplitContainer {
void _animation_remove_confirmed(); void _animation_remove_confirmed();
void _animation_search_text_changed(const String &p_text); void _animation_search_text_changed(const String &p_text);
void _animation_loop_changed(); 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<InputEvent> &p_event);
void _frame_list_item_selected(int p_index);
void _tree_input(const Ref<InputEvent> &p_event);
void _zoom_in(); void _zoom_in();
void _zoom_out(); void _zoom_out();
void _zoom_reset(); void _zoom_reset();

View File

@ -72,7 +72,7 @@ bool AnimatedSprite2D::_edit_use_rect() const {
Ref<Texture2D> t; Ref<Texture2D> t;
if (animation) { if (animation) {
t = frames->get_frame(animation, frame); t = frames->get_frame_texture(animation, frame);
} }
return t.is_valid(); return t.is_valid();
} }
@ -92,7 +92,7 @@ Rect2 AnimatedSprite2D::_get_rect() const {
Ref<Texture2D> t; Ref<Texture2D> t;
if (animation) { if (animation) {
t = frames->get_frame(animation, frame); t = frames->get_frame_texture(animation, frame);
} }
if (t.is_null()) { if (t.is_null()) {
return Rect2(); return Rect2();
@ -172,17 +172,21 @@ void AnimatedSprite2D::_notification(int p_what) {
return; return;
} }
double remaining = get_process_delta_time();
int i = 0;
while (remaining) {
// Animation speed may be changed by animation_finished or frame_changed signals.
double speed = frames->get_animation_speed(animation) * Math::abs(speed_scale); double speed = frames->get_animation_speed(animation) * Math::abs(speed_scale);
if (speed == 0) { if (speed == 0) {
return; // Do nothing. return; // Do nothing.
} }
int last_frame = frames->get_frame_count(animation) - 1;
double remaining = get_process_delta_time(); // Frame count may be changed by animation_finished or frame_changed signals.
while (remaining) { int fc = frames->get_frame_count(animation);
if (timeout <= 0) { if (timeout <= 0) {
timeout = _get_frame_duration(); int last_frame = fc - 1;
if (!playing_backwards) { if (!playing_backwards) {
// Forward. // Forward.
if (frame >= last_frame) { if (frame >= last_frame) {
@ -217,14 +221,21 @@ void AnimatedSprite2D::_notification(int p_what) {
} }
} }
timeout = _get_frame_duration();
queue_redraw(); queue_redraw();
emit_signal(SceneStringNames::get_singleton()->frame_changed); 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; remaining -= to_process;
timeout -= to_process;
i++;
if (i > fc) {
return; // Prevents freezing if to_process is each time much less than remaining.
}
} }
} break; } break;
@ -233,7 +244,7 @@ void AnimatedSprite2D::_notification(int p_what) {
return; return;
} }
Ref<Texture2D> texture = frames->get_frame(animation, frame); Ref<Texture2D> texture = frames->get_frame_texture(animation, frame);
if (texture.is_null()) { if (texture.is_null()) {
return; return;
} }
@ -312,7 +323,6 @@ void AnimatedSprite2D::set_frame(int p_frame) {
frame = p_frame; frame = p_frame;
_reset_timeout(); _reset_timeout();
queue_redraw(); queue_redraw();
emit_signal(SceneStringNames::get_singleton()->frame_changed); emit_signal(SceneStringNames::get_singleton()->frame_changed);
} }
@ -320,22 +330,12 @@ int AnimatedSprite2D::get_frame() const {
return frame; return frame;
} }
void AnimatedSprite2D::set_speed_scale(double p_speed_scale) { void AnimatedSprite2D::set_speed_scale(float p_speed_scale) {
if (speed_scale == p_speed_scale) {
return;
}
double elapsed = _get_frame_duration() - timeout;
speed_scale = p_speed_scale; speed_scale = p_speed_scale;
playing_backwards = signbit(speed_scale) != backwards; 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; return speed_scale;
} }
@ -379,8 +379,8 @@ bool AnimatedSprite2D::is_flipped_v() const {
void AnimatedSprite2D::_res_changed() { void AnimatedSprite2D::_res_changed() {
set_frame(frame); set_frame(frame);
queue_redraw(); queue_redraw();
notify_property_list_changed();
} }
void AnimatedSprite2D::set_playing(bool p_playing) { void AnimatedSprite2D::set_playing(bool p_playing) {
@ -388,7 +388,7 @@ void AnimatedSprite2D::set_playing(bool p_playing) {
return; return;
} }
playing = p_playing; playing = p_playing;
_reset_timeout(); playing_backwards = signbit(speed_scale) != backwards;
set_process_internal(playing); set_process_internal(playing);
notify_property_list_changed(); notify_property_list_changed();
} }
@ -414,23 +414,18 @@ void AnimatedSprite2D::play(const StringName &p_animation, bool p_backwards) {
void AnimatedSprite2D::stop() { void AnimatedSprite2D::stop() {
set_playing(false); set_playing(false);
backwards = false;
_reset_timeout();
} }
double AnimatedSprite2D::_get_frame_duration() { double AnimatedSprite2D::_get_frame_duration() {
if (frames.is_valid() && frames->has_animation(animation)) { if (frames.is_valid() && frames->has_animation(animation)) {
double speed = frames->get_animation_speed(animation) * Math::abs(speed_scale); return frames->get_frame_duration(animation, frame);
if (speed > 0) {
return 1.0 / speed;
}
} }
return 0.0; return 0.0;
} }
void AnimatedSprite2D::_reset_timeout() { void AnimatedSprite2D::_reset_timeout() {
if (!playing) {
return;
}
timeout = _get_frame_duration(); timeout = _get_frame_duration();
is_over = false; is_over = false;
} }
@ -444,8 +439,8 @@ void AnimatedSprite2D::set_animation(const StringName &p_animation) {
} }
animation = p_animation; animation = p_animation;
_reset_timeout();
set_frame(0); set_frame(0);
_reset_timeout();
notify_property_list_changed(); notify_property_list_changed();
queue_redraw(); queue_redraw();
} }
@ -455,12 +450,10 @@ StringName AnimatedSprite2D::get_animation() const {
} }
PackedStringArray AnimatedSprite2D::get_configuration_warnings() const { PackedStringArray AnimatedSprite2D::get_configuration_warnings() const {
PackedStringArray warnings = Node::get_configuration_warnings(); PackedStringArray warnings = Node2D::get_configuration_warnings();
if (frames.is_null()) { 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.")); 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; return warnings;
} }

View File

@ -43,7 +43,7 @@ class AnimatedSprite2D : public Node2D {
bool backwards = false; bool backwards = false;
StringName animation = "default"; StringName animation = "default";
int frame = 0; int frame = 0;
float speed_scale = 1.0f; float speed_scale = 1.0;
bool centered = true; bool centered = true;
Point2 offset; Point2 offset;
@ -94,8 +94,8 @@ public:
void set_frame(int p_frame); void set_frame(int p_frame);
int get_frame() const; int get_frame() const;
void set_speed_scale(double p_speed_scale); void set_speed_scale(float p_speed_scale);
double get_speed_scale() const; float get_speed_scale() const;
void set_centered(bool p_center); void set_centered(bool p_center);
bool is_centered() const; bool is_centered() const;

View File

@ -837,7 +837,7 @@ void AnimatedSprite3D::_draw() {
return; return;
} }
Ref<Texture2D> texture = frames->get_frame(animation, frame); Ref<Texture2D> texture = frames->get_frame_texture(animation, frame);
if (texture.is_null()) { if (texture.is_null()) {
set_base(RID()); set_base(RID());
return; return;
@ -921,17 +921,21 @@ void AnimatedSprite3D::_notification(int p_what) {
return; return;
} }
double remaining = get_process_delta_time();
int i = 0;
while (remaining) {
// Animation speed may be changed by animation_finished or frame_changed signals.
double speed = frames->get_animation_speed(animation) * Math::abs(speed_scale); double speed = frames->get_animation_speed(animation) * Math::abs(speed_scale);
if (speed == 0) { if (speed == 0) {
return; // Do nothing. return; // Do nothing.
} }
int last_frame = frames->get_frame_count(animation) - 1;
double remaining = get_process_delta_time(); // Frame count may be changed by animation_finished or frame_changed signals.
while (remaining) { int fc = frames->get_frame_count(animation);
if (timeout <= 0) { if (timeout <= 0) {
timeout = _get_frame_duration(); int last_frame = fc - 1;
if (!playing_backwards) { if (!playing_backwards) {
// Forward. // Forward.
if (frame >= last_frame) { if (frame >= last_frame) {
@ -966,14 +970,21 @@ void AnimatedSprite3D::_notification(int p_what) {
} }
} }
timeout = _get_frame_duration();
_queue_redraw(); _queue_redraw();
emit_signal(SceneStringNames::get_singleton()->frame_changed); 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; remaining -= to_process;
timeout -= to_process;
i++;
if (i > fc) {
return; // Prevents freezing if to_process is each time much less than remaining.
}
} }
} break; } break;
} }
@ -1028,7 +1039,6 @@ void AnimatedSprite3D::set_frame(int p_frame) {
frame = p_frame; frame = p_frame;
_reset_timeout(); _reset_timeout();
_queue_redraw(); _queue_redraw();
emit_signal(SceneStringNames::get_singleton()->frame_changed); emit_signal(SceneStringNames::get_singleton()->frame_changed);
} }
@ -1036,22 +1046,12 @@ int AnimatedSprite3D::get_frame() const {
return frame; return frame;
} }
void AnimatedSprite3D::set_speed_scale(double p_speed_scale) { void AnimatedSprite3D::set_speed_scale(float p_speed_scale) {
if (speed_scale == p_speed_scale) {
return;
}
double elapsed = _get_frame_duration() - timeout;
speed_scale = p_speed_scale; speed_scale = p_speed_scale;
playing_backwards = signbit(speed_scale) != backwards; 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; return speed_scale;
} }
@ -1065,7 +1065,7 @@ Rect2 AnimatedSprite3D::get_item_rect() const {
Ref<Texture2D> t; Ref<Texture2D> t;
if (animation) { if (animation) {
t = frames->get_frame(animation, frame); t = frames->get_frame_texture(animation, frame);
} }
if (t.is_null()) { if (t.is_null()) {
return Rect2(0, 0, 1, 1); return Rect2(0, 0, 1, 1);
@ -1086,8 +1086,8 @@ Rect2 AnimatedSprite3D::get_item_rect() const {
void AnimatedSprite3D::_res_changed() { void AnimatedSprite3D::_res_changed() {
set_frame(frame); set_frame(frame);
_queue_redraw(); _queue_redraw();
notify_property_list_changed();
} }
void AnimatedSprite3D::set_playing(bool p_playing) { void AnimatedSprite3D::set_playing(bool p_playing) {
@ -1095,7 +1095,7 @@ void AnimatedSprite3D::set_playing(bool p_playing) {
return; return;
} }
playing = p_playing; playing = p_playing;
_reset_timeout(); playing_backwards = signbit(speed_scale) != backwards;
set_process_internal(playing); set_process_internal(playing);
notify_property_list_changed(); notify_property_list_changed();
} }
@ -1121,23 +1121,18 @@ void AnimatedSprite3D::play(const StringName &p_animation, bool p_backwards) {
void AnimatedSprite3D::stop() { void AnimatedSprite3D::stop() {
set_playing(false); set_playing(false);
backwards = false;
_reset_timeout();
} }
double AnimatedSprite3D::_get_frame_duration() { double AnimatedSprite3D::_get_frame_duration() {
if (frames.is_valid() && frames->has_animation(animation)) { if (frames.is_valid() && frames->has_animation(animation)) {
double speed = frames->get_animation_speed(animation) * Math::abs(speed_scale); return frames->get_frame_duration(animation, frame);
if (speed > 0) {
return 1.0 / speed;
}
} }
return 0.0; return 0.0;
} }
void AnimatedSprite3D::_reset_timeout() { void AnimatedSprite3D::_reset_timeout() {
if (!playing) {
return;
}
timeout = _get_frame_duration(); timeout = _get_frame_duration();
is_over = false; is_over = false;
} }
@ -1145,13 +1140,14 @@ void AnimatedSprite3D::_reset_timeout() {
void AnimatedSprite3D::set_animation(const StringName &p_animation) { 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 == 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)); 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) { if (animation == p_animation) {
return; return;
} }
animation = p_animation; animation = p_animation;
_reset_timeout();
set_frame(0); set_frame(0);
_reset_timeout();
notify_property_list_changed(); notify_property_list_changed();
_queue_redraw(); _queue_redraw();
} }
@ -1165,7 +1161,6 @@ PackedStringArray AnimatedSprite3D::get_configuration_warnings() const {
if (frames.is_null()) { 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.")); 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; return warnings;
} }

View File

@ -214,7 +214,7 @@ class AnimatedSprite3D : public SpriteBase3D {
bool backwards = false; bool backwards = false;
StringName animation = "default"; StringName animation = "default";
int frame = 0; int frame = 0;
float speed_scale = 1.0f; float speed_scale = 1.0;
bool centered = false; bool centered = false;
@ -248,8 +248,8 @@ public:
void set_frame(int p_frame); void set_frame(int p_frame);
int get_frame() const; int get_frame() const;
void set_speed_scale(double p_speed_scale); void set_speed_scale(float p_speed_scale);
double get_speed_scale() const; float get_speed_scale() const;
virtual Rect2 get_item_rect() const override; virtual Rect2 get_item_rect() const override;

View File

@ -32,19 +32,40 @@
#include "scene/scene_string_names.h" #include "scene/scene_string_names.h"
void SpriteFrames::add_frame(const StringName &p_anim, const Ref<Texture2D> &p_frame, int p_at_pos) { void SpriteFrames::add_frame(const StringName &p_anim, const Ref<Texture2D> &p_texture, float p_duration, int p_at_pos) {
HashMap<StringName, Anim>::Iterator E = animations.find(p_anim); HashMap<StringName, Anim>::Iterator E = animations.find(p_anim);
ERR_FAIL_COND_MSG(!E, "Animation '" + String(p_anim) + "' doesn't exist."); 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()) { 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 { } else {
E->value.frames.push_back(p_frame); E->value.frames.push_back(frame);
} }
emit_changed(); emit_changed();
} }
void SpriteFrames::set_frame(const StringName &p_anim, int p_idx, const Ref<Texture2D> &p_texture, float p_duration) {
HashMap<StringName, Anim>::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 { int SpriteFrames::get_frame_count(const StringName &p_anim) const {
HashMap<StringName, Anim>::ConstIterator E = animations.find(p_anim); HashMap<StringName, Anim>::ConstIterator E = animations.find(p_anim);
ERR_FAIL_COND_V_MSG(!E, 0, "Animation '" + String(p_anim) + "' doesn't exist."); 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."); ERR_FAIL_COND_MSG(!E, "Animation '" + String(p_anim) + "' doesn't exist.");
E->value.frames.remove_at(p_idx); E->value.frames.remove_at(p_idx);
emit_changed(); 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."); ERR_FAIL_COND_MSG(!E, "Animation '" + String(p_anim) + "' doesn't exist.");
E->value.frames.clear(); E->value.frames.clear();
emit_changed(); emit_changed();
} }
@ -151,7 +174,10 @@ Array SpriteFrames::_get_animations() const {
d["loop"] = anim.loop; d["loop"] = anim.loop;
Array frames; Array frames;
for (int i = 0; i < anim.frames.size(); i++) { 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; d["frames"] = frames;
anims.push_back(d); anims.push_back(d);
@ -175,8 +201,21 @@ void SpriteFrames::_set_animations(const Array &p_animations) {
anim.loop = d["loop"]; anim.loop = d["loop"];
Array frames = d["frames"]; Array frames = d["frames"];
for (int j = 0; j < frames.size(); j++) { for (int j = 0; j < frames.size(); j++) {
// For compatibility.
Ref<Resource> res = frames[j]; Ref<Resource> 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; 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("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("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("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("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("add_frame", "anim", "texture", "duration", "at_position"), &SpriteFrames::add_frame, DEFVAL(1.0), DEFVAL(-1));
ClassDB::bind_method(D_METHOD("get_frame_count", "anim"), &SpriteFrames::get_frame_count); ClassDB::bind_method(D_METHOD("set_frame", "anim", "idx", "texture", "duration"), &SpriteFrames::set_frame, DEFVAL(1.0));
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("remove_frame", "anim", "idx"), &SpriteFrames::remove_frame); 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", "anim"), &SpriteFrames::clear);
ClassDB::bind_method(D_METHOD("clear_all"), &SpriteFrames::clear_all); ClassDB::bind_method(D_METHOD("clear_all"), &SpriteFrames::clear_all);

View File

@ -36,10 +36,15 @@
class SpriteFrames : public Resource { class SpriteFrames : public Resource {
GDCLASS(SpriteFrames, Resource); GDCLASS(SpriteFrames, Resource);
struct Frame {
Ref<Texture2D> texture;
float duration = 1.0;
};
struct Anim { struct Anim {
double speed = 5.0; double speed = 5.0;
bool loop = true; bool loop = true;
Vector<Ref<Texture2D>> frames; Vector<Frame> frames;
}; };
HashMap<StringName, Anim> animations; HashMap<StringName, Anim> animations;
@ -65,9 +70,13 @@ public:
void set_animation_loop(const StringName &p_anim, bool p_loop); void set_animation_loop(const StringName &p_anim, bool p_loop);
bool get_animation_loop(const StringName &p_anim) const; bool get_animation_loop(const StringName &p_anim) const;
void add_frame(const StringName &p_anim, const Ref<Texture2D> &p_frame, int p_at_pos = -1); 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);
void remove_frame(const StringName &p_anim, int p_idx);
int get_frame_count(const StringName &p_anim) const; int get_frame_count(const StringName &p_anim) const;
_FORCE_INLINE_ Ref<Texture2D> get_frame(const StringName &p_anim, int p_idx) const {
_FORCE_INLINE_ Ref<Texture2D> get_frame_texture(const StringName &p_anim, int p_idx) const {
HashMap<StringName, Anim>::ConstIterator E = animations.find(p_anim); HashMap<StringName, Anim>::ConstIterator E = animations.find(p_anim);
ERR_FAIL_COND_V_MSG(!E, Ref<Texture2D>(), "Animation '" + String(p_anim) + "' doesn't exist."); ERR_FAIL_COND_V_MSG(!E, Ref<Texture2D>(), "Animation '" + String(p_anim) + "' doesn't exist.");
ERR_FAIL_COND_V(p_idx < 0, Ref<Texture2D>()); ERR_FAIL_COND_V(p_idx < 0, Ref<Texture2D>());
@ -75,19 +84,20 @@ public:
return Ref<Texture2D>(); return Ref<Texture2D>();
} }
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<Texture2D> &p_frame) { _FORCE_INLINE_ float get_frame_duration(const StringName &p_anim, int p_idx) const {
HashMap<StringName, Anim>::Iterator E = animations.find(p_anim); HashMap<StringName, Anim>::ConstIterator E = animations.find(p_anim);
ERR_FAIL_COND_MSG(!E, "Animation '" + String(p_anim) + "' doesn't exist."); ERR_FAIL_COND_V_MSG(!E, 0.0, "Animation '" + String(p_anim) + "' doesn't exist.");
ERR_FAIL_COND(p_idx < 0); ERR_FAIL_COND_V(p_idx < 0, 0.0);
if (p_idx >= E->value.frames.size()) { 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(const StringName &p_anim);
void clear_all(); void clear_all();

View File

@ -210,9 +210,9 @@ TEST_CASE("[SpriteFrames] Frame addition, removal, and retrieval") {
frames.get_frame_count(test_animation_name) == 0, frames.get_frame_count(test_animation_name) == 0,
"Animation has a default frame count of 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.0, 0);
frames.add_frame(test_animation_name, dummy_frame1, 1); frames.add_frame(test_animation_name, dummy_frame1, 1.0, 1);
frames.add_frame(test_animation_name, dummy_frame1, 2); frames.add_frame(test_animation_name, dummy_frame1, 1.0, 2);
CHECK_MESSAGE( CHECK_MESSAGE(
frames.get_frame_count(test_animation_name) == 3, 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. // These error handling cases should not crash.
ERR_PRINT_OFF; 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(test_animation_name, -99);
frames.remove_frame("does not exist", 0); frames.remove_frame("does not exist", 0);
ERR_PRINT_ON; ERR_PRINT_ON;