腳本模型與生命週期

五類腳本

包含 <mtr/script.hpp>,繼承對應基類並用註冊巨集匯出。基類模板參數是逐實例狀態類型:

資源基類註冊巨集資料包裝器
車輛mtr::VehicleScript<State>MTR_REGISTER_VEHICLE_SCRIPTmtr::Vehicle
PIDSmtr::PidsScript<State>MTR_REGISTER_PIDS_SCRIPTmtr::Pids
屏蔽門mtr::PsdScript<State>MTR_REGISTER_PSD_SCRIPTmtr::Psd
電梯mtr::LiftScript<State>MTR_REGISTER_LIFT_SCRIPTmtr::Lift
裝飾mtr::EyecandyScript<State>MTR_REGISTER_EYECANDY_SCRIPTmtr::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入參為空
-2ABI 版本不符(宿主與模組協商失敗)
-3資源類型不符(例如車輛模組被當作 PIDS 呼叫)

這些傳回碼由 SDK 生成,一般腳本程式碼無需理會;若你在寫自訂宿主 / 測試驅動,按上表解釋即可(詳見 C ABI 與宿主服務)。


接下來:資料快照 —— render 時能讀到甚麼;繪製與畫布 —— 怎樣畫出來。