Search Unity

On the Spotlight Team, we work with the most ambitious Unity developers to try to push the boundary of what a Unity game can be. We see all sorts of innovative and brilliant solutions for complex graphics, performance, and design problems. We also see the same set of issues and solutions coming up again and again.

This blog series looks at some of the most frequent problems we encounter while working with our clients. These are lessons hard won by the teams we have worked with, and we are proud to be able to share their wisdom with all our users.

Many of these problems only become obvious once you are working on a console or a phone, or are dealing with huge amounts of game content. If you take these lessons into consideration earlier in the development cycle, you can make your life easier and your game much more ambitious.

Scalability

With Multi-Scene editing now built into Unity, more and more teams find themselves using multiple Scenes to define a single unit of gameplay or functionality. This makes the long-standing Unity limitation of not being able to reference an Object in another Scene more of a burden.

There are lots of different ways to get around this limitation currently. Many teams make heavy use of spawners, Prefabs, procedural content, or event systems to reduce the necessity to directly reference objects. One of the most common workarounds we see, over and over again, is to give GameObjects a persistent, globally unique identifier or GUID. Once you have a unique identifier, you can then reference a known Instance of a GameObject no matter where it lives, even if it isn’t loaded, or map save game data to your actual runtime game structures.

Since so many of our clients were solving the same problem the same way, we decided to make a reference implementation. After discussing various options, we decided to approach this problem like a user, using only public APIs and C#. In addition to being a good internal test case, this lets us share the code with you directly. No waiting for builds: Just go to GitHub, download the code, and start using it and changing it to fit your needs.

You can find the solution to look through yourself here.

The basic structure of this solution is very simple. There is a global static dictionary of every object by GUID. When you want an object and have a GUID, you look it up in the dictionary. If it exists, you get it; otherwise, you get null. I wanted to keep things as simple as possible since this solution needed to be able to be dropped into all sorts of client projects. You can see how I set up a static manager without the need for any GameObject overhead in GUIDManager.cs at the link above.

Persistence

The first challenge we had to overcome was getting a GUID from System.GUID into a format that Unity understood how to serialize as part of a MonoBehaviour. Thankfully, since the last time I tried to do this myself, we added the ISerializationCallbackReciever, so this was pretty easy. I did some quick performance tests and found that .ToByteArray() was twice as fast and allocated no extraneous memory when compared to .ToString(). Since Unity handles byte[] just fine, that seemed like the clear choice for backing storage.

Sadly, now that I have GUIDs that are saving nicely, they are saving TOO nicely. Prefabs and component duplication both cause GUID collision. While I can and do detect and repair this case, I would much rather simply never create a duplicate GUID in the first place. Thankfully, the PrefabUtility provides a few ways to detect what sort of GameObject I am dealing with and react appropriately.

Further testing showed that there is an odd edge case where a prefab instance with a broken backing prefab will sometimes not save out the instance data of the new GUID. PrefabUtility to the rescue again. From inside the CreateGuid function:

Tools

Once everything was getting saved appropriately, I needed to figure out how to let users get data into this system. Making new GUIDs was easy, but referencing them proved a bit more difficult. Now, to set up a reference across scenes, it is totally reasonable to require a user to load up both scenes. So the initial setup was simple: just have a standard Object selection field looking for our GuidComponent. However, I didn’t want to save that reference, since it would be cross scene, Unity would complain, and then later null it out. I needed to draw a GuidReference as though it were a regular Object field, but save out the GUID and not the reference.

Setting up a custom drawer for a specific type is pretty trivial, thankfully, and exactly what the [PropertyDrawer] tag is for. Easy enough to put up a picker, grab the resultant choice, and then serialize out the GUID data we actually care about. However, what do we want to do when the target GameObject is in a scene that isn’t loaded? I still want to let the user know that they have a value set, and provide as much information about that Object as I can.

The solution we ended up with looks like:

A reference to an Object stored in a different Scene.

A fake, disabled Object selector for the GuidComponent previously set, a disabled field containing the Scene that the target is in, and an ugly button that lets you Clear the currently selected target. This was necessary because I cannot detect the difference between a user selecting None in the Object picker and the value being null because the target Object isn’t loaded. The really nice thing about this, though, is the Scene reference is a real Asset field, and will highlight the Scene you need to load if you want to get at the actual target Object, like so:

Asset ping highlighting the Scene your target is in.

All of the code for this can be found in GuidReferenceDrawer.cs here on GitHub. The trick for making a field unable to be edited, but still act like a real field is here:

Tests

Once I got everything functioning properly, I needed to provide testers and users with the ability to make sure things would keep working. I have written tests for internal Unity purposes before, but never for user code. I admit I was shamefully ignorant of our built-in Test Runner. It is shockingly useful!

What I want to see.

Window->Test Runner will bring up a nice little UI that will let you create tests for your code in both Play Mode and Edit Mode. Making tests turned out to be quite easy.

For example, here is the entirety of a test to make sure that duplicating a GUID gives you a nice message about it.

This code tells the test harness that it should fail if it doesn’t get the expected warning, clones a GuidComponent I have already created, makes sure we don’t end up with a GUID collision, and then ends.

I can depend on guidBase existing because I create it in a set up function using the [OneTimeSetUp] attribute on a function I want to be called once before starting any of the tests in this file. You can find more details in the documentation here. I was really impressed with how easy it was to write these tests, and how much better my code got just by going through the thought process of making tests for it. I highly recommend you test any tools your team depends on.

Moving Forward

After this reference solution has withstood some battle testing from you, our dear developers, I am planning on moving it out of GitHub and onto the Asset Store as a free download or into the PackageManager to let everyone get at it. In the meantime, please provide feedback on anything we can improve or any issues you run into. Keep an eye on this space for more advice from the Spotlight team on how to get the most out of Unity.

Dejar una respuesta

Puede utilizar estas etiquetas y atributos HTML: <a href=""> <b> <code> <pre>

  1. 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.

  2. I will prefer Package manager over the asset store

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

    1. 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

  4. 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. Something really cool

  5. 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.

  6. 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.

  7. 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.

  8. @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. 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.

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

  10. 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. 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. 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

  11. 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. 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