
簡(jiǎn)介
在游戲的教程中,我們了解到可以通過游戲給定的基本 api 如Game.creeps Game.spawns等獲取游戲?qū)ο?,然后設(shè)計(jì)一層層的封裝,并將這些游戲?qū)ο笞鳛閰?shù)傳遞給封裝好的函數(shù),進(jìn)而完成我們的邏輯。
但是這么做有一點(diǎn)缺陷,假如我們代碼封裝做的比較多的話,就會(huì)出現(xiàn)游戲?qū)ο蟊灰粚訉拥膫鬟f下去,如下:
// 獲取游戲?qū)ο?const creep = Game.creeps['creep1']
// 傳遞給任務(wù)分發(fā)函數(shù)
work(creep)
// work 函數(shù)里傳遞給指定的角色函數(shù)
upgrader(creep)
// upgrader 函數(shù)里再傳遞給狀態(tài)更新函數(shù)
updateState(creep)
…
可以看到 creep 對(duì)象被一步步的傳遞給不同的函數(shù),這樣的設(shè)計(jì)會(huì)增加參數(shù)的個(gè)數(shù),不方便理解,進(jìn)而增加代碼的維護(hù)成本。
那么有沒有一種更簡(jiǎn)潔的方式來(lái)避免這種設(shè)計(jì)呢,答案就是本文要講的 游戲原型拓展。
什么是原型?
js 中的繼承是通過原型鏈實(shí)現(xiàn)的,每一個(gè)對(duì)象都有一個(gè)鏈接指向了另一個(gè)對(duì)象,被指向的對(duì)象就稱為前者的原型。
比如 screeps 中的每一個(gè)creep的原型都是Creep,注意區(qū)分這兩者的大小寫,我們常用的.moveTo()、.harvest()就是在Creep原型上定義的。
而如果我們嘗試調(diào)用某個(gè)對(duì)象上不存在的屬性時(shí),它就會(huì)去它的原型上去查找,如果原型對(duì)象上有的話,就會(huì)執(zhí)行原型上的方法。這也就是我們可以進(jìn)行原型拓展的根本。

簡(jiǎn)單的例子
剛才講了一大堆概念,可能對(duì)沒有系統(tǒng)學(xué)過 js 的同學(xué)有點(diǎn)難以理解。接下來(lái)就舉一個(gè)簡(jiǎn)單的例子。我們?cè)谟螒虼a入口處進(jìn)行如下定義:
module.exports.loop = function () {
Creep.prototype.sayHello = function () {
console.log('hello world!')
}
}
然后隨便抓個(gè)正在干活的creep執(zhí)行下面代碼:
Game.creeps['creep name'].sayHello()
然后你就會(huì)發(fā)現(xiàn)控制臺(tái)輸出了hello world!。完美!我們成功的修改了Creep,讓每一個(gè)creep都獲得了sayHello方法。接下來(lái)我們講解一下上面的例子:
我們?cè)?code>Creep.prototype上定義了sayHello,并把一個(gè)函數(shù)賦值給它。這里的Creep.prototype就是Creep原型對(duì)象,當(dāng)creep上找不到sayHello方法的時(shí)候就會(huì)去prototype上尋找,正好就找到了我們剛才定義的方法。
很好,我們來(lái)更近一步,把上面的代碼替換如下:
Creep.prototype.sayHello = function () {
this.say(`我的名字是${this.name}`)
}
然后再找一個(gè)creep執(zhí)行sayHello方法,就會(huì)出現(xiàn)如下情況:

creep他自己說話了!沒錯(cuò),在原型對(duì)象上定義的方法中,你可以使用this訪問到其他所有的屬性!這么一來(lái),我們就可以將常用的方法掛載到creep來(lái)簡(jiǎn)化我們的代碼結(jié)構(gòu),比如下面這樣:
// 建設(shè)房間內(nèi)的建筑工地
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])
}
}
}
這樣我們只需要執(zhí)行creep.buildStructure()就可以讓creep自己跑到建筑工地然后干活了。至于原型拓展的用處還有很多,這里不再深入,有興趣的可以參考官方的這篇文檔 《screeps modifying-prototypes》 來(lái)了解更多用法。

接下來(lái),我們講一下如何優(yōu)雅清晰的規(guī)?;卣乖?。
更好的代碼結(jié)構(gòu)
和其他的邏輯代碼一樣,我們總不能把所有的原型拓展代碼都寫到主入口module.exports.loop里吧,接下來(lái)就分享一種比較清晰的代碼結(jié)構(gòu)。當(dāng)然,如果你有自己的想法的話大可不必按照我的來(lái)做。
首先假設(shè)我們想拓展Creep原型,那么可以新建一個(gè)名為mount.creep.js的文件,其內(nèi)容如下:
// 將拓展簽入 Creep 原型
module.exports = function () {
_.assign(Creep.prototype, creepExtension)
}
// 自定義的 Creep 的拓展
const creepExtension = {
// 自定義敵人檢測(cè)
checkEnemy() {
// 代碼實(shí)現(xiàn)...
},
// 填充所有 spawn 和 extension
fillSpawnEngry() {
// 代碼實(shí)現(xiàn)...
},
// 填充所有 tower
fillTower() {
// 代碼實(shí)現(xiàn)...
},
// 其他更多自定義拓展
}
我們將所有的自定義屬性都存放到creepExtension對(duì)象中,然后使用lodash的assign方法將其一次性全部簽入到Creep原型中。
然后我們?cè)傩陆?code>mount.js文件,這個(gè)文件引入mount.creep.js并將封裝成一個(gè)單獨(dú)的入口函數(shù)。這樣,如果有新的拓展,也可以統(tǒng)一掛載到這層封裝中:
const mountCreep = require('./mount.creep')
// const mountFlag = require('./mount.flag')
// const mountRoom = require('./mount.room')
// 掛載所有的額外屬性和方法
module.exports = function () {
console.log('[mount] 重新掛載拓展')
mountCreep()
// mountFlag()
// mountRoom()
// 其他更多拓展...
}
最后,我們?cè)?main.js 中引入 mount.js 并執(zhí)行就好了。注意,這里的方法調(diào)用應(yīng)該寫在 loop 之外,這樣只會(huì)在全局重置(我們的拓展就是這時(shí)被清空的 )時(shí)重新掛載,這樣就不用每個(gè) tick 都執(zhí)行從而帶來(lái)不必要的消耗:
require('./mount')()
module.exports.loop = function () {
// 其他邏輯代碼
}
這里放一張拓展掛載的流程圖來(lái)方便理解。

總結(jié)
本文簡(jiǎn)單介紹了 screeps 中如何拓展原型,只需要在對(duì)應(yīng)原型的prototype屬性上添加新的屬性即可,添加完成后所有由該原型派生出的對(duì)象都可以調(diào)用該屬性,并且在 screeps - api 中提到的原型都可以通過這種方式進(jìn)行拓展。
最后我們介紹了一種封裝拓展的代碼結(jié)構(gòu),做到了拓展的統(tǒng)一掛載,并且方便了日后的維護(hù)。想要了解更多內(nèi)容歡迎訪問 《Screeps 文集》!