Search Unity

Spotlight Team best practices: GUID-based references

July 19, 2018 in Engine & platform | 8 min. read
Topics covered
Share

Is this article helpful for you?

Thank you for your feedback!

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.

System.Guid guid = System.Guid.Empty;
[SerializeField]
private byte[] serializedGuid;

public void OnBeforeSerialize()
{
   if (guid != System.Guid.Empty)
   {
       serializedGuid = guid.ToByteArray();
   }
}

// On load, we can go ahead and restore our system guid for later use
public void OnAfterDeserialize()
{
   if (serializedGuid != null && serializedGuid.Length == 16)
   {
       guid = new System.Guid(serializedGuid);
   }
}

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.

#if UNITY_EDITOR
// This lets us detect if we are a prefab instance or a prefab asset.
// A prefab asset cannot contain a GUID since it would then be duplicated when instanced.
PrefabType prefabType = PrefabUtility.GetPrefabType(this);
if (prefabType == PrefabType.Prefab || prefabType == PrefabType.ModelPrefab)
{
    serializedGuid = new byte[0];
    guid = System.Guid.Empty;
}
else
#endif

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:

#if UNITY_EDITOR
// If we are creating a new GUID for a prefab instance of a prefab, but we have somehow lost our prefab connection
// force a save of the modified prefab instance properties
PrefabType prefabType = PrefabUtility.GetPrefabType(this);
if (prefabType == PrefabType.PrefabInstance)
{
   PrefabUtility.RecordPrefabInstancePropertyModifications(this);
}
#endif

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:

bool cachedGUIState = GUI.enabled;
GUI.enabled = false;
EditorGUI.ObjectField(position, sceneLabel, sceneProp.objectReferenceValue, typeof(SceneAsset), false);
GUI.enabled = cachedGUIState;

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.

[UnityTest]
public IEnumerator GuidDuplication()
{
   LogAssert.Expect(LogType.Warning, "Guid Collision Detected while creating GuidTestGO(Clone).\nAssigning new Guid.");
    
   GuidComponent clone = GameObject.Instantiate<GuidComponent>(guidBase);

   Assert.AreNotEqual(guidBase.GetGuid(), clone.GetGuid());

   yield return null;
}

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.

July 19, 2018 in Engine & platform | 8 min. read

Is this article helpful for you?

Thank you for your feedback!

Topics covered
Related Posts