代码优化可以说是每一个在 shard3 生活的玩家都要经历的。这篇文章我们就来分析一下 cpu 消耗的来源和如何进行优化。
CPU 消耗的来源
在游戏中,cpu 消耗主要有三个大头,分别是:搜索消耗、寻路消耗和固定消耗。在介绍之前,我们先来看一下如何在 api 文档中查看各个接口的性能消耗:以 StructureLab 的文档为例,下图中圈出的 蓝色方框条目 都是可执行的方法(空心的代表继承自其它原型),这些方法都是有消耗的。
如下,我们可以在 api 介绍的右上角找到他的性能消耗,该方法的消耗越大,它的颜色就越重、横杠就越多。而拥有 A 标志的方法则代表该方法的消耗是固定 0.2 CPU + 代码执行消耗。也就是说,只要该方法执行成功(返回 OK),那么它一定会吃掉 0.2 CPU。
所以,在调用某个方法前,应先检查它的消耗,并由此决定它在你代码中的调用权重。接下来,我们分别讲一下刚才提到的三大消耗:
搜索消耗
搜索消耗的典型例子就是 Room.find
和 RoomPosition.findInRange
方法。这两个方法需要先遍历房间中的所有对象,筛选出所有符合 FIND_*
常量的同类型对象,然后再遍历执行 filter 来找到最终目标。由于教程的原因,在新手的代码中经常会出现 Room.find
从而导致消耗掉了大量 cpu。
内置的搜索缓存
为了节省消耗,这种搜索类方法都拥有一个内置缓存,游戏会将对应的 FIND_*
搜索结果缓存下来,也就是说,在同一 tick 中,如果你执行了两遍 Room.find(FIND_CREEP)
方法,第二次搜索就会自动应用缓存从而节省消耗。
注意,这个缓存会在 tick 开始时清除。并且,这个缓存只会保存所有符合 FIND_*
常量的搜索结果,而你使用 filter 带来的 cpu 消耗是无法避免的,哪怕你两次搜索所用的 filter 完全相同。
在此之外还有一些方法也会产生搜索消耗,例如.look
系列方法,这里不再赘述,在 api 文档中自行搜索即可。
除了房间对象搜索之外,Game.market.getAllOrders
也需要注意,由于需要检查市场上的所有订单,这个方法的 cpu 消耗是巨大的,不过注意它的介绍:“该方法支持resourceType
内置索引”,也就是说,在 filter 里携带 resourceType 属性可以大大减少其消耗。
寻路消耗
相对于搜索消耗,寻路带来的消耗要大的多,几个比较常见的会产生寻路消耗的方法如下:
Creep.moveTo()
Room.findPath()
RoomPosition.findClosestByPath()
RoomPosition.findPathTo()
Game.map.findRoute()
PathFinder.search()
大体瞅一眼就可以发现,这几个方法基本都是非常常用的,哦真糟糕。像是 Creep.moveTo
方法我又不得不用。是的,如何避免寻路消耗是 Screeps 中的一大重要研究课题。现在常见的方法是将常用的路径缓存来减少寻路次数。而对于新手来说,这里还有种更简单的方法来节省寻路消耗:
内置的寻路缓存
Creep.moveTo()
同样包含一个内置缓存,如下:
你可以通过简单的提升该值的大小来节省 cpu 消耗,但是要注意:这个值越高,你的 creep 反应也就越迟钝。如果它之前缓存的路径上有个无法越过的障碍物,它就会一直卡在那里直到缓存时间结束。
这里要重点提一下RoomPosition.findClosestByPath
方法:
该方法会造成大量的遍历运算,所以在代码编写中你应该少用这种方法,如果要用的话,也请根据情况指定默认算法或者对其结果进行缓存来减少搜索次数。
固定消耗
这种消耗几乎存在游戏世界中的各个角落,它有一个特点:会对游戏世界产生影响,例如Creep.withdraw
和Creep.transfer
,他们会将资源移动到其他位置(产生了影响),所以说这两种方法就会包含固定消耗,除此之外还有诸如Creep.harvest
、Tower.attack
等等很多很多。
这个消耗是游戏制订的规则,所以说几乎无法避免。这个消耗会随着你的 creep 和建筑数量的增多而增多,并构成了你每 tick 的基础消耗。虽然无法避免,但是我们可以通过提高其每次执行的效率来节省 cpu。如何节省我们下文再提。
代码执行成本
除了上面三个消耗大头外,执行代码也会产生一定的消耗,这个几乎是无法避免的,想要节省此类消耗需要你对 js 有更深层的了解,并且由于这种消耗比较零碎,所以并不推荐刻意的对其进行优化,上面三种消耗哪怕你能节省任何一点,所带来的收益都比优化代码执行成本要大的多。
这里有一点需要注意的,游戏的 Memory 内存对象需要每 tick 调用JSON.stringify
和JSON.parse
进行解析和储存,内存越大消耗也就越大,所以请节约内存的使用。
缓存的种类
在介绍如何优化之前,我们先来看一下游戏中的缓存种类,已经有很多类似的文章了,所以我们这里只简单提一下:
-
持久化存储:游戏的
Memory
对象,只有这个地方能实现真正可靠的长时间存储。 -
半持久存储:js 的
Global
对象,对象原型都属于半持久存储,这种存储会在游戏全局重置时被清除,一般存放允许丢失的数据。 -
非持久存储:直接定义在游戏对象(非原型)上的属性都属于非持久存储,例如
Game.rooms.W1N1.myCustomProp = 123
,这种存储只有本 tick 能访问到,用来存放 tick 内协同作业需要的数据。
更详细的分析见下文:
如何优化 CPU 消耗
首先,请把 过早的优化是万恶的根源! 这句话重读三遍并牢牢记住。在你的 cpu 消耗大于三分之二之前,不要刻意优化;在你的房间升到 8 级之前,不要考虑修改房间运营逻辑;在你的整体框架还可以应对现有需求时,不要考虑对代码进行重构。
在重构时因为突然出现的新需求导致需要重构现有的重构工作、因为对游戏了解不够全面导致重构后的框架变成了更大的屎山,这种糟糕的体验会让你不想再打开这个游戏,因此弃坑的玩家也大有人在。所以,请把上面那段文字重读一遍后再继续阅读下面的内容。
ok,我们现在来讲一下如何节省这些消耗。
搜索消耗
首先是搜索消耗,优化搜索消耗主要靠 缓存结果和减少重复搜索 来完成。例如自己房间内的建筑,你可以在搜索之后将其 id 缓存在Memory
或者Global
下,之后通过指定的方法或者属性直接调用。如果你了解过原型拓展的话,你也可以新建类似 Room.sources
之类的属性来更加方便的管理这些缓存。这里 提到了如何创建这些缓存。
而像同一 tick 内同个房间的多个 tower 都执行了敌人搜索,这种就属于完全无意义的重复搜索,你可以通过在房间下挂载非持久缓存的形式解决这个问题:
// tower 将会执行的方法
function towerWork(tower) {
// 如果之前没有 tower 搜索过
if (!tower.room._hasRunTowerFind) {
const enemy = tower.room.find(FIND_HOSTILE_CREEPS)
// 处理逻辑 ...
// 后面的 tower 不再搜索
tower.room._hasRunTowerFind = true
}
}
你也可以把搜索结果挂在 Room 对象下来让房间内的所有 tower 都可以获取到目标。
寻路消耗
减少寻路消耗的主要方法是 重用寻路结果来减少寻路次数。例如从 Spawn 到 Source 的路线是固定的;从 Storage 到 Controller 的路线也是固定的。那么就可以将这类路径存储到非持久缓存或者 Memory 中来让 creep 可以一直按照固定路线移动而无需寻路。你可以使用Creep.moveByPath()
或者自行封装Creep.move()
来实现 creep 按照指定路线移动的逻辑。
这里提一个醒,如果你要把寻路结果保存在 Memory 中的话,请先将搜索结果序列化成字符串的形式进行保存,这样可以节省内存空间。而Room.serializePath
方法不支持压缩PathFinder.search()
的搜索结果,所以如果你在用PathFinder
的话就可能需要手写压缩方法,或者你也可以直接将结果保存到非持久缓存中。
固定消耗
由于固定消耗无法避免,所以我们要尽可能的提升每次执行的效果。例如 Creep.harvest
方法,虽然一个 creep 只有 5 个WORK
就可以采干一个 Source。但是我们依旧可以通过继续提升WORK
的数量来减少Creep.harvest()
的执行次数,从而节省 cpu 消耗。
同理,我们也可以通过增大CARRY
的数量来提升每次搬运的资源数量。提高HEAL
的数量来提升每次治疗的效果等等。这也是为什么官方更推荐用增加身体部件而不是 creep 数量的形式来提升效率。
不止 creep,建筑也可以用这种方式来提升效率,例如Link
等到存储满了之后再发送,Terminal
一次交易更多的数量。在日常开发时就要考虑到这个问题。
好的开发习惯
这个涉及的范围就比较大了,大家了解一下就好,对于节省消耗来说:能重用的地方就不要再次新建变量、尽可能的少出现循环嵌套。剩下的就不说太多了,毕竟大家是来玩游戏不是来上班的。如果确实有兴趣的话,链接就在下面:
写在最后
OK!本文简单介绍了一下 Screeps 中几个 cpu 消耗大头以及如何优化它们,并没有出现多少代码,毕竟每个人的架构不同,强行宣传某一种优化方法也并不一定适用于所有玩家,还是那句话:过早重构、盲目重构只会让你的游戏体验更加糟糕。
了解更多 Screeps 的中文教程?欢迎访问 Screeps - 中文系列教程!