diff --git a/docs/en/api/player.md b/docs/en/api/player.md index 1209a8b..9d0662a 100644 --- a/docs/en/api/player.md +++ b/docs/en/api/player.md @@ -55,11 +55,13 @@ func _ready() -> void: ## Part queries -* `get_part_names() -> PackedStringArray`: Every part name in the current animation. +* `get_part_names() -> PackedStringArray`: Every part name in the asset (`.ssab`). Parts do not depend on the animation, so the list is the same for every animation in that asset. * `get_part_index(part_name: String) -> int`: Resolves a part name to its part index, or `-1` if it does not exist. * `get_part_transform(part_name: String) -> Transform2D`: The part's `Transform2D` on the current frame, in the player node's local space (`flip_h` / `flip_v` / `offset` included). Returns the identity when the part is unknown. * `is_part_hidden(part_name: String) -> bool`: Whether the part is hidden on the current frame. +See [Scripting and Event-Driven Control → Part Tracking](../workflow/usage_scripting.md) for `SpriteStudioPartAttachment2D`, the node that makes another node follow a specified part. + ## Part overrides Override a single part's color / cell / visibility so that it wins over the keyframes. Every method returns `true` on success, or `false` when the part is unknown or the runtime rejects the call. See [Scripting and Event-Driven Control → Part Overrides](../workflow/usage_scripting.md) for the details and caveats. @@ -99,6 +101,7 @@ Override a single part's color / cell / visibility so that it wins over the keyf | `animation_changed` | `anim_name: String` | The animation name is changed | | `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 | +| `frame_updated` | `frame_no: float` | The frame's part poses have just been finalized (right after the player's update, before the render phase). Which process it fires in follows `animation_process_mode` | | `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/workflow/usage_scripting.md b/docs/en/workflow/usage_scripting.md index 8a3c223..c1d6050 100644 --- a/docs/en/workflow/usage_scripting.md +++ b/docs/en/workflow/usage_scripting.md @@ -104,6 +104,79 @@ This feature allows you to build an efficient avatar system without needing to p --- +## Part Tracking (Following a Specified Part) + +A feature that makes a user-provided node (weapon, effect, hit detection, etc.) follow a specified part every frame. The player neither creates nor frees nodes; it only writes the pose (constraint style), and you own the target's lifecycle. + +Tracking is done with the dedicated **`SpriteStudioPartAttachment2D`** node. Place it as a child of `SpriteStudioPlayer2D` and set `part_name` to the part you want to follow. Anything you hang under that node — a weapon, an effect — follows along through scene-tree inheritance. + +> **The properties follow Godot's own `RemoteTransform2D`**, plus `follow_path` / `part_name` to say which player and which part to read. + +| Property | Description | +|---|---| +| **Part Name** (`part_name`) | The name of the part to follow (the PartData name in the `.ssab`). The inspector offers a dropdown populated from the asset's part names (still typable, for when the player cannot be resolved) | +| **Follow Path** (`follow_path`) | The `SpriteStudioPlayer2D` to read from. Empty (default) uses the **nearest ancestor** player | +| **Remote Path** (`remote_path`) | The `Node2D` to drive. Empty (default) drives this node itself, and its children follow through scene-tree inheritance. Set it to push the pose to an external node instead (for assets that live outside the player's subtree) | +| **Use Global Coordinates** (`use_global_coordinates`) | ON (default) writes the pose in global coordinates, OFF in the target's local coordinates | +| **Update Position / Update Rotation** (`update_position` / `update_rotation`) | Reflect position / rotation (both ON by default) | +| **Update Scale** (`update_scale`) | Reflect scale (OFF by default) | +| **On Part Hidden** (`on_part_hidden`) | Behavior on frames where the part is hidden. `Follow Always` (keep following; default) / `Hide Target` (hide the target) | + +### Querying from a Script + +Instead of placing a node, you can also ask the player for a part's pose directly. + +```gdscript +@onready var ss_player = $SpriteStudioPlayer2D +@onready var muzzle = $Muzzle + +func _ready(): + print(ss_player.get_part_names()) # -> ["root", "body", "hand_R", ...] + + # Emitted every time the frame's part poses are finalized + ss_player.frame_updated.connect(_on_frame_updated) + +func _on_frame_updated(frame_no: float): + # get_part_transform() is player-local; multiply by the player's transform for global + muzzle.global_transform = ss_player.global_transform * ss_player.get_part_transform("hand_R") +``` + +| API | Description | +|---|---| +| `get_part_names()` | Every part name in the asset (`.ssab`) | +| `get_part_index(part_name)` | Part index, or `-1` if the part is not in the asset | +| `get_part_transform(part_name)` | The part's transform for the current frame (a `Transform2D`, player-local, with `flip_h` / `flip_v` / `offset` already applied). Identity if the part is unknown | +| `is_part_hidden(part_name)` | Whether the part is hidden on the current frame. `false` if the part is unknown | +| signal `frame_updated(frame_no: float)` | Emitted right after the frame's part poses are finalized | + +> When you only need the pose at a single moment (a projectile spawn point, for example) rather than continuous following, calling `get_part_transform()` directly is simpler than placing a `SpriteStudioPartAttachment2D`. + +### Timing and Accuracy + +Tracking is driven by the `frame_updated` signal the player emits right after finishing its own update. That is after the part transforms are finalized and before the render phase, so the target updates **within the same frame**. Which process it fires in follows the player's `animation_process_mode` (`Idle` (default) / `Physics`). + +Godot's `Transform2D` holds a full 2x3 affine transform, so when `update_position` / `update_rotation` / `update_scale` are **all ON** the transform is assigned whole. That matches the part **exactly, including skew and negative scale**, whether the target sits under the player or in a separate hierarchy. + +Turning any of them OFF writes only the enabled components individually, like `RemoteTransform2D`, and skew is not preserved. Only `update_scale` is OFF by default, so **position and rotation alone are reflected out of the box**. + +> **A target in a separate hierarchy can lag by one frame.** The pose is written using the player's `global_transform` as sampled at drive time, so if you move the player afterwards, the target does not follow until the next frame. A `SpriteStudioPartAttachment2D` (and its children) placed under the player always follows, through hierarchy inheritance. + +> **Do not track with a `RigidBody2D`.** Overwriting its transform every frame reads as a teleport to the solver and breaks the physics. If you need to push other bodies — a moving platform, say — target Godot's `AnimatableBody2D` (with `sync_to_physics` ON) and set the player's `animation_process_mode` to `Physics` so tracking is driven on the physics frame. To merely carry a hit box, `Area2D` / `StaticBody2D` is enough. + +### Notes + +- **The attachment controls the target's `visible`.** It is hidden automatically in the two cases below, and shown again automatically once the condition clears, so a visibility state you set yourself may be overwritten. + - The part name does not exist in the asset (always hidden, regardless of the `On Part Hidden` setting) + - The part is hidden on this frame and `On Part Hidden` is `Hide Target` +- Part names resolve against the parts of the `.ssab` the player itself has loaded. **Parts inside an Instance part (the child animation) cannot be specified** (the Instance part itself can). +- Part names resolve per asset (`.ssab`), independent of the animation. Swapping the `.ssab` re-resolves them automatically, so nothing has to be set up again. +- If several parts share a name, the first one found is used. +- Only the **spatial transform** is tracked. Draw order (Z order) is not, so a tracked node is never slotted automatically *between* SpriteStudio parts. Use `z_index` or similar when you need a specific ordering. +- Targets must be `Node2D`-based nodes. `Control` (UI) is laid out by anchors and rects and cannot be targeted. +- In the editor, tracking is applied as well whenever the player updates — during preview playback or while scrubbing frames. + +--- + ## Part Overrides (Color / Cell / Visibility) Per-part runtime overrides let a script say "make this part this color / this cell / hidden **now**". An override wins over both the keyframe and any animation blending, so it does not have to fight the animation. diff --git a/docs/ja/api/player.md b/docs/ja/api/player.md index 918db77..574b768 100644 --- a/docs/ja/api/player.md +++ b/docs/ja/api/player.md @@ -55,11 +55,13 @@ func _ready() -> void: ## パーツの参照 -* `get_part_names() -> PackedStringArray`: 現在のアニメーションに含まれる全パーツ名。 +* `get_part_names() -> PackedStringArray`: アセット(`.ssab`)に含まれる全パーツ名。パーツはアニメーションに依存しないため、同じアセット内のどのアニメーションでも同じ一覧になります。 * `get_part_index(part_name: String) -> int`: パーツ名をパーツインデックスへ解決します。存在しない場合は `-1`。 * `get_part_transform(part_name: String) -> Transform2D`: 現在のフレームでのパーツの `Transform2D`(プレイヤーノードのローカル空間。`flip_h` / `flip_v` / `offset` を含みます)。パーツが不明な場合は単位行列を返します。 * `is_part_hidden(part_name: String) -> bool`: 現在のフレームでそのパーツが非表示かどうか。 +指定したパーツにノードを追従させる `SpriteStudioPartAttachment2D` については [スクリプト制御とイベント駆動 → パーツトラッキング](../workflow/usage_scripting.md) を参照してください。 + ## パーツオーバーライド パーツ単位で、カラー / セル / 表示指定をキーフレームより優先して上書きします。各メソッドは成功時に `true`、パーツが不明な場合やランタイムが受け付けなかった場合に `false` を返します。詳細と注意点は [スクリプト制御とイベント → パーツオーバーライド](../workflow/usage_scripting.md) を参照してください。 @@ -99,6 +101,7 @@ func _ready() -> void: | `animation_changed` | `anim_name: String` | アニメーションが切り替わった時 | | `animation_finished` | `anim_name: String` | 指定したループ回数をすべて再生し終えた時。無限ループでは発火しない | | `animation_looped` | `anim_name: String` | 1周して先頭に戻った時。最終周では発火せず `animation_finished` になる | +| `frame_updated` | `frame_no: float` | そのフレームのパーツ姿勢が確定した直後(プレーヤの更新直後、描画の前)。発火するプロセスは `animation_process_mode` に従う | | `user_data` | `payload: Dictionary` | タイムライン上の「ユーザーデータ」キーに到達した時 | | `signal_emitted` | `command: String, value: Dictionary` | タイムライン上の「シグナル」キーに到達した時 | | `audio` | `payload: Dictionary` | タイムライン上の「オーディオ」キーに到達した時 | diff --git a/docs/ja/workflow/usage_scripting.md b/docs/ja/workflow/usage_scripting.md index 33a2a84..3537632 100644 --- a/docs/ja/workflow/usage_scripting.md +++ b/docs/ja/workflow/usage_scripting.md @@ -104,6 +104,79 @@ func change_costume(): --- +## パーツトラッキング(指定パーツへの追従) + +指定したパーツに、ユーザーが用意したノード(武器・エフェクト・当たり判定など)を毎フレーム追従させる機能です。プレーヤはノードを生成も破棄もせず、姿勢を書き込むだけの連動(Constraint)方式で、対象のライフサイクルはユーザーが所有します。 + +追従には専用ノード **`SpriteStudioPartAttachment2D`** を使います。`SpriteStudioPlayer2D` の子として置き、`part_name` に追従したいパーツ名を指定してください。このノードの下に武器やエフェクトをぶら下げれば、シーンツリーの継承でまとめて追従します。 + +> **プロパティは Godot 標準の `RemoteTransform2D` を踏襲**しています。これに「どのプレーヤの、どのパーツに追従するか」を指定する `follow_path` / `part_name` が加わった形です。 + +| プロパティ | 説明 | +|---|---| +| **Part Name** (`part_name`) | 追従対象のパーツ名(`.ssab` の PartData 名)。インスペクタではアセットのパーツ名からドロップダウンで選べます(プレーヤを解決できない場面のために手入力も可) | +| **Follow Path** (`follow_path`) | 追従元の `SpriteStudioPlayer2D`。空(既定)なら**最も近い祖先**のプレーヤを使います | +| **Remote Path** (`remote_path`) | 駆動する対象の `Node2D`。空(既定)なら自分自身を動かし、子はシーンツリーの継承で追従します。指定するとその外部ノードへ姿勢を書き込みます(プレーヤのサブツリーの外に置いた資産を追従させたい場合) | +| **Use Global Coordinates** (`use_global_coordinates`) | ON(既定)でグローバル座標として、OFF で対象のローカル座標として書き込みます | +| **Update Position / Update Rotation** (`update_position` / `update_rotation`) | 位置 / 回転を反映します(ともに既定 ON) | +| **Update Scale** (`update_scale`) | スケールを反映します(既定 OFF) | +| **On Part Hidden** (`on_part_hidden`) | パーツが hide のフレームでの挙動。`Follow Always`(追従を継続。既定)/ `Hide Target`(対象を非表示) | + +### スクリプトからの参照 + +ノードを置かずに、プレーヤへ直接パーツの姿勢を問い合わせることもできます。 + +```gdscript +@onready var ss_player = $SpriteStudioPlayer2D +@onready var muzzle = $Muzzle + +func _ready(): + print(ss_player.get_part_names()) # → ["root", "body", "hand_R", ...] + + # そのフレームのパーツ姿勢が確定するたびに通知される + ss_player.frame_updated.connect(_on_frame_updated) + +func _on_frame_updated(frame_no: float): + # get_part_transform() はプレーヤローカル。グローバルにするならプレーヤの変換を掛ける + muzzle.global_transform = ss_player.global_transform * ss_player.get_part_transform("hand_R") +``` + +| API | 説明 | +|---|---| +| `get_part_names()` | アセット(`.ssab`)に含まれる全パーツ名 | +| `get_part_index(part_name)` | パーツインデックス。アセットに無ければ `-1` | +| `get_part_transform(part_name)` | そのパーツの現在フレームの変換(`Transform2D`。プレーヤローカルで、`flip_h` / `flip_v` / `offset` を適用済み)。パーツが不明なら単位行列 | +| `is_part_hidden(part_name)` | そのパーツが現在フレームで hide かどうか。パーツが不明なら `false` | +| signal `frame_updated(frame_no: float)` | そのフレームのパーツ姿勢が確定した直後に発火する | + +> **一瞬の姿勢だけが要る場合**: 弾の発射位置を取るなど、常時追従させるまでもない場合は、`SpriteStudioPartAttachment2D` を置かずに `get_part_transform()` を直接呼ぶ方が簡潔です。 + +### 追従のタイミングと精度 + +追従は、プレーヤが自身の更新を終えた直後に発行する `frame_updated` シグナルで駆動されます。パーツの変換が確定した後・描画の前なので、追従先は**同じフレーム内**で更新されます。どのプロセスで発火するかは、プレーヤの `animation_process_mode`(`Idle`(既定)/ `Physics`)に従います。 + +Godot の `Transform2D` は 2×3 のアフィン変換をそのまま保持できるため、`update_position` / `update_rotation` / `update_scale` が**すべて ON** のときは変換を丸ごと代入します。この場合は**せん断(Skew)や負のスケールも含めて厳密に一致**し、対象をプレーヤの子に置いても別階層に置いても差はありません。 + +1 つでも OFF にすると、`RemoteTransform2D` と同じく有効な成分だけを個別に書き込むため、せん断は保持されません。既定は `update_scale` のみ OFF なので、**既定では位置と回転だけが反映されます**。 + +> **別階層の対象は 1 フレーム遅れることがあります。** 姿勢は駆動時点のプレーヤの `global_transform` を使って書き込むため、その後にプレーヤ自身を動かしても、対象が追随するのは次のフレームです。`SpriteStudioPartAttachment2D`(とその子)をプレーヤの子に置いた場合は、シーンツリーの継承で常に追随します。 + +> **`RigidBody2D` を追従対象にしないでください。** 毎フレーム transform を直接書き換えるとソルバがテレポートとして扱い、物理が破綻します。動く床のように他の剛体を押す必要がある場合は、Godot 標準の `AnimatableBody2D`(`sync_to_physics` を ON)を対象にし、プレーヤの `animation_process_mode` を `Physics` にして物理フレームで駆動してください。当たり判定を運ぶだけなら `Area2D` / `StaticBody2D` で十分です。 + +### 注意点 + +- **対象の `visible` はアタッチメントが操作します。** 次の 2 つの場合に自動で非表示になり、条件が解消すると自動で再表示されます。ユーザー側で設定した表示状態は上書きされることがあります。 + - パーツ名がアセットに存在しない(この場合は `On Part Hidden` の設定に関係なく常に非表示) + - パーツが hide のフレームで、かつ `On Part Hidden` が `Hide Target` +- パーツ名は、そのプレーヤ自身が読み込んでいる `.ssab` のパーツから解決されます。**インスタンスパーツの内部(子アニメーション)のパーツは指定できません**(インスタンスパーツ自体は指定できます)。 +- パーツ名の解決はアセット(`.ssab`)単位で、アニメーションには依存しません。`.ssab` を差し替えると自動で解決し直されるため、設定をやり直す必要はありません。 +- 同名のパーツが複数ある場合は、最初に見つかったものが使われます。 +- 追従するのは**空間的な変換だけ**です。描画順(Z 順)は追従しないため、追従させたノードが SpriteStudio のパーツの「間」に自動で挟まることはありません。前後関係が必要な場合は `z_index` などで別途調整してください。 +- 対象にできるのは `Node2D` 系のノードです。`Control`(UI)はアンカーと矩形でレイアウトされるため対象にできません。 +- 編集モードでも、プレビュー再生やフレームのスクラブでプレーヤが更新されれば、そのタイミングで追従します。 + +--- + ## パーツオーバーライド(パーツカラー / セル / 表示指定) パーツ単位のランタイムオーバーライドは、「このパーツを**今**この色に / このセルに / 非表示に」とスクリプトから指示する機能です。オーバーライドはキーフレームやアニメーションのブレンドよりも優先されるので、アニメーションと取り合いになりません。