* 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 编写。
508 lines
12 KiB
Markdown
508 lines
12 KiB
Markdown
# 蓝图可视化脚本 (Blueprint)
|
||
|
||
`@esengine/blueprint` 提供了一个功能完整的可视化脚本系统,支持节点式编程、事件驱动和蓝图组合。
|
||
|
||
## 安装
|
||
|
||
```bash
|
||
npm install @esengine/blueprint
|
||
```
|
||
|
||
## 快速开始
|
||
|
||
```typescript
|
||
import {
|
||
createBlueprintSystem,
|
||
createBlueprintComponentData,
|
||
NodeRegistry,
|
||
RegisterNode
|
||
} from '@esengine/blueprint';
|
||
|
||
// 创建蓝图系统
|
||
const blueprintSystem = createBlueprintSystem(scene);
|
||
|
||
// 加载蓝图资产
|
||
const blueprint = await loadBlueprintAsset('player.bp');
|
||
|
||
// 创建蓝图组件数据
|
||
const componentData = createBlueprintComponentData();
|
||
componentData.blueprintAsset = blueprint;
|
||
|
||
// 在游戏循环中更新
|
||
function gameLoop(dt: number) {
|
||
blueprintSystem.process(entities, dt);
|
||
}
|
||
```
|
||
|
||
## 核心概念
|
||
|
||
### 蓝图资产结构
|
||
|
||
蓝图保存为 `.bp` 文件,包含以下结构:
|
||
|
||
```typescript
|
||
interface BlueprintAsset {
|
||
version: number; // 格式版本
|
||
type: 'blueprint'; // 资产类型
|
||
metadata: BlueprintMetadata; // 元数据
|
||
variables: BlueprintVariable[]; // 变量定义
|
||
nodes: BlueprintNode[]; // 节点实例
|
||
connections: BlueprintConnection[]; // 连接
|
||
}
|
||
```
|
||
|
||
### 节点类型
|
||
|
||
节点按功能分为以下类别:
|
||
|
||
| 类别 | 说明 | 颜色 |
|
||
|------|------|------|
|
||
| `event` | 事件节点(入口点) | 红色 |
|
||
| `flow` | 流程控制 | 灰色 |
|
||
| `entity` | 实体操作 | 蓝色 |
|
||
| `component` | 组件访问 | 青色 |
|
||
| `math` | 数学运算 | 绿色 |
|
||
| `logic` | 逻辑运算 | 红色 |
|
||
| `variable` | 变量访问 | 紫色 |
|
||
| `time` | 时间工具 | 青色 |
|
||
| `debug` | 调试工具 | 灰色 |
|
||
|
||
### 引脚类型
|
||
|
||
节点通过引脚连接:
|
||
|
||
```typescript
|
||
interface BlueprintPinDefinition {
|
||
name: string; // 引脚名称
|
||
type: PinDataType; // 数据类型
|
||
direction: 'input' | 'output';
|
||
isExec?: boolean; // 是否是执行引脚
|
||
defaultValue?: unknown;
|
||
}
|
||
|
||
// 支持的数据类型
|
||
type PinDataType =
|
||
| 'exec' // 执行流
|
||
| 'boolean' // 布尔值
|
||
| 'number' // 数字
|
||
| 'string' // 字符串
|
||
| 'vector2' // 2D 向量
|
||
| 'vector3' // 3D 向量
|
||
| 'entity' // 实体引用
|
||
| 'component' // 组件引用
|
||
| 'any'; // 任意类型
|
||
```
|
||
|
||
### 变量作用域
|
||
|
||
```typescript
|
||
type VariableScope =
|
||
| 'local' // 每次执行独立
|
||
| 'instance' // 每个实体独立
|
||
| 'global'; // 全局共享
|
||
```
|
||
|
||
## 虚拟机 API
|
||
|
||
### BlueprintVM
|
||
|
||
蓝图虚拟机负责执行蓝图图:
|
||
|
||
```typescript
|
||
import { BlueprintVM } from '@esengine/blueprint';
|
||
|
||
// 创建 VM
|
||
const 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;
|
||
```
|
||
|
||
### 执行上下文
|
||
|
||
```typescript
|
||
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;
|
||
}
|
||
```
|
||
|
||
### 执行结果
|
||
|
||
```typescript
|
||
interface ExecutionResult {
|
||
outputs?: Record<string, unknown>; // 输出值
|
||
nextExec?: string | null; // 下一个执行引脚
|
||
delay?: number; // 延迟执行(毫秒)
|
||
yield?: boolean; // 暂停到下一帧
|
||
error?: string; // 错误信息
|
||
}
|
||
```
|
||
|
||
## 自定义节点
|
||
|
||
### 定义节点模板
|
||
|
||
```typescript
|
||
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' }
|
||
]
|
||
};
|
||
```
|
||
|
||
### 实现节点执行器
|
||
|
||
```typescript
|
||
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' // 继续执行
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
### 使用装饰器注册
|
||
|
||
```typescript
|
||
// 方式 1: 使用装饰器
|
||
@RegisterNode(MyNodeTemplate)
|
||
class MyNodeExecutor implements INodeExecutor { ... }
|
||
|
||
// 方式 2: 手动注册
|
||
NodeRegistry.instance.register(MyNodeTemplate, new MyNodeExecutor());
|
||
```
|
||
|
||
## 节点注册表
|
||
|
||
```typescript
|
||
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')) { ... }
|
||
```
|
||
|
||
## 内置节点
|
||
|
||
### 事件节点
|
||
|
||
| 节点 | 说明 |
|
||
|------|------|
|
||
| `EventBeginPlay` | 蓝图启动时触发 |
|
||
| `EventTick` | 每帧触发 |
|
||
| `EventEndPlay` | 蓝图停止时触发 |
|
||
| `EventCollision` | 碰撞时触发 |
|
||
| `EventInput` | 输入事件触发 |
|
||
| `EventTimer` | 定时器触发 |
|
||
| `EventMessage` | 自定义消息触发 |
|
||
|
||
### 时间节点
|
||
|
||
| 节点 | 说明 |
|
||
|------|------|
|
||
| `Delay` | 延迟执行 |
|
||
| `GetDeltaTime` | 获取帧间隔 |
|
||
| `GetTime` | 获取运行时间 |
|
||
|
||
### 数学节点
|
||
|
||
| 节点 | 说明 |
|
||
|------|------|
|
||
| `Add` | 加法 |
|
||
| `Subtract` | 减法 |
|
||
| `Multiply` | 乘法 |
|
||
| `Divide` | 除法 |
|
||
| `Abs` | 绝对值 |
|
||
| `Clamp` | 限制范围 |
|
||
| `Lerp` | 线性插值 |
|
||
| `Min` / `Max` | 最小/最大值 |
|
||
|
||
### 调试节点
|
||
|
||
| 节点 | 说明 |
|
||
|------|------|
|
||
| `Print` | 打印到控制台 |
|
||
|
||
## 蓝图组合
|
||
|
||
### 蓝图片段
|
||
|
||
将可复用的逻辑封装为片段:
|
||
|
||
```typescript
|
||
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: [...]
|
||
}
|
||
});
|
||
```
|
||
|
||
### 组合蓝图
|
||
|
||
```typescript
|
||
import { createComposer, FragmentRegistry } from '@esengine/blueprint';
|
||
|
||
// 注册片段
|
||
FragmentRegistry.instance.register('health', healthFragment);
|
||
FragmentRegistry.instance.register('movement', movementFragment);
|
||
|
||
// 创建组合器
|
||
const composer = createComposer('PlayerBlueprint');
|
||
|
||
// 添加片段到槽位
|
||
composer.addFragment(healthFragment, 'slot1', { position: { x: 0, y: 0 } });
|
||
composer.addFragment(movementFragment, 'slot2', { position: { x: 400, y: 0 } });
|
||
|
||
// 连接槽位
|
||
composer.connect('slot1', 'onDeath', 'slot2', 'disable');
|
||
|
||
// 验证
|
||
const validation = composer.validate();
|
||
if (!validation.isValid) {
|
||
console.error(validation.errors);
|
||
}
|
||
|
||
// 编译成蓝图
|
||
const blueprint = composer.compile();
|
||
```
|
||
|
||
## 触发器系统
|
||
|
||
### 定义触发条件
|
||
|
||
```typescript
|
||
import { TriggerCondition, TriggerDispatcher } from '@esengine/blueprint';
|
||
|
||
const lowHealthCondition: TriggerCondition = {
|
||
type: 'comparison',
|
||
left: { type: 'variable', name: 'health' },
|
||
operator: '<',
|
||
right: { type: 'constant', value: 20 }
|
||
};
|
||
```
|
||
|
||
### 使用触发器分发器
|
||
|
||
```typescript
|
||
const dispatcher = new TriggerDispatcher();
|
||
|
||
// 注册触发器
|
||
dispatcher.register('lowHealth', lowHealthCondition, (context) => {
|
||
context.triggerEvent('OnLowHealth');
|
||
});
|
||
|
||
// 每帧评估
|
||
dispatcher.evaluate(context);
|
||
```
|
||
|
||
## 与 ECS 集成
|
||
|
||
### 使用蓝图系统
|
||
|
||
```typescript
|
||
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);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 触发蓝图事件
|
||
|
||
```typescript
|
||
import { triggerBlueprintEvent, triggerCustomBlueprintEvent } from '@esengine/blueprint';
|
||
|
||
// 触发内置事件
|
||
triggerBlueprintEvent(entity, 'Collision', { other: otherEntity });
|
||
|
||
// 触发自定义事件
|
||
triggerCustomBlueprintEvent(entity, 'OnPickup', { item: itemEntity });
|
||
```
|
||
|
||
## 实际示例
|
||
|
||
### 玩家控制蓝图
|
||
|
||
```typescript
|
||
// 定义输入处理节点
|
||
const InputMoveTemplate: BlueprintNodeTemplate = {
|
||
type: 'InputMove',
|
||
title: 'Get Movement Input',
|
||
category: 'input',
|
||
inputs: [],
|
||
outputs: [
|
||
{ name: 'direction', type: 'vector2', direction: 'output' }
|
||
],
|
||
isPure: true
|
||
};
|
||
|
||
@RegisterNode(InputMoveTemplate)
|
||
class InputMoveExecutor implements INodeExecutor {
|
||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||
const input = context.scene.services.get(InputServiceToken);
|
||
const direction = {
|
||
x: input.getAxis('horizontal'),
|
||
y: input.getAxis('vertical')
|
||
};
|
||
return { outputs: { direction } };
|
||
}
|
||
}
|
||
```
|
||
|
||
### 状态切换逻辑
|
||
|
||
```typescript
|
||
// 在蓝图中实现状态机逻辑
|
||
const stateBlueprint = createEmptyBlueprint('PlayerState');
|
||
|
||
// 添加状态变量
|
||
stateBlueprint.variables.push({
|
||
name: 'currentState',
|
||
type: 'string',
|
||
defaultValue: 'idle',
|
||
scope: 'instance'
|
||
});
|
||
|
||
// 在 Tick 事件中检查状态转换
|
||
// ... 通过节点连接实现
|
||
```
|
||
|
||
## 序列化
|
||
|
||
### 保存蓝图
|
||
|
||
```typescript
|
||
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);
|
||
}
|
||
```
|
||
|
||
### 加载蓝图
|
||
|
||
```typescript
|
||
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;
|
||
}
|
||
```
|
||
|
||
## 最佳实践
|
||
|
||
1. **使用片段复用逻辑**
|
||
- 将通用逻辑封装为片段
|
||
- 通过组合器构建复杂蓝图
|
||
|
||
2. **合理使用变量作用域**
|
||
- `local`: 临时计算结果
|
||
- `instance`: 实体状态(如生命值)
|
||
- `global`: 游戏全局状态
|
||
|
||
3. **避免无限循环**
|
||
- VM 有每帧最大执行步数限制(默认 1000)
|
||
- 使用 Delay 节点打断长执行链
|
||
|
||
4. **调试技巧**
|
||
- 启用 `vm.debug = true` 查看执行日志
|
||
- 使用 Print 节点输出中间值
|
||
|
||
5. **性能优化**
|
||
- 纯节点(`isPure: true`)的输出会被缓存
|
||
- 避免在 Tick 中执行重计算
|