腳本模型與生命週期
五類腳本基類、逐實例狀態、生命週期時序,以及執行緒與崩潰隔離語意。
五類腳本
包含 <mtr/script.hpp>,繼承對應基類並用註冊巨集匯出。基類樣板參數是逐實例狀態型別:
| 資源 | 基類 | 註冊巨集 | 資料包裝器 |
|---|---|---|---|
| 車輛 | mtr::VehicleScript<State> | MTR_REGISTER_VEHICLE_SCRIPT | mtr::Vehicle |
| PIDS | mtr::PidsScript<State> | MTR_REGISTER_PIDS_SCRIPT | mtr::Pids |
| 屏蔽門 | mtr::PsdScript<State> | MTR_REGISTER_PSD_SCRIPT | mtr::Psd |
| 電梯 | mtr::LiftScript<State> | MTR_REGISTER_LIFT_SCRIPT | mtr::Lift |
| 裝飾 | mtr::EyecandyScript<State> | MTR_REGISTER_EYECANDY_SCRIPT | mtr::EyeCandy |
一個車輛腳本骨架(其餘三類只換基類與參數型別):
#include <mtr/script.hpp>
struct MyVehicleState {
mtr::CycleTracker dest{"KOWLOON|TSUEN WAN"}; // 你自己的任意 C++ 型別
mtr::Api::Object tracker; // Java 物件控制代碼
std::unique_ptr<mtr::GraphicsTexture> lcd; // 持有的資源
};
struct MyVehicleScript : mtr::VehicleScript<MyVehicleState> {
static constexpr auto ID = "demo:vehicle";
void create(mtr::VehicleContext& ctx, MyVehicleState& state,
const mtr::Vehicle& vehicle) override {
// 首幀前呼叫一次:建立資源、註冊回呼
}
void render(mtr::VehicleContext& ctx, MyVehicleState& state,
const mtr::Vehicle& vehicle) override {
// 每幀呼叫:讀取快照、寫入繪製記錄
}
void dispose(mtr::VehicleContext& ctx, MyVehicleState& state,
const mtr::Vehicle& vehicle) override {
// 實例銷毀前呼叫:釋放資源
}
};
MTR_REGISTER_VEHICLE_SCRIPT(MyVehicleScript)
ID是腳本的命名空間 id(如demo:vehicle),必須與資源包mtr_custom_resources.json裡宣告條目的id一致。- 三個生命週期函式全部可選擇性覆寫(基類提供空實作),參數簽章固定。
- 一個轉譯單元註冊一個腳本;多個腳本就分成多個模組程式庫。
State:逐實例狀態
每個腳本實例(每輛車、每個 PIDS 方塊實體……)各有一塊獨立的 State,由宿主配置並建構:
- 宿主按
mtrStateSize()(由註冊巨集從sizeof(State)生成)配置狀態區塊,在首次使用前經mtrInit()用 placement-new 建構——不要假設零填充記憶體可用(std::string等非平凡型別在零記憶體上是未定義行為)。 - 狀態區塊內容跨幀持久:把紋理、模型控制代碼、回呼、快取放在這裡。
- 腳本類別的成員(你在
MyVehicleScript裡寫的欄位)則由該模組的全部實例共用——只放唯讀常數與全域設定。 dispose傳回後狀態即被解構;實例從用戶端消失約 5 秒後回收、資源包重新載入(F3+T)時全部銷毀。
提示
背景回呼物件(mtr::Api::Callback)必須保存在 State 裡直到銷毀——Java 側可能在任意時刻呼叫它。
生命週期時序
配置 + 建構
宿主為實例配置狀態區塊並呼叫 mtrInit() → State() 建構。
create
實例首次渲染前呼叫一次。create 期間提交的繪製記錄會被保留到第一幀 render 複用——一次性靜態內容(形狀、常駐貼圖)放在這裡,render 只畫動態部分。資源取得、Api 呼叫、回呼註冊也適合放在這裡。
render(每幀)
讀取快照 → 計算 → 把繪製記錄寫進幀記憶體區。快照與包裝器只在本次呼叫內有效;宿主在 render 傳回後立即讀取輸出並回收幀。實例不可見期間 render 不會被呼叫,但狀態原樣保留。
dispose
實例回收或重新載入時呼叫。在這裡關閉紋理 / 模型等持有的資源;State 解構緊隨其後。即使 dispose 擲出例外,狀態也會被安全解構。
與 JS 的對應
這套 create / render / dispose + 逐實例 state 正是 JCM 為 JS 腳本提供的模型(每個 ScriptInstance 一個 state 物件)的原生映射,遷移 JS 腳本時邏輯可以逐段對譯。
執行緒模型
- 宿主為每個模組在單一背景執行緒上序列呼叫其全部生命週期函式(對應 JCM 對 JS 腳本的逐腳本單執行緒執行器)——同一實例的
create / render / dispose永遠不會並行。 - 但 render 與非同步回呼可以並行(比如
BackgroundWorker裡提交的任務此刻在別的執行緒跑):腳本自己負責同步共用可變狀態。 - 資源包重新載入會先停止回呼、等待進行中的呼叫傳回,再銷毀狀態並卸載程式碼。
崩潰隔離
註冊巨集把每個匯出函式包進崩潰防護(crash_guard.hpp):
- 分段錯誤等硬體故障 → 只停用該模組,遊戲行程繼續執行;
latest.log留下一行native script faulted … module disabled。 - 未被捕捉的 C++ 例外同樣被防護攔截——但請自行 try/catch 以保證語意清晰。
- POSIX 訊號不會被 longjmp 式的「復原」:越過訊號處理常式的跳轉比崩潰本身更危險,模組直接下線是刻意設計。
傳回碼
| mtrCreate / mtrRender / mtrDispose 傳回 | 含義 |
|---|---|
0 | 成功 |
-1 | 空入參 |
-2 | ABI 版本不符(宿主與模組協商失敗) |
-3 | 資源類型不符(例如車輛模組被當作 PIDS 呼叫) |
這些傳回碼由 SDK 生成,一般腳本程式碼無需關心;若你在寫自訂宿主 / 測試驅動程式,按上表解釋即可(詳見 C ABI 與宿主服務)。