快进播放模拟

Playing a simulation in fast-forward

我正在开发一个简单的游戏,用户可以在一个 10x10 网格的扇区中创建结构。有些结构产生资源,有些结构消耗资源。该部门本身可能包含任何结构之外的一些资源。生成器和消费者是相关的。例如,一口井可能在生产水,然后一个分离器在消耗水并制造氢气和氧气,而炼油厂在消耗氢气和氧气并制造火箭燃料等。

它们生成或消耗资源的速率可能因结构而异——我称之为滴答率。每次消费者勾选时,它都会首先尝试从该部门中围绕它的结构中提取这些资源。如果不够,它将尝试从扇区的存储中获取它们。如果还不够,结构将停止。结构将它们生成的资源保存到某个最大值。一旦它们满了,它们将不会产生更多,直到一些被消耗掉。如果一个结构被停止,它也不会产生更多的资源,但它已经拥有的资源仍然可以被另一个相邻的结构使用。

有规律的现象并不少见。例如,如果井的速度很慢,当井中的水用完时,分离器将关闭,然后当分离器中的气体用完时,炼油厂将关闭。然后当井再次产生时,一切都会恢复。

当用户在玩一个扇区时,我以扇区结构的最短刻度率分辨率连续勾选该扇区。这很好用。伪代码如下所示:

const numTicks = (Date.now() - lastTickTime) / shortestTickTime;
let currentTickTime = lastTickTime;
for (i = 0; i < numTicks; i++) {
    currentTickTime += shortestTickTime;
    // check the consumers - all structures that are consumers
    for (curConsumer of consumers) {
       if (curConsumer.isRunning && 
           (currentTickTime - curConsumer.lastTickTime >= curConsumer.tickRate) {
           ... check surrounding structures for resources
           if (curConsumer.stillNeedsResources) {
               ... check sector for researches
           }
           if (curConsumer.stillNeedsResources) {
              ... no resources available
              curConsumer.isRunning = false;
           }
        }
        // check the generators - all structures that are generators
        for (curGenerator of generators) {
           if (curGenerator.isRunning && 
               (currentTickTime - curGenerator.lastTickTime >= curGenerator.tickRate) {
               ... add the generated resources
           }
        }
    }
}

现在我正在处理这样的情况:用户在长时间离开后返回某个扇区 - 比如说几天 - 已经过去了数百或数千个滴答声。如果我只是天真地尝试播放所有滴答声,可能需要几秒钟或几分钟才能完成。

我想知道是否有任何提示或技巧可用于此类模拟,以便在不播放每个报价单的情况下计算净变化。或者,或者,如果我可以对模拟进行更改以使其更易于计算。谢谢!

第一步:将原始数据转换成"graph of nodes"形式,其中每个节点代表一台机器,生产者在最底层,消费者在最上面。例如,它可能看起来像:

          |
        (fuel)
          |
       Refinery
        /   \
       /     \
(hydrogen) (oxygen)
       \     /
        \   /
       Splitter
          |
       (water)
          |
         well

注意:如果一台机器有输出缓冲区(或输入buffer/s);那么这些缓冲区应该是单独的节点。例如,如果一切都有输出缓冲区,它可能看起来像这样:

          |
        (fuel)
          |
  Refinery output buffer
          |
        (fuel)
          |
       Refinery
        /   \
       /     \
(hydrogen) (oxygen)
    |         |
 Hydrogen   Oxygen
  output    output
  buffer    buffer
    |         |
(hydrogen) (oxygen)
       \     /
        \   /
       Splitter
          |
       (water)
          |
   Well output buffer
          |
       (water)
          |
         well

第 2 步:通过(最初)自下而上(生产者到消费者)确定 "current steady state average rates"。例如,如果一口井每 4 个刻度产生 1 个单位的水,那么假设它确实平均每个刻度产生 0.25 个水;如果分离器每 3 个刻度可以将 1 个单位的水转化为 2 个单位的氢气和 1 个单位的氧气,那么这是最大值。 0.333 水转化为 0.666 氢气和 0.333 氧气的速率,但您已经知道井的产水速度不够快,并且可以确定分离器实际上会消耗 0.25 水来产生 0.5 氢气和 0.25 氧气。

请注意,如果生产者生产过多,您将需要回溯。例如,如果一口井每 2 ticks 产生 1 个单位的水,那么您会假设它确实平均每 ticks 产生 0.5 水;如果分离器每 3 个刻度可以将 1 个单位的水转化为 2 个单位的氢气和 1 个单位的氧气,那么您就知道井产生的水多于分离器消耗的水,并且必须返回井中并将其输出固定到每刻 0.333 水。

第 3 步:确定下一件事情发生 之前的时间(多少刻度),这将改变 "current steady state average rates"。如果资源可能耗尽(例如,水井枯竭),您需要知道什么时候会发生。同样,如果 "storage vessel"(输出缓冲区、输入缓冲区、水箱、燃料储藏箱……)变满或变空,那么您也需要知道什么时候会发生这种情况。这完全基于您拥有的 "current steady state average rates" - 例如如果水井的输出缓冲区为空,并且以每滴答 0.5 水的速率(从井中)获取水,并且以每滴答 0.33 水的速率失水(到分离器);然后你可以计算出它存储的数量以“0.5 - 0.33 = 0.17 per tick”的速率增加,并且(结合缓冲区的容量)计算输出缓冲区何时变满。

注意"how many ticks until the next thing happens"也必须限制在你想停止模拟的时候。

第 4 步:将时间提前到下一件事情发生。 这主要意味着使用 "current steady state average rates" 更新存储在 "storage vessels" 中的东西的数量你有;然后修改任何信息(例如将井设置为 "not functioning, ran dry")。

第 5 步:重复前面的步骤,直到到达您想要的时间。这就是“if(current_time < stop_time) goto step 2”。

第 6 步:用所有事物的最终状态更新世界。这主要是第 1 步的逆过程 - 在 "storage vessels" 中设置数量,将资源标记为耗尽等

备注:

  • 您可能需要添加 "transport" 作为节点类型。例如,如果您有传送带将水从井中输送到分流器,那么它可以像 "storage vessel" 一样模拟,但有滞后时间(例如,如果传送带是空的并且物品开始放在传送带上,那么它会在物品到达另一端之前需要 "length * speed" 时间)。

  • 如果你愿意;您可以将 "breakdown" 添加到游戏中(例如,分离器出现故障并需要维修的可能性很小)。这只是第 3 步要考虑的额外事项。

  • 别忘了您可以修改游戏设计以使其更容易。首先,我会避免反馈回路(例如,如果来自反应堆的燃料被送入发电机以产生由分离器消耗的能量……),因为这会使一切变得更加困难。我也很想避免 "variable speed" 事情(例如,矿工在矿田和下车点之间行进,矿工行进的距离随着矿田更近部分的消耗而增加,因此 "average ore per tick" 正在增加并且永远不会恒定)。

  • 不要忘记这是一款游戏 - 它不必 100% 完美准确,只需要有足够的说服力来愚弄玩家。如果它是 "slightly wrong"(例如,输出缓冲区应该有 23 个项目但只有 21 个项目)很可能没​​有人会注意到。

  • 取决于其他细节;你可能会(也可能不会)考虑提前停止并切换到 "simulate one tick at a time"。例如,如果您需要模拟 12345600 个刻度,那么您可以使用我为前 12345500 个刻度描述的方法,然后使用您已经拥有的方法来完成最后 100 个刻度。这有助于使某些东西(例如物品在传送带上的位置)看起来更真实。