« 难产的 Lua 5.4.4 | 返回首页 | 我们需要一个怎样的动画模块 »

流体系统

我们最近在开发的类似异星工厂的游戏中,一个重要的物流子系统就是流体系统。我个人觉得,它是所有子系统中最难实现的一个。

Factorio 的流体系统也经历过多次改动。在开发日志上有记载的就有三次:New fluid systemFluid optimisationsNew fluid system 2。作为一个 5 年近 2000 小时的老玩家,我感觉的到流体系统的修改一直在做,不只这三次;而且直到今天,流体系统在游戏中依然会出现一些反直觉的行为。

一个好的流体系统非常难兼顾高效和拟真。在 Factorio 的仿制品戴森球计划上线时,我饶有兴趣的想看看它是怎么做流体管道的,但让我失望的是,它几乎把流体系统砍掉了。

我在 Factorio 中非常喜欢流体系统的伴生玩法。所以,在我们的游戏中,即使我没有做传送带,也要做一个流体子系统出来。我们的设计直接参考了前面提到的几篇 blog ,并加入了一些我自己的想法。

一个好玩的流体系统要解决的几个问题:

  1. 流体系统和电力系统要有所区别,要体现出流体流动的过程,放入流体网络的流体,不应该瞬间遍布整个网络。

2.管道的分叉要公平。如果上游水流流到 T 口分叉,两个分叉如果管道是一样的,就应该均匀分流;合流也有同样的公平问题,分叉合流吸收支流的量也应该平均。

  1. 管道的流动规则只应该和管道内的液体量有关,不应该和管道建造次序(管道 id )相关。

  2. 流体在管道中流动,总量必须严格不变,不能因为浮点运算的缘故增加或减少。

  3. 引入抽水泵导致流体网络形成环路时,算法不应死锁导致流体无法流动或其它反直觉行为。

  4. 管道有升级空间,升级可以带来玩法上的改变。

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 也变得更多。比如在缺氧中,你就可以用特殊的技巧制作一个无限存水的容器出来。

我最终选择对流体管道排序,按次序来处理流动。

在不考虑水泵时,遵循以下步骤:

  1. 每节管道计算当前水位,和邻接管道水位做比较,决定当前 tick 的流动方向。在接收流体的管道上记录每个上游管道可能输入的流体数量(根据水位差计算出来),记作预留空间。

  2. 根据流动方向做一次拓扑排序,从最下游排到最上游。每个 tick 记录下排序结果,cache 起来供下个 tick 使用。一旦在处理过程中发现上下游比上个 tick 反向,就局部重排。

  3. 根据排序结果,依次处理每节管道的流出。流出量不应超过接收方为它的预留空间(步骤 1 计算,同时在本步骤中调整)。如果有多分支流出,流体数量小于下游预留空间总量,就按预留空间比例分配。流出后,计算剩余空间,和为上游预留空间相比较,如果剩余空间总量小于预留空间总量,则按比例重新分配预留空间。

为了保证计算的确定性,我没有采用浮点数,而全部采用用整数计算。但和 UI 表现相比,扩大了 100 倍。它实际上是一种定点数表示。在上述步骤中需要按比例分配时,不做小数切分,最小单位为 1 。同等水位差分流时,多个支流流量差不超过 1 。

加入水泵是相对简单的。

水泵的抽水是无视水位差的,按泵速把水泵输入端的流体在泵速和水泵容积的限制下把尽可能多的流体抽到泵内。然后将泵视为普通管道,让其中的流体按上面的步骤依照水位差自然流动。

Comments

考虑管道的虹吸效应么?感觉现实中管道里水的势能应该只取决于两侧的压强差,和管道长度没啥关系(不考虑管壁粘滞系数的情况下)
流体仿真可能会涉及high index differential algebraic equation 更详细的例子可以参见Modelica的建模。 当然,如果只要符合直觉,为了计算方便,不妨在预留空间里添加弹性助力(当然最后系统加上弹性后,要确保弹簧振子的系统频率要低于你tick的频率)。 无论怎么建模,建议统计一下系统的总能量。没有必要做到能量守恒,但至少在没有水泵的情况下,系统能量不要自己增加,否则就会很不现实。
云风大哥最近都是喜欢凌晨2.3点发文呀
长知识了,“势能”,“斜面“的那一段很有趣!可能需要参考各种物理公式才能模拟出水流吧?
缺氧的压力系统确实很有趣,但是bug也显然易见,环境中的压力不是均匀分布的,所以它的排气(水)设施只能假定自身的某一格为“传感器”,来判断压力值,那么只需要让那一格里的物质不是他要检测的东西,压力传感器就无法正常工作了。 复杂的系统难免会产生bug,但是也增加了游戏的趣味,我觉得玩家要遵循游戏规则,而不是特意利用漏洞进行游戏也是玩家的乐趣(就像是跑团游戏玩家应当代入角色做好RP,才能享受游戏乐趣。)所以游戏开发者在这种问题上面还是以开发更有乐趣的系统为优先比较好吧,前提是别整崩溃了,玩着玩着闪退掉了。
期待你的游戏~

Post a comment

非这个主题相关的留言请到:留言本