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 ... nameduration 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みたいな値を示すこともあるので、設定しないほうが良いのかも知れません。わからん。
Netjs
Netjsもduocodeと同じくC#からTypeScript(JS)へ変換するプロジェクトです。ちょっと試してみましょう。
インストール
- Netjs
https://github.com/praeclarum/Netjs
これはduocodeほど環境を必要とされません。githubからcloneして、コンパイルしてexeとして使用します。
出力はTypeScriptなので、まだならその環境もインストールしておきましょう。
変換
c#のクラスモジュール、特にPCL(ポータブル・クラス・ライブラリ)が望ましいみたいです。
C#でDLLで吐かせた後、そのDLLからTypeScriptに変換します。
公式の通り
netjs Library.dll
でtsが出力されます。
これも mscorlib 依存ですが、そのファイルは一緒に出力されません。どこにあるのかというと、cloneしたもののトップディレクトに「mscorlib.ts」として存在しするので、適当にコピーして環境にもってきましょう。
コンパイルはバージョンの指定「-t ES5」が必要です。無いと怒られます。
tsc -t ES5 mscorlib.ts Library.ts --out out.js
で、1つのファイルにまとめられて完了です。
duocodeと比べると「mscorlib.ts」のファイル容量がかなり少ないです。これはtsによるところもあるでしょうが、バージョンが古いみたいです。
例えば下記のようなクラスを変換してみたところ
public class text { /** * A to Z change low case. **/ static public string lowercase(string s) { int len = s.Length; //StringBuilder sb = new StringBuilder(len); // error unsupport capacity StringBuilder sb = new StringBuilder(); for (int i = 0; i < len; i++) { char c = s[i]; if ((c >= 'A') && (c <= 'Z')){ c = (char)((int)c + 32); } sb.Append(c); } return sb.ToString(); } } ....
StringBuilder Class は用意されていますが、StringBuilder.capacityが未実装で、変換後にエラーが出ました。
github も2年前に更新となっているので、長らくプロジェクトは停滞しているみたいです。
まとめ
duocodeに比べるとTypeScriptで出力されるのがとても良い感じなのですが、プロジェクトとしてはバージャンアップが望めないみたいなので他を当たるのが良さそうです。
duocode - C# to JS
今までキワモノだぁ!と思っていたdocodeをちょっと試してみたので、忘れないように書き残しておきます。
主にマルチプラットフォームに耐えうるか調べてみます。
- duocode
http://duoco.de/
doccode それはC#をJavaScriptに変換してくれる素敵なツール。VSコンパイラの機能で行っているようです。
30日間無料があるのでそれで使用感をつかみたいと思います。
インストール
インストールは公式にそって大まかに進めてください。
ただし、以下の2点が大まかな説明からは抜けているので、個別にインストールしないとコンパイルできません。。
- iis express のインストール
https://www.microsoft.com/ja-JP/download/details.aspx?id=48264
webアプリのプロジェクト作成に必要になります。もちろんiisが入っていれば不要でしょう。
- node watcher
nodeアプリのデバッグに必要なようです。
実行時に「無いのでこのコマンドでインストールしてね」と出るので省略します。
自分の環境では壮大にインストールエラーになりましたが、その後デバッグ可能になりました。コワイです。きっと機能不完全だと思います。
今回は使わないのでパスします。
プロジェクト
大きく分けて3種類のプロジェクトが選択可能になります。
- webアプリ(html)
- クラスモジュール
- nodeJSアプリ
感触をつかむには最初はwebアプリがいいでしょう。
webアプリデバッグの注意点
さっそく売りの1つのVS上でのデバッグを試してみたくなりますよね。
サンプルにも「ここにブレークポイントを置いてみよう!」なんてコメントもあります。わくわくしますね。
設置して実行すると、あ…豪快に通過しますね…ダメじゃないですか。
理由はリモートターゲット(webなのでブラウザ)によって対応してないみたいです。
そんな…Edgeちゃんここでもダメなの?IEだとちゃんと止まるようですね。しかしめっちゃ重い……。
Edgeの場合はブラウザ上でデバッグすれば問題ありませんでした。吐き出されたJSにSourceMapコードがついてるので、対応ブラウザでちゃんと元のC#ファイルの箇所を示してくれるようです。
うん。
クラスモジュールの作成
慣れてきた(?)ところで、最小構成とマルチプラットフォームの程度が知りたいのでクラスモジュールを試します。
この機能はC#の一連のクラスモジュールをそのままJSのモジュールに変換します。デフォルトでは自動的に一個のファイルの形に結合して落とし込んでくれます。
新規プロジェクトを選ぶと、空のクラスが作成されます。適当に書いて弄ってコンパイル。
script\ フォルダに返還後のJSファイルなどが出力されます。
ClassLibrary1.dll // C#クラスモジュール ClassLibrary1.js // 出力JS ClassLibrary1.js.map // JS SourcaMap debug ClassLibrary1.pdb // VS SourceMap debug mscorlib.d.ts // type script 定義ファイル mscorlib.js // .net 基礎ライブラリのJS版 mscorlib.min.js // mscorlib.js最適化版。改行と空行を無くしたもの
mscorlib と一緒に出力・使用されるのが duocode の特徴です。
動作させるには mscorlib.js/mscorlib.min.js と ClassLibrary1.js の2つを使用します。
webや組み込みであれば
<script src="mscorlib.js" /> <script src="ClassLibrary1.js" />
という感じで2つのソースを連結すれば動作します。
またnodeJSなら
require("mscorlib.js");
を ClassLibrary1.js の先頭に追加すれば動作します。
なお、変換されたJS(ClassLibrary1.js)はこんな感じになっております。
(function ClassLibrary1() { "use strict"; var $asm = { fullName: "ClassLibrary1", anonymousTypes: [], types: [], getAttrs: function() { return [new System.Reflection.AssemblyTitleAttribute.ctor("ClassLibrary1"), new System.Reflection.AssemblyDescriptionAttribute.ctor(""), new System.Reflection.AssemblyConfigurationAttribute.ctor(""), new System.Reflection.AssemblyCompanyAttribute.ctor(""), new System.Reflection.AssemblyProductAttribute.ctor("ClassLibrary1"), new System.Reflection.AssemblyCopyrightAttribute.ctor("Copyright \xA9 2017"), new System.Reflection.AssemblyTrademarkAttribute.ctor(""), new System.Reflection.AssemblyCultureAttribute.ctor(""), new System.Reflection.AssemblyVersionAttribute.ctor("1.0.0.0"), new System.Reflection.AssemblyFileVersionAttribute.ctor("1.0.0.0"), new DuoCode.Runtime.CompilerAttribute.ctor("3.0.1654.0")]; } }; var $g = (typeof(global) !== "undefined" ? global : (typeof(window) !== "undefined" ? window : self)); var ClassLibrary1 = $g.ClassLibrary1 = $g.ClassLibrary1 || {}; var $d = DuoCode.Runtime; $d.$assemblies["ClassLibrary1"] = $asm; ClassLibrary1.Class1 = $d.declare("ClassLibrary1.Class1", 0, $asm); $d.define(ClassLibrary1.Class1, null, function($t, $p) { $t.ctor = function Class1() { $t.$baseType.ctor.call(this); console.log("constructor :)"); }; }); return $asm; })(); //# sourceMappingURL=ClassLibrary1.js.map
TypeScriptの出力と決定的に違うのはすべてが mscorlib に依存したコードで出力されることです。細かい部分も.netをエミュレートしているような印象が強いです。よく見る.ctorなんかconstrucotrですね。
このソースを保守管理するの現実的なレベルではありません。ソースマップがあるとはいえ実行時エラーはかなり厳しい宇宙言語的な内容を返すでしょう。
デバッグや開発は基本的にC#で完結する環境を作って行い、多言語との受け渡しとしてDLLみたいな(もう中身は弄らないぞ!)感じで扱うなら問題無さそうです。とはいえ、DLLに比べれば追跡もエラー箇所特定も初期状態で恵まれた環境なのは確かです。
mscorlib.jsを許せるかどうか
このファイルの存在が duocode を使用した際の最大の課題点です。
.netの基礎部分を記したもので、duocodeで変換を行う限りこれに依存します。クラスの根幹部分もこれに依存しているため事実上分離は不可能です。サイズが mscorlib.js(864K) / mscorlib.min.js(471K)と結構あります。今後も増えるでしょう。容量と初回起動時のコストで評価が分かれると思います。個人的には規模の割に十分軽いと思いますが。
また、外部から使用する際は.netエミュレータの方言があるので、一層自前インターフェイスクラスで包んであげたりする必要があるでしょう。ただし、TypeScriptを使用するのであればこの辺の問題はパス出来そうです(後記)。
吐き出されたコードによるオブジェクト(クラス)の展開は、ちゃんとグローバルに構造を展開してくれているので、プログラムや他のJSから容易に引っ張ってこれる点はとても気が利いています。グローバル汚染と言われるかも知れませんが…。
所感
C#移植とおもって使うと肩すかしを食らいますが、言語変換としてみればとてもよく出来てると思います。デバッグまで付いてきますし。
単なるwebページに使うにはちょっと大がかりかなーというのは確かですが、C#におけるPCL(ポータブル・クラス・ライブラリ)の一種として使用するなら十分に運用の可能性があると思われます。
C#上で環境を構築、テストを行い、その一部モジュール(ただし環境依存性が無いこと)を色んなところで使いたいなら協力だと思います。
サンプルのような1からwebアプリを作るのは他の色んなJSライブラリを入れることを考えると……ちょっと現実的じゃない気がします。すべてC#で組む勢いならいいですが、結局既存のJSライブラリとの連携がどうにもならないので…。
nodeJSアプリについても同様の不安が残ります。データベースや各種HTTPのシェイクハンドなども言語依存性が高いですし…。
あくまでC#による一部資産を生かすための手段の1つ以上のことは望まないほうが幸せかな?と言った印象です。
おっと忘れてましたが、売りのVS上でのデバッグについては、思った以上に素直に動きませんでしたので残念な印象です。まだ産まれたばかりですし今後に期待しましょう。
おまけ、ChakraCoreで動かす場合
基本的には1つのファイルに結合すれば問題ないのですが、一点だけエラーが発生します。
var $g = (typeof(global) !== "undefined" ? global : (typeof(window) !== "undefined" ? window : self));
グローバルオブジェクトの取得で失敗します。検索する3つの変数がどれもデフォルトでは存在しません。まぁそうですよね。
global、windowsをプログラム側で用意してやれば動作します。selfでも大丈夫だと思いますが、他のオブジェクトのプロパティでも使われているのでどうかなーといったところです(参照優先順位的にだいじょうぶなのは確認済みです)
JsGetGlobalObject(globalref); JsGetPropertyIdFromName("global",idref) JsSetProperty(globalre,fidref,globalref); // (global).global = global
そろそろglobal参照を定義しませんか?JSさん…
おまけ、TypeScript出力
VSプロジェクト設定から選ぶことができま…あれ?declare?
/// <reference path="./mscorlib.d.ts" /> declare module ClassLibrary1 { // ClassLibrary1.Class1 export interface Class1 extends System.Object { } export interface Class1TypeFunc extends TypeFunction { (): Class1TypeFunc; prototype: Class1; new (): Class1; ctor: { new (): Class1; }; } var Class1: Class1TypeFunc; }
TypeScriptの定義ファイル(d.ts)が出力されるだけでした。今まで通りJSも出力しとくから最後に勝手に繋げて使ってね!と言うことみたいです。
クラスに関する取り扱いはTypeScriptを通した方が断然楽だと思うので、変換したものにTypeScriptでインターフェイスを書いて一層噛ましてあげるとプログラムとの連携がスムーズになりそうです。全部プログラムで済まそうとするときっと大変です。
mscorlibの対応状況
Microsoftのバックアップもあるので大丈夫かと思うのですが、今のところStringBuilderなどの主要クラス、プロパティもちゃんと実装されておりました。
ただ、ローケルとかあのあたりは避けるべきでしょう。DateFormatあたりは調べておいた方が良さそうです。文字コード変換なども諦めて自前実装しましょう。と、このあたりはマルチプラットフォーム前提だとしょうがないですね。どの言語でも似たようなものです。
ChakaraCore + TypeScript
ChakraCoreに手を出したのは5割ぐらいはTypeScriptしてみたかったからです。
アプリ組み込みでどんな感じに楽できるのかやってみましょう。
TypeScriptインストール
TypeScriptは簡単に言ってしまえば動的言語を静的言語にして、ヒューマンエラーをコンパイラで出来るだけ検出させる仕組みです。
function型も引数による定義付けが(違う引数のfunction型を弾く)可能です。
- TypeScript
https://www.typescriptlang.org/
nodeJSのバージョン管理ツール上で提供されていますので、npm経由で取得します。詳細は省略します。オフィシャルに書いてありますし。
nodeJsがまだの場合はインストールが必要です。
Editor
https://code.visualstudio.com/
対応エディタはいろいろありますが、とりあえずVSCodeでも使ってみましょう。デフォルトで入れた瞬間から対応していますし、リアルタイムにコード補間とエラーチェックまでしてくれます。どのあたりまで補完&エラー補助してくれるのかも試す意味で使ってみましょう。
コンパイル
まず最小構成を作ってコンパイルしてみます。
- main.ts
function main(s:string):void{ console.log(s); } main();
VSCodeのターミナルからTypeScriptコンパイラである「tsc」を使用し、JSを出力させます。
tsc main.ts
- main.js
function main(s) { console.log(s); } main("hallo world :)");
ここまでが基本です。
日本語も使うと思うので保存の際は一応bomをつけておきましょう。
VSCodeだと右下の文字コードをクリックすると保存形式の案内が出ます。
組み込み関数の定義
プログラム(アプリ)側から関数を組み込んだ場合、そのような関数は定義されていませんよ?とエラーが発生します。
どういった関数を組み込んだかを定義する .d.ts ファイルの作成が必要になります。
専用の定義構文が必要なのですが、ts からコンパイラで自動生成してくれる機能があるので、それを使うと簡単です。
- app.ts
// void imp_print(string s); function imp_print(s:string){ }
ターミナルから -d オプションでコンパイル。
tsc -d app.ts
- app.d.ts
// void imp_print(string s); declare function imp_print(s: string): void;
これを先ほどの main.ts に読み込ませます。
- main.ts
/// <reference path="./app.d.ts" /> function main():void{ imp_print("hello world :)"); } main();
これで組み込み関数 imp_print が使用可能&インテリセンスが利きます。
VSCodeの場合は、なぜかもう一度コンソールで .d.ts を生成しなおさなくても、.ts があればそちらから情報をとってくるみたいです。
とはいえ、作業中は恩恵にあずかり、最後はちゃんと生成したほうがいいでしょう。
jsdoc(ドキュメントコメント)
jsdocコメントもつけちゃうと、.d.ts でもインテリセンスの際に説明文が出ます。
- app.ts
/** * 組み込み。コンソール出力。 * @param {string} s 文字列 */ function imp_print(s:string):void{ }
ファイル分割と結合
早速難物がやってきました。
開発においてはモジュールは分割して表記、管理します。ただし、JavaScriptは今のところまだまだモジュール組み込みの環境が整っていないのもあり最終的に1つのファイルにまとめる必要があります。これはChakraCoreにおいても同様です。
単に結合すれば良いかというと大きく分けて二種類が存在します。
TypeScriptコンパイラが持つ単純な結合方法、commonJS形式(AMD形式など)によるrequire命令を使用する方法。
この2つの手法をミックスできれば問題では無いのですが、TypeScriptコンパイラはそれを許していません。また、2つそれぞれの方法で最終的に同じようなJSを出力しようとすると、ソースの表記が若干変わってくる点も問題です。コンパイラマクロでもあれば良かったのですが…。
というわけで、JSとしてもライブラリを出力したい!webとnodeJS両対応にしたいなども考え出すと、非常に頭が痛い問題です。
commonJS方式、require使用
この方式は最終的に require によるファイルのインポート命令に展開され、ファイルは結合されません。なので出力されるJSが元に近いので(この段階では)可読性があります。
また今後もファイルインポート周りで手が入る手はずなので将来性は保証されていると思います。
最大の欠点は、このままではChakraCoreが読み込んでくれないということです……。
まず、sub.ts を main.ts にインポートする構成を作成。
- sub.ts
/// <reference path="./app.d.ts" /> export function sub_test():void{ mp_print("sub test :)"); }
- main.ts
// main.ts /// <reference path="./app.d.ts" /> import * as sub from "./sub"; function main(s:string):void{ imp_print(s); sub.sub_test(); //sub.ts } main("hallo world :)");
sub.ts は export を忘れずに。export が1つも無いと main.ts 側でエラーが出ます。最初なんだこれ?って思いました。
ではコンソールでコンパイルします。main.ts を指定するだけで関連するソースをすべてコンパイルしてくれます。ありがたい。
tsc main.ts -module commonjs
- main.js
"use strict"; exports.__esModule = true; /// <reference path="./app.d.ts" /> var sub = require("./sub"); function main(s) { imp_print(s); sub.sub_test(); //sub.ts } main("hallo world :)");
吐き出された main.js には require が挿入されています。が sub.js はこの段階では組み込まれていません。
また exports というのが追加されていますがこの子はモジュール単位の子でグローバルな情報としては使えません。
nodeJSならここで終わり。
JSエンジンにぶちこんで動かすには、これらを1つのファイルに結合しないといけません。
結合にはweb開発で主流な webpack とか browserify があります。
browserifyで結合
- browserify
http://browserify.org/
何回書いても覚えられないつづりの browserify を試してみます。インストールはnpm任せです。
インストールの後、コンソールから結合します。
browserify main.js -o out.js
- out.js
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ "use strict"; exports.__esModule = true; /// <reference path="./app.d.ts" /> var sub = require("./sub"); function main(s) { imp_print(s); sub.sub_test(); //sub.ts } main("hallo world :)"); },{"./sub":2}],2:[function(require,module,exports){ "use strict"; /// <reference path="./app.d.ts" /> exports.__esModule = true; function sub_test() { imp_print("sub test :)"); } exports.sub_test = sub_test; },{}]},{},[1]);
ごちゃごちゃっとしてますが、即時実行関数になって、独自 require 関数を定義の後、中のコードを実行してる感じです。ちゃんと動きます。
さて、これの問題は、main()などの位置がtsで書いたつもりの位置と微妙に異なることです。グローバルオブジェクト直下じゃありません。なので、プログラムから引いてくるのが困難なことです。
よく見ると「exports」というのが存在しますがこれもモジュール単位なので使えません。
結局なにも手を加えない状態ではアプリからは完全に孤立した空間になるので、アプリとの連携が絶望的です。
グローバルで値をやりとりするなら、JS側でグローバルを取得、そしてそこへ外に出したいものを手動で書くほかないかなと思います。
ただし、グローバルの取得はちょっと細工しないと環境依存の元なので、npmから取得するのが良いみたいです。
- get-global
https://www.npmjs.com/package/get-global
もう一つは、グローバルを一切介さない手法にするのが、モダンかも知れません。
ユーザー専用の受け渡しテーブルを作ってやり取りするか(アプリ側が作成、またはJSで作成した場合はアプリ側へ通達)、またはアプリ側が呼ぶ取得用関数をJS側から通達するのが良いのかも知れません。
// entry call function from app imp_entryappevent(app_oninitialize,"initialize"); imp_entryappevent(app_onupdate,"update"); ...
webではクリーンな感じですが組み込みで使うには面倒な感じですね。
TypeScriptコンパイラで結合
次はTypeScriptコンパイラによる結合です。と言っても、もう使ってますが「///
require と違ってそのまま追加するだけなので、import のように指定の名前空間に変更することができません。
なので最終的に同じようなJSに展開させようとすると、ソース側に namespace{} を追記する必要があります。
- sub.ts
/// <reference path="./app.d.ts" /> namespace sub { export function sub_test():void{ imp_print("sub test :)"); } }
- main.ts
/// <reference path="./app.d.ts" /> /// <reference path="./sub.ts" /> function main(s:string):void{ imp_print(s); sub.sub_test(); //sub.ts } main("hallo world :)");
コンソールから--outFile を使って結合します。この時--module commonjsなどは使用できません。
import 構文などを使っているとエラーが発生します。
tsc main.ts -outFile out.js
- out.js
/// <reference path="./app.d.ts" /> var sub; (function (sub) { function sub_test() { imp_print("sub test :)"); } sub.sub_test = sub_test; })(sub || (sub = {})); /// <reference path="./app.d.ts" /> /// <reference path="./sub.ts" /> function main(s) { imp_print(s); sub.sub_test(); //sub.ts } main("hallo world :)");
JSとして見るならかなりシンプルな展開です。ちゃんとグローバルオブジェクトに展開しています。逆に exports がなくなりました。
この時点で1つのファイルに結合されているので、ちょっとでも規模が大きくなると可読性は最低ですし、各ファイルのJSも出力されません。
プログラムから値引いてくる場合はこちらのほうがとても素直な印象です。
分割したい
手動でぶつ切り、後でプログラムでテキスト的に結合という手段がありますが、browserifyだともうちょっと賢い手法が使えます。
パラメータ -x で目的のモジュールを抜くことで分離が可能です。
browserify main.js -o out.js -x sub.js
- main.js
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ "use strict"; // application implement function /// <reference path="./imp.d.ts" /> exports.__esModule = true; /// <reference path="./sub.ts" /> var sub = require("./sub"); var main; (function (main_1) { main_1.g_value = "test code"; function app_initialize() { imp_print("initialize"); } function app_finalize() { imp_print("finalize"); } function app_update() { imp_print("update"); } function main() { sub.test("aaaaa"); return "script run"; } imp_function(app_initialize, "initialize"); imp_function(app_finalize, "finalize"); imp_function(app_update, "update"); main(); })(main = exports.main || (exports.main = {})); },{"./sub":"/sub.js"}]},{},[1]);
sub.ja 相当のコードが抜かれても、requireなどはちゃんと残ってます。
で、sub.jsを突っ込めば動くかというと動かなくて、require が独自定義というわけなのでsub.jsを準じた感じに変換しないといけないわけです。公開モジュールというわけだ。
browserify -r sub.js -o sub_r.js
エラー。ファイルがありません。うん????
browserify -r ./sub.js -o sub_r.js
ハメですかねこれ。ハメですね。
webであれば
<script src="sub_r.s" /> <script src="out.js" />
みたいな感じで動くようになるので、要するに out.js の最初にこのコードを付け加えれば動きます。
たまにエラーが出ました。
原因は不明ですが公開モジュールの名前が変になったことがあります。これだと sub_r.s の1行目の453列あたりに表記されていますので確認すると良いでしょう。
正解のケースでは「./sub.js」という名前になっていますが、エラーが出た場合は違う書式になっているかも知れません。
filename : out.js error : ScriptError description : Cannot find module '/sub.js' message : Cannot find module '/sub.js'
とエラーに要求名が書かれるのでそれに合わせれば解決できるでしょう。
「:」で強引にモジュール名を変えることも可能です。
browserify -r ./sub.js:./hogehoge.js -o sub_r.js
整合性がとれなくなるので、使うことはあんまり無いとは思いますけど。
所感
TypeScriptを使えばJSの面倒なところも全部見てくれるぜ!って訳でもなく、良くも悪くもしっかりJSの上に乗っかってる感があります。
個人的にはどうにかcommonJS型(モジュール形式でファイル分割)で開発したいのですが、nodeJS以外のマルチプラットフォームも考慮するのであればTypeScritコンパイルで結合して1つのファイルにする方が無難かな……というところで落ち着きました。
散々browseなんとかの説明までしておいて申し訳ないです。いつか役に立ちますって…たぶん:Q
ChakraCore implement 2
長くなるので分けました。
使用する関数は1つ前の記事を参照と言うことで。
エラーハンドリング
開発で一番大切なところ、エラー表示です。
組み込む以上、nodeJs開発環境みたいな気の利いたデバッガは存在しません。でもせめて、エラー箇所の特定ぐらいの情報は欲しいのですが、残念ながらサンプルにはその破片も書かれておりません。
探したところ、ChakraCoreソリューションにある ch プロジェクトのソースがヒントになります。WScriptJsrt.cpp の PrintException がそのものズバリです。
使用箇所を抜粋すれば
... runScript = ChakraRTInterface::JsRun(scriptSource, sourceContext, fname, JsParseScriptAttributeNone, nullptr /*result*/ ); if(runScript != JsNoError){ WScriptJsrt::PrintException(fileName, runScript); }else{ ...
RunScriptでエラーが出たらそのまま収集を開始しています。特にcontextやruntimeなどの情報は要らないようです。
PrintExceptionの中身はgithubでご覧ください。
一目見てうっ!って感じですが、大まかな流れはシンプルです。
bool WScriptJsrt::PrintException(LPCSTR fileName, JsErrorCode jsErrorCode) { LPCWSTR errorTypeString = ConvertErrorCodeToMessage(jsErrorCode); ChakraRTInterface::JsGetAndClearException(&exception); if (exception != nullptr) { if (jsErrorCode == JsErrorCode::JsErrorScriptCompile || jsErrorCode == JsErrorCode::JsErrorScriptException) { if (jsErrorCode == JsErrorCode::JsErrorScriptCompile) { } else { } } else { fwprintf(stderr, _u("Error : %ls\n"), errorTypeString); } return true; } else { fwprintf(stderr, _u("Error : %ls\n"), errorTypeString); } return false; }
errorによって大きく3つにわかれます。コンパイルエラー、実行時エラー、それ以外のエラー。
詳しく調べられるのは、コンパイルと実行時の2つだけ。その他は基本的に JsErrorCode の列挙名を文字列にして返すだけみたいです。
エラーは JsGetAndClearException で得られる exception が詳しい内容を保持しています。exception もJsValueRefであることから、propertyをもった単なるjavascript上のオブジェクトです。(たぶんjavascript上からも値を取得してゴニョゴニョ出来るでしょう)
と思って JsGetValueType するとJsError型と帰ってきました。一応型としては分かれているみたいです。
Exceptionの中身
エラー情報を取得する jsGetAndClearException には以下の説明が載っています。
JsGetAndClearException 関数 現在のコンテキストのランタイムを例外状態にした例外を返し、そのランタイムの例外状態をリセットします。 現在のコンテキストのランタイムが例外状態でない場合、この API は JsErrorInvalidArgument を返します。ランタイムが無効である場合は、スクリプトが終了したことを示す例外を返しますが、例外はクリアしません (JsEnableRuntimeExecution を使用してランタイムを再度有効化すると、例外はクリアされます)。
プログラムが複雑になってくると JsEnableRuntimeExecution なども使用することになってきそうです。
取得出来る exception にどんな property があるのか知りたかったのですがちょっと自力で調べないといけないようです。
エラー型もオブジェクト型みたいなものなので、普通のオブジェクトと同じように中身を出力してみると。
・JsErrorScriptCompile { message : Expected '(' line : 10 column : 4 length : 2 source : for in c of{ url : sample01.js } ・JsErrorScriptExceprion { message : 'o' is not defined description : 'o' is not defined number : -2146823279 stack : ReferenceError: 'o' is not defined \r\n at callback_test (sample01.js:12:5) }
期待したほど入っていませんでした。ほぼ上記のサンプルで情報は出し切っているみたいです。
エラー出力
exception の内容はスッキリしてるので、ぶっちゃけ、エラー表示はオブジェクト内容をそのまま出力すれば良いだけなような気がしてきました。
というわけで、オブジェクト内容を表示するコードさえ書けば以下のような感じに。
exception の型がJsError型であることに注意してください。
// print object property function PrintObject(obj:JsValueRef; space:string=''):string; function inToString(ref:JsValueRef):string; var t : JsValueType; strref : JsValueRef; begin result := ''; JsGetValueType(ref,t); if t = JsString then begin result := GetString(ref); end else begin if jsConvertValueToString(ref,strref) = JsNoError then result := GetString(strref); end; end; var names : JsValueRef; namecount : integer; ref,idref : JsValueRef; t : JsValueType; i,n : integer; name,value : string; begin // is object or error? JsGetValueType(obj,t); if (t <> JsObject)and(t <> jsError) then begin result := inToString(obj); exit; end; result := ''; JsGetOwnPropertyNames(obj,names); // get propert name array object : ["aaa","bbb","cccc"] ref := GetProperty(names,'length',JsNumber); // get array count if ref = nil then exit; JsNumberToInt(ref,namecount); for i:=0 to namecount-1 do begin ref := GetArray(names,i,JsString); if ref = nil then continue; name := GetString(ref); if JsGetPropertyIdFromName(pwchar_t(name),idref) <> JsNoError then continue; if JsGetProperty(obj,idref,ref) <> JsNoError then continue; value := inToString(ref); value := space + name + ' : ' + value; if result = '' then result := result + value else result := result + _nn + value; end; end; // error handling function ScriptErrorHandling(error:JsErrorCode; const filename:string=''):string; var exception : JsValueRef; begin if error = JsNoError then exit; result := 'filename : ' + filename; // file , url , function name result := result + _nn + 'error : ' + jsErrorToString(error); // JsErrorCode(int) -> string //exception object if jsGetAndClearException(exception) = JsNoError then begin result := result + _nn + PrintObject(exception,''); end; result := '----------' + _nn + result + _nn + '----------'; end;
こんなにスッキリとしたエラーハンドルになります。
ついでに exception.toString() もやってみたんですが、出力される内容が異なりました。
具体的には行数などのnumber型が抜かれちゃったので、やっぱり全propertyを調べ上げて全出力するのが良さそうです。
関数実行のエラーハンドリング
JsRunScript の他にもエラーハンドリングが必要な箇所があります。関数実行 JsCallFunction です。
この2つの関数は引数も似ていれば、大本(c++)の内部も似ています。
なので、luaでもそうでしたが、同じようにエラーハンドリング出来るかな?と思ったら出来ました。
// Run the script. error := JsRunScript(pwchar_t(script), @currentSourceContext, pwchar_t(filename), resultValue); // error result := ScriptErrorHandling(error,filename); ... //call function error := jsCallFunction(ref,PJsValueRef(params),paramcount,retvalue); // error result := ScriptErrorHandling(error,'*function:'+name);
エラーをわざと発生させると
・JsErrorScriptCompile ---------- filename : sample01.js error : SyntaxError message : Expected ';' line : 10 column : 23 length : 9 source : dsnfkjssnfkjsmdljkdss()dfslkdlas; url : sample01.js ---------- ・JsErrorScriptException ---------- filename : *function:callback_test error : ScriptError message : 'o' is not defined description : 'o' is not defined number : -2146823279 stack : ReferenceError: 'o' is not defined at callback_test (sample01.js:14:5) ----------
やったー!
おまけ グローバルオブジェクトの中身
中身です。最後の imp_??? はプログラム側から追加した関数です。
NaN : NaN Infinity : Infinity undefined : undefined eval : function eval() { [native code] } parseInt : function parseInt() { [native code] } parseFloat : function parseFloat() { [native code] } isNaN : function isNaN() { [native code] } isFinite : function isFinite() { [native code] } decodeURI : function decodeURI() { [native code] } decodeURIComponent : function decodeURIComponent() { [native code] } encodeURI : function encodeURI() { [native code] } encodeURIComponent : function encodeURIComponent() { [native code] } escape : function escape() { [native code] } unescape : function unescape() { [native code] } CollectGarbage : function CollectGarbage() { [native code] } Object : function Object() { [native code] } Array : function Array() { [native code] } Boolean : function Boolean() { [native code] } Symbol : function Symbol() { [native code] } Proxy : function Proxy() { [native code] } Reflect : [object Object] Promise : function Promise() { [native code] } Date : function Date() { [native code] } Function : function Function() { [native code] } Math : [object Math] Number : function Number() { [native code] } String : function String() { [native code] } RegExp : function RegExp() { [native code] } ArrayBuffer : function ArrayBuffer() { [native code] } DataView : function DataView() { [native code] } Int8Array : function Int8Array() { [native code] } Uint8Array : function Uint8Array() { [native code] } Uint8ClampedArray : function Uint8ClampedArray() { [native code] } Int16Array : function Int16Array() { [native code] } Uint16Array : function Uint16Array() { [native code] } Int32Array : function Int32Array() { [native code] } Uint32Array : function Uint32Array() { [native code] } Float32Array : function Float32Array() { [native code] } Float64Array : function Float64Array() { [native code] } SharedArrayBuffer : function SharedArrayBuffer() { [native code] } Atomics : [object Atomics] JSON : [object JSON] Intl : Map : function Map() { [native code] } Set : function Set() { [native code] } WeakMap : function WeakMap() { [native code] } WeakSet : function WeakSet() { [native code] } Error : function Error() { [native code] } EvalError : function EvalError() { [native code] } RangeError : function RangeError() { [native code] } ReferenceError : function ReferenceError() { [native code] } SyntaxError : function SyntaxError() { [native code] } TypeError : function TypeError() { [native code] } URIError : function URIError() { [native code] } WebAssembly : [object WebAssembly] imp_print : function () { [native code] } imp_function : function () { [native code] }
ChakraCore implement
こっそり出したことで有名なMiscrosoft社製JavaScriptエンジンがChakraCoreです。格好よすぎる…。
DLLが出力できるので、C#じゃないDelphiで読み込めるか試してみたので一通り残しておきます。他言語でもヘッダがあれば大体同じなはずです。
- メモリ上読み込みと実行(文字列データから実行)
- 関数の登録、JS関数の呼び出し
- 値の設定と取得
- ユーザー定義クラスの受け渡しと解放
- array,object操作
- エラーハンドリング
あたりが出来ればとりあえずなんとか出来そうですね。(エラーハンドリングは長いので次に記事に分けました)
github
完成ソースコードを置いておきます。ChakraCoreとそのライブラリヘッダ抜かれています。導入については後記。
https://github.com/Ko-Ta2142/ChakraCore_DelphiSample01
ChakraCoreのコンパイル
・ChakraCore
https://github.com/Microsoft/ChakraCore
コンパイルはお任せ致します。
今回は32bitアプリケーションで動作を行うので「x86-release」で作ってみてください。
DLLヘッダファイル
こちらの方のを使用させて貰いましょう。
・ChakraCore Hello-world Sample
https://github.com/hsreina/Chakra-Samples-Delphi
ChakraCommon.h を整形したもので、命令名やパラメータなどは元ソースとほぼ同一です。C#などのサンプルともほぼ同じです。
使用には ツール - オプション - ライブラリ - ライブラリパス にこのライブラリヘッダのパスを追加するだけです。
ちょっとバグがあるので、あとで修正箇所も説明しますね。
DLLとのやりとりは stdcall になります。これは大本(c++)の CHAKRA_API にてexportと一緒に定義されています。
文字コード
ChakraCoreの文字列はwindowsではutf16でのやりとりが一般的です。
なのでDelphiであればstringのままでいいですし、c++ならstd::wstringで受け渡しすることになります。
幸いutfは変換コストが軽いので、utf8との連携でも大したことないでしょう。
まずは最小構成
中にあるサンプルがほぼオフィシャルのサンプルです。
・Delphi
https://github.com/hsreina/Chakra-Samples-Delphi/blob/develop/ChakraCore%20Samples/Hello%20World/Windows/Delphi/HelloWorld.dpr
・C++
https://github.com/Microsoft/Chakra-Samples/blob/master/ChakraCore%20Samples/Hello%20World/Windows/C%2B%2B/HelloWorld/HelloWorld.cpp
さらに実行のみ切り出せば以下になります。
JsCreateRuntime(JsRuntimeAttributeNone, nil, runtime); JsCreateContext(runtime, context); JsSetCurrentContext(context); JsRunScript(pwchar_t(script), @currentSourceContext, pwchar_t(''), result); JsSetCurrentContext(JS_INVALID_REFERENCE); JsDisposeRuntime(runtime);
JsRunScriptでutf16のpwcharバッファからスクリプトを読み込んでいますので、この段階でメモリ読み込みになっててありがたい。
引数の3番目はデバッグ用のファイル特定用のファイル名などを設定します。分かれば良いので何でも良いです。
最後の result は実行後の返値。最初にルート実行して構築するのはluaなどと同じようです。
サンプルの後半はこの return の値を取得して表示させるコードになっています。
script := '(()=>{return "Hello world!";})()';
即時実行と言うんでしたっけ。括弧が多いですが、無名関数がこんにちわ!をreturnするだけです。
function main(){ return "Hello world!"; } main(); //run
アプリに投げるのってreturn main(); じゃないんですね。関数実行を考えればそうかな?そうかもって感じです。
要素(property)操作
Chakraにおいてもluaよろしく、変数データはオブジェクト(テーブル)で構成されているので、まず必要になるのがそれらの要素 property の操作命令です。
一貫して値の操作はオブジェクトの何々〜という操作でやりとりを行います。
get [対象オブジェクト].[要素ID] -> [値] set [対象オブジェクト].[要素ID] <- [値]
みたいな感じですね。
この操作がこれから多くなるので、関数化しておきましょう。
要素IDは JsGetPropertyIdFromName で要素名を設定すれば作ってくれるの楽ちんです。
get とついてますが JsValueRef を生成するのでmakeとかcreateみたいな命令です。既存のものを指定しても新しく作られて置き換えられます。
function SetProperty(obj:JsValueRef; const name:string; value:JsValueRef):boolean; var idref : JsPropertyIdRef; begin result := true; if JsGetPropertyIdFromName(pwchar_t(name),idref) = JsNoError then if JsSetProperty(obj,idref,value,true) = JsNoError then exit; result := false; end; function GetProperty(obj:JsValueRef; const name:string; _type:JsValueType):JsValueRef; var idref : JsPropertyIdRef; t : JsValueType; begin if JsGetPropertyIdFromName(pwchar_t(name),idref) = JsNoError then if JsGetProperty(obj,idref,result) = JsNoError then if JsGetValueType(result,t) = JsNoError then if _type = t then exit; result := nil; end; function DeleteProperty(obj:JsValueRef; const name:string):boolean; var idref : JsPropertyIdRef; ret : JsValueRef; // Whether the property was deleted. begin result := true; if JsGetPropertyIdFromName(pwchar_t(name),idref) = JsNoError then if jsDeleteProperty(obj,idref,true,ret) = JsNoError then exit; result := false; end;
取得の際は厳格性がほしいので型チェックを入れてあります。
ごめん、動かないわ
ソースは問題ないのですが、ここで、ヘッダに少々修正が必要です。
具体的には JsGetValueType を使用すると指定した JsValueRef(const) が変な数値になります。利かねぇconstだな!
原因は列挙型がChakraCoreは4byte(32bit)、Delphi側が初期では最小1byte(8bit)なのが問題です。
解決方法は2つで、プロジェクト設定で列挙型の最小値を4byteにするか、ChakraCommon.pasの先頭にコンパイルオプションを入れます。
unit ChakraCommon; //enum size 32bit {$Z4} interface
この方法はソースコード単位で利くので、こっちをおすすめします。
値の取得と設定
まずはグローバルに変数を作ったり、設定したり、取得したり、削除したりしましょう。
グローバルといっても、操作対象はグローバルという名前のオブジェクトです。このあたりもluaと同じです。
global : JsValueRef; ref : JsValueRef; a : integer; ... JsGetGlobalObject(global); // set / new JsIntToNumber(123456,ref); // double値の生成 SetProperty(global,'g_int',ref); // get ref := GetProperty(global,'g_int',JsNumber); JsNumberToInt(ref,a); Writeln('a is ' + IntToStr(a)); //print // delete DeleteProperty(global,'g_int');
グローバルオブジェクトは JsGetGlobalObject() だけで取得出来ます。あれ?これでは複数仮想機械を作った場合は…。
たぶん JsSetCurrentContext(context); このあたりで切り替えられると思っては居るのですが、まだ試してません。
任意のオブジェクトに対して操作する場合は、global を他のテーブルにすり替えるだけです。
ごめん、また動きません
豪快にクラッシュすると思います。ごめんね。
JsIntToNumber の宣言に問題があるので、ChakraCommon.pas の JsIntToNumber の箇所を検索で見つけて以下に修正してください。
function JsIntToNumber( intValue : int; //var intValue : int; //** fix var value: JsValueRef ): JsErrorCode; stdcall; external DLL_NAME;
var抜くだけです。
さて、これで IntToNumber がちゃんと ValueRef かえすか確認してみると。
ref : 0x0000001F
というアドレスじゃ無い変な値が返ってきます。
あわわクラッシュじゃ!と思ったらちゃんと動きます…。不安。
大本のソースコードを見てみると
//If number is not heap allocated then we don't need to record/track the creation for time-travel if (Js::JavascriptNumber::TryToVarFast(intValue, asValue)) { return JsNoError; }
最適化してるみたいですね。
ししし知ってるわよ!タイムをトラベル、タイムトラベラーでしょ!
文字列
文字列も数値と同じです。
が、やりとりはutf16文字列の生ポインタを返すので、delphiだとstring、c++だとstd::wstringで扱うにはちょいと領域確保+コピーが必要です。
対象JsValueRefの生存期間も考えると、整数や浮動小数と同じくNativeで値を保持しておくのが安全です。
procedure MakeString(var value:JsValueRef; const s:string); begin if JsPointerToString(pwchar_t(s),length(s),value) = JsNoError then exit; raise exception.Create('CreateString.error'); end; function GetString(value:JsValueRef):string; var wptr : pwchar_t; len : size_t; t : JsValueType; begin result := ''; if JsGetValueType(value,t) = JsNoError then if t = JsString then if jsStringToPointer(value,wptr,len) = JsNoError then begin SetLength(result,len); move(wptr[0],result[1],len*sizeof(wchar_t)); exit; end; raise exception.Create('GetString.error'); end;
jsStringToPointer では文字列の先頭アドレスと文字の長さ(個数)を返します。なので、utf16ですからバイト長に変換するなら2 (sizeof(wchar_t)) をかける必要があります。
Delphiのmoveって気持ち悪いよね…。アドレスでは無く実体指定だから異質すぎる。
ユーザー定義オブジェクト型
いわゆるクラスとか構造体をぶち込みたいあなたの願いを叶えるものです。
luaにもありましたがChakraの指定はそれよりもシンプルで且つ分かりやすくなっています。
JsFinalizeCallback = procedure(data: Pointer); stdcall; function JsCreateExternalObject( data: Pointer; finalizeCallback: JsFinalizeCallback; var _object: JsValueRef ): JsErrorCode; stdcall; external DLL_NAME;
ユーザー定義型(ポインタつっこむ型)はintじゃなく専用のを使って定義します。
作るときに解放処理コールバック関数つけちゃえばいいよね。いい…。luaはごにょごにょする必要があったのでとても有り難い仕様です。
procedure ObjectDispose_callback(data: Pointer); stdcall; begin TObject(data).Free; // call super class dispose Writeln( 'object.dispose:0x' + IntToHex(Integer(data),8) ); end; procedure MakeObject(var value:JsValueRef; obj:TObject); begin Writeln( 'object.entry:0x' + IntToHex(Integer(obj),8) ); if JsCreateExternalObject(pointer(obj),ObjectDispose_callback,value) = JsNoError then exit; raise exception.Create('CreateObject.error'); end; function GetObject(value:JsValueRef):TObject; begin // valuetype is JsObject result := nil; if JsGetExternalData(value,pointer(result)) = JsNoError then exit; raise exception.Create('GetObject.error'); end;
DelphiなのでクラスはすべてTObjectの派生という特性を利用してますが、このへんはおこのみで。
解放タイミング、ガベージコレクト
さて、ユーザー定義型でついて回るのがガベージコレクトによる解放タイミング。
luaは解放すると即 dispose 命令を発効してくれましたが、Chakraではそうはいきません。
オブジェクト生成、即解放するような処理でログを出してみると
object.create object.entry:0x023D8120 object.free get array[1]:45600 12300 45600 object.dispose:0x023D8120 // 解放コールバック関数が最後に呼ばれてる
ものの見事に最後でした。
一応ガベージコレクト全域に対してクリアする命令は存在します。
function JsCollectGarbage( runtime: JsRuntimeHandle ): JsErrorCode; stdcall; external DLL_NAME;
これで即時呼ばれると思ったのですが、ちょっと特性があるみたいです。
JsDeleteProperty -> 呼ばれない JsSetPropertyで値を上書き -> 呼ばれる
この挙動はよく分かりません。
関数の登録
変数が出来たので次は関数です。
Chakraの関数はクラスメソッドと同じで、すべて this が最初についた形で読んだり呼ばれたりします。
グローバルであれば、グローバルオブジェクトが this に入ります。
また、JavaScriptはthisの変更を許容しています。変えて良いんですね。変えませんけど…。
大本(c++)の定義を見てみると
CHAKRA_API JsCallFunction( _In_ JsValueRef function , _In_reads_(argumentCount) JsValueRef *arguments , _In_ unsigned short argumentCount , _Out_opt_ JsValueRef *result);
JsValueRef *arguments 、つまりarray(配列)とその個数指定することになっています。
なので、キャスト用構造体でも作っておくと便利です。
type TJsParamsRecord = packed record //packed _this : JsValueRef; values : array [0..31-1] of JsValueRef; //適当な個数です end; PJsParamsRecord = ^TJsParamsRecord;
ではまず関数を登録しましょう。関数はヘッダで宣言されている型と全く同じでないといけません。ちょっとでも違うとメモリ破壊が始まります。コワイ!
JsNativeFunction型がそれです。
type JsNativeFunction = function( callee: JsValueRef; isConstructCall: bool; arguments: PJsValueRef; argumentCount: Word; callbackState: Pointer ): JsValueRef; stdcall;
arguments: PJsValueRef が最初に説明したthisを含む引数の配列です。nil/nullptrで終わってはいないのでちゃんと argumentCount で終端を限定する必要があります。
まずは print(コンソール出力) 的なものから作るのが世の常です。
// JS : print(s:string) // s : string // rreturn : none function implement_print(callee: JsValueRef; isConstructCall: bool; arguments: PJsValueRef; argumentCount: Word; callbackState: Pointer):JsValueRef; stdcall; var s : string; pParams : PJsParamsRecord; begin if argumentCount <> 2 then begin raise exception.Create('callback implement_printf.param error'); end; pParams := PJsParamsRecord(arguments); //cast s := GetString(pParams^.values[0]); Writeln(s); result := nil; end;
文字列引数を1つ持つため、this も足して引数カウントは2になります。
登録はいくつも行うので適当な関数を作って利用するといいでしょう。
procedure EntryFunction(obj : JsValueRef; const name:string; func:JsNativeFunction; state:pointer); var ref : JsValueRef; begin if jsCreateFunction(func,state,ref) = JsNoError then if SetProperty(obj,name,ref) then exit; raise exception.Create('EntryFunction.error'); end; ... EntryFunction(global,'imp_print',implement_print,nil);
JavaScript側からは以下のように使用できます。
imp_print"hello world :");
JavaScript側の関数をnative側から呼ぶ
この場合は jsCallFunction を使用します。
function JsCallFunction( _function: JsValueRef; arguments: PJsValueRef; argumentCount: short; var result: JsValueRef ): JsErrorCode; stdcall; external DLL_NAME;
関数を登録した際と同じように、引数は JsValueRef の配列で指定します。なので先ほど作った構造体を使用すると楽ちんです。
JavaScript側で定義した、文字列を表示させるグローバルなテスト関数を定義しておいて、それを呼ぶ場合は。
params : TJsParamsRecord; ref,subret : JsValueRef; ... // params params._this := global; // need "this." MakeString(ref,'sample message'); params.values[0] := ref; // call ref := GetProperty(global,'callback_test',JsFunction); jsCallFunction(ref,PJsValueRef(@params),2,subret); // callback_test('sample message');
callback_test('sample message'); という形でJavaScript上の関数が呼ばれます。
最後が返値のJsValueRefです。返さない場合もnilではない値が返ります。なんだろう?
さてこうしてみると、返値や、params.values[0] につっこんだ JsValueRef はちゃんと解放されるのか心配です。
リファレンスのRefによれば、「スタックに乗る限り自動的に解放されます」とのことです。なので返値や引数は大丈夫みたいです。
JsGetPropertyIdFromName で作った値なんかもスタックに乗るのかなどちょっと不安ですが、JsGetPropertyを通す以上乗ると思うのでたぶん大丈夫でしょう。
解放に関しては JsRelease という命令がありますが、基本は addRef 使用時に対で使うことぐらいしか触れられていません。
array配列操作
配列操作です。以下のようなものに対して操作します。
[1,2,5,6,8,9,100]
配列操作は JsGetProperty の配列版 JsGetIndexedProperty でおこないますが、インデックスの指定がintではなくJsValueRefになっています。
なので関数を作っておかないとちょっと面倒です。
procedure SetArray(_array:JsValueRef; index:integer; value:JsValueRef); var indexref : JsValueRef; begin if JsIntToNumber(index,indexref) = JsNoError then if JsSetIndexedProperty(_array,indexref,value) = JsNoError then exit; raise exception.Create('SetArray.error'); end; function GetArray(_array:JsValueRef; index:integer; _type:JsValueType):jsValueRef; var t : JsValueType; //len : integer; indexref : JsValueRef; begin result := nil; // length check //JsNumberToInt( GetProperty(_array,'length',JsValueNumber) , len ); //if (len >= index)or(index < 0) then //begin // raise exception.Create('GetArray.out of index'); //end; if JsIntToNumber(index,indexref) = JsNoError then if JsGetIndexedProperty(_array,indexref,result) = JsNoError then if JsGetValueType(result,t) = JsNoError then if t = _type then exit; raise exception.Create('GetArray.error'); end;
配列の長さ取得はjavascript上同様 length プロパティで出来るみたいです。
が、配列外を指定しても JsGetIndexedProperty がエラーを出してくれるので、ここでは必要なさそうです。
オブジェクト操作
もう今までで散々やってきたので、もういいよね。
オブジェクトの中身を一覧表示させたい場合はちょっと特殊な JsGetOwnPropertyNames を使用します。処理としてはpropertyの要素名の配列を取得して、あとはGetPropertyで回すだけ。
procedure PrintObject(obj:JsValueRef); var names : JsValueRef; namecount : integer; ref,idref : JsValueRef; t : JsValueType; d : double; i,n : integer; name,value : string; begin JsGetOwnPropertyNames(obj,names); // get propert name array object : ["aaa","bbb","cccc"] ref := GetProperty(names,'length',JsNumber); // get array count if ref = nil then exit; JsNumberToInt(ref,namecount); for i:=0 to namecount-1 do begin ref := GetArray(names,i,JsString); if ref = nil then continue; name := GetString(ref); if JsGetPropertyIdFromName(pwchar_t(name),idref) <> JsNoError then continue; if JsGetProperty(obj,idref,ref) <> JsNoError then continue; JsGetValueType(ref,t); value := ''; if t = JsString then begin value := GetString(ref); end; if t = JsNumber then begin JsNumberToDouble(ref,d); value := FloatToStr(d); end; WriteLn(' ' + name + ' : ' + value); end; end;
spine c++ Implement
spineのC++組み込みをちょっとやってみました。
オフシャルgithubでテストコードやライブラリは落とせますが、オフィシャルドキュメントにある最小限の構成が欲しかったので、VisualStdio2015で構築したいと思います。
- 生成と解放
- メモリ上読み込み
- モーション設定
- 動かす
- ポリゴンログの表示
- イベントの取得
あたりが目標です。
github
VS2015プロジェクト一式セットですが、Spine-c のランタイムは抜いてありますので揃えてください。
https://github.com/Ko-Ta2142/Spine_MinimalTest
ドキュメント
C++版spineに関する情報は以下から入手できます。
http://ja.esotericsoftware.com/spine-c
前半は基本操作説明、後半に組み込みに必要な実装がかかれています。
が、ちと資料が古いのでいくつか命令が無かったり、引数が省略されていたり、コードがなんとなくな感じだったりしますので、まんまでは動きません。
まずはライブラリ、ランタイムを入手
githubからcloneして頂きます。読み込むアニメーションサンプルも居るので全部貰っちゃいましょう。
https://github.com/EsotericSoftware/spine-runtimes
今回使うのはC++ランタイムの「spine-c\」と読み込ませるサンプルが入っている「exsample\」です。
VSでプロジェクトを作る
spine-cは環境依存性が無いピュア言語な作りをしているので、作るプロジェクトはCUIの「win32 console application」で作成します。
afxがいる?とかmain生成する?32bit?64bit?とか、このあたりは好みでどうぞ。
プロジェクト設定
まずはライブラリ、インクルードパスを設定しましょう。最後のspine\フォルダまでは含めないように。
[spineランタイムパス]\spine-c\spine-c\include [spineランタイムパス]\spine-c\spine-c\src
次にライブラリのコンパイルですが面倒なのでこのプロジェクトで一緒にやっちゃいましょう(本番は分けて効率化してください)。
\spine-c\spine-c\src\spine\*.c/cpp を全部プロジェクトのソースファイルに突っ込みます。
見にくくなるのでフィルタを作ってそこに入れると良いでしょう。
SpineTest(プロジェクト) 参照 外部参照 ソースコード spine(フィルタ) Animation.c AnimationState.c AnimationStateData.c .... VertexEffect.c main.cpp stdafx.c ヘッダーファイル stdafx.h
次にセキュリティや警告に関する設定が必要です。
全般 構成の種類:exe 文字セット:unicodeを使用する プラットフォームセット:Visual Studio 2015 (v140)(お好みで) c/c++ 警告レベル:3 警告をエラーとして扱う:いいえ SDLチェック:いいえ 基本ランタイムチェック:既定 ランタイムライブラリ:お好みで 浮動小数の例外を有効:いいえ (*重要) プリコンパイル済みヘッダ プリコンパイル済みヘッダ:使用しない プリプロセッサ 定義:_CRT_SECURE_NO_WARNINGS の追加(警告レベルを2へ下げるのが良いのかは好みで)
これで動くとは限りませんが、spineとは別の問題なので省略します。
上の構成とプラットフォームがコンパイルしようとしているターゲットと同じになっているかはご注意ください。たまに変なタイミングで変わるので。
浮動小数の例外設定はspineでは重要です。
浮動小数計算の例外は無効化をお忘れ無く
spine-cではあらゆる箇所で zero divide が発生します。vc++では初期状態でfpuの警告は無効化されているようですが、他のコンパイラを使用する場合は気をつけてください。
コンパイラで設定する方法の他には。プログラム内で設定する方法があります。
- _controlfp
https://msdn.microsoft.com/ja-jp/library/ms350048(v=vs.71).aspx
int fpuword = _controlfp(_EM_INVALID | _EM_ZERODIVIDE | _EM_OVERFLOW | _EM_UNDERFLOW | _EM_INEXACT, _MCW_EM);
この命令はvc依存かなと思いますが、fpuに対して例外を制御できます。それにしてもこの関数ちょっと使い方に癖が……。delphiだとSet8087CWとかありました。
サンプルの配置
githubでcloneした中に入っているので、「example\」を実行パスの直下に配置しましょう。
デフォルトだと $(ProjectDir) とあるので、プロジェクトファイルがあるところ、main.cppがあるところになってます。
これでようやく準備が出来ました…。
まず読み込みの前に
組み込み用途やピュア○○なライブラリは、大抵機種依存部分を関数ポインタやユーザー定義によって補うのが一般的です。
spine-cもこれが必要で、まず最初にそれらを補ってやる必要があり、定義は extention.h にて表記されています。
/* * Functions that must be implemented: */ void _spAtlasPage_createTexture (spAtlasPage* self, const char* path); void _spAtlasPage_disposeTexture (spAtlasPage* self); char* _spUtil_readFile (const char* path, int* length);
最低限だと以上です。更に組み込みだとメモリ上読み込みを行うと思うので _spUtil_readFile も不要となります。nullptrでも返してやれば良いです。
というわけで以下のようになります。
// spine sample code // document http://ja.esotericsoftware.com/spine-c#SpineC-Runtime-Documentation #include "stdafx.h" #include <stdio.h> #include <string> #include <vector> #include "spine\extension.h" #include "spine\spine.h" //implement function void _spAtlasPage_createTexture(spAtlasPage* self, const char* path) { self->rendererObject = nullptr; //set user class or record pointer self->width = 2048; self->height = 2048; printf("texture.create:%s\n", path); //printf("texture.create:%s\n", self->name); } void _spAtlasPage_disposeTexture(spAtlasPage* self) { //dispose self->rendererObject printf("texture.dispose:%s\n", self->name); } //not use char* _spUtil_readFile(const char* path, int* length){ printf("readfile:%s\n", path); return nullptr; //return byte array pointer } int main(int argc, char* argv[]) { //ここにテストコードを書いていきます return 0; }
これに近いコードは以下にあります。
https://github.com/EsotericSoftware/spine-runtimes/blob/3.6/spine-c/spine-c-unit-tests/main.cpp
読み込み
exsampleにあるサンプルを読み込んでみます。とりあえずspineboyでも。
使うデータはspine本体で使うspineファイルでは無く export\ にある物を使用します。今回は。
spineboy.atlas spineboy.png spineboy-pro.json spineboy-pro.skel
pro と ess はプロバージョンと機能限定版によるもので、メッシュ変形などの有無によるデータの違いです。
テストとしてはプロバージョンでいいので pro をつかいます。
読み込みの手順は画像(.atlas)を読み込んで、スケルトン(.json)を読み込みます。
.skel は .json のバイナリ型のようです。他環境だとjsonを推奨しているような雰囲気です。今回は使いません。
読み込むにはドキュメントLoadAssetあたりになります。
http://ja.esotericsoftware.com/spine-c#Loading-Spine-assets
今回はメモリ上読み込みを行うので使う関数は以下になります。
spAtlas_create spSkeletonJson_create spSkeletonJson_readSkeletonData spAnimationStateData_create spAnimationStateData_dispose spSkeletonData_dispose spAtlas_dispose
さてメモリ上で読み込むので受け渡しはcharなどの1byte配列です。
ファイル読み込んで配列にする関数が必要になるので、お好みで作成してください。
includeに
DelphiだとTMemoryStream.LoadFromFileなのは皆さんもご存じで…無い!?。
std::vector<char>* _readfile(const std::string &filename) { std::ifstream fs; fs.open(filename, std::ios::in | std::ios::binary); if (!fs) { printf("error.file.open:%s\n",filename.c_str()); getchar(); return nullptr; } //get file size fs.seekg(0, std::ios::end); std::streampos size = fs.tellg(); fs.seekg(0, std::ios::beg); //read from filestream std::vector<char>* buf = new std::vector<char>(); buf->resize(size); fs.read(buf->data(), size); fs.close(); return buf; }
というわけでエラー処理を飛ばして書くと以下になります。
int main(int argc, char* argv[]) { //_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); //memory leak debug std::string base = "examples\\spineboy\\export\\"; std::string f; //atlas //call create texture f = base + "spineboy.atlas"; std::vector<char>* ms = _readfile(f); spAtlas* atlas = spAtlas_create(ms->data(), ms->size(), base.c_str(), 0); delete ms; printf("Atlas.complete\n"); //SkeletonData ms = _readfile(base + "spineboy-pro.json"); spSkeletonJson* skeletonJson = spSkeletonJson_create(atlas); spSkeletonData* skeletonData = spSkeletonJson_readSkeletonData(skeletonJson, ms->data()); delete ms; spSkeletonJson_dispose(skeletonJson); printf("SkeletonData.complete\n"); //AnimationData spAnimationStateData* animationStateData = spAnimationStateData_create(skeletonData); printf("AnimationStateData.complete\n"); ... spAnimationStateData_dispose(animationStateData); spSkeletonData_dispose(skeletonData); spAtlas_dispose(atlas);
Atlas、Jsonと読み込んで、JsonからskeletonDataを生成、SkeletonDataからAnimationStateDataを生成します。
ただしこれらはまだデータです。静的なもので、ここからアニメーションの動的な情報を保持するものを生成して使用します。
クローンを作成する場合は「このデータからいっぱい作れば早いよー」ということだけ覚えておきましょう。
実態の生成
表示させるにはデータから実態となるSkeletonとAnimationStateを生成する必要があります。
実際の動きの管理はこの実態となる2つのインスタンスで制御を行います。
spSkeleton* skeleton = spSkeleton_create(skeletonData); spAnimationState* animationState = spAnimationState_create(animationStateData); .... spAnimationState_dispose(animationState); spSkeleton_dispose(skeleton);
テクスチャ読み込みはいつ?
さて、ログを取ると以下のようになります。
texture.create:examples\spineboy\export\spineboy.png Atlas.complete SkeletonData.complete AnimationStateData.complete texture.dispose
テクスチャの生成はAtlasの読み込みで行われるようです。最初に定義した _spAtlasPage_createTexture が呼ばれています。
テクスチャパスも spAtlas_create で指定したパス指定に画像ファイル名を付加したものになっています。
アニメーション
ちょっと複雑ですが、ものによってデータ(AnimationStateData)に仕込む場合と、実際のアニメーションインスタンス(AnimationState)の方に仕込む場合があるので注意が必要です。
・アニメーション切り替え時のブレンド
これはAnimationStateDataに仕込みます。
spAnimationStateData_setDefaultMix は廃止命令です。
//spAnimationStateData_setDefaultMix(animationStateData); //probabry disuse animationStateData->defaultMix = 0.1f; spAnimationStateData_setMixByName(animationStateData, "walk", "run", 0.2f); spAnimationStateData_setMixByName(animationStateData, "walk", "shot", 0.1f);
・アニメーションの設定
AnimationState(実態)の方で設定します。
spAnimationState_clearTracks(animationState); //animation clear spAnimationState_setAnimationByName(animationState, 0, "walk", 1); //animationstate , track , name , loop
spAnimationState_setAnimationByName の他に、後方(現在のアニメーションが終点に達したら)に追加する spAnimationState_addAnimationByName があります。
しかし、このByNameですが、実際に使うには大変危険な命令です。存在しない名前を渡すと例外を返すと思いきや、NULLポインタアクセスで停止します……。なのでサンプルでしか使っちゃいけません。
spSkeletonData_findAnimation があるのでそれで名前引き、nullじゃなければ spAnimationState_setAnimation でせっとしましょう。
spTrackEntry* spAnimationState_setAnimationByNameFix (spAnimationState* self, int trackIndex, const char* animationName, int/*bool*/loop) { spAnimation* animation = spSkeletonData_findAnimation(self->data->skeletonData, animationName); if (animation != nullptr) return spAnimationState_setAnimation(self, trackIndex, animation, loop); else return nullptr; }
またByName系はスケルトンにも存在します。そっちも似たような命令形なので自作して対処しましょう。
座標設定
座標設定はskeletonで行います。
設定後は必ず spSkeleton_updateWorldTransform が必要です。
skeleton->x = 500; skeleton->y = 500; spSkeleton_updateWorldTransform(skeleton); //x,y transform after , next call spSkeleton_updateWorldTransform
メインループ
以下のような感じで回すだけです。
なお、ドキュメントにある spAnimationState_updateWorldTransform は廃止命令だと思われます。ポリゴンを描画する際に計算するようになったみたいです。
for (int i = 0; i < 1; i++) { //update (time move) 60fps spAnimationState_update(animationState, 1.0f/60); //probably trunc micro seconds better //apply skeleton spAnimationState_apply(animationState, skeleton); //transform world //spAnimationState_updateWorldTransform(skeleton); //probabry disuse spSkeleton_updateWorldTransform(skeleton); //custom draw function myCustomDraw(skeleton); }
spAnimationState_updateWorldTransform はたぶん spSkeleton_updateWorldTransform のことなんじゃないかと思います。
イベントの取得
spineは基本的にサウンドの再生を持ちません。
ただし、再生中に文字列イベントを投げてくれるので、この起用で代用します。
・event
http://ja.esotericsoftware.com/spine-c#Events
バージョンで若干構造に変化があったので以下のようになります。
void myListener(spAnimationState* state, spEventType type, spTrackEntry* entry, spEvent* event) { switch (type) { // case SP_ANIMATION_START: printf("Animation %s started on track %i\n", entry->animation->name, entry->trackIndex); break; case SP_ANIMATION_INTERRUPT: printf("Animation %s interrupted on track %i\n", entry->animation->name, entry->trackIndex); break; case SP_ANIMATION_END: printf("Animation %s ended on track %i\n", entry->animation->name, entry->trackIndex); break; case SP_ANIMATION_COMPLETE: printf("Animation %s completed on track %i\n", entry->animation->name, entry->trackIndex); break; case SP_ANIMATION_DISPOSE: printf("Track entry for animation %s disposed on track %i\n", entry->animation->name, entry->trackIndex); break; case SP_ANIMATION_EVENT: printf("User defined event for animation %s on track %i\n", entry->animation->name, entry->trackIndex); printf("Event: %s: %d, %f, %s\n", event->data->name, event->intValue, event->floatValue, event->stringValue); break; default: printf("Unknown event type: %i", type); } }
spineboyでwalkさせると「footstep」という文字列のイベントが飛んできます。
intやfloatもやりとり出来るようですが、文字列があるので「*playsound,id:walk02,vol:128,pan:127,loop:0」みたいな文字だけで完結するかと思います。
描画・基礎編
さて、最後に難物、表示についてです。
表示はほぼすべて3頂点によるポリゴン+テクスチャで行われます。簡単にいってしまえば、計算後の頂点とUV情報が得られるので各自で描いてくださいというものです。
リスナー機能のようなイベント型、関数ポインタではありません。
表示に関する情報はSkeletonインスタンスが保持しています。AnimationStateがSkeletonを操作するので、その結果を画面に表示してやります。
今回はCUIなのでprintfで情報を書き出して終了です。
//custom draw function
myCustomDraw(skeleton);
どのように実装するかはドキュメントにもあります。
・Implementing Rendering
http://ja.esotericsoftware.com/spine-c#Implementing-Rendering
色々書いてますが、要約すれば以下のシンプルなコードです。
void myCustomDraw(spSkeleton* skeleton) { for (int i = 0; i < skeleton->slotsCount; i++) { spSlot* slot = skeleton->drawOrder[i]; spAttachment* attachment = slot->attachment; if (!attachment) continue; if (attachment->type == SP_ATTACHMENT_REGION) { myCustomDraw_region(slot,attachment...); } else if (attachment->type == SP_ATTACHMENT_MESH) { myCustomDraw_mesh(slot,attachment...); } else { //unsupport } } }
スケルトンの分だけレンダリングします。階層構造じゃないんですね。色々と処理した結果並び替えて配列して用意してくれてるみたいです。
空も存在するのでその場合はスキップ、typeによって表示は四角形とメッシュがありますよー。と言ったものです。
マニュアルにある ATTACHMENT_REGION などは廃止され、SP_ATTACHMENT_REGION など列挙型になったようです。
ブレンドモードとカラー
ブレンドモード slot->data->blendmode から得ることができます。
情報が得られるだけなので、合成式は各自で実装する必要があります。Direct3DにおけるBllendOPにあたります。
int blendmode = 0; switch (slot->data->blendMode) { case SP_BLEND_MODE_NORMAL: //alpha blend : src.rgb * (src.a) + dest.rgb * (1.0-src.a) blendmode = 0; break; case SP_BLEND_MODE_ADDITIVE: //add : src.rgb * src.a + dest.rgb blendmode = 1; break; case SP_BLEND_MODE_MULTIPLY: //multiply/modulate : (1.0 + (src.rgb-1.0) * src.a) * dest.rgb blendmode = 2; break; case SP_BLEND_MODE_SCREEN: //screen : 1.0 - (1.0-(src.rgb * src.a)) * (1.0-dest.rgb) blendmode = 3; break; }
カラーはver3.6より2つ設定できるようになりました。いままでの乗算色(slot->color : ModulateColor)とティン色(slot->darkColor : tintColor)の2つがあります。
この計算は上記のブレンドより前のテクスチャカラー計算、Direct3DのPixelShader/TextureStateにあたります。
計算式は以下に・・・なりません!
out.rgb = Texture.rgb * color out.rgb = 1.0 - (1.0-out.rgb) * (1.0-tintcolor.rgb) out.a = Texture.a * color.a
tint単体では合成はスクリーンのようです。
ただし、tintcolorとcolor両方が指定された場合、乗算した結果にスクリーンした場合とは違う色を返します。
例えば、黒を乗算した後に白をスクリーンした場合、普通なら白です。spineのエディタ上では反転した結果が表示されます。
これから分かるのは、スクリーン合成部分が元の色とスクリーンした結果の色との差分を足しているようです。乗算結果との差分ではないようです。
というわけで、乗算で真っ黒になった結果に、元の色と白色の差分、つまり黒かった部分が大きな値を持つので反転するみたいです。
mul.rgb = Texture.rgb * color tint.rgb = 1.0 - (1.0-Texture.rgb) * (1.0-tintcolor.rgb) out.rgb = Texture.rgb + (mul.rgb - Texture.rgb) + (tint.rgb - Texture.rgb) out.a = Texture.a * color.a
かな?ちょっと特殊すぎる…。乗算のみ、スクリーンのみで使用するならまぁ大丈夫でしょう。
レンダリングが乗っ取れるので、エディタ上では異なる表示になりますが、プログラムから自由に使える色みたいに使ってもいいかもしれません。unityでは難しそうですが…。(あれだけランタイムの構成が大きく異なるんです)
色の取得は以下になります。
spColor mulcol,addcol; mulcol.r = skeleton->color.r * slot->color.r; mulcol.g = skeleton->color.g * slot->color.g; mulcol.b = skeleton->color.b * slot->color.b; mulcol.a = skeleton->color.a * slot->color.a; screencol.r = 0.0f; screencol.g = 0.0f; screencol.b = 0.0f; screencol.a = 0.0f; // gui deactive , set null if (slot->darkColor != nullptr) { screencol.r = slot->darkColor->r; screencol.g = slot->darkColor->g; screencol.b = slot->darkColor->b; screencol.a = slot->darkColor->a; }
気を付けるところは slot->darkColor が spColor* で定義されている点です。
color は普通の spColor なのになぜ違うのかはエディタでの仕様によるところのようです。darkColor は初期状態では未定義状態(機能が追加されていない状態)で、使用する際に有効、追加を行います。なので有効にされていない場合は nullptr として渡されるようです。
カラーはslotとskeletonの他にもう一個、attachment(画像やメッシュ)にも存在します。
エディタ上ではslotやskeletonは動的なアニメーション変化を与えられるのに対して、attachmentはsetup編集の時しか設定できない固定的な色です。
なので、計3段階の色計算が必要になります。尚、attachmentにはdarkColorは存在しません。
attachmentは型が確定するのがほぼ最後なので、drawする最終段階に計算することになるかと思います。
void myCustomDraw_region(spSlot* slot , spAttachment* attachment , spColor* col , int blendmode) { spRegionAttachment* regionAttachment = (spRegionAttachment*)attachment; // attachment color spColor mulcolor;spColor mulcolor; mulcolor.a = col->a * regionAttachment->color.a; mulcolor.a = col->r * regionAttachment->color.r; mulcolor.a = col->g * regionAttachment->color.g; mulcolor.a = col->b * regionAttachment->color.b; ...
描画・メッシュ編
メッシュ描画について。
メッシュに渡されるのは基本的にはプロフェッショナル版の機能です。スキニングメッシュの他にも、メッシュで透明色部分を最適化した場合、こちらが呼ばれます。
void myCustomDraw_mesh(spSlot* slot , spAttachment* attachment , spColor* col , int blendmode) { spMeshAttachment* mesh = (spMeshAttachment*)attachment; _worldVertices_setlength(mesh->super.worldVerticesLength); //worldVerticesLength buffer length. x,y,x,y,x,y,x,y...x,y void* textureptr = nullptr; textureptr = (void*)((spAtlasRegion*)mesh->rendererObject)->page->rendererObject; //set user pointer in _spAtlasPage_createTexture // SUPER = mesh->super spVertexAttachment_computeWorldVertices(SUPER(mesh), slot, 0, mesh->super.worldVerticesLength, _worldVertices.data(), 0, 2); //probably stride value float x,y count 2. support 3D feature? ... }
計算の流れはシンプルです。
まずメッシュなのでメッシュインスタンスにキャスト。
後で説明します計算領域の確保。
次に createTexture 時にセットしたテクスチャの実態を取得。テストなので残念ながらnullptrです。
最後に行列計算を行います。かんたん。
最後のstep数=2を指定するあたりが今後の可能性を感じます。
さて、行列計算の際に出力用のfloat配列を要求されます。
あらかじめ必要なサイズは mesh->super.worldVerticesLength にて取得出来るので、この段階でリサイズしてやる必要があります。
mesh->super.worldVerticesLength で取得出来るのはfloatの個数になります。(byte長ではありません)
こういう頻度が多い場合は、共有、サイズは基本的に拡大しかしないので _worldVertices_setlength 命令を噛ませてあります。
//computing vertex buffer std::vector<float> _worldVertices; void _worldVertices_setlength(int length) { if (length > _worldVertices.size()) { _worldVertices.resize(length); } } void _worldVertices_clear() { _worldVertices.resize(1024); }
よしなに。
これで計算された頂点情報(UVなども)が手に入りましたので、ポリゴンに分けていきます。
ドキュメントによるとこうあります。
// Mesh attachments use an array of vertices, and an array of indices to define which // 3 vertices make up each triangle. We loop through all triangle indices // and simply emit a vertex for each triangle's vertex. //for (int i = 0; i < mesh->trianglesCount; ++i) { // int index = mesh->triangles[i] << 1; // addVertex(worldVerticesPositions[index], worldVerticesPositions[index + 1], // mesh->uvs[index], mesh->uvs[index + 1], // tintR, tintG, tintB, tintA, &vertexIndex); //}
triangleCountだから1ポリゴンかと思ったら、そうでは無いようです。単純にx,yの頂点indexのようです。ちょっとわかりにくいのですが、addVertex も名前の通り、1頂点の登録です。
つまり、頂点indexがtriangleCount分、3頂点1ポリゴンとして並んでますということのようです。
おら!孕め!ポリゴンの子を!
float xx[3], yy[3]; //vertex float uu[3], vv[3]; //uv int triangleindex = 0; float* sptr = (float*)_worldVertices.data(); int n = mesh->trianglesCount / 3; for (int i = 0; i < n; i++) { //triangle for (int j = 0; j < 3; j++) { //vertex int idx = mesh->triangles[triangleindex] * 2; triangleindex++; xx[j] = sptr[idx + 0]; yy[j] = sptr[idx + 1]; uu[j] = mesh->uvs[idx + 0]; vv[j] = mesh->uvs[idx + 1]; } //add addTriangle(xx,yy,col,blendmode); }
描画・四角形編
メッシュとだいたい一緒です。
ただし、四角形の4頂点分しか計算されないので、三角形の場合は、0-1-2 , 2-3-0 と頂点を使い回す必要があります。
void myCustomDraw_region(spSlot* slot , spAttachment* attachment , spColor* col , int blendmode) { spRegionAttachment* regionAttachment = (spRegionAttachment*)attachment; _worldVertices_setlength(4*2); //4 vertex void* textureptr = nullptr; textureptr = (void*)((spAtlasRegion*)regionAttachment->rendererObject)->page->rendererObject; // Computed the world vertices positions for the 4 vertices that make up spRegionAttachment_computeWorldVertices(regionAttachment, slot->bone, _worldVertices.data(), 0, 2); const float idxtbl[6] = { 0,1,2,2,3,0 }; float xx[3], yy[3]; //vertex float uu[3], vv[3]; //uv int triangleindex = 0; float* sptr = (float*)_worldVertices.data(); int n = 2; for (int i = 0; i < n; i++) { //triangle for (int j = 0; j < 3; j++) { //vertex int idx = idxtbl[triangleindex] * 2; triangleindex++; xx[j] = sptr[idx + 0]; yy[j] = sptr[idx + 1]; uu[j] = regionAttachment->uvs[idx + 0]; vv[j] = regionAttachment->uvs[idx + 1]; } //add addTriangle(xx, yy, col, blendmode); } }
頂点の回転方向が逆だったらごめんなさい。
ポリゴンログ
addTriangleの内容を出せば終了です。
void addTriangle(float* xx, float* yy, spColor* col, int blendmode) { printf("draw.triangle:vertex=(%f,%f)(%f,%f)(%f,%f),argb=(%f,%f,%f,%f),blend(%d)\n", xx[0], yy[0], xx[1], yy[1], xx[2], yy[2], col->a, col->r, col->g, col->b, blendmode); }
あ、UV情報忘れちゃった。テストだし問題ないよね。
ちなみにメッシュだと1フレームで数百のログが流れますのでご注意ください。
draw.triangle:vertex=(544.877563,798.626404)(501.897675,779.607239)(466.691986,859.165771),argb=(1.000000,1.000000,1.000000,1.000000),blend(0) draw.triangle:vertex=(561.553467,768.968384)(510.339386,746.316162)(481.215149,812.162842),argb=(1.000000,1.000000,1.000000,1.000000),blend(0) draw.triangle:vertex=(729.151794,654.932861)(524.608582,607.371765)(478.632874,805.096924),argb=(1.000000,1.000000,1.000000,1.000000),blend(0) draw.triangle:vertex=(562.501709,498.976868)(567.549072,540.956177)(572.811279,539.457520),argb=(1.000000,1.000000,1.000000,1.000000),blend(0) draw.triangle:vertex=(562.501709,498.976868)(572.811279,539.457520)(588.472290,534.956665),argb=(1.000000,1.000000,1.000000,1.000000),blend(0)
最後に
とりあえず、これで最小限の情報は得られたかと思います。