10

在 Screeps 中,每个玩家对 CPU 的使用都是有限的,但是该功能的文档并没有说明强制执行此功能的方式足以编写 CPU 限制健壮代码。我考虑了以下四种可能性:


1、玩家的循环永不中断。

在一种极端情况下,玩家的内存反序列化、主脚本执行和内存重新序列化永远不会中断,超过 CPU 限制仅仅意味着玩家的循环将在后续滴答中被跳过,直到 CPU 债务被偿还。在这种情况下,CPU 限制健壮的代码并不是绝对必要的,但是检测玩家的循环何时被跳过并可能开始更有效地做事仍然很有用。这可以使用如下代码轻松实现:

module.exports.loop = function()
{
  var skippedTicks = 0;

  if ( 'time' in Memory )
  {
    skippedTicks = Game.time - Memory.time - 1;
  }

  // Main body of loop goes here, and possibly uses skippedTicks to try to do
  // things more efficiently.

  Memory.time = Game.time;
};

这种管理玩家 CPU 使用率的方式很容易被无限循环滥用,我几乎可以肯定这不是 Screeps 的行为。

2.播放器的循环是原子的。

下一个可能性是玩家的循环是原子的。如果超过 CPU 限制,则玩家的循环被中断,但既不提交预定的游戏状态更改,也不提交对内存的更改。当检测到中断循环时提高效率变得更加重要,因为忽略它意味着玩家的脚本将无法更改游戏状态或内存。但是,检测中断的周期仍然很简单:

module.exports.loop = function()
{
  var failedTicks = 0;

  if ( 'time' in Memory )
  {
    failedTicks = Game.time - Memory.time - 1;

    // N failed ticks means the result of this calculation failed to commit N times.
    Memory.workQuota /= Math.pow( 2, failedTicks );
  }

  // Main body of loop goes here, and uses Memory.workQuota to limit the number
  // of active game objects to process.

  Memory.time = Game.time;
}

2.5. 对 Memory 的更改是原子的,但对 Game 对象的更改不是。

编辑:阅读RawMemory对象的文档后,我想到了这种可能性。如果脚本被中断,任何已经安排好的游戏状态更改都会提交,但不会提交对内存的更改。考虑到 RawMemory 提供的功能,这是有道理的,因为如果脚本在运行自定义内存序列化之前被中断,则会运行默认的 JSON 序列化,这会使自定义内存序列化更加复杂:自定义反序列化需要能够处理除了自定义序列化编写的任何格式之外的默认 JSON。

3. JavaScript 语句是原子的。

另一种可能性是播放器的循环不是原子的,但 JavaScript 语句是原子的。当玩家的循环因超过 CPU 限制而中断时,会提交不完整的游戏状态更改和内存更改,但需要仔细编码 - 进行 Screeps API 调用的语句必须将调用结果分配给内存键 - 游戏状态更改和内存更改不会相互不一致。为这种情况编写完全受 CPU 限制的健壮代码似乎很复杂——这不是我已经解决的问题,我想在尝试之前确定这是 Screeps 的真实行为。

4. 没有什么是原子的。

在另一个极端,甚至单个语句都不是原子的:将 Screeps API 调用的结果分配给内存中的键的语句可能会在调用完成和分配结果之间中断,并且不完整的游戏状态都会发生变化并且不完整的内存更改(现在彼此不一致)被提交。在这种情况下,编写 CPU 限制健壮代码的可能性非常有限。例如,尽管以下语句写入 Memory 的值的存在无疑表明 Screeps API 调用已完成,但它的缺失并不表明调用未完成毫无疑问:

Memory.callResults[ Game.time ][ creep.name ] = creep.move( TOP );


有谁知道其中哪些是 Screeps 的行为?还是我没有考虑过的其他事情?文档中的以下引用:

CPU 限制 100 意味着在 100 毫秒后执行脚本将被终止,即使它还没有完成一些工作。

暗示它可能是案例 3 或案例 4,但不是很令人信服。

另一方面,模拟模式下的实验结果,其中包含单个蠕变、以下主循环,并在对话框中为无响应脚本选择“终止”:

module.exports.loop = function()
{
  var failedTicks = 0;

  if ( 'time' in Memory )
  {
    var failedTicks = Game.time - Memory.time - 1;

    console.log( '' + failedTicks + ' failed ticks.' );
  }

  for ( var creepName in Game.creeps )
  {
    var creep = Game.creeps[ creepName ];

    creep.move( TOP );
  }

  if ( failedTicks < 3 )
  {
    // This intentional infinite loop was initially commented out, and
    // uncommented after Memory.time had been successfully initialized.

    while ( true )
    {
    }
  }

  Memory.time = Game.time;
};

是蠕变只在无限循环被跳过的滴答声上移动,因为 failedTicks 达到了它的阈值。这指向案例 2,但不是决定性的,因为模拟模式下的 CPU 限制与在线不同 - 它似乎是无限的,除非使用对话框的“终止”按钮终止。

4

3 回答 3

4

默认情况下为案例 4,但可修改为案例 2.5

正如 nehegeb 和 dwurf 所怀疑的那样,并且对私人服务器的实验已经证实,默认行为是案例 4。中断之前发生的游戏状态和内存的更改都已提交。

但是,服务器主循环对默认 JSON 序列化的运行是由 RawMemory 中存在的未记录键“_parsed”控制的;键的值是对内存的引用。在脚本的主循环开始时删除键并在结束时恢复它的效果是使脚本的主循环所做的整组内存更改原子化,即案例 2.5:

module.exports.loop = function()
{
  // Run the default JSON deserialize. This also creates a key '_parsed' in
  // RawMemory - that '_parsed' key and Memory refer to the same object, and the
  // existence of the '_parsed' key tells the server main loop to run the
  // default JSON serialize.
  Memory;

  // Disable the default JSON serialize by deleting the key that tells the
  // server main loop to run it.
  delete RawMemory._parsed;

  ...

  // An example of code that would be wrong without a way to make it CPU limit
  // robust:

  mySpawn.memory.queue.push('harvester');
  // If the script is interrupted here, myRoom.memory.harvesterCreepsQueued is
  // no longer an accurate count of the number of 'harvester's in
  // mySpawn.memory.queue.
  myRoom.memory.harvesterCreepsQueued++;

  ...

  // Re-enable the default JSON serialize by restoring the key that tells the
  // server main loop to run it.
  RawMemory._parsed = Memory;
};
于 2017-03-16T02:45:23.950 回答
3

它不是#1 或#2。我打赌它是#4,监控主循环外部的 CPU 使用情况并在达到限制时将其终止是最有意义的。#3 将需要 screeps 服务器中的复杂代码来执行“语句级”事务。正如您所发现的,模拟器中没有 CPU 限制。

大多数玩家通过简单地将关键代码放在主循环的早期来解决这个问题,例如首先出现塔代码,然后是生成代码,然后是蠕动/工作。这也可以防止您的代码中出现未捕获的异常,因为您最关键的功能(希望)已经执行。虽然这是 CPU 限制的一个糟糕的解决方案,但在我的观察中,一旦您使用了存储桶中的所有 CPU 并且不断达到您的常规限制,您的代码似乎每 2 次滴答就会被跳过一次。

我现在没有 CPU 问题(我有订阅),但我会通过将 CPU 密集型代码放在最后来解决这个问题,如果可能的话,只在你的存储桶中有足够的 CPU 并且你远不及你的 500 CPU pertick 限制。它也有助于拥有更大的小兵,寻路甚至只是移动(每次移动 0.2 次)都会占用相当大的 CPU 资源,而更大的小兵意味着更少的小兵。

于 2016-09-12T22:45:27.730 回答
3

游戏中的“每日提示”之一说:

TIP OF THE DAY: If CPU limit raises, your script will execute only partially.

因此我会说,它很可能是#4
就像 dwurf 所说,在大多数情况下,以下脚本布局方法应该可以解决问题:

大多数玩家通过简单地将关键代码放在主循环的早期来解决这个问题,例如首先出现塔代码,然后是生成代码,然后是蠕动/工作。[...]

于 2016-11-15T15:44:40.303 回答