今回はScriptableObject(スクリプタブル・オブジェクト)をより使いこなすためのエディター拡張テクニックをUI toolKitで再現しようという試みです。
内容はタイトルの通りですが、ゲームオブジェクト側のインスペクターからScriptableObjectのデータを操作できるようにする、というものになります。
ScriptableObjectをデータ・コンテナとして利用することで、ゲームオブジェクトがその基本的な設定データを参照する形にし、複数のオブジェクトによるメモリ消費を抑えると同時にアセットとして設定データを保存、一元管理するというのが今回の記事にも関わるScriptableObjectの代表的な使い方の一つです。
Monobehaviourやシーンからも切り離されており、ひとつのScriptableObjectファイルからデータ編集するので、複数あるオブジェクトに対する変更漏れのようなミスも無くなるわけですが、いちいちScriptableObjectのインスペクターを開く必要があります。
というのも上画像のようにScriptableObjectを参照しているゲームオブジェクト(及び、該当するC#コンポーネント)のインスペクター上にはScriptableObject内部の各データは表示されないからです。
しかし、この弱点を改善する方法がe-bookで紹介されています。それが、エディター拡張でScriptableObjectのインスペクター(上画像)をゲームオブジェクト側に表示するようにする、というテクニックになります。
e-book内で紹介されているこのコードは、従来のOnInspectorGUIメソッドを利用するものであり、行数も少なくこれはこれで十分なのですが、昨今のUnityの中でも目玉のUI作成ツールであるUItoolKitを使って同様な機能を再現できないか?と色々調べた結果をまとめていきたいと思います。
UItoolKitとは
エディター拡張もゲーム内UIどちらも作成できるUnityの次世代UIツールであり、C#からでもUI Builderというコード不要のエディターからでもUI作成が可能です。(なお今回、UI Builderは一切使っていません)
現状、従来のUnity UIと比べてまだ機能的に不足している部分があるようですが、より柔軟で使いやすいツールになっているようで、最近ようやく触り出した自分としてはHTMLやCSSの経験が活きる形になっており、Unityちゃんと進化しているんだなと感動できるツールになっていました。
本ブログでは、UItoolKit解説e-bookが公開された時に記事を出していましたが、これは既に読了しており、そのレビューも近々まとめたいと思っています。
カギは InspectorElement
Editorクラスを継承するC#スクリプトを書きます。Custom Editor attributeで対象となるクラスを指定し、CreateInspectorGUIメソッドをoverrideする、というのが主な流れとなります。
本記事の目的のために色々と試行錯誤はあったのですが、分かってしまえば簡単でInspectorElementを利用する、というのが答えでした。
EditorクラスにはtargetというCustomEditorタグで指定したC#スクリプト・コンポーネントへの参照を持つ変数があるのですが、そこから目標のインタンスを取得することが可能です。OnEnableメソッド内でtargetを変換して参照を得ます。
今回、テスト用にEnemyControllerTestというスクリプトを用意して、その基本設定データを保有するのにScriptableObjectを使う、ということにしています。
そしてEnemyControllerスクリプトのStartメソッドでは、EnemyInitDataのMaxHP変数を利用して、敵キャラクターのHPの開始値を設定しています。
InspectorElement inspector = new InspectorElement(_enemyController.EnemyInitData);
というわけで、このEnemyInitData変数にtargetから取得した参照からアクセスしてInspectorElementのコンストラクターに入力するだけです。このInspectorElementをベースとなるVisualElementにAddすることでゲームオブジェクト側のインスペクターからデータを変更することが可能になります。
また本コードの中ではif文でこの処理を行っていますが、これはシーン内にゲームオブジェクトを置いた時にEnemyInitDataに何も設定されていないとエラーが起こるからです。
というわけで、特に他に機能が要らないならば、後はInspectorElement.FillDefaultInspectorメソッドを使って、デフォルトのインスペクターをその後ろに表示すればいいでしょう。今回の目標については、一応これで解決しました。
しかし、せっかくUI toolKitを使っているので、より柔軟なエディター拡張の機能を活用したいですし、実はこのままだといくつかの問題があるので、それを改善していきたいと思います。
エディター拡張を試す
Unityを使いこなす、ということを考えるとエディター拡張は出来るようにならないといけないとは思っていました。今回、色々と試してみてその通りだと確認出来たのが大きな収穫です。
昨今のUnityはエディターについても改良が進められ大分使いやすくなりましたが、それでもまだ色々と足りない部分や不満点があり、じゃあ自分でなんとかしろ!ということになるからです。
特に自分で書いたC#スクリプトのインスペクターは、使いやすいように拡張出来た方が、そのための手間はかかるとしても将来的にはストレス軽減によるメリットの方が多いはずです。
というわけで、見易くて使いやすいインスペクターを目指しますが、テスト用のシーンとスクリプトのため、そこまで複雑なことはしません。
まずはFillDefaultInspectorを使ったことで起きる弊害を解消したいと思います。
C#スクリプト・アセットを表示させる
FillDefaultInspectorメソッドを使って拡張エディターにデフォルトのインスペクターを表示させると、下から二番目にあるscriptフィールドにあるC#スクリプトをクリックしても、Assetフォルダ内のアセットにフォーカスが飛ばなくなります。これは色々不便です。C#スクリプトを編集しようと思った時に、わざわざProjectウィンドウを操作しなければならないからです。
これを解決するためにEditorGUIUtility.PingObjectメソッドを使います。これは指定したオブジェクトにフォーカスを飛ばし、エディター上で目につくようなアニメーション効果を実行する機能です。
インスペクターにボタンを追加し、これを押すとPingObjectメソッドが呼び出され、C#スクリプトのあるフォルダーに自動で移動するようにします。
スクリプト上でアセットとしてのC#スクリプトを扱う場合、MonoScriptクラスを利用します。[SerializeField] attributeを使い、エディター拡張スクリプトに対象のC#スクリプトを指定。ここから参照を得て、PingObjectメソッドに入力できます。
EditorGUIUtility.PingObject(targetScript);
次にボタン要素を追加します。UItoolKit (UIElements)では全てはVisualElementであり、それらを追加し構成することでUIを作成します。
初めに土台となるVisualElementインスタンスを作成し、そこに様々な要素を追加していくことになります。
というわけでButtonを追加。大きさや表示テキストなどを設定し、ClickイベントにPingObjectメソッドを登録するためにButton要素のRegisterCallBackメソッドを使います。設定が終わった後にきちんと目標の要素からAddすることを忘れないように。
エディター拡張スクリプトのアセットをProjectウィンドウ上で選択するとインスペクターにこのようなフィールドが表示されるようになるので、ここに対象となるC#スクリプトを入力します。(この例の場合はEnemyControllerスクリプト)
これでインスペクター上のボタンを押すことで、そのスクリプトのC#アセットに飛べるようになります。
インスペクターをより美しく
目を引くラベルを追加し、ScriptableObjectの部分を折りたたみ出来るようにします。ラベルはLabelです。見易いように大きめで下に線も引きます。ボタンをこのラベルの横に表示するようにします。
headerSectionという空のVisualElementを用意して、そこにラベルとボタンを追加して、Flexレイアウトに指定します。各種設定をして、これで横並びかつボタンは右端に寄るように設定して見易くしました。
折りたためる要素はFoldoutです。ここにScriptableObjectのインスペクターを追加し、その後メインのVisualElementにこのFoldout要素を追加します。これでデータ編集しない時には表示を隠すことが出来るようになります。
実は問題があった・・・
実はこのままだとScriptableObjectファイルを入れ替えた時にインスペクターGUIが更新されない、という問題が発生します。一回別のオブジェクトを選択し、再度インスペクターを開き直さないとScriptableObject部分が更新されません。
そもそもScriptableObjectの表示の上に、ScriptableObjectファイル指定のフィールドが欲しいです。まずそれを追加し、そのファイルが変更されるイベント時にインスペクターの表示の更新が実行されるようにします。
PropertyFieldを追加します。EditorクラスにはserializedObjectというプロパティがあり、そこからFindPropertyメソッドで目標の変数を指定することが出来ます。
RegisterCallback<ChangeEvent<Object>>を使うことで、ScriptableObjectを入れ替えた時に、インスペクター表示を更新する処理を書き加えます。
Foldout要素を一回消去し、状況に合わせてインスペクター表示を更新するというメソッドを書き、それをRegisterCallbackで登録しています。
最終的な見た目とコード
というわけで、より見易く使いやすいインスペクターになったと思います。今回は項目は少ないですが、データが増えてくればくるほどこのようなエディター拡張は効果が増していくのだろうなと思います。最終的なコードは次のようになりました。
UI toolKitいいね!
UI周りはどうしてもコードが猥雑になる傾向があり、取っつきにくかったのですが、実際にUI toolKitを触り始めてみると意外と使いやすいなと思いましたし、何よりその機能性が面白く、早く使いこなせるようになりたいと思いました。
UI Builderはまだあまり触れていませんが、数年前の試験段階から早く使いたいな~と心待ちにしていたことを思い出し、これからガンガン使っていきたい!とやる気が出ました!今後もUI toolKitについて色々と記事を書いていきたいと思います。