北条ゲームズ Hojo Games

錦の北条の開発ブログ

【Unity】おそらくみなさんの欲しい機能がそろっているNested Prefabを公開します。 


追記:
ネストされたPrefabのGameObjectがRectTransformを持つ場合、Transformと同様に暗黙的な形でその固有な情報(widthなど)が全てNestedのRootにシリアライズされるようにしました

皆さんこんにちは。北条です。

あまり絵的に良い更新内容がなく復活して早々ほっぽりだしていましたが死んでいたわけではないです。元気です。

さて、開発のお話です。
持続可能な大規模開発においては洗練されたワークフローと変化に強いコーディングが必要になります。
変化に強いコーディングは適切に素晴らしきC#を操って常にやっていくとして、ワークフローについては一定最初によく考えて構築していく必要があります。最近潜伏していたのはUnityで大規模ゲーム開発をやっていく際に必要になりそうな概念を横断的にシステムとしてまとめて作成していたからです。(絵的には何も現れないタイプのもの中心なので見せても面白くないものばかりです)

十全とは言えずともまあまあ進んではいて、一定完成して実績を確約出来たらコミュニティへの貢献と勉強を兼ねて公開しようかとは思っているのですが、結構思想など含めマニュアル化するのが厄介な概念が多く時間がかかりそうでした。日本語を書くのは大変です。
しかしその中にもたまに簡単に説明が完結するような独立性の高い概念も結構できていて、そのうちの一つに、やろうと思っているワークフローで必要になってしまってついさっきできあがった小さなサブシステム「Nested Prefab」があります。
実はこの概念、公式で実装すると言ってかれこれ何年か経っているようです。まあ最近Unityさんは色々頑張っているので、優先順位的にもささいなものでしょうし、しょうがないでしょう。
Nested Prefabについては確かに実装上いくつか厄介な問題はあるのですが、解決不可能という感じはしません。

Nested Prefabについてはその名前の通りなのですが、一応解説するとPrefabの中にPrefabを入れてかつそれをそのままPrefabとして認識させ続けるということを成立させる概念です。
これの何がよいのかといえば、まずPrefabとしてまとめたある一つの大きな概念の中にいくつもの同じPrefabパーツが欲しいっていう場面が結構あるんですね。簡単に思い浮かぶものだといくつも同じようなデザインを持ったボタンを包括するUI画面とか、ワークフロー依存ですが敵などの配置をPrefabで一まとめにするとか。NestedPrefabではそれらを同じものだと認識させることができるのでネストされた元のPrefabに対して一回変更をApplyすれば全ての同じPrefabにしっかり反映されるという点と、同じものについては構造に対するパースが一回のみになるので読み込みの時間が削減できるという点で非常にうれしいものになります。(逆に言えばこれらは普通のPrefabでは全く意識されてないハズの所になります。ヤバイ使い方をしているなと思ったら、認識しましょう)

本題なのですが、これは結構利便性が高いのと欲しい人もいるかなと思ったので、ちょっと優先的に公開してみることにしました。

GithubのURLは以下になります
https://github.com/nishikinohojo/HNestedPrefab

ダウンロードしてそのまま自分のプロジェクトにすべて突っ込んでください。
Unity5.5p3で動作確認済み。多分動きます。
予防線を張っておくのですが、初めて公開したものの割りに急に必要性を感じてかなり速攻で作っていたもので、コードの見通しは整理されておらず、結構悪いです。


  • つかいかた

最初に断っておきたいんですがまだ実際には長期運用の中で利用していないという事と、独自UIなどはなくちゃんとUnity公式っぽい範囲での使い方にはなっているのですが自分が示す手順以外で怪しい使い方をする人間のことは考えていないです。
つまり何が起こっても自己責任という事です。
一応何かあったら@nishikinohojoに教えていただけると幸いです。

【基本】

2016y12m24d_045310462.png 
1.GameObjectを用意します

2016y12m24d_045316847.png
2. HNestedPrefabRootをくっつけます
このコンポーネントがくっついているものがNestedPrefabのルートになります。NestedPrefabには自身以外のNestedPrefabのルートをネストすることも可能です。(自身のネストは無限の長さになる謎のものが出来て死ぬので何を意図しているのか分からないためダメです。)

2016y12m24d_045329282.png
3.通常通りProjectViewに対してドラッグドロップでPrefabとして永続化します

2016y12m24d_045347329.png
4.自由に子をいれて構造を作ってください
子になるものはすべて別にPrefabとしてすでに登録されている必要があります。

2016y12m24d_045402043.png
5.普通のPrefabのようにApplyで変更を適応できます。
この画面を例にあげれば、Applyを押して登録した瞬間、該当シーン・他シーン・動的生成のすべてのTestPrefabが登録した構造に変わります。
この際内側のPrefabはPrefabとしての独立性をまもっており、試しにどれかを選択してみてインスペクタ上でSelectを押すとちゃんと内側の選択したPrefabの親が参照されます。

【応用】
1.意図をそれぞれの子に元のPrefabとは別のものを与えたい
Prefabの特徴に、変数をシリアライズしてPrefabのデータとして永続化できるというものがあります。
NestedPrefabにおいては実現方法的に厄介な概念で、残念ながらすべての子の変数フィールドを検知して保持することは非常に難しいでしょう。(NestedPrefabは内側のPrefabをPrefabとして扱うために動的に内側のPrefabたちをそれぞれのソースから生成する概念なのです。)
リフレクションを利用すれば可能かもしれませんが、とくに注入時においてはランタイムでの実行が必須になってしまうためパフォーマンス的に最悪で、死です。

一応全てのGameObjectに存在するTransform(RectTransform)については勝手に別々の意図が保持されるようにはしているのですが、それ以外のすべてについてはHNestedPrefabでは能動的な対応が必要になってきます。
能動的な対応も極力簡単で納得できるものであれば多少は許容できるでしょう。NestedPrefabという概念がUnityのワークフローに入ってくる際に絶対守る必要があるのは「Prefabという単位が状況に応じて独自化したいものを付与されたスクリプトの要求に応じてシリアライズして独自に保つことができる」という所だと思っています。
今回はアプローチとしてインターフェースを用意しました。唯一のオレオレっぽい箇所です。やるべきこととしては二つのメソッドを持ったINestedPrefabRootSerializingContextBridgeというものを永続化したいフィールドを持ったクラスに対して実装してあげるという感じです。これによってネストされたPrefabに付与されている任意のMonoBehaviourを継承したあなたのクラスのフィールドのうち、Inspector上でも確認し操作できるようになっているSerializeされたもので、特にあなたがネストされたPrefabとしてその親の方が個別にDependencyを保持するべきだと思った概念を選択的にシリアライズすることができます。(一応書いておくのですがNestedPrefabの親ではなく個々のネストされたPrefabのソースによる注入の方が適しているフィールドも、例えばtextのフォントとか、明らかにSerializeFieldとして扱った方がUnity的に楽だが個別に操作する必要はあまりないといった概念のようなものとしてもちろん存在すると思います)
これを適切に記述しておくと、子にあるシリアライズされたフィールドが永続化されます。

2016y12m24d_051344825.png 
↑のように、望むものを求めたり、送り出したりする契約です。
出来る限り簡単にしたつもりなので多分画像見れば何すればいいのか、何をしているのかは分かると思うのですが
GetExpectedDataAsContext()でNestedPrefabRootSerializingContextのコンストラクタに登録した順にInject (NestedPrefabRootSerializingContext receiveList)で受け取ってキャストする感じです。
ValueFieldになるものとObjectFieldになるものが別々に登録されています。ValueFieldはstring型で保持しており、ObjectFieldはUnityEngine.Object型として諸々持っています。
ここはちょっとユーザーアンフレンドリーかなと思ってますがまあ…… 
ちなみにここで気付いた人もいるかもしれませんが、Unityがネイティブで厄介な感じにシリアライズしている特殊な概念やユーザーのクラス・構造体は現状シリアライズできません。
ユーザークラスをシリアライズするときは個々の要素を分解してシリアライズし、Injectメソッド内で新たにnewして作ってください。
Unityがネイティブで厄介な感じにシリアライズしているものは実は僕が知る限りだとAnimationCurveしかないのですが、もっとあるかもしれないので情報お待ちしてます。必要なら個々に対応します。(それしか道がないです)
便利でみんな大好きなAnimationCurveについては認識した上で対応していませんが、自分の場合コードの自動生成などもワークフローに含めた上でEnumで1:1でScriptableObjectの一箇所からAnimationCurve定義を引っ張ってくるようにしており、必要ないからやらなかった感じです。必要なら要求してくれればやるかもしれません。
ですが、基本的にAnimationCurveみたいな一つの構造に結構なデータ量があるものって一つの構造が何かしらの概念を表していることが多いと思っていて、プリセット的にどっかに登録して、それを引っ張ってくるほうがメモリ的にもシリアライズフィールドに散乱させるのが好ましくない感じがする話的にも絶対いいと思うんですよね。

さて、少し話が逸れましたが……このインターフェース、実際の利用時に関してはHNestedPrefabRootのPrefabがApplyされた時にGetExpectedDataAsContext()が呼ばれ永続化されるので、Applyされる前に通常通り[SerializeField]した変数をUnityのInspector上で記述しておけばその値が参照されます。
この値の変更は大本のPrefabにはもちろん影響を及ぼしませんし、全て個別に保存されます。
ちなみに各ObjectFieldの参照としては、なんとシーンに存在する時にオブジェクト同士での参照関係を突っ込んでおくとその状態をちゃんと保持しておけます。
いまいち凄さが分からないかもしれないですが、地味に面倒なことやってます。まあとにかく、これによってUniRXを利用したUIパーツへのSubscribeなんかもできちゃいそうですね。動的に生まれてくる子たちをまとめて検出して扱えるようにするのはNestedPrefabを実装する上では課題になりそうな一つでした。
……あ、ちょっと嘘をつきました。参照として保持できるのは最も近いHNestedRoot以下にいるものに対してのもののみで、少なくとも現時点では掌握されているNestedPrefabの範囲外に対してはObjectFieldの参照としては保存できないようになっています。Unityの通常Prefabの場合はシーンに固定的に置かれる場合はPrefab外にObjectFieldの参照を登録できますので、これはちょっとだけ機能的に負けていますね。(まあ、Prefab外のものを能動的にドラッグドロップしてシリアライズフィールドにくっつけるのってそもそも管理的に雑すぎて色々問題な気がしますしどうでもいいでしょう)

余談ですが、MonoDevelopにおいては実装したインターフェースの名前のところを右クリックすればRefactor->Implement interfaceで自動でひな形生成できます。
今はもうIDE時代で、温かみのある手作業は総じてクソです。便利なものは適切に利用して生産性高めていきましょう。
2016y12m24d_055218487.png 



2.NestedPrefabのNestのApply
こちらは応用といってもかなり基本の動作の範疇なので普通にデモンストレーションっぽいですが
2016y12m24d_064343017.png
1.NestedPrefabRootであるUnkoの中に別のNestedPrefabRootであるUnkomanがいます。
 
2016y12m24d_064347580.png
2.中にhageが二匹いますが、そのうちの一匹を選び 

2016y12m24d_064349362.png
3.Deleteします。
 
2016y12m24d_064354430.png
4.この新しくなったUnkomanをApplyしてみると…… 

2016y12m24d_064403146.png
 5.シーン内の別のルートに置かれたUnkomanの構造も変わってくれました。

やったね。
完全に応用でもなんでもねーな。

  • 内側でやっていること

まずUnity公式のApplyで扱えるようになっていますが、セーブする瞬間に食い込んでセーブされる一瞬だけPrefabを構成する子を除去していて、保存されるときはルートのGameObjectのみになっています。
結局のところNestedPrefabをやるには同一ソースからInstantiateしないといけないんですね。ですからPrefabとして残ってほしいのはルートの一つのみになる訳です。
ルートオブジェクトに様々な情報をシリアライズしておき、実際に子を生成する時にいろいろやって必要な機能を実現しています。

あと、実際には動的に生成している概念なのにどうやってプレハブ内ObjectFieldの参照関係を保ってるんだってのが気になるところかもしれません。これ結構怖い予感しますよね。でも多分パフォーマンスはO(1)です。情報構築時にGetSiblingIndexでの順序を記憶していて、構築後にGetChildを繰り返して取得しています。(GetChildってO(1)だよな!?)

以上です。
今制作中のゲームの続報はしばし待たれよ。


Comment

Add your comment