spotlight_Spotlight小组最佳做法:基于GUID的参考

spotlight_Spotlight小组最佳做法:基于GUID的参考

spotlight

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.

在Spotlight小组中,我们与雄心勃勃的Unity开发人员一起努力扩大Unity游戏的界限。 对于复杂的图形,性能和设计问题,我们看到了各种各样的创新和出色的解决方案。 我们还会看到同样的问题和解决方案不断出现。

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.

现在Unity中内置了多场景编辑功能,越来越多的团队发现自己使用多个场景来定义游戏或功能的单个单元。 这使Unity长期存在的局限性成为无法负担另一个场景中的对象的负担。

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.

当前有很多不同的方法来解决此限制。 许多团队大量使用了生成器,预制件,程序内容或事件系统,以减少直接引用对象的必要性。 我们一遍又一遍看到的最常见的解决方法之一是为GameObjects提供持久的全局唯一标识符或GUID。 一旦有了唯一的标识符,就可以引用GameObject的已知实例,无论它位于何处(即使未加载),也可以将保存的游戏数据映射到实际的运行时游戏结构。

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.

由于我们的许多客户都以相同的方式解决相同的问题,因此我们决定进行参考实施。 在讨论了各种选项之后,我们决定像用户一样使用仅使用公共API和C#的方法来解决此问题。 除了成为一个好的内部测试用例之外,这还使我们可以直接与您共享代码。 无需等待构建:只需访问GitHub,下载代码,然后开始使用它,并对其进行更改以适应您的需求。

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.

该解决方案的基本结构非常简单。 GUID每个对象都有一个全局静态字典。 当您想要一个对象并拥有一个GUID时,您可以在字典中查找它。 如果存在,就可以得到; 否则,您将获得null。 我想使事情尽可能简单,因为该解决方案必须能够放入各种客户项目中。 您可以在上面的链接中看到如何在GUIDManager.cs中设置静态管理器而不需要任何GameObject开销。

坚持不懈 (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转换为Unity理解如何作为MonoBehaviour的一部分进行序列化的格式。 值得庆幸的是,自从我上次尝试执行此操作以来,我们添加了ISerializationCallbackReciever,因此这非常容易。 我进行了一些快速的性能测试,发现.ToByteArray()的速度是后者的两倍,并且与.ToString()相比,没有分配额外的内存。 由于Unity可以很好地处理byte [],因此这似乎是备份存储的明确选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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.

遗憾的是,现在我的GUID保存得很好,它们也保存得很好。 预制件和组件重复都会引起GUID冲突。 尽管我可以并且确实能够检测并修复这种情况,但我宁愿从不首先创建重复的GUID。 幸运的是,PrefabUtility提供了几种方法来检测我正在处理的GameObject并做出适当的React。

1
2
3
4
5
6
7
8
9
10
11
#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
1
2
3
4
5
6
7
8
9
10
11
#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:

进一步的测试表明,在某些情况下,具有损坏的预制件的预制实例有时将无法保存新GUID的实例数据。 预制性再次得以营救。 从CreateGuid函数内部:

1
2
3
4
5
6
7
8
9
#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
1
2
3
4
5
6
7
8
9
#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.

妥善保存所有内容后,我需要弄清楚如何让用户将数据获取到该系统中。 制作新的GUID很容易,但事实证明,引用它们要困难得多。 现在,要跨场景设置参考,要求用户加载两个场景是完全合理的。 因此,初始设置非常简单:只需在标准对象选择字段中查找我们的GuidComponent。 但是,我不想保存该引用,因为它将是跨场景的,Unity会抱怨,然后将其清空。 我需要绘制一个GuidReference,就像它是一个普通的Object字段一样,但是要保存GUID而不是引用。

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.

庆幸的是,为特定类型设置自定义抽屉非常简单,而[PropertyDrawer]标记正是针对此目的。 足够容易地设置一个选择器,抓住结果选择,然后序列化我们真正关心的GUID数据。 但是,当目标GameObject处于未加载的场景中时,我们要怎么做? 我仍然想让用户知道他们有一个值集,并尽可能提供有关该对象的信息。

The solution we ended up with looks like:

我们最终得到的解决方案如下所示:

spotlight_Spotlight小组最佳做法:基于GUID的参考

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:

先前为GuidComponent设置的伪造的,禁用的Object选择器,一个禁用的字段,其中包含目标所在的场景,以及一个丑陋的按钮,可让您清除当前选定的目标。 这是必要的,因为我无法检测到用户在“对象选择器”中选择“无”和由于未加载目标对象而该值为null的区别。 不过,真正有趣的是Scene引用是一个真实的Asset字段,如果要获取实际的目标Object,它将突出显示您需要加载的Scene,如下所示:

spotlight_Spotlight小组最佳做法:基于GUID的参考

Asset ping highlighting the Scene your target is in.

资产ping突出显示目标所在的场景。

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:

可以在GitHub上的 GuidReferenceDrawer.cs中找到所有用于此目的的代码。 使字段无法编辑但仍然像真实字段一样起作用的技巧如下:

1
2
3
4
bool cachedGUIState = GUI.enabled;
GUI.enabled = false;
EditorGUI.ObjectField(position, sceneLabel, sceneProp.objectReferenceValue, typeof(SceneAsset), false);
GUI.enabled = cachedGUIState;
1
2
3
4
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!

一旦一切正常运行,我就需要向测试人员和用户提供确保一切正常的能力。 我以前为内部Unity目的编写过测试,但从未为用户代码编写过测试。 我承认我对我们内置的Test Runner感到可耻。 它非常有用!

spotlight_Spotlight小组最佳做法:基于GUID的参考

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.

Window-> Test Runner将显示一个漂亮的小UI,可让您在播放模式和编辑模式下为代码创建测试。 进行测试非常容易。

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

例如,这里是一个完整的测试,以确保重复GUID可以为您提供有关此信息的不错信息。

1
2
3
4
5
6
7
8
9
10
11
[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;
}
1
2
3
4
5
6
7
8
9
10
11
[ 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.

这段代码告诉测试工具,如果没有得到预期的警告,它将失败,克隆我已经创建的GuidComponent,确保我们不会以GUID冲突结束,然后结束。

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.

我可以依赖现有的guidBase,因为我是在设置函数中使用要在此文件中进行任何测试之前调用一次的函数上的[OneTimeSetUp]属性创建的。 您可以在此处的文档中找到更多详细信息。 编写这些测试是如此容易,而且通过经历为它进行测试的思考过程,我的代码给我留下了非常深刻的印象。 我强烈建议您测试团队所依赖的任何工具。

spotlight_Spotlight小组最佳做法:基于GUID的参考
()

向前进 (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.

在这个参考解决方案经受住了您(我们亲爱的开发人员)的艰苦测试之后,我正计划将其从GitHub移出,免费下载到Asset Store或移入PackageManager,以使所有人都能使用。 同时,请提供有关我们可以改进的任何问题或您遇到的任何问题的反馈。 请密切关注此空间,以获取Spotlight团队有关如何充分利用Unity的更多建议。

翻译自: https://blogs.unity3d.com/2018/07/19/spotlight-team-best-practices-guid-based-references/

spotlight