总结一下“牙刷转转”(“Oops! My Toothpaste”)游戏的开发。
业界常用 Postmortem(解剖报告)形容一个项目周期结束后对其冷静客观的回顾与解析。我有时觉得这个概念很酷。它不同于“讲故事”式的心路历程分享——毕竟游戏这种载体本身就已经天然包含了太多的“故事”与“感情”。但要想将体悟真正沉淀下来、带到下一个项目去,反而将已经结束的项目当做“尸体”、不掺杂过多感情地逐条剖析,或许对自己帮助更大。
作为这里真正意义上的第一篇开发日志,我试着久违地写一篇“Game Postmortem”。但完全不掺杂主观感情去描述自己的作品是做不到的,毕竟是我嘛。
这个项目源自一次刷牙的经历。那时在挤牙膏的我,一没留神将牙刷拿反,等开始刷牙才惊觉人类手臂旋转的角度居然如此有限 果然使用这幅身体就是不方便啊。试图将牙刷缓缓回正时,刷头上的牙膏还是败给了重力、吧唧一下掉到水槽里了。但没来得及对牙膏表示惋惜,我灵光乍现,想到:把这个经历做成游戏的形式或许会很有趣也说不定。因为涉及到翻滚(Roll)交互,当时就确定了要做 3D 形式。说到 3D 游戏,自然会选择现代游戏引擎,其中我又与 Unity 最为熟识——毕竟怎么说也有着长达 5 年的爱恨情仇,为图省事就立即拍板了开发的工具。
私心来说,我其实更想用轻快优雅的 Defold 和 Lua 来做 Velas Playhouse 的第一个游戏的(若不是 Defold 更专精于 2D 游戏的话)。Unity 相比之下就显得笨重不少。光是新建空白的 3D 项目就已经占用了 2.8 GB 磁盘空间,做这类小游戏多少有种高射炮打蚊子之嫌。
可取之处 / 进展顺利的地方
去拟真化
因为项目规模不大加之有 ChatGPT 帮忙构建代码,这个项目大概只花了四到五个小时就把最小可行的原型(MVP)做出来了。不过在前期定下游戏开发方向时还是费了一些时间。选用 3D 之后,出于惯性会使人执着于增强游戏的拟真感,而在视觉表现的东西上花费精力:比方说相机的调度、周围环境的建模、材质的质感纹理等等。
可能是脑子里总想着要“还原现实生活里刷牙的感觉”,于是情不自禁开始去抠细节。事后想起来,其实那些视觉表现的东西对于游戏核心体验来说其实可有可无。
所以在这里,这个项目可能做了一个很大胆的转向——从”极致拟真“向”二维卡通化“靠拢,包括不去移动和旋转镜头、加上搞怪的面部动画和配音等,无意间弱化了游玩时对“拟真”(即,每一个细节是否和日常生活一致)的关注,从而让玩家的注意力转向从交互中寻求乐趣。而这一决策也大大缩短了开发的时间和难度,在这次实践中我觉得是做得比较好的。
Vibe Coding
发现这个项目的规模对于 Vibe Coding 来说算是甜蜜区。由于项目核心的 Monobehaviour Script 只有 4 个左右,大语言模型有足够的 context window 关注到各个文件的功能与改动,在构建阶段给出的代码建议“可用性”在 80% 以上,真正把项目炸掉、需要我插手的情况较为罕见。甚至到后期我已经有点对 ChatGPT 写的项目代码放弃思考,这种全权交给 LLM、完全解放双手的情况对于过去那些复杂项目来说是不敢想象的。
也多亏了 Vibe Coding,这次项目我有种当了把“真·游戏设计师”的感觉,能够把更多的精力花在“需要做什么”,而不是“要怎么实现”上面。这对于个人项目来说确实很解放脑力。只是在 Vibe Coding 的时候要向 ChatGPT 分点准确传达需求,切忌用“你懂得”的方式马虎糊弄过去,不然 LLM 会以一种“过饱和”的极端方式去实现,这对后期 debug 来说是灾难级别的。这次其实也踩了一下坑——千万别以为 ChatGPT 比你更懂开发,尤其是对这种涉及到物理引擎的 3D 游戏项目来说,因为缺乏对三维物理世界的理解,ChatGPT 时不时会出现乱点鸳鸯谱的状况,只是当时看来刚好能跑起来罢了。
Vibe Coding 这次让我耳目一新的地方还在于,它在编辑 Monobehaviour Script、往其中加新功能的时候,会自动写出一些 SerializeField。比方说,在牙膏的 Prefab 上,它就给了我诸如: tiltThresholdMinDeg、tiltThresholdMaxDeg、keepStickSpeedMinDeg、keepStickSpeedMaxDeg、detachDelayMinSeconds、detachDelayMaxSeconds 这些参数,让我可以通过调节变量、直观地去修改牙膏与牙刷头之间附着的粘性从而改变手感与难度,这种体验最让我吃惊。能够那么 high level 地体验一把游戏设计师那种“调参/拉表”的感觉,而非直接关注实现细节,在我做游戏开发那么久着实是很少数。有一瞬间窥见 Vibe Coding 的终点搞不好就是“调参工程师”。
总的而言,这个项目的 Vibe Coding 体验于我是积极居多。它让明明已经基本熟识 Unity 的我依旧能大开眼界,接二连三见识到”原来物理引擎原来还有这样玩的骚操作“。我甚至已经期待它哪天帮我去 Vibe 一下 Defold 和 Lua 了。
音效
过往项目中,我其实没怎么试过把音效用在游戏的核心交互里,基本都是按钮点击这种界面音效居多。而在这次项目里,我觉得音效的运用给这个游戏贡献了很多趣味性。
“Squeak”音效本来是被用在牙膏接触牙刷瞬间、以及从牙刷上掉落的时候,以体现史莱姆那种 Q 弹的感觉。后来发现这两个瞬间无法完全展示史莱姆嘴巴张嘴的动画,于是试着把“Squeak”音效放在了史莱姆随牙刷一起转动的时候。试玩时,惊觉这一改直接改善了原本牙刷转动反馈缺失的问题,而且这个思路并非将音效反馈直接加在牙刷上(在交互设计里这通常是很直观的解决方式),而是让史莱姆的怪叫去为玩家的交互提供反馈、并让玩家注意力被因为自己交互带来的史莱姆的反应吸引。这无形中给游戏的核心玩法增添了趣味。这是我这次设计中感觉最神奇的一点,可谓直观感受到了音效对游戏趣味性和交互反馈的加成。
当然,“Weee”音效在 playtest 里面也大受好评。包括我自己玩的时候也注意到,这个设计对玩家挫败感的缓和其实不容小觑。
减少对物理引擎依赖,尤其当它关乎游戏难度
使用 3D 引擎会让人自然而然变得过于依赖物理引擎去处理事务:比方说惯性、碰撞、重力等。这些物理特性经常会给游戏带来随机性、制造意外效果,这对于很多追求趣味性的游戏来说都有优势。但这种不确定性有时也会为游戏设计埋下定时炸弹。特别是当物理特性和关系到游戏的核心体验(难度设计)时,充满不可控因素的物理特性或许会破坏玩家心智模型中对达成游戏目标的预期,给他们更多带去惊吓而非惊喜。
这个项目我本预期使用物理系统来让牙膏“黏附”在旋转的牙刷头上,毕竟这种解法最符合直觉。但实践发现这种设计会使得牙刷与牙膏史莱姆的交互变得不可控。哪怕作为设计者,也无法判断该遵循什么样的游戏规则才能保证牙膏不掉下来——这样放任物理引擎主宰一切的设计,也许和那种连普通行走都会一个平地摔摔出大气层的混沌 3D 游戏没什么两样。
于是在 ChatGPT 的帮助下,我们将方案改成依靠关节约束 + 基于可控物理量(角速度、倾斜角度、停留时间)的逻辑规则的实现,放弃了之前那种完全倚靠碰撞和摩擦的做法。这样的好处是它没有完全抛弃物理效果,玩家依旧会在摸索中发现物理引擎带来的随机的趣味(比方说牙膏史莱姆会和现实生活中一样粘在牙刷头边乱甩),但总体的交互法则是可以被学习和预测的——只要确保牙刷不倾斜、不会在旋转途中失去角速度,牙膏史莱姆就不会意想不到地“飞走”。这使得游戏能提供一定的逻辑规则可被玩家摸索、学习和掌握,而不全是无厘头的胡乱玩耍。
不太可取之处 / 值得反思的地方
输入设备考虑与难度的设计
这个游戏的设计核心主要围绕着“交互”本身。这是我的舒适区。而选择鼠标滚轮和触摸板进行交互的输入设备也是因为它们能提供足够拟真的体验和挑战上的变量。我比较满意的一个小巧思也在于来回摩擦鼠标滚轮/触摸板的交互与手握牙刷翻转的交互感吻合(毕竟人类手臂无法朝一个方向一直扭转下去)。这种手指无需离开鼠标滚轮/触摸板的操作也会使手脑体验更趋于一致,觉得自己在操纵牙刷本身而非滚轮/触摸板,以提供更沉浸拟真的游玩感受。
但在进行游戏设计时,我的难度设计主要取自自身的操作以及更多的,我的罗技 M720 鼠标当前的滚轮设置。随后在 playtest 中发现,这款游戏的难度上下限与玩家的设备/设置关联很大。比方说,对于苹果设备的触控板,(由于采样分辨率/触控面积大小)它的操作会明显比鼠标滚轮更难,而在更小/分辨率更低的触控板上这种难度还会相应增加;而对于游戏鼠标、尤其是在启用了无级滚轮的鼠标上(@mikusa 凸),难度则会走向另一极端。游戏之简单甚至连挑战目标都随之变成“想办法把牙膏甩下去”。
因此,在生产环境的实践中,此类交互难度与输入设备强关联的游戏设计,应当考虑加入针对设备的校正设置(Calibration),从而平衡各玩家的游戏难度。
由于物理变量相对单一(主要取决于牙刷的旋转角速度、最终停留时的倾斜角度以及停留时间),该游戏的难度总体偏向简单。一旦掌握了玩法则会变得相对无聊,使得重复可玩性较弱。若考虑增加挑战难度,则可以考虑适当加入其他物理量(如摩擦力、风力等)。此外,考虑到史莱姆可以从没有回正的牙刷上掉落,也可以从牙刷的操控入手来控制难度:比方说,增加牙刷的转动阈值、适当让牙刷“转过头”,可能可以使得掌控牙刷本身变得更具挑战。
另外,由于数值并非本次设计旳重心,两种史莱姆的粘性数值都是随机分配。但在实际生产实践上,该数值和玩法的手感/难度都关联较大。以后若有相关针对数值的练习再另行分析。
还是 Vibe Coding
虽然在项目构建阶段,Vibe Coding 可以高速构建可玩原型。但其对于 edge cases 的处理偏向过饱和与繁复,会为后期功能/数值调整、以及迭代与 bug 修复增添技术债。比方说,史莱姆掉落的销毁本可以直接使用碰撞检测或是高度检测触发。但当我实际发现 Unity 的碰撞检测(由于物体较大的下落速度等原因)有一定概率不触发时,ChatGPT 却直接转而采用了极端的 Interlace 检测去处理。虽然“看上去能跑”,但实则破坏了代码可读性与增加了隐形的后期维护成本。若在下次项目中继续采用 Vibe Coding,应当为其提供清晰的功能需求、将它局限于补完单个 function 内的功能、并考虑在采用关键的功能代码前先进行较严格的 code review。
有什么经验可以带到下个项目
美术的取舍
这次项目我比较自满的一点是,它没有去依赖 Unity Assets Store 上已有的(收费)素材,直接采用了基础物体和材质去制作史莱姆,并且我还手作了史莱姆的面部贴图。对于原型制作来说,这十分节省金钱与时间,并且也在一定程度上增加了对作品的 sense of authorship 与创作的成就感。
特殊敌人
受 Defold 的教程项目启发,这次我在普通史莱姆的基础上加入了一种特殊的红色史莱姆变体,两者孵化概率比为 3 : 1 。虽然没为其加入奖励机制,但特殊敌人的出现也给游戏增加了趣味与挑战。当长时间游玩、稍微感到视觉疲劳时,能够短暂提振玩家的注意力。
导出为 WebGL
这是我第一次尝试导出 WebGL,最终效果意外的不错,且对于 Playtest 来说也很有帮助,免去了玩家需要专程下载游玩的困扰。只是在 Deploy 阶段遇到了适配难题:在 WebGL 上,普通史莱姆会刚孵化就消失不见,却并未被销毁。
在数小时的排查后,最终 ChatGPT 定位到问题可能出自史莱姆的 Rigidbody 配置。通过关闭 Rigidbody 的自动质心(automaticCenterOfMass)、自动惯性张量(automaticInertiaTensor)和限制去穿插修正速度(maxDepenetrationVelocity)与最大角速度(maxAngularVelocity)最终得以解决。Vibe Coding 的代码如下:
rb.automaticCenterOfMass = false;
rb.centerOfMass = Vector3.zero;
rb.automaticInertiaTensor = false;
rb.inertiaTensorRotation = Quaternion.identity;
rb.inertiaTensor = Vector3.one * 0.02f;
rb.maxDepenetrationVelocity = 2f;
rb.maxAngularVelocity = 50f;
不过虽然 WebGL 为项目部署与游玩天然提供便利,但由于 WebGL 平台的浮点和执行环境与 Windows 上 Unity Editor 存在差异,所以可能出现在 Editor 上被掩盖的 bug。适配时需要特别留意和排查前者对物理引擎与输入设备(如麦克风、键鼠等)的兼容情况。