04/21
_ [商用ゲーム] 「続・ゲームにおけるスクリプト言語の現状」レポートと感想 その2
前回のエントリに引き続き、IGDA日本ゲームテクノロジー研究会(SIG-GT)第13回研究会「続・ゲームにおけるスクリプト言語の現状」 のレポートです。飛ばしている所も多いです。他の方のレポートのついでくらいに見ると良いかも。
汎用スクリプト言語Xtal 設計と実装
バンダイナムコゲームスの石橋氏の発表。会社としてではなく、個人として。
Xtal は Lua の代替を狙った汎用スクリプト言語。google code で公開 している。Xtal は正しくはクリスタルと呼ぶ。2006年から eXTreme Agile Language の略となっているがこれは後付けで、2002年にはクリスタルという名前になっていた。Rubyのように宝石の名前にしたかったのと、FFの最高級宝石の意味を込めて。ライセンスは MIT。Google code に登録しやすいという理由で、できれば Public License にしたかった。
Xtalを作る上で大きな影響の1つがRuby。さくさく書けることに感動した。80:20のパレートの法則 (wikipedia:パレートの法則) にあるとおり、80%の速度への影響が少ないコードは、効率の良いスクリプト言語で書いても良いはず。しかし、開発当初の2002年には、Rubyにはゲームや組み込みに使うには「バイナリコードにコンパイルできない」「浮動小数点がオブジェクト」「ガベージコレクトのラインが長い」などいくつかの問題点があった。
次に Lua に出会った。これは軽く速く組み込みやすい。しかし、「グローバル変数の扱い」「C/C++ との親和性 (配列が1 origin、ブロック、--コメント)」に不満があった。
そこで、Xtalを開発した。ただ、不満があるから作りたいと言うよりは、自分が欲しい言語を作りたかったという理由。登山家が、山がそこにあるから登る、というのと同じ。
文法紹介。「変数」「制御構文」「クラス」これらはC++風。「ファイバー(コルーチン)」「Rubyのようなブロック」あり。詳細は公式サイトのマニュアルを参照。
変数定義と初期化は、"foo = 0;" という代入風の書き方でなく "foo: 0;" と、タイプ数を増やさずに分けて書いている。
構文解析 (wikipedia:構文解析) と字句解析 (wikipedia:字句解析) について。最初はこの2つを一緒に行っていた。拡張しやすいと思い途中から分離している。しかしまた戻すかもしれない。構文解析については Yacc を使わず手書きしている。Boost.Sprit はコンパイルが遅く使っていない。将来的には自作ライブラリに置き換えたい。
バイトコード (1バイト=256) はJavaを参照に独自で生成している。ワードコード (2バイト=65536) も試し、1〜2割の実行速度の向上を確認したが、そのために命令長が2倍になるのは痛いので、前者のままにしている。
オブジェクト管理は、Lua と同じ Variant (Any) 方式を採っている。「整数」「浮動小数点」「その他」の3種の型の種類を示す int を全ての構造体に入れている。
ガベージコレクション (GCアルゴリズム詳細解説 - livedoor Wiki) は参照カウンタ+Mark&Sweep方式を採っている。オブジェクトの開放は自動でなく、gc関数やfull_gc関数を明示的に呼ぶ。これは、消す処理が重いことや stack overflow になることがあるため。他の方式も検討したが、保守的な GC はもともとの動機が Ruby のガベージコレクションへの不満があったため没、世代別GCやインクリメントGCは、ライトバリアの仕組みをC/C++とスクリプトの世界を頻繁に行き来する時に維持するのが困難という理由で使えなかった。
環境依存部分はカスタマイズが容易になっている。
バインダはXtalでも必須。Luaではtolua, luabind、SquirrelではSqPlusが有名。この仕組みは Modern C++ Design (amazon:Modern C++ Design) にあるテンプレートを駆使している。ただしコンパイル時間の低下や、移植性低下にも繋がっている。
Xtalの欠点としては、実績がなくある意味地雷原。実行ファイルのサイズがLuaの2〜3倍と大きく、実行速度も負けている。
開発を振り返ると、理想の言語を追い求めすぎずに妥協すれば良かったと反省。理想の言語は体調によって変わる。LISPになりかけた事もあった。
今後の予定と最後に。そしてQA。
感想。とにかく笑い溢れるセッションでした。発表経験というよりは、もうこれは人柄で。なんとも羨ましい。プレゼンテーションが下手だから 未踏ソフトウェアに申し込んでいなかったとの事ですが、全然そんなことは。是非これを機会に挑戦頂きたいところです。
枯れていないライブラリを商用で使うのはかなりリスクがあり、その点で Lua が強いのですが、Xtal も流行ればいいなぁと思いました。人柱さんに期待。
Squirrelスクリプトを使った実装と活用
スクウェア・エニックスの神尾氏、北出氏の発表。WiiWare の「小さな王様と約束の国 Final Fantasy Crystal Chronicles」の製作にあたって、Squirrel (読み方は様々だが、ここではスクワール) を使用したときのノウハウの公開。このセッションのみ、言語を作る方ではなく使う方法について。
Squirrel は C/C++ ライクなオブジェクト指向の組み込み向けスクリプト言語。標準拡張子は ".nut"。6000行ほどのC++で書かれていて、ソースコードは公開されている。
暫く文法の解説。Google で検索するとわかりやすい日本語ドキュメントが見つかる。(Squirrel2.2 リファレンスマニュアル 邦訳や、Squirrel - 組み込み言語Wiki 参照)
- 関数
- 引数に型がないのでコメント推奨
- 戻り値は1つ
- Array, Table
- Array は 0 origin の配列
- Table は連想配列
- Class
- コンストラクタはあるが、デストラクタがない
- メンバ変数について、コンストラクタで初期化しないとstaticのように振る舞うことに注意 (なぜ?)
- roottable()
- グローバルに定義されている関数や変数のテーブルが取れる
- これを使い、if ("hogehoge" in getroottable() {...} のように、関数が定義済みかどうかをまるで #ifdef のように調べられる
- また、クラスの名前の取得、定義済みのクラスや関数の開放もできる
スクリプトの使い方は、AI作成やイベントスクリプトと言った限られた機能のみスクリプトを使う方法、ゲームコードもデータも全てSquirrelで記述するというどちらの方法もある。
SquirrelからC++のコードを呼び出すにはバインドが必要。このためにSqPlusを使う。関数・クラス・定数のバインドが可能。C++からバインドしたクラスを Squirrel上で派生する事も可能。
別スクリプトの import が比較的簡単に行える。ソースを分けて管理すると便利。ある import したファイルにバグがあったときに、そのファイルだけ修正して再読込すれば、実行を一旦終了せずに迅速に回復できる。
C++から読み込んだスクリプトの呼び出しもできる。スタック操作がややこしいことに注意。
組み込み時の注意は3点。
- C++で生成したクラスのポインタをそのままSquirrelに見せる場合、try〜catchを使い、Squirrelのスタックダンプを呼び出すか、呼び出せるようにしておくのがお勧め
- または、ポインタをそのままSquirrelに出さず、idで管理する方法もある。この場合、C++でポインタを引く前に、idが適正かどうかを確かめる
- 関数の引数が多すぎるとエラーが出る。標準は7個。それ以上必要な時には sqplus.h の template を書き足せばよい
製作初期には、C++ と Squirrel のバインド作業が多く、けっこう大変。データを Squirrel で持たせる場合は、.xml などから .nut に変換するコンバータを作っておくと便利。
バインドを行う工夫として、ツールでバインドコードを自動生成するのを行っている。たとえば、次のような C++のコード SQVector.h に SQREG_* マクロでバインドする変数と関数を書いている。これに対し SqReg.rb スクリプトを実行すると、バインドコード SQVector.sqreg が自動生成される。Squirrel 側で使えるかどうかの情報も C++ のヘッダファイルだけ見れば分かる。(以下、ハンドアウト P52-53 のコードを部分的に省略したもの。赤字がうまく印刷されていないため、間違っている可能性あり)
class SQVector { public: static void BindSquirrel(); float x, y; SQREG_CVAR( x, "x" ); SQREG_CVAR( y, "y" ); SQVector( float x, float y ); void set SQVector( float x, float y ); SQREG_CFUNC( Set, "Set" ); }
Squirrel 上のデータを foreach, typeof を使えば比較的簡単に .nut 形式に出力できる。Wii のデバッグがハードウェアの都合で高速に動かない事もあり、ファイル書きだしが役立った。
デバッグでは、ランタイムエラーが厄介。変数がない、typo、シンタックスエラー、型が違うなどなど。命名規則やエディタの補完機能である程度は解消できるが、動的なシンボル・ラベル作成には対処できない。アドレス例外などでスタックダンプが出ないときには、以下の関数を用意しておき、IDE でプログラムカウンタをここに飛ばすことをお勧めする。
void ScriptManager::PrintCallStack() { sqstd_printcallstack( SquirrelVM::GetVMPtr() ); }
スクリプトエラーが発生したときには、スクリプトの呼び出しを空回りやブレークポイントにより一旦中止させ、その間にソースファイルを修正して、タイムスタンプが更新されたスクリプトを読み込み直す、という流れを行う。class インスタンスの場合は回復に手間がかかる。
VisualStudio上でのデバッガのデモ。丁度講演の日の朝に、Squirrelの掲示板から取得した。
気をつけること。
- SquirrelとC++で頻繁な往復をしないよう、目的に特化したI/Fを用意する。たとえば GetPos() → SetPos() するより、AddPos() 1回で済ませた方が良い。
- 細かいメモリーブロックが作られることに注意。Table は手軽だが Array 容量を消費することを意識する
- VMのスタックサイズ調整。sqvm.cpp_stack.resize() にブレークポイントを張り、何度も通る場合は SequirrelVM::Init() でのサイズを大きくして頻繁な realloc を避けること
- GCを実行するとフレームレートに影響する事があるので、画面切り替えやフェードのタイミングという影響の少ない場所がお勧め。デフラグを防ぐためにリソース入れ替え前が良い
FFCC 小さな王様と約束の国での活用。287ブロックの小さなWiiWare。約70%のプログラムがSquirrelで書かれている。その中にはデータやメインループも含まれる。スクリプトのオーバーヘッドは10%程度。
開発中の FFCC My Life as a Darkloadについて。海外版のリリースは決まっており、日本語版は未定。ゲームデザインを新しく起こしながらも、小さな王様のシステムをなるべくそのまま使っている。講演前日に調べたところ、C++ソースで94%、Squirrelソースで30%が流用。スクリプト部分について、スピード感のあるトライアンドエラー開発が行えた。
工夫した点として、try, catchで実行時エラーを捕らえ、ゲームを再起動することなく復帰させていた。ただし、難しい場所については再起動していた。また、実用可能な速度でデバッガ eclipse / SQDev が動かず、いわゆる printf デバッグを行っていた。
まとめると:
- 利点
- 素速いリトライが可能、短い時間で形にできる
- 柔軟なデータフォーマットと、スクリプトで細かな変更にも容易に対応
- コールスタックが協力で、問題の把握が容易
- IDEが無くても製作やデバッグ可能
- 難解なメモリ破壊系のバグが起こりにくい
- 注意点
- メモリに注意。主にガベージコレクタ
- ランタイムエラーによるデバッグコスト増に注意
- 引数や戻り値の型がないため、しっかりコメントを
今回は小さな王様とMy Life as a Darkloadでゲームコードの部分を取り替えた事例の紹介。今後は同様に、ハードとシステムの部分を取り替えるなど、色々な活用方法が考えられそう。
QA。(ここでは半分程度)
- ライセンスが言語決定の理由の一つか
- マニュアルに Squirrel を使用したと書く必要がないのは確かに気が楽だが、それだけが採用の理由ではない
- 実行中の再コンパイルで、参照を確実に切ることはできるか
- 完璧ではなく、問題がおこりそうな限定した箇所に対して行っていた
- プランナーとプログラマの担当分担は?
- イベントスクリプトがプランナー。それ以外がプログラマ
- アクションゲームでも同じように使えるか?
- よりリアルタイム性が要求されるため、ゲームジャンルに応じてもう少し限定して使うのでは
- どこより上のハードで使えるか
- 今の方法ではWiiが下限。DSではスペックが不足する
- 360, PS3はハード的には申し分ないが、ソフトウェアの規模が大きすぎ、スピード感だけで作ると大変なことになるかもしれない
- Squirrel にはバグが残っていると掲示板で見かけたが
- 徐々に良くなっている。標準的な使い方をしていればあまり地雷を踏まない。
- もし不具合があったとしても6000行のソースコードがあり対処できる、というのが採用のきっかけの一つとなった
感想。これ、かなり高度な「今の」ノウハウの公開かと... スクウェア・エニックスの太っ腹さに素晴らしいと。Squirrel を名前を聞いたことしかない人には、最初の文法紹介の密度にどうしようかとも思いましたが、結果的には一番満足度の高いものに。
サンプルコードがあると、よりSquirrelの強力さが分かります。さすがにそれを全て載せるのは自粛。興味がある型は、こういう機会に是非参加することをお勧めします。
_ [商用ゲーム] 「続・ゲームにおけるスクリプト言語の現状」その3 yield
XtalのQ&Aで、yield に返値が取れるか、C# はダメだけれど Xtal は大丈夫、というような話がありました。これの意味が良く分からなかったもので、ついでに。
0*1 + 2*3 + 4*5 + 6*7 + 8*9 = 140 という式を、無理矢理 ruby の yield を使って書いてみました。
def sum2(ary) i = 0 sum = 0 while (i+1 < ary.size) sum += yield(ary[i], ary[i+1]) i += 2 end sum end ary = Array.new(10) {|i| i} x = sum2(ary) {|i, j| i * j} puts x
sum += yield のように値を得られます。
では C# では。C# に慣れていなくて調べながら書いたもので、間違っていたらごめんなさい。
using System; using System.Collections; namespace YieldSample { class Program { public static IEnumerable Sum2(int[] ary) { int i = 0; while ((i + 1) < ary.Length) { int[] a = new int[]{ ary[i], ary[i+1] }; yield return a; i += 2; } } static void Main(string[] args) { int[] ary = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; int sum = 0; foreach (int[] a in Sum2(ary)) { sum += a[0] * a[1]; } Console.Write(sum); } } }
yield は return を修飾していると思うと、確かに return が返値があるとおかしいかも。
言語を選ぶときの切り口って色々あるんだなと思ってみました。これだけの話が1分くらいのQ&Aの中につまっていても分かるように(汗)。
...と、うちはゲーム製作ではなくてゲーム紹介を中心とした日記でして(汗)、多分こういうプログラミングな話題はこれくらいにすると思います。もしこれっきりにならないとすると、続じゃない方のほったらかしにしているレポートを書くくらいで(汗汗)。