スクリプトエンジンってなに?
スクリプトエンジンとは、ゲーム開発にあたって、プログラマだけでなく
プランナーなど プログラム知識のないスタッフがゲームを簡単に作る、
またはゲームはアプリ化するためのビルド時間が長くため、テキストファイルから読み込んだ内容を
ゲームに反映させ楽をするために実装するものです。
例えば、ADVゲームなどで街を作って人を配置して、近寄ったらどんな会話をするのか、などの設定を記述したりするコードです。
メリットは以下の通りです。
・プランナーなど プログラム知識のないスタッフがゲームを簡単に作れる
・ビルドしなくても更新できる
・AssetBundleからテキスト更新もできるのでスクリプトを追加して新しいイベントの追加、も可能
デメリットは以下の通りです。
・スクリプトエンジンを導入するのにコストがかかる(時間と人員)
・スクリプトのトライアンドエラーが発生する
・ スクリプト でやりたいことが増えると肥大化していってしまう
MiniScriptってなに?
オープンソース(C#,C++)のスクリプトエンジンです。
何が出来るの?
やろうと思えばいくらでも出来てしまうので、何をしないといけないのか、ということをリストアップする必要があります。
私の場合は以下のことをやろうと思ってスクリプトエンジンを導入しました。
- フェードイン・アウト
- キャラクタ表示・非表示
- キャラクタ表示確認
- 会話
- アニメーション
- UI操作
- 入力切替
- 移動
- 回転
- カメラON/OFF
- カメラ 追尾設定
- カメラ 移動
- カメラ 回転
- カメラ シェイク
- フラグ on / off
- フラグ確認
- BGM再生
- 環境音再生
- ジングル再生
例えば(MiniScriptで記述した場合)以下のような形でイベントシーンをテキストで記述したかったのです。
// initialize
playerinput false // 入力不可状態に指定
fadeout {"r":0,"g":0,"b":0,"a":1}, 0.5 // 画面を暗転
wait 0.5 // 待つ
uihide // 基本UIを非表示
cam EVENT, true // イベントカメラをON
cam MAIN, false // メインカメラをOFF
moveto PLAYER, 0, {"x":5.25, "y":0.35, "z":-15.85}// プレイヤー移動
cam_moveto EVENT, 0, {"x":3.75, "y":3.58, "z":-14}// イベントカメラ移動
yield // 1フレーム待って座標更新
lookat PLAYER, 0, {"x":4, "y":0.35, "z":-12} // プレイヤーは適当な方向に向かせる
cam_lookat EVENT, 0, {"t":PLAYER} // カメラはプレイヤーを見る
anim PLAYER, "Down" // 倒れた状態
fadein {"r":0,"g":0,"b":0,"a":0}, 2 // 2秒かけてフェードイン
wait 2
// main
wait 3
anim PLAYER, "Down2Idle" // 倒れた状態から起き上がる
wait 3
anim PLAYER, "LookAround" // 周りを見る
wait 1
anim PLAYER, "Idle" // 待機状態にする
// finalize
fadeout {"r":0,"g":0,"b":0,"a":1}, 0.5 // フェードアウト
wait 0.5
cam MAIN, true // メインカメラON
cam EVENT, false // イベントカメラOFF
fadein {"r":0,"g":0,"b":0,"a":0}, 0.5 // フェードイン
wait 0.5
uishow // UI表示
playerinput true // 入力許可
実装すると以下のようになります。
MiniScriptの導入(Unity)
1.MiniScriptのC#コードを手に入れます。以下URLからDownloadします。
使用するのは、 MiniScript-cs ディレクトリ直下のPrograms.cs以外のC#コードです。
スクリプトエンジンを導入したい Unityプロジェクト内に「Miniscript Source」ディレクトリを作成し、
ソースコードを入れてください。以下のようになるはずです。

エラーが起きなければ導入は完了です。
スクリプトエンジンの使用方法
スクリプトエンジンがあるといってもC#側から呼び出さないといけません。
どのようにスクリプトエンジンを使用するのか、記述します。
新しい関数の追加
MiniScriptでは以下の記述や関数の使用が可能です。
しかし、計算のみをスクリプトエンジンでやらせたいわけではありません。
その場合は、自分で作った関数を登録していくことが可能です。
private static void AddIntrinsics()
{
if (intrinsicsAdded) return; // already done!
intrinsicsAdded = true;
Intrinsic f;
// フェードIn
// fade(color={"r":0-255,"g":0-255,"b":0-255,"a":0-255},name=animationName)
f = Intrinsic.Create("fadein");
f.AddParam("color", new ValMap());
f.AddParam("duration", new ValNumber(0));
f.code = (context, partialResult) => {
MyScriptEngine myscriptengine = context.interpreter.hostData as MyScriptEngine;
var hash = context.GetVar("color") as ValMap;
var color = new Color(hash["r"].FloatValue(), hash["g"].FloatValue(), hash["b"].FloatValue(), hash["a"].FloatValue());
var time = context.GetVar("duration").FloatValue();
myscriptengine.FadeIn(color, time);
return Intrinsic.Result.Null;
};
// フラグチェック
f = Intrinsic.Create("ison");
f.AddParam("flag", new ValString(""));
f.code = (context, partialResult) => {
MyScriptEngine myscriptengine = context.interpreter.hostData as MyScriptEngine;
var flag = context.GetVar("flag").ToString();
if (myscriptengine.IsOnFlag(flag)) return new Intrinsic.Result(ValNumber.one);
return new Intrinsic.Result(ValNumber.zero);
};
}
上記では、”fadein”関数と”ison”関数を追加しています。
context.interpreter.hostDataは後ほどの初期化時に指定することになるので省略しますが、だいたい理解できるのではないでしょうか。
returnした値は、MiniScript側で取得できるので、MiniScript側で if 文による分岐や while による wait も可能になります。
インタプリタの初期化
関数の追加が終わったらインタプリタの初期化をしましょう。
m_interpreter = new Interpreter();
m_interpreter.hostData = this; // MyScriptEngine
m_interpreter.standardOutput = (string s) => Debug.Log(s);
m_interpreter.implicitOutput = (string s) => Debug.Log($"implicit {s}");
m_interpreter.errorOutput = (string s) => {
Debug.LogError(s);
m_interpreter.Stop();
};
hostData が関数実行時の context.interpreter.hostData です。
その他、出力設定が可能です。
スクリプトのコンパイルと値の事前定義
public void ScriptCompile(string eventName)
{
var path = System.IO.Path.Combine(Application.streamingAssetsPath , $"miniscripts/{eventName}.txt" );
var miniScript = System.IO.File.ReadAllText(path);
m_interpreter.Reset(miniScript);
try
{
m_interpreter.Compile();
}
catch (MiniscriptException err)
{
var lastError = err.Description();
Debug.LogError($"Compile error: {lastError}");
}
foreach( var char in Enum.GetValues(typeof(Defines.CHARAS))) {
var char_name = Enum.GetName(typeof(Defines.CHARAS), char );
var char_id = new ValNumber((double)(int)char );
m_interpreter.SetGlobalValue(char_name, char_id);
}
}
m_interpreter.Reset 関数で読み込んだテキストを引数として渡します。
m_interpreter.Compile 関数を実行するとその読み込んだスクリプトをコンパイルします。
例外が発生した際はMiniscriptException で取得可能です。
C#側のEnumの値と同期させたい、という場合などは
m_interpreter.SetGlobalValue でenum名と値を設定してしまえば、同じ値を使いまわすことが可能です。
テキストは今回の例はStreamingAssetsから読み込む形です。
スクリプトの実行
スクリプトはコンパイルしただけではRunning状態になるだけで、実際に実行しないといけません。
public void Update()
{
if (m_interpreter.Running())
{
try
{
m_interpreter.RunUntilDone(0.01f);
}
catch (MiniscriptException err)
{
var lastError = err.Description();
Debug.LogError($"Update error: {lastError}");
}
}
}
m_interpreter.RunUntilDone は引数で最大実行時間を設定できます。
こちらは、0.01秒で終わらせるように指定しています。
まとめ
導入も簡単で、ライブラリ化されていないためエンジン側もカスタムしやすいMiniScript。
しかし、機能が揃っているため今のところスクリプトエンジンをカスタムしなくても
関数追加と変数設定でやりたいことは実装できました。
ADVゲームを作りたい、や、RPGの一部をスクリプトエンジンで動作するようにしたい、などありましたら
無料でパワフルな MiniScript はとても良い選択肢になるのではないでしょうか。