PlayMakerをC#で書きたい
STYLYというXR作品の投稿プラットフォームがあります。
このプラットフォームではNoCodeのスクリプティング環境としてPlayMakerというアセットが利用可能です。
しかし、プログラムを出来る側の人間からすると、そもそももうノードベースじゃなくて、C#のスクリプトで書きたい・・・みたいな要望もいただいたりします。
というわけで、C#でPlayMakerのFSMを書く†闇技術†について公開します。
PlayMakerのFSMを作る
PlayMakerのFSMをC#から生成するには、ビジュアルの要素が、ソースコード上でどのように管理されているのか?ということを把握しておくと分かりやすいので、それについて解説します。
"Fsm"はPlaymakerのFSM全体を指します。その中の状態(State)は"FsmState"として管理されています。そのFsmStateの中にある1つ1つのアクションは"FsmStateAction"として管理されています。また、FINISHEDなどのイベントは"FsmEvent"というクラスで管理されており、状態間の遷移を"FsmTransition"と定義されています。
また、Fsm内の変数(Variables)は、"FsmVariables"として管理されています。
あと把握しておくと便利なのは、データの階層構造です。Fsmという全体に対し、States(状態)、Events(イベント)、Variables(変数)という形で属性が保持されています。Statesは配列で、その中にState(状態)が複数存在します。また、Stateには、StateActions(アクション)とTransitions(状態遷移)が定義されています。StateActionは、その状態でどのようなアクションが行われているのかが定義され、Transitionsは何のイベント(Event)でどの状態に遷移するか(NextState)が定義されています。
全体としては癖の少ない定義かな。と思うのですが、Transitionsの定義が個人的にはちょっと気持ち悪いので、「状態の遷移はグローバルで制御されているのではなく、個別のStateで制御する」ということは頭に置いておくとよいかもしれません。
あと細かい仕様については、公式のWikiも参考になります。
- Fsm
- FsmState
- FsmStateAction
- FsmEvent
- FsmTransition
- FsmVariables
概要はおおむねこんな感じです。これらを使ったスクリプトのサンプルが以下です。
using System.Collections.Generic;
using UnityEngine;
using HutongGames.PlayMaker;
using HutongGames.PlayMaker.Actions;
public class FsmRuntimeSample : MonoBehaviour
{
public void Start()
{
var fsmEditor = this.gameObject.AddComponent<PlayMakerFSM>();
//Fsmの取得
var fsm = fsmEditor.Fsm;
//初期Stateの取得
var startState = fsm.GetState(fsm.StartState);
//Stateの追加
var stateList = new List<FsmState>(fsm.States);
var secondState = new FsmState(fsm); //親となるFsmを引数にnew
secondState.Name = "SecondState";
stateList.Add(secondState); //Listに作ったStateを入れる
fsm.States = stateList.ToArray(); //ListをArrayにして挿入する
//Variableの追加
//FSMが持ってる変数一覧は、fsm.Variables配下に型ごとにぶら下がってる。
var strVariables = new List<FsmString>(fsm.Variables.StringVariables);
var sampleStr = new FsmString("SampleStr"); //UIに表示される変数名
sampleStr.Value = "Hello";
strVariables.Add(sampleStr);
fsm.Variables.StringVariables = strVariables.ToArray(); //Fsmへ変数登録
//StateActionの追加
var stateActions = new List<FsmStateAction>(secondState.Actions);
var debugLogAction = new DebugLog(); //Action系はソースコードが公開されているのでそれっぽく入れる
debugLogAction.text = sampleStr;
debugLogAction.sendToUnityLog = true;
stateActions.Add(debugLogAction);
secondState.Actions = stateActions.ToArray(); //fsmへStateActionを入れる
secondState.SaveActions(); //これがないとうまく保存されない。(保存されることもある)
//Eventの追加
var events = new List<FsmEvent>(fsm.Events);
var sampleEvent = new FsmEvent("SampleEvent"); //カスタムイベントのnew
events.Add(sampleEvent);
//FINISHEDなどの組み込みEventはFindEventで引っこ抜く
var finishedEvent = FsmEvent.FindEvent("FINISHED");
events.Add(finishedEvent);
fsm.Events = events.ToArray(); //Fsmへイベント登録
//Transitionの追加
var finishedTransition = new FsmTransition();
finishedTransition.FsmEvent = finishedEvent; //何のイベントで遷移するか
finishedTransition.ToState = secondState.Name; //注意:両方入れておく!!
finishedTransition.ToFsmState = secondState; //注意:両方入れておく!!
//ToFsmStateを入れなかった場合、そもそも遷移しない
//ToStateを入れなかった場合、PlayMakerEditorの遷移の矢印が出ない
var transitions = new List<FsmTransition>(startState.Transitions);
transitions.Add(finishedTransition);
startState.Transitions = transitions.ToArray(); //Stateへ遷移登録
}
}
適当なGameObjectにComponentとして張り付けて実行すると以下のようなFSMが生成されます。
思うこと
まぁめんどくさいでしょ
そうなんですよ。これってスクリプトで書けるからといって、FSMをC#で生成するコードを書いてもめちゃめちゃ非効率なんです。なので、これ自身の知見をそのまま使うっていうのも難しいです。
なので、自作でスクリプト言語を作って、そのバックエンドとして生かすというのがおそらくは正しいです。いわゆるUdonSharpみたいなアプローチです。実際に試してたのももう2年ぐらい前の話ですが。
技術力で殴る話をすると、最近PlayMakerのノード生成がC#で操作できることが分かった。だから、PlayMakerのノードを生成するC#のコードを生成する独自のアセンブラをPythonで作った。その上で、独自アセンブリを生成できる独自スクリプト言語を自作して、Webサービス化した。大体30時間ぐらいで作った pic.twitter.com/z32e6EgoE2
— こたうち さんさん (@kotauchisunsun) June 25, 2021
Xの画像のやつは確かFizzBuzzだったと思います。私が、最初にPlayMakerを学んだときに、とりあえずFizzBuzz書いとく?と思ってやりはじめたんですが、まぁうまくいかないんですよ。PlayMakerでFizzBuzzは、めちゃめちゃ非効率な書き方でしか書けなくて本当に大変。そういうわけで、リベンジもかねてやった内容です。
では、コードによるFSM生成ってどれくらい実用的なんだっけ?というと、結構シビアです。実はPlaymakerとUnityの耐久テストもやってみて、1,000 StateぐらいがUnityEditorで操作できる限界です。3,000 StateにするとUnityが操作不能になります。
で、簡単なテストも行っていて、Pythonで簡単なプログラムを書いて、それを自分の作ったPlayMakerへの変換システムで変換すると概ねPython1行に対して、5つのStateが生み出されました。したがって、Python200行ぐらいが限度のようです。200行ぐらいじゃなー特に実用性がなーと思います。
fsm_gen
というわけで上記したツイートの画像のような鬼FSMを作成できるスクリプト言語・アセンブラ言語を含んだリポジトリを公開しておきます。
実用性はないんですが、かなり酔狂な人は試してみると良いと思います。
まぁまぁ吐き気を催す邪悪さがあり、FSMを生成するEditorスクリプトのC#のソースコードを生成するPythonスクリプトです。
リポジトリ内には簡易的なスクリプト言語が実装されており、それにより高級言語っぽく記述すると、PlayMakerのFSMが生成されるscript.pyというプログラムが存在します。 実行するとFizzBuzzのFSMを生成する8,000行のC#のソースコードが出力されるのですが、Qiitaに貼り付けたら、Qiitaのエディターが応答不能になったので、そういう覚悟のある人が見た方が良いです。
今回、スクリプト言語まで作ってみた狂人の感想としてはPlayMakerのキツイポイントは2つあります。
- 関数が実装できない
- スタックが存在しない
PlayMakerにはFSMテンプレートのような概念は存在しますが、一般的なプログラミング言語にあるような関数の概念が存在しません。そのため、ある機能を実装したときに、それを再利用する手段が非常に乏しい。という話です。一方で機械語のレイヤーになると、当たり前のように関数という概念は存在しません。そこで、機械語のレイヤーではどのように関数を実装するか。というとスタックで実装されることが多いです。引数や戻り値のアドレスをスタックに積み、その内容によって制御することがあります。一方で、PlayMakerにはもちろんのことグローバルなスタックは存在せず、また、基本的に静的な型であるため配列の扱いも大変です。そのため、機械語で行ってる関数の再現方法を真似ることも難しいので、PlayMaker上でのロジックの再利用性は皆無である。という話になります。なので、fsm_genの中では、相当めんどくさいif文で再利用を計ったり、アセンブラと言いつつ、3割Pythonみたいなスクリプトと混合することで、Python側でアセンブラをマクロ展開するみたいなことで再利用性を図っています。この辺のスクリプトの再利用性は今のところいい方法は思いついてないです。おそらくはPlayMakerではそういうことはするものではなく、GameObject単位でFSMを組むものであり、そのFSMの内側のロジックの再利用性は考慮外である。という割り切りではないか。と思っています。なので、そもそもそういうFSM内のロジックの再利用性を図ろうとしているのが無理筋な印象を受けています。
というわけで、PlayMakerのFSMはプログラムで生成できるけど、普通の頑張りの10倍ぐらい頑張らないと(C#からPlayMakerを生成する知見の学習・アセンブラ言語の設計・アセンブラの開発・スクリプト言語の設計・スクリプト言語の開発・PlayMakerの再利用性のあるロジックの呼び出し方法の発見・State数の上限が厳しいのでアセンブラの設計でState数を削減する工夫)多分実用は難しいよ。という話でした。
宣伝
PsychicVRLabではUnityエンジニア・サーバーサイドエンジニアを募集しています!!ご応募お待ちしています!!