refactor: reorganize package structure and decouple framework packages (#338)

* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View File

@@ -0,0 +1,357 @@
import { BehaviorTreeData, BehaviorNodeData } from './execution/BehaviorTreeData';
import { NodeType } from './Types/TaskStatus';
/**
* 行为树构建器
*
* 提供流式API构建行为树数据结构
*/
export class BehaviorTreeBuilder {
private treeData: BehaviorTreeData;
private nodeStack: string[] = [];
private nodeIdCounter: number = 0;
private constructor(treeName: string) {
this.treeData = {
id: `tree_${Date.now()}`,
name: treeName,
rootNodeId: '',
nodes: new Map(),
blackboardVariables: new Map()
};
}
/**
* 创建构建器
*/
static create(treeName: string = 'BehaviorTree'): BehaviorTreeBuilder {
return new BehaviorTreeBuilder(treeName);
}
/**
* 定义黑板变量
*/
defineBlackboardVariable(key: string, initialValue: any): BehaviorTreeBuilder {
if (!this.treeData.blackboardVariables) {
this.treeData.blackboardVariables = new Map();
}
this.treeData.blackboardVariables.set(key, initialValue);
return this;
}
/**
* 添加序列节点
*/
sequence(name?: string): BehaviorTreeBuilder {
return this.addCompositeNode('Sequence', name || 'Sequence');
}
/**
* 添加选择器节点
*/
selector(name?: string): BehaviorTreeBuilder {
return this.addCompositeNode('Selector', name || 'Selector');
}
/**
* 添加并行节点
*/
parallel(name?: string, config?: { successPolicy?: string; failurePolicy?: string }): BehaviorTreeBuilder {
return this.addCompositeNode('Parallel', name || 'Parallel', config);
}
/**
* 添加并行选择器节点
*/
parallelSelector(name?: string, config?: { failurePolicy?: string }): BehaviorTreeBuilder {
return this.addCompositeNode('ParallelSelector', name || 'ParallelSelector', config);
}
/**
* 添加随机序列节点
*/
randomSequence(name?: string): BehaviorTreeBuilder {
return this.addCompositeNode('RandomSequence', name || 'RandomSequence');
}
/**
* 添加随机选择器节点
*/
randomSelector(name?: string): BehaviorTreeBuilder {
return this.addCompositeNode('RandomSelector', name || 'RandomSelector');
}
/**
* 添加反转装饰器
*/
inverter(name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('Inverter', name || 'Inverter');
}
/**
* 添加重复装饰器
*/
repeater(repeatCount: number, name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('Repeater', name || 'Repeater', { repeatCount });
}
/**
* 添加总是成功装饰器
*/
alwaysSucceed(name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('AlwaysSucceed', name || 'AlwaysSucceed');
}
/**
* 添加总是失败装饰器
*/
alwaysFail(name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('AlwaysFail', name || 'AlwaysFail');
}
/**
* 添加直到成功装饰器
*/
untilSuccess(name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('UntilSuccess', name || 'UntilSuccess');
}
/**
* 添加直到失败装饰器
*/
untilFail(name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('UntilFail', name || 'UntilFail');
}
/**
* 添加条件装饰器
*/
conditional(blackboardKey: string, expectedValue: any, operator?: string, name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('Conditional', name || 'Conditional', {
blackboardKey,
expectedValue,
operator: operator || 'equals'
});
}
/**
* 添加冷却装饰器
*/
cooldown(cooldownTime: number, name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('Cooldown', name || 'Cooldown', { cooldownTime });
}
/**
* 添加超时装饰器
*/
timeout(timeout: number, name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('Timeout', name || 'Timeout', { timeout });
}
/**
* 添加等待动作
*/
wait(duration: number, name?: string): BehaviorTreeBuilder {
return this.addActionNode('Wait', name || 'Wait', { duration });
}
/**
* 添加日志动作
*/
log(message: string, name?: string): BehaviorTreeBuilder {
return this.addActionNode('Log', name || 'Log', { message });
}
/**
* 添加设置黑板值动作
*/
setBlackboardValue(key: string, value: any, name?: string): BehaviorTreeBuilder {
return this.addActionNode('SetBlackboardValue', name || 'SetBlackboardValue', { key, value });
}
/**
* 添加修改黑板值动作
*/
modifyBlackboardValue(key: string, operation: string, value: number, name?: string): BehaviorTreeBuilder {
return this.addActionNode('ModifyBlackboardValue', name || 'ModifyBlackboardValue', {
key,
operation,
value
});
}
/**
* 添加执行动作
*/
executeAction(actionName: string, name?: string): BehaviorTreeBuilder {
return this.addActionNode('ExecuteAction', name || 'ExecuteAction', { actionName });
}
/**
* 添加黑板比较条件
*/
blackboardCompare(key: string, compareValue: any, operator?: string, name?: string): BehaviorTreeBuilder {
return this.addConditionNode('BlackboardCompare', name || 'BlackboardCompare', {
key,
compareValue,
operator: operator || 'equals'
});
}
/**
* 添加黑板存在检查条件
*/
blackboardExists(key: string, name?: string): BehaviorTreeBuilder {
return this.addConditionNode('BlackboardExists', name || 'BlackboardExists', { key });
}
/**
* 添加随机概率条件
*/
randomProbability(probability: number, name?: string): BehaviorTreeBuilder {
return this.addConditionNode('RandomProbability', name || 'RandomProbability', { probability });
}
/**
* 添加执行条件
*/
executeCondition(conditionName: string, name?: string): BehaviorTreeBuilder {
return this.addConditionNode('ExecuteCondition', name || 'ExecuteCondition', { conditionName });
}
/**
* 结束当前节点,返回父节点
*/
end(): BehaviorTreeBuilder {
if (this.nodeStack.length > 0) {
this.nodeStack.pop();
}
return this;
}
/**
* 构建行为树数据
*/
build(): BehaviorTreeData {
if (!this.treeData.rootNodeId) {
throw new Error('No root node defined. Add at least one node to the tree.');
}
return this.treeData;
}
private addCompositeNode(implementationType: string, name: string, config: Record<string, any> = {}): BehaviorTreeBuilder {
const nodeId = this.generateNodeId();
const node: BehaviorNodeData = {
id: nodeId,
name,
nodeType: NodeType.Composite,
implementationType,
children: [],
config
};
this.treeData.nodes.set(nodeId, node);
if (!this.treeData.rootNodeId) {
this.treeData.rootNodeId = nodeId;
}
if (this.nodeStack.length > 0) {
const parentId = this.nodeStack[this.nodeStack.length - 1]!;
const parentNode = this.treeData.nodes.get(parentId);
if (parentNode && parentNode.children) {
parentNode.children.push(nodeId);
}
}
this.nodeStack.push(nodeId);
return this;
}
private addDecoratorNode(implementationType: string, name: string, config: Record<string, any> = {}): BehaviorTreeBuilder {
const nodeId = this.generateNodeId();
const node: BehaviorNodeData = {
id: nodeId,
name,
nodeType: NodeType.Decorator,
implementationType,
children: [],
config
};
this.treeData.nodes.set(nodeId, node);
if (!this.treeData.rootNodeId) {
this.treeData.rootNodeId = nodeId;
}
if (this.nodeStack.length > 0) {
const parentId = this.nodeStack[this.nodeStack.length - 1]!;
const parentNode = this.treeData.nodes.get(parentId);
if (parentNode && parentNode.children) {
parentNode.children.push(nodeId);
}
}
this.nodeStack.push(nodeId);
return this;
}
private addActionNode(implementationType: string, name: string, config: Record<string, any> = {}): BehaviorTreeBuilder {
const nodeId = this.generateNodeId();
const node: BehaviorNodeData = {
id: nodeId,
name,
nodeType: NodeType.Action,
implementationType,
config
};
this.treeData.nodes.set(nodeId, node);
if (!this.treeData.rootNodeId) {
this.treeData.rootNodeId = nodeId;
}
if (this.nodeStack.length > 0) {
const parentId = this.nodeStack[this.nodeStack.length - 1]!;
const parentNode = this.treeData.nodes.get(parentId);
if (parentNode && parentNode.children) {
parentNode.children.push(nodeId);
}
}
return this;
}
private addConditionNode(implementationType: string, name: string, config: Record<string, any> = {}): BehaviorTreeBuilder {
const nodeId = this.generateNodeId();
const node: BehaviorNodeData = {
id: nodeId,
name,
nodeType: NodeType.Condition,
implementationType,
config
};
this.treeData.nodes.set(nodeId, node);
if (!this.treeData.rootNodeId) {
this.treeData.rootNodeId = nodeId;
}
if (this.nodeStack.length > 0) {
const parentId = this.nodeStack[this.nodeStack.length - 1]!;
const parentNode = this.treeData.nodes.get(parentId);
if (parentNode && parentNode.children) {
parentNode.children.push(nodeId);
}
}
return this;
}
private generateNodeId(): string {
return `node_${this.nodeIdCounter++}`;
}
}

View File

@@ -0,0 +1,92 @@
import { Entity, Core } from '@esengine/ecs-framework';
import { BehaviorTreeData } from './execution/BehaviorTreeData';
import { BehaviorTreeRuntimeComponent } from './execution/BehaviorTreeRuntimeComponent';
import { BehaviorTreeAssetManager } from './execution/BehaviorTreeAssetManager';
/**
* 行为树启动辅助类
*
* 提供便捷方法来启动、停止行为树
*/
export class BehaviorTreeStarter {
/**
* 启动行为树
*
* @param entity 游戏实体
* @param treeData 行为树数据
* @param autoStart 是否自动开始执行
*/
static start(entity: Entity, treeData: BehaviorTreeData, autoStart: boolean = true): void {
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
assetManager.loadAsset(treeData);
let runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
if (!runtime) {
runtime = new BehaviorTreeRuntimeComponent();
entity.addComponent(runtime);
}
runtime.treeAssetId = treeData.id;
runtime.autoStart = autoStart;
if (treeData.blackboardVariables) {
for (const [key, value] of treeData.blackboardVariables.entries()) {
runtime.setBlackboardValue(key, value);
}
}
if (autoStart) {
runtime.isRunning = true;
}
}
/**
* 停止行为树
*
* @param entity 游戏实体
*/
static stop(entity: Entity): void {
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
if (runtime) {
runtime.isRunning = false;
runtime.resetAllStates();
}
}
/**
* 暂停行为树
*
* @param entity 游戏实体
*/
static pause(entity: Entity): void {
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
if (runtime) {
runtime.isRunning = false;
}
}
/**
* 恢复行为树
*
* @param entity 游戏实体
*/
static resume(entity: Entity): void {
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
if (runtime) {
runtime.isRunning = true;
}
}
/**
* 重启行为树
*
* @param entity 游戏实体
*/
static restart(entity: Entity): void {
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
if (runtime) {
runtime.resetAllStates();
runtime.isRunning = true;
}
}
}

View File

@@ -0,0 +1,248 @@
export enum BlackboardValueType {
// 基础类型
String = 'string',
Number = 'number',
Boolean = 'boolean',
// 数学类型
Vector2 = 'vector2',
Vector3 = 'vector3',
Vector4 = 'vector4',
Quaternion = 'quaternion',
Color = 'color',
// 引用类型
GameObject = 'gameObject',
Transform = 'transform',
Component = 'component',
AssetReference = 'assetReference',
// 集合类型
Array = 'array',
Map = 'map',
// 高级类型
Enum = 'enum',
Struct = 'struct',
Function = 'function',
// 游戏特定类型
EntityId = 'entityId',
NodePath = 'nodePath',
ResourcePath = 'resourcePath',
AnimationState = 'animationState',
AudioClip = 'audioClip',
Material = 'material',
Texture = 'texture'
}
export interface Vector2 {
x: number;
y: number;
}
export interface Vector3 extends Vector2 {
z: number;
}
export interface Vector4 extends Vector3 {
w: number;
}
export interface Quaternion {
x: number;
y: number;
z: number;
w: number;
}
export interface Color {
r: number;
g: number;
b: number;
a: number;
}
export interface BlackboardTypeDefinition {
type: BlackboardValueType;
displayName: string;
category: 'basic' | 'math' | 'reference' | 'collection' | 'advanced' | 'game';
defaultValue: any;
editorComponent?: string; // 自定义编辑器组件
validator?: (value: any) => boolean;
converter?: (value: any) => any;
}
export const BlackboardTypes: Record<BlackboardValueType, BlackboardTypeDefinition> = {
[BlackboardValueType.String]: {
type: BlackboardValueType.String,
displayName: '字符串',
category: 'basic',
defaultValue: '',
validator: (v) => typeof v === 'string'
},
[BlackboardValueType.Number]: {
type: BlackboardValueType.Number,
displayName: '数字',
category: 'basic',
defaultValue: 0,
validator: (v) => typeof v === 'number'
},
[BlackboardValueType.Boolean]: {
type: BlackboardValueType.Boolean,
displayName: '布尔值',
category: 'basic',
defaultValue: false,
validator: (v) => typeof v === 'boolean'
},
[BlackboardValueType.Vector2]: {
type: BlackboardValueType.Vector2,
displayName: '二维向量',
category: 'math',
defaultValue: { x: 0, y: 0 },
editorComponent: 'Vector2Editor',
validator: (v) => v && typeof v.x === 'number' && typeof v.y === 'number'
},
[BlackboardValueType.Vector3]: {
type: BlackboardValueType.Vector3,
displayName: '三维向量',
category: 'math',
defaultValue: { x: 0, y: 0, z: 0 },
editorComponent: 'Vector3Editor',
validator: (v) => v && typeof v.x === 'number' && typeof v.y === 'number' && typeof v.z === 'number'
},
[BlackboardValueType.Color]: {
type: BlackboardValueType.Color,
displayName: '颜色',
category: 'math',
defaultValue: { r: 1, g: 1, b: 1, a: 1 },
editorComponent: 'ColorEditor',
validator: (v) => v && typeof v.r === 'number' && typeof v.g === 'number' && typeof v.b === 'number' && typeof v.a === 'number'
},
[BlackboardValueType.GameObject]: {
type: BlackboardValueType.GameObject,
displayName: '游戏对象',
category: 'reference',
defaultValue: null,
editorComponent: 'GameObjectPicker'
},
[BlackboardValueType.Transform]: {
type: BlackboardValueType.Transform,
displayName: '变换组件',
category: 'reference',
defaultValue: null,
editorComponent: 'ComponentPicker'
},
[BlackboardValueType.AssetReference]: {
type: BlackboardValueType.AssetReference,
displayName: '资源引用',
category: 'reference',
defaultValue: null,
editorComponent: 'AssetPicker'
},
[BlackboardValueType.EntityId]: {
type: BlackboardValueType.EntityId,
displayName: '实体ID',
category: 'game',
defaultValue: -1,
validator: (v) => typeof v === 'number' && v >= -1
},
[BlackboardValueType.ResourcePath]: {
type: BlackboardValueType.ResourcePath,
displayName: '资源路径',
category: 'game',
defaultValue: '',
editorComponent: 'AssetPathPicker'
},
[BlackboardValueType.Array]: {
type: BlackboardValueType.Array,
displayName: '数组',
category: 'collection',
defaultValue: [],
editorComponent: 'ArrayEditor'
},
[BlackboardValueType.Map]: {
type: BlackboardValueType.Map,
displayName: '映射表',
category: 'collection',
defaultValue: {},
editorComponent: 'MapEditor'
},
[BlackboardValueType.Enum]: {
type: BlackboardValueType.Enum,
displayName: '枚举',
category: 'advanced',
defaultValue: '',
editorComponent: 'EnumPicker'
},
[BlackboardValueType.AnimationState]: {
type: BlackboardValueType.AnimationState,
displayName: '动画状态',
category: 'game',
defaultValue: '',
editorComponent: 'AnimationStatePicker'
},
[BlackboardValueType.AudioClip]: {
type: BlackboardValueType.AudioClip,
displayName: '音频片段',
category: 'game',
defaultValue: null,
editorComponent: 'AudioClipPicker'
},
[BlackboardValueType.Material]: {
type: BlackboardValueType.Material,
displayName: '材质',
category: 'game',
defaultValue: null,
editorComponent: 'MaterialPicker'
},
[BlackboardValueType.Texture]: {
type: BlackboardValueType.Texture,
displayName: '纹理',
category: 'game',
defaultValue: null,
editorComponent: 'TexturePicker'
},
[BlackboardValueType.Vector4]: {
type: BlackboardValueType.Vector4,
displayName: '四维向量',
category: 'math',
defaultValue: { x: 0, y: 0, z: 0, w: 0 },
editorComponent: 'Vector4Editor'
},
[BlackboardValueType.Quaternion]: {
type: BlackboardValueType.Quaternion,
displayName: '四元数',
category: 'math',
defaultValue: { x: 0, y: 0, z: 0, w: 1 },
editorComponent: 'QuaternionEditor'
},
[BlackboardValueType.Component]: {
type: BlackboardValueType.Component,
displayName: '组件',
category: 'reference',
defaultValue: null,
editorComponent: 'ComponentPicker'
},
[BlackboardValueType.Struct]: {
type: BlackboardValueType.Struct,
displayName: '结构体',
category: 'advanced',
defaultValue: {},
editorComponent: 'StructEditor'
},
[BlackboardValueType.Function]: {
type: BlackboardValueType.Function,
displayName: '函数',
category: 'advanced',
defaultValue: null,
editorComponent: 'FunctionPicker'
},
[BlackboardValueType.NodePath]: {
type: BlackboardValueType.NodePath,
displayName: '节点路径',
category: 'game',
defaultValue: '',
editorComponent: 'NodePathPicker'
}
};

View File

@@ -0,0 +1,303 @@
import { NodeType, BlackboardValueType } from '../Types/TaskStatus';
/**
* 行为树资产元数据
*/
export interface AssetMetadata {
name: string;
description?: string;
version: string;
createdAt?: string;
modifiedAt?: string;
}
/**
* 黑板变量定义
*/
export interface BlackboardVariableDefinition {
name: string;
type: BlackboardValueType;
defaultValue: any;
readonly?: boolean;
description?: string;
}
/**
* 行为树节点配置数据
*/
export interface BehaviorNodeConfigData {
className?: string;
[key: string]: any;
}
/**
* 行为树节点数据(运行时格式)
*/
export interface BehaviorTreeNodeData {
id: string;
name: string;
nodeType: NodeType;
// 节点类型特定数据
data: BehaviorNodeConfigData;
// 子节点ID列表
children: string[];
}
/**
* 属性绑定定义
*/
export interface PropertyBinding {
nodeId: string;
propertyName: string;
variableName: string;
}
/**
* 行为树资产(运行时格式)
*
* 这是用于游戏运行时的优化格式不包含编辑器UI信息
*/
export interface BehaviorTreeAsset {
/**
* 资产格式版本
*/
version: string;
/**
* 元数据
*/
metadata: AssetMetadata;
/**
* 根节点ID
*/
rootNodeId: string;
/**
* 所有节点数据扁平化存储通过children建立层级
*/
nodes: BehaviorTreeNodeData[];
/**
* 黑板变量定义
*/
blackboard: BlackboardVariableDefinition[];
/**
* 属性绑定
*/
propertyBindings?: PropertyBinding[];
}
/**
* 资产验证结果
*/
export interface AssetValidationResult {
valid: boolean;
errors?: string[];
warnings?: string[];
}
/**
* 资产验证器
*/
export class BehaviorTreeAssetValidator {
/**
* 验证资产数据的完整性和正确性
*/
static validate(asset: BehaviorTreeAsset): AssetValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
// 检查版本
if (!asset.version) {
errors.push('Missing version field');
}
// 检查元数据
if (!asset.metadata || !asset.metadata.name) {
errors.push('Missing or invalid metadata');
}
// 检查根节点
if (!asset.rootNodeId) {
errors.push('Missing rootNodeId');
}
// 检查节点列表
if (!asset.nodes || !Array.isArray(asset.nodes)) {
errors.push('Missing or invalid nodes array');
} else {
const nodeIds = new Set<string>();
const rootNode = asset.nodes.find((n) => n.id === asset.rootNodeId);
if (!rootNode) {
errors.push(`Root node '${asset.rootNodeId}' not found in nodes array`);
}
// 检查节点ID唯一性
for (const node of asset.nodes) {
if (!node.id) {
errors.push('Node missing id field');
continue;
}
if (nodeIds.has(node.id)) {
errors.push(`Duplicate node id: ${node.id}`);
}
nodeIds.add(node.id);
// 检查节点类型
if (!node.nodeType) {
errors.push(`Node ${node.id} missing nodeType`);
}
// 检查子节点引用
if (node.children) {
for (const childId of node.children) {
if (!asset.nodes.find((n) => n.id === childId)) {
errors.push(`Node ${node.id} references non-existent child: ${childId}`);
}
}
}
}
// 检查是否有孤立节点
const referencedNodes = new Set<string>([asset.rootNodeId]);
const collectReferencedNodes = (nodeId: string) => {
const node = asset.nodes.find((n) => n.id === nodeId);
if (node && node.children) {
for (const childId of node.children) {
referencedNodes.add(childId);
collectReferencedNodes(childId);
}
}
};
collectReferencedNodes(asset.rootNodeId);
for (const node of asset.nodes) {
if (!referencedNodes.has(node.id)) {
warnings.push(`Orphaned node detected: ${node.id} (${node.name})`);
}
}
}
// 检查黑板定义
if (asset.blackboard && Array.isArray(asset.blackboard)) {
const varNames = new Set<string>();
for (const variable of asset.blackboard) {
if (!variable.name) {
errors.push('Blackboard variable missing name');
continue;
}
if (varNames.has(variable.name)) {
errors.push(`Duplicate blackboard variable: ${variable.name}`);
}
varNames.add(variable.name);
if (!variable.type) {
errors.push(`Blackboard variable ${variable.name} missing type`);
}
}
}
// 检查属性绑定
if (asset.propertyBindings && Array.isArray(asset.propertyBindings)) {
const nodeIds = new Set(asset.nodes.map((n) => n.id));
const varNames = new Set(asset.blackboard?.map((v) => v.name) || []);
for (const binding of asset.propertyBindings) {
if (!nodeIds.has(binding.nodeId)) {
errors.push(`Property binding references non-existent node: ${binding.nodeId}`);
}
if (!varNames.has(binding.variableName)) {
errors.push(`Property binding references non-existent variable: ${binding.variableName}`);
}
if (!binding.propertyName) {
errors.push('Property binding missing propertyName');
}
}
}
const result: AssetValidationResult = {
valid: errors.length === 0
};
if (errors.length > 0) {
result.errors = errors;
}
if (warnings.length > 0) {
result.warnings = warnings;
}
return result;
}
/**
* 获取资产统计信息
*/
static getStats(asset: BehaviorTreeAsset): {
nodeCount: number;
actionCount: number;
conditionCount: number;
compositeCount: number;
decoratorCount: number;
blackboardVariableCount: number;
propertyBindingCount: number;
maxDepth: number;
} {
let actionCount = 0;
let conditionCount = 0;
let compositeCount = 0;
let decoratorCount = 0;
for (const node of asset.nodes) {
switch (node.nodeType) {
case NodeType.Action:
actionCount++;
break;
case NodeType.Condition:
conditionCount++;
break;
case NodeType.Composite:
compositeCount++;
break;
case NodeType.Decorator:
decoratorCount++;
break;
}
}
// 计算最大深度
const getDepth = (nodeId: string, currentDepth: number = 0): number => {
const node = asset.nodes.find((n) => n.id === nodeId);
if (!node || !node.children || node.children.length === 0) {
return currentDepth;
}
let maxChildDepth = currentDepth;
for (const childId of node.children) {
const childDepth = getDepth(childId, currentDepth + 1);
maxChildDepth = Math.max(maxChildDepth, childDepth);
}
return maxChildDepth;
};
return {
nodeCount: asset.nodes.length,
actionCount,
conditionCount,
compositeCount,
decoratorCount,
blackboardVariableCount: asset.blackboard?.length || 0,
propertyBindingCount: asset.propertyBindings?.length || 0,
maxDepth: getDepth(asset.rootNodeId)
};
}
}

View File

@@ -0,0 +1,329 @@
import { createLogger, BinarySerializer } from '@esengine/ecs-framework';
import type { BehaviorTreeAsset } from './BehaviorTreeAsset';
import { BehaviorTreeAssetValidator } from './BehaviorTreeAsset';
import { EditorFormatConverter, type EditorFormat } from './EditorFormatConverter';
const logger = createLogger('BehaviorTreeAssetSerializer');
/**
* 行为树序列化格式
* Behavior tree serialization format
*/
export type BehaviorTreeSerializationFormat = 'json' | 'binary';
/**
* 序列化选项
*/
export interface SerializationOptions {
/**
* 序列化格式
*/
format: BehaviorTreeSerializationFormat;
/**
* 是否美化JSON输出仅format='json'时有效)
*/
pretty?: boolean;
/**
* 是否在序列化前验证资产
*/
validate?: boolean;
}
/**
* 反序列化选项
*/
export interface DeserializationOptions {
/**
* 是否在反序列化后验证资产
*/
validate?: boolean;
/**
* 是否严格模式(验证失败抛出异常)
*/
strict?: boolean;
}
/**
* 行为树资产序列化器
*
* 支持JSON和二进制两种格式
*/
export class BehaviorTreeAssetSerializer {
/**
* 序列化资产
*
* @param asset 行为树资产
* @param options 序列化选项
* @returns 序列化后的数据字符串或Uint8Array
*
* @example
* ```typescript
* // JSON格式
* const jsonData = BehaviorTreeAssetSerializer.serialize(asset, { format: 'json', pretty: true });
*
* // 二进制格式
* const binaryData = BehaviorTreeAssetSerializer.serialize(asset, { format: 'binary' });
* ```
*/
static serialize(
asset: BehaviorTreeAsset,
options: SerializationOptions = { format: 'json', pretty: true }
): string | Uint8Array {
// 验证资产(如果需要)
if (options.validate !== false) {
const validation = BehaviorTreeAssetValidator.validate(asset);
if (!validation.valid) {
const errors = validation.errors?.join(', ') || 'Unknown error';
throw new Error(`资产验证失败: ${errors}`);
}
if (validation.warnings && validation.warnings.length > 0) {
logger.warn(`资产验证警告: ${validation.warnings.join(', ')}`);
}
}
// 根据格式序列化
if (options.format === 'json') {
return this.serializeToJSON(asset, options.pretty);
} else {
return this.serializeToBinary(asset);
}
}
/**
* 序列化为JSON格式
*/
private static serializeToJSON(asset: BehaviorTreeAsset, pretty: boolean = true): string {
try {
const json = pretty
? JSON.stringify(asset, null, 2)
: JSON.stringify(asset);
logger.info(`已序列化为JSON: ${json.length} 字符`);
return json;
} catch (error) {
throw new Error(`JSON序列化失败: ${error}`);
}
}
/**
* 序列化为二进制格式
*/
private static serializeToBinary(asset: BehaviorTreeAsset): Uint8Array {
try {
const binary = BinarySerializer.encode(asset);
logger.info(`已序列化为二进制: ${binary.length} 字节`);
return binary;
} catch (error) {
throw new Error(`二进制序列化失败: ${error}`);
}
}
/**
* 反序列化资产
*
* @param data 序列化的数据字符串或Uint8Array
* @param options 反序列化选项
* @returns 行为树资产
*
* @example
* ```typescript
* // 从JSON加载
* const asset = BehaviorTreeAssetSerializer.deserialize(jsonString);
*
* // 从二进制加载
* const asset = BehaviorTreeAssetSerializer.deserialize(binaryData);
* ```
*/
static deserialize(
data: string | Uint8Array,
options: DeserializationOptions = { validate: true, strict: true }
): BehaviorTreeAsset {
let asset: BehaviorTreeAsset;
try {
if (typeof data === 'string') {
asset = this.deserializeFromJSON(data);
} else {
asset = this.deserializeFromBinary(data);
}
} catch (error) {
throw new Error(`反序列化失败: ${error}`);
}
// 验证资产(如果需要)
if (options.validate !== false) {
const validation = BehaviorTreeAssetValidator.validate(asset);
if (!validation.valid) {
const errors = validation.errors?.join(', ') || 'Unknown error';
if (options.strict) {
throw new Error(`资产验证失败: ${errors}`);
} else {
logger.error(`资产验证失败: ${errors}`);
}
}
if (validation.warnings && validation.warnings.length > 0) {
logger.warn(`资产验证警告: ${validation.warnings.join(', ')}`);
}
}
return asset;
}
/**
* 从JSON反序列化
*/
private static deserializeFromJSON(json: string): BehaviorTreeAsset {
try {
const data = JSON.parse(json);
// 检测是否是编辑器格式EditorFormat
// 编辑器格式有 nodes/connections/blackboard但没有 rootNodeId
// 运行时资产格式有 rootNodeId
const isEditorFormat = !data.rootNodeId && data.nodes && data.connections;
if (isEditorFormat) {
logger.info('检测到编辑器格式,正在转换为运行时资产格式...');
const editorData = data as EditorFormat;
const asset = EditorFormatConverter.toAsset(editorData);
logger.info(`已从编辑器格式转换: ${asset.nodes.length} 个节点`);
return asset;
} else {
const asset = data as BehaviorTreeAsset;
logger.info(`已从运行时资产格式反序列化: ${asset.nodes.length} 个节点`);
return asset;
}
} catch (error) {
throw new Error(`JSON解析失败: ${error}`);
}
}
/**
* 从二进制反序列化
*/
private static deserializeFromBinary(binary: Uint8Array): BehaviorTreeAsset {
try {
const asset = BinarySerializer.decode(binary) as BehaviorTreeAsset;
logger.info(`已从二进制反序列化: ${asset.nodes.length} 个节点`);
return asset;
} catch (error) {
throw new Error(`二进制解码失败: ${error}`);
}
}
/**
* 检测数据格式
*
* @param data 序列化的数据
* @returns 格式类型
*/
static detectFormat(data: string | Uint8Array): BehaviorTreeSerializationFormat {
if (typeof data === 'string') {
return 'json';
} else {
return 'binary';
}
}
/**
* 获取序列化数据的信息(不完全反序列化)
*
* @param data 序列化的数据
* @returns 资产元信息
*/
static getInfo(data: string | Uint8Array): {
format: BehaviorTreeSerializationFormat;
name: string;
version: string;
nodeCount: number;
blackboardVariableCount: number;
size: number;
} | null {
try {
const format = this.detectFormat(data);
let asset: BehaviorTreeAsset;
if (format === 'json') {
asset = JSON.parse(data as string);
} else {
asset = BinarySerializer.decode(data as Uint8Array) as BehaviorTreeAsset;
}
const size = typeof data === 'string' ? data.length : data.length;
return {
format,
name: asset.metadata.name,
version: asset.version,
nodeCount: asset.nodes.length,
blackboardVariableCount: asset.blackboard.length,
size
};
} catch (error) {
logger.error(`获取资产信息失败: ${error}`);
return null;
}
}
/**
* 转换格式
*
* @param data 源数据
* @param targetFormat 目标格式
* @param pretty 是否美化JSON仅当目标格式为json时有效
* @returns 转换后的数据
*
* @example
* ```typescript
* // JSON转二进制
* const binary = BehaviorTreeAssetSerializer.convert(jsonString, 'binary');
*
* // 二进制转JSON
* const json = BehaviorTreeAssetSerializer.convert(binaryData, 'json', true);
* ```
*/
static convert(
data: string | Uint8Array,
targetFormat: BehaviorTreeSerializationFormat,
pretty: boolean = true
): string | Uint8Array {
const asset = this.deserialize(data, { validate: false });
return this.serialize(asset, {
format: targetFormat,
pretty,
validate: false
});
}
/**
* 比较两个资产数据的大小
*
* @param jsonData JSON格式数据
* @param binaryData 二进制格式数据
* @returns 压缩率(百分比)
*/
static compareSize(jsonData: string, binaryData: Uint8Array): {
jsonSize: number;
binarySize: number;
compressionRatio: number;
savedBytes: number;
} {
const jsonSize = jsonData.length;
const binarySize = binaryData.length;
const savedBytes = jsonSize - binarySize;
const compressionRatio = (savedBytes / jsonSize) * 100;
return {
jsonSize,
binarySize,
compressionRatio,
savedBytes
};
}
}

View File

@@ -0,0 +1,397 @@
import { createLogger } from '@esengine/ecs-framework';
import type { BehaviorTreeAsset, AssetMetadata, BehaviorTreeNodeData, BlackboardVariableDefinition, PropertyBinding } from './BehaviorTreeAsset';
import { NodeType, BlackboardValueType } from '../Types/TaskStatus';
const logger = createLogger('EditorFormatConverter');
/**
* 编辑器节点格式
*/
export interface EditorNodeTemplate {
displayName: string;
category: string;
type: NodeType;
className?: string;
[key: string]: any;
}
export interface EditorNodeData {
nodeType?: string;
className?: string;
variableName?: string;
name?: string;
[key: string]: any;
}
export interface EditorNode {
id: string;
template: EditorNodeTemplate;
data: EditorNodeData;
position: { x: number; y: number };
children: string[];
}
/**
* 编辑器连接格式
*/
export interface EditorConnection {
from: string;
to: string;
fromProperty?: string;
toProperty?: string;
connectionType: 'node' | 'property';
}
/**
* 编辑器格式
*/
export interface EditorFormat {
version?: string;
metadata?: {
name: string;
description?: string;
createdAt?: string;
modifiedAt?: string;
};
nodes: EditorNode[];
connections: EditorConnection[];
blackboard: Record<string, any>;
canvasState?: {
offset: { x: number; y: number };
scale: number;
};
}
/**
* 编辑器格式转换器
*
* 将编辑器格式转换为运行时资产格式
*/
export class EditorFormatConverter {
/**
* 转换编辑器格式为资产格式
*
* @param editorData 编辑器数据
* @param metadata 可选的元数据覆盖
* @returns 行为树资产
*/
static toAsset(editorData: EditorFormat, metadata?: Partial<AssetMetadata>): BehaviorTreeAsset {
logger.info('开始转换编辑器格式到资产格式');
const rootNode = this.findRootNode(editorData.nodes);
if (!rootNode) {
throw new Error('未找到根节点');
}
const assetMetadata: AssetMetadata = {
name: metadata?.name || editorData.metadata?.name || 'Untitled Behavior Tree',
version: metadata?.version || editorData.version || '1.0.0'
};
const description = metadata?.description || editorData.metadata?.description;
if (description) {
assetMetadata.description = description;
}
const createdAt = metadata?.createdAt || editorData.metadata?.createdAt;
if (createdAt) {
assetMetadata.createdAt = createdAt;
}
const modifiedAt = metadata?.modifiedAt || new Date().toISOString();
if (modifiedAt) {
assetMetadata.modifiedAt = modifiedAt;
}
const nodes = this.convertNodes(editorData.nodes);
const blackboard = this.convertBlackboard(editorData.blackboard);
const propertyBindings = this.convertPropertyBindings(
editorData.connections,
editorData.nodes,
blackboard
);
const asset: BehaviorTreeAsset = {
version: '1.0.0',
metadata: assetMetadata,
rootNodeId: rootNode.id,
nodes,
blackboard
};
if (propertyBindings.length > 0) {
asset.propertyBindings = propertyBindings;
}
logger.info(`转换完成: ${nodes.length}个节点, ${blackboard.length}个黑板变量, ${propertyBindings.length}个属性绑定`);
return asset;
}
/**
* 查找根节点
*/
private static findRootNode(nodes: EditorNode[]): EditorNode | null {
return nodes.find((node) =>
node.template.category === '根节点' ||
node.data.nodeType === 'root'
) || null;
}
/**
* 转换节点列表
*/
private static convertNodes(editorNodes: EditorNode[]): BehaviorTreeNodeData[] {
return editorNodes.map((node) => this.convertNode(node));
}
/**
* 转换单个节点
*/
private static convertNode(editorNode: EditorNode): BehaviorTreeNodeData {
const data = { ...editorNode.data };
delete data.nodeType;
if (editorNode.template.className) {
data.className = editorNode.template.className;
}
return {
id: editorNode.id,
name: editorNode.template.displayName || editorNode.data.name || 'Node',
nodeType: editorNode.template.type,
data,
children: editorNode.children || []
};
}
/**
* 转换黑板变量
*/
private static convertBlackboard(blackboard: Record<string, any>): BlackboardVariableDefinition[] {
const variables: BlackboardVariableDefinition[] = [];
for (const [name, value] of Object.entries(blackboard)) {
const type = this.inferBlackboardType(value);
variables.push({
name,
type,
defaultValue: value
});
}
return variables;
}
/**
* 推断黑板变量类型
*/
private static inferBlackboardType(value: any): BlackboardValueType {
if (typeof value === 'number') {
return BlackboardValueType.Number;
} else if (typeof value === 'string') {
return BlackboardValueType.String;
} else if (typeof value === 'boolean') {
return BlackboardValueType.Boolean;
} else {
return BlackboardValueType.Object;
}
}
/**
* 转换属性绑定
*/
private static convertPropertyBindings(
connections: EditorConnection[],
nodes: EditorNode[],
blackboard: BlackboardVariableDefinition[]
): PropertyBinding[] {
const bindings: PropertyBinding[] = [];
const blackboardVarNames = new Set(blackboard.map((v) => v.name));
const propertyConnections = connections.filter((conn) => conn.connectionType === 'property');
for (const conn of propertyConnections) {
const fromNode = nodes.find((n) => n.id === conn.from);
const toNode = nodes.find((n) => n.id === conn.to);
if (!fromNode || !toNode || !conn.toProperty) {
logger.warn(`跳过无效的属性连接: from=${conn.from}, to=${conn.to}`);
continue;
}
let variableName: string | undefined;
if (fromNode.data.nodeType === 'blackboard-variable') {
variableName = fromNode.data.variableName;
} else if (conn.fromProperty) {
variableName = conn.fromProperty;
}
if (!variableName) {
logger.warn(`无法确定变量名: from节点=${fromNode.template.displayName}`);
continue;
}
if (!blackboardVarNames.has(variableName)) {
logger.warn(`属性绑定引用了不存在的黑板变量: ${variableName}`);
continue;
}
bindings.push({
nodeId: toNode.id,
propertyName: conn.toProperty,
variableName
});
}
return bindings;
}
/**
* 从资产格式转换回编辑器格式(用于加载)
*
* @param asset 行为树资产
* @returns 编辑器格式数据
*/
static fromAsset(asset: BehaviorTreeAsset): EditorFormat {
logger.info('开始转换资产格式到编辑器格式');
const nodes = this.convertNodesFromAsset(asset.nodes);
const blackboard: Record<string, any> = {};
for (const variable of asset.blackboard) {
blackboard[variable.name] = variable.defaultValue;
}
const connections = this.convertPropertyBindingsToConnections(
asset.propertyBindings || []
);
const nodeConnections = this.buildNodeConnections(asset.nodes);
connections.push(...nodeConnections);
const metadata: { name: string; description?: string; createdAt?: string; modifiedAt?: string } = {
name: asset.metadata.name
};
if (asset.metadata.description) {
metadata.description = asset.metadata.description;
}
if (asset.metadata.createdAt) {
metadata.createdAt = asset.metadata.createdAt;
}
if (asset.metadata.modifiedAt) {
metadata.modifiedAt = asset.metadata.modifiedAt;
}
const editorData: EditorFormat = {
version: asset.metadata.version,
metadata,
nodes,
connections,
blackboard,
canvasState: {
offset: { x: 0, y: 0 },
scale: 1
}
};
logger.info(`转换完成: ${nodes.length}个节点, ${connections.length}个连接`);
return editorData;
}
/**
* 从资产格式转换节点
*/
private static convertNodesFromAsset(assetNodes: BehaviorTreeNodeData[]): EditorNode[] {
return assetNodes.map((node, index) => {
const position = {
x: 100 + (index % 5) * 250,
y: 100 + Math.floor(index / 5) * 150
};
const template: any = {
displayName: node.name,
category: this.inferCategory(node.nodeType),
type: node.nodeType
};
if (node.data.className) {
template.className = node.data.className;
}
return {
id: node.id,
template,
data: { ...node.data },
position,
children: node.children
};
});
}
/**
* 推断节点分类
*/
private static inferCategory(nodeType: NodeType): string {
switch (nodeType) {
case NodeType.Action:
return '动作';
case NodeType.Condition:
return '条件';
case NodeType.Composite:
return '组合';
case NodeType.Decorator:
return '装饰器';
default:
return '其他';
}
}
/**
* 将属性绑定转换为连接
*/
private static convertPropertyBindingsToConnections(
bindings: PropertyBinding[]
): EditorConnection[] {
const connections: EditorConnection[] = [];
for (const binding of bindings) {
connections.push({
from: 'blackboard',
to: binding.nodeId,
toProperty: binding.propertyName,
connectionType: 'property'
});
}
return connections;
}
/**
* 根据children关系构建节点连接
*/
private static buildNodeConnections(nodes: BehaviorTreeNodeData[]): EditorConnection[] {
const connections: EditorConnection[] = [];
for (const node of nodes) {
for (const childId of node.children) {
connections.push({
from: node.id,
to: childId,
connectionType: 'node'
});
}
}
return connections;
}
}

View File

@@ -0,0 +1,434 @@
import { BehaviorTreeData, BehaviorNodeData } from '../execution/BehaviorTreeData';
import { NodeType, AbortType } from '../Types/TaskStatus';
/**
* 编辑器节点数据接口
*/
interface EditorNode {
id: string;
template: {
type: string;
className: string;
displayName?: string;
};
data: Record<string, any>;
children?: string[];
}
/**
* 编辑器连接数据接口
*/
interface EditorConnection {
from: string;
to: string;
connectionType: 'node' | 'property';
fromProperty?: string;
toProperty?: string;
}
/**
* 编辑器行为树数据接口
*/
interface EditorBehaviorTreeData {
version?: string;
metadata?: {
name: string;
description?: string;
createdAt?: string;
modifiedAt?: string;
};
nodes: EditorNode[];
connections?: EditorConnection[];
blackboard?: Record<string, any>;
}
/**
* 编辑器格式到运行时格式的转换器
*
* 负责将编辑器的 JSON 格式包含UI信息转换为运行时的 BehaviorTreeData 格式
*/
export class EditorToBehaviorTreeDataConverter {
/**
* 将编辑器 JSON 字符串转换为运行时 BehaviorTreeData
*/
static fromEditorJSON(json: string): BehaviorTreeData {
const editorData: EditorBehaviorTreeData = JSON.parse(json);
return this.convert(editorData);
}
/**
* 将编辑器数据对象转换为运行时 BehaviorTreeData
*/
static convert(editorData: EditorBehaviorTreeData): BehaviorTreeData {
// 查找根节点
const rootNode = editorData.nodes.find((n) =>
n.template.type === 'root' || n.data['nodeType'] === 'root'
);
if (!rootNode) {
throw new Error('Behavior tree must have a root node');
}
// 构建属性绑定映射nodeId -> { propertyName -> blackboardKey }
const propertyBindingsMap = this.buildPropertyBindingsMap(editorData);
// 转换所有节点(过滤掉不可执行的节点,如黑板变量节点)
const nodesMap = new Map<string, BehaviorNodeData>();
for (const editorNode of editorData.nodes) {
// 跳过黑板变量节点,它们只用于编辑器的可视化绑定
if (this.isNonExecutableNode(editorNode)) {
continue;
}
const propertyBindings = propertyBindingsMap.get(editorNode.id);
const behaviorNodeData = this.convertNode(editorNode, propertyBindings);
nodesMap.set(behaviorNodeData.id, behaviorNodeData);
}
// 转换黑板变量
const blackboardVariables = editorData.blackboard
? new Map(Object.entries(editorData.blackboard))
: new Map();
return {
id: this.generateTreeId(editorData),
name: editorData.metadata?.name || 'Untitled',
rootNodeId: rootNode.id,
nodes: nodesMap,
blackboardVariables
};
}
/**
* 从连接数据构建属性绑定映射
* 处理 connectionType === 'property' 的连接,将黑板变量节点连接到目标节点的属性
*/
private static buildPropertyBindingsMap(
editorData: EditorBehaviorTreeData
): Map<string, Record<string, string>> {
const bindingsMap = new Map<string, Record<string, string>>();
if (!editorData.connections) {
return bindingsMap;
}
// 构建节点 ID 到变量名的映射(用于黑板变量节点)
const nodeToVariableMap = new Map<string, string>();
for (const node of editorData.nodes) {
if (node.data['nodeType'] === 'blackboard-variable' && node.data['variableName']) {
nodeToVariableMap.set(node.id, node.data['variableName']);
}
}
// 处理属性连接
for (const conn of editorData.connections) {
if (conn.connectionType === 'property' && conn.toProperty) {
const variableName = nodeToVariableMap.get(conn.from);
if (variableName) {
// 获取或创建目标节点的绑定记录
let bindings = bindingsMap.get(conn.to);
if (!bindings) {
bindings = {};
bindingsMap.set(conn.to, bindings);
}
// 将属性绑定到黑板变量
bindings[conn.toProperty] = variableName;
}
}
}
return bindingsMap;
}
/**
* 转换单个节点
* @param editorNode 编辑器节点数据
* @param propertyBindings 从连接中提取的属性绑定(可选)
*/
private static convertNode(
editorNode: EditorNode,
propertyBindings?: Record<string, string>
): BehaviorNodeData {
const nodeType = this.mapNodeType(editorNode.template.type);
const config = this.extractConfig(editorNode.data);
// 从节点数据中提取绑定
const dataBindings = this.extractBindings(editorNode.data);
// 合并连接绑定和数据绑定(连接绑定优先)
const bindings = { ...dataBindings, ...propertyBindings };
const abortType = this.extractAbortType(editorNode.data);
// 获取 implementationType优先从 template.className其次从 data 中的类型字段
let implementationType: string | undefined = editorNode.template.className;
if (!implementationType) {
// 尝试从 data 中提取类型
implementationType = this.extractImplementationType(editorNode.data, nodeType);
}
if (!implementationType) {
console.warn(`[EditorToBehaviorTreeDataConverter] Node ${editorNode.id} has no implementationType, using fallback`);
// 根据节点类型使用默认实现
implementationType = this.getDefaultImplementationType(nodeType);
}
return {
id: editorNode.id,
name: editorNode.template.displayName || editorNode.template.className || implementationType,
nodeType,
implementationType,
children: editorNode.children || [],
config,
...(Object.keys(bindings).length > 0 && { bindings }),
...(abortType && { abortType })
};
}
/**
* 检查是否为不可执行的节点(如黑板变量节点)
* 这些节点只在编辑器中使用,不参与运行时执行
*/
private static isNonExecutableNode(editorNode: EditorNode): boolean {
const nodeType = editorNode.data['nodeType'];
// 黑板变量节点不需要执行,只用于可视化绑定
return nodeType === 'blackboard-variable';
}
/**
* 从节点数据中提取实现类型
*
* 优先级:
* 1. template.className标准方式
* 2. data 中的类型字段compositeType, actionType 等)
* 3. 特殊节点类型的默认值(如 Root
*/
private static extractImplementationType(data: Record<string, any>, nodeType: NodeType): string | undefined {
// 节点类型到数据字段的映射
const typeFieldMap: Record<NodeType, string> = {
[NodeType.Composite]: 'compositeType',
[NodeType.Decorator]: 'decoratorType',
[NodeType.Action]: 'actionType',
[NodeType.Condition]: 'conditionType',
[NodeType.Root]: '', // Root 没有对应的数据字段
};
const field = typeFieldMap[nodeType];
if (field && data[field]) {
return data[field];
}
// Root 节点的特殊处理
if (nodeType === NodeType.Root) {
return 'Root';
}
return undefined;
}
/**
* 获取节点类型的默认实现
* 当无法确定具体实现类型时使用
*/
private static getDefaultImplementationType(nodeType: NodeType): string {
// 节点类型到默认实现的映射
const defaultImplementations: Record<NodeType, string> = {
[NodeType.Root]: 'Root',
[NodeType.Composite]: 'Sequence',
[NodeType.Decorator]: 'Inverter',
[NodeType.Action]: 'Wait',
[NodeType.Condition]: 'AlwaysTrue',
};
return defaultImplementations[nodeType] || 'Unknown';
}
/**
* 映射节点类型
*/
private static mapNodeType(type: string): NodeType {
switch (type.toLowerCase()) {
case 'root':
return NodeType.Root;
case 'composite':
return NodeType.Composite;
case 'decorator':
return NodeType.Decorator;
case 'action':
return NodeType.Action;
case 'condition':
return NodeType.Condition;
default:
throw new Error(`Unknown node type: ${type}`);
}
}
/**
* 提取节点配置(过滤掉内部字段和绑定字段)
*/
private static extractConfig(data: Record<string, any>): Record<string, any> {
const config: Record<string, any> = {};
const internalFields = new Set(['nodeType', 'abortType']);
for (const [key, value] of Object.entries(data)) {
// 跳过内部字段
if (internalFields.has(key)) {
continue;
}
// 跳过黑板绑定字段(它们会被提取到 bindings 中)
if (this.isBinding(value)) {
continue;
}
config[key] = value;
}
return config;
}
/**
* 提取黑板变量绑定
*/
private static extractBindings(data: Record<string, any>): Record<string, string> {
const bindings: Record<string, string> = {};
for (const [key, value] of Object.entries(data)) {
if (this.isBinding(value)) {
bindings[key] = this.extractBindingKey(value);
}
}
return bindings;
}
/**
* 判断是否为黑板绑定
*/
private static isBinding(value: any): boolean {
if (typeof value === 'object' && value !== null) {
return value._isBlackboardBinding === true ||
value.type === 'blackboard' ||
(value.blackboardKey !== undefined);
}
return false;
}
/**
* 提取黑板绑定的键名
*/
private static extractBindingKey(binding: any): string {
return binding.blackboardKey || binding.key || binding.value || '';
}
/**
* 提取中止类型(条件装饰器使用)
*/
private static extractAbortType(data: Record<string, any>): AbortType | undefined {
if (!data['abortType']) {
return undefined;
}
const abortTypeStr = String(data['abortType']).toLowerCase();
switch (abortTypeStr) {
case 'none':
return AbortType.None;
case 'self':
return AbortType.Self;
case 'lowerpriority':
case 'lower_priority':
return AbortType.LowerPriority;
case 'both':
return AbortType.Both;
default:
return AbortType.None;
}
}
/**
* 生成行为树ID
*/
private static generateTreeId(editorData: EditorBehaviorTreeData): string {
if (editorData.metadata?.name) {
// 将名称转换为合法ID移除特殊字符
return editorData.metadata.name.replace(/[^a-zA-Z0-9_-]/g, '_');
}
return `tree_${Date.now()}`;
}
/**
* 将运行时格式转换回编辑器格式(用于双向转换)
*/
static toEditorJSON(treeData: BehaviorTreeData): string {
const editorData = this.convertToEditor(treeData);
return JSON.stringify(editorData, null, 2);
}
/**
* 将运行时 BehaviorTreeData 转换为编辑器格式
*/
static convertToEditor(treeData: BehaviorTreeData): EditorBehaviorTreeData {
const nodes: EditorNode[] = [];
for (const [_id, nodeData] of treeData.nodes) {
nodes.push(this.convertNodeToEditor(nodeData));
}
const blackboard = treeData.blackboardVariables
? Object.fromEntries(treeData.blackboardVariables)
: {};
return {
version: '1.0.0',
metadata: {
name: treeData.name,
description: '',
modifiedAt: new Date().toISOString()
},
nodes,
blackboard
};
}
/**
* 将运行时节点转换为编辑器节点
*/
private static convertNodeToEditor(nodeData: BehaviorNodeData): EditorNode {
const data: Record<string, any> = { ...nodeData.config };
// 添加绑定回数据对象
if (nodeData.bindings) {
for (const [key, blackboardKey] of Object.entries(nodeData.bindings)) {
data[key] = {
_isBlackboardBinding: true,
blackboardKey
};
}
}
// 添加中止类型
if (nodeData.abortType !== undefined) {
data['abortType'] = nodeData.abortType;
}
// 获取节点类型字符串
let typeStr: string;
if (typeof nodeData.nodeType === 'string') {
typeStr = nodeData.nodeType;
} else {
typeStr = 'action'; // 默认值
}
const result: EditorNode = {
id: nodeData.id,
template: {
type: typeStr,
className: nodeData.implementationType,
displayName: nodeData.name
},
data
};
if (nodeData.children && nodeData.children.length > 0) {
result.children = nodeData.children;
}
return result;
}
}

View File

@@ -0,0 +1,420 @@
import { NodeType } from '../Types/TaskStatus';
import { NodeMetadataRegistry, ConfigFieldDefinition, NodeMetadata } from '../execution/NodeMetadata';
/**
* 节点数据JSON格式
*/
export interface NodeDataJSON {
nodeType: string;
compositeType?: string;
decoratorType?: string;
actionType?: string;
conditionType?: string;
[key: string]: any;
}
/**
* 行为树节点属性类型常量
* Behavior tree node property type constants
*/
export const NodePropertyType = {
/** 字符串 */
String: 'string',
/** 数值 */
Number: 'number',
/** 布尔值 */
Boolean: 'boolean',
/** 选择框 */
Select: 'select',
/** 黑板变量引用 */
Blackboard: 'blackboard',
/** 代码编辑器 */
Code: 'code',
/** 变量引用 */
Variable: 'variable',
/** 资产引用 */
Asset: 'asset'
} as const;
/**
* 节点属性类型(支持自定义扩展)
* Node property type (supports custom extensions)
*
* @example
* ```typescript
* // 使用内置类型
* type: NodePropertyType.String
*
* // 使用自定义类型
* type: 'color-picker'
* type: 'curve-editor'
* ```
*/
export type NodePropertyType = (typeof NodePropertyType)[keyof typeof NodePropertyType] | string;
/**
* 属性定义(用于编辑器)
*/
export interface PropertyDefinition {
name: string;
type: NodePropertyType;
label: string;
description?: string;
defaultValue?: any;
options?: Array<{ label: string; value: any }>;
min?: number;
max?: number;
step?: number;
required?: boolean;
/**
* 字段编辑器配置
*
* 指定使用哪个字段编辑器以及相关选项
*
* @example
* ```typescript
* fieldEditor: {
* type: 'asset',
* options: { fileExtension: '.btree' }
* }
* ```
*/
fieldEditor?: {
type: string;
options?: Record<string, any>;
};
/**
* 自定义渲染配置
*
* 用于指定编辑器如何渲染此属性
*
* @example
* ```typescript
* renderConfig: {
* component: 'ColorPicker', // 渲染器组件名称
* props: { // 传递给组件的属性
* showAlpha: true,
* presets: ['#FF0000', '#00FF00']
* }
* }
* ```
*/
renderConfig?: {
/** 渲染器组件名称或路径 */
component?: string;
/** 传递给渲染器的属性配置 */
props?: Record<string, any>;
/** 渲染器的样式类名 */
className?: string;
/** 渲染器的内联样式 */
style?: Record<string, any>;
/** 其他自定义配置 */
[key: string]: any;
};
/**
* 验证规则
*
* 用于在编辑器中验证输入
*
* @example
* ```typescript
* validation: {
* pattern: /^\d+$/,
* message: '只能输入数字',
* validator: (value) => value > 0
* }
* ```
*/
validation?: {
/** 正则表达式验证 */
pattern?: RegExp | string;
/** 验证失败的提示信息 */
message?: string;
/** 自定义验证函数 */
validator?: string; // 函数字符串,编辑器会解析
/** 最小长度(字符串) */
minLength?: number;
/** 最大长度(字符串) */
maxLength?: number;
};
/**
* 是否允许多个连接
* 默认 false只允许一个黑板变量连接
*/
allowMultipleConnections?: boolean;
}
/**
* 节点模板(用于编辑器)
*/
export interface NodeTemplate {
type: NodeType;
displayName: string;
category: string;
icon?: string;
description: string;
color?: string;
className?: string;
componentClass?: Function;
requiresChildren?: boolean;
minChildren?: number;
maxChildren?: number;
defaultConfig: Partial<NodeDataJSON>;
properties: PropertyDefinition[];
}
/**
* 节点模板库
*/
export class NodeTemplates {
/**
* 获取所有节点模板
*/
static getAllTemplates(): NodeTemplate[] {
const allMetadata = NodeMetadataRegistry.getAllMetadata();
return allMetadata.map((metadata) => this.convertMetadataToTemplate(metadata));
}
/**
* 根据类型和子类型获取模板
*/
static getTemplate(type: NodeType, subType: string): NodeTemplate | undefined {
return this.getAllTemplates().find((t) => {
if (t.type !== type) return false;
const config: any = t.defaultConfig;
switch (type) {
case NodeType.Composite:
return config.compositeType === subType;
case NodeType.Decorator:
return config.decoratorType === subType;
case NodeType.Action:
return config.actionType === subType;
case NodeType.Condition:
return config.conditionType === subType;
default:
return false;
}
});
}
/**
* 将NodeMetadata转换为NodeTemplate
*/
private static convertMetadataToTemplate(metadata: NodeMetadata): NodeTemplate {
const properties = this.convertConfigSchemaToProperties(metadata.configSchema || {});
const defaultConfig: Partial<NodeDataJSON> = {
nodeType: this.nodeTypeToString(metadata.nodeType)
};
switch (metadata.nodeType) {
case NodeType.Composite:
defaultConfig.compositeType = metadata.implementationType;
break;
case NodeType.Decorator:
defaultConfig.decoratorType = metadata.implementationType;
break;
case NodeType.Action:
defaultConfig.actionType = metadata.implementationType;
break;
case NodeType.Condition:
defaultConfig.conditionType = metadata.implementationType;
break;
}
if (metadata.configSchema) {
for (const [key, field] of Object.entries(metadata.configSchema)) {
const fieldDef = field as ConfigFieldDefinition;
if (fieldDef.default !== undefined) {
defaultConfig[key] = fieldDef.default;
}
}
}
// 根据节点类型生成默认颜色和图标
const { icon, color } = this.getIconAndColorByType(metadata.nodeType, metadata.category || '');
// 应用子节点约束
const constraints = metadata.childrenConstraints || this.getDefaultConstraintsByNodeType(metadata.nodeType);
const template: NodeTemplate = {
type: metadata.nodeType,
displayName: metadata.displayName,
category: metadata.category || this.getCategoryByNodeType(metadata.nodeType),
description: metadata.description || '',
className: metadata.implementationType,
icon,
color,
defaultConfig,
properties
};
if (constraints) {
if (constraints.min !== undefined) {
template.minChildren = constraints.min;
template.requiresChildren = constraints.min > 0;
}
if (constraints.max !== undefined) {
template.maxChildren = constraints.max;
}
}
return template;
}
/**
* 获取节点类型的默认约束
*/
private static getDefaultConstraintsByNodeType(nodeType: NodeType): { min?: number; max?: number } | undefined {
switch (nodeType) {
case NodeType.Composite:
return { min: 1 };
case NodeType.Decorator:
return { min: 1, max: 1 };
case NodeType.Action:
case NodeType.Condition:
return { max: 0 };
default:
return undefined;
}
}
/**
* 将ConfigSchema转换为PropertyDefinition数组
*/
private static convertConfigSchemaToProperties(
configSchema: Record<string, ConfigFieldDefinition>
): PropertyDefinition[] {
const properties: PropertyDefinition[] = [];
for (const [name, field] of Object.entries(configSchema)) {
const property: PropertyDefinition = {
name,
type: this.mapFieldTypeToPropertyType(field),
label: name
};
if (field.description !== undefined) {
property.description = field.description;
}
if (field.default !== undefined) {
property.defaultValue = field.default;
}
if (field.min !== undefined) {
property.min = field.min;
}
if (field.max !== undefined) {
property.max = field.max;
}
if (field.allowMultipleConnections !== undefined) {
property.allowMultipleConnections = field.allowMultipleConnections;
}
if (field.options) {
property.options = field.options.map((opt) => ({
label: opt,
value: opt
}));
}
if (field.supportBinding) {
property.renderConfig = {
component: 'BindableInput',
props: {
supportBinding: true
}
};
}
properties.push(property);
}
return properties;
}
/**
* 映射字段类型到属性类型
*/
private static mapFieldTypeToPropertyType(field: ConfigFieldDefinition): NodePropertyType {
if (field.options && field.options.length > 0) {
return NodePropertyType.Select;
}
switch (field.type) {
case 'string':
return NodePropertyType.String;
case 'number':
return NodePropertyType.Number;
case 'boolean':
return NodePropertyType.Boolean;
case 'array':
case 'object':
default:
return NodePropertyType.String;
}
}
/**
* NodeType转字符串
*/
private static nodeTypeToString(nodeType: NodeType): string {
switch (nodeType) {
case NodeType.Composite:
return 'composite';
case NodeType.Decorator:
return 'decorator';
case NodeType.Action:
return 'action';
case NodeType.Condition:
return 'condition';
default:
return 'unknown';
}
}
/**
* 根据NodeType获取默认分类
*/
private static getCategoryByNodeType(nodeType: NodeType): string {
switch (nodeType) {
case NodeType.Composite:
return '组合';
case NodeType.Decorator:
return '装饰器';
case NodeType.Action:
return '动作';
case NodeType.Condition:
return '条件';
default:
return '其他';
}
}
/**
* 根据节点类型获取默认图标和颜色
*/
private static getIconAndColorByType(nodeType: NodeType, _category: string): { icon: string; color: string } {
// 根据节点类型设置默认值
switch (nodeType) {
case NodeType.Composite:
return { icon: 'GitBranch', color: '#1976d2' }; // 蓝色
case NodeType.Decorator:
return { icon: 'Settings', color: '#fb8c00' }; // 橙色
case NodeType.Action:
return { icon: 'Play', color: '#388e3c' }; // 绿色
case NodeType.Condition:
return { icon: 'HelpCircle', color: '#d32f2f' }; // 红色
default:
return { icon: 'Circle', color: '#757575' }; // 灰色
}
}
}

View File

@@ -0,0 +1,179 @@
import { IService } from '@esengine/ecs-framework';
import { BlackboardValueType, BlackboardVariable } from '../Types/TaskStatus';
/**
* 全局黑板配置
*/
export interface GlobalBlackboardConfig {
version: string;
variables: BlackboardVariable[];
}
/**
* 全局黑板服务
*
* 提供所有行为树共享的全局变量存储
*
* 使用方式:
* ```typescript
* // 注册服务(在 BehaviorTreePlugin.install 中自动完成)
* core.services.registerSingleton(GlobalBlackboardService);
*
* // 获取服务
* const blackboard = core.services.resolve(GlobalBlackboardService);
* ```
*/
export class GlobalBlackboardService implements IService {
private variables: Map<string, BlackboardVariable> = new Map();
dispose(): void {
this.variables.clear();
}
/**
* 定义全局变量
*/
defineVariable(
name: string,
type: BlackboardValueType,
initialValue: any,
options?: {
readonly?: boolean;
description?: string;
}
): void {
const variable: BlackboardVariable = {
name,
type,
value: initialValue,
readonly: options?.readonly ?? false
};
if (options?.description !== undefined) {
variable.description = options.description;
}
this.variables.set(name, variable);
}
/**
* 获取全局变量值
*/
getValue<T = any>(name: string): T | undefined {
const variable = this.variables.get(name);
return variable?.value as T;
}
/**
* 设置全局变量值
*/
setValue(name: string, value: any, force: boolean = false): boolean {
const variable = this.variables.get(name);
if (!variable) {
return false;
}
if (variable.readonly && !force) {
return false;
}
variable.value = value;
return true;
}
/**
* 检查全局变量是否存在
*/
hasVariable(name: string): boolean {
return this.variables.has(name);
}
/**
* 删除全局变量
*/
removeVariable(name: string): boolean {
return this.variables.delete(name);
}
/**
* 获取所有变量名
*/
getVariableNames(): string[] {
return Array.from(this.variables.keys());
}
/**
* 获取所有变量
*/
getAllVariables(): BlackboardVariable[] {
return Array.from(this.variables.values());
}
/**
* 清空所有全局变量
*/
clear(): void {
this.variables.clear();
}
/**
* 批量设置变量
*/
setVariables(values: Record<string, any>): void {
for (const [name, value] of Object.entries(values)) {
const variable = this.variables.get(name);
if (variable && !variable.readonly) {
variable.value = value;
}
}
}
/**
* 批量获取变量
*/
getVariables(names: string[]): Record<string, any> {
const result: Record<string, any> = {};
for (const name of names) {
const value = this.getValue(name);
if (value !== undefined) {
result[name] = value;
}
}
return result;
}
/**
* 导出配置
*/
exportConfig(): GlobalBlackboardConfig {
return {
version: '1.0',
variables: Array.from(this.variables.values())
};
}
/**
* 导入配置
*/
importConfig(config: GlobalBlackboardConfig): void {
this.variables.clear();
for (const variable of config.variables) {
this.variables.set(variable.name, variable);
}
}
/**
* 序列化为 JSON
*/
toJSON(): string {
return JSON.stringify(this.exportConfig(), null, 2);
}
/**
* 从 JSON 反序列化
*/
static fromJSON(json: string): GlobalBlackboardConfig {
return JSON.parse(json);
}
}

View File

@@ -0,0 +1,51 @@
/**
* @zh 资产管理器接口(可选依赖)
* @en Asset manager interface (optional dependency)
*
* @zh 行为树系统的可选资产管理器接口。
* 当与 ESEngine 的 asset-system 集成时,传入 IAssetManager 实例。
* 不使用 ESEngine 时,可以直接使用 BehaviorTreeAssetManager.loadFromEditorJSON()。
*
* @en Optional asset manager interface for behavior tree system.
* When integrating with ESEngine's asset-system, pass an IAssetManager instance.
* When not using ESEngine, use BehaviorTreeAssetManager.loadFromEditorJSON() directly.
*/
import type { BehaviorTreeData } from '../execution/BehaviorTreeData';
/**
* @zh 行为树资产内容
* @en Behavior tree asset content
*/
export interface IBehaviorTreeAssetContent {
/** @zh 行为树数据 @en Behavior tree data */
data: BehaviorTreeData;
/** @zh 文件路径 @en File path */
path: string;
}
/**
* @zh 简化的资产管理器接口
* @en Simplified asset manager interface
*
* @zh 这是行为树系统需要的最小资产管理器接口。
* ESEngine 的 IAssetManager 实现此接口。
* 其他引擎可以提供自己的实现。
*
* @en This is the minimal asset manager interface required by the behavior tree system.
* ESEngine's IAssetManager implements this interface.
* Other engines can provide their own implementation.
*/
export interface IBTAssetManager {
/**
* @zh 通过 GUID 加载资产
* @en Load asset by GUID
*/
loadAsset(guid: string): Promise<{ asset: IBehaviorTreeAssetContent | null } | null>;
/**
* @zh 通过 GUID 获取已加载的资产
* @en Get loaded asset by GUID
*/
getAsset<T = IBehaviorTreeAssetContent>(guid: string): T | null;
}

View File

@@ -0,0 +1,127 @@
/**
* 任务执行状态
*/
export enum TaskStatus {
/** 无效状态 - 节点未初始化或已被重置 */
Invalid = 0,
/** 成功 - 节点执行成功完成 */
Success = 1,
/** 失败 - 节点执行失败 */
Failure = 2,
/** 运行中 - 节点正在执行,需要在后续帧继续 */
Running = 3
}
/**
* 内置节点类型常量
*/
export const NodeType = {
/** 根节点 - 行为树的起始节点 */
Root: 'root',
/** 复合节点 - 有多个子节点 */
Composite: 'composite',
/** 装饰器节点 - 有一个子节点 */
Decorator: 'decorator',
/** 动作节点 - 叶子节点 */
Action: 'action',
/** 条件节点 - 叶子节点 */
Condition: 'condition'
} as const;
/**
* 节点类型(支持自定义扩展)
*
* 使用内置类型或自定义字符串
*
* @example
* ```typescript
* // 使用内置类型
* type: NodeType.Action
*
* // 使用自定义类型
* type: 'custom-behavior'
* ```
*/
export type NodeType = typeof NodeType[keyof typeof NodeType] | string;
/**
* 复合节点类型
*/
export enum CompositeType {
/** 序列 - 按顺序执行,全部成功才成功 */
Sequence = 'sequence',
/** 选择 - 按顺序执行,任一成功则成功 */
Selector = 'selector',
/** 并行 - 同时执行所有子节点 */
Parallel = 'parallel',
/** 并行选择 - 并行执行,任一成功则成功 */
ParallelSelector = 'parallel-selector',
/** 随机序列 - 随机顺序执行序列 */
RandomSequence = 'random-sequence',
/** 随机选择 - 随机顺序执行选择 */
RandomSelector = 'random-selector'
}
/**
* 装饰器节点类型
*/
export enum DecoratorType {
/** 反转 - 反转子节点结果 */
Inverter = 'inverter',
/** 重复 - 重复执行子节点 */
Repeater = 'repeater',
/** 直到成功 - 重复直到成功 */
UntilSuccess = 'until-success',
/** 直到失败 - 重复直到失败 */
UntilFail = 'until-fail',
/** 总是成功 - 无论子节点结果都返回成功 */
AlwaysSucceed = 'always-succeed',
/** 总是失败 - 无论子节点结果都返回失败 */
AlwaysFail = 'always-fail',
/** 条件装饰器 - 基于条件执行子节点 */
Conditional = 'conditional',
/** 冷却 - 冷却时间内阻止执行 */
Cooldown = 'cooldown',
/** 超时 - 超时则返回失败 */
Timeout = 'timeout'
}
/**
* 中止类型
*
* 用于动态优先级和条件重新评估
*/
export enum AbortType {
/** 无 - 不中止任何节点 */
None = 'none',
/** 自身 - 条件变化时可以中止自身的执行 */
Self = 'self',
/** 低优先级 - 条件满足时可以中止低优先级的兄弟节点 */
LowerPriority = 'lower-priority',
/** 两者 - 可以中止自身和低优先级节点 */
Both = 'both'
}
/**
* 黑板变量类型
*/
export enum BlackboardValueType {
String = 'string',
Number = 'number',
Boolean = 'boolean',
Vector2 = 'vector2',
Vector3 = 'vector3',
Object = 'object',
Array = 'array'
}
/**
* 黑板变量定义
*/
export interface BlackboardVariable {
name: string;
type: BlackboardValueType;
value: any;
readonly?: boolean;
description?: string;
}

View File

@@ -0,0 +1,10 @@
/**
* Behavior Tree Constants
* 行为树常量
*/
// Asset type constant for behavior tree
// 行为树资产类型常量
// 必须与 module.json 中 assetExtensions 定义的类型一致
// Must match the type defined in module.json assetExtensions
export const BehaviorTreeAssetType = 'behavior-tree' as const;

View File

@@ -0,0 +1,82 @@
/**
* @zh ESEngine 资产加载器
* @en ESEngine asset loader
*
* @zh 实现 IAssetLoader 接口,用于通过 AssetManager 加载行为树文件。
* 此文件仅在使用 ESEngine 时需要。
*
* @en Implements IAssetLoader interface for loading behavior tree files via AssetManager.
* This file is only needed when using ESEngine.
*/
import type {
IAssetLoader,
IAssetParseContext,
IAssetContent,
AssetContentType
} from '@esengine/asset-system';
import { Core } from '@esengine/ecs-framework';
import { BehaviorTreeData } from '../execution/BehaviorTreeData';
import { BehaviorTreeAssetManager } from '../execution/BehaviorTreeAssetManager';
import { EditorToBehaviorTreeDataConverter } from '../Serialization/EditorToBehaviorTreeDataConverter';
import { BehaviorTreeAssetType } from '../constants';
/**
* @zh 行为树资产接口
* @en Behavior tree asset interface
*/
export interface IBehaviorTreeAsset {
/** @zh 行为树数据 @en Behavior tree data */
data: BehaviorTreeData;
/** @zh 文件路径 @en File path */
path: string;
}
/**
* @zh 行为树加载器
* @en Behavior tree loader implementing IAssetLoader interface
*/
export class BehaviorTreeLoader implements IAssetLoader<IBehaviorTreeAsset> {
readonly supportedType = BehaviorTreeAssetType;
readonly supportedExtensions = ['.btree'];
readonly contentType: AssetContentType = 'text';
/**
* @zh 从内容解析行为树资产
* @en Parse behavior tree asset from content
*/
async parse(content: IAssetContent, context: IAssetParseContext): Promise<IBehaviorTreeAsset> {
if (!content.text) {
throw new Error('Behavior tree content is empty');
}
// Convert to runtime data
const treeData = EditorToBehaviorTreeDataConverter.fromEditorJSON(content.text);
// Use file path as ID
const assetPath = context.metadata.path;
treeData.id = assetPath;
// Also register to BehaviorTreeAssetManager for legacy code
const btAssetManager = Core.services.tryResolve(BehaviorTreeAssetManager);
if (btAssetManager) {
btAssetManager.loadAsset(treeData);
}
return {
data: treeData,
path: assetPath
};
}
/**
* @zh 释放资产
* @en Dispose asset
*/
dispose(asset: IBehaviorTreeAsset): void {
const btAssetManager = Core.services.tryResolve(BehaviorTreeAssetManager);
if (btAssetManager && asset.data) {
btAssetManager.unloadAsset(asset.data.id);
}
}
}

View File

@@ -0,0 +1,93 @@
/**
* @zh ESEngine 集成模块
* @en ESEngine integration module
*
* @zh 此文件包含与 ESEngine 引擎核心集成的代码。
* 使用 Cocos/Laya 等其他引擎时不需要此文件。
*
* @en This file contains code for integrating with ESEngine engine-core.
* Not needed when using other engines like Cocos/Laya.
*/
import type { IScene, ServiceContainer, IComponentRegistry } from '@esengine/ecs-framework';
import type { IRuntimeModule, IRuntimePlugin, ModuleManifest, SystemContext } from '@esengine/engine-core';
import { AssetManagerToken } from '@esengine/asset-system';
import { BehaviorTreeRuntimeComponent } from '../execution/BehaviorTreeRuntimeComponent';
import { BehaviorTreeExecutionSystem } from '../execution/BehaviorTreeExecutionSystem';
import { BehaviorTreeAssetManager } from '../execution/BehaviorTreeAssetManager';
import { GlobalBlackboardService } from '../Services/GlobalBlackboardService';
import { BehaviorTreeLoader } from './BehaviorTreeLoader';
import { BehaviorTreeAssetType } from '../constants';
import { BehaviorTreeSystemToken } from '../tokens';
// Re-export tokens for ESEngine users
export { BehaviorTreeSystemToken } from '../tokens';
class BehaviorTreeRuntimeModule implements IRuntimeModule {
private _loaderRegistered = false;
registerComponents(registry: IComponentRegistry): void {
registry.register(BehaviorTreeRuntimeComponent);
}
registerServices(services: ServiceContainer): void {
if (!services.isRegistered(GlobalBlackboardService)) {
services.registerSingleton(GlobalBlackboardService);
}
if (!services.isRegistered(BehaviorTreeAssetManager)) {
services.registerSingleton(BehaviorTreeAssetManager);
}
}
createSystems(scene: IScene, context: SystemContext): void {
// Get dependencies from service registry
const assetManager = context.services.get(AssetManagerToken);
if (!this._loaderRegistered && assetManager) {
assetManager.registerLoader(BehaviorTreeAssetType, new BehaviorTreeLoader());
this._loaderRegistered = true;
}
// Use ECS service container from context.services
const ecsServices = (context as { ecsServices?: ServiceContainer }).ecsServices;
const behaviorTreeSystem = new BehaviorTreeExecutionSystem(ecsServices);
if (assetManager) {
behaviorTreeSystem.setAssetManager(assetManager);
}
if (context.isEditor) {
behaviorTreeSystem.enabled = false;
}
scene.addSystem(behaviorTreeSystem);
// Register service to service registry
context.services.register(BehaviorTreeSystemToken, behaviorTreeSystem);
}
}
const manifest: ModuleManifest = {
id: 'behavior-tree',
name: '@esengine/behavior-tree',
displayName: 'Behavior Tree',
version: '1.0.0',
description: 'AI behavior tree system',
category: 'AI',
icon: 'GitBranch',
isCore: false,
defaultEnabled: false,
isEngineModule: true,
canContainContent: true,
dependencies: ['core'],
exports: { components: ['BehaviorTreeComponent'] },
editorPackage: '@esengine/behavior-tree-editor'
};
export const BehaviorTreePlugin: IRuntimePlugin = {
manifest,
runtimeModule: new BehaviorTreeRuntimeModule()
};
export { BehaviorTreeRuntimeModule };

View File

@@ -0,0 +1,39 @@
/**
* @zh ESEngine 集成入口
* @en ESEngine integration entry point
*
* @zh 此模块包含与 ESEngine 引擎核心集成所需的所有代码。
* 使用 Cocos/Laya 等其他引擎时,只需导入主模块即可。
*
* @en This module contains all code required for ESEngine engine-core integration.
* When using other engines like Cocos/Laya, just import the main module.
*
* @example ESEngine 使用方式 / ESEngine usage:
* ```typescript
* import { BehaviorTreePlugin } from '@esengine/behavior-tree/esengine';
*
* // Register with ESEngine plugin system
* engine.registerPlugin(BehaviorTreePlugin);
* ```
*
* @example Cocos/Laya 使用方式 / Cocos/Laya usage:
* ```typescript
* import {
* BehaviorTreeAssetManager,
* BehaviorTreeExecutionSystem
* } from '@esengine/behavior-tree';
*
* // Load behavior tree from JSON
* const assetManager = new BehaviorTreeAssetManager();
* assetManager.loadFromEditorJSON(jsonContent);
*
* // Add system to your ECS world
* world.addSystem(new BehaviorTreeExecutionSystem());
* ```
*/
// Runtime module and plugin
export { BehaviorTreeRuntimeModule, BehaviorTreePlugin, BehaviorTreeSystemToken } from './BehaviorTreeRuntimeModule';
// Asset loader for ESEngine asset-system
export { BehaviorTreeLoader, type IBehaviorTreeAsset } from './BehaviorTreeLoader';

View File

@@ -0,0 +1,136 @@
import { BehaviorTreeData } from './BehaviorTreeData';
import { createLogger, IService } from '@esengine/ecs-framework';
import { EditorToBehaviorTreeDataConverter } from '../Serialization/EditorToBehaviorTreeDataConverter';
const logger = createLogger('BehaviorTreeAssetManager');
/**
* 行为树资产管理器(服务)
*
* 管理所有共享的BehaviorTreeData
* 多个实例可以引用同一份数据
*
* 使用方式:
* ```typescript
* // 注册服务
* Core.services.registerSingleton(BehaviorTreeAssetManager);
*
* // 使用服务
* const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
* ```
*/
export class BehaviorTreeAssetManager implements IService {
/**
* 已加载的行为树资产
*/
private assets: Map<string, BehaviorTreeData> = new Map();
/**
* 加载行为树资产
*/
loadAsset(asset: BehaviorTreeData): void {
if (this.assets.has(asset.id)) {
logger.warn(`行为树资产已存在,将被覆盖: ${asset.id}`);
}
this.assets.set(asset.id, asset);
logger.info(`行为树资产已加载: ${asset.name} (${asset.nodes.size}个节点)`);
}
/**
* 从编辑器 JSON 格式加载行为树资产
*
* @param json 编辑器导出的 JSON 字符串
* @returns 加载的行为树数据
*
* @example
* ```typescript
* const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
* const jsonContent = await readFile('path/to/tree.btree');
* const treeData = assetManager.loadFromEditorJSON(jsonContent);
* ```
*/
loadFromEditorJSON(json: string): BehaviorTreeData {
try {
const treeData = EditorToBehaviorTreeDataConverter.fromEditorJSON(json);
this.loadAsset(treeData);
return treeData;
} catch (error) {
logger.error('从编辑器JSON加载失败:', error);
throw error;
}
}
/**
* 批量加载多个行为树资产从编辑器JSON
*
* @param jsonDataList JSON字符串列表
* @returns 成功加载的资产数量
*/
loadMultipleFromEditorJSON(jsonDataList: string[]): number {
let successCount = 0;
for (const json of jsonDataList) {
try {
this.loadFromEditorJSON(json);
successCount++;
} catch (error) {
logger.error('批量加载时出错:', error);
}
}
logger.info(`批量加载完成: ${successCount}/${jsonDataList.length} 个资产`);
return successCount;
}
/**
* 获取行为树资产
*/
getAsset(assetId: string): BehaviorTreeData | undefined {
return this.assets.get(assetId);
}
/**
* 检查资产是否存在
*/
hasAsset(assetId: string): boolean {
return this.assets.has(assetId);
}
/**
* 卸载行为树资产
*/
unloadAsset(assetId: string): boolean {
const result = this.assets.delete(assetId);
if (result) {
logger.info(`行为树资产已卸载: ${assetId}`);
}
return result;
}
/**
* 清空所有资产
*/
clearAll(): void {
this.assets.clear();
logger.info('所有行为树资产已清空');
}
/**
* 获取已加载资产数量
*/
getAssetCount(): number {
return this.assets.size;
}
/**
* 获取所有资产ID
*/
getAllAssetIds(): string[] {
return Array.from(this.assets.keys());
}
/**
* 释放资源实现IService接口
*/
dispose(): void {
this.clearAll();
}
}

View File

@@ -0,0 +1,102 @@
import { TaskStatus, NodeType, AbortType } from '../Types/TaskStatus';
/**
* 行为树节点定义(纯数据结构)
*
* 不依赖Entity可以被多个实例共享
*/
export interface BehaviorNodeData {
/** 节点唯一ID */
id: string;
/** 节点名称(用于调试) */
name: string;
/** 节点类型 */
nodeType: NodeType;
/** 节点实现类型对应Component类名 */
implementationType: string;
/** 子节点ID列表 */
children?: string[];
/** 节点特定配置数据 */
config: Record<string, any>;
/** 属性到黑板变量的绑定映射 */
bindings?: Record<string, string>;
/** 中止类型(条件装饰器使用) */
abortType?: AbortType;
}
/**
* 行为树定义可共享的Asset
*/
export interface BehaviorTreeData {
/** 树ID */
id: string;
/** 树名称 */
name: string;
/** 根节点ID */
rootNodeId: string;
/** 所有节点(扁平化存储) */
nodes: Map<string, BehaviorNodeData>;
/** 黑板变量定义 */
blackboardVariables?: Map<string, any>;
}
/**
* 节点运行时状态
*
* 每个BehaviorTreeRuntimeComponent实例独立维护
*/
export interface NodeRuntimeState {
/** 当前执行状态 */
status: TaskStatus;
/** 当前执行的子节点索引(复合节点使用) */
currentChildIndex: number;
/** 执行顺序号(用于调试和可视化) */
executionOrder?: number;
/** 开始执行时间(某些节点需要) */
startTime?: number;
/** 上次执行时间(冷却节点使用) */
lastExecutionTime?: number;
/** 当前重复次数(重复节点使用) */
repeatCount?: number;
/** 缓存的结果(某些条件节点使用) */
cachedResult?: any;
/** 洗牌后的索引(随机节点使用) */
shuffledIndices?: number[];
/** 是否被中止 */
isAborted?: boolean;
/** 上次条件评估结果(条件装饰器使用) */
lastConditionResult?: boolean;
/** 正在观察的黑板键(条件装饰器使用) */
observedKeys?: string[];
}
/**
* 创建默认的运行时状态
*/
export function createDefaultRuntimeState(): NodeRuntimeState {
return {
status: TaskStatus.Invalid,
currentChildIndex: 0
};
}

View File

@@ -0,0 +1,383 @@
import { EntitySystem, Matcher, Entity, Time, Core, ECSSystem, ServiceContainer } from '@esengine/ecs-framework';
import type { IBTAssetManager, IBehaviorTreeAssetContent } from '../Types/AssetManagerInterface';
import { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
import { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager';
import { NodeExecutorRegistry, NodeExecutionContext } from './NodeExecutor';
import { BehaviorTreeData, BehaviorNodeData } from './BehaviorTreeData';
import { TaskStatus } from '../Types/TaskStatus';
import { NodeMetadataRegistry } from './NodeMetadata';
import './Executors';
/**
* 行为树执行系统
*
* 统一处理所有行为树的执行
*/
@ECSSystem('BehaviorTreeExecution')
export class BehaviorTreeExecutionSystem extends EntitySystem {
private btAssetManager: BehaviorTreeAssetManager | null = null;
private executorRegistry: NodeExecutorRegistry;
private _services: ServiceContainer | null = null;
/** 引用外部资产管理器(可选,由外部模块设置) */
private _assetManager: IBTAssetManager | null = null;
/** 已警告过的缺失资产,避免重复警告 */
private _warnedMissingAssets: Set<string> = new Set();
constructor(services?: ServiceContainer) {
super(Matcher.empty().all(BehaviorTreeRuntimeComponent));
this._services = services || null;
this.executorRegistry = new NodeExecutorRegistry();
this.registerBuiltInExecutors();
}
/**
* @zh 设置外部资产管理器引用(可选)
* @en Set external asset manager reference (optional)
*
* @zh 当与 ESEngine 集成时,由 BehaviorTreeRuntimeModule 调用。
* 不使用 ESEngine 时,可以不调用此方法,
* 直接使用 BehaviorTreeAssetManager.loadFromEditorJSON() 加载资产。
*
* @en Called by BehaviorTreeRuntimeModule when integrating with ESEngine.
* When not using ESEngine, you can skip this and use
* BehaviorTreeAssetManager.loadFromEditorJSON() to load assets directly.
*/
setAssetManager(assetManager: IBTAssetManager | null): void {
this._assetManager = assetManager;
}
/**
* 启动所有 autoStart 的行为树(用于预览模式)
* Start all autoStart behavior trees (for preview mode)
*
* 由于编辑器模式下系统默认禁用,实体添加时 onAdded 不会处理自动启动。
* 预览开始时需要手动调用此方法来启动所有需要自动启动的行为树。
*/
startAllAutoStartTrees(): void {
if (!this.scene) {
this.logger.warn('Scene not available, cannot start auto-start trees');
return;
}
const entities = this.scene.entities.findEntitiesWithComponent(BehaviorTreeRuntimeComponent);
for (const entity of entities) {
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
if (runtime && runtime.autoStart && runtime.treeAssetId && !runtime.isRunning) {
this.ensureAssetLoaded(runtime.treeAssetId).then(() => {
if (runtime && runtime.autoStart && !runtime.isRunning) {
runtime.start();
this.logger.debug(`Auto-started behavior tree for entity: ${entity.name}`);
}
}).catch(e => {
this.logger.error(`Failed to load behavior tree for entity ${entity.name}:`, e);
});
}
}
}
/**
* 当实体添加到系统时,处理自动启动
* Handle auto-start when entity is added to system
*/
protected override onAdded(entity: Entity): void {
// 只有在系统启用时才自动启动
// Only auto-start when system is enabled
if (!this.enabled) return;
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
if (runtime && runtime.autoStart && runtime.treeAssetId && !runtime.isRunning) {
// 先尝试加载资产(如果是文件路径)
this.ensureAssetLoaded(runtime.treeAssetId).then(() => {
// 检查实体是否仍然有效
if (runtime && runtime.autoStart && !runtime.isRunning) {
runtime.start();
this.logger.debug(`Auto-started behavior tree for entity: ${entity.name}`);
}
}).catch(e => {
this.logger.error(`Failed to load behavior tree for entity ${entity.name}:`, e);
});
}
}
/**
* 确保行为树资产已加载
* Ensure behavior tree asset is loaded
*/
private async ensureAssetLoaded(assetGuid: string): Promise<void> {
const btAssetManager = this.getBTAssetManager();
// 如果资产已存在,直接返回
if (btAssetManager.hasAsset(assetGuid)) {
return;
}
// 使用 AssetManager 加载(必须通过 setAssetManager 设置)
// Use AssetManager (must be set via setAssetManager)
if (!this._assetManager) {
this.logger.warn(`AssetManager not set, cannot load: ${assetGuid}`);
return;
}
try {
// 使用 loadAsset 通过 GUID 加载,而不是 loadAssetByPath
// Use loadAsset with GUID instead of loadAssetByPath
const result = await this._assetManager.loadAsset(assetGuid);
if (result && result.asset) {
this.logger.debug(`Behavior tree loaded via AssetManager: ${assetGuid}`);
}
} catch (e) {
this.logger.warn(`Failed to load via AssetManager: ${assetGuid}`, e);
}
}
private getBTAssetManager(): BehaviorTreeAssetManager {
if (!this.btAssetManager) {
// 优先使用传入的 services否则回退到全局 Core.services
// Prefer passed services, fallback to global Core.services
const services = this._services || Core.services;
if (!services) {
throw new Error('ServiceContainer is not available. Ensure Core.create() was called.');
}
this.btAssetManager = services.resolve(BehaviorTreeAssetManager);
}
return this.btAssetManager;
}
/**
* 获取行为树数据
* Get behavior tree data from AssetManager or BehaviorTreeAssetManager
*
* 优先从 AssetManager 获取(新方式),如果没有再从 BehaviorTreeAssetManager 获取(兼容旧方式)
*/
private getTreeData(assetGuid: string): BehaviorTreeData | undefined {
// 1. 优先从 AssetManager 获取(如果已加载)
// First try AssetManager (preferred way)
if (this._assetManager) {
// 使用 getAsset 通过 GUID 获取,而不是 getAssetByPath
// Use getAsset with GUID instead of getAssetByPath
const cachedAsset = this._assetManager.getAsset<IBehaviorTreeAssetContent>(assetGuid);
if (cachedAsset?.data) {
return cachedAsset.data;
}
}
// 2. 回退到 BehaviorTreeAssetManager兼容旧方式
// Fallback to BehaviorTreeAssetManager (legacy support)
return this.getBTAssetManager().getAsset(assetGuid);
}
/**
* 注册所有执行器(包括内置和插件提供的)
*/
private registerBuiltInExecutors(): void {
const constructors = NodeMetadataRegistry.getAllExecutorConstructors();
for (const [implementationType, ExecutorClass] of constructors) {
try {
const instance = new ExecutorClass();
this.executorRegistry.register(implementationType, instance);
} catch (error) {
this.logger.error(`注册执行器失败: ${implementationType}`, error);
}
}
}
/**
* 获取执行器注册表
*/
getExecutorRegistry(): NodeExecutorRegistry {
return this.executorRegistry;
}
protected override process(entities: readonly Entity[]): void {
for (const entity of entities) {
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent)!;
if (!runtime.isRunning) {
continue;
}
const treeData = this.getTreeData(runtime.treeAssetId);
if (!treeData) {
// 只警告一次,避免每帧重复输出
// Only warn once to avoid repeated output every frame
if (!this._warnedMissingAssets.has(runtime.treeAssetId)) {
this._warnedMissingAssets.add(runtime.treeAssetId);
this.logger.warn(`未找到行为树资产: ${runtime.treeAssetId}`);
}
continue;
}
// 如果标记了需要重置,先重置状态
if (runtime.needsReset) {
runtime.resetAllStates();
runtime.needsReset = false;
}
// 初始化黑板变量(如果行为树定义了默认值)
// Initialize blackboard variables from tree definition
if (treeData.blackboardVariables && treeData.blackboardVariables.size > 0) {
runtime.initializeBlackboard(treeData.blackboardVariables);
}
this.executeTree(entity, runtime, treeData);
}
}
/**
* 执行整个行为树
*/
private executeTree(
entity: Entity,
runtime: BehaviorTreeRuntimeComponent,
treeData: BehaviorTreeData
): void {
const rootNode = treeData.nodes.get(treeData.rootNodeId);
if (!rootNode) {
this.logger.error(`未找到根节点: ${treeData.rootNodeId}`);
return;
}
const status = this.executeNode(entity, runtime, rootNode, treeData);
// 如果树完成了标记在下一个tick时重置状态
// 这样UI可以看到节点的最终状态
if (status !== TaskStatus.Running) {
runtime.needsReset = true;
} else {
runtime.needsReset = false;
}
}
/**
* 执行单个节点
*/
private executeNode(
entity: Entity,
runtime: BehaviorTreeRuntimeComponent,
nodeData: BehaviorNodeData,
treeData: BehaviorTreeData
): TaskStatus {
const state = runtime.getNodeState(nodeData.id);
if (runtime.shouldAbort(nodeData.id)) {
runtime.clearAbortRequest(nodeData.id);
state.isAborted = true;
const executor = this.executorRegistry.get(nodeData.implementationType);
if (executor && executor.reset) {
const context = this.createContext(entity, runtime, nodeData, treeData);
executor.reset(context);
}
runtime.activeNodeIds.delete(nodeData.id);
state.status = TaskStatus.Failure;
return TaskStatus.Failure;
}
runtime.activeNodeIds.add(nodeData.id);
state.isAborted = false;
if (state.executionOrder === undefined) {
runtime.executionOrderCounter++;
state.executionOrder = runtime.executionOrderCounter;
}
const executor = this.executorRegistry.get(nodeData.implementationType);
if (!executor) {
this.logger.error(`未找到执行器: ${nodeData.implementationType}`);
state.status = TaskStatus.Failure;
return TaskStatus.Failure;
}
const context = this.createContext(entity, runtime, nodeData, treeData);
try {
const status = executor.execute(context);
state.status = status;
if (status !== TaskStatus.Running) {
runtime.activeNodeIds.delete(nodeData.id);
if (executor.reset) {
executor.reset(context);
}
}
return status;
} catch (error) {
this.logger.error(`执行节点时发生错误: ${nodeData.name}`, error);
state.status = TaskStatus.Failure;
runtime.activeNodeIds.delete(nodeData.id);
return TaskStatus.Failure;
}
}
/**
* 创建执行上下文
*/
private createContext(
entity: Entity,
runtime: BehaviorTreeRuntimeComponent,
nodeData: BehaviorNodeData,
treeData: BehaviorTreeData
): NodeExecutionContext {
return {
entity,
nodeData,
state: runtime.getNodeState(nodeData.id),
runtime,
treeData,
deltaTime: Time.deltaTime,
totalTime: Time.totalTime,
executeChild: (childId: string) => {
const childData = treeData.nodes.get(childId);
if (!childData) {
this.logger.warn(`未找到子节点: ${childId}`);
return TaskStatus.Failure;
}
return this.executeNode(entity, runtime, childData, treeData);
}
};
}
/**
* 执行子节点列表
*/
executeChildren(
context: NodeExecutionContext,
childIndices?: number[]
): TaskStatus[] {
const { nodeData, treeData, entity, runtime } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return [];
}
const results: TaskStatus[] = [];
const indicesToExecute = childIndices ||
Array.from({ length: nodeData.children.length }, (_, i) => i);
for (const index of indicesToExecute) {
if (index >= nodeData.children.length) {
continue;
}
const childId = nodeData.children[index]!;
const childData = treeData.nodes.get(childId);
if (!childData) {
this.logger.warn(`未找到子节点: ${childId}`);
results.push(TaskStatus.Failure);
continue;
}
const status = this.executeNode(entity, runtime, childData, treeData);
results.push(status);
}
return results;
}
}

View File

@@ -0,0 +1,278 @@
import { Component, ECSComponent, Property } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { NodeRuntimeState, createDefaultRuntimeState } from './BehaviorTreeData';
import { TaskStatus } from '../Types/TaskStatus';
/**
* 黑板变化监听器
*/
export type BlackboardChangeListener = (key: string, newValue: any, oldValue: any) => void;
/**
* 黑板观察者信息
*/
interface BlackboardObserver {
nodeId: string;
keys: Set<string>;
callback: BlackboardChangeListener;
}
/**
* 行为树运行时组件
*
* 挂载到游戏Entity上引用共享的BehaviorTreeData
* 维护该Entity独立的运行时状态
*/
@ECSComponent('BehaviorTreeRuntime')
@Serializable({ version: 1 })
export class BehaviorTreeRuntimeComponent extends Component {
/**
* 引用的行为树资产ID可序列化
*/
@Serialize()
@Property({ type: 'asset', label: 'Behavior Tree', extensions: ['.btree'] })
treeAssetId: string = '';
/**
* 是否自动启动
*/
@Serialize()
@Property({ type: 'boolean', label: 'Auto Start' })
autoStart: boolean = true;
/**
* 是否正在运行
*/
@IgnoreSerialization()
isRunning: boolean = false;
/**
* 节点运行时状态(每个节点独立)
* 不序列化,每次加载时重新初始化
*/
@IgnoreSerialization()
private nodeStates: Map<string, NodeRuntimeState> = new Map();
/**
* 黑板数据该Entity独立的数据
* 不序列化,通过初始化设置
*/
@IgnoreSerialization()
private blackboard: Map<string, any> = new Map();
/**
* 黑板观察者列表
*/
@IgnoreSerialization()
private blackboardObservers: Map<string, BlackboardObserver[]> = new Map();
/**
* 当前激活的节点ID列表用于调试
*/
@IgnoreSerialization()
activeNodeIds: Set<string> = new Set();
/**
* 标记是否需要在下一个tick重置状态
*/
@IgnoreSerialization()
needsReset: boolean = false;
/**
* 需要中止的节点ID列表
*/
@IgnoreSerialization()
nodesToAbort: Set<string> = new Set();
/**
* 执行顺序计数器(用于调试和可视化)
*/
@IgnoreSerialization()
executionOrderCounter: number = 0;
/**
* 获取节点运行时状态
*/
getNodeState(nodeId: string): NodeRuntimeState {
if (!this.nodeStates.has(nodeId)) {
this.nodeStates.set(nodeId, createDefaultRuntimeState());
}
return this.nodeStates.get(nodeId)!;
}
/**
* 重置节点状态
*/
resetNodeState(nodeId: string): void {
const state = this.getNodeState(nodeId);
state.status = TaskStatus.Invalid;
state.currentChildIndex = 0;
delete state.startTime;
delete state.lastExecutionTime;
delete state.repeatCount;
delete state.cachedResult;
delete state.shuffledIndices;
delete state.isAborted;
delete state.lastConditionResult;
delete state.observedKeys;
}
/**
* 重置所有节点状态
*/
resetAllStates(): void {
this.nodeStates.clear();
this.activeNodeIds.clear();
this.executionOrderCounter = 0;
}
/**
* 获取黑板值
*/
getBlackboardValue<T = any>(key: string): T | undefined {
return this.blackboard.get(key) as T;
}
/**
* 设置黑板值
*/
setBlackboardValue(key: string, value: any): void {
const oldValue = this.blackboard.get(key);
this.blackboard.set(key, value);
if (oldValue !== value) {
this.notifyBlackboardChange(key, value, oldValue);
}
}
/**
* 检查黑板是否有某个键
*/
hasBlackboardKey(key: string): boolean {
return this.blackboard.has(key);
}
/**
* 初始化黑板(从树定义的默认值)
*/
initializeBlackboard(variables?: Map<string, any>): void {
if (variables) {
variables.forEach((value, key) => {
if (!this.blackboard.has(key)) {
this.blackboard.set(key, value);
}
});
}
}
/**
* 清空黑板
*/
clearBlackboard(): void {
this.blackboard.clear();
}
/**
* 启动行为树
*/
start(): void {
this.isRunning = true;
this.resetAllStates();
}
/**
* 停止行为树
*/
stop(): void {
this.isRunning = false;
this.activeNodeIds.clear();
}
/**
* 暂停行为树
*/
pause(): void {
this.isRunning = false;
}
/**
* 恢复行为树
*/
resume(): void {
this.isRunning = true;
}
/**
* 注册黑板观察者
*/
observeBlackboard(nodeId: string, keys: string[], callback: BlackboardChangeListener): void {
const observer: BlackboardObserver = {
nodeId,
keys: new Set(keys),
callback
};
for (const key of keys) {
if (!this.blackboardObservers.has(key)) {
this.blackboardObservers.set(key, []);
}
this.blackboardObservers.get(key)!.push(observer);
}
}
/**
* 取消注册黑板观察者
*/
unobserveBlackboard(nodeId: string): void {
for (const observers of this.blackboardObservers.values()) {
const index = observers.findIndex((o) => o.nodeId === nodeId);
if (index !== -1) {
observers.splice(index, 1);
}
}
}
/**
* 通知黑板变化
*/
private notifyBlackboardChange(key: string, newValue: any, oldValue: any): void {
const observers = this.blackboardObservers.get(key);
if (!observers) return;
for (const observer of observers) {
try {
observer.callback(key, newValue, oldValue);
} catch (error) {
console.error(`黑板观察者回调错误 (节点: ${observer.nodeId}):`, error);
}
}
}
/**
* 请求中止节点
*/
requestAbort(nodeId: string): void {
this.nodesToAbort.add(nodeId);
}
/**
* 检查节点是否需要中止
*/
shouldAbort(nodeId: string): boolean {
return this.nodesToAbort.has(nodeId);
}
/**
* 清除中止请求
*/
clearAbortRequest(nodeId: string): void {
this.nodesToAbort.delete(nodeId);
}
/**
* 清除所有中止请求
*/
clearAllAbortRequests(): void {
this.nodesToAbort.clear();
}
}

View File

@@ -0,0 +1,40 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 总是失败装饰器执行器
*
* 无论子节点结果如何都返回失败
*/
@NodeExecutorMetadata({
implementationType: 'AlwaysFail',
nodeType: NodeType.Decorator,
displayName: '总是失败',
description: '无论子节点结果如何都返回失败',
category: 'Decorator'
})
export class AlwaysFailExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
const childId = nodeData.children[0]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
return TaskStatus.Failure;
}
reset(context: NodeExecutionContext): void {
if (context.nodeData.children && context.nodeData.children.length > 0) {
context.runtime.resetNodeState(context.nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,40 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 总是成功装饰器执行器
*
* 无论子节点结果如何都返回成功
*/
@NodeExecutorMetadata({
implementationType: 'AlwaysSucceed',
nodeType: NodeType.Decorator,
displayName: '总是成功',
description: '无论子节点结果如何都返回成功',
category: 'Decorator'
})
export class AlwaysSucceedExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Success;
}
const childId = nodeData.children[0]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
return TaskStatus.Success;
}
reset(context: NodeExecutionContext): void {
if (context.nodeData.children && context.nodeData.children.length > 0) {
context.runtime.resetNodeState(context.nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,73 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 黑板比较条件执行器
*
* 比较黑板中的值
*/
@NodeExecutorMetadata({
implementationType: 'BlackboardCompare',
nodeType: NodeType.Condition,
displayName: '黑板比较',
description: '比较黑板中的值',
category: 'Condition',
configSchema: {
key: {
type: 'string',
default: '',
description: '黑板变量名'
},
compareValue: {
type: 'object',
description: '比较值',
supportBinding: true
},
operator: {
type: 'string',
default: 'equals',
description: '比较运算符',
options: ['equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterOrEqual', 'lessOrEqual']
}
}
})
export class BlackboardCompare implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { runtime } = context;
const key = BindingHelper.getValue<string>(context, 'key', '');
const compareValue = BindingHelper.getValue(context, 'compareValue');
const operator = BindingHelper.getValue<string>(context, 'operator', 'equals');
if (!key) {
return TaskStatus.Failure;
}
const actualValue = runtime.getBlackboardValue(key);
if (this.compare(actualValue, compareValue, operator)) {
return TaskStatus.Success;
}
return TaskStatus.Failure;
}
private compare(actualValue: any, compareValue: any, operator: string): boolean {
switch (operator) {
case 'equals':
return actualValue === compareValue;
case 'notEquals':
return actualValue !== compareValue;
case 'greaterThan':
return actualValue > compareValue;
case 'lessThan':
return actualValue < compareValue;
case 'greaterOrEqual':
return actualValue >= compareValue;
case 'lessOrEqual':
return actualValue <= compareValue;
default:
return false;
}
}
}

View File

@@ -0,0 +1,51 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 黑板存在检查条件执行器
*
* 检查黑板中是否存在指定的键
*/
@NodeExecutorMetadata({
implementationType: 'BlackboardExists',
nodeType: NodeType.Condition,
displayName: '黑板存在',
description: '检查黑板中是否存在指定的键',
category: 'Condition',
configSchema: {
key: {
type: 'string',
default: '',
description: '黑板变量名'
},
checkNull: {
type: 'boolean',
default: false,
description: '检查是否为null'
}
}
})
export class BlackboardExists implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { runtime } = context;
const key = BindingHelper.getValue<string>(context, 'key', '');
const checkNull = BindingHelper.getValue<boolean>(context, 'checkNull', false);
if (!key) {
return TaskStatus.Failure;
}
const value = runtime.getBlackboardValue(key);
if (value === undefined) {
return TaskStatus.Failure;
}
if (checkNull && value === null) {
return TaskStatus.Failure;
}
return TaskStatus.Success;
}
}

View File

@@ -0,0 +1,182 @@
import { TaskStatus, NodeType, AbortType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 条件装饰器执行器
*
* 根据条件决定是否执行子节点
* 支持动态优先级和中止机制
*/
@NodeExecutorMetadata({
implementationType: 'Conditional',
nodeType: NodeType.Decorator,
displayName: '条件',
description: '根据条件决定是否执行子节点',
category: 'Decorator',
configSchema: {
blackboardKey: {
type: 'string',
default: '',
description: '黑板变量名'
},
expectedValue: {
type: 'object',
description: '期望值',
supportBinding: true
},
operator: {
type: 'string',
default: 'equals',
description: '比较运算符',
options: ['equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterOrEqual', 'lessOrEqual']
},
abortType: {
type: 'string',
default: 'none',
description: '中止类型',
options: ['none', 'self', 'lower-priority', 'both']
}
}
})
export class ConditionalExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, runtime, state } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
const blackboardKey = BindingHelper.getValue<string>(context, 'blackboardKey', '');
const expectedValue = BindingHelper.getValue(context, 'expectedValue');
const operator = BindingHelper.getValue<string>(context, 'operator', 'equals');
const abortType = (nodeData.abortType || AbortType.None) as AbortType;
if (!blackboardKey) {
return TaskStatus.Failure;
}
const actualValue = runtime.getBlackboardValue(blackboardKey);
const conditionMet = this.evaluateCondition(actualValue, expectedValue, operator);
const wasRunning = state.status === TaskStatus.Running;
if (abortType !== AbortType.None) {
if (!state.observedKeys || state.observedKeys.length === 0) {
state.observedKeys = [blackboardKey];
this.setupObserver(context, blackboardKey, expectedValue, operator, abortType);
}
if (state.lastConditionResult !== undefined && state.lastConditionResult !== conditionMet) {
if (conditionMet) {
this.handleConditionBecameTrue(context, abortType);
} else if (wasRunning) {
this.handleConditionBecameFalse(context, abortType);
}
}
}
state.lastConditionResult = conditionMet;
if (!conditionMet) {
return TaskStatus.Failure;
}
const childId = nodeData.children[0]!;
const status = context.executeChild(childId);
return status;
}
private evaluateCondition(actualValue: any, expectedValue: any, operator: string): boolean {
switch (operator) {
case 'equals':
return actualValue === expectedValue;
case 'notEquals':
return actualValue !== expectedValue;
case 'greaterThan':
return actualValue > expectedValue;
case 'lessThan':
return actualValue < expectedValue;
case 'greaterOrEqual':
return actualValue >= expectedValue;
case 'lessOrEqual':
return actualValue <= expectedValue;
default:
return false;
}
}
/**
* 设置黑板观察者
*/
private setupObserver(
context: NodeExecutionContext,
blackboardKey: string,
expectedValue: any,
operator: string,
abortType: AbortType
): void {
const { nodeData, runtime } = context;
runtime.observeBlackboard(nodeData.id, [blackboardKey], (_key, newValue) => {
const conditionMet = this.evaluateCondition(newValue, expectedValue, operator);
const lastResult = context.state.lastConditionResult;
if (lastResult !== undefined && lastResult !== conditionMet) {
if (conditionMet) {
this.handleConditionBecameTrue(context, abortType);
} else {
this.handleConditionBecameFalse(context, abortType);
}
}
context.state.lastConditionResult = conditionMet;
});
}
/**
* 处理条件变为true
*/
private handleConditionBecameTrue(context: NodeExecutionContext, abortType: AbortType): void {
if (abortType === AbortType.LowerPriority || abortType === AbortType.Both) {
this.requestAbortLowerPriority(context);
}
}
/**
* 处理条件变为false
*/
private handleConditionBecameFalse(context: NodeExecutionContext, abortType: AbortType): void {
const { nodeData, runtime } = context;
if (abortType === AbortType.Self || abortType === AbortType.Both) {
if (nodeData.children && nodeData.children.length > 0) {
runtime.requestAbort(nodeData.children[0]!);
}
}
}
/**
* 请求中止低优先级节点
*/
private requestAbortLowerPriority(context: NodeExecutionContext): void {
const { runtime } = context;
runtime.requestAbort('__lower_priority__');
}
reset(context: NodeExecutionContext): void {
const { nodeData, runtime, state } = context;
if (state.observedKeys && state.observedKeys.length > 0) {
runtime.unobserveBlackboard(nodeData.id);
delete state.observedKeys;
}
delete state.lastConditionResult;
if (nodeData.children && nodeData.children.length > 0) {
runtime.resetNodeState(nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,64 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 冷却装饰器执行器
*
* 子节点执行成功后进入冷却时间
*/
@NodeExecutorMetadata({
implementationType: 'Cooldown',
nodeType: NodeType.Decorator,
displayName: '冷却',
description: '子节点执行成功后进入冷却时间',
category: 'Decorator',
configSchema: {
cooldownTime: {
type: 'number',
default: 1.0,
description: '冷却时间(秒)',
min: 0,
supportBinding: true
}
}
})
export class CooldownExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, state, totalTime } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
const cooldownTime = BindingHelper.getValue<number>(context, 'cooldownTime', 1.0);
if (state.lastExecutionTime !== undefined) {
const timeSinceLastExecution = totalTime - state.lastExecutionTime;
if (timeSinceLastExecution < cooldownTime) {
return TaskStatus.Failure;
}
}
const childId = nodeData.children[0]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Success) {
state.lastExecutionTime = totalTime;
return TaskStatus.Success;
}
return TaskStatus.Failure;
}
reset(context: NodeExecutionContext): void {
delete context.state.lastExecutionTime;
if (context.nodeData.children && context.nodeData.children.length > 0) {
context.runtime.resetNodeState(context.nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,46 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 执行动作执行器
*
* 执行自定义动作逻辑
*/
@NodeExecutorMetadata({
implementationType: 'ExecuteAction',
nodeType: NodeType.Action,
displayName: '执行动作',
description: '执行自定义动作逻辑',
category: 'Action',
configSchema: {
actionName: {
type: 'string',
default: '',
description: '动作名称黑板中action_前缀的函数'
}
}
})
export class ExecuteAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { runtime, entity } = context;
const actionName = BindingHelper.getValue<string>(context, 'actionName', '');
if (!actionName) {
return TaskStatus.Failure;
}
const actionFunction = runtime.getBlackboardValue<(entity: NodeExecutionContext['entity']) => TaskStatus>(`action_${actionName}`);
if (!actionFunction || typeof actionFunction !== 'function') {
return TaskStatus.Failure;
}
try {
return actionFunction(entity);
} catch (error) {
console.error(`ExecuteAction failed: ${error}`);
return TaskStatus.Failure;
}
}
}

View File

@@ -0,0 +1,46 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 执行条件执行器
*
* 执行自定义条件逻辑
*/
@NodeExecutorMetadata({
implementationType: 'ExecuteCondition',
nodeType: NodeType.Condition,
displayName: '执行条件',
description: '执行自定义条件逻辑',
category: 'Condition',
configSchema: {
conditionName: {
type: 'string',
default: '',
description: '条件名称黑板中condition_前缀的函数'
}
}
})
export class ExecuteCondition implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { runtime, entity } = context;
const conditionName = BindingHelper.getValue<string>(context, 'conditionName', '');
if (!conditionName) {
return TaskStatus.Failure;
}
const conditionFunction = runtime.getBlackboardValue<(entity: NodeExecutionContext['entity']) => boolean>(`condition_${conditionName}`);
if (!conditionFunction || typeof conditionFunction !== 'function') {
return TaskStatus.Failure;
}
try {
return conditionFunction(entity) ? TaskStatus.Success : TaskStatus.Failure;
} catch (error) {
console.error(`ExecuteCondition failed: ${error}`);
return TaskStatus.Failure;
}
}
}

View File

@@ -0,0 +1,52 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 反转装饰器执行器
*
* 反转子节点的执行结果
*/
@NodeExecutorMetadata({
implementationType: 'Inverter',
nodeType: NodeType.Decorator,
displayName: '反转',
description: '反转子节点的执行结果',
category: 'Decorator',
childrenConstraints: {
min: 1,
max: 1
}
})
export class InverterExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
const childId = nodeData.children[0]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Success) {
return TaskStatus.Failure;
}
if (status === TaskStatus.Failure) {
return TaskStatus.Success;
}
return TaskStatus.Failure;
}
reset(context: NodeExecutionContext): void {
if (context.nodeData.children && context.nodeData.children.length > 0) {
context.runtime.resetNodeState(context.nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,71 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 日志动作执行器
*
* 输出日志信息
*/
@NodeExecutorMetadata({
implementationType: 'Log',
nodeType: NodeType.Action,
displayName: '日志',
description: '输出日志信息',
category: 'Action',
configSchema: {
message: {
type: 'string',
default: '',
description: '日志消息,支持{key}占位符引用黑板变量',
supportBinding: true
},
logLevel: {
type: 'string',
default: 'info',
description: '日志级别',
options: ['info', 'warn', 'error']
}
}
})
export class LogAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { runtime } = context;
const message = BindingHelper.getValue<string>(context, 'message', '');
const logLevel = BindingHelper.getValue<string>(context, 'logLevel', 'info');
const finalMessage = this.replaceBlackboardVariables(message, runtime);
this.log(finalMessage, logLevel);
return TaskStatus.Success;
}
private replaceBlackboardVariables(message: string, runtime: NodeExecutionContext['runtime']): string {
if (!message.includes('{') || !message.includes('}')) {
return message;
}
// 使用限制长度的正则表达式避免 ReDoS 攻击
// 限制占位符名称最多100个字符只允许字母、数字、下划线和点号
return message.replace(/\{([\w.]{1,100})\}/g, (_, key) => {
const value = runtime.getBlackboardValue(key.trim());
return value !== undefined ? String(value) : `{${key}}`;
});
}
private log(message: string, level: string): void {
switch (level) {
case 'error':
console.error(message);
break;
case 'warn':
console.warn(message);
break;
case 'info':
default:
console.log(message);
break;
}
}
}

View File

@@ -0,0 +1,74 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 修改黑板值动作执行器
*
* 对黑板中的数值进行运算
*/
@NodeExecutorMetadata({
implementationType: 'ModifyBlackboardValue',
nodeType: NodeType.Action,
displayName: '修改黑板值',
description: '对黑板中的数值进行运算',
category: 'Action',
configSchema: {
key: {
type: 'string',
default: '',
description: '黑板变量名'
},
operation: {
type: 'string',
default: 'add',
description: '运算类型',
options: ['add', 'subtract', 'multiply', 'divide', 'set']
},
value: {
type: 'number',
default: 0,
description: '操作数',
supportBinding: true
}
}
})
export class ModifyBlackboardValue implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { runtime } = context;
const key = BindingHelper.getValue<string>(context, 'key', '');
const operation = BindingHelper.getValue<string>(context, 'operation', 'add');
const value = BindingHelper.getValue<number>(context, 'value', 0);
if (!key) {
return TaskStatus.Failure;
}
const currentValue = runtime.getBlackboardValue<number>(key) || 0;
let newValue: number;
switch (operation) {
case 'add':
newValue = currentValue + value;
break;
case 'subtract':
newValue = currentValue - value;
break;
case 'multiply':
newValue = currentValue * value;
break;
case 'divide':
newValue = value !== 0 ? currentValue / value : currentValue;
break;
case 'set':
newValue = value;
break;
default:
return TaskStatus.Failure;
}
runtime.setBlackboardValue(key, newValue);
return TaskStatus.Success;
}
}

View File

@@ -0,0 +1,99 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 并行节点执行器
*
* 同时执行所有子节点
*/
@NodeExecutorMetadata({
implementationType: 'Parallel',
nodeType: NodeType.Composite,
displayName: '并行',
description: '同时执行所有子节点',
category: 'Composite',
configSchema: {
successPolicy: {
type: 'string',
default: 'all',
description: '成功策略',
options: ['all', 'one']
},
failurePolicy: {
type: 'string',
default: 'one',
description: '失败策略',
options: ['all', 'one']
}
},
childrenConstraints: {
min: 2
}
})
export class ParallelExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData } = context;
const successPolicy = BindingHelper.getValue<string>(context, 'successPolicy', 'all');
const failurePolicy = BindingHelper.getValue<string>(context, 'failurePolicy', 'one');
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Success;
}
let hasRunning = false;
let successCount = 0;
let failureCount = 0;
for (const childId of nodeData.children) {
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
hasRunning = true;
} else if (status === TaskStatus.Success) {
successCount++;
} else if (status === TaskStatus.Failure) {
failureCount++;
}
}
if (successPolicy === 'one' && successCount > 0) {
this.stopAllChildren(context);
return TaskStatus.Success;
}
if (successPolicy === 'all' && successCount === nodeData.children.length) {
return TaskStatus.Success;
}
if (failurePolicy === 'one' && failureCount > 0) {
this.stopAllChildren(context);
return TaskStatus.Failure;
}
if (failurePolicy === 'all' && failureCount === nodeData.children.length) {
return TaskStatus.Failure;
}
return hasRunning ? TaskStatus.Running : TaskStatus.Success;
}
private stopAllChildren(context: NodeExecutionContext): void {
const { nodeData, runtime } = context;
if (!nodeData.children) return;
for (const childId of nodeData.children) {
runtime.activeNodeIds.delete(childId);
runtime.resetNodeState(childId);
}
}
reset(context: NodeExecutionContext): void {
const { nodeData, runtime } = context;
if (!nodeData.children) return;
for (const childId of nodeData.children) {
runtime.resetNodeState(childId);
}
}
}

View File

@@ -0,0 +1,85 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 并行选择器执行器
*
* 并行执行子节点,任一成功则成功
*/
@NodeExecutorMetadata({
implementationType: 'ParallelSelector',
nodeType: NodeType.Composite,
displayName: '并行选择器',
description: '并行执行子节点,任一成功则成功',
category: 'Composite',
configSchema: {
failurePolicy: {
type: 'string',
default: 'all',
description: '失败策略',
options: ['all', 'one']
}
}
})
export class ParallelSelectorExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData } = context;
const failurePolicy = BindingHelper.getValue<string>(context, 'failurePolicy', 'all');
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
let hasRunning = false;
let successCount = 0;
let failureCount = 0;
for (const childId of nodeData.children) {
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
hasRunning = true;
} else if (status === TaskStatus.Success) {
successCount++;
} else if (status === TaskStatus.Failure) {
failureCount++;
}
}
if (successCount > 0) {
this.stopAllChildren(context);
return TaskStatus.Success;
}
if (failurePolicy === 'one' && failureCount > 0) {
this.stopAllChildren(context);
return TaskStatus.Failure;
}
if (failurePolicy === 'all' && failureCount === nodeData.children.length) {
return TaskStatus.Failure;
}
return hasRunning ? TaskStatus.Running : TaskStatus.Failure;
}
private stopAllChildren(context: NodeExecutionContext): void {
const { nodeData, runtime } = context;
if (!nodeData.children) return;
for (const childId of nodeData.children) {
runtime.activeNodeIds.delete(childId);
runtime.resetNodeState(childId);
}
}
reset(context: NodeExecutionContext): void {
const { nodeData, runtime } = context;
if (!nodeData.children) return;
for (const childId of nodeData.children) {
runtime.resetNodeState(childId);
}
}
}

View File

@@ -0,0 +1,39 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 随机概率条件执行器
*
* 根据概率返回成功或失败
*/
@NodeExecutorMetadata({
implementationType: 'RandomProbability',
nodeType: NodeType.Condition,
displayName: '随机概率',
description: '根据概率返回成功或失败',
category: 'Condition',
configSchema: {
probability: {
type: 'number',
default: 0.5,
description: '成功概率0-1',
min: 0,
max: 1,
supportBinding: true
}
}
})
export class RandomProbability implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const probability = BindingHelper.getValue<number>(context, 'probability', 0.5);
const clampedProbability = Math.max(0, Math.min(1, probability));
if (Math.random() < clampedProbability) {
return TaskStatus.Success;
}
return TaskStatus.Failure;
}
}

View File

@@ -0,0 +1,67 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 随机选择器执行器
*
* 随机顺序执行子节点,任一成功则成功
*/
@NodeExecutorMetadata({
implementationType: 'RandomSelector',
nodeType: NodeType.Composite,
displayName: '随机选择器',
description: '随机顺序执行子节点,任一成功则成功',
category: 'Composite'
})
export class RandomSelectorExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, state } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
if (!state.shuffledIndices || state.shuffledIndices.length === 0) {
state.shuffledIndices = this.shuffleIndices(nodeData.children.length);
}
while (state.currentChildIndex < state.shuffledIndices.length) {
const shuffledIndex = state.shuffledIndices[state.currentChildIndex]!;
const childId = nodeData.children[shuffledIndex]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Success) {
state.currentChildIndex = 0;
delete state.shuffledIndices;
return TaskStatus.Success;
}
state.currentChildIndex++;
}
state.currentChildIndex = 0;
delete state.shuffledIndices;
return TaskStatus.Failure;
}
private shuffleIndices(length: number): number[] {
const indices = Array.from({ length }, (_, i) => i);
for (let i = indices.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = indices[i]!;
indices[i] = indices[j]!;
indices[j] = temp;
}
return indices;
}
reset(context: NodeExecutionContext): void {
context.state.currentChildIndex = 0;
delete context.state.shuffledIndices;
}
}

View File

@@ -0,0 +1,70 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 随机序列执行器
*
* 随机顺序执行子节点序列,全部成功才成功
*/
@NodeExecutorMetadata({
implementationType: 'RandomSequence',
nodeType: NodeType.Composite,
displayName: '随机序列',
description: '随机顺序执行子节点,全部成功才成功',
category: 'Composite',
childrenConstraints: {
min: 1
}
})
export class RandomSequenceExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, state } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Success;
}
if (!state.shuffledIndices || state.shuffledIndices.length === 0) {
state.shuffledIndices = this.shuffleIndices(nodeData.children.length);
}
while (state.currentChildIndex < state.shuffledIndices.length) {
const shuffledIndex = state.shuffledIndices[state.currentChildIndex]!;
const childId = nodeData.children[shuffledIndex]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Failure) {
state.currentChildIndex = 0;
delete state.shuffledIndices;
return TaskStatus.Failure;
}
state.currentChildIndex++;
}
state.currentChildIndex = 0;
delete state.shuffledIndices;
return TaskStatus.Success;
}
private shuffleIndices(length: number): number[] {
const indices = Array.from({ length }, (_, i) => i);
for (let i = indices.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = indices[i]!;
indices[i] = indices[j]!;
indices[j] = temp;
}
return indices;
}
reset(context: NodeExecutionContext): void {
context.state.currentChildIndex = 0;
delete context.state.shuffledIndices;
}
}

View File

@@ -0,0 +1,80 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 重复装饰器执行器
*
* 重复执行子节点指定次数
*/
@NodeExecutorMetadata({
implementationType: 'Repeater',
nodeType: NodeType.Decorator,
displayName: '重复',
description: '重复执行子节点指定次数',
category: 'Decorator',
configSchema: {
repeatCount: {
type: 'number',
default: 1,
description: '重复次数(-1表示无限循环',
supportBinding: true
},
endOnFailure: {
type: 'boolean',
default: false,
description: '子节点失败时是否结束'
}
},
childrenConstraints: {
min: 1,
max: 1
}
})
export class RepeaterExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, state, runtime } = context;
const repeatCount = BindingHelper.getValue<number>(context, 'repeatCount', 1);
const endOnFailure = BindingHelper.getValue<boolean>(context, 'endOnFailure', false);
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Success;
}
const childId = nodeData.children[0]!;
if (!state.repeatCount) {
state.repeatCount = 0;
}
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Failure && endOnFailure) {
state.repeatCount = 0;
return TaskStatus.Failure;
}
state.repeatCount++;
runtime.resetNodeState(childId);
const shouldContinue = (repeatCount === -1) || (state.repeatCount < repeatCount);
if (shouldContinue) {
return TaskStatus.Running;
} else {
state.repeatCount = 0;
return TaskStatus.Success;
}
}
reset(context: NodeExecutionContext): void {
delete context.state.repeatCount;
if (context.nodeData.children && context.nodeData.children.length > 0) {
context.runtime.resetNodeState(context.nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,37 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 根节点执行器
*
* 行为树的入口节点,执行其唯一的子节点
*/
@NodeExecutorMetadata({
implementationType: 'Root',
nodeType: NodeType.Root,
displayName: '根节点',
description: '行为树的入口节点',
category: 'Root',
childrenConstraints: {
min: 1,
max: 1
}
})
export class RootExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData } = context;
// 根节点必须有且仅有一个子节点
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
const childId = nodeData.children[0]!;
return context.executeChild(childId);
}
reset(_context: NodeExecutionContext): void {
// 根节点没有需要重置的状态
}
}

View File

@@ -0,0 +1,51 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 选择器节点执行器
*
* 按顺序执行子节点,任一成功则成功,全部失败才失败
*/
@NodeExecutorMetadata({
implementationType: 'Selector',
nodeType: NodeType.Composite,
displayName: '选择器',
description: '按顺序执行子节点,任一成功则成功',
category: 'Composite',
childrenConstraints: {
min: 1
}
})
export class SelectorExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, state } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
while (state.currentChildIndex < nodeData.children.length) {
const childId = nodeData.children[state.currentChildIndex]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Success) {
state.currentChildIndex = 0;
return TaskStatus.Success;
}
state.currentChildIndex++;
}
state.currentChildIndex = 0;
return TaskStatus.Failure;
}
reset(context: NodeExecutionContext): void {
context.state.currentChildIndex = 0;
}
}

View File

@@ -0,0 +1,51 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 序列节点执行器
*
* 按顺序执行子节点,全部成功才成功,任一失败则失败
*/
@NodeExecutorMetadata({
implementationType: 'Sequence',
nodeType: NodeType.Composite,
displayName: '序列',
description: '按顺序执行子节点,全部成功才成功',
category: 'Composite',
childrenConstraints: {
min: 1
}
})
export class SequenceExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, state } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Success;
}
while (state.currentChildIndex < nodeData.children.length) {
const childId = nodeData.children[state.currentChildIndex]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Failure) {
state.currentChildIndex = 0;
return TaskStatus.Failure;
}
state.currentChildIndex++;
}
state.currentChildIndex = 0;
return TaskStatus.Success;
}
reset(context: NodeExecutionContext): void {
context.state.currentChildIndex = 0;
}
}

View File

@@ -0,0 +1,144 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* Service执行接口
*/
export interface IServiceExecutor {
/**
* Service开始执行
*/
onServiceStart?(context: NodeExecutionContext): void;
/**
* Service每帧更新
*/
onServiceTick(context: NodeExecutionContext): void;
/**
* Service结束执行
*/
onServiceEnd?(context: NodeExecutionContext): void;
}
/**
* Service注册表
*/
class ServiceRegistry {
private static services: Map<string, IServiceExecutor> = new Map();
static register(name: string, service: IServiceExecutor): void {
this.services.set(name, service);
}
static get(name: string): IServiceExecutor | undefined {
return this.services.get(name);
}
static has(name: string): boolean {
return this.services.has(name);
}
static unregister(name: string): boolean {
return this.services.delete(name);
}
}
/**
* Service装饰器执行器
*
* 在子节点执行期间持续运行后台逻辑
*/
@NodeExecutorMetadata({
implementationType: 'Service',
nodeType: NodeType.Decorator,
displayName: 'Service',
description: '在子节点执行期间持续运行后台逻辑',
category: 'Decorator',
configSchema: {
serviceName: {
type: 'string',
default: '',
description: 'Service名称'
},
tickInterval: {
type: 'number',
default: 0,
description: 'Service更新间隔0表示每帧更新',
supportBinding: true
}
}
})
export class ServiceDecorator implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, state, totalTime } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
const serviceName = BindingHelper.getValue<string>(context, 'serviceName', '');
const tickInterval = BindingHelper.getValue<number>(context, 'tickInterval', 0);
if (!serviceName) {
return TaskStatus.Failure;
}
const service = ServiceRegistry.get(serviceName);
if (!service) {
console.warn(`未找到Service: ${serviceName}`);
return TaskStatus.Failure;
}
if (state.status !== TaskStatus.Running) {
state.startTime = totalTime;
state.lastExecutionTime = totalTime;
if (service.onServiceStart) {
service.onServiceStart(context);
}
}
const shouldTick = tickInterval === 0 ||
(state.lastExecutionTime !== undefined &&
(totalTime - state.lastExecutionTime) >= tickInterval);
if (shouldTick) {
service.onServiceTick(context);
state.lastExecutionTime = totalTime;
}
const childId = nodeData.children[0]!;
const childStatus = context.executeChild(childId);
if (childStatus !== TaskStatus.Running) {
if (service.onServiceEnd) {
service.onServiceEnd(context);
}
}
return childStatus;
}
reset(context: NodeExecutionContext): void {
const { nodeData, runtime, state } = context;
const serviceName = BindingHelper.getValue<string>(context, 'serviceName', '');
if (serviceName) {
const service = ServiceRegistry.get(serviceName);
if (service && service.onServiceEnd) {
service.onServiceEnd(context);
}
}
delete state.startTime;
delete state.lastExecutionTime;
if (nodeData.children && nodeData.children.length > 0) {
runtime.resetNodeState(nodeData.children[0]!);
}
}
}
export { ServiceRegistry };

View File

@@ -0,0 +1,43 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 设置黑板值动作执行器
*
* 设置黑板中的变量值
*/
@NodeExecutorMetadata({
implementationType: 'SetBlackboardValue',
nodeType: NodeType.Action,
displayName: '设置黑板值',
description: '设置黑板中的变量值',
category: 'Action',
configSchema: {
key: {
type: 'string',
default: '',
description: '黑板变量名'
},
value: {
type: 'object',
description: '要设置的值',
supportBinding: true
}
}
})
export class SetBlackboardValue implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { runtime } = context;
const key = BindingHelper.getValue<string>(context, 'key', '');
const value = BindingHelper.getValue(context, 'value');
if (!key) {
return TaskStatus.Failure;
}
runtime.setBlackboardValue(key, value);
return TaskStatus.Success;
}
}

View File

@@ -0,0 +1,161 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
import { BehaviorTreeAssetManager } from '../BehaviorTreeAssetManager';
import { Core } from '@esengine/ecs-framework';
/**
* SubTree执行器
*
* 引用并执行其他行为树,实现模块化和复用
*/
@NodeExecutorMetadata({
implementationType: 'SubTree',
nodeType: NodeType.Action,
displayName: '子树',
description: '引用并执行其他行为树',
category: 'Special',
configSchema: {
treeAssetId: {
type: 'string',
default: '',
description: '要执行的行为树资产ID',
supportBinding: true
},
shareBlackboard: {
type: 'boolean',
default: true,
description: '是否共享黑板数据'
}
}
})
export class SubTreeExecutor implements INodeExecutor {
private assetManager: BehaviorTreeAssetManager | null = null;
private getAssetManager(): BehaviorTreeAssetManager {
if (!this.assetManager) {
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
}
return this.assetManager;
}
execute(context: NodeExecutionContext): TaskStatus {
const { runtime, state, entity } = context;
const treeAssetId = BindingHelper.getValue<string>(context, 'treeAssetId', '');
const shareBlackboard = BindingHelper.getValue<boolean>(context, 'shareBlackboard', true);
if (!treeAssetId) {
return TaskStatus.Failure;
}
const assetManager = this.getAssetManager();
const subTreeData = assetManager.getAsset(treeAssetId);
if (!subTreeData) {
console.warn(`未找到子树资产: ${treeAssetId}`);
return TaskStatus.Failure;
}
const rootNode = subTreeData.nodes.get(subTreeData.rootNodeId);
if (!rootNode) {
console.warn(`子树根节点未找到: ${subTreeData.rootNodeId}`);
return TaskStatus.Failure;
}
if (!shareBlackboard && state.status !== TaskStatus.Running) {
if (subTreeData.blackboardVariables) {
for (const [key, value] of subTreeData.blackboardVariables.entries()) {
if (!runtime.hasBlackboardKey(key)) {
runtime.setBlackboardValue(key, value);
}
}
}
}
const subTreeContext: NodeExecutionContext = {
entity,
nodeData: rootNode,
state: runtime.getNodeState(rootNode.id),
runtime,
treeData: subTreeData,
deltaTime: context.deltaTime,
totalTime: context.totalTime,
executeChild: (childId: string) => {
const childData = subTreeData.nodes.get(childId);
if (!childData) {
console.warn(`子树节点未找到: ${childId}`);
return TaskStatus.Failure;
}
const childContext: NodeExecutionContext = {
entity,
nodeData: childData,
state: runtime.getNodeState(childId),
runtime,
treeData: subTreeData,
deltaTime: context.deltaTime,
totalTime: context.totalTime,
executeChild: subTreeContext.executeChild
};
return this.executeSubTreeNode(childContext);
}
};
return this.executeSubTreeNode(subTreeContext);
}
private executeSubTreeNode(context: NodeExecutionContext): TaskStatus {
const { nodeData, runtime } = context;
const state = runtime.getNodeState(nodeData.id);
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Success;
}
const childId = nodeData.children[state.currentChildIndex]!;
const childStatus = context.executeChild(childId);
if (childStatus === TaskStatus.Running) {
return TaskStatus.Running;
}
if (childStatus === TaskStatus.Failure) {
state.currentChildIndex = 0;
return TaskStatus.Failure;
}
state.currentChildIndex++;
if (state.currentChildIndex >= nodeData.children.length) {
state.currentChildIndex = 0;
return TaskStatus.Success;
}
return TaskStatus.Running;
}
reset(context: NodeExecutionContext): void {
const treeAssetId = BindingHelper.getValue<string>(context, 'treeAssetId', '');
if (treeAssetId) {
const assetManager = this.getAssetManager();
const subTreeData = assetManager.getAsset(treeAssetId);
if (subTreeData) {
const rootNode = subTreeData.nodes.get(subTreeData.rootNodeId);
if (rootNode) {
context.runtime.resetNodeState(rootNode.id);
if (rootNode.children) {
for (const childId of rootNode.children) {
context.runtime.resetNodeState(childId);
}
}
}
}
}
}
}

View File

@@ -0,0 +1,63 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 超时装饰器执行器
*
* 限制子节点的执行时间
*/
@NodeExecutorMetadata({
implementationType: 'Timeout',
nodeType: NodeType.Decorator,
displayName: '超时',
description: '限制子节点的执行时间',
category: 'Decorator',
configSchema: {
timeout: {
type: 'number',
default: 1.0,
description: '超时时间(秒)',
min: 0,
supportBinding: true
}
}
})
export class TimeoutExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, state, totalTime } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
const timeout = BindingHelper.getValue<number>(context, 'timeout', 1.0);
if (state.startTime === undefined) {
state.startTime = totalTime;
}
const elapsedTime = totalTime - state.startTime;
if (elapsedTime >= timeout) {
delete state.startTime;
return TaskStatus.Failure;
}
const childId = nodeData.children[0]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
delete state.startTime;
return status;
}
reset(context: NodeExecutionContext): void {
delete context.state.startTime;
if (context.nodeData.children && context.nodeData.children.length > 0) {
context.runtime.resetNodeState(context.nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,45 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 直到失败装饰器执行器
*
* 重复执行子节点直到失败
*/
@NodeExecutorMetadata({
implementationType: 'UntilFail',
nodeType: NodeType.Decorator,
displayName: '直到失败',
description: '重复执行子节点直到失败',
category: 'Decorator'
})
export class UntilFailExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, runtime } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Success;
}
const childId = nodeData.children[0]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Failure) {
return TaskStatus.Failure;
}
runtime.resetNodeState(childId);
return TaskStatus.Running;
}
reset(context: NodeExecutionContext): void {
if (context.nodeData.children && context.nodeData.children.length > 0) {
context.runtime.resetNodeState(context.nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,45 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 直到成功装饰器执行器
*
* 重复执行子节点直到成功
*/
@NodeExecutorMetadata({
implementationType: 'UntilSuccess',
nodeType: NodeType.Decorator,
displayName: '直到成功',
description: '重复执行子节点直到成功',
category: 'Decorator'
})
export class UntilSuccessExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, runtime } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
const childId = nodeData.children[0]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Success) {
return TaskStatus.Success;
}
runtime.resetNodeState(childId);
return TaskStatus.Running;
}
reset(context: NodeExecutionContext): void {
if (context.nodeData.children && context.nodeData.children.length > 0) {
context.runtime.resetNodeState(context.nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,46 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 等待动作执行器
*
* 等待指定时间后返回成功
*/
@NodeExecutorMetadata({
implementationType: 'Wait',
nodeType: NodeType.Action,
displayName: '等待',
description: '等待指定时间后返回成功',
category: 'Action',
configSchema: {
duration: {
type: 'number',
default: 1.0,
description: '等待时长(秒)',
min: 0,
supportBinding: true
}
}
})
export class WaitAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { state, totalTime } = context;
const duration = BindingHelper.getValue<number>(context, 'duration', 1.0);
if (!state.startTime) {
state.startTime = totalTime;
return TaskStatus.Running;
}
if (totalTime - state.startTime >= duration) {
return TaskStatus.Success;
}
return TaskStatus.Running;
}
reset(context: NodeExecutionContext): void {
delete context.state.startTime;
}
}

View File

@@ -0,0 +1,29 @@
import { TaskStatus } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
/**
* 等待动作执行器
*
* 等待指定时间后返回成功
*/
export class WaitActionExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { state, nodeData, totalTime } = context;
const duration = nodeData.config['duration'] as number || 1.0;
if (!state.startTime) {
state.startTime = totalTime;
return TaskStatus.Running;
}
if (totalTime - state.startTime >= duration) {
return TaskStatus.Success;
}
return TaskStatus.Running;
}
reset(context: NodeExecutionContext): void {
delete context.state.startTime;
}
}

View File

@@ -0,0 +1,31 @@
export { RootExecutor } from './RootExecutor';
export { SequenceExecutor } from './SequenceExecutor';
export { SelectorExecutor } from './SelectorExecutor';
export { ParallelExecutor } from './ParallelExecutor';
export { ParallelSelectorExecutor } from './ParallelSelectorExecutor';
export { RandomSequenceExecutor } from './RandomSequenceExecutor';
export { RandomSelectorExecutor } from './RandomSelectorExecutor';
export { InverterExecutor } from './InverterExecutor';
export { RepeaterExecutor } from './RepeaterExecutor';
export { AlwaysSucceedExecutor } from './AlwaysSucceedExecutor';
export { AlwaysFailExecutor } from './AlwaysFailExecutor';
export { UntilSuccessExecutor } from './UntilSuccessExecutor';
export { UntilFailExecutor } from './UntilFailExecutor';
export { ConditionalExecutor } from './ConditionalExecutor';
export { CooldownExecutor } from './CooldownExecutor';
export { TimeoutExecutor } from './TimeoutExecutor';
export { ServiceDecorator, ServiceRegistry } from './ServiceDecorator';
export type { IServiceExecutor } from './ServiceDecorator';
export { WaitAction } from './WaitAction';
export { LogAction } from './LogAction';
export { SetBlackboardValue } from './SetBlackboardValue';
export { ModifyBlackboardValue } from './ModifyBlackboardValue';
export { ExecuteAction } from './ExecuteAction';
export { SubTreeExecutor } from './SubTreeExecutor';
export { BlackboardCompare } from './BlackboardCompare';
export { BlackboardExists } from './BlackboardExists';
export { RandomProbability } from './RandomProbability';
export { ExecuteCondition } from './ExecuteCondition';

View File

@@ -0,0 +1,181 @@
import { Entity } from '@esengine/ecs-framework';
import { TaskStatus } from '../Types/TaskStatus';
import { BehaviorNodeData, BehaviorTreeData, NodeRuntimeState } from './BehaviorTreeData';
import { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
/**
* 节点执行上下文
*
* 包含执行节点所需的所有信息
*/
export interface NodeExecutionContext {
/** 游戏Entity行为树宿主 */
readonly entity: Entity;
/** 节点数据 */
readonly nodeData: BehaviorNodeData;
/** 节点运行时状态 */
readonly state: NodeRuntimeState;
/** 运行时组件(访问黑板等) */
readonly runtime: BehaviorTreeRuntimeComponent;
/** 行为树数据(访问子节点等) */
readonly treeData: BehaviorTreeData;
/** 当前帧增量时间 */
readonly deltaTime: number;
/** 总时间 */
readonly totalTime: number;
/** 执行子节点 */
executeChild(childId: string): TaskStatus;
}
/**
* 节点执行器接口
*
* 所有节点类型都需要实现对应的执行器
* 执行器是无状态的状态存储在NodeRuntimeState中
*/
export interface INodeExecutor {
/**
* 执行节点逻辑
*
* @param context 执行上下文
* @returns 执行结果状态
*/
execute(context: NodeExecutionContext): TaskStatus;
/**
* 重置节点状态(可选)
*
* 当节点完成或被中断时调用
*/
reset?(context: NodeExecutionContext): void;
}
/**
* 复合节点执行结果
*/
export interface CompositeExecutionResult {
/** 节点状态 */
status: TaskStatus;
/** 要激活的子节点索引列表undefined表示激活所有 */
activateChildren?: number[];
/** 是否停止所有子节点 */
stopAllChildren?: boolean;
}
/**
* 复合节点执行器接口
*/
export interface ICompositeExecutor extends INodeExecutor {
/**
* 执行复合节点逻辑
*
* @param context 执行上下文
* @returns 复合节点执行结果
*/
executeComposite(context: NodeExecutionContext): CompositeExecutionResult;
}
/**
* 绑定辅助工具
*
* 处理配置属性的黑板绑定
*/
export class BindingHelper {
/**
* 获取配置值(考虑黑板绑定)
*
* @param context 执行上下文
* @param configKey 配置键名
* @param defaultValue 默认值
* @returns 解析后的值
*/
static getValue<T = any>(
context: NodeExecutionContext,
configKey: string,
defaultValue?: T
): T {
const { nodeData, runtime } = context;
if (nodeData.bindings && nodeData.bindings[configKey]) {
const blackboardKey = nodeData.bindings[configKey];
const boundValue = runtime.getBlackboardValue<T>(blackboardKey);
return boundValue !== undefined ? boundValue : (defaultValue as T);
}
const configValue = nodeData.config[configKey];
return configValue !== undefined ? configValue : (defaultValue as T);
}
/**
* 检查配置是否绑定到黑板变量
*/
static hasBinding(context: NodeExecutionContext, configKey: string): boolean {
return !!(context.nodeData.bindings && context.nodeData.bindings[configKey]);
}
/**
* 获取绑定的黑板变量名
*/
static getBindingKey(context: NodeExecutionContext, configKey: string): string | undefined {
return context.nodeData.bindings?.[configKey];
}
}
/**
* 节点执行器注册表
*
* 管理所有节点类型的执行器
*/
export class NodeExecutorRegistry {
private executors: Map<string, INodeExecutor> = new Map();
/**
* 注册执行器
*
* @param implementationType 节点实现类型对应BehaviorNodeData.implementationType
* @param executor 执行器实例
*/
register(implementationType: string, executor: INodeExecutor): void {
if (this.executors.has(implementationType)) {
console.warn(`执行器已存在,将被覆盖: ${implementationType}`);
}
this.executors.set(implementationType, executor);
}
/**
* 获取执行器
*/
get(implementationType: string): INodeExecutor | undefined {
return this.executors.get(implementationType);
}
/**
* 检查是否有执行器
*/
has(implementationType: string): boolean {
return this.executors.has(implementationType);
}
/**
* 注销执行器
*/
unregister(implementationType: string): boolean {
return this.executors.delete(implementationType);
}
/**
* 清空所有执行器
*/
clear(): void {
this.executors.clear();
}
}

View File

@@ -0,0 +1,108 @@
import { NodeType } from '../Types/TaskStatus';
/**
* 配置参数定义
*/
export interface ConfigFieldDefinition {
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
default?: any;
description?: string;
min?: number;
max?: number;
options?: string[];
supportBinding?: boolean;
allowMultipleConnections?: boolean;
}
/**
* 子节点约束配置
*/
export interface ChildrenConstraints {
min?: number;
max?: number;
required?: boolean;
}
/**
* 节点元数据
*/
export interface NodeMetadata {
implementationType: string;
nodeType: NodeType;
displayName: string;
description?: string;
category?: string;
configSchema?: Record<string, ConfigFieldDefinition>;
childrenConstraints?: ChildrenConstraints;
}
/**
* 节点元数据默认值
*/
export class NodeMetadataDefaults {
static getDefaultConstraints(nodeType: NodeType): ChildrenConstraints | undefined {
switch (nodeType) {
case NodeType.Composite:
return { min: 1 };
case NodeType.Decorator:
return { min: 1, max: 1 };
case NodeType.Action:
case NodeType.Condition:
return { max: 0 };
default:
return undefined;
}
}
}
/**
* 节点元数据注册表
*/
export class NodeMetadataRegistry {
private static metadataMap: Map<string, NodeMetadata> = new Map();
private static executorClassMap: Map<Function, string> = new Map();
private static executorConstructors: Map<string, new () => any> = new Map();
static register(target: Function, metadata: NodeMetadata): void {
this.metadataMap.set(metadata.implementationType, metadata);
this.executorClassMap.set(target, metadata.implementationType);
this.executorConstructors.set(metadata.implementationType, target as new () => any);
}
static getMetadata(implementationType: string): NodeMetadata | undefined {
return this.metadataMap.get(implementationType);
}
static getAllMetadata(): NodeMetadata[] {
return Array.from(this.metadataMap.values());
}
static getByCategory(category: string): NodeMetadata[] {
return this.getAllMetadata().filter((m) => m.category === category);
}
static getByNodeType(nodeType: NodeType): NodeMetadata[] {
return this.getAllMetadata().filter((m) => m.nodeType === nodeType);
}
static getImplementationType(executorClass: Function): string | undefined {
return this.executorClassMap.get(executorClass);
}
static getExecutorConstructor(implementationType: string): (new () => any) | undefined {
return this.executorConstructors.get(implementationType);
}
static getAllExecutorConstructors(): Map<string, new () => any> {
return new Map(this.executorConstructors);
}
}
/**
* 节点执行器元数据装饰器
*/
export function NodeExecutorMetadata(metadata: NodeMetadata) {
return function (target: Function) {
NodeMetadataRegistry.register(target, metadata);
};
}

View File

@@ -0,0 +1,11 @@
export type { BehaviorTreeData, BehaviorNodeData, NodeRuntimeState } from './BehaviorTreeData';
export { createDefaultRuntimeState } from './BehaviorTreeData';
export { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
export { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager';
export type { INodeExecutor, NodeExecutionContext } from './NodeExecutor';
export { NodeExecutorRegistry, BindingHelper } from './NodeExecutor';
export { BehaviorTreeExecutionSystem } from './BehaviorTreeExecutionSystem';
export type { NodeMetadata, ConfigFieldDefinition, NodeExecutorMetadata } from './NodeMetadata';
export { NodeMetadataRegistry } from './NodeMetadata';
export * from './Executors';

View File

@@ -0,0 +1,67 @@
/**
* @esengine/behavior-tree
*
* @zh AI 行为树系统,支持运行时执行和可视化编辑
* @en AI Behavior Tree System with runtime execution and visual editor support
*
* @zh 此包是通用的行为树实现,可以与任何 ECS 框架配合使用。
* 对于 ESEngine 集成,请从 '@esengine/behavior-tree/esengine' 导入插件。
*
* @en This package is a generic behavior tree implementation that works with any ECS framework.
* For ESEngine integration, import the plugin from '@esengine/behavior-tree/esengine'.
*
* @example Cocos/Laya/通用 ECS 使用方式:
* ```typescript
* import {
* BehaviorTreeAssetManager,
* BehaviorTreeExecutionSystem,
* BehaviorTreeRuntimeComponent
* } from '@esengine/behavior-tree';
*
* // 1. Register service
* Core.services.registerSingleton(BehaviorTreeAssetManager);
*
* // 2. Load behavior tree from JSON
* const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
* assetManager.loadFromEditorJSON(jsonContent);
*
* // 3. Add component to entity
* entity.addComponent(new BehaviorTreeRuntimeComponent());
*
* // 4. Add system to scene
* scene.addSystem(new BehaviorTreeExecutionSystem());
* ```
*
* @packageDocumentation
*/
// Constants
export { BehaviorTreeAssetType } from './constants';
// Types
export * from './Types/TaskStatus';
export type { IBTAssetManager, IBehaviorTreeAssetContent } from './Types/AssetManagerInterface';
// Execution (runtime core)
export * from './execution';
// Utilities
export * from './BehaviorTreeStarter';
export * from './BehaviorTreeBuilder';
// Serialization
export * from './Serialization/NodeTemplates';
export * from './Serialization/BehaviorTreeAsset';
export * from './Serialization/EditorFormatConverter';
export * from './Serialization/BehaviorTreeAssetSerializer';
export * from './Serialization/EditorToBehaviorTreeDataConverter';
// Services
export * from './Services/GlobalBlackboardService';
// Blackboard types (excluding BlackboardValueType which is already exported from TaskStatus)
export type { BlackboardTypeDefinition } from './Blackboard/BlackboardTypes';
export { BlackboardTypes } from './Blackboard/BlackboardTypes';
// Service tokens (using ecs-framework's createServiceToken, not engine-core)
export { BehaviorTreeSystemToken } from './tokens';

View File

@@ -0,0 +1,17 @@
/**
* 行为树模块服务令牌
* Behavior tree module service tokens
*/
import { createServiceToken } from '@esengine/ecs-framework';
import type { BehaviorTreeExecutionSystem } from './execution/BehaviorTreeExecutionSystem';
// ============================================================================
// 行为树模块导出的令牌 | Tokens exported by behavior tree module
// ============================================================================
/**
* 行为树执行系统令牌
* Behavior tree execution system token
*/
export const BehaviorTreeSystemToken = createServiceToken<BehaviorTreeExecutionSystem>('behaviorTreeSystem');