Notifications
Article
最佳实践|基于GUID的引用解决方案
Updated 4 months ago
274
0
Unity Spotlight团队与优秀的开发人员一起合作,深度挖掘Unity在游戏开发中的潜力。针对复杂图形、性能和设计方面的问题,我们看到了各种具有创新性的优秀解决方案。
最佳实践系列文章探讨我们在与客户合作中遇到的一些常见的问题。这些都是宝贵的经验和教训,我们很自豪能够和大家分享。
我们此前已经分享过的最佳实践系列文章:
  • 《影子战术》层级优化经验分享
  • 碰撞性能优化
  • Unity项目设置
  • 对象放置和物理效果
  • 在Unity中创作逼真的视觉效果

可拓展性

Unity中已经加入了多场景编辑功能,越来越多团队使用多个场景来定义单个游戏玩法或功能。这让无法引用另一场景中的对象的限制成为了一个问题。目前有很多不同的方法来解决这个限制问题。许多团队大量使用物体创建器、预制件、程序化内容或是事件系统来减少直接引用对象的必要性。
我们发现最为常见的一个解决方法是:给游戏对象提供一个固定的全局唯一标识符,即GUID。一旦获得了唯一标识符,便可以引用游戏对象的已知实例,无论它存在于何处,即使它没有被加载,也可以将游戏数据保存到实际的运行时游戏结构中。
由于我们的许多客户以同样的方法解决了同样的问题,我们决定进行参考实践。在讨论了各种选项后,我们决定像用户一样只使用公共API和C#来解决这个问题。除了这样做是一个良好的测试用例外,我们还能直接分享代码。无需等待构建,只要访问GitHub下载代码,然后开始使用它并根据需要进行修改即可。
访问GitHub下载解决方案代码:
https://github.com/Unity-Technologies/guid-based-reference
该解决方案的基本结构很简单,GUID对每个对象都有一个全局静态字典。当你想要一个对象并拥有其GUID,可以在字典中查找。如果它的GUID存在,你会得到GUID;否则你会得到Null。我们希望让解决方案保持尽可能简单,因为它需要放入各种客户端项目中。你可以在GitHub上,了解我们如何在GUIDManager.cs中设置静态管理器,而不需要任何游戏对象开销。

持续性

我们必需克服的首要难题是,将System.GUID中的GUID转换为特定格式,使Unity理解如何将它作为MonoBehaviour的一部分进行序列化。
我们加入了ISerializationCallbackReciever,所以过程会很简单。我们执行了一些快速性能测试,发现.ToByteArray()的速度是.ToString()的二倍,并且没有分配无关的内存。由于Unity能处理好byte[],这似乎对数据储器来说是一个不错的选择。
System.Guid guid = System.Guid.Empty; [SerializeField] private byte[] serializedGuid; public void OnBeforeSerialize() { if (guid != System.Guid.Empty) { serializedGuid = guid.ToByteArray(); } } // 在加载时,可以继续恢复我们的系统Guid,以供使用 public void OnAfterDeserialize() { if (serializedGuid != null && serializedGuid.Length == 16) { guid = new System.Guid(serializedGuid); } }
虽然GUID都很好地保存了,但是保存得似乎有点太好。预制件和组件的拷贝都会造成GUID冲突。虽然我们检测并修复了这个问题,但我们宁可简单,也不可创建重复的GUID。不过PrefabUtility提供了几个方法,来检测我们正在处理的游戏对象,并给出适当的反馈。
#if UNITY_EDITOR // 检测这是预制件实例,还是预制件资源 // 由于预制件资源在实例化后会被复制,所以它无法包含GUID PrefabType prefabType = PrefabUtility.GetPrefabType(this); if (prefabType == PrefabType.Prefab || prefabType == PrefabType.ModelPrefab) { serializedGuid = new byte[0]; guid = System.Guid.Empty; } else #endif
进一步的测试表明,这会存在一个奇怪的边缘情况,由受损的预制件创建的实例有时不会保存新GUID的实例数据。所以我们又使用了PrefabUtility来解决问题。
在CreateGuid函数内部的代码如下:
#if UNITY_EDITOR // 如果我们为预制件的预制件实例新建了GUID,但不知为何丢失了预制件连接 // 强制保存已修改的预制件实例属性 PrefabType prefabType = PrefabUtility.GetPrefabType(this); if (prefabType == PrefabType.PrefabInstance) { PrefabUtility.RecordPrefabInstancePropertyModifications(this); } #endif

工具

一旦所有内容都得到妥善保存后,我们需要弄清如何让用户将数据导入到这个系统中。制作新的GUID很简单,但是引用这些GUID却有些困难。现在为了设置跨场景的引用,完全可以要求用户载入二个场景。
初始设置很简单:只需要一个标准对象选择字段来查找我们的GuidComponent。但是我们不想保存此引用,因为它会跨场景使用,使Unity报错并在随后将它设为Null。我们需要按照常见对象字段的方法绘制一个GuidReference,但是保存的是GUID而不是引用。
为特定类型设置自定义绘图工具非常简单, [PropertyDrawer]标签就有这样的作用。通过它可以轻松建立一个选取器,抓取结果选择,然后序列化我们要的GUID数据。然而,当目标游戏对象在未加载的场景中又要怎么办呢?我们依旧想要让用户知道他们拥有一个数值集,并尽可能多地提供有关该对象的信息。
我们最后的解决方案如下:
解决方案由三部分组成:用在GuidComponent之前集合的一个伪造的禁用对象选择器,一个包含目标所在场景的禁用字段,还有一个让你清除当前选中目标的按钮。
这个方案是必要的,因为我们无法检测用户在对象选取器中选择None和由于目标对象未载入而产生的Null值这二者之间的区别。优点是场景引用是个真正的资源字段,如果你想要获得真正的目标对象,它会高亮需要加载的场景,如下图所示:
该解决方案的代码可以在GitHub上的GuidReferenceDrawer.cs中找到。这部分代码使用了一个小技巧,使得字段无法被编辑,但仍可以作为真正的字段使用。
bool cachedGUIState = GUI.enabled; GUI.enabled = false; EditorGUI.ObjectField(position, sceneLabel, sceneProp.objectReferenceValue, typeof(SceneAsset), false); GUI.enabled = cachedGUIState;

测试

当所有内容都正常工作后,我们需要为测试人员和用户,提供确保所有部分都正常工作的能力。我们此前编写了几个测试在Unity内部使用,但从未给用户代码使用过。但Unity内置Test Runner非常实用!
在菜单中,点击Window->Test Runner,这会打开一个UI,让你在运行模式和编辑模式中为代码创建测试。使用Test Runner,编写测试的过程将非常简单。
下面是一个完整的测试代码,用来确保复制GUID时会给你提供信息。
[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; }
这部份代码告诉测试工具,如果它没得到预期警告的话,测试工具会失效,然后创建一个已有的GuidComponent,从而确保我们不会遇到GUID冲突,然后终止程序。
我们可以依靠已有的guidBase函数来实现,因为我们在一个设置好的函数中使用[OneTimeSetUp]属性来创建它,我们希望在启动此文件中的任意测试前调用一次该函数。编写这些测试的过程非常简单,代码只需经过编写测试就可以很好地实现出来。所以我们强烈建议你使用它来测试团队中所使用的任意工具。

未来计划

在这个引用解决方案经受住开发者的各种考验后,我们将计划将其移出GitHub。并将它转移到Asset Store资源商店以供免费下载,或是放入资源包管理器中方便所有人获取。
请提供反馈告诉我们还有哪些改进之处或是遇到了什么问题。也请继续关注最佳实践系列文章,从Unity Spotlight团队得到更多关于充分使用Unity的建议。
更多Unity技术经验分享尽在Unity官方中文论坛(UnityChina.cn)!

Unity China
356
Comments