PrismatiXEngine
介面與擴充Extension 開發

製作 Extension

新增 Story 指令與 UI Action,再用 TypeScript 撰寫和打包擴充功能。

English

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.jsonextensions 陣列追加描述檔路徑:

{
  "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 走到這一行。診斷輸出會出現訊息;這個範例不會在畫面上顯示通知。

新增指令時,描述檔的 commandsRegisterCommand 都要使用相同 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 也要處理取消與資源清理。

參數與權限

參數支援 nullbooleanintegernumberstringvec2rectcoloruuidresourcetokenarrayobject,可指定必填、預設值、列舉值與範圍。

下面宣告一個加分指令,稍後會用 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,讓編譯器追蹤資源。

能力名稱包括 runtimeanimationuiaudiovideopersistenceinputrender,只加入功能實際需要的項目。safetyrollback 描述的是程式行為;狀態是否能正確回復,仍取決於程式實作與測試。

改用 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 SDKesbuild 安裝esbuild 選項

On this page