Open6

Unity上で動く自作言語を作る

chicchiiskchicchiisk

言語を自作する目的

Unity上で動くイベントシステムを作りたい。
イベントシステムは、テキストアドベンチャーゲームのように、2Dの立ち絵が演技し、下にテキストが表示される形式シーンを簡単に実現するためのシステムと定義する。

イベントシステムの実現には、既存のツールを使ったいくつかの方法が考えられる。

  • C#スクリプトとして記述する
  • CSVなどの表形式ファイルを利用する
  • jsonやyamlなどのDSLを利用する

上記の実装方法の例を下記に示す。

C#スクリプトとして記述する

SampleScript.cs
public void EV000_SampleScene(EventArgs arg)
{
    Acter acterA = LoadActor("John");  //ジョンの立ち絵を読み込む
    Acter actorB = LoadActor("Bob");   //ボブの立ち絵を読み込む
    Window msgWindow = CreateMsgWindow(); //メッセージウィンドウを作成する
    Dictionary<string,string> scenarioDic = LoadScenario("ev000.csv");

    acterA.Show(EPos.Left, EDir.Right);  //ジョンを画面左に、右向きで配置する
    acterB.Show(EPos.Right, EDir.Left);  //ボブを画面右に、左向きで配置する

    //台詞を表示
    msgWindow.ShowMsg(acterA, scenarioDic["ev000_lb0000"]);
    msgWindow.ShowMsg(acterB, scenarioDic["ev000_lb0001"]);

    acterA.Move(new Vector2(100,0)); //立ち絵を動かす
    acterB.ChangeFacial(EFacialExpr.Surprise);  //立ち絵の表情を変える
}
ev000.csv
ev000_lb0000,やあボブ。こんにちは。
ev000_lb0001,こんにちはジョン。久しぶりだね。

csvなどの表形式ファイルを利用する

SampleScript.cs
public void Execute(List<EventRecord> script)
{
    foreach(var r in script)
    {
        switch(r.action)
        {
            case : EAction.Msg : ShowMsg(r.acter, r.arg1); break;
            case : EAction.Move : Move(r.acter, r.arg1, r.arg2); break;
            case : EAction.Facial : ChangeFacial(r.acter, r.arg1); break;
        }
    }
}
ID アクター アクション 引数1 引数2
0 ジョン セリフ やあボブ。こんにちは。
1 ボブ セリフ こんにちはジョン。久しぶりだね。
2 ジョン 移動 100 0
3 ボブ 表情 驚き

jsonやYAMLなどのDSLを利用する

※データがjsonなだけで、ほぼcsv形式と処理は同じなので省略

chicchiiskchicchiisk

上記のような実装は、それぞれ下記のようなメリット、デメリットが有る。

パターン メリット デメリット
C# C#の資産をフルに活かせる。最適化が比較的容易 事前コンパイルが必要。シナリオ記述が得意なわけではないので、冗長になる。スクリプターの学習コストが高い
Csv Excelがあれば作業ができ、学習コストが低い 必要な機能に応じてデータ構造の修正が必要。制限が多い
Json デシリアライザがあるため、C#への組み込みがし易い 書きづらい。Csvと同様のデメリットが有る
chicchiiskchicchiisk

上記理由により、正直いずれの採用も悩ましいところ。
シナリオ記述に特化した自作言語を作成し、C#との連携を強化することで、C#のメリットとCsv/Jsonのメリットを両立しつつ、いずれの方法よりも作業時間効率の良い環境を作ることが本プロジェクトの目的。

コンセプト

  • インディー規模向けシナリオスクリプト
  • 素早くかける、簡単にかける、自由にかける
    • シナリオライター/スクリプターがすぐに習得できる
    • 日本語キーボードでタイプしやすい
    • 一般の高級言語が実装しているような機能が使える自由度
    • C#との相互連携による、.Net資産の活用
chicchiiskchicchiisk

スクリプト言語の名前

KoromoEventScript
略して、KES

コンパイラの構造

コンパイラはまず、スクリプトを字句解析/構文解析により抽象構文木(AST)を作成する。
抽象構文木に型情報及び変数/関数のアドレス計算を施したものがHIRである。
HIRを解析することで、VMの実行形式であるKES_ILを生成する。

KES言語を実行するVMをC#で構築し、Unityランタイム上で動かす。
VMはシステムコールのような形式でC#のコードを呼び出すことができ、
C#側からも、KESで定義した関数を呼び出すことができる。

chicchiiskchicchiisk

文法を考える

C#をベースとした文法に、ノベル構文を組み込めるような構造にする。

ノベル構文の組み込み

演出文脈で使用するものはKES言語の関数で記述する。
関数の中身はKES言語で記述するが、根本のUnityの機能などはC#で実装したラップ関数をコールする作りにする。

脚本文脈で使用するものは「登場人物の設定」「ト書き」「セリフ」である。
これらは独自のノベル構文を組み込んだような文法で記載するものとする。
下記サンプルの、#{ }ブロックがそれである。

#{ }では書きづらいので、以後はノベルブロックと呼称する。
ノベルブロックは、その一つがメッセージウィンドウの表示/非表示に対応している。メッセージウィンドウには名前ウィンドウをつけることができ、名前を付ける場合は#<名前>{ }、つけない場合は##{ }のように記述する。
カッコが開いたときにウィンドウが表示され、閉じたときにウィンドウが閉じる仕組みとなる。
また、ウィンドウが閉じる際に、ユーザーのキー入力待ちを行う。

func EV000() -> int
{
    let actor1 = LoadActor("actor1");
    let actor2 = LoadActor("actor2");

    // ##から始まるブロックは名前なしウィンドウ
    ##
    {
        ここは名前なしウィンドウとして表示されます。
        改行もできます。

        空行を挟むと、改頁されます。
    }

    // #<名前>から始まるブロックは名前付きウィンドウ
    #人物A
    {
        ここは名前付きウィンドウとして表示されます。
        人物Aが話しているセリフとして表示されます。
    }

    // Actor型の変数を渡すことで、そのアクターの名前でウィンドウを表示できます。
    // これは、名前が可変の登場キャラクターの管理などに便利です。
    #{actor1}
    {
        セリフ中にもアクターの名前を仕込めます。
        {actor1}とすることで、actor1の名前を表示できます。
    }

    //動きなどの演出は、関数を呼び出すことで行います。
    actor1.Move(100,0);
    actor2.Facial("angry");
}