為了不讓自己下線時出現(xiàn) creep 都涼了的情況键袱,你的代碼里或多或少都有一個用于控制他們數(shù)量的模塊。在教程中摹闽,官方給出了一個簡單有效的方法:檢查每個角色的數(shù)量蹄咖,當數(shù)量低于指定數(shù)量時就進行生成。而本文主要講述的內(nèi)容付鹿,就是介紹數(shù)量控制系統(tǒng)的設計思路比藻。如果有不同的意見和看法歡迎評論區(qū)留言探討~
系統(tǒng)組成
首先,讓我們先來了解下一個完整的數(shù)量控制系統(tǒng)的組成部分:
- 數(shù)量檢查邏輯:這一部分負責發(fā)現(xiàn)需要進行生成的 creep倘屹,并把要生成的 creep 的信息(如名稱银亲、內(nèi)存、身體部分)提交給 spawn纽匙。
- spawn 生成邏輯:這一部分負責接收數(shù)量檢查邏輯傳過來的任務务蝠,并用其生成 creep。
- 期望數(shù)量配置:這部分將給數(shù)量檢查邏輯提供指定 creep 期望的數(shù)量烛缔,可以手動維護馏段,也可以通過分析當前殖民地狀態(tài)自動修改轩拨。
這三個模塊共同構成了一個數(shù)量控制系統(tǒng),缺一不可院喜。接下來亡蓉,我將對這三部分進行分別介紹:
數(shù)量檢查:集中式還是分布式?
在教程的有這么一行代碼:
var harvesters = _.filter(Game.creeps, creep => creep.memory.role == 'harvester');
這行代碼統(tǒng)計了采集者harvester
的數(shù)量喷舀,并在后面進行對比砍濒。這就是一個典型的集中式數(shù)量檢查,簡單明了硫麻。集中式的優(yōu)點就是非嘲中希可靠,通過實時的監(jiān)控拿愧,每一個角色的數(shù)量永遠都是已知的杠河。無論因為什么原因導致的 creep 死亡,數(shù)量控制系統(tǒng)都可以立刻得知浇辜,并重新安排生成券敌。
集中式數(shù)量檢查的缺點
但是這種可靠性的代價就是代碼復雜度的提高和 cpu 的消耗。在開發(fā)數(shù)量控制系統(tǒng)時有一個重要的前提:每個房間的運營 creep 數(shù)量都是不同的柳洋。也就是說待诅,我們要針對每個房間運行一次數(shù)量檢查邏輯,從而保證房間的正常運營膳灶。
而除此之外,有一些例如外礦采集者和對外作戰(zhàn)的 creep 的數(shù)量是不屬于某個具體的房間的立由,所以我們還需要針對這些 creep 做一次數(shù)量檢查轧钓。
越來越多的額外邏輯會讓你的代碼不再簡潔,慢慢的變成一座屎山锐膜,從而摧毀你的游戲體驗毕箍。
分布式設計 - creep 自檢查
那么與之對應,分布式的設計思想就是把數(shù)量檢查的邏輯分散到每個 creep 中:每個 creep 會定期檢查自己是否健康道盏,如果不健康的話就通知 spawn 進行生成而柑。從而“自發(fā)的”維持指定的數(shù)量。
Creep.prototype.work = function() {
// ...
// 如果 creep 還沒有發(fā)送重生信息的話荷逞,執(zhí)行健康檢查媒咳,保證只發(fā)送一次生成任務
// 健康檢查不通過則向 spawnList 發(fā)送自己的生成任務
if (!this.memory.hasSendRebirth) {
const health = this.isHealthy()
if (!health) {
// 向指定 spawn 推送生成任務
// ...
this.memory.hasSendRebirth = true
}
}
}
// creep 監(jiān)控狀態(tài)檢查
Creep.prototype.isHealthy = function() {
if (this.ticksToLive <= 10) return false
else return true
}
分布式數(shù)量檢查的優(yōu)點
分布式數(shù)量檢查的優(yōu)點就在于結構簡單,因為每個 creep 都只關注自身种远,所以不會產(chǎn)生多房間的數(shù)量需要統(tǒng)計多次的問題涩澡。
并且,分布式檢查還有一個其他模式難以實現(xiàn)的優(yōu)點坠敷,可以自由控制發(fā)送重生任務的時機妙同,你可以由此實現(xiàn) creep 的無縫替換射富,例如計算從出生點到工作崗位的距離,然后算出到達崗位的時間粥帚,從而提前生成接班的 creep胰耗,避免空閑。
分布式數(shù)量檢查的缺點
這種設計思路最大的缺點是 不夠可靠芒涡,為什么呢柴灯?假如有入侵者殺死了 creep ,或者有核彈直接殺死了所有的 creep 呢拖陆?如果還沒有發(fā)送任務就已經(jīng)死了弛槐,那么這個角色的數(shù)量就會 -1 并且不會回到正常的狀態(tài),因為可以發(fā)送生成任務的 creep 已經(jīng)不在了依啰。
所以說乎串,優(yōu)點也是缺點,問題的根本在于 很難決定什么時候應該發(fā)送生成任務速警。如果你使用這種方式來管理 creep 的數(shù)量叹誉,那你就要額外設計一個定時檢查任務,來確保沒有 creep 提前倒下闷旧。
最直接的方法 - 死亡檢查
那么有沒有一種簡單不燒腦长豁!也不燒 cpu 的方法來解決這個問題呢!答案是有的忙灼!并且這個設計方式甚至在教程中已經(jīng)給出來了匠襟。他就是通過檢查死去 creep 的記憶:
for(const name in Memory.creeps) {
if(!Game.creeps[name]) {
// 不再刪除了!
// delete Memory.creeps[name]
// 向 spawn 發(fā)送生成任務
// ...
}
}
當我們發(fā)現(xiàn)有 creep 死亡時该园,就可以根據(jù)其 memory 中的角色直接將其加入生成隊列酸舍,非常的簡單無腦。
creep 的同名問題
如果新生成的 creep 和原來的保持同名里初,那么它將 直接繼承原來的記憶啃勉。這在某種情況下是個壞處,例如這個 creep 需要在生成之后執(zhí)行一個準備階段双妨,但是它內(nèi)存中的一個字段表示它已經(jīng)完成了準備 (繼承了上個 creep 的記憶)淮阐。從而導致 creep 錯誤的跳過了準備階段。
雖然解決了 creep 的持續(xù)生成問題刁品,但是這種設計理念并不會檢查配置項的變動(剛才講的 creep 自檢查也不會 )泣特,也就是說新增的角色并不會自動生成。不過這種問題就屬于小問題了挑随,定時檢查配置項的變動群扶,并在變動時將新增的 creep 加入生成。這并不是什么難事不是么?
Spawn 生成邏輯
結束了數(shù)量檢查竞阐,spawn 生成邏輯就要簡單很多缴饭,可能細心的你已經(jīng)發(fā)現(xiàn)了,上文中所有提到新生成 creep 的時候都是用的 "向 Spawn 推送生成任務" 而非 "調(diào)用 Spawn 進行生成"骆莹。
為什么要這么說呢颗搂?首先強調(diào)一個結論:無論在什么情況下,都推薦 使用任務隊列進行 creep 生成幕垦。這樣寫可以將 spawn 的生成邏輯和其他邏輯解耦丢氢,方便維護。并且這種寫法足夠簡單先改,也足夠清晰疚察。
OK,來介紹一下流程:首先仇奶,其他想要進行 creep 生成的模塊不允許直接訪問Spawn.spawnCreep
方法貌嫡,而是調(diào)用 加入生成隊列 的函數(shù)將 creep 生成任務追加到隊列的末尾。同時该溯,每個 tick 里 spawn 只需要檢查任務隊列就可以 了岛抄,如果有的話就從隊列中彈出第一個任務進行生成。下面是流程圖:
并且狈茉,游戲也非常貼心的在每個 spawn 上都分配了一塊內(nèi)存可以使用夫椭,我們的任務隊列就保存在這里。
接下來我們來實現(xiàn)一下氯庆,這里我們在Spawn
原型上拓展三個方法蹭秋,一個是 spawn 的工作:檢查任務隊列,一個用來 讓其他模塊添加生成任務堤撵,還有一個 封裝 creep 生成的主要實現(xiàn):
// 檢查任務隊列
Spawn.prototype.work = function() {
// 代碼...
}
// 將生成任務推入隊列
Spawn.prototype.addTask = function(taskName) {
// 代碼...
}
// creep 生成主要實現(xiàn)
Spawn.prototype.mainSpawn = function(taskName) {
// 代碼...
}
首先是第一個拓展仁讨,work
方法:首先進行檢查,在無法生成時直接跳過來節(jié)省 cpu粒督。然后嘗試進行生成陪竿,如果生成成功的話就將完成生成的任務從隊列中移除禽翼。
// 檢查任務隊列
Spawn.prototype.work = function() {
// 自己已經(jīng)在生成了 / 內(nèi)存里沒有生成隊列 / 生產(chǎn)隊列為空 就啥都不干
if (this.spawning || !this.memory.spawnList || this.memory.spawnList.length == 0) return
// 進行生成
const spawnSuccess = this.mainSpawn(this.memory.spawnList[0])
// 生成成功后移除任務
if (spawnSuccess) this.memory.spawnList.shift()
}
然后是添加生成任務的方法屠橄,addTask
:這個方法將任務的名稱追加到任務隊列的末尾,然后返回它的排隊位置:
// 將生成任務推入隊列
Spawn.prototype.addTask = function(taskName) {
// 任務加入隊列
this.memory.spawnList.push(taskName)
return this.memory.spawnList.length
}
這樣闰挡,其他模塊想要生成 creep 就只需要調(diào)用這個方法锐墙,然后傳入期望要生成 creep 的任務名稱即可,極大的減輕了和其他模塊的耦合度长酗。
最后一個拓展方法mainSpawn
溪北,為了讓 spawn 的work()
邏輯保持清晰,我更傾向于將其封裝成一個獨立的方法。這個方法包含了 creep 生成的核心實現(xiàn)之拨,例如 通過任務名稱taskName
獲取任務的具體配置項茉继、內(nèi)存初始化、指定身體部件等蚀乔。因為 creep 生成實現(xiàn)的方法每個人的區(qū)別都比較大烁竭,所以這里不進行過多介紹,具體生成實現(xiàn)請結合自己的代碼進行修改吉挣。唯一一點需要注意的就是 在生成成功是返回true
派撕,work
方法將根據(jù)這個返回值決定是不是要移除已經(jīng)嘗試過生成的任務。
拓展:為什么要使用生成隊列睬魂?
假如我們有一個角色數(shù)量配置列表终吼,在進行生成檢查時,代碼會 按照這個列表的順序依次檢查氯哮,那么無論什么情況下际跪,在列表上方的角色都會被優(yōu)先生成。如果配置列表下方有一個重要角色A蛙粘,那么它上面的所有角色垫卤、無論重要不重要,都會先生成完出牧,才會輪到A穴肘。在很多情況下,這會導致導致房間運營因為某個重要角色的缺失從而陷入癱瘓舔痕。
如果你采用的是 creep 自檢查的話评抚,為了避免重復生成,一個 creep 一生只會發(fā)送一次重生任務伯复,假如 spawn 正在生成沒辦法立刻響應這個重生任務慨代,同時自身也沒有任務隊列保存這個任務,那么這個 creep 的任務就會被遺棄掉啸如,從而導致問題的產(chǎn)生侍匙。
接下來,我們來講一下最容易被忽視的一點:期望數(shù)量配置叮雳。
期望數(shù)量配置
在角色數(shù)量逐漸增多的時候想暗,很多人都會選擇進行第一次重構 —— 將所有角色數(shù)量及其配置放置在一個列表中來方便維護,如下:
const creepConfigs = [
{
role: 'harvester',
bodys: [ WORK, CARRY, MOVE ],
number: 1
}, {
role: 'upgrader',
bodys: [ WORK, CARRY, MOVE ],
number: 1
},
// 更多角色 ...
]
這是很重要的一部分帘不,可以極大的減輕你代碼的復雜程度说莫,并在某些時刻節(jié)省 cpu 消耗,例如我們在上一小節(jié)中介紹的 Spawn 生成邏輯 中寞焙,并沒有直接將 creep 的詳細生成信息一股腦的塞進生成隊列中储狭,而是將 任務名稱 放進隊列互婿,這里的任務名稱就可以是上面列表中的role
字段。然后在mainSpawn
中通過這個任務名稱來取出對應的配置項辽狈。
這么做有什么好處呢慈参,首先,保存在內(nèi)存中的數(shù)據(jù)每 tick 都要經(jīng)過JSON.stringify
的序列化刮萌,所以 內(nèi)存中的數(shù)據(jù)越簡單懂牧,所消耗的 cpu 也就越少。其次尊勿,這么做可以提高系統(tǒng)的響應效率僧凤,無論我們對其配置項進行什么修改,spawn 通過任務名稱取到的永遠是最新的配置元扔。如果 spawn 隊列里保存了整個任務的話躯保,就有可能導致保存了舊版本的角色配置,從而生成出一個不符合預期的 creep 來澎语。
動態(tài)調(diào)控期望
當我們將配置項抽象成一個獨立的列表后途事,就可以采取一些更”智能“的方式來管理它了。例如擅羞,我們可以額外設計一個模塊尸变,這個模塊會監(jiān)測房間的狀態(tài),從而調(diào)整某個角色的數(shù)量或者身體部件:
借此减俏,我們就可以完成房間的自動化運維召烂,從而在不影響代碼可讀性的前提下節(jié)省我們?nèi)肆S護成本。
不過需要注意的是娃承,如果你覺得自己對于游戲的了解還不夠深入的話奏夫,那么我是 不推薦在自己的代碼里加入這個自動數(shù)量維護的,因為你可以能會花不少的時間來設計這個模塊历筝,但最后因為經(jīng)驗不足導致需要大量重構酗昼,從而影響游戲體驗。至少在房間 8 級之前梳猪,你可以先手工維護來積累經(jīng)驗麻削。
設計小 tips
文章的最后,我們簡單的提幾點在設計上述系統(tǒng)時應該注意的幾點要素春弥。
支持多房間
首先呛哟,這個系統(tǒng)要支持多房間,因為我們?nèi)粘9芾砜隙ㄊ且苑块g為單位的惕稻。每個房間的數(shù)量配置都是不一樣的竖共。獨立的配置文件
其次蝙叛,代碼應該和配置文件解耦俺祠,將配置項單獨放在一個文件中,因為隨著基地的不斷發(fā)展,我們的creep
的數(shù)量蜘渣、身體的組成部分都是會頻繁變動的淌铐,沒有人會希望在修改這些配置時還要改一堆的代碼。多
Spawn
兼容
在教程中蔫缸,只使用了一個出生點腿准,但是在實際發(fā)展的時候,我們每個房間都可以擁有多個Spawn
的拾碌,所以在開發(fā)時我們也要考慮到這一點吐葱。例如,你可以不用 Spawn 自帶的內(nèi)存空間校翔,而是把孵化隊列存放到房間內(nèi)存中弟跑,這樣就可以多個 Spawn 共同處理孵化任務。
總結
本文我們將 creep 的數(shù)量控制系統(tǒng)劃分為三個模塊防症,數(shù)量檢查孟辑、spawn 生成 以及 期望數(shù)量配置。并簡單介紹了這三個模塊的設計思路以及彼此之間的關系蔫敲。當然饲嗽,本文所講的并不一定是最好的設計模式,如果你有更好的想法的話奈嘿,歡迎評論或者私信交流 ~
想要查看更多教程貌虾?歡迎訪問 《Screeps 中文教程》!