続・ゲームにおけるスクリプト言語の現状 メモ

4/18 に行われた 研究会のメモ。
すでに何人もの方がレポートを書いて下さっているので、興味がある部分があればリンク先を参照してください。
基本的に軽く記述してあるだけですが、他の記事であまり触れてない Squirrel に関しては、ある程度記述してあります。

感想

今回は、第1回(前回)のパネルディスカッションの面々が個々の事例を掘り下げるという、事例ベースのセッションとなっていた。
間に15分休憩があるだけの4連続だったのでけっこう疲れたけど、充実した内容にかなり満足。
前回がっかりした人ほど、今回は満足できたんじゃないかな。

ごく簡単な並列処理スクリプトシステムの設計と実装

  • スクエニの、クロノトリガーで生まれたスクリプト
  • 並列動作を上手く書くためにアクターモデルを採用
    • アクター間通信によるイベント駆動
  • レキシカル(辞書)コンパイラ
    • FORTH ライクな辞書記述
    • 開発の途中でどんどんワードのバリエーションを増やせる。同義語でもシンタックスシュガーでもOK
  • スクリプタ向けなので、分かりやすい日本語的単語を使う
    • 例えば、add, sub ではなく、plus, minus
  • コンパイル時にはじけるバグは全てはじく。
    • その際、エラーメッセージはていねいに。さらに、なるべく対処法を記述するようにする。

サクラ大戦Ⅴ』でのスクリプト運用事例

  • サクラ3,4,5 での事例
  • フラグ管理
    • シナリオ型ゲームでは、フラグ操作ミスが致命的なエラーに結びつきやすい
      • スクリプト内では変数を確保できなくした
      • プログラム側にフラグ領域を用意し、スクリプト側からそれを操作する
  • イベント画面演出
    • マルチスレッド記述
      • スクリプタにはしきいが高かった(実際1人しか使えなかった)ので、次の2つを用意
    • アニメーションをタイムテーブルで記述するアクションシステム
    • 2D画面にエフェクトを重ねて表示するエフェクトシステム
      • 見た目が派手なので、エフェクトシステムが最も使われた
  • 中断セーブ
    • フラグ領域やスタックを丸ごと保存(スクリプトシステム)
    • 各システムの基底クラスでシリアライズ/デシリアライズ(ゲーム状態)
    • 先読みと相性が悪い(セーブ時、スクリプトが数行先に進んでいるため)
      • Print命令が呼ばれる度、メインメモリに中断データを作成するようにした
  • マクロ形式
    • シナリオ担当者向けの簡易マークアップ言語
    • プログラマ泣かせの仕様
      • 各定義・命令は全角。でも半角も受け付けて欲しい
      • 略字や誤字脱字も受け付けて欲しい
      • 背景・キャラ名などを後から追加できるように
  • HSL
    • C言語の構造体ライクな記述をできる、チーム自作のデータフォーマット
    • DOM風APIでデータにアクセス
    • XMLよりパーサプログラムが軽量


ここまで前半戦。
どちらも、プランナーがいかに書きやすいか、いかにバグを出さないかを念頭に置いて開発している印象。
プログラマがいかに気配りできるかが大切だと感じると共に、もしかしてゲーム業界的にプログラマはそんな数要らない?とも思った。


汎用スクリプト言語Xtal 設計と実装

講演者の人柄か、終止笑いが堪えない良い感じのセッションだった。

  • クリスタルと読む。Ruby のように宝石の名前にしたかった。
  • プログラマの多くが望む「いつか俺言語、俺OSを作りたい」という夢を自分も果たしたかった。
  • Lua の代替を狙う
  • 動的な型は、評判がよくないので静的にする予定
  • もしかして機能
    • うっかり変数名を間違えて参照してしまったら?
    • foe は定義されていません。'foo' と間違えている可能性があります。
  • for文には、最初の一回であることを示す機能などがある。
  • Ruby のようなブロック
  • 変数は、全てがオブジェクト
  • シリアライズ機能
  • 使用者が独自のmalloc, free を登録可能。内部で小さいメモリ確保の最適化
  • バインダにはテンプレートを活用。
    • 実際に使われるまで登録を遅延
      • Xtal標準ライブラリが、メモリのかなりの部分を占める。
      • これらはスクリプト側から呼ばれるのみなので、使わなければ登録されないように
  • 構文解析
    • 手書き再帰降下。Yacc などのツールは使っていない。
  • ByteCode は、Java のByteCode を参考に。
  • VMループはswitchで分岐
    • 他の手法でも、速度はあまり変わらなかったので、移植性のあるswitchで
    • 質問タイムに、「switchは明らかに遅い!」という指摘もあった。
  • ガベージコレクション
    • 参照カウンタ+Mark&Sweep

Squirrelスクリプトを使った実装と活用

他のセッションと異なり、過去のことではなくここ1,2年の状況を惜しみなく提供していた。

ちょっとしたテクニック
  • グローバルシンボルは、getroottable() で、ルートテーブルを探せばすべて見つかる
foreach(key, val in getroottable() )
    print("key=" + key + ", val=" + val + "\n");
  • これを活用して、#ifdef のようなことが可能
if( "hogehoge" in getroottable() ) {
    print("hogehoge 発見!値は=" + ::hogehoge + "\n");
}
  • roottableを使って、class の名前を取得
// instance か getclass の値を入れたら、クラス名が返る
function getClassName( classDef )
{
    if( typeof(classdef) != "class" ) {
        return "*NotClass*"
    }

    foreach( name, item int getroottable() {
        if( item == classdef ) {
            return name;
        }
    }
    return "*UnknownClass";
}
}}}

class Foo {}
local ci = Foo();
print( getClassName( ci.getclass() ) + "\n" );
print( "ci=" + getClassName( ci.getclass() ) + "\n" );
  • 定義済みのFunction, classを解放
delete getroottable()["関数名/クラス名"];
  • ごく簡単なprintf実装
function printf( msg, ... )
{
    local arg = array(7);
    local i = 0:
    while( i < 7) {
        if( i < vargc ) {
            arg[i] = vargv[i];
        }
        i += 1;
    }
    print( format(msg, arg[0], arg[1], arg[2], arg[3], arg[4], arg[5], arg[6] ) );
}

local i = 10;
local f = 0.1;
printf("%s", %d, %f, %s\n, str, i, f, "aaa");

[実行結果]
str, 10, 0.100000, aaa
実装
  • 組み込みにはSQPlus を使用。
  • import は、比較的簡単に実装可能。これがあるとソースを分けて管理できる
    • FileLoad() → CompileBuffer() → RunScript()
    • importしたファイルをプログラムで管理し、タイムスタンプをチェックしながら再読み込み出来るようにしておくと、エラーの回復がし易くなる
  • C++で生成したクラスのポインタをそのままSquirrelに見せる場合、try 〜 catch を使って Squirrel のスタックダンプを呼び出せるようにしておくのがオススメ
    • VM の中でアドレス例外などが起きると、スタックダンプが出ない場合がある。
    • あるいは、ポインタをそのまま Squirrel に出さず、id で管理する方法もある。この場合、C++側でポインタを引く前に適正な id であるかをチェックすることでこの問題を防ぐことができる。
  • コードとデータ例
// 敵データ例
::gameData enemyDataArray <-
{
    {
        label = "worm",
        vitality = 4;
        strength = 3;
    },
    {
        label = "moo",
        vitality = 3,
        strength = 4,
    },
};

// 敵クラス例
class Enemy
{
    _enemyData = null;

    constructor( id )
    {
        _enemyData = ::gameData.enemyDataArray[ id ];
    }

    function GetVitalityMax() {
        return _enemyData.vitality;
    }
}
  • 以下のような変更にも、柔軟に対応できる
// 敵データ例
::gameData enemyDataArray <-
{
    {
        label = "worm",
        vitality = 4;
        strength = 3;
    },
    {
        label = "moo",
        vitality = 3,
        strength = 4,
        abilityArray = [ ::ALIBITY_BITE ]; // 追加
    },
};

// Ability に対応する処理を追加する
if( "abilityArray" in _enemyData ) {
    // アビリティありデータ
}
else {
    // アビリティなしデータ
}
制作
  • プランナーとの連携のために、.xml などから .nut へのコンバータを用意しておくと便利。
  • C++のヘッダファイル情報から、バインドコードを自動生成するツールを作った(Rubyで作ったぽい?)
  • .nut をデバック用に出力
    • foreach, typeof などを使って、比較的簡単に Squirrel 上のデータを .nut に出力することが出来る
    • 実機上でデバックメニューなどで調整したデータを、ファイルに書き出せると調整等がやりやすくなる。
  • デバック
  • Squirrel を使った開発では、エラーの多くがランタイム上で起こる
  • 対策
    • ネーミングルールを統一する、タイプしにくい名前は避ける
    • コマンドラインツールや IDE を使って、シンタックスを事前にチェック
      • 動的シンボルのチェックはできない・・・
    • データラベルなどは、名前を規則化してツールなどでチェック
      • key参照などで、ラベルを動的に作っている場合もチェックできない
  • スタックダンプ
    • スタックダンプを見れば、スクリプトが停止した場所が簡単にわかる。
    • アドレス例外などで落ちた場合、Squirrel のスタックダンプが出ずに停止してしまう場合がある。
      • 下記のような関数を用意し、IDE で PC(プログラムカウンタ)をここに飛ばせば、いつでも Squirrel のスタックダンプを print させることができる。
void ScriptManager::PrintCallStack()
{
    sqstd_printcallstack( SquirrelVM::GetVMPtr() );
}
  • エラーの回復
    • スクリプトエラーが発生した場合は、スクリプトの呼び出しを一時中止し、その間に問題のソースを修正
    • タイムスタンプが更新されたスクリプトファイルを読み込み直して、処理を続行
      • ただし、class インスタンスのような場合、これだけでは回復できない
    • 最も簡単な方法は、その class を保持しているプログラムが、Squirrel 上で try 〜 catch して、再読み込みを行った後で再度インスタンス化すること。
      • 例えば、Enemy の中で止まった場合、EnemyManager が再読み込みを発行し、問題の敵を除外し、敵セット情報を元に Enemy を再生成する。
  • 最適化
  • 気をつけるべき重い処理
    • 文字列の加工
      • realloc が頻発する。フレームレートに影響するほど。
    • コピーの発生する引数や戻り値の受け渡し。(String など)
  • GC
    • 毎フレーム実行するには重い
  • GC実行タイミング
    • フレームレートに影響しにくい、画面の切り替えやフェードのタイミングなどで
    • リソースの入れ替え前(デフラグメントを減らす)
    • ヒープがなくなった時
      • ほっておくとゴミが貯まっていき、いずれメモリーが無くなる
  • 往復を減らす
// これを
local player = SQPlayer();
local vec = player.GetPos(); // retval SQVector
vec.y += speed;
player.SetPos( vec );

// こうする
local player = SQPlayer();
player.AddPosY( speed ): // C++ 側で posVec.y += speed される
  • メモリ
    • 100バイト以下の細かいブロックが沢山できる。
      • VM には専用のヒープ領域を割り当てるように。
    • データ構造に Table を用いるのは便利だが、Array よりもメモリーを消費する
  • VMスタックサイズの調整
    • sqvm.cpp 内の _stack.resize() にブレークを張って、何度も通るようなら VM のスタックは大きめに。
    • 頻繁な realloc を避ける


会場の雰囲気を見る限り、これから 国内では Squirrel が流行っていくかもしれないという印象。
現在 Lua を使っているところも結構多いが、やっぱり文法などに不満を持っている人もかなりいるようで。
Xtal は、やっぱ実績がないと厳しいんじゃないだろうか。

Squirrei は国内でスクエニが導入して、情報を惜しまず公開してきたから流行っている側面もあるし。
Squirrel にとってのスクエニのような企業が出てくればあるいは。