Unity教程之-谈谈 UI 的那些事

 

UI, 差不多是玩家打开一个游戏最先看到的东西, 差不多也是玩家最不在意的东西. 对开发者来说, 几乎每个游戏模块都与 UI 有联系, 处理不当 UI 就是恶梦. 宽高比适应, 分辨率适应, 像素对齐…光是这些就足够没有经验的开发者浪费大量的时间了. 好在 Unity 这样的引擎提供了已经很强大的 UI 解决方案, 以及许多其他开发者提供的插件. 但是这还不够. 这篇文章主要谈一谈 UI 的逻辑设计而不是排版设计上的经验, 同时也在说明我的插件可以用来解决什么问题.

使用事件

屏幕角落里有一个 “血条”, 代表玩家的生命值. 决战的时候到了, 玩家抽出他的刚剑与怪物厮杀. 几乎没有人可以毫发无伤的打败怪物. 那么问题来了, 应该在什么时候修改 “血条” 的填充率?

一个偷懒的做法是每帧去读取玩家的生命值, 然后应用到 “血条” 的填充率上. 好吧这的确可以, 暂时没什么问题 (其实还有一种人神共粪的办法是依据 “谁开发谁保护, 谁污染谁治理” 原则…). 然而策划说, 当玩家被攻击时,  “血条” 应当播放一个粒子动画效果, 因为帅. 如果你还是不打算改进方法, 那就缓存玩家的生命值然后每帧对比吧! 但是接下来策划又说, 因中毒损失生命值时播放的粒子效果是紫色的, 因被攻击损失生命值时播放的粒子效果是黄色的. 啊哦, 你放弃了, 你不想改了, 都写那么多了不是吗? 你严肃的对策划说, 这个做不了, 要大改, 游戏做不完的话, 这锅……

观察者模式闪亮登场! 如果你与世无争对<四十二章经>这种神功秘笈没有兴趣的话, 那么应当很了解 C# 的 delegate 和 event 了 (不了解的点这里). 如果玩家每次生命值变化都大叫一声 “我因为什么原因而损伤/增加多少生命值”, AI 模块可以听见, UI 模块可以听见, Story 模块可以听见, 整个世界都可以听见……我的意思是, UI 在初始化时将 OnPlayerHpChange 方法注册到玩家的 onHpChange 事件里, 当事件触发时根据具体的参数来修改 “血条” 填充率和播放相应的粒子效果, 多么简洁优雅!

可是不要忘了在恰当的时候取消注册事件. 这大概是最常发生的错误了. HUD 是作为一个非游戏核心模块存在的, 一般仅用来展示一些玩家需要关心的信息. 无论如何游戏不能因为 HUD 模块的错误而停止运行. 然而忘记取消注册事件可能导致这种情况发生. 比如某些 HUD 对象已经销毁后, 玩家生命值发生变化时触发这些 HUD 对象曾经注册的事件就会造成空引用异常. 另外一种不取消注册事件造成的错误是, 当游戏关卡重新开始时, HUD 因重新初始化而重复注册事件, 相同的方法在事件触发后执行多次.

 

使用动画

制作精美的游戏, 几乎都少不了带动画的 UI. 生硬的直接弹出窗口的方式已经是过去式了. 好吧, 那么加动画就是了, 有什么可以说的呢?

比如说活动窗口切换问题. 因为动画的存在, 活动窗口的变化不再是 “瞬间完成” 的而是 “经历一段时间”. 时间就是问题. 现在我们在游戏主菜单界面, 点了一下 Options 按钮. 主菜单界面开始滑出屏幕左边界, Options 界面从屏幕右边界滑入. 在动画过程中, 如果玩家手速惊人, 又点了一下主菜单界面的开始游戏按钮会发生什么事呢? 或者只是快速的连续点击了 Options 按钮呢? 我们可以认为没有人想这么做, 除非他是误操作的. 好吧既然是误操作那么就忽略吧, 离开一个界面的动画开始时就立即关闭这个界面的 “可交互性”. 那么什么时候打开新界面的 “可交互性” 呢? 其实在动画开始到结束的任何时候都可以, 但是从避免误操作的角度出发还是应当在动画结束后才打开 “可交互性”.

然后是动画控制问题. 现在我们有两个界面同时发生移动, 可能还有颜色, 不透明度, 缩放等一系列视觉属性的变化, 甚至还有 3D 相机的位置和朝向的变化. 我们希望所有的动画同时开始, 同时结束, 并且大多数动画具备相同的变化过程, 比如先加速后减速. 既然如此, 为何不设计一种动画控制器来负责这一切呢? 可以调整变化曲线, 可以设置持续时间, 可以关联多个动画元素, 可以触发结束事件……当动画开始的时候, 我们只需要打开这一个控制器, 这个控制器负责更新所有关联的动画, 最后在动画结束时打开新界面的 “可交互性”.

最后是开发成本问题. 为了兼容各种不同的设备, 开发中 UI 本身就十分复杂易变, 再加上 UI 动画的存在让制作和修改成本再上一层楼. 在动画这一方面, 如果可以避免技术人员重复劳动, 修改代码, 甚至不需要技术人员的参与就可以实现, 无疑可以省去大量的成本. 在使用 Unity 这样的完善的开发工具时, 在编辑器中就可以方便的编辑和预览动画将可以大大减少技术人员的开发时间.

 

使用栈状态机

你可能知道栈, 也可能知道状态机, 但栈状态机是什么鬼?

你的主菜单界面有 5 个按钮, 每个按钮点击后都打开一个新的界面, 同时让主菜单界面消失. Easy! 聪明的你写了 “隐藏主菜单” 和 5 个 “显示xx界面” 方法在每个按钮事件里调用. 这没什么嘛. 然后打开的新界面都有返回主菜单的按钮, 你同样写了 5 个 “隐藏xx界面” 和 “显示主菜单” 的方法, 在返回按钮事件里调用它们. 你开始有点烦了. 更麻烦的是每个打开的界面还可以打开下一级界面, 每个按钮都要绑定一个 “隐藏当前界面” “显示下一个界面” 的方法, 最后你会想, 为什么我总是重复做一件相同的事情?

因为你没有使用状态机. 主菜单界面的 5 个按钮, 虽然点击后目标界面不同, 但都需要先离开主菜单. 如果每个界面是一个 “状态”, 当进入状态时显示这个界面, 离开状态时隐藏这个界面, 那么只要状态发生变化, 离开和进入的状态都会做好自己的事情, 这样每个按钮的唯一任务就是切换状态了.

我们再进一步思考这个问题. 既然同一时间只有一个活动界面, 并且只能从这个界面进入下一层界面或返回上一层界面, 这不正是 “栈” 的概念吗? 如果所有的状态都存储在栈里, 那么 “进入下一层界面” 就是栈的 Push 操作, “返回上一层界面” 就是栈的 Pop 操作. 哇哦, 听起来很不错的样子……但仔细想想, 与使用普通的状态机相比, 栈状态机 Push 操作与普通状态机 SetState 一样需要一个新状态参数, 只是 Pop 操作比普通状态机 SetState 省略了一个参数而已. 似乎并没有什么根本的区别嘛! 这里我们再扩展 “状态” 为 “栈状态”, 在 OnEnter 和 OnExit 基础上添加 OnPush 和 OnPop, 就可以做更多的事情了! 想一想, 一个新界面入栈的时候, 旧界面不必完全消失, 可以只是不可交互了而已. 这样就可以实现更丰富的 UI 效果了!

现在你已经掌握了开发 UI 的秘诀了, 而且也不必重新造轮子,  White Cat’s Toolbox插件可以省去你半年的休息时间, 更重要的是可以立即为你创造更多价值. 而且这已经是第三个版本了. 当然不仅可以做 UI, 还有许多其他实用的工具, 比如路径. 几乎所有的东西都可以通过脚本访问和扩展. 包含完整源代码哦!