簡介
在設(shè)計自己的角色系統(tǒng)的時候炼绘,很多人都會被角色越來越多的問題所困擾,本文不討論如何去削減角色的數(shù)量杨何,而是從“發(fā)布成本”的角度出發(fā),來介紹如何解決這個問題沥邻,并提高角色系統(tǒng)的可維護(hù)性危虱。
本文將會使用到以下概念,如果對其不太了解唐全,可以先閱讀后方的拓展鏈接:
- 原型拓展 - Screeps 淺談游戲中的原型拓展
什么是發(fā)布成本埃跷?
在正式開始前,我們先來簡單了解一下什么是發(fā)布成本,發(fā)布成本可以簡單的理解成 創(chuàng)建一個新角色時要新增的代碼量弥雹。發(fā)布成本越高垃帅,我們就越抗拒在自己的系統(tǒng)里加入新的角色。那么反過來缅糟,假如我們創(chuàng)建一個新角色只需要寥寥十幾行甚至幾行代碼就可以完成挺智,非常簡單的就可以完成新角色的加入,那么不就從根本上 放棄治療 解決問題了么窗宦?
如何降低發(fā)布成本赦颇?
降低發(fā)布成本的核心思想就是 將不同角色中的可復(fù)用代碼抽象出來,形成一個新的“平臺”赴涵,而把不可復(fù)用的邏輯代碼整合成統(tǒng)一的配置項媒怯。這樣,在發(fā)布新角色時我們只需復(fù)制配置項模板髓窜,然后填寫其中的可變邏輯即可扇苞。
你或許在游戲的過程中已經(jīng)或多或少的做過了類似的事情,例如將狀態(tài)的更新邏輯封裝成一個函數(shù)寄纵,或是將常用的 creep 方法封裝起來鳖敷。同樣的,本文的主要內(nèi)容就是如何高效的將不可變的邏輯抽象出來程拭,避免大家少走彎路定踱。
《設(shè)計模式》 —— GoF
考慮你的設(shè)計中哪些地方可能變化,這種方式與關(guān)注會導(dǎo)致重新設(shè)計的原因相反恃鞋。它不是考慮什么時候會迫使你的設(shè)計改變崖媚,而是考慮你怎樣才能夠 在不重新設(shè)計的情況下進(jìn)行改變。這里的關(guān)鍵在于封裝發(fā)生變化的概念恤浪,這是許多設(shè)計模式的主題畅哑。
了解 creep 的通用運行模式
在抽象可復(fù)用的代碼之前,我們要先了解 creep 的運行模式水由,通俗點說就是每個 creep 都要執(zhí)行的代碼邏輯荠呐。通過對基本的采集者harvester
、升級者upgrader
砂客、建筑者builder
進(jìn)行觀察直秆,我們不難看出:
creep 運行時通常會在兩個狀態(tài)之間進(jìn)行循環(huán)。
例如鞭盟,我們對 Screeps 基本角色系統(tǒng) 一文中提到的角色進(jìn)行拆分:
英文名 | 角色名 | 狀態(tài)A | 切換條件 | 狀態(tài)B |
---|---|---|---|---|
havester |
采礦者 | 開采能量 | carry 是否到達(dá)上限 | 存入指定的結(jié)構(gòu) |
upgrader |
升級者 | 取出能量 | carry 是否到達(dá)上限 | 升級房間控制器 |
builder |
建造者 | 取出能量 | carry 是否到達(dá)上限 | 建造結(jié)構(gòu) |
carrier |
運輸者 | 取出能量 | carry 是否到達(dá)上限 | 存入指定的結(jié)構(gòu) |
repairer |
維修者 | 取出能量 | carry 是否到達(dá)上限 | 修復(fù)受損的結(jié)構(gòu) |
defender |
防御者 | 駐守指定區(qū)域 | 房間內(nèi)是否有入侵者 | 攻擊入侵者 |
但是這并不是全部,例如在開拓者 (在新殖民房間中建造 spawn) 需要先抵達(dá)執(zhí)行房間瑰剃,然后再執(zhí)行建造者的邏輯齿诉。又比如士兵,可能需要在作戰(zhàn)前先獲取強(qiáng)化。所以說:
在某些角色中粤剧,需要執(zhí)行一個額外的準(zhǔn)備階段歇竟。
所以,我們可以整理出如下 creep 生命周期:
進(jìn)而抵恋,我們就可以得到如下 creep 生命周期階段的基本結(jié)構(gòu)焕议,這一段只是用作說明,不需要加進(jìn)你的代碼弧关。使用了 typescript 中的接口來描述盅安,如果你沒有用過 typescript 的話,可以參考其中的注釋進(jìn)行理解 :
/**
* creep 生命周期階段
*/
interface CreepLifeCycle {
/**
* [可選] 準(zhǔn)備階段世囊,接受 creep 并執(zhí)行對應(yīng)的準(zhǔn)備邏輯
* 根據(jù)其返回值判斷是否準(zhǔn)備完成别瞭,在準(zhǔn)備完成前是不會執(zhí)行下面的 target 和 source 階段的
*/
prepare?: (creep: Creep) => boolean
/**
* [必須] 工作階段,接受 creep 并執(zhí)行對應(yīng)的工作邏輯(例如建造建筑株憾,升級控制器)
* 在返回 true 時代表所需資源不足蝙寨,將在紫萼個 tick 開始執(zhí)行 source 階段
*/
target: (creep: Creep) => boolean
/**
* [可選] 資源獲取階段,接受 creep 并執(zhí)行對應(yīng)的資源獲取邏輯(例如獲取能量嗤瞎,采集礦物)
* 在返回 true 時代表能量獲取完成墙歪,將在下個 tick 開始執(zhí)行 target 階段
*/
source?: (creep: Creep) => boolean
}
注意,這里每個階段的值都是函數(shù)Function
贝奇,我們對應(yīng)階段的實際代碼邏輯就包含在這些函數(shù)里虹菲,這樣對于底層架構(gòu)來說,只需要根據(jù) creep 當(dāng)前的狀態(tài)調(diào)用不同的函數(shù)即可弃秆,不需要關(guān)心 creep 的具體工作邏輯是怎樣的届惋。這在軟件設(shè)計上被稱為 關(guān)注點分離。
設(shè)計 creep 運行流程
我們已經(jīng)了解了 creep 的運行模式菠赚,現(xiàn)在來重新設(shè)計一下代碼流程脑豹,使其可以兼容我們的新設(shè)計。
首先衡查,我們要將角色的邏輯整合在一起做成一個函數(shù)瘩欺,這個函數(shù)接受必要的參數(shù),并返回要執(zhí)行的工作邏輯拌牲,而返回的工作邏輯對象的結(jié)構(gòu)就是上文中的 CreepLifeCycle
俱饿。為什么要這么設(shè)計呢?必要的參數(shù)又是什么呢塌忽?
要解答這個問題拍埠,我們先來思考一下在設(shè)計角色邏輯時面臨的最大問題是什么。是的土居,如何獲取自己的操作目標(biāo)枣购。這里的操作目標(biāo)指的是 creep 在工作時要面對的東西嬉探,例如 harvester 要采集的 source。你是不是糾結(jié)了很久如何讓 creep 采集不同的 source棉圈?
在之前的代碼中涩堤,我們的工作邏輯里耦合了太多由 if-else 組成的目標(biāo)獲取代碼,例如根據(jù)某個內(nèi)存字段獲取到不同的 source 對象分瘾。這實際上違反了 單一職責(zé)原則胎围。所以我們現(xiàn)在將這些目標(biāo)獲取代碼拿到外邊,然后通過函數(shù)參數(shù)的形式傳遞給 creep 的工作邏輯德召,工作邏輯不用關(guān)心這些目標(biāo)是怎么來的白魂,直接無腦執(zhí)行即可,這樣就保證了工作函數(shù)的純潔性氏捞。
這個參數(shù)根據(jù)角色的不同也是不一樣的碧聪,例如 harvester 會接受一個 source Id 作為他要采集的能量來源,而 defender 會接受一個房間名作為他要防御的目標(biāo)房間液茎。
接下來我們會在內(nèi)存中創(chuàng)建一個對象來保存這些配置逞姿,并開放一套 api 來對這個配置對象進(jìn)行管理。最后我們會在Creep
原型上添加一個 work 方法捆等,將我們上一小節(jié)中的生命周期邏輯存放到其中滞造。這樣只需要遍歷 Game.creeps 并調(diào)用creep.work()
即可完成每個 creep 的工作。
具體的流程如下:
實現(xiàn)運行流程
上一小節(jié)是不是看的有些暈栋烤,沒有關(guān)系谒养,接下來接下來我們會把所有的代碼實現(xiàn)出來并一一講解。為了方便理解明郭,這里會按照上面的流程圖 從下往上 進(jìn)行實現(xiàn)买窟。本節(jié)內(nèi)容推薦先在 訓(xùn)練場 中進(jìn)行實驗。
1> 在配置項中定義運行邏輯
首先我們實現(xiàn)最后一步:在配置項中定義運行邏輯薯定,按照上面的CreepLifeCycle
實現(xiàn)最簡單的upgrader
升級者始绍,新建role.upgrader.js
并填入如下內(nèi)容:
/**
* 升級者配置生成器
* source: 從指定礦中挖礦
* target: 將其轉(zhuǎn)移到指定的 roomController 中
*
* @param sourceId 要挖的礦 id
*/
module.exports = sourceId => ({
// 采集能量礦
source: creep => {
const source = Game.getObjectById(sourceId)
if (creep.harvest(source) == ERR_NOT_IN_RANGE) creep.moveTo(source)
// 自己身上的能量裝滿了,返回 true(切換至 target 階段)
return creep.store.getFreeCapacity() <= 0
},
// 升級控制器
target: creep => {
const controller = creep.room.controller
if (creep.upgradeController(controller) == ERR_NOT_IN_RANGE) creep.moveTo(controller)
// 自己身上的能量沒有了话侄,返回 true(切換至 source 階段)
return creep.store[RESOURCE_ENERGY] <= 0
}
})
可以看到我們用非常少的代碼就實現(xiàn)了升級者的邏輯亏推。當(dāng)然這里并不能直接運行,稍后我們會繼續(xù)進(jìn)行完善年堆。
這里先簡單介紹一下這段代碼吞杭,可以看到最外層我們用module.exports
和箭頭函數(shù)導(dǎo)出了一個函數(shù),這個函數(shù) 接收一個能量礦的 id 变丧,并返回升級者的工作邏輯芽狗,這里返回的工作邏輯對象就是上文中的 CreepLifeCycle
。稍后我們會使用這個函數(shù)快捷的生成一個升級者痒蓬。而由于升級者不需要準(zhǔn)備階段童擎,所以我們省略了prepare
階段的實現(xiàn)曼月。
值得注意的是 source 和 target 方法的返回值,最終的框架會根據(jù)其返回值決定是否要切換至另一個階段柔昼。
2> 創(chuàng)建 creep 管理 api
ok,接下來我們來創(chuàng)建一個全局模塊炎辨,這個模塊將負(fù)責(zé) creep 的增刪捕透。新增文件 creepApi.js
并填入如下內(nèi)容:
global.creepApi = {
/**
* 新增 creep 配置項
* @param configName 配置項名稱
* @param role 該 creep 的角色
* @param args creep 的工作參數(shù)
*/
add(configName, role, ...args) {
if (!Memory.creepConfigs) Memory.creepConfigs = {}
Memory.creepConfigs[configName] = { role, args }
return `${configName} 配置項已更新:[角色] ${role} [工作參數(shù)] ${args}`
},
/**
* 移除指定 creep 配置項
* @param configName 要移除的配置項名稱
*/
remove(configName) {
delete Memory.creepConfigs[configName]
return `${configName} 配置項已移除`
},
/**
* 獲取 creep 配置項
* @param configName 要獲取的配置項名稱
* @returns 對應(yīng)的配置項,若不存在則返回 undefined
*/
get(configName) {
if (!Memory.creepConfigs) return undefined
return Memory.creepConfigs[configName]
}
}
這個模塊一共暴露了三個方法碴萧,分別用于添加 creep 配置乙嘀、移除配置以及獲取配置,非常的簡單破喻。注意其中使用了 es6 的 解構(gòu)操作符 ...
來讓代碼更加精簡虎谢。
好了,現(xiàn)在我們已經(jīng)有了配置工具曹质,接下來我們將拓展 Creep 原型婴噩,讓 creep 們可以從自己持有的配置中明白需要做什么。
4> 進(jìn)行 Creep 拓展
首先新建mount.creep.js
羽德,并填入如下內(nèi)容:
/**
* 引入 creep 配置項
* 其鍵為角色名(role)几莽,其值為對應(yīng)角色的邏輯生成函數(shù)
*/
const roles = {
upgrader: require('role.upgrader.js')
}
// 添加 work 方法
Creep.prototype.work = function() {
// ------------------------ 第一步:獲取 creep 執(zhí)行邏輯 ------------------------
// 獲取對應(yīng)配置項
const creepConfig = creepApi.get(this.memory.configName)
// 檢查 creep 內(nèi)存中的配置是否存在
if (!creepConfig) {
console.log(`creep ${this.name} 攜帶了一個無效的配置項 ${this.memory.configName}`)
this.say('找不到配置!')
return
}
const creepLogic = roles[creepConfig.role](...creepConfig.args)
// ------------------------ 第二步:執(zhí)行 creep 準(zhǔn)備階段 ------------------------
// 沒準(zhǔn)備的時候就執(zhí)行準(zhǔn)備階段
if (!this.memory.ready) {
// 有準(zhǔn)備階段配置則執(zhí)行
if (creepLogic.prepare) {
this.memory.ready = creepLogic.prepare(this)
}
// 沒有就直接準(zhǔn)備完成
else this.memory.ready = true
return
}
// ------------------------ 第三步:執(zhí)行 creep 工作階段 ------------------------
let stateChange = true
// 執(zhí)行對應(yīng)階段
// 階段執(zhí)行結(jié)果返回 true 就說明需要更換 working 狀態(tài)
if (this.memory.working) {
if (creepLogic.target) stateChange = creepLogic.target(this)
}
else {
if (creepLogic.source) stateChange = creepLogic.source(this)
}
// 狀態(tài)變化了就切換工作階段
if (stateChange) this.memory.working = !this.memory.working
}
這一段代碼比較長宅静,我們來詳細(xì)介紹一下章蚣,首先我們引入了 role.upgrader.js 并將其放在一個對象 roles
中,這個對象包含了我們所有的角色姨夹,后期我們新增了角色的話需要添加到這里纤垂。
然后我們通過修改 Creep 原型的方式為所有的 creep 都添加了 work 方法,這個方法中包含的內(nèi)容就是我們在一開始提到的 “基礎(chǔ)框架”磷账。其中一共包含了三部分峭沦,上面已經(jīng)通過注釋形式標(biāo)注了起來,分別是:
獲取工作邏輯:通過 creep 內(nèi)存中保存的 configName 字段借助 creepApi 獲取對應(yīng)的配置項够颠。
執(zhí)行準(zhǔn)備階段:檢查 creep 內(nèi)存中的 ready 字段熙侍,如果不為 true 的話則說明 creep 還沒準(zhǔn)備好,去執(zhí)行準(zhǔn)備階段履磨。在準(zhǔn)備完成前不會執(zhí)行下面的工作階段蛉抓。
執(zhí)行工作階段:狀態(tài)機(jī),檢查 creep 內(nèi)存中的 working 字段剃诅,如果為 true 則執(zhí)行 target 階段巷送,為 false 就執(zhí)行 source 階段,并根據(jù)這兩個階段的返回值決定要不要切換狀態(tài)矛辕。
你可以通過下面這張圖理解 creep 是如何找到自己要執(zhí)行的代碼的:
5> 掛載拓展并調(diào)用 creep
ok笑跛,現(xiàn)在我們已經(jīng)完成了全部的準(zhǔn)備工作付魔,接下來只需要把他們實裝即可,在main.js
里填寫如下代碼:
// 掛載 creep 管理模塊
require('creepApi.js')
// 掛載 creep 拓展
require('mount.creep.js')
module.exports.loop = function() {
// 遍歷所有 creep 并執(zhí)行上文中拓展的 work 方法
Object.values(Game.creeps).forEach(creep => creep.work())
}
現(xiàn)在我們就可以來進(jìn)行測試了飞蹂,首先執(zhí)行如下代碼來孵化一個 creep:
// 注意修改其中的 spawn 名稱
Game.spawns.Spawn1.spawnCreep([WORK, CARRY, MOVE], 'firstUpgrader', { memory: { configName: 'upgrader1' }})
有一點和官方教程不同的是几苍,在 creep 內(nèi)存中保存了 configName: upgrader1
而不是 role: upgrader
,因為在這個架構(gòu)里陈哑,不同的升級者的配置是不同的(例如 upgrader1 會去能量礦 A妻坝,而 upgrader2 會去能量礦 B ),所以我們要通過upgrader1
來找到其對應(yīng)的配置項惊窖。
在他孵化完成后你可以看到它在嚷嚷著找不到配置項刽宪,這是因為我們給他內(nèi)存中設(shè)置的配置 upgrader1
并不存在,接下來我們在控制臺執(zhí)行如下代碼來新建這個配置:
// 注意把第三個參數(shù)改成房間中存在的 source id
creepApi.add('upgrader1', 'upgrader', '5bbcaa7d9099fc012e631786')
現(xiàn)在我們就能看到 creep 已經(jīng)開始執(zhí)行他的升級任務(wù)了界酒!
這行代碼的意思就是新增配置項 upgrader1圣拄,指定角色為 upgrader,將采集對應(yīng) source 中的能量并升級 controller毁欣,是不是非常簡單庇谆。你可以將上面的 source id 換成房間內(nèi)的另一個 source,然后再執(zhí)行一遍署辉,然后就可以看到 creep 迅速的響應(yīng)了我們的變更族铆。
你也可以在控制臺執(zhí)行下面的代碼來刪除配置項,刪除后 creep 將會重新變?yōu)橐粋€無頭蒼蠅:
creepApi.remove('upgrader1')
也就是說哭尝,我們只需要使用 creepApi 對配置項進(jìn)行控制哥攘,就可以靈活的指導(dǎo) creep 的行為邏輯。而不用關(guān)系其他角色細(xì)節(jié)材鹦,即下圖所示:
和其他模塊進(jìn)行對接
這里為了簡單起見逝淹,我們手動創(chuàng)建了 creep 的配置項,如果你還是個新手的話桶唐,推薦你先以這種形式手動調(diào)整房間的運營單位來積累經(jīng)驗栅葡,在你對游戲的了解有所深入之后,你可以嘗試結(jié)合自己的 spawn 孵化模塊和 creep 數(shù)量控制模塊來動態(tài)的調(diào)用 creepApi 進(jìn)行 creep 增刪 以達(dá)到動態(tài)調(diào)整運營單位的目的尤泽,調(diào)用方式和上面控制臺命令完全一致欣簇,這里不再過多深入。
寫在最后
本文中提到的框架并不復(fù)雜坯约,只有兩個需要注意的點:
- source 和 target 生命周期階段會根據(jù)函數(shù)的返回值(是否為 true)決定下個 tick 是否要切換為另一個階段熊咽。
- creepApi 是指導(dǎo) creep 工作的核心工具。通過其他模塊調(diào)用 creepApi闹丐,可以完成各種各樣的 creep 工作横殴。
如果你對上面提到的代碼還有不了解的地方,推薦把上面的 設(shè)計 creep 運行流程 小節(jié)多讀幾次卿拴。接下來提幾點可以優(yōu)化的地方衫仑,你可以酌情考慮升級:
-
添加
isNeed
階段:上面配置項只能滿足那些會一直生成的 creep 發(fā)布梨与,而元素礦采集單位和房間守衛(wèi)這種有可能很長時間都不會孵化的單位該怎么辦呢?通過添加額外的 isNeed 階段文狱,并在 spawn 孵化前進(jìn)行檢查粥鞋,這樣就可以決定是否要重新孵化某個單位。 - 在配置項中添加 body 函數(shù):creep 在不同時期的體型是會發(fā)生改變的瞄崇,我們可以在配置中添加一個 body 函數(shù)陷虎,這個函數(shù)會在孵化時由 spawn 調(diào)用,并將函數(shù)的返回值作為要孵化 creep 的 body 體型杠袱,由此來提高角色的內(nèi)聚性。
如果你不知道如何著手進(jìn)行修改的話窝稿,可以參考我的 Screeps 項目 HoPGoldy/my-screeps-ai楣富。以上就是本文的全部內(nèi)容了,了解更多 Screeps 的中文教程伴榔?歡迎訪問 Screeps - 中文系列教程纹蝴!