投稿者 StrangePan,Earendel 于 2024-08-23

大家好,
为五足生物开路吧!
上周(工厂报#424)我们宣布了两种全新的敌人即将来到《异星工厂:太空时代》:踩踏棘(Stomper)和扫射棘(Strafer)这周,我们会带你走进舞台里,欣赏可爱怪物背后的特性、系统和优化。我们也会谈谈我们用于准备上周的宣布的技术。我们要涵盖广阔的范围,所以现在就开始探索吧。
--------------------
以优化开始 ~ StrangePan
从最开始,我们就知道五足生物需要重用操作蜘蛛机甲的腿的代码,但是我们也知道蜘蛛机甲有些性能问题。如果我们不能解决这些性能问题,那么把五足生物加到异星工厂里边绝对会重度损害 UPS。虽然“过早优化是一切问题的源泉”,但是我们在这个问题上没得选;为了实现五足生物,我首先得优化蜘蛛机甲。
--- 去重工作 ---
蜘蛛机甲上,最明显的性能问题每当试图越过障碍物时都会发生。在 1.1 中,每当你操纵蜘蛛大军渡江时,你都可以注意到游戏速度大幅降低了。在某些存档中,哪怕只有 10 只蜘蛛机甲,UPS 也会降低。
我注意到的第一个罪犯是,搜索空地来放脚的算法。在 1.1 中,这个算法从内向外螺旋搜索,检测某格的 1/5 是否和瓷砖(比如水)或实体碰撞了,范围最大为 16 格 × 16 格。最坏的情况下,也就是没有空地能放脚时,这个算法会进行约 6400 次碰撞检测((8 [半径格数] × 2 ÷ 0.2 [检测间距])²)。

蜘蛛机甲腿需要检测碰撞的所有位置。这个示例显示了 1600 次检测,但这还不是最坏的情况。
刚才没说,蜘蛛机甲的每条腿都会进行这个检测,而且每一游戏刻都会检测。
要想解决这个问题,首先需要意识到扫描精度太高了(0.2 格)。每一格瓷砖的边长都是这个的 5 倍,并且大多数实体的边长都至少是这个的 10 倍。因此,这个算法经常会一次又一次检测同一个瓷砖或同一个实体。所有我们不使用这个算法,而是寻找一个碰撞区域,新算法立即跳过这个碰撞器的包围盒的边缘。如果我们能记住每个碰撞器的包围盒,那么就有可能可以跳过搜索整个行和列了。

蜘蛛机甲腿现在会检测少得多的位置。这个例子显示了大约 70 个碰撞检测。另外,请注意新的扫描模式,这会比螺旋式产生更好的结果。
仅靠这一个改变,蜘蛛机甲的性能显著提高了。但是工作还没有结束。我们需要释放出更多的计算周期才能使五足生物成为可能。
--- 共享知识就是力量 ---
第二个性能罪犯是,一组蜘蛛机甲腿中每一个都会在大致相同的区域中执行相同的空地搜索。即使每条腿都执行了更少的碰撞检测,临近的几条腿仍然会重复相同的检测。这自然引发了一个问题:蜘蛛机腿能否协同工作以减少搜索呢?
要想解决这个问题,首先要意识到算法从中心开始向外搜索,并在首次发现空地时停止。这就是说,如果我们发现离搜索原点几格的位置是空地,我们就可以知道在它里边的正方形区域全都被堵得严严实实了。如果我们把这块里边的正方形区域标记为“搜索排除区”然后把它放在列表里,以后蜘蛛机腿再搜索时就可以检查这个列表,然后跳过落在区域中的位置的碰撞检测。

在搜索一片区域找到空地之后,这片区域会被记录为“搜索排除区”,等到当前游戏刻结束时清空。其它蜘蛛机甲腿在搜索空地时会跳过这整个区域。
说实话,我对这个优化没多大希望。实现非常幼稚,而且搜索排除区只在被添加时的那个游戏刻有效。但是结果出乎我的意料,这个小技巧节省了大堆时间!在又一些调整和其它优化之后,五足生物现在终于成为了可能,它们现在可以被加入扩展包了。
--- 新行走算法 ---
最后的性能罪犯涉及到了蜘蛛机甲要踏出哪条腿。你也发现了吧,我们现在所做的所有优化都是针对空位搜索的,但是还有一个显著的低效处:每条腿——无论是正在移动还是等着旁边的腿动完——都会在每一游戏刻搜索空地,只要蜘蛛机甲还在移动。在蜘蛛机甲决定踏出腿时,每条腿都会根据需要踏出的距离获得一个分数,并且只有分数最高的腿会踏出。如果这条腿正在移动,那么结果只会被丢弃。
显然这会导致许多浪费掉的计算,可是我们也需要确保蜘蛛机甲总是要朝着目标移动。如果没有这个估分系统,需要踏出一大步的腿经常会卡住,因为旁边的腿可能会重复小步走。我们需要一个高效的方法来确保每条腿都有相等的机会来移动。
这个问题在五足生物出现于早期开发几个月后才被解决。在添加五足生物时,旧的估分动腿算法不再合乎需要了。Earendel 想完全控制五足踏出的顺序,以便这些外星生物的行走模式更独立、更有机。我也趁这个机会大修了整个蜘蛛机甲行走算法,并且实现了最后的大型优化。
有五只脚的生物该怎么样行走呢?地球上没什么例子,但是 Earendel 还是想到了个好办法。为了维护平衡和稳定,五足生物以五个点的星星模式行走。

在所有第 1 组中的腿移动完后,第 2 组的腿会开始移动。在它们也移动完后,第 3 组也开始移动。然后是第 4 组和第 5 组。
然而也有些五足生物喜欢倒着数。
为了实现这个,每条蜘蛛机甲或者五足生物上长的腿都以数字标号,称为“行走组”。在行走时,当前行走组中的一条或多条会被选中,被选中的腿会踏出,搜索清晰的目标,然后向那里移动。不再有浪费的搜索。在当前行走组中的所有腿都踏出落地过后(或者移动超过了某些特定的阈值,但是我今天说不清楚了),这样的过程会在下一行走组上重复。这个新系统显著减少了碰撞检测的数量,同时给予了设计师对蜘蛛机甲和五足生物行走动画的前所未有的控制能力。
在这三个大型优化,以及其它几个我没时间讲的小优化以后,蜘蛛机甲对 UPS 的抢夺大大减少了。在一个用于压力测试蜘蛛机甲移动的存档里,我测出的结果是 每次更新减少了 53.56% 的毫秒(约 2.15 倍速度)。所以在《异星工厂 2.0》里,玩家可以操控更大规模的蜘蛛机甲大军,而不会毁掉 UPS(结果可能会改变)。
--------------------
新敌人,新功能 ~ StrangePan
随新敌人而来的是新功能!随新功能而来的是新引擎特性。接下来我会讲讲影响了五足生物设计的游戏设计决策,以及我们为了实现它们所做的系统更改。
--- 跨过障碍物的寻路 ---
我们不久之前刚知道了,把 Gleba 上的工厂包裹在城墙里会极大拖累游玩体验。所以我们设计了一种不关心我们那些弱小的城墙的敌人。相反,玩家只能用现有的炮塔和新的火箭炮塔来抵御五足生物的攻击。
为了使这个机制能工作,我们需要允许寻路器支持跨江,并且能忽略障碍物。幸运的是,我可以扩展已有的虫子寻路系统(工厂报#317),并加入一些新功能。
在执行抽象(即,块级)寻路时,只记录块边缘上可走过的位置不再足够。额外的,块边缘的不可走过的瓷砖还需要记录到最近可走过的瓷砖(最多两块不同的陆地之一)的距离。我们之后在两个块之间连接多块陆地时会用到这个信息。
两个瓷砖之间的距离按东西方向距离和南北方向距离的最大值来算。

一个块内,所有可走过的瓷砖都会被分组放在一起。如果它们之间的距离小于最大允许的间隔,它们就会被分到同一组。块边缘的瓷砖会记住到这个块内最近的可走过的瓷砖的距离。
(注意:这张截图并没有显示边缘瓷砖位于多于一块陆地范围内的情况。)
接下来是基础(即,瓷砖级)寻路时,每个节点都必须记住它是否可以走过,并且总是指回上次已知的可走过节点。这允许算法向不可走过的区域(水、墙壁和其它实体)深入搜索,以找到虫子不可能走过的路径。

节点向不可走过的地形搜索一定距离,总是指回上次已知的可走过瓷砖。典型的寻路过程中会搜索比这张示例图中多得多的节点。
不连接的寻路在工厂 2.0 中可以通过模组 API 访问,虽然原版游戏里还完全没用到它。
--- 空气中的孢子 ---
现在那些墙拦不住五足生物了,但是玩家有问题了:五足生物仍然可以从任何方向攻击你的工厂的任一部分!工程师除了把工厂用更多更强的炮塔环绕以外还能怎么办?这也不是我们想让玩家做的策略,所以我们需要使这个策略不必要。为了完成这个需求,我们改变了 Gleba 上污染这个机制的工作方式。
敌方单位(虫子和五足生物之类的)往往基于污染选择攻击目标。在攻击队集结时,它们找到附近污染最大的区块,然后向其中最近的进发,并攻击那个块中产生污染的设施。
Gleba 上的污染与其它星球相比截然不同。有些在 Nauvis 上产生大量污染的设施在 Gleba 上不会产生任何污染,而在 Gleba 上产生大量污染的设施在 Nauvis 上不产生污染。我们怎么样才能让五足生物在不改变虫子行为的情况下优先攻击你的工厂的特定部分?
工厂 2.0 引入了污染类型。模组可以添加不同类型的污染。机器、工厂、虫巢和瓷砖都可以配置,以产生或吸收每种污染类型的不同数量。然而,因为性能原因,每个表面上只能启用一种污染类型。对于启用了污染的新的和现存的表面,“污染(Pollution)”是默认的污染类型。在《异星工厂:太空时代》中,Gleba 上孢子(Spores)会取代污染。每种污染类型都有自己的名字、吸收率、排放率、地图颜色,还有更多。
在新工具的加持下,我们终于可以调整 Gleba 以使五足生物把进攻聚焦于一些特定的建筑上,同时忽略掉工厂的主体部分。一般是这样的,除非你把它们惹火了。
--- 重新设计腿 ---
最后,五足生物的腿需要返工。毕竟它们是碳基生物,我们想最小化每条腿伸展和压缩的程度,而强调膝盖和臀部的弯曲。这使得这种生物看起来更逼真,并且不那么像机器人。
我们甚至让腿基于相对于身体的旋转更新精灵图像。这件事不太容易干,原因是异星工厂有透视,而且角度有些奇怪,但是 Fearghall 和 Earendel 确实做到了。最终的效果非常好,这些生物像是现实中存在的一样。方法是,保持光线角度一致,并且总是考虑玩家的视角。

踩踏棘和扫射棘的腿会试图在臀部和膝盖处弯曲,如果不能的话才会伸缩。精灵图像基于旋转改变。
因为蜘蛛机甲和五足生物共享一大堆代码,现在蜘蛛机甲的外观也稍微更新了一点。现在腿不那么僵硬了,更容易弯曲了,并且,说实话,跟之前相比更吓人。蜘蛛机甲仍然是显然的机器人样,所以我相信我们设法保持了它们符号的外观,并且给他们做了个小小的整容。

左:1.1 中蜘蛛机甲行走动画。右:2.0 中蜘蛛机甲行走动画。
--------------------
舞台里 ~ Earendel
在我们准备决定这周工厂报的内容时,我开玩笑说我可以写一篇关于上周工厂报的工厂报。额……你们好像真喜欢这个点子啊。所以,我们才在这里相见。
这个点子挺有意思,粉丝们喜欢欣赏舞台里。展示视频制作方法不会剧透游戏秘密,同时也可以展示舞台里内容。
大多数工厂报,(比如这一个)在发布的前一周就写好了。通常我会再提前一周,这样压力小一点。但是对于关于敌人的工厂报,我会提前好几周,以确保从产品进程中消除所有的“未知”。
我使用某个随机生成的地形作为“角斗场”,放一些敌人,然后把它导出为场景(每个人用地图编辑器也可以办到)。
在之前的视频中,我编写了一些性能捕捉代码,可以捕捉玩家的角色奔跑操作和坦克操作(转弯、加速等)。我扩展了这个系统,现在它可以捕获玩家的武器输入,基本就是选中哪个武器,开火与否,以及目标位置。
这是玩家装备栏。武器伤害点到 8 级。


我记录了在角斗场中与敌人战斗时的玩家输入,然后把这些输入应用回场景,再运行一次,然后……结果不一样。
所有玩家输入都是一样的,但是敌人干的事不一样。有些徘徊到了不一样的地方,因此在不同的时间攻击。在最开始的捕获版本中,地图上有一只扫射棘,而另一只在卵筏刚被打破之前出现。在脚本控制角色的版本中,没有生成第二只扫射棘,而是新生成了一只蠕动棘。踩踏棘因某些原因加入战斗的时间早了很多。角色被滑翔的蠕动棘击中从而减缓速度的时间也不一样,所以移动脱轨了,最终角色卡进树里被踩死了。
所以,开始不是很理想。再次重运行场景,确实角色卡进树里被踩死了,跟之前一模一样。所以至少不是每次所有结果都是随机的。我试图找出是什么东西导致了或者不会导致场景的结果被随机化。
在一些测试后我意识到,场景重播是不可能会跟输入记录一样的,因为录制代码和应用程序代码本身都会改变结果,而且没办法解决这个问题。这有点烦人,但也不是什么大麻烦,因为我知道角斗场周围的一大堆景观都会发生变化,最有意义的办法是,添加其它可靠的工具来使模拟更稳定。
最迫在眉睫的问题使,玩家总是脱轨。显然这种事情会简单地发生,因为滑翔蠕动棘会减慢速度。在躲开滑翔蠕动棘时,我有很多次差点就撞上了,所以结果是一大堆可变量。任何对玩家移动的改变也会影响敌人的路径,所以整个战斗会被拉向预料之外的方向。


在一些新地形被完成时,它们被加入了地图,但是这会“随机化”敌人的行为,在不同的时间减慢角色,所以重播出的所有东西都不一样了,并且玩家最终的路径看起来也不一样了。
上:原始移动路径。下:使用同样的操作但是在不同的时间被减速导致的路径。
我对此的解决方法是,测量角色与预期路径的距离,如果它们之间偏差得太多,那么强制朝某个方向跑以纠正。无论效果如何,玩家仍会被减慢速度,但这意味着玩家可以走捷径或跳过闪避动作以回到正确的路径上。
这种追踪玩家的移动的方法给了我一种替换摄像机系统的方法。不像之前的视频中摄像机是全景的,平滑地覆盖了整个环境,现在的这个视频会跟随角色的不规则的运动而移动。我们之前使用贝塞尔控制点系统来控制摄像机,要匹配玩家运动时简直就是灾难,尤其是,重新记录的时候,而且基本一定会遇上这种情况。
我的新系统从性能捕获中获取注册的角色位置,并且使用一个宽松的过滤器来平滑那条路径,本质上使得每个点成为之前和之后的点的平均值。

实际的角色移动以白色显示。用于摄像机的平滑后的路径以蓝色显示。
这种方法给出了漂亮的相机运动,但是并不能紧密地跟随操作,因为它总是把角色作为中心。然后我添加了次级目标系统,其中虚拟点可以移动至目标敌人,并且摄像机会在平滑后的路径和次级目标之间维持某个百分比。这个系统使用虚拟点而不是敌人本身,这是为了在敌人死了之后以有限的速度移动至新的目标。额外的控制层可以决定相机被拉向次级目标的强度有多少。在视频的开头次级目标是卵筏,但是影响被设为 0。

所有的调试数据都启用:平滑后的主摄像机路径以蓝色显示。次级目标路径以橙色显示。实际相机结果以白色显示。
紫色方块是相机边界。
所有的位置和时间都比平时更重要,因为战斗中的一切都是随着音乐编排的。卵筏在节拍开始之前就死了。当有一种忧郁的音调,并且音乐开始混乱起来时,扫射棘死了。踩踏棘也需要在特定时间死亡(稍后会详细介绍)。
一路上,我了解到什么会改变场景结果以及敌人会受到怎样的影响。
只要没有 任何 变化,敌人的行为就会始终如一。换掉图形是可以的,但改变图形移位就不行了。移动敌人,添加或移除一棵树,改变瓷砖,为角色添加一些弹药,甚至添加装饰品,都会破坏结果。许多操作也会破坏结果。
刷怪器(卵筏)可能会也可能不会生成一个单位,如果生成了,也可能是不同类型的单位。有一次它随机生成了一个踩踏棘,所以有 2 个踩踏棘。运气不太好。该场景是带有一组视频场景的模组的一部分,因此为了解决这个问题,我更新了场景模组,以便卵筏只能生成蠕动棘。
任何五足生物都可能向随机方向徘徊,因此如果战斗开始,它们可能会超出“呼救”的范围,并且永远不会出现在战斗中。
我试图在敌人出现在摄像机视野之前将他们传送到正确的位置,但我很快发现没法传送长腿敌人,它们会在一帧内像橡皮筋一样弹回之前的位置。
取而代之的是,我必须通过 LUA 脚本将五足生物生成到正确的位置,然后它们就会出现在摄像机视野中。这使位置更加一致。但是……
脚本生成的长腿五足生物有五五开的几率出现左或右扫射偏向,这意味着它们会朝着随机方向前进。跟其他东西一样,只要没有任何变化,方向就是一致的。这是最尴尬的问题,因为它们的方向影响了战斗的很大一部分。这个问题从来没有一个很好的解决方案,但 3 个长腿敌人只有 8 个左右组合的结果,但真正最重要的方向是第一个扫射棘。使用其他一致性安全网,再通过微妙地调整其起始位置,直到它重新滚动到我想要的方向,就足以确保第一个扫射棘具有一致的方向。
直到很久以后,我才发现了搞乱场景模拟结果的最严重的罪魁祸首:哪怕只是把摄像机脚本代码从预览模式更改为用于保存屏幕截图的渲染模式,也会破坏场景输出。这意味着,花在预览将要发生的情况上的所有时间都是无用的,因为它没有按照预览显示的方式工作。这个问题并没有立即引起人们的注意,因为在我经历这个过程的前几次,渲染结果并没有明显的不同,但不可避免地,在某些时候,它变成了一个巨大的问题。幸运的是,我发现我可以一直保持渲染开启状态,但将捕获大小减少到 1 像素 × 1 像素,因此虽然它仍然在保存文件,但是它们非常小,以至于保存速度非常快,这对于预览来说已经足够了。
武器输入捕获记录射击时间、射击位置和使用的武器。对于蠕动棘来说没关系,瞄准辅助会选择附近的敌人,所以不准确仍然有效。然而,对于火箭与扫射棘来说,位置会非常错误,以至于火箭通常会瞄准蠕动棘,还经常会根本不开火,只有少数情况下会攻击预定目标。为了解决这个问题,我编辑了输入数据以删除所有火箭发射,改而使用我的事件系统直接向扫射棘发射火箭弹,这种方式更容易更改时间以确保射程内有扫射棘。
使用火焰喷射器,没有目标锁定,但对于第一次使用,跟蠕动棘战斗时,它总是工作得很好。这时战斗已经接近开始,所以它更加一致,无论如何,他们都被引导进入杀戮漏斗。在对踩踏棘的战斗中最后一次使用时,踩踏棘的位置非常不一致。它总是在角色附近的某个地方,但可以是任何方向。在大多数情况下,基于原始位置的射击看起来非常错误,因为火焰会朝着看似随机的方向前进。一旦火焰舞开始,会变得更加一致,因为我只是呆在一个小区域,在圆圈周围燃起火焰。不过,为了修复这个序列的开始,我添加了一个脚本,让火焰喷射器在前 2 秒瞄准踩踏棘的位置,然后切换回捕获的输入。只是一点烟雾和镜子。
下一个问题是最后一个踩踏棘死亡的时间。踩踏棘需要在音乐结束前死亡,但不能太提前,几秒钟内什么都不会发生。对于卵筏来说,这很容易,只需在正确的时间射击即可。对于扫射棘,这取决于火箭的时间,但有时它们会超出射程,但如果延迟几秒钟也没关系。对于踩踏棘来说,死亡时间真的很难控制。在性能捕捉中,我把时间安排得恰到好处(经过一些练习),但在重播中,时间总是不对的。通常是晚了,因为额外的蠕动棘挡住了路,或者火焰喷射器错过了更多。最终的解决方法是把那只踩踏棘的生命值乘以 10 倍,并使用脚本在正确的时间立即干掉它。无论如何,血量条都是隐藏的。如果它有正确的血量,那么到那一点造成的大致伤害仍然大致足够它死亡,所以看起来不会错得太离谱。这实际上是最后用火焰喷射器旋转的策略的一部分。这个策略确保了地上有一些火焰能造成伤害,这样踩踏棘就不会在不受伤害的情况下死亡。

和原始战斗视频一样的场景,但是显示了敌人的血条。现在是澄清的日子。
踩踏棘的死亡时间还没有脚本,所以这次它死得有点早了,但是重随机化东西可能会导致它死得太晚了。
这次角色碰巧活着,但是并不总是这样。
最后一个问题是角色的死亡。在最初的性能捕捉中,我活着走到了最后,但其中很大一部分与躲避有关。在回放这个场景时,即使没有撞到树上,角色也只有 25% 的机会活着到达终点,因为他不一定会躲避位于不同位置的踩踏,有时会直接撞到伤害。这本来可以通过升级盔甲或装备的品质来解决,但我选择了更快的方法,只是通过脚本保持角色血量不会太低。
只是为了好玩,看看遇上更大的敌人时会发生什么。

还是同样的场景,但这次敌人进化了点。视频放出来感觉有点不同……
--------------------
创造新生活 ~ StrangePan
感谢加入我们这趟旅程!我们希望我们提供了良好的体验,带你们欣赏了创造新敌人需要做的事,以及生产像上周那样的激动人心的工厂报所需的努力。
我现在也对敌人种类繁多的游戏有了更大的欣赏。尽管在现有系统基础上构建有些好处,但我仍然必须了解每个系统的功能、限制、怪癖和错误,然后弄清楚如何让它们很好地协同工作。每个更改都需要仔细测试,以确保游戏的其他部分不会中断或变得不平衡。在此过程中,我们进行了大量的重构、清理、错误修复和许多页的笔记。即便如此,很明显,以前的程序员在构建这些系统时考虑到了可扩展性、性能和(有时)代码健康。
无论如何,我想我已经见过足够多的这些令人毛骨悚然的爬虫,可以持续一辈子。我现在把 Gleba 留给五足动物。它们应得的!此外,现在我该检查我在 Vulcanus 上的工厂了。我从那里收到了一堆警报,所以我肯定希望没有什么能干扰我的铸造厂……

大家好,
为五足生物开路吧!
上周(工厂报#424)我们宣布了两种全新的敌人即将来到《异星工厂:太空时代》:踩踏棘(Stomper)和扫射棘(Strafer)这周,我们会带你走进舞台里,欣赏可爱怪物背后的特性、系统和优化。我们也会谈谈我们用于准备上周的宣布的技术。我们要涵盖广阔的范围,所以现在就开始探索吧。
--------------------
以优化开始 ~ StrangePan
从最开始,我们就知道五足生物需要重用操作蜘蛛机甲的腿的代码,但是我们也知道蜘蛛机甲有些性能问题。如果我们不能解决这些性能问题,那么把五足生物加到异星工厂里边绝对会重度损害 UPS。虽然“过早优化是一切问题的源泉”,但是我们在这个问题上没得选;为了实现五足生物,我首先得优化蜘蛛机甲。
--- 去重工作 ---
蜘蛛机甲上,最明显的性能问题每当试图越过障碍物时都会发生。在 1.1 中,每当你操纵蜘蛛大军渡江时,你都可以注意到游戏速度大幅降低了。在某些存档中,哪怕只有 10 只蜘蛛机甲,UPS 也会降低。
我注意到的第一个罪犯是,搜索空地来放脚的算法。在 1.1 中,这个算法从内向外螺旋搜索,检测某格的 1/5 是否和瓷砖(比如水)或实体碰撞了,范围最大为 16 格 × 16 格。最坏的情况下,也就是没有空地能放脚时,这个算法会进行约 6400 次碰撞检测((8 [半径格数] × 2 ÷ 0.2 [检测间距])²)。

蜘蛛机甲腿需要检测碰撞的所有位置。这个示例显示了 1600 次检测,但这还不是最坏的情况。
刚才没说,蜘蛛机甲的每条腿都会进行这个检测,而且每一游戏刻都会检测。
要想解决这个问题,首先需要意识到扫描精度太高了(0.2 格)。每一格瓷砖的边长都是这个的 5 倍,并且大多数实体的边长都至少是这个的 10 倍。因此,这个算法经常会一次又一次检测同一个瓷砖或同一个实体。所有我们不使用这个算法,而是寻找一个碰撞区域,新算法立即跳过这个碰撞器的包围盒的边缘。如果我们能记住每个碰撞器的包围盒,那么就有可能可以跳过搜索整个行和列了。

蜘蛛机甲腿现在会检测少得多的位置。这个例子显示了大约 70 个碰撞检测。另外,请注意新的扫描模式,这会比螺旋式产生更好的结果。
仅靠这一个改变,蜘蛛机甲的性能显著提高了。但是工作还没有结束。我们需要释放出更多的计算周期才能使五足生物成为可能。
--- 共享知识就是力量 ---
第二个性能罪犯是,一组蜘蛛机甲腿中每一个都会在大致相同的区域中执行相同的空地搜索。即使每条腿都执行了更少的碰撞检测,临近的几条腿仍然会重复相同的检测。这自然引发了一个问题:蜘蛛机腿能否协同工作以减少搜索呢?
要想解决这个问题,首先要意识到算法从中心开始向外搜索,并在首次发现空地时停止。这就是说,如果我们发现离搜索原点几格的位置是空地,我们就可以知道在它里边的正方形区域全都被堵得严严实实了。如果我们把这块里边的正方形区域标记为“搜索排除区”然后把它放在列表里,以后蜘蛛机腿再搜索时就可以检查这个列表,然后跳过落在区域中的位置的碰撞检测。

在搜索一片区域找到空地之后,这片区域会被记录为“搜索排除区”,等到当前游戏刻结束时清空。其它蜘蛛机甲腿在搜索空地时会跳过这整个区域。
说实话,我对这个优化没多大希望。实现非常幼稚,而且搜索排除区只在被添加时的那个游戏刻有效。但是结果出乎我的意料,这个小技巧节省了大堆时间!在又一些调整和其它优化之后,五足生物现在终于成为了可能,它们现在可以被加入扩展包了。
--- 新行走算法 ---
最后的性能罪犯涉及到了蜘蛛机甲要踏出哪条腿。你也发现了吧,我们现在所做的所有优化都是针对空位搜索的,但是还有一个显著的低效处:每条腿——无论是正在移动还是等着旁边的腿动完——都会在每一游戏刻搜索空地,只要蜘蛛机甲还在移动。在蜘蛛机甲决定踏出腿时,每条腿都会根据需要踏出的距离获得一个分数,并且只有分数最高的腿会踏出。如果这条腿正在移动,那么结果只会被丢弃。
显然这会导致许多浪费掉的计算,可是我们也需要确保蜘蛛机甲总是要朝着目标移动。如果没有这个估分系统,需要踏出一大步的腿经常会卡住,因为旁边的腿可能会重复小步走。我们需要一个高效的方法来确保每条腿都有相等的机会来移动。
这个问题在五足生物出现于早期开发几个月后才被解决。在添加五足生物时,旧的估分动腿算法不再合乎需要了。Earendel 想完全控制五足踏出的顺序,以便这些外星生物的行走模式更独立、更有机。我也趁这个机会大修了整个蜘蛛机甲行走算法,并且实现了最后的大型优化。
有五只脚的生物该怎么样行走呢?地球上没什么例子,但是 Earendel 还是想到了个好办法。为了维护平衡和稳定,五足生物以五个点的星星模式行走。

在所有第 1 组中的腿移动完后,第 2 组的腿会开始移动。在它们也移动完后,第 3 组也开始移动。然后是第 4 组和第 5 组。
然而也有些五足生物喜欢倒着数。
为了实现这个,每条蜘蛛机甲或者五足生物上长的腿都以数字标号,称为“行走组”。在行走时,当前行走组中的一条或多条会被选中,被选中的腿会踏出,搜索清晰的目标,然后向那里移动。不再有浪费的搜索。在当前行走组中的所有腿都踏出落地过后(或者移动超过了某些特定的阈值,但是我今天说不清楚了),这样的过程会在下一行走组上重复。这个新系统显著减少了碰撞检测的数量,同时给予了设计师对蜘蛛机甲和五足生物行走动画的前所未有的控制能力。
在这三个大型优化,以及其它几个我没时间讲的小优化以后,蜘蛛机甲对 UPS 的抢夺大大减少了。在一个用于压力测试蜘蛛机甲移动的存档里,我测出的结果是 每次更新减少了 53.56% 的毫秒(约 2.15 倍速度)。所以在《异星工厂 2.0》里,玩家可以操控更大规模的蜘蛛机甲大军,而不会毁掉 UPS(结果可能会改变)。
--------------------
新敌人,新功能 ~ StrangePan
随新敌人而来的是新功能!随新功能而来的是新引擎特性。接下来我会讲讲影响了五足生物设计的游戏设计决策,以及我们为了实现它们所做的系统更改。
--- 跨过障碍物的寻路 ---
我们不久之前刚知道了,把 Gleba 上的工厂包裹在城墙里会极大拖累游玩体验。所以我们设计了一种不关心我们那些弱小的城墙的敌人。相反,玩家只能用现有的炮塔和新的火箭炮塔来抵御五足生物的攻击。
为了使这个机制能工作,我们需要允许寻路器支持跨江,并且能忽略障碍物。幸运的是,我可以扩展已有的虫子寻路系统(工厂报#317),并加入一些新功能。
在执行抽象(即,块级)寻路时,只记录块边缘上可走过的位置不再足够。额外的,块边缘的不可走过的瓷砖还需要记录到最近可走过的瓷砖(最多两块不同的陆地之一)的距离。我们之后在两个块之间连接多块陆地时会用到这个信息。
两个瓷砖之间的距离按东西方向距离和南北方向距离的最大值来算。

一个块内,所有可走过的瓷砖都会被分组放在一起。如果它们之间的距离小于最大允许的间隔,它们就会被分到同一组。块边缘的瓷砖会记住到这个块内最近的可走过的瓷砖的距离。
(注意:这张截图并没有显示边缘瓷砖位于多于一块陆地范围内的情况。)
接下来是基础(即,瓷砖级)寻路时,每个节点都必须记住它是否可以走过,并且总是指回上次已知的可走过节点。这允许算法向不可走过的区域(水、墙壁和其它实体)深入搜索,以找到虫子不可能走过的路径。

节点向不可走过的地形搜索一定距离,总是指回上次已知的可走过瓷砖。典型的寻路过程中会搜索比这张示例图中多得多的节点。
不连接的寻路在工厂 2.0 中可以通过模组 API 访问,虽然原版游戏里还完全没用到它。
--- 空气中的孢子 ---
现在那些墙拦不住五足生物了,但是玩家有问题了:五足生物仍然可以从任何方向攻击你的工厂的任一部分!工程师除了把工厂用更多更强的炮塔环绕以外还能怎么办?这也不是我们想让玩家做的策略,所以我们需要使这个策略不必要。为了完成这个需求,我们改变了 Gleba 上污染这个机制的工作方式。
敌方单位(虫子和五足生物之类的)往往基于污染选择攻击目标。在攻击队集结时,它们找到附近污染最大的区块,然后向其中最近的进发,并攻击那个块中产生污染的设施。
Gleba 上的污染与其它星球相比截然不同。有些在 Nauvis 上产生大量污染的设施在 Gleba 上不会产生任何污染,而在 Gleba 上产生大量污染的设施在 Nauvis 上不产生污染。我们怎么样才能让五足生物在不改变虫子行为的情况下优先攻击你的工厂的特定部分?
工厂 2.0 引入了污染类型。模组可以添加不同类型的污染。机器、工厂、虫巢和瓷砖都可以配置,以产生或吸收每种污染类型的不同数量。然而,因为性能原因,每个表面上只能启用一种污染类型。对于启用了污染的新的和现存的表面,“污染(Pollution)”是默认的污染类型。在《异星工厂:太空时代》中,Gleba 上孢子(Spores)会取代污染。每种污染类型都有自己的名字、吸收率、排放率、地图颜色,还有更多。
在新工具的加持下,我们终于可以调整 Gleba 以使五足生物把进攻聚焦于一些特定的建筑上,同时忽略掉工厂的主体部分。一般是这样的,除非你把它们惹火了。
--- 重新设计腿 ---
最后,五足生物的腿需要返工。毕竟它们是碳基生物,我们想最小化每条腿伸展和压缩的程度,而强调膝盖和臀部的弯曲。这使得这种生物看起来更逼真,并且不那么像机器人。
我们甚至让腿基于相对于身体的旋转更新精灵图像。这件事不太容易干,原因是异星工厂有透视,而且角度有些奇怪,但是 Fearghall 和 Earendel 确实做到了。最终的效果非常好,这些生物像是现实中存在的一样。方法是,保持光线角度一致,并且总是考虑玩家的视角。

踩踏棘和扫射棘的腿会试图在臀部和膝盖处弯曲,如果不能的话才会伸缩。精灵图像基于旋转改变。
因为蜘蛛机甲和五足生物共享一大堆代码,现在蜘蛛机甲的外观也稍微更新了一点。现在腿不那么僵硬了,更容易弯曲了,并且,说实话,跟之前相比更吓人。蜘蛛机甲仍然是显然的机器人样,所以我相信我们设法保持了它们符号的外观,并且给他们做了个小小的整容。

左:1.1 中蜘蛛机甲行走动画。右:2.0 中蜘蛛机甲行走动画。
--------------------
舞台里 ~ Earendel
在我们准备决定这周工厂报的内容时,我开玩笑说我可以写一篇关于上周工厂报的工厂报。额……你们好像真喜欢这个点子啊。所以,我们才在这里相见。
这个点子挺有意思,粉丝们喜欢欣赏舞台里。展示视频制作方法不会剧透游戏秘密,同时也可以展示舞台里内容。
大多数工厂报,(比如这一个)在发布的前一周就写好了。通常我会再提前一周,这样压力小一点。但是对于关于敌人的工厂报,我会提前好几周,以确保从产品进程中消除所有的“未知”。
我使用某个随机生成的地形作为“角斗场”,放一些敌人,然后把它导出为场景(每个人用地图编辑器也可以办到)。
在之前的视频中,我编写了一些性能捕捉代码,可以捕捉玩家的角色奔跑操作和坦克操作(转弯、加速等)。我扩展了这个系统,现在它可以捕获玩家的武器输入,基本就是选中哪个武器,开火与否,以及目标位置。
这是玩家装备栏。武器伤害点到 8 级。


我记录了在角斗场中与敌人战斗时的玩家输入,然后把这些输入应用回场景,再运行一次,然后……结果不一样。
所有玩家输入都是一样的,但是敌人干的事不一样。有些徘徊到了不一样的地方,因此在不同的时间攻击。在最开始的捕获版本中,地图上有一只扫射棘,而另一只在卵筏刚被打破之前出现。在脚本控制角色的版本中,没有生成第二只扫射棘,而是新生成了一只蠕动棘。踩踏棘因某些原因加入战斗的时间早了很多。角色被滑翔的蠕动棘击中从而减缓速度的时间也不一样,所以移动脱轨了,最终角色卡进树里被踩死了。
所以,开始不是很理想。再次重运行场景,确实角色卡进树里被踩死了,跟之前一模一样。所以至少不是每次所有结果都是随机的。我试图找出是什么东西导致了或者不会导致场景的结果被随机化。
在一些测试后我意识到,场景重播是不可能会跟输入记录一样的,因为录制代码和应用程序代码本身都会改变结果,而且没办法解决这个问题。这有点烦人,但也不是什么大麻烦,因为我知道角斗场周围的一大堆景观都会发生变化,最有意义的办法是,添加其它可靠的工具来使模拟更稳定。
最迫在眉睫的问题使,玩家总是脱轨。显然这种事情会简单地发生,因为滑翔蠕动棘会减慢速度。在躲开滑翔蠕动棘时,我有很多次差点就撞上了,所以结果是一大堆可变量。任何对玩家移动的改变也会影响敌人的路径,所以整个战斗会被拉向预料之外的方向。


在一些新地形被完成时,它们被加入了地图,但是这会“随机化”敌人的行为,在不同的时间减慢角色,所以重播出的所有东西都不一样了,并且玩家最终的路径看起来也不一样了。
上:原始移动路径。下:使用同样的操作但是在不同的时间被减速导致的路径。
我对此的解决方法是,测量角色与预期路径的距离,如果它们之间偏差得太多,那么强制朝某个方向跑以纠正。无论效果如何,玩家仍会被减慢速度,但这意味着玩家可以走捷径或跳过闪避动作以回到正确的路径上。
这种追踪玩家的移动的方法给了我一种替换摄像机系统的方法。不像之前的视频中摄像机是全景的,平滑地覆盖了整个环境,现在的这个视频会跟随角色的不规则的运动而移动。我们之前使用贝塞尔控制点系统来控制摄像机,要匹配玩家运动时简直就是灾难,尤其是,重新记录的时候,而且基本一定会遇上这种情况。
我的新系统从性能捕获中获取注册的角色位置,并且使用一个宽松的过滤器来平滑那条路径,本质上使得每个点成为之前和之后的点的平均值。

实际的角色移动以白色显示。用于摄像机的平滑后的路径以蓝色显示。
这种方法给出了漂亮的相机运动,但是并不能紧密地跟随操作,因为它总是把角色作为中心。然后我添加了次级目标系统,其中虚拟点可以移动至目标敌人,并且摄像机会在平滑后的路径和次级目标之间维持某个百分比。这个系统使用虚拟点而不是敌人本身,这是为了在敌人死了之后以有限的速度移动至新的目标。额外的控制层可以决定相机被拉向次级目标的强度有多少。在视频的开头次级目标是卵筏,但是影响被设为 0。

所有的调试数据都启用:平滑后的主摄像机路径以蓝色显示。次级目标路径以橙色显示。实际相机结果以白色显示。
紫色方块是相机边界。
所有的位置和时间都比平时更重要,因为战斗中的一切都是随着音乐编排的。卵筏在节拍开始之前就死了。当有一种忧郁的音调,并且音乐开始混乱起来时,扫射棘死了。踩踏棘也需要在特定时间死亡(稍后会详细介绍)。
一路上,我了解到什么会改变场景结果以及敌人会受到怎样的影响。
只要没有 任何 变化,敌人的行为就会始终如一。换掉图形是可以的,但改变图形移位就不行了。移动敌人,添加或移除一棵树,改变瓷砖,为角色添加一些弹药,甚至添加装饰品,都会破坏结果。许多操作也会破坏结果。
刷怪器(卵筏)可能会也可能不会生成一个单位,如果生成了,也可能是不同类型的单位。有一次它随机生成了一个踩踏棘,所以有 2 个踩踏棘。运气不太好。该场景是带有一组视频场景的模组的一部分,因此为了解决这个问题,我更新了场景模组,以便卵筏只能生成蠕动棘。
任何五足生物都可能向随机方向徘徊,因此如果战斗开始,它们可能会超出“呼救”的范围,并且永远不会出现在战斗中。
我试图在敌人出现在摄像机视野之前将他们传送到正确的位置,但我很快发现没法传送长腿敌人,它们会在一帧内像橡皮筋一样弹回之前的位置。
取而代之的是,我必须通过 LUA 脚本将五足生物生成到正确的位置,然后它们就会出现在摄像机视野中。这使位置更加一致。但是……
脚本生成的长腿五足生物有五五开的几率出现左或右扫射偏向,这意味着它们会朝着随机方向前进。跟其他东西一样,只要没有任何变化,方向就是一致的。这是最尴尬的问题,因为它们的方向影响了战斗的很大一部分。这个问题从来没有一个很好的解决方案,但 3 个长腿敌人只有 8 个左右组合的结果,但真正最重要的方向是第一个扫射棘。使用其他一致性安全网,再通过微妙地调整其起始位置,直到它重新滚动到我想要的方向,就足以确保第一个扫射棘具有一致的方向。
直到很久以后,我才发现了搞乱场景模拟结果的最严重的罪魁祸首:哪怕只是把摄像机脚本代码从预览模式更改为用于保存屏幕截图的渲染模式,也会破坏场景输出。这意味着,花在预览将要发生的情况上的所有时间都是无用的,因为它没有按照预览显示的方式工作。这个问题并没有立即引起人们的注意,因为在我经历这个过程的前几次,渲染结果并没有明显的不同,但不可避免地,在某些时候,它变成了一个巨大的问题。幸运的是,我发现我可以一直保持渲染开启状态,但将捕获大小减少到 1 像素 × 1 像素,因此虽然它仍然在保存文件,但是它们非常小,以至于保存速度非常快,这对于预览来说已经足够了。
武器输入捕获记录射击时间、射击位置和使用的武器。对于蠕动棘来说没关系,瞄准辅助会选择附近的敌人,所以不准确仍然有效。然而,对于火箭与扫射棘来说,位置会非常错误,以至于火箭通常会瞄准蠕动棘,还经常会根本不开火,只有少数情况下会攻击预定目标。为了解决这个问题,我编辑了输入数据以删除所有火箭发射,改而使用我的事件系统直接向扫射棘发射火箭弹,这种方式更容易更改时间以确保射程内有扫射棘。
使用火焰喷射器,没有目标锁定,但对于第一次使用,跟蠕动棘战斗时,它总是工作得很好。这时战斗已经接近开始,所以它更加一致,无论如何,他们都被引导进入杀戮漏斗。在对踩踏棘的战斗中最后一次使用时,踩踏棘的位置非常不一致。它总是在角色附近的某个地方,但可以是任何方向。在大多数情况下,基于原始位置的射击看起来非常错误,因为火焰会朝着看似随机的方向前进。一旦火焰舞开始,会变得更加一致,因为我只是呆在一个小区域,在圆圈周围燃起火焰。不过,为了修复这个序列的开始,我添加了一个脚本,让火焰喷射器在前 2 秒瞄准踩踏棘的位置,然后切换回捕获的输入。只是一点烟雾和镜子。
下一个问题是最后一个踩踏棘死亡的时间。踩踏棘需要在音乐结束前死亡,但不能太提前,几秒钟内什么都不会发生。对于卵筏来说,这很容易,只需在正确的时间射击即可。对于扫射棘,这取决于火箭的时间,但有时它们会超出射程,但如果延迟几秒钟也没关系。对于踩踏棘来说,死亡时间真的很难控制。在性能捕捉中,我把时间安排得恰到好处(经过一些练习),但在重播中,时间总是不对的。通常是晚了,因为额外的蠕动棘挡住了路,或者火焰喷射器错过了更多。最终的解决方法是把那只踩踏棘的生命值乘以 10 倍,并使用脚本在正确的时间立即干掉它。无论如何,血量条都是隐藏的。如果它有正确的血量,那么到那一点造成的大致伤害仍然大致足够它死亡,所以看起来不会错得太离谱。这实际上是最后用火焰喷射器旋转的策略的一部分。这个策略确保了地上有一些火焰能造成伤害,这样踩踏棘就不会在不受伤害的情况下死亡。

和原始战斗视频一样的场景,但是显示了敌人的血条。现在是澄清的日子。
踩踏棘的死亡时间还没有脚本,所以这次它死得有点早了,但是重随机化东西可能会导致它死得太晚了。
这次角色碰巧活着,但是并不总是这样。
最后一个问题是角色的死亡。在最初的性能捕捉中,我活着走到了最后,但其中很大一部分与躲避有关。在回放这个场景时,即使没有撞到树上,角色也只有 25% 的机会活着到达终点,因为他不一定会躲避位于不同位置的踩踏,有时会直接撞到伤害。这本来可以通过升级盔甲或装备的品质来解决,但我选择了更快的方法,只是通过脚本保持角色血量不会太低。
只是为了好玩,看看遇上更大的敌人时会发生什么。

还是同样的场景,但这次敌人进化了点。视频放出来感觉有点不同……
--------------------
创造新生活 ~ StrangePan
感谢加入我们这趟旅程!我们希望我们提供了良好的体验,带你们欣赏了创造新敌人需要做的事,以及生产像上周那样的激动人心的工厂报所需的努力。
我现在也对敌人种类繁多的游戏有了更大的欣赏。尽管在现有系统基础上构建有些好处,但我仍然必须了解每个系统的功能、限制、怪癖和错误,然后弄清楚如何让它们很好地协同工作。每个更改都需要仔细测试,以确保游戏的其他部分不会中断或变得不平衡。在此过程中,我们进行了大量的重构、清理、错误修复和许多页的笔记。即便如此,很明显,以前的程序员在构建这些系统时考虑到了可扩展性、性能和(有时)代码健康。
无论如何,我想我已经见过足够多的这些令人毛骨悚然的爬虫,可以持续一辈子。我现在把 Gleba 留给五足动物。它们应得的!此外,现在我该检查我在 Vulcanus 上的工厂了。我从那里收到了一堆警报,所以我肯定希望没有什么能干扰我的铸造厂……