Files
esengine/docs/en/modules/blueprint/index.md
YHH 4a16e30794 docs(modules): 添加框架模块文档 (#350)
* docs(modules): 添加框架模块文档

添加以下模块的完整文档:
- FSM (状态机): 状态定义、转换条件、优先级、事件监听
- Timer (定时器): 定时器调度、冷却系统、服务令牌
- Spatial (空间索引): GridSpatialIndex、AOI 兴趣区域管理
- Pathfinding (寻路): A* 算法、网格地图、导航网格、路径平滑
- Procgen (程序化生成): 噪声函数、种子随机数、加权随机

所有文档均基于实际源码 API 编写,包含:
- 快速开始示例
- 完整 API 参考
- 实际使用案例
- 蓝图节点说明
- 最佳实践建议

* docs(modules): 添加 Blueprint 模块文档和所有模块英文版

新增中文文档:
- Blueprint (蓝图可视化脚本): VM、自定义节点、组合系统、触发器

新增英文文档 (docs/en/modules/):
- FSM: State machine API, transitions, ECS integration
- Timer: Timers, cooldowns, service tokens
- Spatial: Grid spatial index, AOI management
- Pathfinding: A*, grid map, NavMesh, path smoothing
- Procgen: Noise functions, seeded random, weighted random
- Blueprint: Visual scripting, custom nodes, composition

所有文档均基于实际源码 API 编写。
2025-12-26 20:02:21 +08:00

9.8 KiB

Blueprint Visual Scripting

@esengine/blueprint provides a full-featured visual scripting system supporting node-based programming, event-driven execution, and blueprint composition.

Installation

npm install @esengine/blueprint

Quick Start

import {
    createBlueprintSystem,
    createBlueprintComponentData,
    NodeRegistry,
    RegisterNode
} from '@esengine/blueprint';

// Create blueprint system
const blueprintSystem = createBlueprintSystem(scene);

// Load blueprint asset
const blueprint = await loadBlueprintAsset('player.bp');

// Create blueprint component data
const componentData = createBlueprintComponentData();
componentData.blueprintAsset = blueprint;

// Update in game loop
function gameLoop(dt: number) {
    blueprintSystem.process(entities, dt);
}

Core Concepts

Blueprint Asset Structure

Blueprints are saved as .bp files:

interface BlueprintAsset {
    version: number;             // Format version
    type: 'blueprint';           // Asset type
    metadata: BlueprintMetadata; // Metadata
    variables: BlueprintVariable[]; // Variable definitions
    nodes: BlueprintNode[];      // Node instances
    connections: BlueprintConnection[]; // Connections
}

Node Categories

Category Description Color
event Event nodes (entry points) Red
flow Flow control Gray
entity Entity operations Blue
component Component access Cyan
math Math operations Green
logic Logic operations Red
variable Variable access Purple
time Time utilities Cyan
debug Debug utilities Gray

Pin Types

Nodes connect through pins:

interface BlueprintPinDefinition {
    name: string;        // Pin name
    type: PinDataType;   // Data type
    direction: 'input' | 'output';
    isExec?: boolean;    // Execution pin
    defaultValue?: unknown;
}

type PinDataType =
    | 'exec'      // Execution flow
    | 'boolean'   // Boolean
    | 'number'    // Number
    | 'string'    // String
    | 'vector2'   // 2D vector
    | 'vector3'   // 3D vector
    | 'entity'    // Entity reference
    | 'component' // Component reference
    | 'any';      // Any type

Variable Scopes

type VariableScope =
    | 'local'     // Per execution
    | 'instance'  // Per entity
    | 'global';   // Shared globally

Virtual Machine API

BlueprintVM

The virtual machine executes blueprint graphs:

import { BlueprintVM } from '@esengine/blueprint';

const vm = new BlueprintVM(blueprintAsset, entity, scene);

vm.start();           // Start (triggers BeginPlay)
vm.tick(deltaTime);   // Update (triggers Tick)
vm.stop();            // Stop (triggers EndPlay)

vm.pause();
vm.resume();

// Trigger events
vm.triggerEvent('EventCollision', { other: otherEntity });
vm.triggerCustomEvent('OnDamage', { amount: 50 });

// Debug mode
vm.debug = true;

Execution Context

interface ExecutionContext {
    blueprint: BlueprintAsset;
    entity: Entity;
    scene: IScene;
    deltaTime: number;
    time: number;

    getInput<T>(nodeId: string, pinName: string): T;
    setOutput(nodeId: string, pinName: string, value: unknown): void;
    getVariable<T>(name: string): T;
    setVariable(name: string, value: unknown): void;
}

Execution Result

interface ExecutionResult {
    outputs?: Record<string, unknown>; // Output values
    nextExec?: string | null;          // Next exec pin
    delay?: number;                    // Delay execution (ms)
    yield?: boolean;                   // Pause until next frame
    error?: string;                    // Error message
}

Custom Nodes

Define Node Template

import { BlueprintNodeTemplate } from '@esengine/blueprint';

const MyNodeTemplate: BlueprintNodeTemplate = {
    type: 'MyCustomNode',
    title: 'My Custom Node',
    category: 'custom',
    description: 'A custom node example',
    keywords: ['custom', 'example'],
    inputs: [
        { name: 'exec', type: 'exec', direction: 'input', isExec: true },
        { name: 'value', type: 'number', direction: 'input', defaultValue: 0 }
    ],
    outputs: [
        { name: 'exec', type: 'exec', direction: 'output', isExec: true },
        { name: 'result', type: 'number', direction: 'output' }
    ]
};

Implement Node Executor

import { INodeExecutor, RegisterNode } from '@esengine/blueprint';

@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor {
    execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
        const value = context.getInput<number>(node.id, 'value');
        const result = value * 2;

        return {
            outputs: { result },
            nextExec: 'exec'
        };
    }
}

Registration Methods

// Method 1: Decorator
@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor { ... }

// Method 2: Manual registration
NodeRegistry.instance.register(MyNodeTemplate, new MyNodeExecutor());

Node Registry

import { NodeRegistry } from '@esengine/blueprint';

const registry = NodeRegistry.instance;

const allTemplates = registry.getAllTemplates();
const mathNodes = registry.getTemplatesByCategory('math');
const results = registry.searchTemplates('add');

if (registry.has('MyCustomNode')) { ... }

Built-in Nodes

Event Nodes

Node Description
EventBeginPlay Triggered on blueprint start
EventTick Triggered every frame
EventEndPlay Triggered on blueprint stop
EventCollision Triggered on collision
EventInput Triggered on input
EventTimer Triggered by timer

Time Nodes

Node Description
Delay Delay execution
GetDeltaTime Get frame delta
GetTime Get total runtime

Math Nodes

Node Description
Add, Subtract, Multiply, Divide Basic operations
Abs, Clamp, Lerp, Min, Max Utility functions

Debug Nodes

Node Description
Print Print to console

Blueprint Composition

Blueprint Fragments

Encapsulate reusable logic as fragments:

import { createFragment } from '@esengine/blueprint';

const healthFragment = createFragment('HealthSystem', {
    inputs: [
        { name: 'damage', type: 'number', internalNodeId: 'input1', internalPinName: 'value' }
    ],
    outputs: [
        { name: 'isDead', type: 'boolean', internalNodeId: 'output1', internalPinName: 'value' }
    ],
    graph: { nodes: [...], connections: [...], variables: [...] }
});

Compose Blueprints

import { createComposer, FragmentRegistry } from '@esengine/blueprint';

// Register fragments
FragmentRegistry.instance.register('health', healthFragment);
FragmentRegistry.instance.register('movement', movementFragment);

// Create composer
const composer = createComposer('PlayerBlueprint');

// Add fragments to slots
composer.addFragment(healthFragment, 'slot1', { position: { x: 0, y: 0 } });
composer.addFragment(movementFragment, 'slot2', { position: { x: 400, y: 0 } });

// Connect slots
composer.connect('slot1', 'onDeath', 'slot2', 'disable');

// Validate
const validation = composer.validate();
if (!validation.isValid) {
    console.error(validation.errors);
}

// Compile to blueprint
const blueprint = composer.compile();

Trigger System

Define Trigger Conditions

import { TriggerCondition, TriggerDispatcher } from '@esengine/blueprint';

const lowHealthCondition: TriggerCondition = {
    type: 'comparison',
    left: { type: 'variable', name: 'health' },
    operator: '<',
    right: { type: 'constant', value: 20 }
};

Use Trigger Dispatcher

const dispatcher = new TriggerDispatcher();

dispatcher.register('lowHealth', lowHealthCondition, (context) => {
    context.triggerEvent('OnLowHealth');
});

dispatcher.evaluate(context);

ECS Integration

Using Blueprint System

import { createBlueprintSystem } from '@esengine/blueprint';

class GameScene {
    private blueprintSystem: BlueprintSystem;

    initialize() {
        this.blueprintSystem = createBlueprintSystem(this.scene);
    }

    update(dt: number) {
        this.blueprintSystem.process(this.entities, dt);
    }
}

Triggering Blueprint Events

import { triggerBlueprintEvent, triggerCustomBlueprintEvent } from '@esengine/blueprint';

triggerBlueprintEvent(entity, 'Collision', { other: otherEntity });
triggerCustomBlueprintEvent(entity, 'OnPickup', { item: itemEntity });

Serialization

Save Blueprint

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);
}

Load Blueprint

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;
}

Best Practices

  1. Use fragments for reusable logic
  2. Choose appropriate variable scopes
    • local: Temporary calculations
    • instance: Entity state (e.g., health)
    • global: Game-wide state
  3. Avoid infinite loops - VM has max steps per frame (default 1000)
  4. Debug techniques
    • Enable vm.debug = true for execution logs
    • Use Print nodes for intermediate values
  5. Performance optimization
    • Pure nodes (isPure: true) cache outputs
    • Avoid heavy computation in Tick