脚本模型与生命周期
五类脚本基类、逐实例状态、生命周期时序,以及线程与崩溃隔离语义。
五类脚本
包含 <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 与宿主服务)。