ECS
Engine 的 ECS 只增加 Object3D 缺少的 typed data、场景级行为、Entity 局部行为与 World 查询。Object3D scene graph 仍是唯一层级事实。
Entity 与 Object3D binding
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:
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:
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:
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,再执行 cleanupsetup 只接收 { 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 读取:
const moving = engine.world.createQuery(Velocity);
moving.forEach((entity) => {
const velocity = entity.getComponent(Velocity)!;
entity.object.position.x += velocity.x;
});
const snapshot = moving.list();组合条件:
const visibleMoving = engine.world.createQuery({
all: [Velocity],
any: [Selected, Hovered],
none: [Hidden],
});forEach 不分配结果数组;list 返回独立快照。Query 每次读取当前 membership 与 Component presence,求值期间不可重入,也不应修改会影响当前结果的 presence/membership。
System:场景级能力
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 局部能力
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
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 只执行一次。