fix(behavior-tree): 修复插件节点执行问题并完善文档
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行条件节点
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 复合节点类型
|
||||
|
||||
Reference in New Issue
Block a user