fix(behavior-tree): 修复插件节点执行问题并完善文档

This commit is contained in:
YHH
2025-10-28 11:45:35 +08:00
parent fe791e83a8
commit f0b4453a5f
28 changed files with 5475 additions and 127 deletions

View File

@@ -1,5 +1,6 @@
import { NodeTemplate, PropertyDefinition } from '../Serialization/NodeTemplates';
import { NodeType } from '../Types/TaskStatus';
import { getComponentTypeName } from '@esengine/ecs-framework';
/**
* 行为树节点元数据
@@ -80,7 +81,7 @@ export function BehaviorNode(metadata: BehaviorNodeMetadata) {
return function <T extends { new (...args: any[]): any }>(constructor: T) {
const metadataWithClassName = {
...metadata,
className: constructor.name
className: getComponentTypeName(constructor as any)
};
NodeClassRegistry.registerNodeClass(constructor, metadataWithClassName);
return constructor;
@@ -129,14 +130,12 @@ export const NodeProperty = BehaviorProperty;
*/
export function getRegisteredNodeTemplates(): NodeTemplate[] {
return NodeClassRegistry.getAllNodeClasses().map(({ metadata, constructor }) => {
// 从类的 __nodeProperties 收集属性定义
const propertyDefs = constructor.__nodeProperties || [];
const defaultConfig: any = {
nodeType: metadata.type.toLowerCase()
};
// 从类的默认值中提取配置,并补充 defaultValue
const instance = new constructor();
const properties: PropertyDefinition[] = propertyDefs.map((prop: PropertyDefinition) => {
const defaultValue = instance[prop.name];
@@ -149,7 +148,6 @@ export function getRegisteredNodeTemplates(): NodeTemplate[] {
};
});
// 添加子类型字段
switch (metadata.type) {
case NodeType.Composite:
defaultConfig.compositeType = metadata.displayName;
@@ -173,6 +171,7 @@ export function getRegisteredNodeTemplates(): NodeTemplate[] {
description: metadata.description,
color: metadata.color,
className: metadata.className,
componentClass: constructor,
requiresChildren: metadata.requiresChildren,
defaultConfig,
properties

View File

@@ -1,4 +1,4 @@
import { Entity, IScene, createLogger } from '@esengine/ecs-framework';
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';
@@ -306,6 +306,19 @@ export class BehaviorTreeAssetLoader {
} 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}`);
}
@@ -335,6 +348,19 @@ export class BehaviorTreeAssetLoader {
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}`);
}

View File

@@ -67,13 +67,11 @@ export class EditorFormatConverter {
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',
description: metadata?.description || editorData.metadata?.description,
@@ -82,13 +80,10 @@ export class EditorFormatConverter {
modifiedAt: metadata?.modifiedAt || new Date().toISOString()
};
// 转换节点
const nodes = this.convertNodes(editorData.nodes);
// 转换黑板
const blackboard = this.convertBlackboard(editorData.blackboard);
// 转换属性绑定
const propertyBindings = this.convertPropertyBindings(
editorData.connections,
editorData.nodes,
@@ -130,11 +125,13 @@ export class EditorFormatConverter {
* 转换单个节点
*/
private static convertNode(editorNode: EditorNode): BehaviorTreeNodeData {
// 复制data去除编辑器特有的字段
const data = { ...editorNode.data };
// 移除可能存在的UI相关字段
delete data.nodeType; // 这个信息已经在nodeType字段中
delete data.nodeType;
if (editorNode.template.className) {
data.className = editorNode.template.className;
}
return {
id: editorNode.id,
@@ -152,7 +149,6 @@ export class EditorFormatConverter {
const variables: BlackboardVariableDefinition[] = [];
for (const [name, value] of Object.entries(blackboard)) {
// 推断类型
const type = this.inferBlackboardType(value);
variables.push({
@@ -191,7 +187,6 @@ export class EditorFormatConverter {
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) {
@@ -205,7 +200,6 @@ export class EditorFormatConverter {
let variableName: string | undefined;
// 检查 from 节点是否是黑板变量节点
if (fromNode.data.nodeType === 'blackboard-variable') {
variableName = fromNode.data.variableName;
} else if (conn.fromProperty) {
@@ -241,22 +235,18 @@ export class EditorFormatConverter {
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 || [],
asset.nodes
);
// 添加节点连接基于children关系
const nodeConnections = this.buildNodeConnections(asset.nodes);
connections.push(...nodeConnections);
@@ -287,19 +277,24 @@ export class EditorFormatConverter {
*/
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: {
displayName: node.name,
category: this.inferCategory(node.nodeType),
type: node.nodeType
},
template,
data: { ...node.data },
position,
children: node.children
@@ -335,10 +330,8 @@ export class EditorFormatConverter {
const connections: EditorConnection[] = [];
for (const binding of bindings) {
// 需要找到代表这个黑板变量的节点(如果有的话)
// 这里简化处理,在实际使用中可能需要更复杂的逻辑
connections.push({
from: 'blackboard', // 占位符,实际使用时需要更复杂的处理
from: 'blackboard',
to: binding.nodeId,
toProperty: binding.propertyName,
connectionType: 'property'

View File

@@ -2,7 +2,7 @@ import { NodeType } from '../Types/TaskStatus';
import { getRegisteredNodeTemplates } from '../Decorators/BehaviorNodeDecorator';
/**
* 节点数据JSON格式(用于编辑器)
* 节点数据JSON格式
*/
export interface NodeDataJSON {
nodeType: string;
@@ -11,12 +11,49 @@ export interface NodeDataJSON {
[key: string]: any;
}
/**
* 内置属性类型常量
*/
export const PropertyType = {
/** 字符串 */
String: 'string',
/** 数值 */
Number: 'number',
/** 布尔值 */
Boolean: 'boolean',
/** 选择框 */
Select: 'select',
/** 黑板变量引用 */
Blackboard: 'blackboard',
/** 代码编辑器 */
Code: 'code',
/** 变量引用 */
Variable: 'variable',
/** 资产引用 */
Asset: 'asset'
} as const;
/**
* 属性类型(支持自定义扩展)
*
* @example
* ```typescript
* // 使用内置类型
* type: PropertyType.String
*
* // 使用自定义类型
* type: 'color-picker'
* type: 'curve-editor'
* ```
*/
export type PropertyType = typeof PropertyType[keyof typeof PropertyType] | string;
/**
* 属性定义(用于编辑器)
*/
export interface PropertyDefinition {
name: string;
type: 'string' | 'number' | 'boolean' | 'select' | 'blackboard' | 'code' | 'variable' | 'asset';
type: PropertyType;
label: string;
description?: string;
defaultValue?: any;
@@ -25,6 +62,62 @@ export interface PropertyDefinition {
max?: number;
step?: number;
required?: boolean;
/**
* 自定义渲染配置
*
* 用于指定编辑器如何渲染此属性
*
* @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;
};
}
/**
@@ -38,6 +131,7 @@ export interface NodeTemplate {
description: string;
color?: string;
className?: string;
componentClass?: Function;
requiresChildren?: boolean;
defaultConfig: Partial<NodeDataJSON>;
properties: PropertyDefinition[];

View File

@@ -67,7 +67,10 @@ export class LeafExecutionSystem extends EntitySystem {
} else if (entity.hasComponent(ExecuteAction)) {
status = this.executeCustomAction(entity);
} else {
this.outputLog(entity, `动作节点没有找到任何已知的动作组件`, 'warn');
status = this.executeGenericAction(entity);
if (status === TaskStatus.Failure) {
this.outputLog(entity, `动作节点没有找到任何已知的动作组件`, 'warn');
}
}
node.status = status;
@@ -298,6 +301,41 @@ export class LeafExecutionSystem extends EntitySystem {
return func(entity, blackboard, Time.deltaTime);
}
/**
* 执行通用动作组件
* 查找实体上具有 execute 方法的自定义组件并执行
*/
private executeGenericAction(entity: Entity): TaskStatus {
for (const component of entity.components) {
if (component instanceof BehaviorTreeNode ||
component instanceof ActiveNode ||
component instanceof BlackboardComponent ||
component instanceof PropertyBindings ||
component instanceof LogOutput) {
continue;
}
if (typeof (component as any).execute === 'function') {
try {
const blackboard = this.findBlackboard(entity);
const status = (component as any).execute(entity, blackboard);
if (typeof status === 'number' &&
(status === TaskStatus.Success ||
status === TaskStatus.Failure ||
status === TaskStatus.Running)) {
return status;
}
} catch (error) {
this.outputLog(entity, `执行动作组件时发生错误: ${error}`, 'error');
return TaskStatus.Failure;
}
}
}
return TaskStatus.Failure;
}
/**
* 执行条件节点
*/

View File

@@ -13,18 +13,34 @@ export enum TaskStatus {
}
/**
* 节点类型
* 内置节点类型常量
*/
export enum NodeType {
export const NodeType = {
/** 复合节点 - 有多个子节点 */
Composite = 'composite',
Composite: 'composite',
/** 装饰器节点 - 有一个子节点 */
Decorator = 'decorator',
Decorator: 'decorator',
/** 动作节点 - 叶子节点 */
Action = 'action',
Action: 'action',
/** 条件节点 - 叶子节点 */
Condition = 'condition'
}
Condition: 'condition'
} as const;
/**
* 节点类型(支持自定义扩展)
*
* 使用内置类型或自定义字符串
*
* @example
* ```typescript
* // 使用内置类型
* type: NodeType.Action
*
* // 使用自定义类型
* type: 'custom-behavior'
* ```
*/
export type NodeType = typeof NodeType[keyof typeof NodeType] | string;
/**
* 复合节点类型