By gongqing 2021/11/12

以下是我使用Unity开发VR博物馆项目中的一个经验总结,可能网上其他unity大佬早就提过,或者大家都心知肚明,或者其实有更好的方案,只是我因为太菜所以一直没想到,总之我在开发完整个项目才自己悟到,本可以减少至少一半的工作量,所以记录一下。

工程特点:大量的“你死我活”判断

这个VR博物馆的开发目的是进行VR环境中文物展示的灯光与用户体验关系的实验,本质上不是一个真正的VR展示项目,是一个人机实验用的系统。这就导致这个系统有以下特点:
(1)重复的对象很多:人机实验为了寻找规律需要设置多个实验组,在这个项目里有6个不同的文物,但每个文物的摆放环境以及灯光控制系统都是一样的。
(2)独立的对象很多:在这个人机实验中有“照度”、“色温”、“主光经度”、“主光纬度”、“辅光经度”、“辅光纬度”、“手电筒开关”至少7个自变量,在每个文物周围有三个聚光灯,其中“背光”、“主光”和“辅光”,这三个灯除了“照度”和“色温”是联动的,在经纬度上均是分开调整,且6个文物的灯光调整都是互相独立的,另外还有一个手电筒的功能,当手电筒打开时,三个灯均需要关闭,手电筒关闭时,三个灯又需要打开。每个文物灯光的控制面板也都是独立的,当靠近一个文物时对应的面板才能被打开,每个面板上都有6个控制灯光的按钮。
(3)面向普通用户,强调用户体验:这个系统预期要寻找若干名被试来进行实验,但是被试不一定是熟悉VR系统、熟悉HTC VIVE手柄的人,所以在这个系统中需要设置不少UI用作提示或操作引导(就是要把用户当傻子),另外还需要在正式实验开始前加一段新手引导,强制被试进行某些任务,以帮助他们熟悉操作。由于时间限制,我在设计新手引导部分时用了大量的文字来描述,这必然不能达到最佳的用户体验,但当时我认为这样能减少不少逻辑判断,缩短开发时间。举例来说就是需要加不少这样的程序:点击按钮,隐藏一个画布,同时模型旋转一个角度,同时出现另一个模型;判断主体位置是否在某个范围内,如果是,就隐藏一个模型,同时另一个模型旋转一个角度再移动到另一个位置,同时出现一个新画布;判断主体是否在某个空间范围内,如果不是,当用户试图按一个按钮时就出现提示告诉他这样不行;当用户已经打开了一个面板时,他如果没有关闭这个面板就尝试打开另一个面板,就要帮他先把原来的面板隐藏了……

其他详细的功能见这个文档(非团队成员不可读):
VR博物馆开发笔记 · IABC数字创意组

总结一下,这样的工程最繁琐的地方就是要加大量的“有我没他,有他没我”的判断,我暂时把它简称为“你死我活”判断

我之前的方案

刚开始开发的时候因为还没有想到最后会有这么多按钮以及这么多看似不一样实际一样的逻辑判断,所以我把判断全部挂在按钮上,直接在unity的面板里给按钮onclick事件挂一堆对象:点击按钮,设置一个对象隐藏,且设置另一个对象显示…后来工程越做越大,在最终的工程里,这样的判断可能有几百个,最多的按钮可能挂了快20个对象。
image.png

显然这样很麻烦,而且修改起来更麻烦,加上我的命名习惯不好,比如当我副本了一个对象,我可能只会改一下父物体的名字,子物体名字都不改(也有可能父物体都不改),最后一跑工程,发现有逻辑问题,然后想去校对控制的对象有没有问题时,发现我在一个按钮上挂了5、6个名字一样的对象(比如上图就有两个名字一样的torch,但实际上是两个不一样的对象),根本看不出哪个是哪个的,要一个个点一下,让unity在项目树里给我高亮出来看看对不对……由于上一节提到的那些工程特点,这样的事在我的开发过程中几乎每时每刻都在发生,我现在回过头去看发现这极大地降低了效率。好在unity这点做的还算比较好,如果按钮和要控制的对象同在一个父物体里,即使不改名字,副本之后也不会出现问题,所以如果出问题一般都是我自己手滑选错了之类的,不然会出更多问题。但如果未来经手了比这个更大的工程,或者涉及多人协作,这绝对不是一个好办法。

我如今才想到的方案

设置“flag对象”。和编程时定义一个flag变量的思路是一样的,相当于用这个flag来保存一种状态。在unity里可以设置多个flag对象(空对象),把它们都放在一个父物体下,规范命名

当要做“你死我活”判断时在onclick的框里不添加要死或要活的那个对象本身,而是加这个flag对象(这个用脚本控制也行)。然后再挂一个脚本,挂哪都行,但最好都挂在一起,要控制的实际对象全定义为public变量,在面板里把对象拖进去,而不是用Find等类似的方法查找对象,这样对命名习惯不好的开发者来说有更大的容错率,后期维护工程时也不用读代码,更直观。
脚本伪代码:

  1. public GameObject 对象A, 对象B, flag对象;
  2. //用Update()保持程序一直运行,检测flag对象的状态
  3. void Update(){
  4. if (flag对象.activeSelf)
  5. {
  6. 对象A.SetActive(true);
  7. 对象B.SetActive(false);
  8. }
  9. else
  10. {
  11. 对象A.SetActive(false);
  12. 对象B.SetActive(true);
  13. }
  14. }
  15. //写成方法也可以,用按钮的onclick事件调用或在其他脚本里根据需求调用
  16. public void youDieIsurvive(){
  17. if (flag对象.activeSelf)
  18. {
  19. 对象A.SetActive(true);
  20. 对象B.SetActive(false);
  21. }
  22. else
  23. {
  24. 对象A.SetActive(false);
  25. 对象B.SetActive(true);
  26. }
  27. }

这样即使所有对象都是互相独立的,也能减少一半的工作量,更别提实际项目中并不是所有对象都是完全独立的,有很多联动,因为对象很多,但状态只有1和0两种,这样工作量实际上就能减少更多。然而Update()理论上是一直在运行的,可能会占资源,这点我不是很有概念,也许是有一定弊端的。但如果写成方法应该能解决这个问题。

  1. 一个可能的槽点:为什么不设置一个flag公共变量而是要设置一个空对象呢?<br />我认为应该设置flag对象而不是在脚本里定义一个flag公共变量是因为这样在Unity工程里可以更方便地打开或关闭这个对象(控制其父对象的开关),以打开或关闭这个你死我活判断用于调试,不需要去脚本里注释来注释去,毕竟重新编译又要花几秒时间。这只是我出于整个开发工作流的考虑,可能也会对性能有影响,具体要实践过才知道。

总结

工程如果很简单,没啥按钮的话,还是直接在按钮的onclick上挂对象来的快、直观,但如果是符合上述工程特点的项目(不一定是VR项目),都可以考虑使用flag对象的方案,这样几百个“你死我活”判断做完之后头发也许还能活着。