Search Unity

Spotlight チームでは、野心溢れる Unity デベロッパーの方々との共同作業により、Unity ゲームの可能性の限界を押し上げる取り組みを進めています。私達は日々、複雑なグラフィックス、パフォーマンス、デザインの問題に対する、革新的な優れたソリューションを目にしています。また、同じ問題が同じソリューションで繰り返し解決されている状況も目の当たりにしています。

本ブログシリーズでは、Unity の顧客の皆様と連携して取り組む中で、最も頻繁に発生している問題のいくつかについてご紹介していきます。Unity と共に取り組んだ様々なチームが多大な労力の末に獲得した知恵を、ここですべてのユーザーの皆様に共有できることを嬉しく思います。

これらの問題の多くは、実際にゲーム機やスマートフォンでの作業を開始した後や、膨大な量のゲームコンテンツを扱う段階になってから初めて発覚するものです。ここでご紹介する教訓を開発の早期の段階から考慮しておくことで、制作の進行がよりスムーズになり、ゲームはさらに高みを目指せるはずです。

スケーラビリティ

マルチシーン編集が Unity に組み込みになったことで、複数のシーンを使用してゲームプレイの特定のユニットや機能を定義するチームが増えて来ています。これに伴い、Unity が長年抱える「別のシーン内にあるオブジェクトを参照できない」という制約が、以前にも増して負担となる状況になりました。

現在は、この制約は様々な方法で回避できるようになっています。多くのチームが、オブジェクトを直接参照する必要性を抑えるために、スポナーやプレハブ、プロシージャルコンテンツ、あるいはイベントシステムを多用しています。最も多く見られる回避方法は、ゲームオブジェクトに永続的なグローバル一意識別子(GUID)を与えるというものです。一意な識別子があれば、ゲームオブジェクトの既知のインスタンスを、それがどこに存在していても、(それが読み込まれていない場合や、セーブしたゲームデータを実際のランタイムのゲーム構造にマップする場合にも)参照できるようになります。

非常に多くのクライアントが同じ問題を同じ方法で解決されていたので、参照の実装に踏み切りました。様々な選択肢を検討の末、ユーザー視点に立った、公開 API と C# のみを使用するアプローチを採ることにしました。これには Unity 社内で有益なテストケースになるというだけでなく、コードを皆様に直接シェアできるという利点があります。ビルドの提供をお待ちいただく必要はありません。GitHub からコードをダウンロードしてすぐに使用を開始し、ご自身のニーズに合わせて変更を行っていただけます。

こちらでソリューション全体をご確認いただけます。

このソリューションの基本的な構造は非常に単純です。GUID ですべてのオブジェクトを参照できる、グローバルで静的なディクショナリが 1 つあります。特定のオブジェクトが必要で GUID がある場合は、ディクショナリ内を検索します。そのオブジェクトが存在していればそれが取得され、存在しなければ null が取得されます。このソリューションは多種多様なユーザーのプロジェクトに導入されることになるので、可能な限りシンプルにすることを目指しました。上記のリンクから、GUIDManager.cs の中で、どのようにしてゲームオブジェクトによるオーバーヘッドが一切生じない形で静的なマネージャーをセットアップしているか、ご確認していただけます。

永続性

最初に乗り越えるべき課題は、System.GUID から取得する GUID を、Unity が MonoBehaviour の一部としてシリアライズできる形式にすることでした。私は以前、これを自分で試してみましたが、幸いなことにその後 ISerializationCallbackReceiver が追加されたので、これは比較的簡単に行えました。簡単なパフォーマンステストを行ったところ、.ToByteArray() は .ToString() に比較して 2 倍速く、外部のメモリは一切割り当てられないことが分かりました。Unity は byte[] を問題なく扱えるので、補助記憶装置用には明らかに .ToByteArray() が適していると分かりました。

GUID が適切に保存されるようになったのですが、残念なことに、これだけでは不十分でした。プレハブの複製、およびコンポーネントの複製は、両方とも GUID のコリジョンを発生させます。このケースでは、その検知と修復は可能ですが、始めから重複した GUID が一切作成されないようにするほうが明らかに効率的です。ありがたいことに PrefabUtility は、扱っているゲームオブジェクトの種類を特定して、適切に反応するようにする方法をいくつか提供しています。

さらにテストを行った結果、「元になるプレハブの壊れたプレハブインスタンスが、新しい GUID のインスタンスデータを保存しない」という稀なエッジケースがあることが分かりました。ここでも PrefabUtility が役に立ちます。CreateGuid 関数の内部で以下の処理を行って、このエッジケースに対処しています。

ツール

すべてが適切に保存されたところで、次に考えるべきは、ユーザーがこのシステム内にどのような方法でデータを取り込むかでした。新しい GUID の作成は簡単でしたが、それらを参照するのは少し困難なことが分かりました。複数のシーンにわたる参照を設定するためには、ユーザーに両方のシーンを読み込んでもらうというのは非常に理に適っています。したがって初期設定は単純に、通常のオブジェクト選択フィールドに GuidComponent を設定するだけで済みました。しかし、その参照は保存されないようにする必要がありました。異なるシーンにまたがっているため、Unity のエラーが出て、後に無効化されてしまうからです。GuidReference をあたかも通常のオブジェクトフィールドのような作りとしながらも、参照は保存せずに GUID だけを保存する必要がありました。

幸い、特定のタイプ用にカスタムの Drawer をセットアップするのは簡単です。これがまさに [PropertyDrawer] タグの目的です。ピッカーを設定し、結果を取得して、必要な GUID データをシリアライズするだけです。しかし、読み込まれていないシーンの中にターゲットのゲームオブジェクトがある場合はどうするのでしょうか。ユーザーに対して、値のセットがあることを通知し、そのオブジェクトに関するできる限り多くの情報を提供する必要があります。

結果、以下のようなソリューションになりました。

複数のシーンに格納された同一オブジェクトへの参照

上記で設定された GuidComponent を指定する、無効化されたオブジェクトセレクター、ターゲットの含まれるシーンを含んだ無効化されたフィールド、そして現在選択されているターゲットをクリアできるボタンが 1 つあります。これが必要だった理由は、ユーザーがオブジェクトピッカー内で None を選択しているのか、あるいはターゲットのオブジェクトが読み込まれていないために値が null になっているのか、どちらであるか検知することができないからです。しかしこれには大きな利点があります。シーン参照が Asset フィールドになっているので、実際のターゲットオブジェクトが必要な場合に読み込む必要のあるシーンが、以下のように強調表示されることです。

ターゲットのあるシーンが強調表示された状態

すべてのコードは GitHub の GuidReferenceDrawer.cs 内にあります。フィールドを編集不可にしながらも実際のフィールドのような挙動を実装する方法は、以下の通りです。

テスト

すべてが正しく機能するようになったところで、次に必要となったのは、正常な機能が確実に維持されるようにすることでした。私は、Unity 社内で使用するためのテストを記述したことはありましたが、ユーザーコード用の記述経験はありませんでした。お恥ずかしながら、組み込みの Test Runner に関しては知識が乏しかったのです。ですが、Test Runner は驚くほど便利なのです!

上のような状態を作ることが目的です。

「Window」メニューからアクセス可能な Test Runner は、再生モードと編集モードの両方で、テストを作成できる便利な UI を表示します。テストの作成は比較的簡単に行うことができました。

例えば以下は、GUID が重複した時にそれを知らせるメッセージが出るようにするテストの全体です。

このコードは、出るべき警告が取得されない場合は失敗するようにテストハーネスに伝え、私が事前に作成した GuidComponent のクローンを作成し、GUID コリジョンが発生しないようにした上で終了します。

guidBase は、[OneTimeSetUp] 属性をつけたセットアップ関数を、このファイル内のテストを開始する前に 1 度だけ呼び出して生成しています。このため、guidBase の存在は保証されています。詳細はこちらのドキュメンテーションでご覧ください。このテストの記述は予想した以上に簡単でした。また、テスト記述の思考プロセスをなぞっただけでコード自体が各段に改善されたことにも驚かされました。皆様も、ご自身のチームで使用しているツールのテストをぜひ行ってみてください。

今後の計画

この参照のためのソリューションは、ユーザーの皆様に厳しくテストしていただき、実際の使用に耐え得ることが実証されたら、GitHub ではなくアセットストアでの無料ダウンロードあるいはパッケージマネージャー経由で入手可能になる予定です。差し当たっては、改善の余地がある要素や問題に関して、ぜひフィードバックをお寄せください。また今後も Spotlight チームから、Unity を最大限に活用するためのヒントを本ブログでお届けしてまいりますので、お楽しみに!

28 コメント

コメントの配信登録

コメント受付を終了しました。

  1. I just tried to implement this for our use-case. Two problems: PrefabUtility functions seem to be among the functions that can’t be safely called from the serialisation callback, it triggers a “ReleaseAllScriptCaches did not release all script caches” assert when the scene is loaded. This seems to be depending on the scene size, as smaller test scenes work fine. Second, sometimes you want to create a prefab of an object that contains both references between parts of the prefab while it is a prefab, and cross-scene references once it is instantiated in a scene.
    In the end I went for a more traditional implementation that resolves guid collisions in an editor script, and stores both a Unity Object reference and a guid reference depending on how the object can be reached.
    I feel the same way as the first poster: It’s annoying how we as clients have to jump through hoops to solve problems like this on the game side with complicated extra mechanisms, that would be solved trivially on the engine side. (Especially things like this that have been generally solved decades ago…)

  2. So we have to add GuidComponent to all GameObject with cross reference ? Is that really practical for any AAA production? (Or any production)
    Client did a workaround. You are at the engine level, and should work on the engine level. It is doable, as I already did it on a previous big engine I’ve worked on….10 yeras ago.
    So don’t put in the mindset of porting a client solution, as you can get it done, totally transparent for any user, on the serialization level.

    Have a dictionnary with a UID assign to every GameObject.
    When you serialised a Game Object reference (or monobiavior), if it’s crossscene (or in all case), write the UID.

    At load time:
    load all dictionnary of all scene loaded. And then resolve reference to game object.
    If the scene is not loaded, the dictionnary for this UI will return null, but that’s of course a normal behave.
    And it works!

    Of course if you’re like us (client), you can’t do this on the egine level, and you find half broken solution. This is not you’re case.

    And if you ask yourself why Unity is not used by more AAA, it’s not because of a lack of fancy shader, it’s also because it’s not practical to work smoothly at 100 people on the same game with Unity.

  3. I will prefer Package manager over the asset store

  4. Speaking about GUID, why not add GUID Id display into the inspector debug mode? might be useful

    1. William Armstrong

      7月 24, 2018 6:28 pm

      You are not wrong, it would be useful. Unfortunately, the performance cost of storing the string version of the GUID is non-trivial. In my tests it was allocating quite a lot of memory every time it did the conversion. We don’t really have a great way of customizing the debug inspector either, so it would need to be stored at Editor time.
      While it might certainly be worth it, I wasn’t willing to sacrifice that much memory and performance in the Editor.

      1. Interesting, and yeah it make sense. It’s not worth it to sacrifice editor performance for this. Well at least for now we can use text editor to work with GUID. I hope in the future there’s a solution for this, having a built in tools to work with GUID directly are very helpfull when dealing with broken assets :D

  5. matthew radford

    7月 20, 2018 5:43 pm

    This is great. Fantastic effort! Glad you’ve got a blog post covering the issue. But this is a flaw of Unity. Can we not push to just never create duplicate GUIDs and allow referencing between scenes? I know its hard but If it’s a feature tons of developers are requesting (like me) I personally would love to see the problem solved elegantly and officially.

    1. Lenard Denisz Pop

      7月 24, 2018 10:26 pm

      Something really cool

  6. How about instantiating GameObjects into another scene? (Not the current or active one)

      1. The doc says you can only move root objects to another scene, so that idea of instantiating everything once in a separate scene should work.

  7. Would be nice to have a solution where we can cross-reference monobehaviours in unity events and select public methods in the property drawer of the events.

  8. Was there any particular reason not to use the built-in Object.GetInstanceID? You get pretty unique ids, regardless of the object being an asset, prefab or an instance.

    1. GetInstanceID is not persistent. restart the editor and it may be different

    2. I dont think they are persistent, next time you start up they will differ.

  9. @Nevermind Sounds very similar to our project. We use that pattern for the exact same reasons. And yes, asset duplication is the biggest headache to manage, because designers will often duplicate an existing asset as a starting point for a new one, for productivity reasons. Duplication detection would help, or alternatively if I could rebind Ctrl+D hotkey to custom logic I could account for new guid creation that way.

    1. Also, is there a potential for overlap here with the upcoming Addressables? That system also allows for assigning unique IDs to assets, no? I’m not sure about the scene management side of Addressables though.

      1. Will Armstrong

        7月 19, 2018 8:24 pm

        Not really. The Addressables system only works with assets on disk, which we already have Unique IDs for. This is for GameObjects in scene, a completely different domain.
        Addressables is going to abstract out how you currently work with Assets, so you won’t be worrying about if a given asset on disk is in an AssetBundle, just hard referenced, or coming from somewhere else.

      2. Addressables are for assets. this is for scene objects

        1. Right, but what I’m saying here is that the usefulness of the home brew GUID pattern goes beyond scene management, and this technique described in the blog won’t really replace it unless it also addresses assets. Both this approach and Addressables are ultimately using persistent unique IDs to refer to objects, and it would be nice to have one single consistent solution that addresses both sides of the problem.

  10. Anthony Rosenbaum

    7月 19, 2018 7:44 pm

    Definitely add to Package Manager, I had to do something similar by manipulating ID’s and the AssetDatabase

  11. We have a pretty similar system in our project. We do not use it for cross-scene references, but rather to save references to scene objects in saved games and in ScriptableObject assets.

    We do not use ISerializationCallbackReciever, but rather directly save the stringified version of the guid. It takes a bit more memory, but that does not really matter in practice, and the code is much simpler. In our experience, the system is not as robust as we wanted – it’s too easy to inadvertently change the guid on an object and break all the links to it. It’s not bad, though, we have probably around 100k guid-ed objects in our scenes and broken links are rare. Still, I really wish there was a better way to detect object duplication than “the guid is already in use so that’s probably a duplication or something.”

    I’ve also noticed one thing in your code that shouldn’t be there: some properties in GuidReference hidden behind #if UNITY_EDITOR. While this seems like a good idea, in practice Unity would serialize assets with UNITY_EDITOR defined even when building a standalone player – but the code for the player would not have these fields, and would throw errors trying to deserialize its own assets.

    1. Will Armstrong

      7月 19, 2018 7:19 pm

      Thanks for your feedback! Sounds like you have a way that works well for what you need.
      There are certainly lots of different ways to do this same thing. I found the .ToString() function on a GUID to be far too costly for my purposes compared to using the byte[].
      As of 2017.4 I am not seeing any issues in a built player finding fields it can’t DeSerialize. Everything seems to be working just fine.

    2. Just out of curiousity, since I’m currently building up essentially the same system: How do you end up checking for duplicates? All ideas I have amount to having a file containing all GUIDs that have been already ‘issued’ and checking against that. Another idea was to somehow try and use the timestamp of the file creation, but that likely won’t play nicely with version control.

      1. Will Armstrong

        7月 25, 2018 6:31 pm

        Chris –
        You only need to worry about Duplicating a GUID through copying, loading the same scene twice, serializing it into a prefab, or other workflow issue like that.
        Getting a system guid is ‘guaranteed’ unique.
        “The CoCreateGuid function calls the RPC function UuidCreate, which creates a GUID, a globally unique 128-bit integer. Use CoCreateGuid when you need an absolutely unique number that you will use as a persistent identifier in a distributed environment.To a very high degree of certainty, this function returns a unique value – no other invocation, on the same or any other system (networked or not), should return the same value.”
        from : https://docs.microsoft.com/en-us/windows/desktop/api/combaseapi/nf-combaseapi-cocreateguid

  12. It’s great that you’re adding this. But you probably ought to think about making your implementation threadsafe as the backing Dictionary isn’t threadsafe.

    1. Will Armstrong

      7月 19, 2018 7:22 pm

      I thought about that, but creating GameObjects / MonoBehaviours is also not threadsafe. If you are in ECS land, all this becomes a bit un-necessary as the ECS system has its own stable id system. If you are dealing with traditional GameObjects you need to be on the Main Thread any-who.

    2. neither is the rest of Unity. except the job system, but you can’t use Dictionary or byte[] in a job.

      could throw an exception if called from another thread though