Skip to content

ECS ​

Engine 的 ECS 只增加 Object3D 缺少的 typed data、场景级行为、Entity 局部行为与 World 查询。Object3D scene graph 仍是唯一层级事实。

Entity 与 Object3D binding ​

ts
const rootEntity = engine.createEntity(gltf.scene);
engine.world.root.add(rootEntity);

const meshEntity = engine.getEntity(mesh);

createEntity(root) 要求 root 没有 parent,并一次性为完整 raw subtree 建立一对一 binding。子 Object3D 无需逐个调用 createEntity。

不传 root 时,Engine 创建一个 Group:

ts
const floating = engine.createEntity();
const child = engine.createEntity();

floating.add(child);          // 普通 reparent
engine.world.root.add(floating); // 整棵 floating subtree 进入 World

接纳后只通过 Entity facade 改层级:

名称签名说明
add(...children)this按 Three.js add 语义重新挂接,不保持世界变换。
attach(child)this按 Three.js attach 语义重新挂接,尽量保持世界变换。
remove(...children)this移除当前直接子 Entity,不销毁。
removeFromParent()this脱离 Entity parent,变为 floating subtree。
clear()this移除全部直接子 Entity,不销毁。
destroy()void递归终结子树并释放标准 Three.js 资源。

Entity 不能跨 Engine 挂接。World root 只能随 World 销毁。active frame 中会改变 World topology 的操作被拒绝。

World ​

每个 World 拥有一个原生 Scene,并以其 root Entity 的 ancestry 定义 membership:

ts
console.log(engine.world.scene);
console.log(entity.world); // World | null

const extra = engine.createWorld();
engine.destroyWorld(extra);

Entity 可以在同一 Engine 的 World 间移动,也可以保持 floating。默认 World 不能单独销毁;extra World 销毁时会移除绑定 View 并销毁完整 Scene Entity 子树。

Component ​

Component 是冻结、以 Definition object identity 区分的 Entity-local 同步 setup Definition:

ts
import { createComponent } from '@tripo3d/engine';

interface Velocity {
  x: number;
  y: number;
  z: number;
}

const Velocity = createComponent('velocity', ({ entity, onDestroy }) => {
  const velocity: Velocity = { x: 1, y: 0, z: 0 };
  const release = registerVelocityDebugState(entity.object, velocity);
  onDestroy(release);
  return velocity;
});

const velocity = entity.addComponent(Velocity);
velocity.x = 2;
entity.getComponent(Velocity);    // 与 velocity 是同一 identity
entity.hasComponent(Velocity);    // true
entity.removeComponent(Velocity); // true;先发布 absence,再执行 cleanup

setup 只接收 { engine, entity, onDestroy },严格同步执行且不接受 installation input。Promise 或自定义 thenable result 会被拒绝。setup 成功后,addComponent() 与 getComponent() 原样返回 result;重复 add 同一 Definition 直接返回现有 identity,不重新 setup。

ComponentDefinition 是 structural type,没有 nominal brand。createComponent() 是负责输入校验与冻结的规范构造入口;Engine 运行时以传入 Definition 的 object identity 作为 key,不检查它由哪个工厂创建。

setup 可以返回 undefined marker 或 primitive,但 Engine 不提供 replacement API。需要后续修改时返回 mutable object,并直接修改其字段;整体替换要先 removeComponent(),再重新 add。Engine 不自动调用 result 的 dispose(),也不替业务添加 Vue reactive、Proxy 或 markRaw。

onDestroy 只能在 setup 期间登记。普通 remove、Entity destroy 或 Engine dispose 会按 LIFO 尽量执行全部 callbacks;普通 remove 在 callback 前已从 Entity lookup 与 World Query 消失。Definition 本身可复用,不随任何 owner 销毁。

Query ​

World Query 是无注册、无 dispose 的 lazy 读取:

ts
const moving = engine.world.createQuery(Velocity);

moving.forEach((entity) => {
  const velocity = entity.getComponent(Velocity)!;
  entity.object.position.x += velocity.x;
});

const snapshot = moving.list();

组合条件:

ts
const visibleMoving = engine.world.createQuery({
  all: [Velocity],
  any: [Selected, Hovered],
  none: [Hidden],
});

forEach 不分配结果数组;list 返回独立快照。Query 每次读取当前 membership 与 Component presence,求值期间不可重入,也不应修改会影响当前结果的 presence/membership。

System:场景级能力 ​

ts
const MovementSystem = createSystem('movement', ({
  engine,
  onDestroy,
  onEvent,
  onResize,
  onUpdate,
}) => {
  const query = engine.world.createQuery(Velocity);
  const resource = createBusinessResource();

  onUpdate(({ delta, elapsed, frame, views }) => {
    query.forEach((entity) => {
      entity.object.position.x += entity.getComponent(Velocity)!.x * delta;
    });
  });
  onResize(size => resizeBusinessResource(resource, size));
  onEvent(resetEvent, () => reset(query));
  onDestroy(() => resource.dispose());

  return { query };
}, { enabled: true, order: 0 });

const state = engine.addSystem(MovementSystem);
engine.setSystemEnabled(MovementSystem, false);
engine.removeSystem(MovementSystem);

System Definition 的 order 同时用于 update 与 late-update;同 order 保持加入顺序。setup 同步执行一次,返回值保持业务原始 identity。

SystemContext 提供 onUpdate、onLateUpdate、onResize、onEnable、onDisable、onDestroy 以及 typed Event 方法。没有 mount、start、beforeRender 或 Manager API。

Script:Entity 局部能力 ​

ts
const SpinScript = createScript('spin', ({
  entity,
  onDestroy,
  onUpdate,
}) => {
  const speed = 0.8;

  onUpdate(({ delta, world, views }) => {
    entity.object.rotation.y += speed * delta;
  });
  onDestroy(() => console.log('script removed'));

  return { speed };
});

const script = entity.addScript(SpinScript);
entity.setScriptEnabled(SpinScript, false);
entity.removeScript(SpinScript);

Script 在 floating Entity 上可以完成 setup 与 resize,但只有进入 live World 后才 effective。进入 World 或重新启用时触发 onEnable;离开、disable、remove/destroy 时触发 onDisable。frame context 的 views 只包含当前 Script World 的 View。

Script 没有 order;它适合 Entity 级定制行为。需要跨实体扫描或明确调度顺序时用 System。

类型化 Event ​

ts
const selectionChanged = createEvent<Entity | null>('selection-changed');

const off = engine.onEvent(selectionChanged, (entity) => {
  console.log(entity);
});

engine.emitEvent(selectionChanged, selected);
off();

EngineEvent 是 opaque token;name 只用于诊断,同名 token 互不相同。dispatch 严格同步、按注册顺序且 fail-fast。System/Script 的 onEvent/onceEvent 会自动受 owner effective gate 控制,并在 owner 终态释放。

EventBus identity 不对外暴露,也没有字符串频道或 wildcard API。

生命周期时序 ​

  • setup、首次 resize 同步完成后才发布 owner;
  • frame 内新增 effective Script 延后到下一帧;
  • disable、remove、World leave、destroy 等负向事实立即阻止后续回调;
  • remove/destroy 的终态回调只执行一次;
  • Component/System/Script Definition 是可复用 blueprint,不被 Engine dispose;每个已安装 owner 的 cleanup 只执行一次。

下一步 ​

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