From cb4fa0bbc9621503f2fc86f532aa64053f8212f8 Mon Sep 17 00:00:00 2001 From: Naruto TAKAHASHI Date: Wed, 15 Jul 2026 19:11:16 +0900 Subject: [PATCH 1/3] fix: emit animation_finished only when all loops complete `ss_runtime_is_end_frame_reached` means "the last frame of a cycle is showing", not "playback finished". With a finite loop count the runtime raises it once per cycle, and under the default infinite count it is never raised at all -- so wiring `animation_finished` straight to it fired the signal once per loop iteration. `set_loop_count(3)` emitted three `animation_finished` instead of `looped, looped, finished`. Switch to `ss_runtime_is_finished`, the terminal signal added upstream. It is a sticky state rather than a pulse; the existing `is_playing` early-return is what keeps it from re-emitting, so note that dependency where it matters. `animation_looped` is unchanged -- it still fires on every wrap, including under an infinite loop count, which is where cycle hooks are most useful. Also corrects the docs, which claimed `0` plays once (it is a synonym for infinite) and that `animation_finished` is emitted for non-looping animations only. Verified headless against this SDK branch: set_loop_count(3) -> looped, looped, finished set_loop_count(1) -> finished set_loop_count(-1) -> looped, looped, ... (never finishes) set_loop_count(0) -> identical to -1 The SDK submodule is bumped to the branch of cri-middleware/SpriteStudio-SDK#307 so CI can build the runtime; it needs re-pointing at develop once that merges. --- docs/en/api/player.md | 6 +++--- docs/en/migration_from_v1.md | 2 +- docs/en/workflow/usage_scripting.md | 2 +- docs/ja/api/player.md | 6 +++--- docs/ja/migration_from_v1.md | 2 +- docs/ja/workflow/usage_scripting.md | 4 ++-- ss_player/SpriteStudio-SDK | 2 +- ss_player/ss_internal_player.cpp | 6 +++++- 8 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/en/api/player.md b/docs/en/api/player.md index 45c8b0a..0eeab6d 100644 --- a/docs/en/api/player.md +++ b/docs/en/api/player.md @@ -39,7 +39,7 @@ func _ready() -> void: * `set_frame_rate(fps: int)` / `get_frame_rate() -> int` * `set_animation_section(start: int, end: int)`: Limits the playback to a specific frame range. * `set_playback_direction(direction: int, style: int)`: Sets the playback direction and style. See the table below for values. -* `set_loop_count(count: int)` / `get_loop_count() -> int`: `-1` means infinite loop. `0` plays once with no repeat. `n` repeats `n` times. +* `set_loop_count(count: int)` / `get_loop_count() -> int`: `n` plays `n` cycles then stops (`1` plays once). `-1` means infinite loop (`0` is an alias for infinite). * `set_frame_skip_enabled(enabled: bool)` / `is_frame_skip_enabled() -> bool` (default: `true`) * `set_sub_frame_enabled(enabled: bool)` / `is_sub_frame_enabled() -> bool` (default: `false`) * `set_cellmap_texture(cellmap_name: String, texture: Texture2D)` / `get_cellmap_texture(cellmap_name: String) -> Texture2D` @@ -59,8 +59,8 @@ func _ready() -> void: | --- | --- | --- | | `animation_started` | `anim_name: String` | Playback starts | | `animation_changed` | `anim_name: String` | The animation name is changed | -| `animation_finished` | `anim_name: String` | Playback reaches the end (non-looping only) | -| `animation_looped` | `anim_name: String` | The animation loops back to the start | +| `animation_finished` | `anim_name: String` | Every configured loop has been played. Never emitted under an infinite loop | +| `animation_looped` | `anim_name: String` | The animation looped back to the start. Not emitted on the final cycle, which emits `animation_finished` instead | | `user_data` | `payload: Dictionary` | A "User Data" keyframe on the timeline is hit | | `signal_emitted` | `command: String, value: Dictionary` | A "Signal" keyframe on the timeline is hit | | `audio` | `payload: Dictionary` | An "Audio" keyframe on the timeline is hit | diff --git a/docs/en/migration_from_v1.md b/docs/en/migration_from_v1.md index 9fc7f42..4fd5c15 100644 --- a/docs/en/migration_from_v1.md +++ b/docs/en/migration_from_v1.md @@ -12,7 +12,7 @@ When migrating from v1.x to the current version, please note the following major - **Resource Assignment**: - Previously, you had to assign multiple files via `res_player.res_project = load("...sspj")` and `set_anime_pack("...ssae")`. In the current version, a `.ssab` file is generated per `.ssae` (Anime Pack). - Therefore, you simply need to set the `.ssab` containing the animation you want to play using `set_ssab_resource(load("...ssab"))`. -- **Loop Playback**: The previous `set_loop(bool)` method has been replaced with `set_loop_count(int)` (`-1` for infinite loop, `1` for play once / no loop, `0` for no playback). +- **Loop Playback**: The previous `set_loop(bool)` method has been replaced with `set_loop_count(int)` (`-1` for infinite loop, `1` for play once / no loop). - **Method Renames and Removals**: - `set_player_resource()` → `set_ssab_resource()` - `get_fps()` → `get_frame_rate()` diff --git a/docs/en/workflow/usage_scripting.md b/docs/en/workflow/usage_scripting.md index 52887a5..fab68a7 100644 --- a/docs/en/workflow/usage_scripting.md +++ b/docs/en/workflow/usage_scripting.md @@ -40,7 +40,7 @@ One of the most powerful features for Godot users is event linkage using "Signal * `animation_changed(anim_name)`: Emitted when the animation is changed. * `animation_started(anim_name)`: Emitted when animation playback starts. -* `animation_finished(anim_name)`: Emitted when animation playback finishes (for non-looping animations). +* `animation_finished(anim_name)`: Emitted once every configured loop has been played (never under an infinite loop). * `animation_looped(anim_name)`: Emitted when the animation loops and returns to the beginning. * `user_data(payload)`: Emitted when reaching a frame containing user data (events) configured in the animation. diff --git a/docs/ja/api/player.md b/docs/ja/api/player.md index dde36f3..7c171d2 100644 --- a/docs/ja/api/player.md +++ b/docs/ja/api/player.md @@ -39,7 +39,7 @@ func _ready() -> void: * `set_frame_rate(fps: int)` / `get_frame_rate() -> int` * `set_animation_section(start: int, end: int)`: 再生するフレーム区間を限定します。 * `set_playback_direction(direction: int, style: int)`: 再生方向と再生スタイルを指定します。値の意味は下表を参照してください。 -* `set_loop_count(count: int)` / `get_loop_count() -> int`: `-1` で無限ループ、`0` で1回だけ再生、`n` で `n` 回繰り返し。 +* `set_loop_count(count: int)` / `get_loop_count() -> int`: `n` で `n` 回再生して停止(`1` なら1回のみ)。`-1` で無限ループ(`0` も無限ループの別名)。 * `set_frame_skip_enabled(enabled: bool)` / `is_frame_skip_enabled() -> bool` (デフォルト: `true`) * `set_sub_frame_enabled(enabled: bool)` / `is_sub_frame_enabled() -> bool` (デフォルト: `false`) * `set_cellmap_texture(cellmap_name: String, texture: Texture2D)` / `get_cellmap_texture(cellmap_name: String) -> Texture2D` @@ -59,8 +59,8 @@ func _ready() -> void: | --- | --- | --- | | `animation_started` | `anim_name: String` | 再生が開始された時 | | `animation_changed` | `anim_name: String` | アニメーションが切り替わった時 | -| `animation_finished` | `anim_name: String` | 再生が終了した時(非ループ時のみ) | -| `animation_looped` | `anim_name: String` | ループして先頭に戻った時 | +| `animation_finished` | `anim_name: String` | 指定したループ回数をすべて再生し終えた時。無限ループでは発火しない | +| `animation_looped` | `anim_name: String` | 1周して先頭に戻った時。最終周では発火せず `animation_finished` になる | | `user_data` | `payload: Dictionary` | タイムライン上の「ユーザーデータ」キーに到達した時 | | `signal_emitted` | `command: String, value: Dictionary` | タイムライン上の「シグナル」キーに到達した時 | | `audio` | `payload: Dictionary` | タイムライン上の「オーディオ」キーに到達した時 | diff --git a/docs/ja/migration_from_v1.md b/docs/ja/migration_from_v1.md index f8042a6..534ba6e 100644 --- a/docs/ja/migration_from_v1.md +++ b/docs/ja/migration_from_v1.md @@ -12,7 +12,7 @@ v1.x から本バージョンへ移行する場合は、以下の大きな変更 - **リソースの割り当て**: - 以前は `res_player.res_project = load("...sspj")` や `set_anime_pack("...ssae")` のように複数ファイルを設定していましたが、本バージョンでは `.ssab` が `.ssae` (アニメパック) 単位で生成されます。 - そのため、利用する(再生したい)アニメーションが含まれている `.ssab` を `set_ssab_resource(load("...ssab"))` のようにセットしてください。 -- **ループ再生の設定**: 以前の `set_loop(bool)` は廃止され、ループ回数を指定する `set_loop_count(int)` に変更されました(`-1` で無限ループ、`1` で1回再生(ループなし)、`0` は再生しません)。 +- **ループ再生の設定**: 以前の `set_loop(bool)` は廃止され、ループ回数を指定する `set_loop_count(int)` に変更されました(`-1` で無限ループ、`1` で1回再生(ループなし))。 - **メソッドの変更と廃止**: - `set_player_resource()` → `set_ssab_resource()` - `get_fps()` → `get_frame_rate()` diff --git a/docs/ja/workflow/usage_scripting.md b/docs/ja/workflow/usage_scripting.md index 57d7d79..d575db3 100644 --- a/docs/ja/workflow/usage_scripting.md +++ b/docs/ja/workflow/usage_scripting.md @@ -40,8 +40,8 @@ Godot ユーザーにとって最も強力な機能の一つが「シグナル ( * `animation_changed(anim_name)` : アニメーションが切り替わった時 * `animation_started(anim_name)` : アニメーションの再生が開始された時 -* `animation_finished(anim_name)`: アニメーションの再生が終了した時(非ループ時) -* `animation_looped(anim_name)` : アニメーションがループして先頭に戻った時 +* `animation_finished(anim_name)`: 指定したループ回数をすべて再生し終えた時(無限ループでは発火しません) +* `animation_looped(anim_name)` : アニメーションが1周して先頭に戻った時 * `user_data(payload)` : アニメーションに設定されたユーザーデータ(イベント)のフレームに到達した時 ### 実装例: アニメーションの連続再生 diff --git a/ss_player/SpriteStudio-SDK b/ss_player/SpriteStudio-SDK index 81d53be..7737f5b 160000 --- a/ss_player/SpriteStudio-SDK +++ b/ss_player/SpriteStudio-SDK @@ -1 +1 @@ -Subproject commit 81d53be389e403ab774b2be27bbf70ba5de75c91 +Subproject commit 7737f5bf96c1327eebb18a0d5f276dd540e3d62b diff --git a/ss_player/ss_internal_player.cpp b/ss_player/ss_internal_player.cpp index 18bc828..94b2a78 100644 --- a/ss_player/ss_internal_player.cpp +++ b/ss_player/ss_internal_player.cpp @@ -700,11 +700,15 @@ void SsInternalPlayer::update(float delta_seconds) { auto d = delta_seconds * 1000.0f; float frame_no = ss_runtime_update(runtime_ctx, d); + // `is_looped` is a pulse the runtime clears on entry to every update, so + // reading it right after the tick is what catches it. `is_finished` is a + // sticky state instead; the `is_playing` early-return above is what keeps + // it from re-emitting, since only play() clears it. const bool was_looped = ss_runtime_is_looped(runtime_ctx); if (was_looped) { if (_event_sink) _event_sink->onAnimationLooped(_strAnimationSelected); } - if (ss_runtime_is_end_frame_reached(runtime_ctx)) { + if (ss_runtime_is_finished(runtime_ctx)) { if (_event_sink) _event_sink->onAnimationFinished(_strAnimationSelected); } From bf7103b5cccb677531123831e4471a60f32656b4 Mon Sep 17 00:00:00 2001 From: Naruto TAKAHASHI Date: Wed, 15 Jul 2026 19:28:43 +0900 Subject: [PATCH 2/3] update ss-sdk submodule --- ss_player/SpriteStudio-SDK | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ss_player/SpriteStudio-SDK b/ss_player/SpriteStudio-SDK index 7737f5b..c590d7d 160000 --- a/ss_player/SpriteStudio-SDK +++ b/ss_player/SpriteStudio-SDK @@ -1 +1 @@ -Subproject commit 7737f5bf96c1327eebb18a0d5f276dd540e3d62b +Subproject commit c590d7de6e777b1956e1d0f3c6cde624ff412ba7 From 719dd1782cf3bb9c11320068b48ae37b78a978bd Mon Sep 17 00:00:00 2001 From: Naruto TAKAHASHI Date: Wed, 15 Jul 2026 19:54:07 +0900 Subject: [PATCH 3/3] chore: point the SDK submodule at the tip of SpriteStudio-SDK#307 Picks up the end-frame event-duplication fix, so CI builds the runtime this PR is actually written against. Still re-point at the default branch once #307 lands. --- ss_player/SpriteStudio-SDK | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ss_player/SpriteStudio-SDK b/ss_player/SpriteStudio-SDK index c590d7d..dc9b987 160000 --- a/ss_player/SpriteStudio-SDK +++ b/ss_player/SpriteStudio-SDK @@ -1 +1 @@ -Subproject commit c590d7de6e777b1956e1d0f3c6cde624ff412ba7 +Subproject commit dc9b9876d81dd129d891d2791f3626d7c4123003