腳本模型與生命週期
五類腳本基類、逐實例狀態、生命週期時序,以及執行緒與崩潰隔離語義。
五類腳本
包含 <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一致。- 三個生命週期函式全部可選 override(基類提供空實作),參數簽章固定。
- 一個翻譯單元註冊一個腳本;多個腳本就分成多個模組程式庫。
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 與宿主服務)。