虚拟机 API
BlueprintVM
Section titled “BlueprintVM”蓝图虚拟机负责执行蓝图图:
import { BlueprintVM } from '@esengine/blueprint';
// 创建 VMconst vm = new BlueprintVM(blueprintAsset, entity, scene);
// 启动(触发 BeginPlay)vm.start();
// 每帧更新(触发 Tick)vm.tick(deltaTime);
// 停止(触发 EndPlay)vm.stop();
// 暂停/恢复vm.pause();vm.resume();
// 触发事件vm.triggerEvent('EventCollision', { other: otherEntity });vm.triggerCustomEvent('OnDamage', { amount: 50 });
// 调试模式vm.debug = true;在普通组件上运行(无需 ECS)
Section titled “在普通组件上运行(无需 ECS)”如果承载蓝图的类不是 ECS 组件(例如 Cocos 的 cc.Component),用 BlueprintRunner 把蓝图直接跑在该实例上,无需 Scene / 实体 / BlueprintSystem:
import { BlueprintRunner, registerAllComponentNodes } from '@esengine/blueprint';
@ccclass('Playground')@BlueprintExpose({ displayName: 'Playground' })export class Playground extends Component { @property(JsonAsset) bluePrintJson: JsonAsset = null!;
@BlueprintProperty({ displayName: '当前生命值', type: 'float' }) current = 100;
@BlueprintMethod({ displayName: '治疗', params: [{ name: 'amount', type: 'float' }] }) heal(amount: number) { this.current += amount; }
private runner: BlueprintRunner | null = null;
onLoad() { registerAllComponentNodes(); // 生成组件节点(进程内一次即可) this.runner = new BlueprintRunner(this.bluePrintJson!.json as any, { self: this }); this.runner.start(); } update(dt: number) { this.runner?.tick(dt); } onDestroy() { this.runner?.stop(); }}- Target 默认 = Self:组件方法/属性节点的
component引脚未连线时,默认作用于self(这里就是这个Playground实例),所以Heal节点什么都不用连就会调用this.heal(amount),参数走amount引脚传入。 entity/scene在BlueprintRunner下为null,纯 ECS 节点(Create Entity、Add Component 等)不可用;方法调用、属性读写、事件、流程、数学节点均正常工作。- 需要 ECS 集成时仍用
BlueprintComponent+BlueprintSystem。
interface ExecutionContext { blueprint: BlueprintAsset; // 蓝图资产 entity: Entity | null; // 当前实体(BlueprintRunner 下为 null) scene: IScene | null; // 当前场景(BlueprintRunner 下为 null) self: object | null; // 蓝图所绑定的宿主实例(方法/属性节点未连线时默认作用于它) deltaTime: number; // 帧间隔时间 time: number; // 总运行时间
// 获取输入值 evaluateInput(nodeId: string, pinName: string, defaultValue: unknown): unknown;
// 设置输出值 setOutput(nodeId: string, pinName: string, value: unknown): void;
// 变量访问 getVariable<T>(name: string): T; setVariable(name: string, value: unknown): void;}interface ExecutionResult { outputs?: Record<string, unknown>; // 输出值 nextExec?: string | null; // 下一个执行引脚(null 表示停止) nextExecs?: string[]; // 按顺序执行多个引脚(优先于 nextExec) delay?: number; // 延迟执行(秒) yield?: boolean; // 暂停到下一帧 error?: string; // 错误信息}一个 exec 输出引脚可以连到多个节点。VM 会按连线顺序执行全部连线,且每个分支跑完整条下游链后才进入下一个分支 —— 效果等同于 Sequence:
Event Begin Play ──┬──> Play Sound ──> Print "done" ← 分支 1 整条链先跑完 ├──> Log Data ← 再跑分支 2 └──> Print "ready" ← 最后分支 3分支之间相互独立:其中一个分支遇到 Delay 或报错,不会阻止其它分支执行。
需要在一个节点里触发多个自己的输出引脚时(如 Sequence),返回 nextExecs:
execute(): ExecutionResult { return { nextExecs: ['then0', 'then1', 'then2', 'then3'] };}未连线的引脚会被自动跳过。
执行器按节点类型注册为一个共享实例,所以需要跨执行保留的状态不能放在执行器字段上 —— 那样同类型的所有节点、所有蓝图实例都会共用同一份状态。用 context.getNodeState() 按节点存放:
@RegisterNode(MyCounterTemplate)export class MyCounterExecutor implements INodeExecutor { execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult { // ✅ 每个节点一份,蓝图重新 start() 时重置 const state = context.getNodeState(node.id, () => ({ count: 0 })); state.count++;
return { outputs: { count: state.count }, nextExec: 'exec' }; }}有多个 exec 输入引脚的节点(如 Gate 的 Open / Close)可以通过 node.data._lastInputPin 判断本次是从哪个引脚进入的。
与 ECS 集成
Section titled “与 ECS 集成”使用内置蓝图系统
Section titled “使用内置蓝图系统”import { Scene, Core } from '@esengine/ecs-framework';import { BlueprintSystem, BlueprintComponent } from '@esengine/blueprint';
// 添加蓝图系统到场景const scene = new Scene();scene.addSystem(new BlueprintSystem());Core.setScene(scene);
// 为实体添加蓝图const entity = scene.createEntity('Player');const blueprint = new BlueprintComponent();blueprint.blueprintAsset = await loadBlueprintAsset('player.blueprint.json');entity.addComponent(blueprint);触发蓝图事件
Section titled “触发蓝图事件”// 从实体获取蓝图组件并触发事件const blueprint = entity.getComponent(BlueprintComponent);if (blueprint?.vm) { blueprint.vm.triggerEvent('EventCollision', { other: otherEntity }); blueprint.vm.triggerCustomEvent('OnPickup', { item: itemEntity });}import { validateBlueprintAsset } from '@esengine/blueprint';
function saveBlueprint(blueprint: BlueprintAsset, path: string): void { if (!validateBlueprintAsset(blueprint)) { throw new Error('Invalid blueprint structure'); } const json = JSON.stringify(blueprint, null, 2); fs.writeFileSync(path, json);}async function loadBlueprint(path: string): Promise<BlueprintAsset> { const json = await fs.readFile(path, 'utf-8'); const asset = JSON.parse(json);
if (!validateBlueprintAsset(asset)) { throw new Error('Invalid blueprint file'); }
return asset;}