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