refactor(behavior-tree)!: 迁移到 Runtime 执行器架构 (#196)

* refactor(behavior-tree)!: 迁移到 Runtime 执行器架构

* fix(behavior-tree): 修复LogAction中的ReDoS安全漏洞

* feat(behavior-tree): 完善行为树核心功能并修复类型错误
This commit is contained in:
YHH
2025-10-31 17:27:38 +08:00
committed by GitHub
parent c58e3411fd
commit 61813e67b6
113 changed files with 7795 additions and 10564 deletions

View File

@@ -22,6 +22,14 @@ export interface BlackboardVariableDefinition {
description?: string;
}
/**
* 行为树节点配置数据
*/
export interface BehaviorNodeConfigData {
className?: string;
[key: string]: any;
}
/**
* 行为树节点数据(运行时格式)
*/
@@ -31,7 +39,7 @@ export interface BehaviorTreeNodeData {
nodeType: NodeType;
// 节点类型特定数据
data: Record<string, any>;
data: BehaviorNodeConfigData;
// 子节点ID列表
children: string[];
@@ -216,11 +224,19 @@ export class BehaviorTreeAssetValidator {
}
}
return {
valid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
warnings: warnings.length > 0 ? warnings : undefined
const result: AssetValidationResult = {
valid: errors.length === 0
};
if (errors.length > 0) {
result.errors = errors;
}
if (warnings.length > 0) {
result.warnings = warnings;
}
return result;
}
/**

View File

@@ -1,422 +0,0 @@
import { Entity, IScene, createLogger, ComponentRegistry, Component } from '@esengine/ecs-framework';
import type { BehaviorTreeAsset, BehaviorTreeNodeData, BlackboardVariableDefinition, PropertyBinding } from './BehaviorTreeAsset';
import { BehaviorTreeNode } from '../Components/BehaviorTreeNode';
import { BlackboardComponent } from '../Components/BlackboardComponent';
import { PropertyBindings } from '../Components/PropertyBindings';
import { NodeType } from '../Types/TaskStatus';
// 导入所有节点组件
import { RootNode } from '../Components/Composites/RootNode';
import { SequenceNode } from '../Components/Composites/SequenceNode';
import { SelectorNode } from '../Components/Composites/SelectorNode';
import { ParallelNode } from '../Components/Composites/ParallelNode';
import { ParallelSelectorNode } from '../Components/Composites/ParallelSelectorNode';
import { RandomSequenceNode } from '../Components/Composites/RandomSequenceNode';
import { RandomSelectorNode } from '../Components/Composites/RandomSelectorNode';
import { InverterNode } from '../Components/Decorators/InverterNode';
import { RepeaterNode } from '../Components/Decorators/RepeaterNode';
import { UntilSuccessNode } from '../Components/Decorators/UntilSuccessNode';
import { UntilFailNode } from '../Components/Decorators/UntilFailNode';
import { AlwaysSucceedNode } from '../Components/Decorators/AlwaysSucceedNode';
import { AlwaysFailNode } from '../Components/Decorators/AlwaysFailNode';
import { ConditionalNode } from '../Components/Decorators/ConditionalNode';
import { CooldownNode } from '../Components/Decorators/CooldownNode';
import { TimeoutNode } from '../Components/Decorators/TimeoutNode';
import { WaitAction } from '../Components/Actions/WaitAction';
import { LogAction } from '../Components/Actions/LogAction';
import { SetBlackboardValueAction } from '../Components/Actions/SetBlackboardValueAction';
import { ModifyBlackboardValueAction } from '../Components/Actions/ModifyBlackboardValueAction';
import { ExecuteAction } from '../Components/Actions/ExecuteAction';
import { BlackboardCompareCondition, CompareOperator } from '../Components/Conditions/BlackboardCompareCondition';
import { BlackboardExistsCondition } from '../Components/Conditions/BlackboardExistsCondition';
import { RandomProbabilityCondition } from '../Components/Conditions/RandomProbabilityCondition';
import { ExecuteCondition } from '../Components/Conditions/ExecuteCondition';
import { AbortType } from '../Types/TaskStatus';
const logger = createLogger('BehaviorTreeAssetLoader');
/**
* 实例化选项
*/
export interface InstantiateOptions {
/**
* 实体名称前缀
*/
namePrefix?: string;
/**
* 是否共享黑板如果为true将使用全局黑板服务
*/
sharedBlackboard?: boolean;
/**
* 黑板变量覆盖(用于运行时动态设置初始值)
*/
blackboardOverrides?: Record<string, any>;
/**
* 是否作为子树实例化
* 如果为 true根节点不会添加 RootNode 组件,避免触发预加载逻辑
*/
asSubTree?: boolean;
}
/**
* 行为树资产加载器
*
* 将BehaviorTreeAsset实例化为可运行的Entity树
*/
export class BehaviorTreeAssetLoader {
/**
* 从资产实例化行为树
*
* @param asset 行为树资产
* @param scene 目标场景
* @param options 实例化选项
* @returns 根实体
*
* @example
* ```typescript
* const asset = await loadAssetFromFile('enemy-ai.btree.bin');
* const aiRoot = BehaviorTreeAssetLoader.instantiate(asset, scene);
* BehaviorTreeStarter.start(aiRoot);
* ```
*/
static instantiate(
asset: BehaviorTreeAsset,
scene: IScene,
options: InstantiateOptions = {}
): Entity {
logger.info(`开始实例化行为树: ${asset.metadata.name}`);
// 创建节点映射
const nodeMap = new Map<string, BehaviorTreeNodeData>();
for (const node of asset.nodes) {
nodeMap.set(node.id, node);
}
// 查找根节点
const rootNodeData = nodeMap.get(asset.rootNodeId);
if (!rootNodeData) {
throw new Error(`未找到根节点: ${asset.rootNodeId}`);
}
// 创建实体映射
const entityMap = new Map<string, Entity>();
// 递归创建实体树
const rootEntity = this.createEntityTree(
rootNodeData,
nodeMap,
entityMap,
scene,
options.namePrefix,
options.asSubTree
);
// 添加黑板
this.setupBlackboard(rootEntity, asset.blackboard, options.blackboardOverrides);
// 设置属性绑定
if (asset.propertyBindings && asset.propertyBindings.length > 0) {
this.setupPropertyBindings(asset.propertyBindings, entityMap);
}
logger.info(`行为树实例化完成: ${asset.nodes.length} 个节点`);
return rootEntity;
}
/**
* 递归创建实体树
*/
private static createEntityTree(
nodeData: BehaviorTreeNodeData,
nodeMap: Map<string, BehaviorTreeNodeData>,
entityMap: Map<string, Entity>,
scene: IScene,
namePrefix?: string,
asSubTree?: boolean,
isRootOfSubTree: boolean = true
): Entity {
const entityName = namePrefix ? `${namePrefix}_${nodeData.name}` : nodeData.name;
const entity = scene.createEntity(entityName);
// 记录实体
entityMap.set(nodeData.id, entity);
// 添加BehaviorTreeNode组件
const btNode = entity.addComponent(new BehaviorTreeNode());
btNode.nodeType = nodeData.nodeType;
btNode.nodeName = nodeData.name;
// 添加节点特定组件(如果是子树的根节点,跳过 RootNode
this.addNodeComponents(entity, nodeData, asSubTree && isRootOfSubTree);
// 递归创建子节点
for (const childId of nodeData.children) {
const childData = nodeMap.get(childId);
if (!childData) {
logger.warn(`子节点未找到: ${childId}`);
continue;
}
const childEntity = this.createEntityTree(
childData,
nodeMap,
entityMap,
scene,
namePrefix,
asSubTree,
false // 子节点不是根节点
);
entity.addChild(childEntity);
}
return entity;
}
/**
* 添加节点特定组件
* @param skipRootNode 是否跳过添加 RootNode 组件(用于子树)
*/
private static addNodeComponents(entity: Entity, nodeData: BehaviorTreeNodeData, skipRootNode: boolean = false): void {
const { nodeType, data, name } = nodeData;
logger.debug(`addNodeComponents: name=${name}, data.nodeType=${data.nodeType}, skipRootNode=${skipRootNode}`);
// 根据节点类型和名称添加对应组件
if (data.nodeType === 'root' || name === '根节点' || name === 'Root') {
if (!skipRootNode) {
logger.debug(`添加 RootNode 组件: ${name}`);
entity.addComponent(new RootNode());
} else {
// 子树的根节点,使用第一个子节点的类型(通常是 SequenceNode
logger.debug(`跳过为子树根节点添加 RootNode: ${name}`);
// 添加一个默认的 SequenceNode 作为子树的根
this.addCompositeComponent(entity, '序列', data);
}
}
// 组合节点
else if (nodeType === NodeType.Composite) {
this.addCompositeComponent(entity, name, data);
}
// 装饰器节点
else if (nodeType === NodeType.Decorator) {
this.addDecoratorComponent(entity, name, data);
}
// 动作节点
else if (nodeType === NodeType.Action) {
this.addActionComponent(entity, name, data);
}
// 条件节点
else if (nodeType === NodeType.Condition) {
this.addConditionComponent(entity, name, data);
}
}
/**
* 添加组合节点组件
*/
private static addCompositeComponent(entity: Entity, name: string, data: Record<string, any>): void {
const nameLower = name.toLowerCase();
if (nameLower.includes('sequence') || nameLower.includes('序列')) {
const node = entity.addComponent(new SequenceNode());
node.abortType = (data.abortType as AbortType) ?? AbortType.None;
} else if (nameLower.includes('selector') || nameLower.includes('选择')) {
const node = entity.addComponent(new SelectorNode());
node.abortType = (data.abortType as AbortType) ?? AbortType.None;
} else if (nameLower.includes('parallelselector') || nameLower.includes('并行选择')) {
const node = entity.addComponent(new ParallelSelectorNode());
node.failurePolicy = data.failurePolicy ?? 'one';
} else if (nameLower.includes('parallel') || nameLower.includes('并行')) {
const node = entity.addComponent(new ParallelNode());
node.successPolicy = data.successPolicy ?? 'all';
node.failurePolicy = data.failurePolicy ?? 'one';
} else if (nameLower.includes('randomsequence') || nameLower.includes('随机序列')) {
entity.addComponent(new RandomSequenceNode());
} else if (nameLower.includes('randomselector') || nameLower.includes('随机选择')) {
entity.addComponent(new RandomSelectorNode());
} else {
logger.warn(`未知的组合节点类型: ${name}`);
}
}
/**
* 添加装饰器组件
*/
private static addDecoratorComponent(entity: Entity, name: string, data: Record<string, any>): void {
const nameLower = name.toLowerCase();
if (nameLower.includes('inverter') || nameLower.includes('反转')) {
entity.addComponent(new InverterNode());
} else if (nameLower.includes('repeater') || nameLower.includes('重复')) {
const node = entity.addComponent(new RepeaterNode());
node.repeatCount = data.repeatCount ?? -1;
node.endOnFailure = data.endOnFailure ?? false;
} else if (nameLower.includes('untilsuccess') || nameLower.includes('直到成功')) {
entity.addComponent(new UntilSuccessNode());
} else if (nameLower.includes('untilfail') || nameLower.includes('直到失败')) {
entity.addComponent(new UntilFailNode());
} else if (nameLower.includes('alwayssucceed') || nameLower.includes('总是成功')) {
entity.addComponent(new AlwaysSucceedNode());
} else if (nameLower.includes('alwaysfail') || nameLower.includes('总是失败')) {
entity.addComponent(new AlwaysFailNode());
} else if (nameLower.includes('conditional') || nameLower.includes('条件装饰')) {
const node = entity.addComponent(new ConditionalNode());
node.conditionCode = data.conditionCode ?? '';
node.shouldReevaluate = data.shouldReevaluate ?? true;
} else if (nameLower.includes('cooldown') || nameLower.includes('冷却')) {
const node = entity.addComponent(new CooldownNode());
node.cooldownTime = data.cooldownTime ?? 1.0;
} else if (nameLower.includes('timeout') || nameLower.includes('超时')) {
const node = entity.addComponent(new TimeoutNode());
node.timeoutDuration = data.timeoutDuration ?? 1.0;
} else {
logger.warn(`未知的装饰器类型: ${name}`);
}
}
/**
* 添加动作组件
*/
private static addActionComponent(entity: Entity, name: string, data: Record<string, any>): void {
const nameLower = name.toLowerCase();
if (nameLower.includes('wait') || nameLower.includes('等待')) {
const action = entity.addComponent(new WaitAction());
action.waitTime = data.waitTime ?? 1.0;
} else if (nameLower.includes('log') || nameLower.includes('日志')) {
const action = entity.addComponent(new LogAction());
action.message = data.message ?? '';
action.level = data.level ?? 'log';
} else if (nameLower.includes('setblackboard') || nameLower.includes('setvalue') || nameLower.includes('设置变量')) {
const action = entity.addComponent(new SetBlackboardValueAction());
action.variableName = data.variableName ?? '';
action.value = data.value;
} else if (nameLower.includes('modifyblackboard') || nameLower.includes('modifyvalue') || nameLower.includes('修改变量')) {
const action = entity.addComponent(new ModifyBlackboardValueAction());
action.variableName = data.variableName ?? '';
action.operation = data.operation ?? 'add';
action.operand = data.operand ?? 0;
} else if (nameLower.includes('execute') || nameLower.includes('自定义')) {
const action = entity.addComponent(new ExecuteAction());
action.actionCode = data.actionCode ?? 'return TaskStatus.Success;';
} else if (data.className) {
const ComponentClass = ComponentRegistry.getComponentType(data.className);
if (ComponentClass) {
try {
const component = new (ComponentClass as any)();
Object.assign(component, data);
entity.addComponent(component as Component);
} catch (error) {
logger.error(`创建动作组件失败: ${data.className}, error: ${error}`);
}
} else {
logger.warn(`未找到动作组件类: ${data.className}`);
}
} else {
logger.warn(`未知的动作类型: ${name}`);
}
}
/**
* 添加条件组件
*/
private static addConditionComponent(entity: Entity, name: string, data: Record<string, any>): void {
const nameLower = name.toLowerCase();
if (nameLower.includes('compare') || nameLower.includes('比较变量')) {
const condition = entity.addComponent(new BlackboardCompareCondition());
condition.variableName = data.variableName ?? '';
condition.operator = (data.operator as CompareOperator) ?? CompareOperator.Equal;
condition.compareValue = data.compareValue;
condition.invertResult = data.invertResult ?? false;
} else if (nameLower.includes('exists') || nameLower.includes('变量存在')) {
const condition = entity.addComponent(new BlackboardExistsCondition());
condition.variableName = data.variableName ?? '';
condition.checkNotNull = data.checkNotNull ?? false;
condition.invertResult = data.invertResult ?? false;
} else if (nameLower.includes('random') || nameLower.includes('概率')) {
const condition = entity.addComponent(new RandomProbabilityCondition());
condition.probability = data.probability ?? 0.5;
} else if (nameLower.includes('execute') || nameLower.includes('执行条件')) {
const condition = entity.addComponent(new ExecuteCondition());
condition.conditionCode = data.conditionCode ?? '';
condition.invertResult = data.invertResult ?? false;
} else if (data.className) {
const ComponentClass = ComponentRegistry.getComponentType(data.className);
if (ComponentClass) {
try {
const component = new (ComponentClass as any)();
Object.assign(component, data);
entity.addComponent(component as Component);
} catch (error) {
logger.error(`创建条件组件失败: ${data.className}, error: ${error}`);
}
} else {
logger.warn(`未找到条件组件类: ${data.className}`);
}
} else {
logger.warn(`未知的条件类型: ${name}`);
}
}
/**
* 设置黑板
*/
private static setupBlackboard(
rootEntity: Entity,
blackboardDef: BlackboardVariableDefinition[],
overrides?: Record<string, any>
): void {
const blackboard = rootEntity.addComponent(new BlackboardComponent());
for (const variable of blackboardDef) {
const value = overrides && overrides[variable.name] !== undefined
? overrides[variable.name]
: variable.defaultValue;
blackboard.defineVariable(
variable.name,
variable.type,
value,
{
readonly: variable.readonly,
description: variable.description
}
);
}
logger.info(`已设置黑板: ${blackboardDef.length} 个变量`);
}
/**
* 设置属性绑定
*/
private static setupPropertyBindings(
bindings: PropertyBinding[],
entityMap: Map<string, Entity>
): void {
for (const binding of bindings) {
const entity = entityMap.get(binding.nodeId);
if (!entity) {
logger.warn(`属性绑定引用的节点不存在: ${binding.nodeId}`);
continue;
}
let propertyBindings = entity.getComponent(PropertyBindings);
if (!propertyBindings) {
propertyBindings = entity.addComponent(new PropertyBindings());
}
propertyBindings.addBinding(binding.propertyName, binding.variableName);
}
logger.info(`已设置属性绑定: ${bindings.length} 个绑定`);
}
}

View File

@@ -1,189 +0,0 @@
import { Entity, IScene, SceneSerializer, SerializedScene, SerializedEntity } from '@esengine/ecs-framework';
import { BehaviorTreeNode } from '../Components/BehaviorTreeNode';
/**
* 行为树持久化工具
*
* 使用框架的序列化系统进行二进制/JSON序列化
*/
export class BehaviorTreePersistence {
/**
* 序列化行为树JSON格式
*
* @param rootEntity 行为树根实体
* @param pretty 是否格式化
* @returns 序列化数据JSON字符串或二进制
*
* @example
* ```typescript
* const data = BehaviorTreePersistence.serialize(aiRoot);
* ```
*/
static serialize(rootEntity: Entity, pretty: boolean = true): string | Uint8Array {
if (!rootEntity.hasComponent(BehaviorTreeNode)) {
throw new Error('Entity must have BehaviorTreeNode component');
}
if (!rootEntity.scene) {
throw new Error('Entity must be attached to a scene');
}
// 使用 SceneSerializer但只序列化这棵行为树
// 创建一个临时场景包含只这个实体树
return SceneSerializer.serialize(rootEntity.scene, {
format: 'json',
pretty: pretty,
includeMetadata: true
});
}
/**
* 从序列化数据加载行为树
*
* @param scene 场景实例
* @param data 序列化数据JSON字符串或二进制
*
* @example
* ```typescript
* // 从文件读取
* const json = await readFile('behavior-tree.json');
*
* // 恢复行为树到场景
* BehaviorTreePersistence.deserialize(scene, json);
* ```
*/
static deserialize(scene: IScene, data: string | Uint8Array): void {
SceneSerializer.deserialize(scene, data, {
strategy: 'merge'
});
}
/**
* 序列化为 JSON 字符串
*
* @param rootEntity 行为树根实体
* @param pretty 是否格式化
* @returns JSON 字符串
*/
static toJSON(rootEntity: Entity, pretty: boolean = true): string {
const data = this.serialize(rootEntity, pretty);
return JSON.stringify(data, null, pretty ? 2 : 0);
}
/**
* 从 JSON 字符串加载
*
* @param scene 场景实例
* @param json JSON 字符串
*/
static fromJSON(scene: IScene, json: string): void {
this.deserialize(scene, json);
}
/**
* 保存到文件(需要 Tauri 环境)
*
* @param rootEntity 行为树根实体
* @param filePath 文件路径
*
* @example
* ```typescript
* await BehaviorTreePersistence.saveToFile(aiRoot, 'ai-behavior.json');
* ```
*/
static async saveToFile(rootEntity: Entity, filePath: string): Promise<void> {
const json = this.toJSON(rootEntity, true);
// 需要在 Tauri 环境中使用
// const { writeTextFile } = await import('@tauri-apps/api/fs');
// await writeTextFile(filePath, json);
throw new Error('saveToFile requires Tauri environment. Use toJSON() for manual saving.');
}
/**
* 从文件加载(需要 Tauri 环境)
*
* @param scene 场景实例
* @param filePath 文件路径
* @returns 恢复的根实体
*
* @example
* ```typescript
* const aiRoot = await BehaviorTreePersistence.loadFromFile(scene, 'ai-behavior.json');
* ```
*/
static async loadFromFile(scene: IScene, filePath: string): Promise<Entity> {
// 需要在 Tauri 环境中使用
// const { readTextFile } = await import('@tauri-apps/api/fs');
// const json = await readTextFile(filePath);
// return this.fromJSON(scene, json);
throw new Error('loadFromFile requires Tauri environment. Use fromJSON() for manual loading.');
}
/**
* 验证是否为有效的行为树数据
*
* @param data 序列化数据(字符串格式)
* @returns 是否有效
*/
static validate(data: string): boolean {
try {
const parsed = JSON.parse(data) as SerializedScene;
if (!parsed || typeof parsed !== 'object') {
return false;
}
// 检查必要字段
if (!parsed.name ||
typeof parsed.version !== 'number' ||
!Array.isArray(parsed.entities) ||
!Array.isArray(parsed.componentTypeRegistry)) {
return false;
}
// 检查是否至少有一个实体包含 BehaviorTreeNode 组件
const hasBehaviorTreeNode = parsed.entities.some((entity: SerializedEntity) => {
return entity.components.some(
(comp: any) => comp.type === 'BehaviorTreeNode'
);
});
return hasBehaviorTreeNode;
} catch {
return false;
}
}
/**
* 克隆行为树
*
* @param scene 场景实例
* @param rootEntity 要克隆的行为树根实体
* @returns 克隆的新实体
*
* @example
* ```typescript
* const clonedAI = BehaviorTreePersistence.clone(scene, originalAI);
* ```
*/
static clone(scene: IScene, rootEntity: Entity): Entity {
const data = this.serialize(rootEntity);
const entityCountBefore = scene.entities.count;
this.deserialize(scene, data);
// 找到新添加的根实体(最后添加的实体)
const entities = Array.from(scene.entities.buffer);
for (let i = entities.length - 1; i >= entityCountBefore; i--) {
const entity = entities[i];
if (entity.hasComponent(BehaviorTreeNode) && !entity.parent) {
return entity;
}
}
throw new Error('Failed to find cloned root entity');
}
}

View File

@@ -7,15 +7,26 @@ 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: {
displayName: string;
category: string;
type: NodeType;
[key: string]: any;
};
data: Record<string, any>;
template: EditorNodeTemplate;
data: EditorNodeData;
position: { x: number; y: number };
children: string[];
}
@@ -74,12 +85,24 @@ export class EditorFormatConverter {
const assetMetadata: AssetMetadata = {
name: metadata?.name || editorData.metadata?.name || 'Untitled Behavior Tree',
description: metadata?.description || editorData.metadata?.description,
version: metadata?.version || editorData.version || '1.0.0',
createdAt: metadata?.createdAt || editorData.metadata?.createdAt,
modifiedAt: metadata?.modifiedAt || new Date().toISOString()
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);
@@ -95,10 +118,13 @@ export class EditorFormatConverter {
metadata: assetMetadata,
rootNodeId: rootNode.id,
nodes,
blackboard,
propertyBindings: propertyBindings.length > 0 ? propertyBindings : undefined
blackboard
};
if (propertyBindings.length > 0) {
asset.propertyBindings = propertyBindings;
}
logger.info(`转换完成: ${nodes.length}个节点, ${blackboard.length}个黑板变量, ${propertyBindings.length}个属性绑定`);
return asset;
@@ -243,21 +269,31 @@ export class EditorFormatConverter {
}
const connections = this.convertPropertyBindingsToConnections(
asset.propertyBindings || [],
asset.nodes
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: {
name: asset.metadata.name,
description: asset.metadata.description,
createdAt: asset.metadata.createdAt,
modifiedAt: asset.metadata.modifiedAt
},
metadata,
nodes,
connections,
blackboard,
@@ -324,8 +360,7 @@ export class EditorFormatConverter {
* 将属性绑定转换为连接
*/
private static convertPropertyBindingsToConnections(
bindings: PropertyBinding[],
nodes: BehaviorTreeNodeData[]
bindings: PropertyBinding[]
): EditorConnection[] {
const connections: EditorConnection[] = [];

View File

@@ -1,5 +1,5 @@
import { NodeType } from '../Types/TaskStatus';
import { getRegisteredNodeTemplates } from '../Decorators/BehaviorNodeDecorator';
import { NodeMetadataRegistry, ConfigFieldDefinition } from '../Runtime/NodeMetadata';
/**
* 节点数据JSON格式
@@ -8,6 +8,8 @@ export interface NodeDataJSON {
nodeType: string;
compositeType?: string;
decoratorType?: string;
actionType?: string;
conditionType?: string;
[key: string]: any;
}
@@ -118,6 +120,12 @@ export interface PropertyDefinition {
/** 最大长度(字符串) */
maxLength?: number;
};
/**
* 是否允许多个连接
* 默认 false只允许一个黑板变量连接
*/
allowMultipleConnections?: boolean;
}
/**
@@ -138,16 +146,15 @@ export interface NodeTemplate {
}
/**
* 编辑器节点模板库
*
* 使用装饰器系统管理所有节点
* 节点模板库
*/
export class NodeTemplates {
/**
* 获取所有节点模板(通过装饰器注册)
* 获取所有节点模板
*/
static getAllTemplates(): NodeTemplate[] {
return getRegisteredNodeTemplates();
const allMetadata = NodeMetadataRegistry.getAllMetadata();
return allMetadata.map(metadata => this.convertMetadataToTemplate(metadata));
}
/**
@@ -172,4 +179,188 @@ export class NodeTemplates {
}
});
}
/**
* 将NodeMetadata转换为NodeTemplate
*/
private static convertMetadataToTemplate(metadata: any): 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 || '');
return {
type: metadata.nodeType,
displayName: metadata.displayName,
category: metadata.category || this.getCategoryByNodeType(metadata.nodeType),
description: metadata.description || '',
className: metadata.implementationType,
icon,
color,
defaultConfig,
properties
};
}
/**
* 将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): PropertyType {
if (field.options && field.options.length > 0) {
return PropertyType.Select;
}
switch (field.type) {
case 'string':
return PropertyType.String;
case 'number':
return PropertyType.Number;
case 'boolean':
return PropertyType.Boolean;
case 'array':
case 'object':
default:
return PropertyType.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' }; // 灰色
}
}
}