製作 Extension
新增 Story 指令與 UI Action,再用 TypeScript 撰寫和打包擴充功能。
Extension 用來加入遊戲邏輯。這章先用 JavaScript 做一個記錄訊息的 Story 指令,以及一個開啟存檔畫面的按鈕動作,最後再改用 TypeScript。
建立描述檔和程式
在遊戲專案內新增:
Content/Extensions/tutorial.pxextension
Content/Extensions/tutorial.js.pxextension 告訴引擎要載入哪個程式、有哪些指令,以及需要哪些權限。tutorial.pxextension 的完整內容如下:
{
"format": "PrismatiXExtension",
"schemaRevision": 2,
"language": "javascript",
"id": "tutorial",
"version": "1.0.0",
"requiredEngineVersion": ">=0.2.0 <0.3.0",
"entry": "tutorial.js",
"modules": [],
"capabilities": ["runtime", "ui"],
"safety": {
"previewSafe": true,
"deterministic": true,
"seekSafe": true,
"rollbackSafe": true
},
"commands": [
{
"id": "tutorial.note",
"await": false,
"rollback": "transient",
"capabilities": ["runtime"],
"parameters": [
{"name": "message", "type": "string", "required": true}
]
}
],
"actions": [
{
"id": "tutorial.openSave",
"reentry": "ignoreWhileRunning",
"capabilities": ["ui"],
"parameters": []
}
]
}接著在 tutorial.js 實作這兩個功能:
Engine.RegisterCommand('tutorial.note', ({ message }) => {
if (typeof message !== 'string') {
throw new TypeError('tutorial.note requires a string message');
}
Engine.log('tutorial.note', message);
});
Engine.RegisterAction('tutorial.openSave', () => {
Engine.ShowModal('save');
});Engine 由 Player 或 Preview 提供,這份程式要在遊戲裡執行。方法名稱區分大小寫:註冊指令用 RegisterCommand,記錄訊息用小寫的 log。
加入專案
在 prismatix.json 的 extensions 陣列追加描述檔路徑:
{
"extensions": ["Content/Extensions/tutorial.pxextension"]
}專案設定中的路徑相對於專案根目錄;描述檔的 entry 則相對於描述檔所在的資料夾。因此這裡只寫 tutorial.js。
在 Story 呼叫
在 Story/main.pxstory 的 [end] 前加入:
[id tutorial.note.first]
[tutorial.note message="Hello from the extension"]執行 prismatix validate my-game,再開 Preview 走到這一行。診斷輸出會出現訊息;這個範例不會在畫面上顯示通知。
新增指令時,描述檔的 commands 與 RegisterCommand 都要使用相同 ID。缺少 message 或型別不符,編譯器會指出錯誤。驗證通過後仍須實際執行到該行,測試 callback。
綁定 UI Action
找到既有 .pxui 按鈕,把它的 onClick 改為:
{
"id": "tutorial.openSave",
"arguments": {}
}保留按鈕原本的 ID、位置及其他設定。這個 Action 會開啟預設 save 路由,適合先放在遊戲中的介面測試;若已移除預設路由,就要改成自己提供的畫面。
commands 給 Story 呼叫,actions 給 UI 呼叫,所以 tutorial.openSave 不能直接寫成 Story 指令。
reentry 決定前一次執行尚未結束時,如何處理再次點擊:allow 允許再次執行,ignoreWhileRunning 忽略,restart 重新開始。非同步 Action 也要處理取消與資源清理。
參數與權限
參數支援 null、boolean、integer、number、string、vec2、rect、color、uuid、resource、token、array、object,可指定必填、預設值、列舉值與範圍。
下面宣告一個加分指令,稍後會用 TypeScript 實作。把這筆資料追加到 commands:
{
"id": "tutorial.addScore",
"await": true,
"rollback": "reversible",
"capabilities": ["runtime"],
"parameters": [
{"name": "amount", "type": "integer", "required": true,
"range": {"minimum": 0, "maximum": 10}}
]
}Extension 的 color 使用 0–255 四個整數通道,與 shader uniform 使用的 0–1 不同。檔案參數適合宣告成 resource,讓編譯器追蹤資源。
能力名稱包括 runtime、animation、ui、audio、video、persistence、input、render,只加入功能實際需要的項目。safety 與 rollback 描述的是程式行為;狀態是否能正確回復,仍取決於程式實作與測試。
改用 TypeScript
在遊戲根目錄安裝建置工具。此處的 runtime SDK 對應 0.2.0,須能從使用中的 registry 取得,詳見安裝說明。
npm install --save-dev --save-exact @prismatix/runtime@0.2.0 esbuild先依變數教學宣告 score,再建立 Scripts/tutorial.ts:
import { createPrismatiXContext, defineAction, defineCommand } from '@prismatix/runtime';
const ctx = createPrismatiXContext();
defineCommand<{ message: string }>('tutorial.note', ({ message }) => {
ctx.raw.log('tutorial.note', message);
});
defineAction('tutorial.openSave', () => {
ctx.ui.showModal('save');
});
defineCommand<{ amount: number }>('tutorial.addScore', async ({ amount }) => {
const current = Number(ctx.variables.get('score') ?? 0);
ctx.variables.set('score', current + amount, 'session');
await ctx.time.wait(0.1);
});這份來源保留前面的兩個註冊,再加入加分指令。ctx.time.wait(0.1) 等待 0.1 秒,交由引擎排程;它與 Story 的毫秒單位不同。
把 TypeScript 和 SDK 打包到描述檔指定的 JavaScript:
npm exec -- esbuild Scripts/tutorial.ts --bundle --format=esm --platform=neutral --target=es2020 --outfile=Content/Extensions/tutorial.js
npm exec -- prismatix validate .之後在 Story 使用 [tutorial.addScore amount=1]。可以把打包命令加入 package.json,例如命名為 extensions:build,保留既有 scripts。
每次修改 TypeScript 都要重新打包,因為 CLI 監看的是輸出的 JavaScript。esbuild 只轉譯與打包,型別檢查需另外配置 TypeScript。Player 不會解析 npm 套件,輸出中不能留下未打包的套件 import、Node.js 或 DOM 依賴。
拆成多個模組
純 JavaScript 可以使用 import { value } from './lib/value.js',並在描述檔的 modules 登記 ["lib/value.js"]。import 路徑相對於發出 import 的模組,modules 則相對於描述檔。
前面的 TypeScript 範例打包成單一檔案,因此保留 modules: []。需要保存擴充自己的資料時,使用遊戲變數或 state provider;讀檔與非同步指令的處理見 Runtime API與存檔。
實作參考:Extension schema、官方範例、Runtime SDK、esbuild 安裝、esbuild 選項。