因为磨坊在我们的游戏中经历了一个从糟糕到不错的蜕变过程,这种进化本身感觉值得记录。Unity特定,比较长,没代码,只是关于磨坊成长过程的故事。
首先,背景。我们的游戏是地狱清洁工,你扮演地狱的清洁工,清扫尸体并将它们喂入一个肉磨,奖励物品和灵魂。磨坊基本上是整个游戏循环,所以它已经被重写了比其他任何东西都要多。以下是它的旅程,尴尬的部分留在了原处。
v0,傻瓜式的磨坊
第一个版本的磨坊基本上就是一个触发器碰撞体与一个协程相连。一个ragdoll掉进去,你等一秒钟,然后删除它,生成一些骨骼碎片并以随机速度抛出它们,以至于它感觉像一个赢得奖励的彩票一样。整个设计就是这样。
在事实上,第一轮测试就像这样。人们喂它尸体,骨骼碎片飞出,它工作了。但是,它在几个方面都是糟糕的,所有这些问题都源于同一个根源问题:磨坊没有角色。
尸体就消失了。一个帧,它是ragdoll,下一个帧,它就消失了,没有过渡,感觉每次都像是一个bug一样。每个敌人也掉落相同的垃圾,因为奖励只是一个平坦的“生成10个骨骼”的数字,所以没有理由去喂它哪个尸体。因为它在每次奖励时都生成和销毁对象,所以垃圾收集器在地板上堆满了奖励时会暂停。
我保留了那个老脚本在存档文件夹里,它是奇怪地令人安心的东西。下面所有的都是慢慢爬出它的过程。
v1,变成一个生物而不是一个容器
重大的转变是决定磨坊不是一个容器,而是一个生物。它伸出手臂,抓住尸体,并吞咽它们。这个决定驱动了几乎所有其他系统。
首先,我需要解决的问题是“我能看到食物”和“我正在吞咽它”这两个瞬间是完全不同的。所以磨坊有了两个触发器体积。一个外部区域意味着“有尸体在附近,请伸出手臂”,而一个内部口腔区域意味着“一个尸体刚刚进入我,请将它放入磨坊”。外部区域只是维护一个附近尸体的列表并将它们交给手臂。内部区域才是真正进入磨坊的尸体。
这个系统的关键部分是一个标识列表,标识已经被保留的尸体。一个ragdoll有十几个碰撞体,每一个碰撞体都会触发一个触发事件,所以没有一个标识列表会导致同一个尸体被队列了十一次并奖励十一次。这个标识列表在下游也被检查,手臂不会抓住已经被保留的尸体,口腔也不会重复计算尸体等等。听起来很微不足道,但它不是,我的早期bug中有一半都是“为什么一个尸体掉落奖励五次”。
v2,所有人都在问的那部分,IK手臂
首先要感谢一下。这个部分不是我做的,而是我们的工程师丹做的。这是人们总是问的那部分。磨坊有两个手臂,它们物理上伸出手臂,抓住一个ragdoll,抬起并将它扔进口腔。丹手动编写了IK,而不是使用IK资产。原因之一是为了保持对目标的控制,原因之一是因为手臂很短,数学不太可怕。
它的工作方式是,每个手臂从手腕到肩膀的骨骼链上走,记住这个链条。然后每帧,如果它正在尝试抓住东西,它会旋转每个骨骼几次,使手腕指向目标,缓和以免突然改变。几次旋转后,手腕就指向目标了。这发生在正常动画之后,所以手腕的动画覆盖了手臂的动画。每个手臂都是一个小型状态机。它有几个状态:空闲,抓取,抬起到中点,带到口腔,吞咽,返回休息。中点状态是因为一个很蠢但很重要的原因。没有中点状态,手臂会拖住尸体穿过磨坊的身体mesh,造成一个很丑的效果。所以它在中点状态时将尸体抬起并在前面,然后将它扔进口腔。
丹花了很长时间才让手臂感觉正常。有两个问题。
一个是,手臂不会目标整个ragdoll,而是目标最接近手腕的骨骼。它会在一个更接近的尸体出现时放弃当前目标。这样看起来像它是在 oportunistic 和 grabby 而不是锁定在一个目标上。
二是,实际上携带尸体。手臂抓住后,它会取代ragdoll的物理并将尸体每帧移动到手腕骨骼上。这样看起来比teleport 更真实。
有一个真正的诅咒的特殊情况。多数敌人都是正常的多骨骼ragdoll,但有一种敌人只有一个单独的骨骼。通常,当手臂抓住一个东西时,它会关闭这个骨骼的碰撞,以免它与物理打架。但是,如果我这样做到单骨骼敌人,口腔触发器就不会触发了,所以手臂会一直抓住它而不会吞咽。所以这个特殊情况的敌人保留了碰撞。丹花了一整晚才解决这个问题。
手臂也会让整个生物向前倾斜,当它们伸出手臂时。它们也会放弃抓取目标如果它们不能到达目标。这样看起来比会一直flailing。
我喜欢丹的做法。手臂和磨坊的脑子之间只有事件通信。它们之间不会有任何其他通信。这样看起来比会变成一坨糟糕的代码。
v3,真正的磨坊和让奖励感觉好
一旦尸体进入队列,一次协程就开始磨坊。这个版本的磨坊比v0大得多。奖励现在是数据驱动的,每个尸体都有自己的奖励表和灵魂表。磨坊通过对这些权重进行随机抽取来奖励。这样,一个大型敌人可以掉落不同的东西,而不是一个小敌人。现在有一个真正的理由去狩猎特定的敌人。奖励的数量是范围值,然后乘以你的升级值。这样,技能树可以在游戏中真正地增加奖励。
奖励可以在磨坊工作的整个时间内缓慢释放,也可以在一段时间后一次性释放。这样看起来像一个真正的彩票。
三个小技巧做了大部分的工作。奖励从零大小开始,突然弹出到实际大小,然后消失。这个小过度是奖励感觉满足的主要原因。每个奖励都会被推向一个目标点,以便它们看起来像从一个口中喷出一样。还有一个循环磨坊效果和一个一次性爆发效果。爆发效果会在每个奖励出现时触发。
动画上,我使用了跨fade到命名状态:空闲,磨坊,吐出,走,等等。这样看起来比维护一个巨大的动画转换网要好。有一点我不太自豪。如果艺术家给我一个不循环的磨坊动画,它会播放一次,然后冻结在最后一帧上。所以我写了一个小的watchdog来检测动画是否已经结束,并将其重置到开始。
v4,磨坊学会走路
在某个时候,磨坊不再是家具,而是可以被拖拽的东西。它被锁在一个升级后才能解锁。它的移动是很简单的。它射出一个射线到地板上并将自己粘在那里的高度上。它会转向玩家,并且当它被拖拽时,它会每个物理步骤向玩家移动一点。它会在移动时切换到行走动画。第一次它跟着我穿过地图时,我感觉它终于活了起来,而不是一个道具。
v5,让世界干扰它而不破坏它
开发后期,我添加了一个腐烂的危害。基本上就是一个垃圾堆在磨坊下面,会减慢它的速度。我的想法是不要让奖励系统进入动画状态或磨坊队列并破坏所有的约束。所以磨坊就暴露了几个简单的速度调节器。动画速度,磨坊速度,拖拽速度。任何外部系统都可以调整这些调节器而不需要知道内部系统的任何细节。它们都默认为正常,所以所有现有的设置都和之前一样。
当垃圾堆够大时,这些调节器就会降低到零,整个磨坊都会冻结在一个姿势中,看起来像它已经被破坏了。然而拖拽调节器有一个底线,所以你总是可以拖拽它并将它从垃圾堆中拽出来。它会变得很重很慢。
这个default-to-normal调节器的想法现在是我的去处当我想让其他系统干扰它而不把它们粘在一起时。
性能问题,因为尸体到奖励是流量
两个问题很重要。首先,不再创建和销毁对象了。奖励和灵魂和ragdoll都来自一个池子并返回到池子。这个改变消除了v0的几乎所有的抖动。第二个问题是,当一个奖励最终停在地上时,它会冻结自己并关闭碰撞器。这样它在那里静止时几乎没有成本。只有当你或清洁机器人接近它时,它才会唤醒。一个共享计时器检查所有睡眠的奖励,而不是每个骨骼都有自己的计时器。这样在地板上堆满了奖励时会有很大的不同。
我会做得不一样
预留的尸体标识列表的书写工作是分散在两个类之间通过事件通信的。理论上这是很干净的,但它花了我很多时间。下次我会将整个预留列表放在一个拥有者下,并且有一个真正的api,而不是两个脚本通过回调通信。
IK方法是完全可以接受的,但如果手臂变得更长的话,我和丹都同意我们不会继续使用这个方法了。有更好的求解器。
还有一个“这个动画是否循环”的watchdog是为了弥补没有在导入时强制设置循环的内容规则。
总之,这就是一个坑变成一个倾斜,走路,抓取的小尸体食物机。欢迎去更深入地讨论任何一个部分,手臂和速度调节器的解耦可能是最有用的部分。
评论 (0)