spine-cppランタイムドキュメント
ライセンスについて
Spineランタイムをアプリケーションに組み込む前に、必ずSpine Runtimes Licenseを確認してください。
はじめに
spine-cppは、C++と連携できるゲームエンジンやフレームワークに Spineアニメーションを統合するための汎用ランタイムです。
spine-cppは、以下の機能を提供します:
- Spine スケルトンとテクスチャアトラスのロードと操作
- クロスフェードを用いたアニメーションの適用とミックス
- 見た目のバリエーションのためのスキンの管理
- 現在のスケルトンのポーズ、スロットおよびアタッチメントの状態に基づく、レンダリングや物理演算に必要なデータの操作と計算
このランタイムはエンジン非依存です。テクスチャのロードは自分で TextureLoader を実装して提供し、レンダーコマンドをエンジンのレンダリングシステムに渡すことになります。
spine-cppはC++11で書かれており、spine-c 経由でプレーンなC APIとしても公開されています。
例となる統合実装:
- spine-ios - iOS 統合
- spine-flutter - Flutter 統合
- spine-sdl - SDL 統合
- spine-glfw - GLFW 統合
- spine-ue - Unreal Engine 統合
- spine-godot - Godot 統合
注意: 当ガイドでは、Spineで使用される基本的なランタイムのアーキテクチャと用語を理解されていることを前提としています。ランタイムのより高度な機能については、APIリファレンスも参照してください。
spine-cppの統合
CMakeによる統合(推奨)
spine-cppをプロジェクトに統合する最も簡単な方法は、CMake の 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のヘッダをインクルードしてください:
using namespace spine;
手動による統合
手動で統合する場合:
- git (
git clone https://github.com/esotericsoftware/spine-runtimes) でSpineランタイムのソースを取得するか、zipをダウンロードします - 必要なソースファイルをプロジェクトに追加します:
spine-cpp/srcのソースを追加
- インクルードディレクトリを追加します:
spine-cpp/include
コード内でspine-cppのヘッダをインクルードしてください:
using namespace spine;
Spineアセットのspine-cpp向けエクスポート

以下の実行方法については、Spineユーザーガイド内で紹介されています :
- JSONまたはバイナリ形式でのスケルトン&アニメーションデータのエクスポート
- スケルトンの画像を含むテクスチャアトラスのエクスポート
スケルトンデータとテクスチャアトラスをエクスポートすると、以下のファイルが得られます :

skeleton-name.jsonまたはskeleton-name.skel: これはスケルトンとアニメーションのデータを含んでいます。skeleton-name.atlas: これはテクスチャアトラスの情報を含んでいます。- 1つまたは複数の
.pngファイル: これはテクスチャアトラスの各ページで、スケルトンが使用するイメージを含んでいます。
注意: 複数のスケルトンの画像を単一のテクスチャアトラスにパックして効率化することができます。詳しくは テクスチャパッキングのガイド を参照してください。
Spineアセットのロード
spine-cppは、テクスチャアトラス、Spineスケルトンデータ(ボーン、スロット、アタッチメント、スキン、アニメーション)をロードし、AnimationStateDataによってアニメーション間のミックスタイムを定義するためのAPIを提供します。この3種類のデータはセットアップポーズデータとも呼ばれ、通常、一度ロードされた後はすべてのゲームオブジェクトで共有されます。この共有の仕組みは、各ゲームオブジェクトに独自のスケルトンとAnimationState(インスタンスデータ)を持つことで実現します。
補足: ローディングの全体的なアーキテクチャの詳細については、全般的なSpineランタイムドキュメントを参照してください。
テクスチャアトラスのロード
テクスチャアトラスデータは、アトラスページ内の個々の画像の位置を記述するカスタムアトラス形式で保存されます。アトラスページ自体はアトラスとは別にプレーンな .png ファイルとして保存されます。
spine-cppはテクスチャをロードするために TextureLoader インターフェースを使用します。エンジン用にこのインターフェースを実装する必要があります:
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を実装すれば、アトラスをロードできます:
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コンストラクタは自動的に:
- アトラスデータを解析
- 各アトラスページについてあなたのTextureLoaderを呼び出す
- すべての領域(region)にテクスチャ参照を設定する
スケルトンデータのロード
スケルトンデータ(ボーン、スロット、アタッチメント、スキン、アニメーション)はヒューマン・リーダブル(対人可読形式)な JSON またはカスタムの バイナリ形式 にエクスポートできます。spine-cpp はスケルトンデータを SkeletonData オブジェクトとして保持します。
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);
}
バイナリからロードする場合
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 クラスを提供します:
AnimationStateData* animStateData = new AnimationStateData(skeletonData);
// 2つのアニメーション間のデフォルトミックスタイムを秒単位で設定する
animStateData->setDefaultMix(0.1f);
// 特定のアニメーション間のミックスタイムを設定し、デフォルト値を上書きする
animStateData->setMix("jump", "walk", 0.2f);
AnimationStateData で定義されたミックス時間は、アニメーション適用時に明示的に上書きすることもできます(下記参照)。
スケルトン
セットアップポーズデータ(スケルトンデータ、テクスチャアトラス)はゲームオブジェクト間で共有されます。各ゲームオブジェクトは共有された SkeletonData と Atlas を参照する独自の Skeleton インスタンスを持ちます。
Skeleton は(プロシージャルなボーン操作、アニメーション、アタッチメント、スキンなど)自由に変更できますが、基になるデータはそのまま保持されます。これにより多くのゲームオブジェクトで効率的に共有できます。
スケルトンの作成
各ゲームオブジェクトは自身の skeleton インスタンスを持つ必要があります。大部分のデータは共有されたままになり、メモリ消費とテクスチャ切り替えを削減します。
注意: 不要になったら
delete skeletonで明示的に削除する必要があります。
ボーン
スケルトンはボーンの階層で構成され、スロットはボーンに、アタッチメントはスロットにアタッチされます。
ボーンを探す
スケルトン内のすべてのボーンは一意の名前を持ちます:
Bone* bone = skeleton->findBone("mybone");
ローカルトランスフォーム
ボーンは親ボーンの影響を受け、ルートボーンまで遡ります。親からの継承方法はトランスフォームの継承設定で制御されます。各ボーンは親に対するローカルなトランスフォームを持ち、以下で構成されます:
- 親を基準とした
x、y座標。 - 度単位の
rotation(回転)。 scaleX(スケールX)、scaleY(スケールY)。- 度単位の
shearX(シアーX)、shearY(シアーY)。
ローカルトランスフォームはボーンのポーズ(BoneLocal)からアクセスします:
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、トランスフォーム、パス、スライダー のような各コンストレイントも適用します。
ワールドトランスフォームを計算するには:
skeleton->updateWorldTransform(Physics_Update);
deltaTime はフレーム間の秒数です。第二引数は物理の挙動を指定します。一般的には Physics_Update をデフォルト値にするのが良いでしょう。
ワールドトランスフォームはボーンの適用済みポーズ(BonePose)からアクセスします:
// ワールドトランスフォーム行列の成分を取得
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();
worldX と worldY はスケルトンの x, y 位置によってオフセットされることに注意してください。
ワールドトランスフォームは直接変更しないでください。常にローカルトランスフォームから skeleton->updateWorldTransform() を呼んで派生させる必要があります。
座標系の変換
spine-cppは座標系間の変換関数を提供します。これらはワールドトランスフォームが計算済みであることを前提としています:
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()を呼ぶとワールドトランスフォームに反映されます。
ポジションの決定
デフォルトではスケルトンはワールド座標系の原点にあります。ゲーム内でスケルトンを配置するには:
skeleton->setX(myGameObject->worldX);
skeleton->setY(myGameObject->worldY);
// あるいは、両方を一度に設定する
skeleton->setPosition(myGameObject->worldX, myGameObject->worldY);
注意: スケルトン位置の変更は
skeleton->updateWorldTransform()を呼んだ後にボーンのワールドトランスフォームに反映されます。
反転
スケルトンは反転して、反対方向のためにアニメーションを再利用できます:
skeleton->setScaleY(-1); // 垂直反転
// あるいは、両方を一度に設定する
skeleton->setScale(-1, 1); // 水平反転
skeleton->setScale(1, -1); // 垂直反転
y軸が下向きの座標系(Spineはデフォルトでy軸は上向き)を使う場合はグローバルに設定します:
注意: スケールの変更は
skeleton->updateWorldTransform()を呼びだした後にボーンのワールドトランスフォームに反映されます。
スキンの設定
アーティストは同じスケルトンの見た目を変えるために複数のスキンを作成できます(例: 異なるキャラクターや装備など)。ランタイムにおけるスキンはどのアタッチメントをどのスロットに入れるかをマップします。
すべてのスケルトンには少なくともセットアップポーズを定義するスキンが1つあります。追加のスキンには必ず名前が付いています:
skeleton->setSkin("my_skin_name");
// デフォルトのセットアップポーズのスキンを設定する
skeleton->setSkin(nullptr);
カスタムスキンの作成
既存のスキンを組み合わせて、実行時にカスタムスキンを作成できます:
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で手動で削除する必要があります。
注意: スキンを設定するときは以前にアクティブだったアタッチメントが考慮されます。詳細はスキンの変更を参照してください。
アタッチメントの設定
装備の切り替えを行いたい時など、特定のスロットにアタッチメントを直接設定できます:
skeleton->setAttachment("hand", "sword");
// スロット"hand"のアタッチメントをクリアする
skeleton->setAttachment("hand", nullptr);
アタッチメントはまずアクティブなスキン内で検索され、見つからなければデフォルトスキンから検索されます。
ティント
スケルトン内のすべてのアタッチメントは色付け(ティント)できます:
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] です。
各スロットにも独自のカラーがあり、以下のように操作できます:
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のインスタンスを持つ必要があります。これらは基になる SkeletonData と AnimationStateData を他のインスタンスと共有してメモリ消費を削減します。
AnimationStateの作成
コンストラクタはロード時に作成した AnimationStateData を受け取り、デフォルトミックスタイムや特定のアニメーション間のミックスタイム(クロスフェード)を定義します。
注意: 不要になったら
delete animationStateで明示的に削除する必要があります。
トラックとキューイング
AnimationStateは、1つまたは複数のトラックを管理します。各トラックはアニメーションのリストで、トラックに追加された順番に再生します。これはキューイングと呼ばれます。トラックは0から始まるインデックスを持っています。
以下のようにして、トラック上にアニメーションをキューすることができます:
int track = 0;
bool loop = true;
float delay = 0;
animationState->addAnimation(track, "walk", loop, delay);
また、複数のアニメーションを一度にキューして、アニメーションシーケンスを作成することができます:
animationState->addAnimation(0, "walk", true, 0);
// 3秒後にジャンプ
animationState->addAnimation(0, "jump", false, 3);
// ジャンプが完了したら、無限に待機(idle)を再生
animationState->addAnimation(0, "idle", true, 0);
また、トラック内にキューされているすべてのアニメーションをクリアすることができます:
animationState->clearTrack(0);
// 全トラックでキューされているアニメーションを全てクリアする
animationState->clearTracks();
前のアニメーションからクロスフェードで切り替えつつ新しいアニメーションを設定するには:
animationState->setAnimation(0, "shot", false);
// "shot"の後に再生されるよう"idle"をキューに追加する
animationState->addAnimation(0, "idle", true, 0);
セットアップポーズへクロスフェードするには:
animationState->setEmptyAnimation(0, 0.5f);
// あるいは、シーケンスの一部としてセットアップポーズへのクロスフェードをキューに追加する
animationState->addEmptyAnimation(0, 0.5f, 0);
複雑なゲームのために複数トラックを使ってアニメーションをレイヤー化することもできます:
animationState->setAnimation(0, "walk", true);
// 同時にトラック1で射撃する
animationState->setAnimation(1, "shoot", false);
注意: 上位トラックのアニメーションは、下位トラックのアニメーションと同じプロパティをアニメーションしている場合、下位トラックのアニメーションを上書きします。レイヤー化して下位トラックのアニメーションを残したい時は、同じプロパティにキーを打たないよう注意してください。
トラックエントリ
アニメーションを設定またはキューしたとき、トラックエントリが返されます:
トラックエントリを使ってそのアニメーション再生のインスタンスをさらにカスタマイズできます:
entry.setMixDuration(0.5f);
トラックエントリはそれが表すアニメーションが終了するまで有効です。アニメーション設定時に保存しておき、アニメーション適用中は再利用できます。あるいは getCurrent を呼んで現在再生中のトラックのトラックエントリを取得することもできます:
イベント
AnimationStateは、キューイングされたアニメーションを再生しながらイベントを生成し、リスナーに以下の変更を通知します:
- アニメーションが開始された(start)。
- トラックをクリアするなどにより、アニメーションが中断された(interrupt)。
- アニメーションが完了した(complete)。※ループしている場合は複数回発生
- アニメーションが終了した(end)。※中断されたか、または完了しループされていない場合に発生。
- アニメーションとそれに対応する
TrackEntryが破棄された(dispose)。 - ユーザーが定義したイベント(event)が発生した。
これらのイベントはAnimationStateまたは個別のトラックエントリにリスナーを登録することで受け取れます。以下はC++11のラムダを使う例です:
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 を使用できます:
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を進め、それをスケルトンに適用します:
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に送ることができます。
レンダーコマンド
スケルトンのワールドトランスフォームを更新した後、レンダーコマンドを生成します:
SkeletonRenderer renderer;
RenderCommand* command = renderer.render(*skeleton);
レンダラーは自動的に次を処理します:
- 同じテクスチャとブレンドモードを共有する連続する領域およびメッシュアタッチメントから三角形をバッチ化
- クリッピングアタッチメントに対するクリッピングの適用
- 最適化されたドローコールの生成
各レンダーコマンドは次を表すバッチです:
- 頂点データ(位置、UV、色)
- 三角形のインデックスデータ
- サンプリングするテクスチャ
- ブレンドモード(normal, additive, multiply, screen)
レンダーコマンドの処理
コマンドを反復してグラフィックス 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 のブレンド関数を設定します:
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;
}
}
実装例
完全なレンダリング実装の例については次を参照してください:
- spine-sfml: SFMLベースのレンダラー
- spine-sdl: SDLベースのレンダラー
- spine-glfw: GLFW を使ったOpenGLレンダラー
- spine-ue: Unreal Engineレンダラー
- spine-godot: Godotレンダラー
これらの例は異なるグラフィックス APIやフレームワークとspine-cppのレンダリングを統合する方法を示しています。
メモリ管理
spine-cppは標準的なC++のメモリ管理を使用します。new で作成したオブジェクトは delete で破棄する必要があります。
ライフタイムのガイドライン:
- インスタンス間で共有されるセットアップポーズデータ(
Atlas,SkeletonData,AnimationStateData)はゲームやレベルの起動時に作成し、ゲームやレベルの終了時に削除しましょう。 - インスタンスデータ(
Skeleton,AnimationState)はゲームオブジェクト作成時に作成し、オブジェクト破棄時に削除しましょう。
トラックエントリ(TrackEntry)は AnimationState が管理し手動で削除してはいけません。キューに入れられた時点から dispose イベントが発生するまで有効です。
オブジェクトを作成するときに他のオブジェクトへの参照を渡す場合、参照する側は参照される側を削除しないことに注意して下さい:
Skeletonを削除してもSkeletonDataやAtlasは削除されません。スケルトンデータは他のスケルトンインスタンスと共有されている可能性があります。SkeletonDataを削除してもAtlasは削除されません。アトラスは複数のスケルトンデータインスタンスで共有される場合があります。
カスタムメモリ割り当てとファイルI/O
spine-cppは、メモリ割り当てとファイル I/OのためにExtensionシステムを使用します。独自のExtensionクラスを作成してこれをカスタマイズできます:
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 を提供しています:
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 演算子を使用します:
Skeleton* skeleton = new Skeleton(skeletonData);
// 以下の記述を使用してください:
Skeleton* skeleton = new (__FILE__, __LINE__) Skeleton(skeletonData);
// これにより、DebugExtensionがメモリ割り当ての正確な位置を報告できるようになります
これは開発中にメモリリークを見つけるために非常に有用です。