微信号:Unity-GreaterChina

介绍:Unity官方开发者平台,分享最前沿的技术文章和开发经验,精彩的Unity活动和社区相关信息.

最佳实践|基于GUID的引用解决方案

2018-07-31 12:18 Unity

Unity Spotlight团队与优秀的开发人员一起合作,深度挖掘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)!

 

推荐阅读

官方活动

ChinaJoy | 精彩Made with Unity体验,让你“酷玩一夏”

Unity将于8月3-5日在W4馆的M301号展位欢迎大家的到来。今年我们邀请到了十多家出色的开发团队,带来了横跨VR/AR,主机,移动端和PC等多个平台的酷炫前卫的Made with Unity作品,期待大家届时光临Unity展位和我们一起引爆夏日游戏狂欢![了解详情


实时优化,智造收益 - Unity Monetization开发者生态沙龙

8月3日,Unity将于举办为期半天的Unity Monetization开发者生态沙龙!本次活动将与大家共同探讨移动游戏开发与变现相关难点与痛点。[了解详情]

报名地址:

https://connect.unity.com/events/unityads


7月Asset Store资源商店促销 

7月,只需在Asset Store资源商店消费满30-199美元,最高可获得价值235美元的5款免费资源,参与活动,获得夏日精选资源包。

活动地址:

https://assetstore.unity.com/g/july-promo-activation-cn



点击“阅读原文”Unity官方中文论坛

 
Unity官方平台 更多文章 360场景如何和3D物体互动 ChinaJoy | Unity展位现场互动指南 制作蝴蝶烟火视觉特效 Unity ML-Agents模仿学习示例解析 Unity和Google合作推动移动游戏广告发展
猜您喜欢 揭秘MIUI视频从0到千万用户的迭代 我们是谁?我们是程序猿! greenplum导入数据的几种方法 【精彩回复】| 英文LOGO就是要比中文高大上? 【安卓党】这些强大的微信隐藏功能,超过一半人都不知道。