spine-cランタイム ドキュメント

ライセンスについて

Spineランタイムをアプリケーションに組み込む前に、必ずSpine Runtimes Licenseを確認してください。

はじめに

spine-cは、C、C++、Swift、Rust、Objective-Cなど、C言語とネイティブに連携できる言語で書かれたゲームエンジンやフレームワークにSpineアニメーションを統合するための汎用ランタイムです。

spine-cは、以下の機能を提供します。

spine-cランタイムはエンジンに依存しない汎用ランタイムです。ユーザー自身が、テクスチャローディング用のコールバックを用意し、生成された描画コマンドをお使いのエンジンのレンダリングシステムへ渡す形で利用します。

spine-cランタイムは最大限の互換性を確保するために C99 で記述されています。APIはspine-cppから自動生成されており、完全性と型安全性が保証されています。

統合例:

注意: 当ガイドでは、Spineで使用される基本的なランタイムのアーキテクチャと用語を理解されていることを前提としています。ランタイムのより高度な機能については、APIリファレンスも参照してください。

spine-cをエンジンに統合する

CMakeを使った統合(推奨)

spine-c をプロジェクトに統合する最も簡単な方法は、以下のようにCMake のFetchContentを使用することです:

cmake
include(FetchContent)
FetchContent_Declare(
   spine-c
   GIT_REPOSITORY https://github.com/esotericsoftware/spine-runtimes.git
   GIT_TAG 4.3
   SOURCE_SUBDIR spine-c
)
FetchContent_MakeAvailable(spine-c)

# spine-c をリンク
target_link_libraries(your_target spine-c)

この方法では、依存関係である spine-cpp とともに、spine-c が自動的に取得およびビルドされます。

コード内で spine-c のヘッダーをインクルードしてください:

c
#include <spine-c.h>

手動での統合

手動で統合したい場合は、以下の手順に従ってください:

  1. Spineランタイムのソースを、gitを使用してダウンロード (git clone https://github.com/esotericsoftware/spine-runtimes) またはzipファイルとしてダウンロードします。
  2. 必要なソースファイルをプロジェクトに追加します:
    • spine-cpp/src および spine-c/src 内のソースを追加してください

  3. 以下のインクルードディレクトリを追加します: spine-cpp/includespine-c/include

コード内で spine-c のヘッダーをインクルードしてください:

c
#include <spine-c.h>

Spineアセットのspine-c向けエクスポート

以下の実行方法については、Spineユーザーガイド内で紹介されています :

  1. JSONまたはバイナリ形式でのスケルトン&アニメーションデータのエクスポート
  2. スケルトンの画像を含むテクスチャアトラスのエクスポート

スケルトンデータとテクスチャアトラスをエクスポートすると、以下のファイルが得られます :

  1. skeleton-name.jsonまたはskeleton-name.skel: これはスケルトンとアニメーションのデータを含んでいます。
  2. skeleton-name.atlas: これはテクスチャアトラスの情報を含んでいます。
  3. 1つまたは複数の.pngファイル: これはテクスチャアトラスの各ページで、スケルトンが使用するイメージを含んでいます。

注意: スケルトンごとに1つのテクスチャアトラスを作成するのではなく、複数のスケルトンの画像を1つのテクスチャアトラスにパックすることもできます。詳しくはテクスチャパッキングのガイドをご覧ください。

Spineアセットのロード

spine-c は、テクスチャアトラス、Spine スケルトンデータ(ボーン、スロット、アタッチメント、スキン、アニメーション)を読み込むための API を提供しています。また、AnimationStateDataによってアニメーション間のミックスタイムを定義することもできます。この3種類のデータはセットアップポーズデータとも呼ばれ、通常、一度ロードされた後はすべてのゲームオブジェクトで共有されます。この共有の仕組みは、各ゲームオブジェクトに独自のスケルトンとAnimationStateを与えることで実現されます。

補足: ローディングの全体的なアーキテクチャの詳細については、全般的なSpineランタイムドキュメントを参照してください。

テクスチャアトラスのロード

テクスチャアトラスデータは、アトラスページ内の個々の画像の位置を記述した独自のアトラス形式で保存されます。アトラスページ自体はアトラスとは別にプレーンな.pngファイルとして保存されます。

spine-c では、アトラスをロードするために2つの方法が用意されています。

方法1:テクスチャをロードせずにアトラスをロード

spine_atlas_load を使用すると、テクスチャをロードせずにアトラスデータのみをパースできます。この場合、各アトラスページに対応するテクスチャは手動でロードする必要があります:

c
// まず、.atlas ファイルの内容を文字列としてロード
char* atlasData = read_file_to_string("path/to/skeleton.atlas");

// アトラスデータをパース(テクスチャはロードされません)
spine_atlas_result result = spine_atlas_load(atlasData);

// エラーチェック
if (spine_atlas_result_get_error(result)) {
   printf("Error loading atlas: %s\n", spine_atlas_result_get_error(result));
   spine_atlas_result_dispose(result);
   exit(1);
}

spine_atlas atlas = spine_atlas_result_get_atlas(result);
spine_atlas_result_dispose(result);

// 手動でのテクスチャローディング: spine_atlas_load はテクスチャポインタではなくページインデックスを設定します
// 各ページごとにテクスチャをロードして、該当する領域に設定する必要があります
spine_array_atlas_page pages = spine_atlas_get_pages(atlas);
int num_pages = spine_array_atlas_page_size(pages);

// 各ページ用のテクスチャをロード
void** page_textures = malloc(num_pages * sizeof(void*));
spine_atlas_page* pages_buffer = spine_array_atlas_page_buffer(pages);
for (int i = 0; i < num_pages; i++) {
   spine_atlas_page page = pages_buffer[i];

   // アトラスからテクスチャファイル名を取得
   const char* texture_name = spine_atlas_page_get_texture_path(page);

   // フルパスを構築(テクスチャの配置場所を把握している必要があります)
   char full_path[256];
   snprintf(full_path, sizeof(full_path), "%s/%s", atlas_dir, texture_name);

   // 使用しているエンジンでテクスチャをロード
   page_textures[i] = engine_load_texture(full_path);
}

// 各領域に対し、対応するページのテクスチャをレンダラーオブジェクトとして設定
spine_array_atlas_region regions = spine_atlas_get_regions(atlas);
int num_regions = spine_array_atlas_region_size(regions);
spine_atlas_region* regions_buffer = spine_array_atlas_region_buffer(regions);

for (int i = 0; i < num_regions; i++) {
   spine_atlas_region region = regions_buffer[i];
   spine_atlas_page page = spine_atlas_region_get_page(region);

   // spine_atlas_load がページの texture フィールドにページインデックスを格納
   int page_index = (int)(intptr_t)spine_atlas_page_get_texture(page);

   // 実際のテクスチャをその領域のレンダラーオブジェクトとして設定
   spine_atlas_region_set_renderer_object(region, page_textures[page_index]);
}

free(page_textures);

方法2:テクスチャローディング用コールバックを指定する

spine_atlas_load_callbackを使用して、アトラスのパース時にテクスチャを自動的にロードすることができます:

c
// 使用しているエンジンのテクスチャシステム用コールバックを定義
void* my_load_texture(const char* path) {
   // path はフルパス: atlas_dir + "/" + texture_name
   // 例:"path/to/atlas/dir/skeleton.png"
   return engine_load_texture(path);
}

void my_unload_texture(void* texture) {
   engine_unload_texture(texture);
}

// まず、.atlas ファイルの内容を文字列としてロード
char* atlasData = read_file_to_string("path/to/skeleton.atlas");

// テクスチャを自動ロードしながらアトラスをロード
spine_atlas_result result = spine_atlas_load_callback(
   atlasData,          // アトラスファイルの内容(文字列)
   "path/to/atlas/dir",   // テクスチャファイルが配置されているディレクトリ
   my_load_texture,      // テクスチャロード関数
   my_unload_texture    // テクスチャアンロード関数
);

// エラーチェック
if (spine_atlas_result_get_error(result)) {
   printf("Error loading atlas: %s\n", spine_atlas_result_get_error(result));
   spine_atlas_result_dispose(result);
   exit(1);
}

spine_atlas atlas = spine_atlas_result_get_atlas(result);
spine_atlas_result_dispose(result);

スケルトンデータのロード

スケルトンデータ(ボーン、スロット、アタッチメント、スキン、アニメーション)は、ヒューマン・リーダブル(対人可読形式)なJSON形式、または独自のバイナリ形式でエクスポートできます。spine-c では、スケルトンデータは spine_skeleton_data 構造体として保持されます。

JSON形式のスケルトンデータのロード例:

c
// まず、スケルトンのJSONファイル内容を文字列としてロード
char* jsonString = read_file_to_string("path/to/skeleton.json");

// JSONからスケルトンデータをロード
spine_skeleton_data_result json_result = spine_skeleton_data_load_json(
   atlas,       // 事前にロードしたアトラス
   jsonString,    // JSONファイルの内容(文字列)
   "skeleton.json" // エラー表示用のパス
);

// エラーチェック
if (spine_skeleton_data_result_get_error(json_result)) {
   printf("Error loading skeleton: %s\n", spine_skeleton_data_result_get_error(json_result));
   spine_skeleton_data_result_dispose(json_result);
   exit(1);
}

// 結果からスケルトンデータを取得
spine_skeleton_data skeleton_data = spine_skeleton_data_result_get_data(json_result);

// 結果オブジェクトを破棄(スケルトンデータ自体は保持されます)
spine_skeleton_data_result_dispose(json_result);

バイナリ形式のスケルトンデータのロード例:

c
// まず、スケルトンのバイナリファイルをメモリにロード
uint8_t* binaryData = read_file_to_bytes("path/to/skeleton.skel", &dataLength);

// バイナリからスケルトンデータをロード
spine_skeleton_data_result binary_result = spine_skeleton_data_load_binary(
   atlas,       // 事前にロードしたアトラス
   binaryData,    // uint8_t 配列としてのバイナリデータ
   dataLength,    // バイナリデータのサイズ
   "skeleton.skel" // エラー表示用のパス
);

// エラーチェックおよびスケルトンデータの取得(JSON形式の場合と同様)
if (spine_skeleton_data_result_get_error(binary_result)) {
   printf("Error loading skeleton: %s\n", spine_skeleton_data_result_get_error(binary_result));
   spine_skeleton_data_result_dispose(binary_result);
   exit(1);
}

spine_skeleton_data skeleton_data = spine_skeleton_data_result_get_data(binary_result);
spine_skeleton_data_result_dispose(binary_result);

補足: 一般的に、バイナリ形式の方がJSONよりもデータサイズが小さくロードも高速なため、製品版ではこちらの使用が推奨されます。

アニメーションステートデータの準備

Spineは、あるアニメーションから別のアニメーションに切り替える際のスムーズなトランジション(クロスフェード)に対応しています。このクロスフェードは、あるアニメーションと別のアニメーションを指定したミックスタイムの間だけミックスすることで実現されています。spine-c では、これらのミックスタイムを定義するために spine_animation_state_data 構造体が用意されています:

c
// アニメーションステートデータを作成
spine_animation_state_data anim_state_data = spine_animation_state_data_create(skeleton_data);

// 任意のアニメーションのペア間のデフォルトのミックスタイムを秒単位で設定する
spine_animation_state_data_set_default_mix(anim_state_data, 0.1f);

// 特定のアニメーション間のミックスタイムを設定し、デフォルト設定を上書きする
spine_animation_state_data_set_mix_1(anim_state_data, "jump", "walk", 0.2f);

spine_animation_state_dataで定義されたミックスタイムは、アニメーションの適用時に明示的に上書きすることも可能です(後述)。

スケルトン

セットアップポーズデータ(スケルトンデータ、テクスチャアトラス)は、各ゲームオブジェクト間で共有されることが前提になっています。各ゲームオブジェクトは、共有された spine_skeleton_dataspine_atlas を参照する独自の spine_skeleton インスタンスを持ちます。

スケルトンは自由に変更できます(プロシージャルなボーン操作、アニメーション、アタッチメント、スキンなど)が、基となるデータは変更されません。これにより、任意の数のゲームオブジェクト間で効率的にデータを共有できます。

スケルトンの作成

c
spine_skeleton skeleton = spine_skeleton_create(skeleton_data);

各ゲームオブジェクトには、それぞれ専用のスケルトンインスタンスが必要です。一方で、大部分のデータは共有されたままとなるため、メモリ使用量やテクスチャ切り替えの回数を抑えることができます。

*注意:** スケルトンが不要になった時点で、spine_skeleton_dispose(skeleton) を呼び出して明示的に破棄する必要があります。

ボーン

スケルトンはボーンによる階層構造で、ボーンにスロットが、スロットにアタッチメントが取り付けられています。

ボーンを探す

スケルトン内のすべてのボーンには一意の名前が付けられています:

c
// 指定した名前のボーンが存在しない場合は NULL を返す
spine_bone bone = spine_skeleton_find_bone(skeleton, "mybone");

ローカルトランスフォーム

ボーンは、親ボーンの影響を受けながら、ルートボーンに至るまで階層的に連結されています。ボーンが親ボーンからどのように変形を継承するかは、トランスフォームの継承の設定によって制御されます。各ボーンは、親に対する相対的なローカルトランスフォームを持っており、以下の要素で構成されます:

  • 親を基準としたxy座標
  • 度単位のrotation(回転)
  • scaleX(スケールX)scaleY(スケールY)
  • 度単位のshearX(シアーX)shearY(シアーY)

ローカルトランスフォームは、ボーンのポーズ(spine_bone_local)を通してアクセスします:

c
spine_bone bone = spine_skeleton_find_bone(skeleton, "mybone");
spine_bone_local pose = spine_bone_get_pose(bone);

// ローカルトランスフォームの取得
float x = spine_bone_local_get_x(pose);
float y = spine_bone_local_get_y(pose);
float rotation = spine_bone_local_get_rotation(pose);
float scaleX = spine_bone_local_get_scale_x(pose);
float scaleY = spine_bone_local_get_scale_y(pose);
float shearX = spine_bone_local_get_shear_x(pose);
float shearY = spine_bone_local_get_shear_y(pose);

// ローカルトランスフォームの変更
spine_bone_local_set_position(pose, 100, 50);
spine_bone_local_set_rotation(pose, 45);
spine_bone_local_set_scale_1(pose, 2, 2);

ローカルトランスフォームは、プロシージャルな操作によっても、アニメーションによっても変更できます。これらは同時に適用することができ、その合成結果がポーズに反映されます。

ワールドトランスフォーム

ボーンのローカルトランスフォームをプロシージャルに変更するか、アニメーションを適用するかによって設定した後、レンダリングや物理演算のために、各ボーンのワールドトランスフォームを取得する必要があります。

この計算はrootボーンから始まり、すべての子ボーンのワールドトランスフォームを再起的に計算します。この計算では、アーティストがSpineエディター内で定義したIKトランスフォームパススライダー などのコンストレイントも適用されます。

ワールドトランスフォームを計算するには、次のようにします:

c
spine_skeleton_update(skeleton, deltaTime);
spine_skeleton_update_world_transform(skeleton, SPINE_PHYSICS_UPDATE);

deltaTime は現在のフレームと最後のフレームの間の経過時間を秒単位で指定します。第2引数は物理を適用するかどうかを指定します。一般的にデフォルト値は SPINE_PHYSICS_UPDATE にしておくのが良いでしょう。

ワールドトランスフォームは、ボーンの適用済みポーズ(spine_bone_pose)を通して参照します:

c
spine_bone_pose applied = spine_bone_get_applied_pose(bone);

// ワールドトランスフォーム行列の成分を取得
float a = spine_bone_pose_get_a(applied); // 2x2 行列の成分で、
float b = spine_bone_pose_get_b(applied); // 回転・スケール
float c = spine_bone_pose_get_c(applied); // ・シアーを表す
float d = spine_bone_pose_get_d(applied);

// ワールド座標を取得
float worldX = spine_bone_pose_get_world_x(applied);
float worldY = spine_bone_pose_get_world_y(applied);

なお、worldXworldY は、スケルトン自体の x および y 位置によるオフセットが加算されていることに注意してください。

ボーンのワールドトランスフォームは直接変更してはいけません。代わりに、spine_skeleton_update_world_transform を呼び出すことで、常にローカルトランスフォームから算出させる必要があります。

座標系の変換

spine-c には、座標系同士を変換するための様々な関数が用意されています。これらの関数は、あらかじめワールド変換が計算済みであることを前提としています:

c
spine_bone bone = spine_skeleton_find_bone(skeleton, "mybone");
spine_bone_pose applied = spine_bone_get_applied_pose(bone);

// ワールド回転とスケールを取得
float rotationX = spine_bone_pose_get_world_rotation_x(applied);
float rotationY = spine_bone_pose_get_world_rotation_y(applied);
float scaleX = spine_bone_pose_get_world_scale_x(applied);
float scaleY = spine_bone_pose_get_world_scale_y(applied);

// ワールド空間とローカル空間の変換
float localX, localY, worldX, worldY;
spine_bone_pose_world_to_local(applied, worldX, worldY, &localX, &localY);
spine_bone_pose_local_to_world(applied, localX, localY, &worldX, &worldY);

// 回転の変換
float localRotation = spine_bone_pose_world_to_local_rotation(applied, worldRotation);
float worldRotation = spine_bone_pose_local_to_world_rotation(applied, localRotation);

注意: ボーン(およびそのすべての子)のローカルトランスフォームへの変更は、次にspine_skeleton_update_world_transformを呼び出した後に、ボーンのワールドトランスフォームに反映されます。

ポジションの決定

デフォルトでは、スケルトンはワールド座標系の原点に配置されます。ゲームワールド内でスケルトンの位置を設定するには、以下のようにします:

c
// ゲームオブジェクトにスケルトンを追従させる
spine_skeleton_set_x(skeleton, myGameObject->worldX);
spine_skeleton_set_y(skeleton, myGameObject->worldY);

// まとめて設定する場合
spine_skeleton_set_position(skeleton, myGameObject->worldX, myGameObject->worldY);

注意: スケルトンの位置を変更した場合、その変更は spSkeleton_updateWorldTransform を呼び出した後に、各ボーンのワールド変換へ反映されます。

反転

スケルトンは反転させることで、反対方向用のアニメーションを再利用できます:

c
spine_skeleton_set_scale(skeleton, -1, 1); // 水平方向に反転
spine_skeleton_set_scale(skeleton, 1, -1); // 垂直方向に反転
// 個別に設定する場合: spine_skeleton_set_scale_x(skeleton, -1); spine_skeleton_set_scale_y(skeleton, -1);

Y 軸が下向きの座標系を使用する場合(SpineはデフォルトでY軸が上向きであると想定しています)、以下をグローバルに設定します。

c
spine_bone_set_y_down(true); // すべてのスケルトンに影響

注意: スケールの変更は、spine_skeleton_update_world_transformを呼び出した後に、ボーンのワールドトランスフォームに反映されます。

スキンの設定

アーティストは、同一のスケルトンに対して見た目のバリエーション(例:異なるキャラクターや装備)を提供するために、複数のスキンを作成できます。ランタイムにおけるスキンは、どのアタッチメントをスケルトンのどのスロットに入れるかを定義したマップです。

すべてのスケルトンは少なくとも1つのスキンを持っており、どのアタッチメントがスケルトンのセットアップポーズのどのスロットにあるのかを定義しています。追加のスキンには名前が付けられます:

c
// 名前を指定してスキンを設定
spine_skeleton_set_skin_1(skeleton, "my_skin_name");

// デフォルトのセットアップポーズ用スキンを設定
spine_skeleton_set_skin_2(skeleton, NULL);

カスタムスキンの作成

既存のスキンを組み合わせることで、実行時にカスタムスキンを作成できます:

c
// 新しいカスタムスキンを作成
spine_skin custom_skin = spine_skin_create("custom-character");

// 複数のスキンを追加して組み合わせる
spine_skin_add_skin(custom_skin, spine_skeleton_data_find_skin(skeleton_data, "skin-base"));
spine_skin_add_skin(custom_skin, spine_skeleton_data_find_skin(skeleton_data, "armor/heavy"));
spine_skin_add_skin(custom_skin, spine_skeleton_data_find_skin(skeleton_data, "weapon/sword"));
spine_skin_add_skin(custom_skin, spine_skeleton_data_find_skin(skeleton_data, "hair/long"));

// カスタムスキンをスケルトンに適用
spine_skeleton_set_skin_2(skeleton, custom_skin);

注意: カスタムスキンは、不要になった時点で spine_skin_dispose(custom_skin) を呼び出して明示的に破棄する必要があります。

注意: スキンの設定時には、それまでに有効だったアタッチメントが考慮されます。詳しくはスキンの変更を参照してください。

アタッチメントの設定

装備の切り替えなどに便利な方法として、スロットに個別のアタッチメントを直接設定できます:

c
// "hand"スロットに"sword"というアタッチメントをセットする
spine_skeleton_set_attachment(skeleton, "hand", "sword");

// スロット"hand"のアタッチメントをクリアする何も表示されないようにする
spine_skeleton_set_attachment(skeleton, "hand", NULL);

アタッチメントは、まず現在アクティブなスキンから検索され、見つからない場合はデフォルトスキンから検索されます。

ティント

スケルトン内のすべてのアタッチメントはティント(着色)することができます:

c
// 半透明の赤色で全アタッチメントをティント
spine_skeleton_set_color_2(skeleton, 1.0f, 0.0f, 0.0f, 0.5f);

// カラー構造体を使用する場合
spine_color color = spine_skeleton_get_color(skeleton);
// color を変更...
spine_skeleton_set_color_1(skeleton, color);

注意: spine-cにおける色はRGBAで示され、各チャンネルの値は[0-1]の範囲を取ります。

各スロットにも個別のカラーがあり、これも操作可能です:

c
spine_slot slot = spine_skeleton_find_slot(skeleton, "mySlot");
spine_slot_pose pose = spine_slot_get_pose(slot);
spine_color slot_color = spine_slot_pose_get_color(pose);
// レンダリング時には、スロットカラーはスケルトンカラーと乗算されます

なお、スロットカラーもアニメーションさせることができます。スロットカラーを手動で変更した後に、そのスロットカラーをキーにしたアニメーションを適用すると、手動での変更内容は上書きされることに注意してください。

アニメーションの適用

Spineエディターでは、アーティストが一意の名前のアニメーションを複数作成することができます。アニメーションとは、タイムラインの集合体です。各タイムラインは、ボーンのトランスフォーム、アタッチメントの表示/非表示、スロットカラーなどのプロパティについて、時間経過に伴う値を定義します。

Timeline API

spine-cは、タイムラインを直接操作する必要が生じた場合に、Timeline APIを提供します。この低レベルな機能を利用することで、アニメーションがどのように適用されるかを完全にカスタマイズできます。

AnimationState API

ほとんどの場合、Timeline APIではなくAnimationState APIを使用します。 AnimationState APIは、以下の処理を担当しています:

  • 時間経過に伴うアニメーションの適用
  • アニメーションのキューイング
  • アニメーション間のミキシング(クロスフェード)
  • 複数アニメーションの同時適用(レイヤリング)

AnimationState API は内部的には Timeline API を使用しています。

spine-c では、アニメーションステートを spine_animation_state として表現します。各ゲームオブジェクトは、それぞれ専用のスケルトンとアニメーションステートのインスタンスを持つ必要があります。一方で、基盤となる spine_skeleton_data および spine_animation_state_data は他のインスタンスと共有されるため、メモリ消費を抑えることができます。

AnimationStateの作成

c
spine_animation_state animation_state = spine_animation_state_create(animation_state_data);

この関数は、通常スケルトンデータのロードの際に作成した spine_animation_state_data を受け取り、デフォルトのミックスタイムや特定のアニメーション間のクロスフェード用のミックスタイムを定義します。

トラックとキューイング

AnimationStateは、1つまたは複数のトラックを管理します。各トラックはアニメーションのリストで、トラックに追加された順番に再生します。これはキューイングと呼ばれます。トラックは0から始まるインデックスを持っています。

以下のようにして、トラック上にアニメーションをキューすることができます:

c
// "walk"アニメーションをトラック0に、ループ有り、ディレイ無しで追加
int track = 0;
bool loop = true;
float delay = 0;
spine_animation_state_add_animation_1(animation_state, track, "walk", loop, delay);

また、複数のアニメーションを一度にキューして、アニメーションシーケンスを作成することができます:

c
// walkをスタート (ループ有り)
spine_animation_state_add_animation_1(animation_state, 0, "walk", true, 0);

// 3秒後にジャンプ
spine_animation_state_add_animation_1(animation_state, 0, "jump", false, 3);

// ジャンプが完了したら、無限に待機(idle)を再生
spine_animation_state_add_animation_1(animation_state, 0, "idle", true, 0);

トラック内にキューされているすべてのアニメーションをクリアする場合:

c
// トラック0にキューされているアニメーションを全てクリアする
spine_animation_state_clear_track(animation_state, 0);

// 全トラックでキューされているアニメーションを全てクリアする
spine_animation_state_clear_tracks(animation_state);

直前のアニメーションからクロスフェードしつつ、新しいアニメーションを設定する場合:

c
// トラック 0 をクリアし、"shot"にクロスフェード(ループなし)
spine_animation_state_set_animation_1(animation_state, 0, "shot", false);

// "shot"の後に"idle"をキュー
spine_animation_state_add_animation_1(animation_state, 0, "idle", true, 0);

セットアップポーズへクロスフェードする場合:

c
// トラック 0 をクリアし、0.5 秒かけてセットアップポーズにクロスフェード
spine_animation_state_set_empty_animation(animation_state, 0, 0.5f);

// もしくは、シーケンスの一部としてセットアップポーズへのクロスフェードをキュー
spine_animation_state_add_empty_animation(animation_state, 0, 0.5f, 0);

複雑なゲームでは、複数のトラックを使ってアニメーションをレイヤー化できます:

c
// トラック 0 で歩行
spine_animation_state_set_animation_1(animation_state, 0, "walk", true);

// 同時にトラック 1 で射撃
spine_animation_state_set_animation_1(animation_state, 1, "shoot", false);

注意: 上位トラックのアニメーションが同じプロパティをアニメーションしている場合、下位トラックのアニメーションを上書きします。レイヤー化する際は、同一のプロパティのキーが無いことを確認してください。

トラックエントリ

アニメーションをセットまたはキューすると、トラックエントリが返されます。

c
spine_track_entry entry = spine_animation_state_set_animation_1(animation_state, 0, "walk", true);

トラックエントリを使用すると、この特定のアニメーション再生インスタンスをさらに細かくカスタマイズできます。

c
// このアニメーションへ遷移する際のミックスデュレーションを上書き
spine_track_entry_set_mix_duration(entry, 0.5f);

トラックエントリは、それが表すアニメーションの再生が終了するまでの間だけ有効です。アニメーション設定時に保持しておき、アニメーションが適用されている間は再利用できます。 または、spine_animation_state_get_current を呼び出すことで、指定したトラックで現在再生中のアニメーションに対応するトラックエントリを取得することもできます:

c
spine_track_entry current = spine_animation_state_get_current(animation_state, 0);

イベント

AnimationStateは、キューされたアニメーションを再生しながら以下のイベントを生成します:

  • アニメーションが開始された(start)
  • トラックをクリアするなどにより、アニメーションが中断された(interrupt)
  • アニメーションが完了した(complete)。※ループしている場合は複数回発生
  • アニメーションが終了した(end)。※中断されたか、または完了しループされていない場合に発生。
  • アニメーションとそれに対応するspTrackEntry破棄された(dispose)、且つ無効になった。
  • ユーザーが定義したイベント(event)が発生した。

これらのイベントは、AnimationState全体、または個別のトラックエントリに関数を登録することで受け取れます。

c
// イベント発生時に呼び出される関数を定義
void my_listener(spine_animation_state state, spine_event_type type,
            spine_track_entry entry, spine_event event, void* user_data) {
   // 必要に応じて user_data を自分のコンテキスト型にキャスト
   MyGameContext* context = (MyGameContext*)user_data;

   switch (type) {
      case SPINE_EVENT_TYPE_START:
         printf("Animation started\n");
         break;
      case SPINE_EVENT_TYPE_INTERRUPT:
         printf("Animation interrupted\n");
         break;
      case SPINE_EVENT_TYPE_END:
         printf("Animation ended\n");
         break;
      case SPINE_EVENT_TYPE_COMPLETE:
         printf("Animation completed (loops fire this each loop)\n");
         break;
      case SPINE_EVENT_TYPE_DISPOSE:
         printf("Track entry disposed\n");
         break;
      case SPINE_EVENT_TYPE_EVENT:
         // アニメーション内で定義されたユーザーイベント
         if (event) {
            spine_event_data data = spine_event_get_data(event);
            const char* name = spine_event_data_get_name(data);
            printf("Event: %s\n", name);

            // イベントデータへアクセス
            int int_value = spine_event_data_get_int_value(data);
            float float_value = spine_event_data_get_float_value(data);
            const char* string_value = spine_event_data_get_string_value(data);
         }
         break;
   }
}

// アニメーションステート全体に対してリスナーを登録する場合
MyGameContext* context = get_my_game_context();
spine_animation_state_set_listener(animation_state, my_listener, context);

// 特定のトラックエントリに対してリスナーを登録する場合
spine_track_entry entry = spine_animation_state_set_animation_1(animation_state, 0, "walk", true);
spine_track_entry_set_listener(entry, my_listener, context);

// リスナーを解除するには、NULL を指定します
spine_animation_state_set_listener(animation_state, NULL, NULL);
spine_track_entry_set_listener(entry, NULL, NULL);

トラックエントリは、それが表すアニメーションの再生が終了するまでの間だけ有効です。登録されたリスナーは、トラックエントリが破棄されるまでの間、対応するイベントが発生するたびに呼び出されます。

更新と適用

毎フレーム、フレーム間の経過時間(デルタタイム)分だけアニメーションステートを進め、その結果をスケルトンに適用します:

c
// ゲームループ内
void update(float deltaTime) {
   // アニメーションステートを deltaTime 秒分進める
   spine_animation_state_update(animation_state, deltaTime);

   // アニメーションステートをスケルトンのローカルトランスフォームに適用
   spine_animation_state_apply(animation_state, skeleton);

   // レンダリングのためにワールドトランスフォームを計算
   spine_skeleton_update(skeleton, deltaTime);
   spine_skeleton_update_world_transform(skeleton, SPINE_PHYSICS_UPDATE);
}

spine_animation_state_update は、すべてのトラックをデルタタイム分だけ進め、必要に応じてイベントを発生させます。

spine_animation_state_apply は、全トラックの現在の状態に基づいて、スケルトンのローカルトランスフォームをポーズ付けします。これには以下が含まれます:

  • 個々のアニメーションの適用
  • アニメーション間のクロスフェード
  • 複数トラックによるアニメーションのレイヤリング

アニメーションを適用した後、spine_skeleton_update_world_transform を呼び出して、レンダリングのためにワールドトランスフォームを計算します。

Skeleton drawable

管理を簡略化するために、spine-c ではスケルトン、アニメーションステート、アニメーションステートデータを1つにまとめた spine_skeleton_drawable が提供されています:

c
// スケルトンデータから drawable を作成
spine_skeleton_drawable drawable = spine_skeleton_drawable_create(skeleton_data);

// スケルトンとアニメーションステートにアクセス
spine_skeleton skeleton = spine_skeleton_drawable_get_skeleton(drawable);
spine_animation_state animation_state = spine_skeleton_drawable_get_animation_state(drawable);
spine_animation_state_data animation_state_data = spine_skeleton_drawable_get_animation_state_data(drawable);

// 更新と描画を1回の呼び出しで実行
spine_skeleton_drawable_update(drawable, deltaTime);
spine_render_command render_command = spine_skeleton_drawable_render(drawable);

// アニメーションステートのイベントを取得
spine_animation_state_events events = spine_skeleton_drawable_get_animation_state_events(drawable);
int num_events = spine_animation_state_events_get_num_events(events);
for (int i = 0; i < num_events; i++) {
   spine_event_type type = spine_animation_state_events_get_event_type(events, i);
   spine_track_entry entry = spine_animation_state_events_get_track_entry(events, i);
   if (type == SPINE_EVENT_TYPE_EVENT) {
      spine_event event = spine_animation_state_events_get_event(events, i);
      // イベントを処理
   }
}
spine_animation_state_events_reset(events);

// 使用後に破棄(スケルトンとアニメーションステートは破棄されますが、スケルトンデータは破棄されません)
spine_skeleton_drawable_dispose(drawable);

このように、drawable は更新処理と適用処理をまとめることで更新サイクルを簡素化します。アニメーション処理パイプラインを細かく制御したい場合は、Skeleton API と AnimationState API を直接使用してください。

レンダリング

spine-c は、エンジンに依存しない描画インターフェースを提供します。レンダリング処理では spine_render_command オブジェクトが生成されます。各コマンドは、ブレンドモードやテクスチャ情報を含む、テクスチャ付き三角形のバッチを表しており、任意のグラフィックスAPIに送信できます。

レンダーコマンド

スケルトンのワールドトランスフォームを更新した後、以下のようにしてレンダーコマンドを生成します:

c
// skeleton drawable を使用する場合
spine_render_command command = spine_skeleton_drawable_render(drawable);

// または skeleton renderer を直接使用する場合
//(複数のスケルトンで再利用可能。ただしスレッドセーフではありません)
spine_skeleton_renderer renderer = spine_skeleton_renderer_create();
spine_render_command command = spine_skeleton_renderer_render(renderer, skeleton);

レンダラーは以下の処理を自動的に行います:

  • 同じテクスチャとブレンドモードを共有する連続した領域アタッチメントやメッシュアタッチメントをまとめてバッチ化
  • クリッピングアタッチメントによるクリッピングの適用
  • 最適化されたドローコールの生成

各レンダーコマンドは、以下の情報を持つ 1 つのバッチを表します:

  • 頂点データ(座標、UV、カラー)
  • 三角形用のインデックスデータ
  • 使用するテクスチャ
  • ブレンドモード(通常、加算、乗算、スクリーン)

レンダーコマンドの処理

以下のようにコマンドを順に処理し、グラフィックスAPIに送信します:

c
//説明用の簡易的なグラフィックスAPI
void render_skeleton(spine_render_command first_command) {
   spine_render_command command = first_command;

   while (command) {
      // コマンドデータの取得
      float* positions = spine_render_command_get_positions(command);
      float* uvs = spine_render_command_get_uvs(command);
      uint32_t* colors = spine_render_command_get_colors(command);
      uint16_t* indices = spine_render_command_get_indices(command);
      int num_vertices = spine_render_command_get_num_vertices(command);
      int num_indices = spine_render_command_get_num_indices(command);

      // テクスチャとブレンドモードを取得
      void* texture = spine_render_command_get_texture(command);
      spine_blend_mode blend_mode = spine_render_command_get_blend_mode(command);

      // 描画状態を設定
      graphics_bind_texture(texture);
      graphics_set_blend_mode(blend_mode);

      // 頂点とインデックスをGPUに送信
      graphics_set_vertices(positions, uvs, colors, num_vertices);
      graphics_draw_indexed(indices, num_indices);

      // 次のコマンドへ
      command = spine_render_command_get_next(command);
   }
}

ブレンドモード

ブレンドモードに応じて、グラフィックス API のブレンド関数を設定します:

c
void graphics_set_blend_mode(spine_blend_mode mode, bool premultiplied_alpha) {
   switch (mode) {
      case SPINE_BLEND_MODE_NORMAL:
         // 乗算済みアルファの場合: src=GL_ONE, dst=GL_ONE_MINUS_SRC_ALPHA
         // ストレートアルファの場合: src=GL_SRC_ALPHA, dst=GL_ONE_MINUS_SRC_ALPHA
         break;
      case SPINE_BLEND_MODE_ADDITIVE:
         // 乗算済みアルファの場合: src=GL_ONE, dst=GL_ONE
         // ストレートアルファの場合: src=GL_SRC_ALPHA, dst=GL_ONE
         break;
      case SPINE_BLEND_MODE_MULTIPLY:
         // 両方共通: src=GL_DST_COLOR, dst=GL_ONE_MINUS_SRC_ALPHA
         break;
      case SPINE_BLEND_MODE_SCREEN:
         // 両方共通: src=GL_ONE, dst=GL_ONE_MINUS_SRC_COLOR
         break;
   }
}

実装例

完全なレンダリング実装については、以下を参照してください:

これらの例では、異なるグラフィックスAPIやフレームワークとspine-cのレンダリング処理をどのように統合すれば良いのかが示されています。

メモリ管理

spine-cのメモリ管理はシンプルです。spine_*_create で生成されたすべてのオブジェクトは、必ず spine_*_dispose によって破棄する必要があります。ローダーから返されるオブジェクトは結果型を使用しており、spine_*_result_dispose によって破棄しなければなりません。

ライフタイムのガイドラインは以下のとおりです:

  • インスタンス間で共有されるセットアップポーズ用データ (spine_atlasspine_skeleton_dataspine_animation_state_data) は、ゲームやレベルの開始時に生成し、終了時に破棄します。
  • インスタンス用データ (spine_skeleton, spine_animation_state) は、ゲームオブジェクト生成時に作成し、オブジェクト破棄時に破棄します。
  • 管理を簡略化したい場合は spine_skeleton_drawable を使用してください。これはスケルトン、アニメーションステート、アニメーションステートデータをまとめて管理します。

トラックエントリ (spine_track_entry )は、アニメーションがキューに追加された時点(spine_animation_state_set_animation_*spine_animation_state_add_animation_*) から、SPINE_EVENT_TYPE_DISPOSE イベントがリスナーに送信されるまでの間だけ有効です。このイベント以降にトラックエントリへアクセスすると、未定義動作となります。

オブジェクト生成時には、他のオブジェクトへの参照を渡します。参照している側のオブジェクトが、参照先のオブジェクトを破棄することはありません:

  • spine_skeleton を破棄しても、 spine_skeleton_dataspine_atlas は破棄されません。スケルトンデータは、他のスケルトンインスタンスと共有されている可能性があります。
  • spine_skeleton_data を破棄しても、 spine_atlas は破棄されません。アトラスは複数のスケルトンデータで共有される場合があります。
  • spine_skeleton_drawable を破棄すると、その内部のスケルトンとアニメーションステートは破棄されますが、スケルトンデータは破棄されません。

型情報とキャスト

spine-c では、C++オブジェクトを表現するために不透明ポインタを使用しています。一部の型には継承関係があり、基底型と派生型を相互に変換する際には、明示的なキャストが必要です。

RTTI (Runtime Type Information | 実行時型情報)

spine-c におけるすべての多態型は、実行時に具体的な型を識別するための RTTI を提供します:

c
spine_array_constraint constraints = spine_skeleton_get_constraints(skeleton);
spine_constraint* buffer = spine_array_constraint_buffer(constraints);

for (int i = 0; i < spine_array_constraint_size(constraints); i++) {
   spine_constraint constraint = buffer[i];
   spine_rtti rtti = spine_constraint_get_rtti(constraint);

   // 正確な型を確認
   if (spine_rtti_is_exactly(rtti, spine_transform_constraint_rtti())) {
      // これは正確に TransformConstraint型だと確定
   }

   // 特定の型のインスタンスかどうかを確認(派生型を含む)
   if (spine_rtti_is_instance_of(rtti, spine_constraint_rtti())) {
      // これは Constraint、または Constraint から派生した型だと確定
   }

   // デバッグ用にクラス名を取得
   const char* class_name = spine_rtti_get_class_name(rtti);
   printf("Constraint type: %s\n", class_name);
}

型キャスト

C++ の多重継承の影響により、型間でキャストを行うとポインタ値が変化します。spine-c では、これらの調整を正しく行うためのキャスト関数が提供されています。

アップキャスト(派生型から基底型へ)

アップキャストは常に安全で、派生型を基底型のコンテナに格納する際に使用します:

c
spine_transform_constraint tc = /* ... */;

// 配列に格納するために基底型へキャスト
spine_constraint base = spine_transform_constraint_cast_to_constraint(tc);
spine_array_constraint_add(constraints_array, base);

ダウンキャスト(基底型から派生型へ)

ダウンキャストを行うには、実際の型を把握している必要があります。キャスト前に RTTI を使用して確認してください:

c
spine_constraint constraint = buffer[i];

// ダウンキャスト前に型を確認
spine_rtti rtti = spine_constraint_get_rtti(constraint);
if (spine_rtti_is_exactly(rtti, spine_transform_constraint_rtti())) {
   // 安全にダウンキャスト可能
   spine_transform_constraint tc = spine_constraint_cast_to_transform_constraint(constraint);
   spine_transform_constraint_data data = spine_transform_constraint_get_data(tc);
   // TransformConstraint を使用...
}

主な型階層

キャストが必要となる主な継承関係は以下のとおりです:

  • Constraints: IkConstraintPathConstraint, PhysicsConstraintTransformConstraintConstraint
  • Constraint data: IkConstraintDataPathConstraintDataなど → ConstraintData
  • Attachments: RegionAttachmentMeshAttachmentBoundingBoxAttachmentなど → Attachment
  • Timelines: タイムライン型全般 → CurveTimelineTimeline

アタッチメントの例は以下のとおりです:

c
spine_slot slot = spine_skeleton_find_slot(skeleton, "weapon");
spine_attachment attachment = spine_slot_get_attachment(slot);

if (attachment) {
   spine_rtti rtti = spine_attachment_get_rtti(attachment);

   if (spine_rtti_is_exactly(rtti, spine_region_attachment_rtti())) {
      spine_region_attachment region = spine_attachment_cast_to_region_attachment(attachment);
      // RegionAttachment を処理...
   } else if (spine_rtti_is_exactly(rtti, spine_mesh_attachment_rtti())) {
      spine_mesh_attachment mesh = spine_attachment_cast_to_mesh_attachment(attachment);
      // MeshAttachment を処理...
   }
}

注意: spine-c の型同士で Cスタイルキャストを使用しないでください。必ず提供されているキャスト関数を使用し、ポインタ調整が正しく行われるようにしてください。