
前言
其實嚴格意義上來講,上一篇教程 中所搭建的并不是一個工程化的編程項目,他只是提供了一個更好看一點的“代碼編輯窗口”。而本篇教程將站在更專業(yè)的角度,解決你在游戲中會遇到的一些痛點:
優(yōu)缺點對比
優(yōu)點:
- 不需要再擔心代碼丟失:由于上一篇教程中我們依舊是在游戲的代碼目錄中進行的開發(fā),而當你網(wǎng)絡不好時有可能會導致你的代碼被直接吞掉(是真的所有代碼都消失了 ),而使用 rollup 打包后,無論游戲代碼再怎么被吞,也不會影響到我們的源代碼。
- 支持多文件夾:screeps 有個非常嚴重的問題就是不支持文件夾,這對于喜歡解耦的同學來說簡直是一場災難,而 rollup 可以完美的解決這個問題。
- 引入龐大的 npm 生態(tài):npm 是 node 的第三方庫管理器,我們可以通過它安裝成千上萬已經(jīng)上傳的第三方包。
缺點
- 沒有缺點,請繼續(xù)往下看
什么是 rollup?
你可能注意到了我們多次提到了 rollup,這是個啥東西呢?簡單來說,這是一個構建工具,構建嘛,你給他一堆東西(源代碼),它按照你的想法做一些事情,最后產(chǎn)出一個文件(成果代碼),很簡單對不對。
現(xiàn)在我們知道這么多就足夠了,如果你想了解更多的話,可以查看 rollup 中文文檔。接下來,我們就從頭開始,一步步的搭建我們的 screeps 游戲開發(fā)環(huán)境。請確保你電腦上安裝有 node 哦。
步驟1:項目配置及 rollup 安裝
我們先來做一些最基本的準備工作。首先新建一個文件夾,命名為 my-screeps-bot 或其他你喜歡的名字。然后用 VScode 打開這個文件夾,ctrl + ~ 打開終端,在其中輸入 npm init -y 進行項目初始化,再輸入 npm install -D @types/screeps @types/lodash@3.10.1 安裝自動補全。等命令執(zhí)行完畢后就可以看到如下目錄:

接下來我們在其中新建一個 src 目錄,這個目錄就是源代碼存放目錄,我們所有的 screeps 游戲代碼都將寫在這里,然后在其中新建 main.js 并填入如下代碼,這個就是 screeps 的游戲入口函數(shù):
// 游戲入口函數(shù)
export const loop = function () {
console.log('hello world')
}
你可能會發(fā)現(xiàn),不對啊,這和游戲里的入口寫法不一樣啊。是的,確切來說,我們現(xiàn)在使用的 import / export 是游戲默認使用的 module.exports 語法的升級版本(es6),這個語法目前游戲還不支持,所以你沒辦法直接使用,不過不用擔心,稍后我們的 rollup 會自動把這個語法編譯成游戲可以理解的樣子。
ok,測試代碼準備好了,接下來我們來安裝 rollup,在終端中執(zhí)行如下命令即可:
npm install -D rollup
然后在 package.json 中的 scripts 里添加一個 build 字段:
"scripts": {
"build": "rollup -cw",
"test": "echo \"Error: no test specified\" && exit 1"
},
現(xiàn)在 rollup 已經(jīng)安裝完成了,但是還無法運行,因為我們還沒有告訴 rollup 它要做什么工作,在根目錄中新建 rollup.config.js 并填入如下內(nèi)容: 注意是根目錄,不要添加到 src 里了
// 告訴 rollup 他要打包什么
export default {
// 源代碼的入口是哪個文件
input: 'src/main.js',
// 構建產(chǎn)物配置
output: {
// 輸出到哪個文件
file: 'dist/main.js',
format: 'cjs',
sourcemap: true
}
};
rollup 會默認在根目錄下尋找這個名字并作為自己的配置文件。好了現(xiàn)在萬事具備,我們在終端中執(zhí)行:npm run build 即可開始構建:

當看到如圖所示的內(nèi)容后,我們就已經(jīng)成功執(zhí)行了第一次編譯,你可以在 dist/main.js 目錄中看到我們的編譯成果:
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
// 游戲入口函數(shù)
const loop = function () {
console.log('hello world');
};
exports.loop = loop;
//# sourceMappingURL=main.js.map
可以看到,剛才的 export 語法已經(jīng)被轉(zhuǎn)換成了一些奇奇怪怪的代碼,雖然你有可能看不懂,不過沒有關系,游戲能看懂就行了。
并且還有一個好消息,雖然你還沒有做什么額外的配置,但是現(xiàn)在你的項目已經(jīng)可以支持創(chuàng)建文件夾了,你可以通過如下內(nèi)容進行測試:
src/modules/utils.js
/**
* 打印 hello world
*/
export const sayHello = function () {
console.log('hello world')
}
src/main.js
// 引入外部依賴
import { sayHello } from './modules/utils'
export const loop = function () {
sayHello()
}
并且當你把鼠標懸停到函數(shù)的調(diào)用代碼上時,可以發(fā)現(xiàn)我們寫在函數(shù)上的注釋被顯示出來了!

是的,這都得益于我們使用的 export / import 語法,讓 vscode 可以尋找到對應的函數(shù)并將其頭部注釋顯示出來,你可以在 這里 這里找到關于 export / import 更詳細的用法。
函數(shù)的頭部注釋必須使用多行注釋,并且支持 markdown 語法,你也可以使用 jsdoc 風格的注釋讓 vscode 更智能的提示你的函數(shù)。
如果你控制臺沒有關閉的話,你就會發(fā)現(xiàn)每當你保存代碼的時候,rollup 都會自動運行構建,然后在 dist 目錄中生成最新的代碼。真好,讓我們高呼自動化永遠滴神!現(xiàn)在讓我們?nèi)タ匆幌聵嫿ǔ晒?/p>

什么!為什么沒有看到我的新代碼!不用擔心,打開 main.js 就可以看到:
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
/**
* 封裝 hello world 邏輯并導出
*/
const sayHello = function () {
console.log('hello world');
};
// 引入外部依賴
const loop = function () {
sayHello();
};
exports.loop = loop;
//# sourceMappingURL=main.js.map
這就是 rollup 的本職工作:通過分析你的依賴代碼,將一個復雜嵌套、相互調(diào)用的項目打包成單獨一個文件,并且還會剔除掉那些沒有使用的代碼。也就是說,我們最終上傳到 screeps 運行的代碼只會有這一個文件,這不就另辟蹊徑的解決了 screeps 不能使用文件夾的問題了嘛?
ok,代碼已經(jīng)構建好了,但是現(xiàn)在成果代碼還在本地電腦上,接下來我們就需要把代碼傳遞給 screeps 服務器,這樣才能讓 screeps 運行我們寫的代碼。
步驟2:上傳代碼到 screeps
rollup 本身支持使用插件進行拓展,我們接下來就使用插件進行代碼上傳,上傳代碼到游戲服務器一共有兩種方法:
-
直接上傳至服務器:將打包好的代碼直接上傳到 screeps 服務器,只要啟用了 HTTP 訪問接口的游戲服務器都可以用(比如官服和一些大型私服 ),但是如果你網(wǎng)絡不太好的話,很容易出現(xiàn)上傳失敗的問題。使用插件
rollup-plugin-screeps。 -
復制到游戲客戶端目錄:將打包好的代碼自動復制到 screeps 的代碼存放目錄中,借助 screeps 客戶端將代碼進行上傳,所以只有游戲客戶端啟動時這種方式才有效果。這種一般都是用來訪問本地的小型測試服務器,使用插件
rollup-plugin-copy。
接下來我們開始進行配置,首先安裝我們需要的插件,打開終端指定下面命令:
npm install rollup-plugin-clear rollup-plugin-screeps rollup-plugin-copy -D
先在項目根目錄下新建文件 .secret.json 并填入如下內(nèi)容:
{
"main": {
"token": "你的 screeps token 填在這里",
"protocol": "https",
"hostname": "screeps.com",
"port": 443,
"path": "/",
"branch": "default"
},
"local": {
"copyPath": "你要上傳到的游戲路徑,例如 C:\\Users\\DELL\\AppData\\Local\\Screeps\\scripts\\screeps.com\\default"
}
}
注意需要填寫里邊的 main.token 字段和 local.copyPath 字段(如果你不想用這種方式的話可以直接不填 ),token 可以從 這里 獲取。copyPath 可以通過游戲客戶端控制臺左下角的 Open local folder 按鈕找到。
創(chuàng)建這個文件的目標是因為其中包含了我們的隱私信息,所以需要單獨拿出來,如果你想把代碼上傳到 github 上的話一定要把這個文件加入 .gitignore。
接下來我們改造一下 rollup.config.js,下面代碼直接覆蓋進去即可,注意我們還使用了 rollup-plugin-clear 插件在每次編譯前先清空目標代碼文件夾:
import clear from 'rollup-plugin-clear'
import screeps from 'rollup-plugin-screeps'
import copy from 'rollup-plugin-copy'
let config
// 根據(jù)指定的目標獲取對應的配置項
if (!process.env.DEST) console.log("未指定目標, 代碼將被編譯但不會上傳")
else if (!(config = require("./.secret.json")[process.env.DEST])) {
throw new Error("無效目標,請檢查 secret.json 中是否包含對應配置")
}
// 根據(jù)指定的配置決定是上傳還是復制到文件夾
const pluginDeploy = config && config.copyPath ?
// 復制到指定路徑
copy({
targets: [
{
src: 'dist/main.js',
dest: config.copyPath
},
{
src: 'dist/main.js.map',
dest: config.copyPath,
rename: name => name + '.map.js',
transform: contents => `module.exports = ${contents.toString()};`
}
],
hook: 'writeBundle',
verbose: true
}) :
// 更新 .map 到 .map.js 并上傳
screeps({ config, dryRun: !config })
export default {
input: 'src/main.js',
output: {
file: 'dist/main.js',
format: 'cjs',
sourcemap: true
},
plugins: [
// 清除上次編譯成果
clear({ targets: ["dist"] }),
// 執(zhí)行上傳或者復制
pluginDeploy
]
};
接下來打開 package.json 新增構建命令:
"scripts": {
"push": "rollup -cw --environment DEST:main",
"local": "rollup -cw --environment DEST:local",
...
},
ok,現(xiàn)在已經(jīng)一切準備就緒了,接下來我們測試一下。首先執(zhí)行 npm run push 來直接提交我們的代碼到游戲服務器,如果 rollup 還在運行的話,可以先用 ctrl + c 停止:
?。?!備份注意,執(zhí)行該條命令后,線上代碼將會被直接覆蓋,請在執(zhí)行前妥善保存!??!

由于插件是靜默提交的,所以當看到控制臺開始重新 waiting for changes 時就說明代碼已經(jīng)提交完成了,這時候打開游戲客戶端就可以看到我們的代碼已經(jīng)被上傳到了服務器并且正常執(zhí)行了(在網(wǎng)頁端上同樣可以看到):

如果你一直看不到代碼上傳成功,并且過一會之后發(fā)現(xiàn)終端里報了錯誤,這說明你的網(wǎng)絡還是不夠穩(wěn)定,你可以直接保存一下來重新觸發(fā)上傳,或者嘗試通過其他手段改善你的網(wǎng)絡。
接下來我們來試一下第二種方式,打開游戲 steam 客戶端后在 vscode 終端執(zhí)行 npm run local,等待 rollup 編譯完成后即可看到上傳成功:

然后即可在游戲客戶端中看到我們上傳的代碼,如果沒有看到的話,請檢查你復制到的目標文件夾是否是你當前代碼分支對應的文件夾。
使用 SourceMap 校正異常信息
到這里我們的項目配置基本已經(jīng)告一段落了并且可以使用了... 等等,不太對,最后所有的代碼都被打包到了一個文件里,那如果有代碼報錯的話,它提示的報錯位置豈不是和我源代碼的不一致?例如我在 src/modules/utils.js 里拋一個錯:
/**
* 顯示 hello world
*/
export const sayHello = function () {
console.log('hello world')
throw new Error('我是 sayHello 里的報錯')
}
然后上傳到游戲服務器之后就會發(fā)現(xiàn),這報錯信息是在 main.js 里而不是實際的 src/modules/utils.js 啊,難道每次遇到報錯我還要打開編譯后的代碼對比一下才能確定報錯的實際位置?

是的,你的感覺很敏銳,這雖然不會影響代碼的正常運行,但是會讓我們在查找 bug 時更加麻煩,那么怎么解決這個問題呢?回想一下,我們的編譯產(chǎn)物除了 main.js 之外是不是還有一個東西:

這個 .map 文件是什么呢?它的全稱叫做 SourceMap,是一張對照表,能夠描述代碼編譯前后的對應關系,而我們借助一些小工具的幫助,就可以讓報錯信息直接顯示對應的源代碼的位置!
我們要借助的小工具名字就叫做 source-map,是一個 npm 的第三方包,執(zhí)行如下命令即可安裝:
npm install source-map@0.6.1
然后新建文件 modules/errorMapper.js 并填入如下內(nèi)容:
/**
* 校正異常的堆棧信息
*
* 由于 rollup 會打包所有代碼到一個文件,所以異常的調(diào)用棧定位和源碼的位置是不同的
* 本模塊就是用來將異常的調(diào)用棧映射至源代碼位置
*
* @see https://github.com/screepers/screeps-typescript-starter/blob/master/src/utils/ErrorMapper.ts
*/
import { SourceMapConsumer } from 'source-map'
// 緩存 SourceMap
let consumer = null
// 第一次報錯時創(chuàng)建 sourceMap
const getConsumer = function () {
if (consumer == null) consumer = new SourceMapConsumer(require("main.js.map"))
return consumer
}
// 緩存映射關系以提高性能
const cache = {}
/**
* 使用源映射生成堆棧跟蹤,并生成原始標志位
* 警告 - global 重置之后的首次調(diào)用會產(chǎn)生很高的 cpu 消耗 (> 30 CPU)
* 之后的每次調(diào)用會產(chǎn)生較低的 cpu 消耗 (~ 0.1 CPU / 次)
*
* @param {Error | string} error 錯誤或原始追蹤棧
* @returns {string} 映射之后的源代碼追蹤棧
*/
const sourceMappedStackTrace = function (error) {
const stack = error instanceof Error ? error.stack : error
// 有緩存直接用
if (cache.hasOwnProperty(stack)) return cache[stack]
const re = /^\s+at\s+(.+?\s+)?\(?([0-z._\-\\\/]+):(\d+):(\d+)\)?$/gm
let match
let outStack = error.toString()
console.log("ErrorMapper -> sourceMappedStackTrace -> outStack", outStack)
while ((match = re.exec(stack))) {
// 解析完成
if (match[2] !== "main") break
// 獲取追蹤定位
const pos = getConsumer().originalPositionFor({
column: parseInt(match[4], 10),
line: parseInt(match[3], 10)
})
// 無法定位
if (!pos.line) break
// 解析追蹤棧
if (pos.name) outStack += `\n at ${pos.name} (${pos.source}:${pos.line}:${pos.column})`
else {
// 源文件沒找到對應文件名,采用原始追蹤名
if (match[1]) outStack += `\n at ${match[1]} (${pos.source}:${pos.line}:${pos.column})`
// 源文件沒找到對應文件名并且原始追蹤棧里也沒有,直接省略
else outStack += `\n at ${pos.source}:${pos.line}:${pos.column}`
}
}
cache[stack] = outStack
return outStack
}
/**
* 錯誤追蹤包裝器
* 用于把報錯信息通過 source-map 解析成源代碼的錯誤位置
* 和原本 wrapLoop 的區(qū)別是,wrapLoop 會返回一個新函數(shù),而這個會直接執(zhí)行
*
* @param next 玩家代碼
*/
export const errorMapper = function (next) {
return () => {
try {
// 執(zhí)行玩家代碼
next()
}
catch (e) {
if (e instanceof Error) {
// 渲染報錯調(diào)用棧,沙盒模式用不了這個
const errorMessage = Game.rooms.sim ?
`沙盒模式無法使用 source-map - 顯示原始追蹤棧<br>${_.escape(e.stack)}` :
`${_.escape(sourceMappedStackTrace(e))}`
console.log(`<text style="color:#ef9a9a">${errorMessage}</text>`)
}
// 處理不了,直接拋出
else throw e
}
}
}
這段代碼的主要作用就是讀取同目錄下的 main.js.map.js 文件,并用其校正我們的異常追蹤棧。接下來我們在 main.js 中引用它(代碼直接覆蓋即可 ):
import { errorMapper } from './modules/errorMapper'
import { sayHello } from './modules/utils'
export const loop = errorMapper(() => {
sayHello()
})
這里使用我們剛才定義的錯誤捕獲器包裹整個入口函數(shù),這樣其中的代碼報錯都會被捕獲然后進行校正。ok,然后我們把代碼提交到游戲:
[下午8:46:34][shard2]Error: Unknown module 'source-map'
at Object.requireFn (<runtime>:46500:23)
at main:5:17
at main:118:3
at Object.exports.evalCode (<runtime>:15845:76)
at Object.requireFn (<runtime>:46518:28)
at Object.exports.run (<runtime>:46461:60)
運行之后發(fā)現(xiàn),代碼報錯了!我們找不到 source-map 這個模塊!這是什么原因?qū)е碌哪兀?/p>
如果我們打開我們上傳后的代碼,就可以發(fā)現(xiàn)編譯后的代碼使用了下面這行代碼來引入 source-map:
var sourceMap = require('source-map');
但是我們并沒有把這個模塊的代碼上傳到 screeps,自然就出現(xiàn)了找不到模塊的問題。所以,接下來我們要 把 source-map 也打包進我們的最終代碼。首先我們來安裝所需的 rollup 插件:
npm install -D @rollup/plugin-node-resolve @rollup/plugin-commonjs
然后在 rollup.config.js 中引入并調(diào)用這兩個模塊:
// 在代碼頭部引入包
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
// ...
// 在 plugins 中調(diào)用插件
export default {
// ...
plugins: [
// 清除上次編譯成果
clear({ targets: ["dist"] }),
// 打包依賴
resolve(),
// 模塊化依賴
commonjs(),
// 執(zhí)行上傳或者復制
pluginDeploy
]
};
現(xiàn)在重新構建并把代碼上傳到 screeps,這次我們就可以看到剛才的測試報錯指向了正確的位置:

結束
ok,這次我們真的完成了本階段的所有配置,下面是項目的文件目錄,之后我們在 src 文件中進行游戲代碼的開發(fā)即可:

本篇教程介紹了如何使用 rollup 完成 screeps 項目的構建,并解決了代碼被清、不支持多文件夾以及異常追蹤棧不準確的問題。注意最后,我們引入了其他人開發(fā)的 source-map 模塊,如果你有興趣的話,也可以嘗試把自己的代碼封裝成模塊并發(fā)布到 npm,這樣其他的玩家就可以按照相同的方式非常簡單的引入并使用你的模塊。
接下來,你可以開始把精力投入到 screeps 的游玩中,也可以參照 Screeps 使用 TypeScript 進行靜態(tài)類型檢查 來升級你的項目。想要了解更多 Screeps 的中文教程?歡迎訪問 Screeps - 中文系列教程!