はじめに
Unityで色々とゲームを作っていると、たまに
「RPGやADVで見るような会話イベント処理をつくりたい」
といった事を思うことはないでしょうか。えっわたしだけ?
会話イベントでは「メッセージの再生」「キー入力待ち」などの処理の流れがよく出てきます。
それを実装しようと思った時通常のC#スクリプトで無理に頑張ろうとすると
yield return mes("ほげほげ"); // メッセージ表示&キー入力待ち
yield return mes("アヘアヘ"); // 同上
...以下色々会話イベントの処理...
みたいな、yield returnをつけないといけない仕組みになったりします。いちいち書くのは正直つらいですね。
どうせテキストで書くならこれくらいシンプルにしちゃいたいところです。
mes "ほげほげ" # メッセージ表示&キー入力待ち
mes "アヘアヘ"
...以下色々...
なお、会話シーンを実装するのに役立つアセットとして
- ノードを使ったり、Unity上でコマンドを入れていくFungus
- Excelでシナリオデータを記述する 宴
- テキストベースで吉里吉里やティラノスクリプト寄りの構文で書ける JokerScript
など色々とありますが・・・
今回は、そこまで高機能なアセットは要らないんだけどテキストベースのスクリプトを使ったゲーム中のシナリオ再生/イベント作成の仕組みをある程度手軽に組み込みたい…といった人向けに
「MiniScript」という、組み込み型のスクリプト言語を用いた「メッセージ表示&キー入力待ち」の方法についてざっくり解説します。
(結構ニッチな気がする…)
MiniScript
オープンソースのC++, C#向け組み込みスクリプト言語です。
比較的シンプルな初期命令セットと括弧の無い構文で構成されており、習得が容易な部類の言語となっています。
こちらアセットストアで有料アセットとして販売されていますが、言語のコア部分はgithubからダウンロードしてすぐに使用することができます。
有料アセットの方に含まれているのは「ゲーム中にスクリプトを編集できる、補完機能搭載のインゲーム用エディタ」です。コア機能的にはいっしょ(のはず)
今回はとりあえずコア部分だけ使えればよいので、githubからソースをダウンロードして組み込みを行います。
解説プロジェクトについて
Unity 2019.4.13f1 で動作確認しています。
MiniScriptの組み込み
まずgithubからソース一式をダウンロードします。
https://github.com/JoeStrout/miniscript
解凍して「MiniScript-cs」の中身から必要なソースコード(画像参照)を取り出してUnityプロジェクトのどこかにコピーします。
メインプログラムコードやソリューションファイルは不要なため今回は省きます。
これでプロジェクトへの組み込みは完了です。
ちなみに、「MiniScript-cs」ディレクトリをまるごとUnityプロジェクトに放り込んでも一応問題なく動きます。
実行環境の作成
MiniScriptのスクリプトを実行するには、実行環境にあたるインタプリタを生成、管理する必要があります。
とりあえず今回はお試し感覚で大雑把につくりましょう。
以下のスクリプトを適当な場所に置きます。
using Miniscript;
using UnityEngine;
public class MiniScriptPlayer : MonoBehaviour {
private readonly Interpreter _interpreter = new Interpreter();
private void Start() {
_interpreter.hostData = this;
_interpreter.standardOutput = Debug.Log;
_interpreter.implicitOutput = s => Debug.Log($@"implicit {s}");
_interpreter.errorOutput = Debug.LogError;
// スクリプトテキストの入力
_interpreter.Reset($"print \"Hello World!\"");
// 入力したスクリプトの実行を開始
_interpreter.Compile();
}
private void Update() {
if (!_interpreter.Running()) return;
// スクリプトが終了する、または途中で中断(yield)されたり、一定の処理時間が経過するまで処理が実行されます
_interpreter.RunUntilDone();
}
}
}
適当なGameObjectにこのスクリプトをアタッチしてゲームを起動すると、文字列で入力されたMiniScriptのテキストが実行されます。
実行後、コンソールに以下のログが出ていれば正しく実行されています。
スクリプト入力口の作成
インスペクタからスクリプトを入力できるようにしてみます。
実際に運用するときはスクリプトはテキストファイルなどにするものですが、今回は楽な方に走ります。
MiniScriptPlayerクラスを以下のように変更します。
using Miniscript;
using UnityEngine;
public class MiniScriptPlayer : MonoBehaviour {
private readonly Interpreter _interpreter = new Interpreter();
[Multiline(7)] public string _scriptField; // 追加
private void Start() {
_interpreter.hostData = this;
_interpreter.standardOutput = Debug.Log;
_interpreter.implicitOutput = s => Debug.Log($@"implicit {s}");
_interpreter.errorOutput = Debug.LogError;
_interpreter.Reset(_scriptField); // 変更
_interpreter.Compile();
}
private void Update() {
if (!_interpreter.Running()) return;
// スクリプトが終了する、または途中で中断(yield)されたり、一定の処理時間が経過するまで処理が実行されます
_interpreter.RunUntilDone();
}
}
作成した入力口に以下のスクリプトを入力して実行してみましょう。
print "Hello World!"
wait 1
print "a"
wait 1
print "b"
wait 1
print "c"
wait コマンドは入力した秒数分スクリプトの実行を待機するものです。
実行後、コンソールに1秒ずつログが流れれば成功しています。
コマンドの自作
本題となる、コマンドの自作をやってみましょう。
まずはコマンドの仕様をざっくり策定します。
今回UIまわりにまでは手を出しません。
そのため、最低限テキストに入力したメッセージが見えればよいということで以下のような仕様としました。
- コマンド名は mes
- 引数は文字列ひとつ
- 返り値は無し
- 実行されるとDebug.Logにメッセージが流し込まれる
- メッセージ表示後、マウス左クリックで次のコマンドに進む
Intrinsic.Create() メソッドでコマンドを作成することができます。
コマンド名は前述の通り mes とするため、引数に mes と入れます。
var f = Intrinsic.Create("mes");
次にコマンドの引数を設定します。
スクリプト上だと露出しませんが、内部処理のために引数に名前をつける必要があります。
今回は適当に text としました。
var f = Intrinsic.Create("mes");
f.AddParam("text", string.Empty); // 第二引数は コマンド引数textのデフォルト値
メッセージ再生
ここからコマンド内部の処理を作成していきます。
Create時に返ってきたIntrinsicの code に実際の処理を記述します。
var f = Intrinsic.Create("mes");
f.AddParam("text", string.Empty);
f.code = (context, result) => {
// ここに色々書く
};
まずは先程策定した引数 text をコンソールに表示する処理を書いてみます。
f.code = (context, result) => {
Debug.Log(context.GetVar("text").ToString());
return Intrinsic.Result.Null;
};
context.GetVar()でスクリプト側で入力された引数textの値を拾いコンソールログに送っています。
return の Intrinsic.Result.Null についてですが、
各コマンドの処理は何かしらの Result インスタンスを返す必要があります。
前述の仕様の通り、今回のコマンドでは返り値を無しとするため
MiniScript側に用意されている Null Result インスタンスを使用して返しています。
(空文字列を返したり真偽値を返したりといくつか既定の種類がありますが、Resultの中身は自分で色々設定する事もできます)
この時点で、mes コマンドを用いたコンソールログの出力が可能になりました。
MiniScriptPlayerを以下のようにして、スクリプト側で mes コマンドを使ってみましょう。
using Miniscript;
using UnityEngine;
public class MiniScriptPlayer : MonoBehaviour {
private readonly Interpreter _interpreter = new Interpreter();
[Multiline(7)] public string _scriptField;
private void Start() {
_interpreter.hostData = this;
_interpreter.standardOutput = Debug.Log;
_interpreter.implicitOutput = s => Debug.Log($@"implicit {s}");
_interpreter.errorOutput = Debug.LogError;
var f = Intrinsic.Create("mes");
f.AddParam("text", string.Empty);
f.code = (context, result) => {
Debug.Log(context.GetVar("text").ToString());
return Intrinsic.Result.Null;
};
_interpreter.Reset(_scriptField);
_interpreter.Compile();
}
private void Update() {
if (!_interpreter.Running()) return;
// スクリプトが終了する、または途中で中断(yield)されたり、一定の処理時間が経過するまで処理が実行されます
_interpreter.RunUntilDone();
}
}
print コマンドと合わせて使っても同じようにうごきます。
キー入力待ち
それでは、最後に「キー入力待ち」を実装しましょう。
現状 mes コマンドで最終的に Null の Result を返すとコマンドが終了してしまいます…が、
Result は、返り値の他に「このコマンドの処理が終了したかどうか」をシステムに返す事もできます。
処理が終了していないと返せば、スクリプト全体の実行が一時中断(yield)され、次のフレームで再度該当のコマンドが再実行されるようになります。
MiniScript側に用意されている Intrinsic.Result.Waiting を用いる事でその挙動を取ることができるようになります。
試しに mes コマンドの実装の返り値を Waiting に変えてみましょう。
f.code = (context, result) => {
Debug.Log(context.GetVar("text").ToString());
return Intrinsic.Result.Waiting; // 変更
};
以下のような状態になるはずです。無限にログが流れますね。
コマンドの再実行が行われるという仕様なので、素直に上記のようにしただけだと無限にDebug.Logが呼ばれ続けてしまいます。
これをどうにかするには、コマンド実行の最初のフレームでのみログ表示を行う…といった処理にする必要があります。
Waitingで返したコマンドが再実行される際は、処理の第二引数である result に値が入ってくるようになっており中断時の状態に合わせた処理を書くことができます。
この仕様を利用し、以下のように処理を変更します。
f.code = (context, result) => {
if (result == null) {
Debug.Log(context.GetVar("text").ToString());
}
return Intrinsic.Result.Waiting;
};
すると、コンソールログへの "b" の表示が一つだけになります。
さて、後もう一息です。
ログは正しく出るようになりましたが、今のままだと一生コマンドが終了しないので先に進むことができません。
マウスクリックを検知したら Null リザルトを返すように処理を修正しましょう。
f.code = (context, result) => {
if (result == null) {
Debug.Log(context.GetVar("text").ToString());
}
else {
if (Input.GetMouseButtonDown(0)) {
return Intrinsic.Result.Null;
}
}
return Intrinsic.Result.Waiting;
};
動きがわかりやすいようにMiniScriptのスクリプトも少し変更しましょう。
mes "Hello World!"
mes "a"
mes "b"
mes "c"
これで実行し、マウスの左クリックを行うごとにログが出てくるようになれば完成です!
後はDebug.Logなところを自作のメッセージ表示処理に変えてやるなり、ゲームに合わせて改造していく事でそれっぽいコマンドを作り上げることができるようになります。
全体図
今回作成したクラスは再生環境+コマンド含めて以下な感じになりました。
using Miniscript;
using UnityEngine;
public class MiniScriptPlayer : MonoBehaviour {
private readonly Interpreter _interpreter = new Interpreter();
[Multiline(7)] public string _scriptField;
private void Start() {
// ログ出力先など初期設定
_interpreter.hostData = this;
_interpreter.standardOutput = Debug.Log;
_interpreter.implicitOutput = s => Debug.Log($@"implicit {s}");
_interpreter.errorOutput = Debug.LogError;
// mes コマンド定義
var f = Intrinsic.Create("mes");
f.AddParam("text", string.Empty);
f.code = (context, result) => {
if (result == null) {
Debug.Log(context.GetVar("text").ToString());
}
else {
if (Input.GetMouseButtonDown(0)) {
return Intrinsic.Result.Null;
}
}
return Intrinsic.Result.Waiting;
};
// スクリプトコンパイル、実行
_interpreter.Reset(_scriptField);
_interpreter.Compile();
}
private void Update() {
if (!_interpreter.Running()) return;
// スクリプトが終了する、または途中で中断(yield)されたり、一定の処理時間が経過するまで処理が実行されます
_interpreter.RunUntilDone();
}
}
実運用の事を考えてない都合上かなり端折った部分も多いですが、比較的容易に機能を拡張していけるという事が少しだけでも伝われば幸いです。
さいごに
ここまで見た方はお気づきかなと思いますが、今回MiniScriptが元々持ってる構文やコマンドの解説だったり実運用上の話に関してはまったく解説していません。色々訳わからなかったらごめんなさい。
公式サイトのドキュメントがそれなりに充実しているので、MiniScriptについて知りたいことがあればそちらを参照してみてください。
シンプルで扱いやすい印象ではあるものの日本語情報が超少ないので、
もっと増えてくれると嬉しいな~~~~~~と思っています。
みんなもさわってみよう、MiniScript。