插件系统
Plugin 是 engine 级别的单例横切扩展。它不关心具体的 System / Component / Script 蓝图,而是在每一个新实例创建完成后拿到一次改写机会——典型用途包括响应式包装、日志埋点、错误捕获等。
EnginePlugin 接口
一个插件就是一个对象,按需实现若干可选钩子。
| 字段 | 签名 | 说明 |
|---|---|---|
name | string | 插件名,用于调试 / 检索(必填) |
setup | (ctx: { engine }) => void | 插件被 use() 安装时调用一次 |
onSystemCreated | (ctx: EnginePluginSystemContext<T>) => void | 每次 addSystem 创建实例后调用,可通过 ctx.replace(next) 替换实例 |
onComponentCreated | (ctx: EnginePluginComponentContext<T>) => void | 每次 entity.addComponent 创建实例后调用,可 replace |
onScriptCreated | (ctx: EnginePluginScriptContext<T>) => void | 每次 entity.addScript 创建实例后调用,可 replace |
dispose | (ctx: { engine }) => void | engine.dispose 时调用,负责释放插件自身持有的资源 |
三个 onXCreated 上下文对象共享以下字段:engine、definition(对应蓝图)、instance(当前实例,可能已被前序插件替换过)、replace(next)(把实例替换为 next);组件 / 脚本上下文额外带有 entity 与 object(Three.js Object3D)。
注册方式
插件既可以在 createEngine 时声明,也可以事后挂载。engine.use 对同一实例幂等——已安装过则跳过。
ts
import { createEngine } from '@tripo3d/engine';
import { createPluginVue } from '@tripo3d/engine/vue';
// 1) 构造时声明
const engine = createEngine({
plugins: [createPluginVue()],
});
// 2) 事后安装(返回 engine 自身,可链式)
engine.use(myLoggerPlugin).use(myMetricsPlugin);
// 重复安装同一个插件实例会被跳过,不会触发第二次 setup
engine.use(myLoggerPlugin);生命周期
插件在 engine 全生命周期中的执行时机:
engine.use(plugin)加入插件列表并立即调用 plugin.setup(ctx)engine.addSystem(def)内部 setup 完成后,按插件注册顺序逐个 onSystemCreatedentity.addComponent(def)组件 setup 完成后,逐个 onComponentCreatedentity.addScript(def)脚本 setup 完成后,逐个 onScriptCreatedengine.dispose()按插件注册顺序逐个 plugin.dispose(ctx)
由于每个钩子都能通过 replace(next) 替换下游传递的实例,后装的插件看到的是前序插件的结果;建议把需要「原始实例」的插件放在列表最前。
实现自定义 Plugin
下面是一个完整示例——为每个新创建的 System 包一层打点代理,打印所有方法调用。
ts
import type { EnginePlugin } from '@tripo3d/engine';
/** 为每个 system 方法包一层 console.log 的调试插件。 */
export const createLoggerPlugin = (): EnginePlugin => {
return {
name: 'logger',
setup({ engine }) {
console.log(`[logger] attached to engine ${engine.id}`);
},
onSystemCreated(ctx) {
if (typeof ctx.instance !== 'object' || ctx.instance === null) {
return;
}
const wrapped: Record<string, unknown> = { ...ctx.instance as object };
for (const key of Object.keys(wrapped)) {
const value = (wrapped as Record<string, unknown>)[key];
if (typeof value !== 'function') continue;
wrapped[key] = (...args: unknown[]) => {
console.log(`[${ctx.definition.name}] ${key}(...)`, args);
return (value as (...a: unknown[]) => unknown).apply(ctx.instance, args);
};
}
ctx.replace(wrapped as typeof ctx.instance);
},
dispose({ engine }) {
console.log(`[logger] detached from engine ${engine.id}`);
},
};
};使用:
ts
createEngine({ plugins: [createLoggerPlugin()] });createPluginVue 深入
createPluginVue 是官方附带的 Vue 响应式适配插件,在 Vue 项目里几乎是必装项。它只做三件事:
shallowReactive包裹 每个新建的 System / Component / Script 实例,使其属性变化能在 Vue 模板中触发更新markRaw重对象 —— 实例顶层属性若是Object3D/Material/Texture/BufferGeometry/Camera/Scene/WebGLRenderer等 Three.js 对象,会被标记为 raw,避免被深度代理- 依据
meta.vue微调 —— 蓝图可以在meta.vue.rawKeys指定额外需markRaw的属性键,或用meta.vue.reactivity: 'none'跳过响应式包装
配置
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
markEngineRaw | boolean | true | setup 阶段是否对 engine 本体及其 events / history / lifecycle / pipeline / plugins / pointer / view / world 执行 markRaw。关闭后 engine 将被 Vue 深代理,性能急剧下降 |
蓝图侧声明
ts
const MySystem = createSystem('MySystem', (ctx) => ({
// ...
}), {
meta: {
vue: {
rawKeys: ['gizmo', 'controls'],
reactivity: 'shallow', // 或 'none' 完全跳过
},
},
});即便不写 meta,createPluginVue 也会自动识别 Three.js 重型对象并 markRaw,一般无需额外配置。
最佳实践
- 一个 engine 配一个插件实例。
createPluginVue()内部维护了一个WeakMap<instance, reactiveProxy>缓存,跨 engine 共用会造成缓存混乱 - 副作用要放在
onXCreated或 System 的 lifecycle 回调里,而不是放在setup里。setup里最好只做「登记 / 订阅」,保证插件可安全重复构造 dispose必须幂等。engine.dispose 会同步触发,插件不能依赖「只被调一次」;也不要在 dispose 里抛错,否则会阻塞后续插件的清理- 不要在插件里直接持有 System 实例引用。用
engine.getSystem(def)按需查询,避免 engine 销毁后悬挂引用 - 顺序相关的插件显式排序。若插件 A 需要看到插件 B 替换前的实例,把 A 放在 B 前面
下一步
- Vue 集成 —— 完整的
<EngineCanvas>+useEngine+createPluginVue组合示例 - createEngine —— 通过
options.plugins声明式注入 - 架构总览 —— 插件在帧循环中的位置