流体系统
我们最近在开发的类似异星工厂的游戏中,一个重要的物流子系统就是流体系统。我个人觉得,它是所有子系统中最难实现的一个。
Factorio 的流体系统也经历过多次改动。在开发日志上有记载的就有三次:New fluid system,Fluid optimisations,New fluid system 2。作为一个 5 年近 2000 小时的老玩家,我感觉的到流体系统的修改一直在做,不只这三次;而且直到今天,流体系统在游戏中依然会出现一些反直觉的行为。
一个好的流体系统非常难兼顾高效和拟真。在 Factorio 的仿制品戴森球计划上线时,我饶有兴趣的想看看它是怎么做流体管道的,但让我失望的是,它几乎把流体系统砍掉了。
我在 Factorio 中非常喜欢流体系统的伴生玩法。所以,在我们的游戏中,即使我没有做传送带,也要做一个流体子系统出来。我们的设计直接参考了前面提到的几篇 blog ,并加入了一些我自己的想法。
一个好玩的流体系统要解决的几个问题:
- 流体系统和电力系统要有所区别,要体现出流体流动的过程,放入流体网络的流体,不应该瞬间遍布整个网络。
2.管道的分叉要公平。如果上游水流流到 T 口分叉,两个分叉如果管道是一样的,就应该均匀分流;合流也有同样的公平问题,分叉合流吸收支流的量也应该平均。
管道的流动规则只应该和管道内的液体量有关,不应该和管道建造次序(管道 id )相关。
流体在管道中流动,总量必须严格不变,不能因为浮点运算的缘故增加或减少。
引入抽水泵导致流体网络形成环路时,算法不应死锁导致流体无法流动或其它反直觉行为。
管道有升级空间,升级可以带来玩法上的改变。
Factorio 在流体系统改进的过程中,收集了大量玩家的建议,其中不乏专业人士。按 blog 的原话说,“differents kinds of engineers (mechanical, CS, electrical, ...), mathematicians, physicists, and even people with real pipes hands on experience.” 可见这个问题即有趣,又不那么简单。
我倒不想从真实物理角度去模拟流体在管道中的流动,因为它不够简单,而且很难保证确定性。我需要的是一个简单,但大致符合直觉,好玩的系统。
我在 Factorio 的新手阶段,完全搞不懂流体系统的运作原理。但它的流体系统是符合直觉的,所以并不妨碍我游戏。换为制作者身份,就不能再搞不清楚了。
比如:为什么在 Factorio 里,水管越长流动就越慢,必须加泵来改进流速?
这个问题体现了流体系统和传送带系统的不同,引出了不同的物流问题需要玩家解决。
传送带无论多长,只要你塞满传送带,那么你在传送带一段放入一个物件,同时就能从另一端取走一个物件。传送带的速度决定的仅仅是它单位时间能传送的物品个数,和你铺设传送带的长度无关。
但管道则不然,管道的长度会决定你可以通过管道以怎样的速度传输流体。游戏中,如果管道过长,玩家必须在一定数量的管道间加一个抽水泵来维持流速。
这是为什么呢?我玩了很久才明白。
因为,一个固定在一端生产,另一端消耗的管道,是永远塞不满的。流体主要靠势能发生流动。管道内流动的稳定状态是一个斜面。没有水泵的管道,当你把它视为一个整体时,这个斜面的坡度就越平缓,流速就越慢。而当坡度越平缓,管道的入水口留给进水的空间就越小,游戏中的表现就是,入口几乎是满的,一个 tick 塞不了多少水到系统内,而出口几乎是空的,一个 tick 流不了多少水出去。
想明白这点,就可以设计出一个简单的流体流动算法。
如果不考虑抽水泵的因素,我觉得最简单的算法就是把流体看成是静态的,在每个时刻,流体的流动方向和流速仅和每节管道中的水位有关。流体永远从高水位向低水位流动。水位差决定了一节管道大约可以向临近的管道流多少流体。
这样的算法比 Factorio 的还要简单,Factorio 除了考虑水位,还考虑了当前的流速,即上个 tick 液体的流动量。我试过一版类似的算法,感觉还是过于复杂,很难同时满足前面列出的条件。
一开始的想法是,每节管道最好可以同时运算,相互不干扰。这样更方便后期做并行优化。尝试过两版实现后,我放弃了这个想法。因为当管道有多个输入和输出时,独立的运算很难保证液体在流动后不超出管道容量的限制。
即,若一节管道可以装 200 单位的水,如果它独立计算,很难保证它内部的流体在减去流出量且加上流入量后,总量不超过 200 。在我玩的另一个游戏“缺氧”中,设计了容器的压力值和承压能力限制,允许流体容器偶尔超过它的容积,但增加它的压力直到破裂。我觉得虽然玩法丰富了,但不确定性更多,bug 也变得更多。比如在缺氧中,你就可以用特殊的技巧制作一个无限存水的容器出来。
我最终选择对流体管道排序,按次序来处理流动。
在不考虑水泵时,遵循以下步骤:
每节管道计算当前水位,和邻接管道水位做比较,决定当前 tick 的流动方向。在接收流体的管道上记录每个上游管道可能输入的流体数量(根据水位差计算出来),记作预留空间。
根据流动方向做一次拓扑排序,从最下游排到最上游。每个 tick 记录下排序结果,cache 起来供下个 tick 使用。一旦在处理过程中发现上下游比上个 tick 反向,就局部重排。
根据排序结果,依次处理每节管道的流出。流出量不应超过接收方为它的预留空间(步骤 1 计算,同时在本步骤中调整)。如果有多分支流出,流体数量小于下游预留空间总量,就按预留空间比例分配。流出后,计算剩余空间,和为上游预留空间相比较,如果剩余空间总量小于预留空间总量,则按比例重新分配预留空间。
为了保证计算的确定性,我没有采用浮点数,而全部采用用整数计算。但和 UI 表现相比,扩大了 100 倍。它实际上是一种定点数表示。在上述步骤中需要按比例分配时,不做小数切分,最小单位为 1 。同等水位差分流时,多个支流流量差不超过 1 。
加入水泵是相对简单的。
水泵的抽水是无视水位差的,按泵速把水泵输入端的流体在泵速和水泵容积的限制下把尽可能多的流体抽到泵内。然后将泵视为普通管道,让其中的流体按上面的步骤依照水位差自然流动。
Comments
Posted by: fictiony | (6) February 8, 2022 06:07 PM
Posted by: riczxc | (5) February 2, 2022 08:05 AM
Posted by: 军 | (4) January 28, 2022 10:10 AM
Posted by: myss77 | (3) January 12, 2022 06:59 PM
Posted by: whong | (2) January 11, 2022 03:17 PM
Posted by: coco | (1) January 10, 2022 04:18 PM