简介
在游戏的教程中,我们了解到可以通过游戏给定的基本 api 如Game.creeps
Game.spawns
等获取游戏对象,然后设计一层层的封装,并将这些游戏对象作为参数传递给封装好的函数,进而完成我们的逻辑。
但是这么做有一点缺陷,假如我们代码封装做的比较多的话,就会出现游戏对象被一层层的传递下去,如下:
// 获取游戏对象
const creep = Game.creeps['creep1']
// 传递给任务分发函数
work(creep)
// work 函数里传递给指定的角色函数
upgrader(creep)
// upgrader 函数里再传递给状态更新函数
updateState(creep)
…
可以看到 creep 对象被一步步的传递给不同的函数,这样的设计会增加参数的个数,不方便理解,进而增加代码的维护成本。
那么有没有一种更简洁的方式来避免这种设计呢,答案就是本文要讲的 游戏原型拓展。
什么是原型?
js 中的继承是通过原型链实现的,每一个对象都有一个链接指向了另一个对象,被指向的对象就称为前者的原型。
比如 screeps 中的每一个creep
的原型都是Creep
,注意区分这两者的大小写,我们常用的.moveTo()
、.harvest()
就是在Creep
原型上定义的。
而如果我们尝试调用某个对象上不存在的属性时,它就会去它的原型上去查找,如果原型对象上有的话,就会执行原型上的方法。这也就是我们可以进行原型拓展的根本。
简单的例子
刚才讲了一大堆概念,可能对没有系统学过 js 的同学有点难以理解。接下来就举一个简单的例子。我们在游戏代码入口处进行如下定义:
module.exports.loop = function () {
Creep.prototype.sayHello = function () {
console.log('hello world!')
}
}
然后随便抓个正在干活的creep
执行下面代码:
Game.creeps['creep name'].sayHello()
然后你就会发现控制台输出了hello world!
。完美!我们成功的修改了Creep
,让每一个creep
都获得了sayHello
方法。接下来我们讲解一下上面的例子:
我们在Creep.prototype
上定义了sayHello
,并把一个函数赋值给它。这里的Creep.prototype
就是Creep
原型对象,当creep
上找不到sayHello
方法的时候就会去prototype
上寻找,正好就找到了我们刚才定义的方法。
很好,我们来更近一步,把上面的代码替换如下:
Creep.prototype.sayHello = function () {
this.say(`我的名字是${this.name}`)
}
然后再找一个creep
执行sayHello
方法,就会出现如下情况:
creep
他自己说话了!没错,在原型对象上定义的方法中,你可以使用this
访问到其他所有的属性!这么一来,我们就可以将常用的方法挂载到creep
来简化我们的代码结构,比如下面这样:
// 建设房间内的建筑工地
Creep.prototype.buildStructure = function () {
const targets = this.room.find(FIND_CONSTRUCTION_SITES)
// 找到就去建造
if (targets.length > 0) {
if(this.build(targets[0]) == ERR_NOT_IN_RANGE) {
this.moveTo(targets[0])
}
}
}
这样我们只需要执行creep.buildStructure()
就可以让creep
自己跑到建筑工地然后干活了。至于原型拓展的用处还有很多,这里不再深入,有兴趣的可以参考官方的这篇文档 《screeps modifying-prototypes》 来了解更多用法。
接下来,我们讲一下如何优雅清晰的规模化拓展原型。
更好的代码结构
和其他的逻辑代码一样,我们总不能把所有的原型拓展代码都写到主入口module.exports.loop
里吧,接下来就分享一种比较清晰的代码结构。当然,如果你有自己的想法的话大可不必按照我的来做。
首先假设我们想拓展Creep
原型,那么可以新建一个名为mount.creep.js
的文件,其内容如下:
// 将拓展签入 Creep 原型
module.exports = function () {
_.assign(Creep.prototype, creepExtension)
}
// 自定义的 Creep 的拓展
const creepExtension = {
// 自定义敌人检测
checkEnemy() {
// 代码实现...
},
// 填充所有 spawn 和 extension
fillSpawnEngry() {
// 代码实现...
},
// 填充所有 tower
fillTower() {
// 代码实现...
},
// 其他更多自定义拓展
}
我们将所有的自定义属性都存放到creepExtension
对象中,然后使用lodash
的assign
方法将其一次性全部签入到Creep
原型中。
然后我们再新建mount.js
文件,这个文件引入mount.creep.js
并将封装成一个单独的入口函数:
const mountCreep = require('./mount.creep')
// 挂载所有的额外属性和方法
module.exports = function () {
if (!global.hasExtension) {
console.log('[mount] 重新挂载拓展')
global.hasExtension = true
mountCreep()
}
}
注意这里做了一次检查,当global.hasExtension
(这个值同样也是我们自定义的) 不存在时我们就重新挂载,不然什么都不做。为什么要进行这个检查呢?因为 挂载完成之后,我们自定义的属性是具有持久性的。这也就意味着,我们只需要挂载一次就可以在之后一直使用,就不需要每个tick
都挂载一遍从而造成 cpu 损耗了。
但是挂载完拓展之后并不是一劳永逸,每当我们有代码提交或者是游戏清除缓存后,我们挂载的拓展就会被清除掉。这时候就需要重新进行挂载。加上这个检查之后,一旦global.hasExtension
不存在了,那就说明其他的拓展也大概率的被清掉了,这时候统一进行重新挂载就好了。
并且加上这一层封装之后,如果我们再有新的拓展,比如mount.flag.js
或者mount.room.js
,我们也都可以再这个文件里统一引入、统一挂载。如下:
const mountCreep = require('./mount.creep')
const mountFlag = require('./mount.flag')
const mountRoom = require('./mount.room')
module.exports = function () {
if (!global.hasExtension) {
console.log('[mount] 重新挂载拓展')
global.hasExtension = true
mountCreep()
mountFlag()
mountRoom()
// 其他更多拓展...
}
}
最后,我们再main.js
中引入mount.js
并执行就好了:
const mount = require('./mount')
module.exports.loop = function () {
// 挂载所有拓展
mount()
}
这里放一张拓展挂载的流程图来方便理解。
总结
本文简单介绍了 screeps 中如何拓展原型,只需要在对应原型的prototype
属性上添加新的属性即可,添加完成后所有由该原型派生出的对象都可以调用该属性,并且在 .screeps - api 中提到的原型都可以通过这种方式进行拓展。
最后我们介绍了一种封装拓展的代码结构,做到了拓展的统一挂载,并且方便了日后的维护。想要了解更多内容欢迎访问 《Screeps 文集》!