spine c++ serialize

たぶん必要になるでしょうアニメーションのシリアライズ、保存、復元です。ただし、ゆるーいシリアライズです(後記)。
spineにはアニメーションを保存する機能が、ありそうでありません。作らないといけません。付けて欲しいなぁ…。

spineのアニメーション

アニメーションはトラックという概念で管理されています。エディタだと「preview」機能で確認できます。

[優先↑]

track2:[何もしない][瞬き][何もしない][瞬き]
track1:[狙う]
track0:[歩く][走る]
base  :[setup状態]

横方向が単純なモーションの切り替わりです。上の例では歩きから走るモーションに切り替わります。切り替わるタイミングは「delay」で指定することが出来、0(指定無し)だとループポイントで切り替わります。アニメーションを作成する際はループポイントで繋がるように作りましょうって感じみたいです。もちろん切り替わりが分からないようにミックス(ブレンド)されます。
で、縦方向がtrack(トラック)の概念です。難しいものではありません、単にキーフレームが上書きされるだけです。キーフレームが無い箇所(ボーンやスキニングポイント)はそのまま通過、歩いているアニメーションが使用されます。
こんな感じで、部分部分のアニメーションを上書きさせていって、ゲームで必要な動的なアニメーションをプログラムから制御しましょうというのがtrackです。
なお、trackが何も無い状態だと、エディタにおける setup 状態となります。


頑張ればlive2Dみたいなことも出来そうですが、顔を左右に動かすようなものをプログラムからブレンドさせてコントロールするのは現状無理があるので、そこが利用用途が異なる理由の1つでしょう。決まったアニメーションには強いのですが。
(でもソースを見てるとちょっと弄れば出来そうなので暇が出来たらまとめたいと思います)

データ構造

アニメーションを保存するには spAnimationState の情報を使用します。

  • spAnimationState
struct spAnimationState {
	spAnimationStateData* const data;

	int tracksCount;
	spTrackEntry** tracks;

	spAnimationStateListener listener;

	float timeScale;

	spTrackEntryArray* mixingTo;
	void* rendererObject;
};

trackscount,tracks が上で説明したアニメーションの構造そのままです。ポインタのポインタ(**)がきたら9割は「あー配列かぁ」なので恐れないでください。trackscount分だけ構造体のポインタが並んでいるだけです。timescaleは使うかも知れませんね。保存して復元させると良いでしょう。
あとは復元には不要なので省略します。


次にtrackの情報 spTrackEntry について。

  • spTrackEntry
struct spTrackEntry {
	spAnimation* animation;
	spTrackEntry* next;
	spTrackEntry* mixingFrom;
	spAnimationStateListener listener;
	int trackIndex;
	int /*boolean*/ loop;
	float eventThreshold, attachmentThreshold, drawOrderThreshold;
	float animationStart, animationEnd, animationLast, nextAnimationLast;
	float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale;
	float alpha, mixTime, mixDuration, interruptAlpha, totalAlpha;
	spIntArray* timelineData;
	spTrackEntryArray* timelineDipMix;
	float* timelinesRotation;
	int timelinesRotationCount;
	void* rendererObject;
	void* userData;
};

色々ありますが、9割ぐらいはアニメーションをセットした際に設定される値なので省略しちゃっても良いかなと思います。厳密にやり出すと前のアニメーションとのブレンドミックスなどもあるので相当大変です。今回はそこまでやりませんし、そこまで必要ないと思いますし。
必要そうなのは、現在のアニメーション名を知るための animation、次のアニメーションを知るための next、再生時間関連の tracktime,delay,timescale あたりでしょうか。alphaも弄るなら要りそうです(このalphaは色では無く、trackごとのアニメーションのブレンド率です)。


名前取得するアニメーションデータの構造体 spAnimation は簡単です。

  • spAnimation
typedef struct spAnimation {
	const char* const name;
	float duration;

	int timelinesCount;
	spTimeline** timelines;
} spAnimation;

これはデータなので、弄ってはいけません。動的な情報を管理する state ではありません。
name だけ拾えば良いですね。
duration はこのアニメーションの1ループの長さです。1.0で1secの単位で表されていますが、これは今回不要です。

シリアライズ、保存

主に spTrackEntry の情報を保存すれば、復元は出来そうです。trackごとにアニメーションが何個並んでいて、それぞれがどのような状態なのかを記憶していきます。簡単に言えば2次元ループ処理ですね。

  • _savestate
void _savestate(spAnimationState* animation,const std::string &filename) {
	std::stringstream* ss = new std::stringstream();
	int count = animation->tracksCount;

	*ss << "trackcount " << count << std::endl;
	for (int i = 0; i < count; i++) {
		if (animation->tracks[i] == nullptr) {
			*ss << "exists 0" << std::endl;
		}
		else {
			*ss << "exists 1" << std::endl;
			_savestate_tracks(ss, animation->tracks[i]);
		}
	}
	*ss << "[eof]";

	_writestringfile(ss, filename);  //stringstreamを保存する関数

	delete ss;
}

状態保存は構造体にして保存したほうが良いかなと思いましたが、テキストの方が見易いので stringstream 使っておきます。実際組む場合はもうちょっと賢いjsonなどに出力させた方がいいでしょう。
trackscount でループを回す際に注意点があります。アニメーションを track0,2 と指定した場合、track1 がnullになります。なので、nullチェックが必要です。ここでは「exists」がそれにあたります。存在する場合は、track の中のアニメーション情報 spTrackEntry の stringstream に追加していきます。

  • _savestate_tracks
void _savestate_tracks(std::stringstream* ss, spTrackEntry* track) {
	//entry animation count
	int count=1;
	{
		spTrackEntry* data = track;
		while (data->next != nullptr) {
			data = data->next;
			count++;
		}
		*ss << "animationcount " << count << std::endl;
	}
	//each animation satate (track entry)
	spTrackEntry* data = track;
	for (int i = 0; i < count; i++) {
		*ss << "name " << data->animation->name << std::endl;			// string
		*ss << "duration " << data->animation->duration << std::endl;	// animation time length
		*ss << "tracktime " << data->trackTime << std::endl;
		*ss << "timescale " << data->timeScale << std::endl;
		*ss << "loop " << data->loop << std::endl;						// int
		*ss << "delay " << data->delay << std::endl;
		//next
		data = data->next;
	}
}

横方向に当たるtrackないのアニメーションですが、数を知る場合は count が存在しないのでちょっと工夫が必要です。next で次の情報が取得出来る数珠つなぎで管理されているので、nextをくるくる遡ってカウントを数えます。nullがでたら終端です。
count が取得出来たら、あとは同じようにくるくる回して情報を保存していきます。
できあがったのがこちら。

  • savestate.txt
trackcount 3
  exists 1
  animationcount 2
    name walk
      duration 0.8667
      tracktime 0.333333
      timescale 1
      loop 1
      delay 0
    name run
      duration 0.8
      tracktime 0
      timescale 1
      loop 1
      delay 0.6667

  exists 0

  exists 1
  animationcount 1
    name aim
      duration 0
      tracktime 0.333333
      timescale 1
      loop 0
      delay 0

改行とスペースと空行は実際にはありません。分かりやすいように付けただけなので、実際のデータとは異なります。

シリアライズ・復元

この情報から復元します。まっさらなモーション状態 spAnimationState_clearTracks にして、そこから再構築します。
コードもシリアライズとほぼ同じ構造なので、理解は簡単でしょう。
「>> name >> exists」のような構文は、値の読み込みです。便利ですが手抜きです:)

  • _loadstate
void _loadstate(spAnimationState* animation, const std::string &filename) {
	std::stringstream* ss = _readstringfile(filename); // stringstreamにファイル読み込みます
	std::string name;
	int count;
	*ss >> name >> count;
	printf("%s:%d\r\n", name.c_str(),count);

	for (int i = 0; i < count; i++){
		int exists;
		*ss >> name >> exists;
		if (exists == 0) {
			// null
		}
		else {
			_loadstate_tracks(ss, animation, i);
		}
	}
	delete ss;
}

void _loadstate_tracks(std::stringstream* ss, spAnimationState* animation, int no) {
	// animation count
	std::string name;
	int count;
	*ss >> name >> count;

	// each animation
	for (int i = 0; i < count; i++) {
		// animation name
		std::string animationname;
		*ss >> name >> animationname;
		// duration (not use)
		float duration;
		*ss >> name >> duration;
		// tracktime
		float tracktime;
		*ss >> name >> tracktime;
		// timescale
		float timescale;
		*ss >> name >> timescale;
		// loop
		int loop;
		*ss >> name >> loop;
		// delay
		float delay;
		*ss >> name >> delay;

		spTrackEntry* entry;
		if (i == 0) {
			entry = spAnimationState_setAnimationByName(animation, no, animationname.c_str(), loop);
		}
		else {
			entry = spAnimationState_addAnimationByName(animation, no, animationname.c_str(), loop, delay);
		}
		entry->trackTime = tracktime;
		entry->timeScale = timescale;
		//printf("animation:%s time:%f delay:%f loop:%d\r\n", animationname.c_str(), tracktime, delay, loop);
	}
}

モーションを構築する際に一応 setAnimation と addAnimation を使い分けてあります。全部 add で良いような気もしますが、set のほうがtrackにあるモーションをすべて破棄して設定するとあるので、安全かな?と思います。
実は stringstream の特性でアニメーション名にスペースがあると動きません。実装の際は string convertspaceword(const string) みたいな関数で文字を置き換えて対処などしてください。
あと AnimationByName はサンプルなので使ってますが、無い名前を渡すとnull例外アクセスで死ぬので、ちゃんと自分で名前チェック入れるか自前でfix命令作って代用してくださいね。

実験

前回作ったポリゴン表示ログに細工して、出力ファイルが同じかどうか比較します。

//before
triangle:vector(824.137,932.741,728.086,745.995,547.565,838.845):argb=(1,1,1,1)
triangle:vector(547.565,838.845,643.616,1025.59,824.137,932.741):argb=(1,1,1,1)
triangle:vector(568.606,539.411,586.991,577.27,592.505,573.412):argb=(1,1,1,1)
//after
triangle:vector(824.138,932.741,728.086,745.995,547.565,838.845):argb=(1,1,1,1)
triangle:vector(547.565,838.845,643.616,1025.59,824.138,932.741):argb=(1,1,1,1)
triangle:vector(558.639,534.024,592.008,535.425,590.503,537.489):argb=(1,1,1,1)

あれ?ちょっと違う…。
いろいろ試した結果、spSkeleton_updateWorldTransform(ワールド座標などをスケルトンに反映)を抜いてみると

//before
triangle:vector(824.137,932.741,728.086,745.995,547.565,838.845):argb=(1,1,1,1)
triangle:vector(547.565,838.845,643.616,1025.59,824.137,932.741):argb=(1,1,1,1)
triangle:vector(568.606,539.411,586.991,577.27,592.505,573.412):argb=(1,1,1,1)
//after
triangle:vector(824.137,932.741,728.086,745.995,547.565,838.845):argb=(1,1,1,1)
triangle:vector(547.565,838.845,643.616,1025.59,824.137,932.741):argb=(1,1,1,1)
triangle:vector(568.606,539.411,586.991,577.27,592.505,573.412):argb=(1,1,1,1)

やったーおなじだー。
spSkeleton_updateWorldTransform のドキュメントを見ると「IKやら物理計算も反映させます」とあるので、それかなと思うのですが、サンプル「spineboy」には設定されてないような……。


これ以上は実際に表示させて詰めるしか無さそうです。
なお、復元直後に_savestateして保存したアニメーション情報を見てみると一緒だったので、アニメーション情報に関してはちゃんと復元されているようです。

忘れてました、emptyアニメーション

アニメーションをその状態で停止させる命令に EmptyAnimation というのがあります。これは特殊なアニメーションになるので、対応させるには上記にちょっと工夫が必要になります。


まず empty をセットすると trackentry の情報がどうなるのかを調べましょう。

spAnimationState_addEmptyAnimation(animationState, 0, 0.0f, 0.0f);  //mixDuration=0.0 delay=0.0

...
  name walk
...
  name 
    duration 0
    tracktime 0
    timescale 1
    loop 0
    delay 0.7667
  name run
    duration 0.8
    tracktime 0
...

名前が になるのが特徴です。そのほかのパラメータは特に変わりはありません。
mixDurationは前のモーションとミックスブレンドするものだと思われます。停止させるのにミックス?よくわかりませんが、試しに1秒を指定してみると。

spAnimationState_addEmptyAnimation(animationState, 0, 1.0f, 0.0f);  //mixDuration=0.0 delay=0.0

...
  name walk
...
  name run
    duration 0.8
    tracktime 0
...

消えました。…なんで?
コードを追うと spAnimationState_update あたりで消滅しています。
最後に追加しないとだめよ!という事で最適化されたのかもしれませんが、コードを追ってもよく分からなかったので……見なかったことに。

emptyアニメーションの判断

このemptyアニメーションなんですが、trackentryから判別する手段がこれといって用意されているわけではありません。なので、自前で判別する必要があるのですが、大きく2パターンあります。

  • 名前から判別

entry->animation->name の名前が となるのが特徴です。
たぶんこの仕様は変わらないと思います。たぶん。(なおconstで名前が定義されてたりはしません)
くれぐれも自分が作ったモーションに なんて名前を付けてはいけません。ってマニュアルには書いてなかったので、世界中で数人ぐらいはこれで苦しんだ人も居たかも知れません。

  • animetionポインタのアドレス

もっとプログラム的に判別するなら AnimationState.c ファイルの戦闘を見るとemptyアニメーションがちゃっかり static されてます。

static spAnimation* SP_EMPTY_ANIMATION = 0;

いわゆるひとつのしんぐるとんで、entry->animation が SP_EMPTY_ANIMATION と同値であればemptyだと判断できます。
問題はこの SP_EMPTY_ANIMATION がexternされていないことなんです……。


というわけで、名前なりで判断して、loadstateのところを以下のように切り替えてあげましょう。

void _loadstate_tracks(std::stringstream* ss, spAnimationState* animation, int no) {
...省略...
		spTrackEntry* entry;
		if (i == 0) {
			if (animationname == "<empty>")
				entry = spAnimationState_setEmptyAnimation(animation, no, 0.0f);
			else
				entry = spAnimationState_setAnimationByName(animation, no, animationname.c_str(), loop);
		}
		else {
			if (animationname == "<empty>")
				entry = spAnimationState_addEmptyAnimation(animation, no, 0.0f, delay);
			else
				entry = spAnimationState_addAnimationByName(animation, no, animationname.c_str(), loop, delay);
		}
		entry->trackTime = tracktime;
		entry->timeScale = timescale;
...省略...

なお、delayの値がどうにも-999.7みたいな値を示すこともあるので、設定しないほうが良いのかも知れません。わからん。