目次
組み込み時、Miniscriptから取得した変数を保存する方法
概要
MiniScript組み込み時に関数経由で変数を取得したい時があります。 少なくとも管理者は以下の理由により必要がありました。
- 敵スクリプトを書いてる時に1回命令を送って、行動を変えたい
- プレイヤーの行動に合わせて敵やボスの行動を変更したい
- ツールを作っていて、特定命令を1回実行したらツールの色が変わるようにしたい等
上記内容について、実装がうまく行ってるのでその内容を記載します。 なお、C#/C++について記載していますが、C++については大分内容が込み入ったものになります。
C#での実装
public class SampleClass: MonoBehaviour { //サンプルのノート。 public Value play_list; void intrinsic_define(){ f = Intrinsic.Create("get_list"); f.AddParam("note"); //リスト想定 f.code = (context, partialResult) => { var rs = context.interpreter.hostData as SampleClass; //ノート Value temp_value = context.GetVar("note"); rs.play_list = temp_value; return Intrinsic.Result.Null; } }
だいぶ簡単です。メンバ変数にValueを保管して、デリゲート処理で指定を間違わないようにすれば、楽に変数を格納可能です。 また、C#は値渡しがメインの機能で提供されているので、勝手に参照先が開放される等がなく管理は楽になっているかと思います。
つまりC#での実装においては以下の理由からあまり難易度が高くないと言えます。
- デリゲート機能があるため、クラス内のメンバ変数にアクセスができる
- 値渡しが主の機能になっているので参照先が開放される等のリスクを考慮しなくていい(auto使う場合はその限りではありません)
C++での実装
ではC++の実装でMiniScriptを使う場合はどうでしょうか。
結論から言うと、以下の理由でそう簡単にはいかないことが分かっています。
- C++にはデリゲート機能がなく、Valueについては起動しているVM内で保存する変数の「参照渡しになる」
- ValueクラスがVMへの参照しか持っておらず、実変数・文字列は全部VM上に持っている
- Context(VMがスクリプトを読んで計算する1回の処理)上での変数定義は保存しているが、基本的にはContext上でのローカル変数は開放されてしまう
※Contextについては以下解釈が近い模様。
https://e-words.jp/w/%E3%82%B3%E3%83%B3%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88.html
現在ツールを開発していますが、上記問題に遭遇したため、色々資料を集めながらなんとか対応しました。 以下にその対応方法を記載します。
1.関数から引数を受け取るための実装
これは楽で、以下のような実装になります。 組み込み関数の実装が色々でてきますがご了承ください。
static IntrinsicResult intrinsic_test(Context* context, IntrinsicResult partialResult) { Value x = context->GetVar("value"); context->vm->GetGlobalContext()->variables.SetValue("_sequence1", x); //staticなフラグ管理する。 seq[1] = true; return IntrinsicResult::Null; }
関数の引数から値を参照する
上記は第1引数を取得する組み込み関数の実装です。
まず、単純にValueをContext上から取得します。これによってContext上の変数を参照できるわけですね。
Context上では一意となる文字列をもって値を取得することができるため、あらかじめ組み込み変数で使う文字列を指定しておいて、処理中に参照することが可能になっております。(今回は記述を省略してます)
グローバル領域に変数を保存
次に、vm上の「グローバル変数を保存しておく領域」となるGetGlobalContext()に変数を保管します。
これはContext外やstatic変数で保存しようとする場合、あくまでVM上の一時的な参照領域にアクセスする事になってしまうため、処理中ならまだしも結果を受け取りたい場合処理終了後になることが多く、その場合VM上ですべて開放されているため、アクセスができない事になってしまいます。
結果、組み込み関数上でstatic変数に値を格納しようとしても、後でアクセスしようとしたときには変数がすべて初期化されていることになります。
ただこのおかげでMiniScriptがプログラム本体に影響なく処理を行えるシステムになっているということではあります。
これらのグローバル変数について正確なウォッチはしてませんが、恐らく「次のスクリプト起動タイミング」までは有効かと思います。 よって、次の実行までに別の場所に保存する必要があります。
※正確に言うと変数のIndexが作られたら以降のプログラム実行後もそのままではあるんですが、グローバル変数である関係上いつ更新されるか分からないので早い段階で別の場所に保存しておく必要はあると思います。
2.保存した変数をキープしておく
Value x = interp.vm->GetGlobalContext()->GetVar("_sequence1"); savedInterpreter.vm->GetGlobalContext()->variables.SetValue("_sequence1", x);
コードがややこしすぎて説明も難しいんですが書いていきます。
上記は、「スクリプトを実行した後で保存したグローバル変数を、別インタプリタのグローバル変数として保存する」処理を書いてます。
先ほどの話の通りで、基本的にはMiniScriptの変数はInterpreterクラス上で保存されているため、もう1回起動したり開放する場合、一時的に保存した変数等もずっとアクセスできることが保証されていません。
上記を回避する策として、もう1個Interpreterクラスを作り、このVM上のグローバル変数として保管する実装を行います。
上記の例では、「SavedInterpreter」がもう1個作成したInterpreterクラスのインスタンスになります。
ちなみに未使用のInterpreterのほうは実行しない想定です。そうしないと状態がキープできないですからね。 とは言うものの一度Contextを起動しないとうまく初期化ができないので、ソースコードが空の状態で起動して、上記の処理を行うことになります。
3.未使用Interpreter上から変数を参照する
これでいつでも変数を参照できるようにはなりましたが、先程の話の通り利用中のInterpreter上だといつ更新されるかわかりません。 よって、参照するときはコピーした未使用のInterpreterクラスからの参照を行います。
Value search; search = savedInterpreter.vm->GetGlobalContext()->GetVar("_sequence0");
上記は、変数を作成し、未使用InterpreterクラスからIndexerで変数にアクセスし取得する処理です。
ややこしいですがここまででようやく変数をContext外で保存、ないしはアクセスすることができるようになりました。
ちなみに、特定のインデクサでアクセスできる事自体は「誰も管理していません」。
すなわち何も登録していない場合、アクセスした時点で内部で例外的なアクセス扱いになります。(つまりエラー)
よって、このインデクサで変数を登録しているよ!という管理が必要になります。
自分の実装では、別途変数を設けてこのインデクサにアクセスしていることを変数を見て確認した上でアクセスできるようにしました。
最後に
サンプルプロジェクトも用意したほうがいいかなと思い始めてます。説明がややこしいので。