N煎ログブログ

n番煎じと言っても過言ではない今更な、でも個人的に躓いたUnityやUE4等での開発についての云々を書いていきます

【Unity5】エディタ拡張を複数のクラスに渡って記述する方法

目次

はじめに

皆さんエディタ拡張はしていますでしょうか。

エディタ拡張を使えばある程度自由にInspectorの見た目を変えたりEditorWindowを自作したりできますよね。

でも機能拡張の処理が複雑化したり、処理そのものが長くなってくると、どうしても全体のコードの長さが膨大なものになってしまいます。

そこで拡張処理を機能ごとに複数に分けようと考えるわけですが、そこでいくつかの問題がありました。

処理を分けたい時の問題点

問題1.処理クラス:拡張クラス=1:1 の仕様?

初めは私も、「一つのファイルのソースコードが長くなるのなら複数クラスに分ければいいじゃない」と考え、各処理ごとに拡張クラスを作成(変数定義は実際にオブジェクトにアタッチするクラスに)。各クラスに「CustomEditorアトリビュート」を付与させてみました。

docs.unity3d.com

しかしなんということでしょう。

古い拡張クラスの処理は新しい(後に処理された)拡張クラスに上書きされ、最後に読み込まれた拡張処理しか適用されなかったのです。

gist.github.com

このコードを作成して見るとどうなるでしょう。

おそらく、Inspectorには「intValue1」「intValue2」の両方が表示されているということはないはずです。

これおそらく、一方が拡張処理を割り当てた後で別の拡張処理が実行されるために、前の処理が上書きされているためと考えられます。

つまり、「1クラスに1つしか拡張クラスが割り当てられない」ということです。

問題2.「serializedObject」の罠

次に、serializedObjectについての問題点もあります。

serializedObjectとはEditorクラスを継承したクラス内で用いることのできる変数ですが、これは拡張される側のクラスのシリアライズされた情報が格納されています。

docs.unity3d.com

つまり、「問題1.」関連で「CustomEditorアトリビュート」が付与できるクラスが一つのみという縛りではかなり致命的な問題になります。

どういうことかというと、上記リファレンスリンク内でも記述されているように、拡張される側のクラスのシリアライズされた情報を基にserializedObjectに情報を格納しているため、「CustomEditorアトリビュート」が付与されていない拡張クラスではserializedObjectの中身は拡張されるクラスがどれか分からないためnullとなってしまいます。もちろんUnity側でエラー吐かれます。

解決策

 問題1解決策.一括定義とC#のPropertyの活用

問題1について、要は「1クラスに1つ拡張クラス」が満たせていれば良いので、マネージャクラスなりを作成し、そのクラスにだけ「CustomEditorアトリビュート」を付与させます。

ここで重要なのは、「拡張する変数はマネージャクラスで一括定義する」ということです。

つまり、Hoge1, Hoge2,...等のクラス処理で使用する変数を全て定義するということです。

そして、定義した変数は全てprivate+SerializeField化し、実際に処理を行うクラスからの変数アクセスはC#の機能であるProperty機能を用いてアクセス権限を「取得(get)」のみに留めることでカプセル化を図ります。

どういうことかというと、従来は

f:id:isemito:20170613025615p:plain

のように各クラス(Component)に対してエディタ拡張のクラスを割り当てていたのを、

f:id:isemito:20170613030655p:plain

というように、エディタ拡張をManagerクラス側に任せるようにします。

 これにより、各処理とそれら処理のエディタ拡張処理を別々に記述することが可能になりました。

 問題2解決策.引数で渡す

問題2については簡単です。

マネージャクラスのシリアライズ情報を引数として各クラスに渡してやればよいのです。

これにより「CustomEditorアトリビュート」を付与していないクラスからでもマネージャクラスのシリアライズ情報を取得することが出来ます。

メリット

さて、ここまで複数に分けて拡張処理を書く方法を紹介してきましたが、結局これの何が良いかというと、「機能単位で処理を記述・管理することが出来る」事に尽きます。

どういうことかというと、例えば私が過去に開発した以下アセットでは、1クラス1拡張制約の為、全ての処理を一つのクラスに記述していました。

例えば以下のように。

gyazo.com

見てわかる通り、行数がとんでもないことになっています(これよりさらに下があります)。これでは管理するのも処理を変更・改変するのも苦労すること請け合いです。

では機能単位で記述したものはどうでしょう。

gyazo.com

このように機能ごとに記述するクラスを分けることが出来、かつそれぞれの行数も処理内容によりますが大体多くて100行かそこらで抑えられています。これにより、各機能の改変や拡張を行いやすくなりました。以前のように「あの機能のあの処理どこに書いてたっけ...」というようなことにはならなくなります。

デメリット

逆に問題となるのは、複数のクラスに分けて記述し、かつこの方法の仕様上、どうしてもプログラムを記述する量やファイル数が多くなります。マネージャクラスを経由する為、1つの機能を追加しようとすると「新規機能クラス」「新規機能クラスのエディタ拡張」「マネージャクラスへの変数やプロパティ追加」と3つの工程が必要になります。

また、マネージャクラスへの外部アクセスがプロパティを経由して頻繁に起こる為、アクセスのオーバーヘッドが心配になります。

と、思ったのですがどうやらプロパティはインライン展開されるようでオーバーヘッドは生じないそうです。

プロパティの使用 (C# プログラミング ガイド) | Microsoft Docs

が、インライン展開されるという事はその分プロパティ使用箇所に埋め込まれるため1つ1つのファイルサイズが大きくなりがちになりそうです。

まとめ

各問題点に対する解決策を適用したコード例が以下になります。

gist.github.com

一つのコード内に書いてますが実際に実装する際はもちろん別々のファイルに記述してください。

またLoadProperty等の中で共通処理がある場合Baseクラスなり作って継承させると良いでしょう。

例えば、私はBaseクラスのOnEnable関数に

this.hideFlags = HideFlags.HideInInspector;

と 記述することでInspectorにマネージャクラス以外を表示させないようにしています。

関連記事

isemito.hatenablog.com