Skip to content

插件系统

Plugin 是 engine 级别的单例横切扩展。它不关心具体的 System / Component / Script 蓝图,而是在每一个新实例创建完成后拿到一次改写机会——典型用途包括响应式包装、日志埋点、错误捕获等。

EnginePlugin 接口

一个插件就是一个对象,按需实现若干可选钩子。

字段签名说明
namestring插件名,用于调试 / 检索(必填)
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 }) => voidengine.dispose 时调用,负责释放插件自身持有的资源

三个 onXCreated 上下文对象共享以下字段:enginedefinition(对应蓝图)、instance(当前实例,可能已被前序插件替换过)、replace(next)(把实例替换为 next);组件 / 脚本上下文额外带有 entityobject(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 完成后,按插件注册顺序逐个 onSystemCreated
  • entity.addComponent(def)组件 setup 完成后,逐个 onComponentCreated
  • entity.addScript(def)脚本 setup 完成后,逐个 onScriptCreated
  • engine.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 项目里几乎是必装项。它只做三件事:

  1. shallowReactive 包裹 每个新建的 System / Component / Script 实例,使其属性变化能在 Vue 模板中触发更新
  2. markRaw 重对象 —— 实例顶层属性若是 Object3D / Material / Texture / BufferGeometry / Camera / Scene / WebGLRenderer 等 Three.js 对象,会被标记为 raw,避免被深度代理
  3. 依据 meta.vue 微调 —— 蓝图可以在 meta.vue.rawKeys 指定额外需 markRaw 的属性键,或用 meta.vue.reactivity: 'none' 跳过响应式包装

配置

名称 类型 默认值 说明
markEngineRawbooleantruesetup 阶段是否对 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 声明式注入
  • 架构总览 —— 插件在帧循环中的位置

基于 MIT 协议发布(内部使用)