Behavior TreeによるUnity上でのUIフローコントロール

Wright Flyer Studiosでゲーム開発を行っている久富木隆一(@ryukbk)といいます。
本記事では、
- 代表的なUnityアセットPlayMaker
- 2014年初頭にリリースされたUnityアセットBehavior Designer
の紹介と、利用例を通じ、Unityと、Unityアセット利用のメリットについてまとめてゆきます。
Unityをまだ使っていない方、あるいは使い始めたもののどんな作法に則って使うべきか探っている方にお勧めの内容です。

Unityって

最早普及しすぎて説明不要とも思われるUnityですが、始めに、私から見てUnityとは何か、という視点をお伝えします。Unityそのものは利用者によって全く異なる用途に使われうるミドルウェアであり媒体であるからです。

私にとってのUnityとは、まずもって、スマートフォン上での代表的RAD(Rapid Application Development)環境です。
- Editor上のInspectorで値を変更すると直ちにGameビューに反映される
- スクリプト変更時も、.NETのAppDomainの仕組みを用いて、リビルドに長大な時間をかけることなく再プレビューできる(cocos2d-xでのスクリプト言語の利用や、C++ホットリロード機構のあるUnreal Engine 4でも、似たことは可能)
- EditorのIDE自体をゲーム同様のコードで動的にカスタマイズしツールを容易に構成できる

等々、少人数開発を初期から高速に進めるために必要な勘所を、絶妙なセンスとバランスで押さえているのがUnityである(Pro版の費用を忘れれば)というのが私の感想です。

つぎに、Unityが扱える対象プロジェクトですが、Unityは元々Unity 3Dとも呼ばれてきたとおり、3Dアプリケーションの開発に定評があります。一方で、モバイルゲームでは2Dが主流なので、
- 2Dに適したAPI
- 無料
- オープンソース
- 低レベル最適化可能性
というメリットのあるcocos2d-xを用いるプロジェクトも増えています。

これに対しUnity側では、2013年にUnity 2Dと呼ばれる一連の機能をリリースして2Dサポートを明瞭にしました。Sprite Packerが有料版のみの機能であるなど制限は一部あるものの、Mecanim Animatorによる2Dスプライト/キーフレームアニメーションとPhysics2Dの物理演算で、従来のワークフローを活かしつつ手軽に2Dゲームを制作できる環境は整ったといえます。

ここで、Unityの思想的側面にも触れておきましょう(完全に私の趣味の世界ですが)。

モバイル向け開発に入る以前に、私は、C++を使ったMicrosoft Windows向けのアプリケーション開発に携わっていたのですが、その当時影響を受けたモデルとして、COM(Component Object Model)がありました。大雑把に言うと、COMは、バイナリコンポーネントにC++のvtableを基礎とする言語非依存のインターフェイス(IUnknown etc.)を付け、スクリプト言語を含む各種言語から利用可能としたテクノロジーで、Mozilla Firefoxの基盤技術であるXPCOM等に影響を与えています。複数のコンポーネントをハイレベルのスクリプトで都度バインドすることでアプリケーションを構築するので、コンポーネントの再利用が促される、コンポーネント指向という考え方がその基盤でした。オープンソースの勃興で霞んでしまいましたが、現在のモバイルアプリストアやUnityアセットストアにも通ずる、バイナリのActiveXコンポーネントの商用流通も当時はありました。

COMが、(皮肉にもCOMと最も親和性の高かった)Javaの影響を受けて生まれ変わったのが.NET Framework/CLIであり、付随するC#言語でした。Unityは、2005年に発表された当初から、開発環境Editorと実行環境Playerの双方で、.NET Frameworkの非Windows向け移植版であるMonoを組み込んで最大限活かしつつ構築されています。Unityの実体はC++で書かれたゲームエンジンですが、Monoを介在させることで、ネイティブの世界に存在するオブジェクトを表象し手軽に操作するためのハンドルをユーザーに公開しています。

また、Unityは、シーンに存在する個々のオブジェクト(GameObject)に、C#他の言語で書かれたスクリプトをMonoBehaviourを基底とするコンポーネントとして付加して振る舞いを追加していく、コンポジションの構造を採用しています。この点は、継承を基礎とするオブジェクト指向のクラス階層とはやや位相を異にします(もちろん、この構造からは独立して、C#のスクリプトでクラスライブラリを構成することもできます)。

個々のスクリプト同士は、GetComponent APIやSendMessage APIで、シーンを構成するオブジェクト階層のレジストリ(Hierarchy)から対象を見つけてメッセージを送り、通信しあいます。複数のMonoBehaviourがシングルスレッド上で協調動作する仕組みは、coroutineを利用します

2008年にiOS向け出力をサポートして以来、モバイルの世界で一躍有名になったUnityですが、将来のIL2CPPによっても、Editorでの柔軟性を維持しつつ、C++トランスレータによってバイナリ出力時に.NET Nativeライクな最適化を施せるようになるという方向性が示されています。

ツールボックスとしてのPlayMaker

それでは今回扱うツールの紹介に入りましょう。Unity上で利用できる出来合いの部品を販売するアセットストアで販売中のアセットの中でも、特に有名なのがPlayMakerです。PlayMakerは、有限状態機械(FSM:Finite State Machine)を簡単にGUI上で作成できるツールです。

FSMをゲームで使うといえば、まずキャラクターの行動を制御するAIが思い浮かびます。他には、イベント管理でしょう。一定の条件を満たしたら、何らかのイベントが起こる...という台本を、FSMを使って書くのです。ところが、私が実際にPlayMakerに触れてみて特に印象的に感じたのは、それらの主要な特徴とは別の、PlayMakerが持つ役割でした。

もちろん、ビジュアルデバッガとしても使えるPlayMakerは非常に便利なツールで、ろくに寝ておらずまともなコードが読み書きできるか怪しい時ですらも、やすやすとロジックを構築し、その抜け漏れを視覚的にチェックすることができます(実際はそんなに上手く行きませんが:p)。そういった特徴とは別に私が感じたのは、PlayMakerに対応することが、他のツール系アセットの一種のステータスになっているということです。

どういうことかというと、PlayMakerはFSMを扱うツールであるという以上にビジュアルプログラミングのためのツールでもあるので、スクリプトを書かなくても良いように、多種多様な処理があらかじめアクションとして添付されているのです。例えば、Physics2Dという機能がUnityに入れば、「2Dオブジェクトに物体が衝突したイベントを検知する」という処理が追加パッケージとして提供され、やがては本体に標準添付されるようになるでしょう。

アクションは、数値を変数に代入したり比較したりといったプリミティブなものから、物理演算やグラフィックス処理まで、様々なものが多数添付されており、コードを全く書かなくてもゲームをある程度構築できるようになります(実際、本記事で後ほど紹介する例は、10数行しかコードを書きません)。そして、PlayMaker以外のアセットの開発者も、PlayMakerユーザーが多数に上ることを知っているので、ある段階になるとPlayMaker用アクションを用意し、PlayMakerサポートを謳うようになるというわけです。Unityアセットエコシステムのいわばハブであり、道具箱となっているのがPlayMakerです。

そうして優秀な道具箱としてPlayMakerを捉えた時、ツールの束をまとめるFSMという機構自体の価値はどうなのかという点について再検討すべき時が来るかもしれません。その際に1つのオルタナティブとして有用なのが、これから紹介するBehavior Treeというシステムです。

Behavior Designerによるフロー記述

FSMの欠点を補うものとして、階層型有限状態機械(HFSM:Hierarchical Finite State Machine)という構造があり、これはFSMを入れ子状にすることで、あるステートの内部に複数のステートマシンが動くといったような複雑な状態の表現を可能とします。近年のゲーム開発では、AIの表現としてさらにHFSMに代わり、ビヘイビアツリー(BT:Behavior Tree)(和訳の定訳は無いようです)という仕組みが流行っています。私が以前開発に携わったタイトルMONPLA SMASHでもHFSMやBTを利用していました。

BTとは何であるかについては紹介記事を参照頂くとして、ここではひとまず、様々な処理ノードをつなげたツリーをルートから深さ優先で探索してゆき、ノードの実行結果に応じて兄弟ノードの実行を決めたり、子ノード相互の実行順を制御できる親ノードがあったり、といった形で一定種類のノードが有り、それらのノードの組み合わせでロジックを構築するのがBTである、と考えてください。

単純な例を挙げると、例えばFSMを持つ複数のアクターがいたとして、それらアクター群をまとめて制御するようなロジックをFSMで書こうとすると面倒なところ、BTを使うと容易に表現できます。個々のFSMが常に何らかの1つの状態にあらねばならないのに対し、BTには複数の子ノードを並列に実行する仕組みがあるため、より複雑な状態を1つのBT内部で表現可能です。また、FSMの各ステートはどこから来てどこへ行くという遷移の結線で密結合されており再利用が難しい(実際にはPlayMakerはFSMテンプレートのコピーペーストをサポートしていますが)のに対し、BTのノードは相互に独立しているため、再利用が容易です。BTのノードは、疎結合された、再利用可能なコンポーネントなのです。

BTをUnity上で扱うのに有用なツールが、2014年始めにリリースされたBehavior Desiginerです。そして、Behavior Designerもまた、PlayMakerをフルサポートしています。これを用いれば、フローをBTで記述しつつ、PlayMakerの多彩なアクションを応用して、相互補完させながら強力なコードレスプログラミングができるのです。

Behavior Designerにも、ごくごく基本的な具体的処理を行うタスクは添付されていますが、PlayMakerには全く及ばないので、コードを書かずにそれなりのシステムを構築しようとするとPlayMakerに頼らざるをえないのが現状であるとも言えます。もちろん、タスク自体はコードで拡張できますので、大量のデータを複雑なロジックで処理する必要がある場合はコードを書いて対応できますし、PlayMaker無しでBehavior Designer単体で使うだけでも十分有用です。Behavior Designer自体のソースコードも添付されていますので、もしもの場合にも対処可能です。

一方で、BTは毎フレーム走査される(tick)ため、パフォーマンス上の懸念が出がちですが、Behavior Designerのデータを参照する限り、多用しすぎなければ問題ないレベルであるといえます。このあたりはPlayMakerについても同様で、便利なので何でもかんでもFSMで書くという方針にはパフォーマンス上の限界がありますので、パレートの法則に従い、精緻なコードを編み出すリソースを2割のホットスポットに集中させるために、その他の、低レイテンシの応答性を要求しないような部分を、こういったツールで適当に賄うという方針が妥当です。

サンプル: UIフローコントロール

では、Behavior DesignerとPlayMakerを組み合わせたシステムの例として、UI遷移の制御システムを作成してみます。ゲーム開発に留まらず、一般アプリケーション開発でも頻繁に登場する要件です。Unityでは、UIを構成する個々のコンポーネントはNGUIuGUIといったシステムが提供しますが、現状では、それら部品で構築した画面間の配線については、遷移を制御する構造を別途用意する必要があります。

今回用いたツールのバージョンは
- Unity 4.5.1
- PlayMaker 1.7.7.2
- Behavior Designer 1.3 ベータ版
です。Behavior Designer 1.3には、Behavior Designer内のローカル/グローバル変数をPlayMaker内の変数と同期させられるという大変重要な新機能が追加されています。

余談ですが、大抵のUnityアセットは1人2人程度の少人数で開発されているケースに該当するため、大企業が開発した、QAするにも長大な時間を要するミドルウェアに比べ、直接開発元に問い合わせれば柔軟な対応をしてくれる場合が私の経験上多いです。このあたりは、Unity Technologiesが説くところの「開発の民主化」面目躍如といったところでしょうか。

まず、大まかに、何をしたいか考えてみましょう。UIの遷移ですから、
- 画面Aから画面Bに、ボタンXを押したら切り替わる
という処理が考えられます。この処理があったら、たとえばWebブラウザで「戻る」ボタンを押した時のように、
- ボタンYを押したら画面Bから画面Aに戻る
という操作があってもいいでしょう。他の操作としては、
- 画面AのボタンZを押したら画面Aの上にダイアログCが出る
というのはよく見る状況です。そして、
- ダイアログC上のボタンを押したら更にその上にダイアログDが出る
という状況も発生するかもしれません。最後に、
- 閉じるボタンを押したらダイアログDが閉じる
という機能が要りそうです。これらを、手持ちの道具を使って表現してみましょう。

最初に、シーン内にUI Flowという、今回のUIフローコントロールを司る役目のGameObjectを配置します。UI Flowは、1つのBT(Bootstrap Behavior)と2つのFSM(CreateRootWindowFSM、CreateChildWindowFSM)を持ちます。順番に見てゆきましょう。
UI_Flow1

Bootstrap Behaviorは、名前通り、最初の起動コードを実行するだけの単純なBTで、それ自体はFSMでも記述できるものです。内容は、一番最初の画面を表示するというPlayMaker FSMをタスクとして実行するというだけです。BT上のこのPlayMakerタスクが、CreateRootWindowFSMを参照しているわけです。

UI_Flow2

CreateRootWindowFSMは、始めのStartFSMと終わりのResume Behavior Treeという、Behavior DesignerからPlayMakerのFSMを呼び出すための作法の他、主要な処理として、RootWindowというprefabをシーン内にインスタンス化します。RootWindowは、ウィンドウを表現する白いスプライトの上に、
- 青いボタン1: 子ウィンドウ生成
- 赤いボタン2: 終了
- 緑のボタン3: 別画面表示
という3つのボタンが配置されているという、素朴なものです。このRootWindow prefabを、PlayMakerの備えるCreate Objectアクションを用いて、PlayMaker内のグローバル変数SpawnPositionの座標に生成します。CreateChildWindowFSMは、緑のボタンを欠くChildWindow prefabのインスタンスを生成するだけで、他はCreateRootWindowFSMと同じです。

UI_Flow3

では実行してみましょう。RootWindowがシーン内に生成されました。このRootWindowは、Window EventsというBTを持っています。Window Eventsは、Parallel SelectorというCompositeタスク(複数タスクの関係を制御するタスク)を利用して、画面上の3つのボタンを押した時のイベントをハンドリングします。Parallel Selectorは、複数のタスクを並列実行し、そのうちの1つが成功を返した時点で他の処理を切り上げて完了します。ここでは、ボタンのどれかが押されて、その結果何かの処理が実行された、というのを配下のタスクの成功とみなすわけです。

UI_Flow4

各ボタンのGameObjectには、ButtonClickFSMが付加されています。このFSMは、Mouse Pick Eventアクションでマウスクリックを検知するほか、ボタンの種類に応じ、異なる処理を行います。
- ボタン1: 子ウィンドウを生成するなら、新しい子ウィンドウの位置をグローバル変数SpawnPositionに設定
- ボタン2: 画面を消去するなら、カメラを前に生成した画面に向け戻して、自己を消去
- ボタン3: 新しい画面を生成するなら、新しい画面の位置をSpawnPositionに設定し、新しい画面を向くようカメラを移動
PlayMakerはSmooth Follow Actionのような気の利いたアクションも用意していますが、ここではやや泥臭く、Get Main Cameraアクションでカメラをローカル変数に格納した上でMove Towardsアクションで動かす、といったことをやっています。PlayMakerのアクションは上から順に評価されていきます。この時、同一ステート内でDestroy Objectアクションを実行すると、実装上の都合なのかステートの最下部であっても他のアクションが実行されないので、Destroy Objectを使う場合はステートを分けています。

UI_Flow5

では、青いボタン1を押してみましょう。これが押されると、Window Events上のSequence Compositeタスク下のタスクが順次実行されるので、"Create child window"なるExternal Behavior Tree Reference(外部BT参照)に処理が移ります。Behavior Designerには、GameObjectに紐付かない独立したBTとして作成した外部BTを、assetファイルに保存した上で、その外部BTのルートノードを別のBTのノードに挿入する機能があり、ここではそれを用いてBTの再利用を行っています。

この場合、子ウィンドウを生成する外部BTを参照しているので、その処理に入ります。この外部BTを見てみましょう。

UI_Flow6

Create Child Window外部BTはごく単純な処理です。(Log Shared Variableは単にコンソールにログを出力するためだけに作ったカスタムタスククラスなので無視してください。)この外部BTの実体は、Start FSM In UI Flowタスクを実行するだけです。このタスクは、PlayMaker FSMタスクを拡張し、シーン上のオブジェクト(UI Flow)をFSM実行時に参照しており、これが今回書いた唯一のコードです。

このBTタスクが、UI Flow上のCreateChildWindowFSMをキックし、ChildWindow prefabをシーン内に生成するわけです。生成位置は、PlayMakerとBehavior Designer間で共有するグローバル変数SpawnPositionを用いて、生成元ウィンドウからZ方向へ-1ずれています。これにより、Z方向に伸びてゆくビジュアルなスタックを構成しています。子ウィンドウを削除するときは、スタックポインタを戻してやるだけです。また、本来はSprite RendererのOrderInLayerプロパティの数値を適切に変えて、重なった子ウィンドウが裏写りしないようにすべきですが、今回はそこまで処理せず、重なりが見えるようになっています。

UI_Flow7

Create Child Window外部BT同様に、Create Root Window外部BTもassetファイルとして存在し、こちらは、もうお分かりだと思いますが、ボタン3の押下に応じてUI Flow上のCreateRootWindowFSMをキックし、別画面を作成します。別画面は、Y方向に積まれていくスタックで、カメラが追随することによって画面遷移を模しています。元の画面に戻るときはカメラが戻ります。とても非効率なシステムですが、3D空間ならではの面白い表現です。

UI_Flow8

結果は、このようになります。

こうして、いくつかの部品を用いて、汎用の画面遷移システムを構築することができました。あとは、これら部品を拡張し、あるいは種類を増やして、各画面に応じた特殊な振る舞い(behavior)を付加していくだけです。

今回紹介したようなツールを用いることのデメリットも挙げておくと、何と言っても、複数人のチームで開発した場合に、成果物のマージが行えないことです。ピアレビューも、行えないことはないですが手間がかかります。反対に、スクリプトを用いるメリットは、チーム開発時にコードマージが可能な場合があることです。従って、PlayMaker等を使おうとすれば、効率的なコンポーネント分割戦略とチーム内作業分担がプロジェクトの成否を分けると言っても過言ではないかもしれません。

とはいえ、最早ビジュアルプログラミングは大量のアセットを扱うゲーム開発のような環境では無くてはならないメインストリームの手法となりました。ツールチェインの出来不出来によって、プロジェクトのスコープは多大な影響を受けます。同じ機能を実現するのに、さほど複雑ではないロジックであったとしても、コードで表現すると、書く者によって様々なスタイルに応じた大して意味のない違いが出がちなのですが、そういった差異の影響を減らすことにもこれらツールは貢献するように思います。

また、こういったビジュアルプログラミングツールで表現された、ゆるふわなグルーロジックの場所と、コードで記述された最適化コンポーネントの場所とが明白に分離されることによって、アーキテクチャ上の見通しが良くなるといった効果も考えられます。チーム人員的にも、スクリプトを記述する者とコードを記述する者とは別のキャッシュ階層に存在することになります。

いずれにせよ、ゲームをプロトタイプから組むのであれば、細かいことは後回しにして、早い段階で着想をどんどん積み上げていける仕組みが欲されているのではないでしょうか。その意味で、Unityとそれを取り巻く環境は、一定の成熟を迎えつつあるようです。