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

ライセンスについて

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

はじめに

spine-cppは、C++と連携できるゲームエンジンやフレームワークに Spineアニメーションを統合するための汎用ランタイムです。

spine-cppは、以下の機能を提供します:

このランタイムはエンジン非依存です。テクスチャのロードは自分で TextureLoader を実装して提供し、レンダーコマンドをエンジンのレンダリングシステムに渡すことになります。

spine-cppはC++11で書かれており、spine-c 経由でプレーンなC APIとしても公開されています。

例となる統合実装:

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

spine-cppの統合

CMakeによる統合(推奨)

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

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

# spine-cpp に対してリンクする
target_link_libraries(your_target spine-cpp)

これによりspine-cppが自動的に取得されビルドされます。

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

C++
#include <spine/spine.h>
using namespace spine;

手動による統合

手動で統合する場合:

  1. git (git clone https://github.com/esotericsoftware/spine-runtimes) でSpineランタイムのソースを取得するか、zipをダウンロードします
  2. 必要なソースファイルをプロジェクトに追加します:
    • spine-cpp/src のソースを追加

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

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

C++
#include <spine/spine.h>
using namespace spine;

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

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

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

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

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

注意: 複数のスケルトンの画像を単一のテクスチャアトラスにパックして効率化することができます。詳しくは テクスチャパッキングのガイド を参照してください。

Spineアセットのロード

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

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

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

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

spine-cppはテクスチャをロードするために TextureLoader インターフェースを使用します。エンジン用にこのインターフェースを実装する必要があります:

TextureLoaderの実装

C++
class MyTextureLoader : public TextureLoader {
public:
   virtual void load(AtlasPage& page, const String& path) {
      // 指定されたパスからテクスチャをロード
      void* texture = engine_load_texture(path.buffer());

      // テクスチャをページに格納
      page.texture = texture;

      // テクスチャの寸法を設定(必須)
      page.width = texture_width;
      page.height = texture_height;
   }

   virtual void unload(void* texture) {
      // テクスチャをアンロード
      engine_unload_texture(texture);
   }
};

アトラスのロード

TextureLoaderを実装すれば、アトラスをロードできます:

C++
// テクスチャローダーを作成
MyTextureLoader* textureLoader = new MyTextureLoader();

// ファイルからアトラスをロードする。textureLoaderは、アトラスが破棄されるまでアトラスによって保持されます
// アトラスは、指定されたパスからファイルをロードする際にDefaultExtensionを使用します。これは、
// システム上でstdio.hが利用可能であることを前提としています。
Atlas* atlas = new Atlas("path/to/skeleton.atlas", textureLoader);

// または、メモリからアトラスをロードすることもできます
const char* atlasData = read_file_to_string("path/to/skeleton.atlas");
Atlas* atlas = new Atlas(atlasData, strlen(atlasData), "path/to/atlas/dir", textureLoader);

Atlasコンストラクタは自動的に:

  1. アトラスデータを解析
  2. 各アトラスページについてあなたのTextureLoaderを呼び出す
  3. すべての領域(region)にテクスチャ参照を設定する

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

スケルトンデータ(ボーン、スロット、アタッチメント、スキン、アニメーション)はヒューマン・リーダブル(対人可読形式)な JSON またはカスタムの バイナリ形式 にエクスポートできます。spine-cpp はスケルトンデータを SkeletonData オブジェクトとして保持します。

JSONからロードする場合

C++
// atlasを使用してJSONローダーを作成する
SkeletonJson json(*atlas);

// 必要に応じてスケールを設定する
json.setScale(0.5f); // スケルトンを50%に縮小

// ファイルからスケルトンデータをロードする
SkeletonData* skeletonData = json.readSkeletonDataFile("path/to/skeleton.json");

// またはメモリからロードする
const char* jsonString = read_file_to_string("path/to/skeleton.json");
SkeletonData* skeletonData = json.readSkeletonData(jsonString);

// エラーの確認
if (!skeletonData) {
   printf("Error loading skeleton: %s\n", json.getError().buffer());
   exit(1);
}

バイナリからロードする場合

C++
// atlasを使用してバイナリローダーを作成する
SkeletonBinary binary(*atlas);

// 必要に応じてスケールを設定する
binary.setScale(0.5f); // スケルトンを50%に縮小

// ファイルからスケルトンデータをロードする
SkeletonData* skeletonData = binary.readSkeletonDataFile("path/to/skeleton.skel");

// またはメモリからロードする
unsigned char* binaryData = read_file_to_bytes("path/to/skeleton.skel", &dataLength);
SkeletonData* skeletonData = binary.readSkeletonData(binaryData, dataLength);

// エラーの確認
if (!skeletonData) {
   printf("Error loading skeleton: %s\n", binary.getError().buffer());
   exit(1);
}

補足: バイナリ形式の方がJSONより小さく、ロードも高速なため、本番ではこちらを使用することが推奨されます。

AnimationStateDataの準備

Spineは一つのアニメーションから別のアニメーションへ切り替える際のスムーズな遷移(クロスフェード)をサポートしています。クロスフェードは特定のミックスタイムで一つのアニメーションを別のアニメーションとミックスすることで実現されます。spine-cppはこれらのミックスタイムを定義する AnimationStateData クラスを提供します:

C++
// AnimationStateDataを作成
AnimationStateData* animStateData = new AnimationStateData(skeletonData);

// 2つのアニメーション間のデフォルトミックスタイムを秒単位で設定する
animStateData->setDefaultMix(0.1f);

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

AnimationStateData で定義されたミックス時間は、アニメーション適用時に明示的に上書きすることもできます(下記参照)。

スケルトン

セットアップポーズデータ(スケルトンデータ、テクスチャアトラス)はゲームオブジェクト間で共有されます。各ゲームオブジェクトは共有された SkeletonDataAtlas を参照する独自の Skeleton インスタンスを持ちます。

Skeleton は(プロシージャルなボーン操作、アニメーション、アタッチメント、スキンなど)自由に変更できますが、基になるデータはそのまま保持されます。これにより多くのゲームオブジェクトで効率的に共有できます。

スケルトンの作成

C++
Skeleton* skeleton = new Skeleton(skeletonData);

各ゲームオブジェクトは自身の skeleton インスタンスを持つ必要があります。大部分のデータは共有されたままになり、メモリ消費とテクスチャ切り替えを削減します。

注意: 不要になったら delete skeleton で明示的に削除する必要があります。

ボーン

スケルトンはボーンの階層で構成され、スロットはボーンに、アタッチメントはスロットにアタッチされます。

ボーンを探す

スケルトン内のすべてのボーンは一意の名前を持ちます:

C++
// 該当する名前のボーンが存在しない場合はNULLを返す
Bone* bone = skeleton->findBone("mybone");

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

ボーンは親ボーンの影響を受け、ルートボーンまで遡ります。親からの継承方法はトランスフォームの継承設定で制御されます。各ボーンは親に対するローカルなトランスフォームを持ち、以下で構成されます:

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

ローカルトランスフォームはボーンのポーズ(BoneLocal)からアクセスします:

C++
Bone* bone = skeleton->findBone("mybone");
BoneLocal& pose = bone->getPose();

// ローカルトランスフォームのプロパティを取得する
float x = pose.getX();
float y = pose.getY();
float rotation = pose.getRotation();
float scaleX = pose.getScaleX();
float scaleY = pose.getScaleY();
float shearX = pose.getShearX();
float shearY = pose.getShearY();

// ローカルトランスフォームを変更する
pose.setPosition(100, 50);
pose.setRotation(45);
pose.setScale(2, 2);

ローカルトランスフォームは手続き的に、またはアニメーションによって操作できます。両方を同時に行い、結果をポーズに格納することもできます。

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

(手続き的に、またはアニメーションによって)ローカルトランスフォームを設定した後、レンダリングや物理演算のために各ボーンのワールドトランスフォームが必要になります。

計算はrootボーンから開始して再帰的に子ボーンのワールドトランスフォームを算出します。また IKトランスフォームパススライダー のような各コンストレイントも適用します。

ワールドトランスフォームを計算するには:

C++
skeleton->update(deltaTime);
skeleton->updateWorldTransform(Physics_Update);

deltaTime はフレーム間の秒数です。第二引数は物理の挙動を指定します。一般的には Physics_Update をデフォルト値にするのが良いでしょう。

ワールドトランスフォームはボーンの適用済みポーズ(BonePose)からアクセスします:

C++
BonePose& applied = bone->getAppliedPose();

// ワールドトランスフォーム行列の成分を取得
float a = applied.getA(); // 2x2行列のエンコーディング
float b = applied.getB(); // 回転、スケール
float c = applied.getC(); // およびシアー
float d = applied.getD();

// ワールド座標を取得
float worldX = applied.getWorldX();
float worldY = applied.getWorldY();

worldXworldY はスケルトンの x, y 位置によってオフセットされることに注意してください。

ワールドトランスフォームは直接変更しないでください。常にローカルトランスフォームから skeleton->updateWorldTransform() を呼んで派生させる必要があります。

座標系の変換

spine-cppは座標系間の変換関数を提供します。これらはワールドトランスフォームが計算済みであることを前提としています:

C++
Bone* bone = skeleton->findBone("mybone");
BonePose& applied = bone->getAppliedPose();

// ワールド回転とスケールを取得する
float rotationX = applied.getWorldRotationX();
float rotationY = applied.getWorldRotationY();
float scaleX = applied.getWorldScaleX();
float scaleY = applied.getWorldScaleY();

// ワールド座標系とローカル座標系の変換
float localX, localY, worldX, worldY;
applied.worldToLocal(worldX, worldY, localX, localY);
applied.localToWorld(localX, localY, worldX, worldY);

// 回転の変換
float localRotation = applied.worldToLocalRotation(worldRotation);
float worldRotation = applied.localToWorldRotation(localRotation);

注意: ボーンのローカルトランスフォーム(およびその子)は、skeleton->updateWorldTransform() を呼ぶとワールドトランスフォームに反映されます。

ポジションの決定

デフォルトではスケルトンはワールド座標系の原点にあります。ゲーム内でスケルトンを配置するには:

C++
// スケルトンをゲームオブジェクトに追従させる
skeleton->setX(myGameObject->worldX);
skeleton->setY(myGameObject->worldY);

// あるいは、両方を一度に設定する
skeleton->setPosition(myGameObject->worldX, myGameObject->worldY);

注意: スケルトン位置の変更は skeleton->updateWorldTransform() を呼んだ後にボーンのワールドトランスフォームに反映されます。

反転

スケルトンは反転して、反対方向のためにアニメーションを再利用できます:

C++
skeleton->setScaleX(-1); // 水平反転
skeleton->setScaleY(-1); // 垂直反転

// あるいは、両方を一度に設定する
skeleton->setScale(-1, 1); // 水平反転
skeleton->setScale(1, -1); // 垂直反転

y軸が下向きの座標系(Spineはデフォルトでy軸は上向き)を使う場合はグローバルに設定します:

C++
Bone::setYDown(true); // すべてのスケルトンに影響します

注意: スケールの変更は skeleton->updateWorldTransform() を呼びだした後にボーンのワールドトランスフォームに反映されます。

スキンの設定

アーティストは同じスケルトンの見た目を変えるために複数のスキンを作成できます(例: 異なるキャラクターや装備など)。ランタイムにおけるスキンはどのアタッチメントをどのスロットに入れるかをマップします。

すべてのスケルトンには少なくともセットアップポーズを定義するスキンが1つあります。追加のスキンには必ず名前が付いています:

C++
// 名前によってスキンを設定
skeleton->setSkin("my_skin_name");

// デフォルトのセットアップポーズのスキンを設定する
skeleton->setSkin(nullptr);

カスタムスキンの作成

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

C++
// 新しいカスタムスキンを作成する
Skin* customSkin = new Skin("custom-character");

// 複数のスキンを追加して、組み合わせを作成する
customSkin->addSkin(skeletonData->findSkin("skin-base"));
customSkin->addSkin(skeletonData->findSkin("armor/heavy"));
customSkin->addSkin(skeletonData->findSkin("weapon/sword"));
customSkin->addSkin(skeletonData->findSkin("hair/long"));

// カスタムスキンをスケルトンに適用
skeleton->setSkin(customSkin);

注意: カスタムスキンは不要になったら delete customSkin で手動で削除する必要があります。

注意: スキンを設定するときは以前にアクティブだったアタッチメントが考慮されます。詳細はスキンの変更を参照してください。

アタッチメントの設定

装備の切り替えを行いたい時など、特定のスロットにアタッチメントを直接設定できます:

C++
// "hand"スロットに"sword"というアタッチメントをセットする
skeleton->setAttachment("hand", "sword");

// スロット"hand"のアタッチメントをクリアする
skeleton->setAttachment("hand", nullptr);

アタッチメントはまずアクティブなスキン内で検索され、見つからなければデフォルトスキンから検索されます。

ティント

スケルトン内のすべてのアタッチメントは色付け(ティント)できます:

C++
// すべてのアタッチメントを半透明の赤色で染める
skeleton->getColor().set(1.0f, 0.0f, 0.0f, 0.5f);

// または、個々の成分を指定して設定する場合
skeleton->getColor().r = 1.0f;
skeleton->getColor().g = 0.0f;
skeleton->getColor().b = 0.0f;
skeleton->getColor().a = 0.5f;

注意: spine-cpp におけるカラーは RGBA で、値域は [0-1] です。

各スロットにも独自のカラーがあり、以下のように操作できます:

C++
Slot* slot = skeleton->findSlot("mySlot");
SlotPose& pose = slot->getPose();
Color& slotColor = pose.getColor();
// レンダリング時には、スロットカラーがスケルトンカラーと掛け合わされます

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

アニメーションの適用

Spineエディターでは、アーティストが一意の名前を持つアニメーションを複数作成できます。アニメーションは一連のタイムラインから成り、各タイムラインはボーンのトランスフォーム、アタッチメントの表示、スロットカラーなどのプロパティの時間に対する値を指定します。

Timeline API

spine-cppはタイムラインを直接操作するためのTimeline APIを提供します。この低レベル機能により、アニメーションの適用方法を完全にカスタマイズできます。

AnimationState API

一般的なユースケースでは、Timeline APIの代わりにAnimationState APIを使用してください。これにより以下が扱われます:

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

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

spine-cppは AnimationState を介してアニメーションの状態を表現します。各ゲームオブジェクトはスケルトンとAnimationStateのインスタンスを持つ必要があります。これらは基になる SkeletonDataAnimationStateData を他のインスタンスと共有してメモリ消費を削減します。

AnimationStateの作成

C++
AnimationState* animationState = new AnimationState(animationStateData);

コンストラクタはロード時に作成した AnimationStateData を受け取り、デフォルトミックスタイムや特定のアニメーション間のミックスタイム(クロスフェード)を定義します。

注意: 不要になったら delete animationState で明示的に削除する必要があります。

トラックとキューイング

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

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

C++
// トラック0に "walk" アニメーションを追加し、ループ再生し、遅延なしにする
int track = 0;
bool loop = true;
float delay = 0;
animationState->addAnimation(track, "walk", loop, delay);

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

C++
// walkをスタート (ループ有り)
animationState->addAnimation(0, "walk", true, 0);

// 3秒後にジャンプ
animationState->addAnimation(0, "jump", false, 3);

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

また、トラック内にキューされているすべてのアニメーションをクリアすることができます:

C++
// トラック0にキューされているアニメーションを全てクリアする
animationState->clearTrack(0);

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

前のアニメーションからクロスフェードで切り替えつつ新しいアニメーションを設定するには:

C++
// トラック0をクリアし、"shot"へクロスフェードする(ループなし)
animationState->setAnimation(0, "shot", false);

// "shot"の後に再生されるよう"idle"をキューに追加する
animationState->addAnimation(0, "idle", true, 0);

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

C++
// トラック 0をクリアし、0.5秒かけてセットアップポーズへクロスフェードさせる
animationState->setEmptyAnimation(0, 0.5f);

// あるいは、シーケンスの一部としてセットアップポーズへのクロスフェードをキューに追加する
animationState->addEmptyAnimation(0, 0.5f, 0);

複雑なゲームのために複数トラックを使ってアニメーションをレイヤー化することもできます:

C++
// トラック0で歩く
animationState->setAnimation(0, "walk", true);

// 同時にトラック1で射撃する
animationState->setAnimation(1, "shoot", false);

注意: 上位トラックのアニメーションは、下位トラックのアニメーションと同じプロパティをアニメーションしている場合、下位トラックのアニメーションを上書きします。レイヤー化して下位トラックのアニメーションを残したい時は、同じプロパティにキーを打たないよう注意してください。

トラックエントリ

アニメーションを設定またはキューしたとき、トラックエントリが返されます:

C++
TrackEntry& entry = animationState->setAnimation(0, "walk", true);

トラックエントリを使ってそのアニメーション再生のインスタンスをさらにカスタマイズできます:

C++
// このアニメーションへの遷移時に、ミックスデュレーションを上書きする
entry.setMixDuration(0.5f);

トラックエントリはそれが表すアニメーションが終了するまで有効です。アニメーション設定時に保存しておき、アニメーション適用中は再利用できます。あるいは getCurrent を呼んで現在再生中のトラックのトラックエントリを取得することもできます:

C++
TrackEntry* current = animationState->getCurrent(0);

イベント

AnimationStateは、キューイングされたアニメーションを再生しながらイベントを生成し、リスナーに以下の変更を通知します:

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

これらのイベントはAnimationStateまたは個別のトラックエントリにリスナーを登録することで受け取れます。以下はC++11のラムダを使う例です:

C++
// コンテキストをキャプチャしたラムダ
MyGameContext* context = getMyGameContext();

auto listener = [context](AnimationState* state, EventType type, TrackEntry* entry, Event* event) {
   switch (type) {
      case EventType_Start:
         printf("Animation %s started\n", entry->getAnimation()->getName().buffer());
         break;
      case EventType_Interrupt:
         printf("Animation interrupted\n");
         break;
      case EventType_End:
         printf("Animation ended\n");
         break;
      case EventType_Complete:
         printf("Animation completed (loops fire this each loop)\n");
         context->onAnimationComplete(); // キャプチャされたコンテキストにアクセス
         break;
      case EventType_Dispose:
         printf("Track entry disposed\n");
         break;
      case EventType_Event:
         // アニメーションからのユーザー定義イベント
         if (event) {
            const String& name = event->getData().getName();
            printf("Event: %s\n", name.buffer());

            // イベントデータにアクセス
            int intValue = event->getIntValue();
            float floatValue = event->getFloatValue();
            const String& stringValue = event->getStringValue();

            // 特定のイベントを処理
            if (name == "footstep") {
               context->playFootstepSound(intValue); // 足音のIDとしてintを使用
            }
         }
         break;
   }
};

// すべてのトラックに対してリスナーを登録
animationState->setListener(listener);

// または、特定のトラックエントリに対してリスナーを登録
TrackEntry& entry = animationState->setAnimation(0, "walk", true);
entry.setListener(listener);

// 別の方法:単純なケース向けのインラインラムダ
animationState->setListener([](AnimationState* state, EventType type, TrackEntry* entry, Event* event) {
   if (type == EventType_Complete) {
      printf("Animation loop completed: %s\n", entry->getAnimation()->getName().buffer());
   }
});

// nullptrを設定してリスナーをクリア
animationState->setListener(nullptr);
entry.setListener(nullptr);

より複雑なイベント処理には AnimationStateListenerObject を使用できます:

C++
class MyAnimationListener : public AnimationStateListenerObject {
   MyGameContext* context;

public:
   MyAnimationListener(MyGameContext* ctx) : context(ctx) {}

   virtual void callback(AnimationState* state, EventType type, TrackEntry* entry, Event* event) override {
      switch (type) {
         case EventType_Start:
            context->onAnimationStart(entry->getAnimation()->getName());
            break;
         case EventType_Event:
            if (event && event->getData().getName() == "attack") {
               context->dealDamage(event->getFloatValue());
            }
            break;
         // その他のイベントを処理する...
      }
   }
};

// リスナーオブジェクトを使用する
MyAnimationListener* listener = new MyAnimationListener(context);
animationState->setListener(listener);

// 作業が終わったら、忘れずに削除してください
delete listener;

トラックエントリはそれが表すアニメーションが終了するまで有効です。登録されたリスナーはトラックエントリが破棄されるまでイベントのたびに呼ばれます。

更新と適用

各フレームで、フレームのデルタ時間分だけAnimationStateを進め、それをスケルトンに適用します:

C++
// ゲームループ内
void update(float deltaTime) {
   // アニメーションの状態を deltaTime 秒分進める
   animationState->update(deltaTime);

   // アニメーションの状態をスケルトンのローカルトランスフォームに適用する
   animationState->apply(*skeleton);

   // レンダリング用のワールドトランスフォームを計算
   skeleton->update(deltaTime);
   skeleton->updateWorldTransform(Physics_Update);
}

animationState->update() はすべてのトラックをデルタ時間分進め、イベント を発生させることがあります。

animationState->apply() はすべてのトラックの現在の状態に基づいてスケルトンのローカルトランスフォームをポーズします。これには以下が含まれます:

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

アニメーション適用後、レンダリング用のワールドトランスフォームを計算するために skeleton->updateWorldTransform() を呼んでください。

レンダリング

spine-cppはスケルトン描画のためのレンダー非依存のインターフェースを提供します。レンダリング処理は RenderCommand オブジェクトを生成し、それぞれがブレンドモードとテクスチャ情報を持つテクスチャ付き三角形のバッチを表します。そしてそれを任意のグラフィックス APIに送ることができます。

レンダーコマンド

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

C++
// SkeletonRendererの使用(複数のスケルトンで再利用可能、スレッドセーフではない)
SkeletonRenderer renderer;
RenderCommand* command = renderer.render(*skeleton);

レンダラーは自動的に次を処理します:

  • 同じテクスチャとブレンドモードを共有する連続する領域およびメッシュアタッチメントから三角形をバッチ化
  • クリッピングアタッチメントに対するクリッピングの適用
  • 最適化されたドローコールの生成

各レンダーコマンドは次を表すバッチです:

  • 頂点データ(位置、UV、色)
  • 三角形のインデックスデータ
  • サンプリングするテクスチャ
  • ブレンドモード(normal, additive, multiply, screen)

レンダーコマンドの処理

コマンドを反復してグラフィックス APIに送信します:

C++
// 説明用の簡略化されたグラフィックスAPI
void render_skeleton(RenderCommand* firstCommand) {
   RenderCommand* command = firstCommand;

   while (command) {
      // コマンドデータを取得
      float* positions = command->positions;
      float* uvs = command->uvs;
      uint32_t* colors = command->colors;
      uint16_t* indices = command->indices;
      int numVertices = command->numVertices;
      int numIndices = command->numIndices;

      // テクスチャとブレンドモードを取得
      void* texture = command->texture;
      BlendMode blendMode = command->blendMode;

      // グラフィックス状態を設定
      graphics_bind_texture(texture);
      graphics_set_blend_mode(blendMode);

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

      // 次のコマンドへ移動
      command = command->next;
   }
}

ブレンドモード

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

C++
void graphics_set_blend_mode(BlendMode mode, bool premultipliedAlpha) {
   switch (mode) {
      case BlendMode_Normal:
         // 乗算済みアルファの場合: src=GL_ONE, dst=GL_ONE_MINUS_SRC_ALPHA
         // ストレートアルファの場合: src=GL_SRC_ALPHA, dst=GL_ONE_MINUS_SRC_ALPHA
         break;
      case BlendMode_Additive:
         // 乗算済みアルファの場合: src=GL_ONE, dst=GL_ONE
         // ストレートアルファの場合: src=GL_SRC_ALPHA, dst=GL_ONE
         break;
      case BlendMode_Multiply:
         // どちらでも: src=GL_DST_COLOR, dst=GL_ONE_MINUS_SRC_ALPHA
         break;
      case BlendMode_Screen:
         // どちらでも: src=GL_ONE, dst=GL_ONE_MINUS_SRC_COLOR
         break;
   }
}

実装例

完全なレンダリング実装の例については次を参照してください:

これらの例は異なるグラフィックス APIやフレームワークとspine-cppのレンダリングを統合する方法を示しています。

メモリ管理

spine-cppは標準的なC++のメモリ管理を使用します。new で作成したオブジェクトは delete で破棄する必要があります。

ライフタイムのガイドライン:

  • インスタンス間で共有されるセットアップポーズデータ(Atlas, SkeletonData, AnimationStateData)はゲームやレベルの起動時に作成し、ゲームやレベルの終了時に削除しましょう。
  • インスタンスデータ(Skeleton, AnimationState)はゲームオブジェクト作成時に作成し、オブジェクト破棄時に削除しましょう。

トラックエントリ(TrackEntry)は AnimationState が管理し手動で削除してはいけません。キューに入れられた時点から dispose イベントが発生するまで有効です。

オブジェクトを作成するときに他のオブジェクトへの参照を渡す場合、参照する側は参照される側を削除しないことに注意して下さい:

  • Skeleton を削除しても SkeletonDataAtlas は削除されません。スケルトンデータは他のスケルトンインスタンスと共有されている可能性があります。
  • SkeletonData を削除しても Atlas は削除されません。アトラスは複数のスケルトンデータインスタンスで共有される場合があります。

カスタムメモリ割り当てとファイルI/O

spine-cppは、メモリ割り当てとファイル I/OのためにExtensionシステムを使用します。独自のExtensionクラスを作成してこれをカスタマイズできます:

C++
class MyExtension : public spine::SpineExtension {
public:
   virtual void* _alloc(size_t size, const char* file, int line) override {
      // ご自身のカスタムアロケーター
      return my_custom_malloc(size);
   }

   virtual void* _calloc(size_t size, const char* file, int line) override {
      void* ptr = my_custom_malloc(size);
      if (ptr) memset(ptr, 0, size);
      return ptr;
   }

   virtual void* _realloc(void* ptr, size_t size, const char* file, int line) override {
      return my_custom_realloc(ptr, size);
   }

   virtual void _free(void* mem, const char* file, int line) override {
      my_custom_free(mem);
   }

   virtual char* _readFile(const String& path, int* length) override {
      // ご自身のカスタムファイルリーダー
      return my_custom_file_reader(path.buffer(), length);
   }
};

// spine-cppを使用する前に、Extensionを設定してください
MyExtension* extension = new MyExtension();
spine::SpineExtension::setInstance(extension);

メモリリークの検出

spine-cppは、別の extension をラップしてアロケーションを追跡しリークを検出する DebugExtension を提供しています:

C++
// ベースとなるExtensionを作成する(またはデフォルトを使用する)
spine::DefaultSpineExtension* baseExtension = new spine::DefaultSpineExtension();

// これを DebugExtension でラップする
spine::DebugExtension* debugExtension = new spine::DebugExtension(baseExtension);
spine::SpineExtension::setInstance(debugExtension);

// ... spine-cppを通常通り使用 ...

// メモリリークをチェック
debugExtension->reportLeaks(); // 解放されていないすべてのアロケーションを出力
size_t usedMemory = debugExtension->getUsedMemory(); // 現在のメモリ使用量を取得

// 追跡情報をクリア(テスト間のリセットに便利)
debugExtension->clearAllocations();

DebugExtensionは以下を追跡します:

  • ファイル名と行番号を含むすべてのアロケーション
  • メモリ使用統計
  • ダブルフリー検出
  • 未追跡メモリの警告

自分のコード内でSpineオブジェクトのアロケーションをファイルと行情報付きで追跡するには、placement new 演算子を使用します:

C++
// 以下の記述の代わりに:
Skeleton* skeleton = new Skeleton(skeletonData);

// 以下の記述を使用してください:
Skeleton* skeleton = new (__FILE__, __LINE__) Skeleton(skeletonData);

// これにより、DebugExtensionがメモリ割り当ての正確な位置を報告できるようになります

これは開発中にメモリリークを見つけるために非常に有用です。