refactor(behavior-tree)!: 迁移到 Runtime 执行器架构 (#196)
* refactor(behavior-tree)!: 迁移到 Runtime 执行器架构 * fix(behavior-tree): 修复LogAction中的ReDoS安全漏洞 * feat(behavior-tree): 完善行为树核心功能并修复类型错误
This commit is contained in:
@@ -1,547 +1,357 @@
|
||||
import { Entity, IScene } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeNode } from './Components/BehaviorTreeNode';
|
||||
import { CompositeNodeComponent } from './Components/CompositeNodeComponent';
|
||||
import { DecoratorNodeComponent } from './Components/DecoratorNodeComponent';
|
||||
import { BlackboardComponent } from './Components/BlackboardComponent';
|
||||
import { NodeType, CompositeType, DecoratorType, BlackboardValueType } from './Types/TaskStatus';
|
||||
|
||||
// 导入动作组件
|
||||
import { WaitAction } from './Components/Actions/WaitAction';
|
||||
import { LogAction } from './Components/Actions/LogAction';
|
||||
import { SetBlackboardValueAction } from './Components/Actions/SetBlackboardValueAction';
|
||||
import { ModifyBlackboardValueAction, ModifyOperation } from './Components/Actions/ModifyBlackboardValueAction';
|
||||
import { ExecuteAction, CustomActionFunction } 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, CustomConditionFunction } from './Components/Conditions/ExecuteCondition';
|
||||
|
||||
// 导入装饰器组件
|
||||
import { RepeaterNode } from './Components/Decorators/RepeaterNode';
|
||||
import { InverterNode } from './Components/Decorators/InverterNode';
|
||||
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 { BehaviorTreeData, BehaviorNodeData } from './Runtime/BehaviorTreeData';
|
||||
import { NodeType } from './Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 行为树构建器
|
||||
*
|
||||
* 提供流式 API 来构建行为树结构
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const aiRoot = BehaviorTreeBuilder.create(scene, 'AI')
|
||||
* .blackboard()
|
||||
* .defineVariable('health', BlackboardValueType.Number, 100)
|
||||
* .defineVariable('target', BlackboardValueType.Object, null)
|
||||
* .endBlackboard()
|
||||
* .selector('MainSelector')
|
||||
* .sequence('AttackSequence')
|
||||
* .condition((entity, blackboard) => {
|
||||
* return blackboard?.getValue('health') > 50;
|
||||
* })
|
||||
* .action('Attack', (entity) => TaskStatus.Success)
|
||||
* .end()
|
||||
* .action('Flee', (entity) => TaskStatus.Success)
|
||||
* .end()
|
||||
* .build();
|
||||
* ```
|
||||
* 提供流式API构建行为树数据结构
|
||||
*/
|
||||
export class BehaviorTreeBuilder {
|
||||
private scene: IScene;
|
||||
private currentEntity: Entity;
|
||||
private entityStack: Entity[] = [];
|
||||
private blackboardEntity?: Entity;
|
||||
private treeData: BehaviorTreeData;
|
||||
private nodeStack: string[] = [];
|
||||
private nodeIdCounter: number = 0;
|
||||
|
||||
private constructor(scene: IScene, rootName: string) {
|
||||
this.scene = scene;
|
||||
this.currentEntity = scene.createEntity(rootName);
|
||||
private constructor(treeName: string) {
|
||||
this.treeData = {
|
||||
id: `tree_${Date.now()}`,
|
||||
name: treeName,
|
||||
rootNodeId: '',
|
||||
nodes: new Map(),
|
||||
blackboardVariables: new Map()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建行为树构建器
|
||||
*
|
||||
* @param scene 场景实例
|
||||
* @param rootName 根节点名称
|
||||
* @returns 构建器实例
|
||||
* 创建构建器
|
||||
*/
|
||||
static create(scene: IScene, rootName: string = 'BehaviorTreeRoot'): BehaviorTreeBuilder {
|
||||
return new BehaviorTreeBuilder(scene, rootName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加黑板组件到根节点
|
||||
*/
|
||||
blackboard(): BehaviorTreeBuilder {
|
||||
this.blackboardEntity = this.currentEntity;
|
||||
this.currentEntity.addComponent(new BlackboardComponent());
|
||||
return this;
|
||||
static create(treeName: string = 'BehaviorTree'): BehaviorTreeBuilder {
|
||||
return new BehaviorTreeBuilder(treeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义黑板变量
|
||||
*/
|
||||
defineVariable(
|
||||
name: string,
|
||||
type: BlackboardValueType,
|
||||
initialValue: any,
|
||||
options?: { readonly?: boolean; description?: string }
|
||||
): BehaviorTreeBuilder {
|
||||
if (!this.blackboardEntity) {
|
||||
throw new Error('Must call blackboard() first');
|
||||
defineBlackboardVariable(key: string, initialValue: any): BehaviorTreeBuilder {
|
||||
if (!this.treeData.blackboardVariables) {
|
||||
this.treeData.blackboardVariables = new Map();
|
||||
}
|
||||
|
||||
const blackboard = this.blackboardEntity.getComponent(BlackboardComponent);
|
||||
if (blackboard) {
|
||||
blackboard.defineVariable(name, type, initialValue, options);
|
||||
}
|
||||
|
||||
this.treeData.blackboardVariables.set(key, initialValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束黑板定义
|
||||
* 添加序列节点
|
||||
*/
|
||||
endBlackboard(): BehaviorTreeBuilder {
|
||||
this.blackboardEntity = undefined;
|
||||
return this;
|
||||
sequence(name?: string): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('Sequence', name || 'Sequence');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建序列节点
|
||||
* 添加选择器节点
|
||||
*/
|
||||
sequence(name: string = 'Sequence'): BehaviorTreeBuilder {
|
||||
return this.composite(name, CompositeType.Sequence);
|
||||
selector(name?: string): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('Selector', name || 'Selector');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建选择器节点
|
||||
* 添加并行节点
|
||||
*/
|
||||
selector(name: string = 'Selector'): BehaviorTreeBuilder {
|
||||
return this.composite(name, CompositeType.Selector);
|
||||
parallel(name?: string, config?: { successPolicy?: string; failurePolicy?: string }): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('Parallel', name || 'Parallel', config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并行节点
|
||||
* 添加并行选择器节点
|
||||
*/
|
||||
parallel(name: string = 'Parallel'): BehaviorTreeBuilder {
|
||||
return this.composite(name, CompositeType.Parallel);
|
||||
parallelSelector(name?: string, config?: { failurePolicy?: string }): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('ParallelSelector', name || 'ParallelSelector', config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并行选择器节点
|
||||
* 添加随机序列节点
|
||||
*/
|
||||
parallelSelector(name: string = 'ParallelSelector'): BehaviorTreeBuilder {
|
||||
return this.composite(name, CompositeType.ParallelSelector);
|
||||
randomSequence(name?: string): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('RandomSequence', name || 'RandomSequence');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建随机序列节点
|
||||
* 添加随机选择器节点
|
||||
*/
|
||||
randomSequence(name: string = 'RandomSequence'): BehaviorTreeBuilder {
|
||||
return this.composite(name, CompositeType.RandomSequence);
|
||||
randomSelector(name?: string): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('RandomSelector', name || 'RandomSelector');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建随机选择器节点
|
||||
* 添加反转装饰器
|
||||
*/
|
||||
randomSelector(name: string = 'RandomSelector'): BehaviorTreeBuilder {
|
||||
return this.composite(name, CompositeType.RandomSelector);
|
||||
inverter(name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('Inverter', name || 'Inverter');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建复合节点
|
||||
* 添加重复装饰器
|
||||
*/
|
||||
private composite(name: string, type: CompositeType): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Composite;
|
||||
node.nodeName = name;
|
||||
|
||||
const composite = entity.addComponent(new CompositeNodeComponent());
|
||||
composite.compositeType = type;
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
repeater(repeatCount: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('Repeater', name || 'Repeater', { repeatCount });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建反转装饰器
|
||||
* 添加总是成功装饰器
|
||||
*/
|
||||
inverter(name: string = 'Inverter'): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
entity.addComponent(new InverterNode());
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
alwaysSucceed(name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('AlwaysSucceed', name || 'AlwaysSucceed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建重复装饰器
|
||||
* 添加总是失败装饰器
|
||||
*/
|
||||
repeater(name: string = 'Repeater', count: number = -1, endOnFailure: boolean = false): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
const decorator = entity.addComponent(new RepeaterNode());
|
||||
decorator.repeatCount = count;
|
||||
decorator.endOnFailure = endOnFailure;
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
alwaysFail(name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('AlwaysFail', name || 'AlwaysFail');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建直到成功装饰器
|
||||
* 添加直到成功装饰器
|
||||
*/
|
||||
untilSuccess(name: string = 'UntilSuccess'): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
entity.addComponent(new UntilSuccessNode());
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
untilSuccess(name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('UntilSuccess', name || 'UntilSuccess');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建直到失败装饰器
|
||||
* 添加直到失败装饰器
|
||||
*/
|
||||
untilFail(name: string = 'UntilFail'): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
entity.addComponent(new UntilFailNode());
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
untilFail(name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('UntilFail', name || 'UntilFail');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建总是成功装饰器
|
||||
* 添加条件装饰器
|
||||
*/
|
||||
alwaysSucceed(name: string = 'AlwaysSucceed'): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
entity.addComponent(new AlwaysSucceedNode());
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
conditional(blackboardKey: string, expectedValue: any, operator?: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('Conditional', name || 'Conditional', {
|
||||
blackboardKey,
|
||||
expectedValue,
|
||||
operator: operator || 'equals'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建总是失败装饰器
|
||||
* 添加冷却装饰器
|
||||
*/
|
||||
alwaysFail(name: string = 'AlwaysFail'): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
entity.addComponent(new AlwaysFailNode());
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
cooldown(cooldownTime: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('Cooldown', name || 'Cooldown', { cooldownTime });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建条件装饰器
|
||||
* 添加超时装饰器
|
||||
*/
|
||||
conditional(name: string, conditionCode: string): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
const decorator = entity.addComponent(new ConditionalNode());
|
||||
decorator.conditionCode = conditionCode;
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
timeout(timeout: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('Timeout', name || 'Timeout', { timeout });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建冷却装饰器
|
||||
* 添加等待动作
|
||||
*/
|
||||
cooldown(name: string = 'Cooldown', cooldownTime: number = 1.0): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
const decorator = entity.addComponent(new CooldownNode());
|
||||
decorator.cooldownTime = cooldownTime;
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
wait(duration: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addActionNode('Wait', name || 'Wait', { duration });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建超时装饰器
|
||||
* 添加日志动作
|
||||
*/
|
||||
timeout(name: string = 'Timeout', timeoutDuration: number = 5.0): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
const decorator = entity.addComponent(new TimeoutNode());
|
||||
decorator.timeoutDuration = timeoutDuration;
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
log(message: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addActionNode('Log', name || 'Log', { message });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建等待动作
|
||||
* 添加设置黑板值动作
|
||||
*/
|
||||
wait(waitTime: number, name: string = 'Wait'): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Action;
|
||||
node.nodeName = name;
|
||||
|
||||
const action = entity.addComponent(new WaitAction());
|
||||
action.waitTime = waitTime;
|
||||
|
||||
return this;
|
||||
setBlackboardValue(key: string, value: any, name?: string): BehaviorTreeBuilder {
|
||||
return this.addActionNode('SetBlackboardValue', name || 'SetBlackboardValue', { key, value });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建日志动作
|
||||
* 添加修改黑板值动作
|
||||
*/
|
||||
log(message: string, level: 'log' | 'info' | 'warn' | 'error' = 'log', name: string = 'Log'): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Action;
|
||||
node.nodeName = name;
|
||||
|
||||
const action = entity.addComponent(new LogAction());
|
||||
action.message = message;
|
||||
action.level = level;
|
||||
|
||||
return this;
|
||||
modifyBlackboardValue(key: string, operation: string, value: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addActionNode('ModifyBlackboardValue', name || 'ModifyBlackboardValue', {
|
||||
key,
|
||||
operation,
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建设置黑板值动作
|
||||
* 添加执行动作
|
||||
*/
|
||||
setBlackboardValue(variableName: string, value: any, name: string = 'SetValue'): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Action;
|
||||
node.nodeName = name;
|
||||
|
||||
const action = entity.addComponent(new SetBlackboardValueAction());
|
||||
action.variableName = variableName;
|
||||
action.value = value;
|
||||
|
||||
return this;
|
||||
executeAction(actionName: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addActionNode('ExecuteAction', name || 'ExecuteAction', { actionName });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建修改黑板值动作
|
||||
* 添加黑板比较条件
|
||||
*/
|
||||
modifyBlackboardValue(
|
||||
variableName: string,
|
||||
operation: ModifyOperation,
|
||||
operand: any,
|
||||
name: string = 'ModifyValue'
|
||||
): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Action;
|
||||
node.nodeName = name;
|
||||
|
||||
const action = entity.addComponent(new ModifyBlackboardValueAction());
|
||||
action.variableName = variableName;
|
||||
action.operation = operation;
|
||||
action.operand = operand;
|
||||
|
||||
return this;
|
||||
blackboardCompare(key: string, compareValue: any, operator?: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addConditionNode('BlackboardCompare', name || 'BlackboardCompare', {
|
||||
key,
|
||||
compareValue,
|
||||
operator: operator || 'equals'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义动作
|
||||
* 添加黑板存在检查条件
|
||||
*/
|
||||
action(name: string, func: CustomActionFunction): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Action;
|
||||
node.nodeName = name;
|
||||
|
||||
const action = entity.addComponent(new ExecuteAction());
|
||||
action.setFunction(func);
|
||||
|
||||
return this;
|
||||
blackboardExists(key: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addConditionNode('BlackboardExists', name || 'BlackboardExists', { key });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建黑板比较条件
|
||||
* 添加随机概率条件
|
||||
*/
|
||||
compareBlackboardValue(
|
||||
variableName: string,
|
||||
operator: CompareOperator,
|
||||
compareValue: any,
|
||||
name: string = 'Compare'
|
||||
): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Condition;
|
||||
node.nodeName = name;
|
||||
|
||||
const condition = entity.addComponent(new BlackboardCompareCondition());
|
||||
condition.variableName = variableName;
|
||||
condition.operator = operator;
|
||||
condition.compareValue = compareValue;
|
||||
|
||||
return this;
|
||||
randomProbability(probability: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addConditionNode('RandomProbability', name || 'RandomProbability', { probability });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建黑板变量存在条件
|
||||
* 添加执行条件
|
||||
*/
|
||||
checkBlackboardExists(variableName: string, checkNotNull: boolean = false, name: string = 'Exists'): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Condition;
|
||||
node.nodeName = name;
|
||||
|
||||
const condition = entity.addComponent(new BlackboardExistsCondition());
|
||||
condition.variableName = variableName;
|
||||
condition.checkNotNull = checkNotNull;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建随机概率条件
|
||||
*/
|
||||
randomProbability(probability: number, name: string = 'Random'): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Condition;
|
||||
node.nodeName = name;
|
||||
|
||||
const condition = entity.addComponent(new RandomProbabilityCondition());
|
||||
condition.probability = probability;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义条件
|
||||
*/
|
||||
condition(func: CustomConditionFunction, name: string = 'Condition'): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Condition;
|
||||
node.nodeName = name;
|
||||
|
||||
const condition = entity.addComponent(new ExecuteCondition());
|
||||
condition.setFunction(func);
|
||||
|
||||
return this;
|
||||
executeCondition(conditionName: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addConditionNode('ExecuteCondition', name || 'ExecuteCondition', { conditionName });
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束当前节点,返回父节点
|
||||
*/
|
||||
end(): BehaviorTreeBuilder {
|
||||
if (this.entityStack.length === 0) {
|
||||
throw new Error('No parent node to return to');
|
||||
if (this.nodeStack.length > 0) {
|
||||
this.nodeStack.pop();
|
||||
}
|
||||
|
||||
this.currentEntity = this.entityStack.pop()!;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建并返回根节点实体
|
||||
* 构建行为树数据
|
||||
*/
|
||||
build(): Entity {
|
||||
// 确保返回到根节点
|
||||
while (this.entityStack.length > 0) {
|
||||
this.currentEntity = this.entityStack.pop()!;
|
||||
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;
|
||||
}
|
||||
|
||||
return this.currentEntity;
|
||||
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++}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { Core } from '@esengine/ecs-framework';
|
||||
import type { ServiceContainer, IPlugin, IScene } from '@esengine/ecs-framework';
|
||||
import { WorldManager } from '@esengine/ecs-framework';
|
||||
import { LeafExecutionSystem } from './Systems/LeafExecutionSystem';
|
||||
import { DecoratorExecutionSystem } from './Systems/DecoratorExecutionSystem';
|
||||
import { CompositeExecutionSystem } from './Systems/CompositeExecutionSystem';
|
||||
import { SubTreeExecutionSystem } from './Systems/SubTreeExecutionSystem';
|
||||
import { BehaviorTreeExecutionSystem } from './Runtime/BehaviorTreeExecutionSystem';
|
||||
import { GlobalBlackboardService } from './Services/GlobalBlackboardService';
|
||||
import { BehaviorTreeAssetManager } from './Runtime/BehaviorTreeAssetManager';
|
||||
|
||||
/**
|
||||
* 行为树插件
|
||||
@@ -33,11 +31,12 @@ export class BehaviorTreePlugin implements IPlugin {
|
||||
/**
|
||||
* 安装插件
|
||||
*/
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
async install(_core: Core, services: ServiceContainer): Promise<void> {
|
||||
this.services = services;
|
||||
|
||||
// 注册全局黑板服务
|
||||
// 注册全局服务
|
||||
services.registerSingleton(GlobalBlackboardService);
|
||||
services.registerSingleton(BehaviorTreeAssetManager);
|
||||
|
||||
this.worldManager = services.resolve(WorldManager);
|
||||
}
|
||||
@@ -46,9 +45,9 @@ export class BehaviorTreePlugin implements IPlugin {
|
||||
* 卸载插件
|
||||
*/
|
||||
async uninstall(): Promise<void> {
|
||||
// 注销全局黑板服务
|
||||
if (this.services) {
|
||||
this.services.unregister(GlobalBlackboardService);
|
||||
this.services.unregister(BehaviorTreeAssetManager);
|
||||
}
|
||||
|
||||
this.worldManager = null;
|
||||
@@ -58,11 +57,7 @@ export class BehaviorTreePlugin implements IPlugin {
|
||||
/**
|
||||
* 为场景设置行为树系统
|
||||
*
|
||||
* 向场景添加所有必需的行为树系统:
|
||||
* - LeafExecutionSystem (updateOrder: 100)
|
||||
* - DecoratorExecutionSystem (updateOrder: 200)
|
||||
* - CompositeExecutionSystem (updateOrder: 300)
|
||||
* - SubTreeExecutionSystem (updateOrder: 300)
|
||||
* 向场景添加行为树执行系统
|
||||
*
|
||||
* @param scene 目标场景
|
||||
*
|
||||
@@ -73,10 +68,7 @@ export class BehaviorTreePlugin implements IPlugin {
|
||||
* ```
|
||||
*/
|
||||
public setupScene(scene: IScene): void {
|
||||
scene.addSystem(new LeafExecutionSystem());
|
||||
scene.addSystem(new DecoratorExecutionSystem());
|
||||
scene.addSystem(new CompositeExecutionSystem());
|
||||
scene.addSystem(new SubTreeExecutionSystem());
|
||||
scene.addSystem(new BehaviorTreeExecutionSystem());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,179 +1,92 @@
|
||||
import { Entity } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeNode } from './Components/BehaviorTreeNode';
|
||||
import { ActiveNode } from './Components/ActiveNode';
|
||||
import { TaskStatus } from './Types/TaskStatus';
|
||||
import { Entity, Core } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeData } from './Runtime/BehaviorTreeData';
|
||||
import { BehaviorTreeRuntimeComponent } from './Runtime/BehaviorTreeRuntimeComponent';
|
||||
import { BehaviorTreeAssetManager } from './Runtime/BehaviorTreeAssetManager';
|
||||
|
||||
/**
|
||||
* 行为树启动/停止辅助类
|
||||
* 行为树启动辅助类
|
||||
*
|
||||
* 提供便捷方法来启动、停止和暂停行为树
|
||||
* 提供便捷方法来启动、停止行为树
|
||||
*/
|
||||
export class BehaviorTreeStarter {
|
||||
/**
|
||||
* 启动行为树
|
||||
*
|
||||
* 给根节点添加 ActiveNode 组件,使行为树开始执行
|
||||
*
|
||||
* @param rootEntity 行为树根节点实体
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const aiRoot = scene.createEntity('aiRoot');
|
||||
* // ... 构建行为树结构
|
||||
* BehaviorTreeStarter.start(aiRoot);
|
||||
* ```
|
||||
* @param entity 游戏实体
|
||||
* @param treeData 行为树数据
|
||||
* @param autoStart 是否自动开始执行
|
||||
*/
|
||||
static start(rootEntity: Entity): void {
|
||||
if (!rootEntity.hasComponent(BehaviorTreeNode)) {
|
||||
throw new Error('Entity must have BehaviorTreeNode component');
|
||||
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);
|
||||
}
|
||||
|
||||
if (!rootEntity.hasComponent(ActiveNode)) {
|
||||
rootEntity.addComponent(new ActiveNode());
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止行为树
|
||||
*
|
||||
* 移除所有节点的 ActiveNode 组件,停止执行
|
||||
*
|
||||
* @param rootEntity 行为树根节点实体
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* BehaviorTreeStarter.stop(aiRoot);
|
||||
* ```
|
||||
* @param entity 游戏实体
|
||||
*/
|
||||
static stop(rootEntity: Entity): void {
|
||||
this.stopRecursive(rootEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归停止所有子节点
|
||||
*/
|
||||
private static stopRecursive(entity: Entity): void {
|
||||
// 移除活跃标记
|
||||
if (entity.hasComponent(ActiveNode)) {
|
||||
entity.removeComponentByType(ActiveNode);
|
||||
}
|
||||
|
||||
// 重置节点状态
|
||||
const node = entity.getComponent(BehaviorTreeNode);
|
||||
if (node) {
|
||||
node.reset();
|
||||
}
|
||||
|
||||
// 递归处理子节点
|
||||
for (const child of entity.children) {
|
||||
this.stopRecursive(child);
|
||||
static stop(entity: Entity): void {
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
if (runtime) {
|
||||
runtime.isRunning = false;
|
||||
runtime.resetAllStates();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停行为树
|
||||
*
|
||||
* 移除 ActiveNode 但保留节点状态,可以恢复执行
|
||||
*
|
||||
* @param rootEntity 行为树根节点实体
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 暂停
|
||||
* BehaviorTreeStarter.pause(aiRoot);
|
||||
*
|
||||
* // 恢复
|
||||
* BehaviorTreeStarter.resume(aiRoot);
|
||||
* ```
|
||||
* @param entity 游戏实体
|
||||
*/
|
||||
static pause(rootEntity: Entity): void {
|
||||
this.pauseRecursive(rootEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归暂停所有子节点
|
||||
*/
|
||||
private static pauseRecursive(entity: Entity): void {
|
||||
// 只移除活跃标记,不重置状态
|
||||
if (entity.hasComponent(ActiveNode)) {
|
||||
entity.removeComponentByType(ActiveNode);
|
||||
}
|
||||
|
||||
// 递归处理子节点
|
||||
for (const child of entity.children) {
|
||||
this.pauseRecursive(child);
|
||||
static pause(entity: Entity): void {
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
if (runtime) {
|
||||
runtime.isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复行为树执行
|
||||
* 恢复行为树
|
||||
*
|
||||
* 从暂停状态恢复,重新添加 ActiveNode 到之前正在执行的节点
|
||||
*
|
||||
* @param rootEntity 行为树根节点实体
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* BehaviorTreeStarter.resume(aiRoot);
|
||||
* ```
|
||||
* @param entity 游戏实体
|
||||
*/
|
||||
static resume(rootEntity: Entity): void {
|
||||
this.resumeRecursive(rootEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归恢复所有正在执行的节点
|
||||
*/
|
||||
private static resumeRecursive(entity: Entity): void {
|
||||
const node = entity.getComponent(BehaviorTreeNode);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果节点状态是 Running,恢复活跃标记
|
||||
if (node.status === TaskStatus.Running) {
|
||||
if (!entity.hasComponent(ActiveNode)) {
|
||||
entity.addComponent(new ActiveNode());
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理子节点
|
||||
for (const child of entity.children) {
|
||||
this.resumeRecursive(child);
|
||||
static resume(entity: Entity): void {
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
if (runtime) {
|
||||
runtime.isRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启行为树
|
||||
*
|
||||
* 停止并重置所有节点,然后重新启动
|
||||
*
|
||||
* @param rootEntity 行为树根节点实体
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* BehaviorTreeStarter.restart(aiRoot);
|
||||
* ```
|
||||
* @param entity 游戏实体
|
||||
*/
|
||||
static restart(rootEntity: Entity): void {
|
||||
this.stop(rootEntity);
|
||||
this.start(rootEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查行为树是否正在运行
|
||||
*
|
||||
* @param rootEntity 行为树根节点实体
|
||||
* @returns 是否正在运行
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* if (BehaviorTreeStarter.isRunning(aiRoot)) {
|
||||
* console.log('AI is active');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
static isRunning(rootEntity: Entity): boolean {
|
||||
return rootEntity.hasComponent(ActiveNode);
|
||||
static restart(entity: Entity): void {
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
if (runtime) {
|
||||
runtime.resetAllStates();
|
||||
runtime.isRunning = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { Component, ECSComponent, Entity } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { BlackboardComponent } from '../BlackboardComponent';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
|
||||
/**
|
||||
* 自定义动作函数类型
|
||||
*/
|
||||
export type CustomActionFunction = (
|
||||
entity: Entity,
|
||||
blackboard?: BlackboardComponent,
|
||||
deltaTime?: number
|
||||
) => TaskStatus;
|
||||
|
||||
/**
|
||||
* 执行自定义函数动作组件
|
||||
*
|
||||
* 允许用户提供自定义的动作执行函数
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '自定义动作',
|
||||
category: '动作',
|
||||
type: NodeType.Action,
|
||||
icon: 'Code',
|
||||
description: '执行自定义代码',
|
||||
color: '#FFC107'
|
||||
})
|
||||
@ECSComponent('ExecuteAction')
|
||||
@Serializable({ version: 1 })
|
||||
export class ExecuteAction extends Component {
|
||||
@BehaviorProperty({
|
||||
label: '动作代码',
|
||||
type: 'code',
|
||||
description: 'JavaScript 代码,返回 TaskStatus',
|
||||
required: true
|
||||
})
|
||||
@Serialize()
|
||||
actionCode?: string = 'return TaskStatus.Success;';
|
||||
|
||||
@Serialize()
|
||||
parameters: Record<string, any> = {};
|
||||
|
||||
/** 编译后的函数(不序列化) */
|
||||
@IgnoreSerialization()
|
||||
private compiledFunction?: CustomActionFunction;
|
||||
|
||||
/**
|
||||
* 获取或编译执行函数
|
||||
*/
|
||||
getFunction(): CustomActionFunction | undefined {
|
||||
if (!this.compiledFunction && this.actionCode) {
|
||||
try {
|
||||
const func = new Function(
|
||||
'entity',
|
||||
'blackboard',
|
||||
'deltaTime',
|
||||
'parameters',
|
||||
'TaskStatus',
|
||||
`
|
||||
const { Success, Failure, Running, Invalid } = TaskStatus;
|
||||
try {
|
||||
${this.actionCode}
|
||||
} catch (error) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
this.compiledFunction = (entity, blackboard, deltaTime) => {
|
||||
return func(entity, blackboard, deltaTime, this.parameters, TaskStatus) || TaskStatus.Success;
|
||||
};
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return this.compiledFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自定义函数(运行时使用)
|
||||
*/
|
||||
setFunction(func: CustomActionFunction): void {
|
||||
this.compiledFunction = func;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { NodeType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
|
||||
/**
|
||||
* 日志动作组件
|
||||
*
|
||||
* 输出日志信息
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '日志',
|
||||
category: '动作',
|
||||
type: NodeType.Action,
|
||||
icon: 'FileText',
|
||||
description: '输出日志消息',
|
||||
color: '#673AB7'
|
||||
})
|
||||
@ECSComponent('LogAction')
|
||||
@Serializable({ version: 1 })
|
||||
export class LogAction extends Component {
|
||||
@BehaviorProperty({
|
||||
label: '消息',
|
||||
type: 'string',
|
||||
required: true
|
||||
})
|
||||
@Serialize()
|
||||
message: string = 'Hello';
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '级别',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Log', value: 'log' },
|
||||
{ label: 'Info', value: 'info' },
|
||||
{ label: 'Warn', value: 'warn' },
|
||||
{ label: 'Error', value: 'error' }
|
||||
]
|
||||
})
|
||||
@Serialize()
|
||||
level: 'log' | 'info' | 'warn' | 'error' = 'log';
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '包含实体信息',
|
||||
type: 'boolean'
|
||||
})
|
||||
@Serialize()
|
||||
includeEntityInfo: boolean = false;
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { NodeType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
|
||||
/**
|
||||
* 修改操作类型
|
||||
*/
|
||||
export enum ModifyOperation {
|
||||
/** 加法 */
|
||||
Add = 'add',
|
||||
/** 减法 */
|
||||
Subtract = 'subtract',
|
||||
/** 乘法 */
|
||||
Multiply = 'multiply',
|
||||
/** 除法 */
|
||||
Divide = 'divide',
|
||||
/** 取模 */
|
||||
Modulo = 'modulo',
|
||||
/** 追加(数组/字符串) */
|
||||
Append = 'append',
|
||||
/** 移除(数组) */
|
||||
Remove = 'remove'
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改黑板变量值动作组件
|
||||
*
|
||||
* 对黑板变量执行数学或逻辑操作
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '修改变量',
|
||||
category: '动作',
|
||||
type: NodeType.Action,
|
||||
icon: 'Calculator',
|
||||
description: '对黑板变量执行数学或逻辑操作',
|
||||
color: '#FF9800'
|
||||
})
|
||||
@ECSComponent('ModifyBlackboardValueAction')
|
||||
@Serializable({ version: 1 })
|
||||
export class ModifyBlackboardValueAction extends Component {
|
||||
@BehaviorProperty({
|
||||
label: '变量名',
|
||||
type: 'variable',
|
||||
required: true
|
||||
})
|
||||
@Serialize()
|
||||
variableName: string = '';
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '操作类型',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '加法', value: 'add' },
|
||||
{ label: '减法', value: 'subtract' },
|
||||
{ label: '乘法', value: 'multiply' },
|
||||
{ label: '除法', value: 'divide' },
|
||||
{ label: '取模', value: 'modulo' },
|
||||
{ label: '追加', value: 'append' },
|
||||
{ label: '移除', value: 'remove' }
|
||||
]
|
||||
})
|
||||
@Serialize()
|
||||
operation: ModifyOperation = ModifyOperation.Add;
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '操作数',
|
||||
type: 'string',
|
||||
description: '可以是固定值或变量引用 {{varName}}'
|
||||
})
|
||||
@Serialize()
|
||||
operand: any = 0;
|
||||
|
||||
@Serialize()
|
||||
force: boolean = false;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { NodeType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
|
||||
/**
|
||||
* 设置黑板变量值动作组件
|
||||
*
|
||||
* 将指定值或另一个黑板变量的值设置到目标变量
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '设置变量',
|
||||
category: '动作',
|
||||
type: NodeType.Action,
|
||||
icon: 'Edit',
|
||||
description: '设置黑板变量的值',
|
||||
color: '#3F51B5'
|
||||
})
|
||||
@ECSComponent('SetBlackboardValueAction')
|
||||
@Serializable({ version: 1 })
|
||||
export class SetBlackboardValueAction extends Component {
|
||||
@BehaviorProperty({
|
||||
label: '变量名',
|
||||
type: 'variable',
|
||||
required: true
|
||||
})
|
||||
@Serialize()
|
||||
variableName: string = '';
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '值',
|
||||
type: 'string',
|
||||
description: '可以使用 {{varName}} 引用其他变量'
|
||||
})
|
||||
@Serialize()
|
||||
value: any = '';
|
||||
|
||||
@Serialize()
|
||||
sourceVariable?: string;
|
||||
|
||||
@Serialize()
|
||||
force: boolean = false;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
|
||||
import { NodeType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
|
||||
/**
|
||||
* 等待动作组件
|
||||
*
|
||||
* 等待指定时间后返回成功
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '等待',
|
||||
category: '动作',
|
||||
type: NodeType.Action,
|
||||
icon: 'Clock',
|
||||
description: '等待指定时间',
|
||||
color: '#9E9E9E'
|
||||
})
|
||||
@ECSComponent('WaitAction')
|
||||
@Serializable({ version: 1 })
|
||||
export class WaitAction extends Component {
|
||||
@BehaviorProperty({
|
||||
label: '等待时间',
|
||||
type: 'number',
|
||||
min: 0,
|
||||
step: 0.1,
|
||||
description: '等待时间(秒)',
|
||||
required: true
|
||||
})
|
||||
@Serialize()
|
||||
waitTime: number = 1.0;
|
||||
|
||||
/** 已等待时间(秒) */
|
||||
@IgnoreSerialization()
|
||||
elapsedTime: number = 0;
|
||||
|
||||
/**
|
||||
* 重置等待状态
|
||||
*/
|
||||
reset(): void {
|
||||
this.elapsedTime = 0;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 活跃节点标记组件
|
||||
*
|
||||
* 标记当前应该被执行的节点。
|
||||
* 只有带有此组件的节点才会被各个执行系统处理。
|
||||
*
|
||||
* 这是一个标记组件(Tag Component),不包含数据,只用于标识。
|
||||
*
|
||||
* 执行流程:
|
||||
* 1. 初始时只有根节点带有 ActiveNode
|
||||
* 2. 父节点决定激活哪个子节点时,为子节点添加 ActiveNode
|
||||
* 3. 节点执行完成后移除 ActiveNode
|
||||
* 4. 通过这种方式实现按需执行,避免每帧遍历整棵树
|
||||
*/
|
||||
@ECSComponent('ActiveNode')
|
||||
export class ActiveNode extends Component {
|
||||
// 标记组件,无需数据字段
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 资产元数据组件
|
||||
*
|
||||
* 附加到从资产实例化的行为树根节点上,
|
||||
* 用于标记资产ID和版本信息,便于循环引用检测和调试。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const rootEntity = BehaviorTreeAssetLoader.instantiate(asset, scene);
|
||||
*
|
||||
* // 添加元数据
|
||||
* const metadata = rootEntity.addComponent(new BehaviorTreeAssetMetadata());
|
||||
* metadata.assetId = 'patrol';
|
||||
* metadata.assetVersion = '1.0.0';
|
||||
* ```
|
||||
*/
|
||||
@ECSComponent('BehaviorTreeAssetMetadata')
|
||||
@Serializable({ version: 1 })
|
||||
export class BehaviorTreeAssetMetadata extends Component {
|
||||
/**
|
||||
* 资产ID
|
||||
*/
|
||||
@Serialize()
|
||||
assetId: string = '';
|
||||
|
||||
/**
|
||||
* 资产版本
|
||||
*/
|
||||
@Serialize()
|
||||
assetVersion: string = '';
|
||||
|
||||
/**
|
||||
* 资产名称
|
||||
*/
|
||||
@Serialize()
|
||||
assetName: string = '';
|
||||
|
||||
/**
|
||||
* 加载时间
|
||||
*/
|
||||
@Serialize()
|
||||
loadedAt: number = 0;
|
||||
|
||||
/**
|
||||
* 资产描述
|
||||
*/
|
||||
@Serialize()
|
||||
description: string = '';
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
initialize(assetId: string, assetVersion: string, assetName?: string): void {
|
||||
this.assetId = assetId;
|
||||
this.assetVersion = assetVersion;
|
||||
this.assetName = assetName || assetId;
|
||||
this.loadedAt = Date.now();
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
|
||||
import { TaskStatus, NodeType } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 行为树节点基础组件
|
||||
*
|
||||
* 所有行为树节点都必须包含此组件
|
||||
*/
|
||||
@ECSComponent('BehaviorTreeNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class BehaviorTreeNode extends Component {
|
||||
/** 节点类型 */
|
||||
@Serialize()
|
||||
nodeType: NodeType = NodeType.Action;
|
||||
|
||||
/** 节点名称(用于调试) */
|
||||
@Serialize()
|
||||
nodeName: string = 'Node';
|
||||
|
||||
/** 当前执行状态 */
|
||||
@IgnoreSerialization()
|
||||
status: TaskStatus = TaskStatus.Invalid;
|
||||
|
||||
/** 当前执行的子节点索引(用于复合节点) */
|
||||
@IgnoreSerialization()
|
||||
currentChildIndex: number = 0;
|
||||
|
||||
/**
|
||||
* 重置节点状态
|
||||
*/
|
||||
reset(): void {
|
||||
this.status = TaskStatus.Invalid;
|
||||
this.currentChildIndex = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记节点为失效(递归重置子节点)
|
||||
* 注意:此方法只重置当前节点,子节点需要在 System 中处理
|
||||
*/
|
||||
invalidate(): void {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import { Component, ECSComponent, Core } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { BlackboardValueType } from '../Types/TaskStatus';
|
||||
import { GlobalBlackboardService } from '../Services/GlobalBlackboardService';
|
||||
|
||||
/**
|
||||
* 黑板变量定义
|
||||
*/
|
||||
export interface BlackboardVariable {
|
||||
name: string;
|
||||
type: BlackboardValueType;
|
||||
value: any;
|
||||
readonly?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 黑板组件 - 用于节点间共享数据
|
||||
*
|
||||
* 支持分层查找:
|
||||
* 1. 先查找本地变量
|
||||
* 2. 如果找不到,自动查找全局 Blackboard
|
||||
*
|
||||
* 通常附加到行为树的根节点上
|
||||
*/
|
||||
@ECSComponent('Blackboard')
|
||||
@Serializable({ version: 1 })
|
||||
export class BlackboardComponent extends Component {
|
||||
/** 存储的本地变量 */
|
||||
@Serialize()
|
||||
private variables: Map<string, BlackboardVariable> = new Map();
|
||||
|
||||
/** 是否启用全局 Blackboard 查找 */
|
||||
private useGlobalBlackboard: boolean = true;
|
||||
|
||||
/**
|
||||
* 定义一个新变量
|
||||
*/
|
||||
defineVariable(
|
||||
name: string,
|
||||
type: BlackboardValueType,
|
||||
initialValue: any,
|
||||
options?: {
|
||||
readonly?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
): void {
|
||||
this.variables.set(name, {
|
||||
name,
|
||||
type,
|
||||
value: initialValue,
|
||||
readonly: options?.readonly ?? false,
|
||||
description: options?.description
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取变量值
|
||||
* 先查找本地变量,找不到则查找全局变量
|
||||
*/
|
||||
getValue<T = any>(name: string): T | undefined {
|
||||
const variable = this.variables.get(name);
|
||||
if (variable !== undefined) {
|
||||
return variable.value as T;
|
||||
}
|
||||
|
||||
if (this.useGlobalBlackboard) {
|
||||
return Core.services.resolve(GlobalBlackboardService).getValue<T>(name);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本地变量值(不查找全局)
|
||||
*/
|
||||
getLocalValue<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) {
|
||||
if (variable.readonly && !force) {
|
||||
return false;
|
||||
}
|
||||
variable.value = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.useGlobalBlackboard) {
|
||||
return Core.services.resolve(GlobalBlackboardService).setValue(name, value, force);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置本地变量值(不影响全局)
|
||||
*/
|
||||
setLocalValue(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 {
|
||||
if (this.variables.has(name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.useGlobalBlackboard) {
|
||||
return Core.services.resolve(GlobalBlackboardService).hasVariable(name);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查本地变量是否存在
|
||||
*/
|
||||
hasLocalVariable(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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有本地变量
|
||||
*/
|
||||
clear(): void {
|
||||
this.variables.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用全局 Blackboard 查找
|
||||
*/
|
||||
setUseGlobalBlackboard(enabled: boolean): void {
|
||||
this.useGlobalBlackboard = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用全局 Blackboard 查找
|
||||
*/
|
||||
isUsingGlobalBlackboard(): boolean {
|
||||
return this.useGlobalBlackboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有变量(包括本地和全局)
|
||||
*/
|
||||
getAllVariables(): BlackboardVariable[] {
|
||||
const locals = Array.from(this.variables.values());
|
||||
|
||||
if (this.useGlobalBlackboard) {
|
||||
const globals = Core.services.resolve(GlobalBlackboardService).getAllVariables();
|
||||
const localNames = new Set(this.variables.keys());
|
||||
const filteredGlobals = globals.filter(v => !localNames.has(v.name));
|
||||
return [...locals, ...filteredGlobals];
|
||||
}
|
||||
|
||||
return locals;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局 Blackboard 服务的引用
|
||||
*/
|
||||
static getGlobalBlackboard(): GlobalBlackboardService {
|
||||
return Core.services.resolve(GlobalBlackboardService);
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { CompositeType } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 复合节点组件
|
||||
*
|
||||
* 用于标识复合节点类型(Sequence, Selector, Parallel等)
|
||||
*/
|
||||
@ECSComponent('CompositeNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class CompositeNodeComponent extends Component {
|
||||
/** 复合节点类型 */
|
||||
@Serialize()
|
||||
compositeType: CompositeType = CompositeType.Sequence;
|
||||
|
||||
/** 随机化的子节点索引顺序 */
|
||||
protected shuffledIndices: number[] = [];
|
||||
|
||||
/** 是否在重启时重新洗牌(子类可选) */
|
||||
protected reshuffleOnRestart: boolean = true;
|
||||
|
||||
/**
|
||||
* 获取下一个子节点索引
|
||||
*/
|
||||
getNextChildIndex(currentIndex: number, totalChildren: number): number {
|
||||
// 对于随机类型,使用洗牌后的索引
|
||||
if (this.compositeType === CompositeType.RandomSequence ||
|
||||
this.compositeType === CompositeType.RandomSelector) {
|
||||
|
||||
// 首次执行或需要重新洗牌
|
||||
if (this.shuffledIndices.length === 0 || currentIndex === 0 && this.reshuffleOnRestart) {
|
||||
this.shuffleIndices(totalChildren);
|
||||
}
|
||||
|
||||
if (currentIndex < this.shuffledIndices.length) {
|
||||
return this.shuffledIndices[currentIndex];
|
||||
}
|
||||
return totalChildren; // 结束
|
||||
}
|
||||
|
||||
// 普通顺序执行
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 洗牌子节点索引
|
||||
*/
|
||||
private shuffleIndices(count: number): void {
|
||||
this.shuffledIndices = Array.from({ length: count }, (_, i) => i);
|
||||
|
||||
// Fisher-Yates 洗牌算法
|
||||
for (let i = this.shuffledIndices.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[this.shuffledIndices[i], this.shuffledIndices[j]] =
|
||||
[this.shuffledIndices[j], this.shuffledIndices[i]];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置洗牌状态
|
||||
*/
|
||||
resetShuffle(): void {
|
||||
this.shuffledIndices = [];
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { NodeType, CompositeType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { CompositeNodeComponent } from '../CompositeNodeComponent';
|
||||
|
||||
/**
|
||||
* 并行节点
|
||||
*
|
||||
* 同时执行所有子节点
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '并行',
|
||||
category: '组合',
|
||||
type: NodeType.Composite,
|
||||
icon: 'Layers',
|
||||
description: '同时执行所有子节点',
|
||||
color: '#CDDC39'
|
||||
})
|
||||
@ECSComponent('ParallelNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class ParallelNode extends CompositeNodeComponent {
|
||||
@BehaviorProperty({
|
||||
label: '成功策略',
|
||||
type: 'select',
|
||||
description: '多少个子节点成功时整体成功',
|
||||
options: [
|
||||
{ label: '全部成功', value: 'all' },
|
||||
{ label: '任意一个成功', value: 'one' }
|
||||
]
|
||||
})
|
||||
@Serialize()
|
||||
successPolicy: 'all' | 'one' = 'all';
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '失败策略',
|
||||
type: 'select',
|
||||
description: '多少个子节点失败时整体失败',
|
||||
options: [
|
||||
{ label: '任意一个失败', value: 'one' },
|
||||
{ label: '全部失败', value: 'all' }
|
||||
]
|
||||
})
|
||||
@Serialize()
|
||||
failurePolicy: 'one' | 'all' = 'one';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.compositeType = CompositeType.Parallel;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { NodeType, CompositeType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { CompositeNodeComponent } from '../CompositeNodeComponent';
|
||||
|
||||
/**
|
||||
* 并行选择节点
|
||||
*
|
||||
* 并行执行子节点,任一成功则成功
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '并行选择',
|
||||
category: '组合',
|
||||
type: NodeType.Composite,
|
||||
icon: 'Sparkles',
|
||||
description: '并行执行子节点,任一成功则成功',
|
||||
color: '#FFC107'
|
||||
})
|
||||
@ECSComponent('ParallelSelectorNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class ParallelSelectorNode extends CompositeNodeComponent {
|
||||
@BehaviorProperty({
|
||||
label: '失败策略',
|
||||
type: 'select',
|
||||
description: '多少个子节点失败时整体失败',
|
||||
options: [
|
||||
{ label: '任意一个失败', value: 'one' },
|
||||
{ label: '全部失败', value: 'all' }
|
||||
]
|
||||
})
|
||||
@Serialize()
|
||||
failurePolicy: 'one' | 'all' = 'all';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.compositeType = CompositeType.ParallelSelector;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { NodeType, CompositeType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { CompositeNodeComponent } from '../CompositeNodeComponent';
|
||||
|
||||
/**
|
||||
* 随机选择节点
|
||||
*
|
||||
* 随机顺序执行子节点选择
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '随机选择',
|
||||
category: '组合',
|
||||
type: NodeType.Composite,
|
||||
icon: 'Dices',
|
||||
description: '随机顺序执行子节点选择',
|
||||
color: '#F44336'
|
||||
})
|
||||
@ECSComponent('RandomSelectorNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class RandomSelectorNode extends CompositeNodeComponent {
|
||||
@BehaviorProperty({
|
||||
label: '重启时重新洗牌',
|
||||
type: 'boolean',
|
||||
description: '每次重启时是否重新随机子节点顺序'
|
||||
})
|
||||
@Serialize()
|
||||
override reshuffleOnRestart: boolean = true;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.compositeType = CompositeType.RandomSelector;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { NodeType, CompositeType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { CompositeNodeComponent } from '../CompositeNodeComponent';
|
||||
|
||||
/**
|
||||
* 随机序列节点
|
||||
*
|
||||
* 随机顺序执行子节点序列
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '随机序列',
|
||||
category: '组合',
|
||||
type: NodeType.Composite,
|
||||
icon: 'Shuffle',
|
||||
description: '随机顺序执行子节点序列',
|
||||
color: '#FF5722'
|
||||
})
|
||||
@ECSComponent('RandomSequenceNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class RandomSequenceNode extends CompositeNodeComponent {
|
||||
@BehaviorProperty({
|
||||
label: '重启时重新洗牌',
|
||||
type: 'boolean',
|
||||
description: '每次重启时是否重新随机子节点顺序'
|
||||
})
|
||||
@Serialize()
|
||||
override reshuffleOnRestart: boolean = true;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.compositeType = CompositeType.RandomSequence;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable } from '@esengine/ecs-framework';
|
||||
import { NodeType, CompositeType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { CompositeNodeComponent } from '../CompositeNodeComponent';
|
||||
|
||||
/**
|
||||
* 根节点
|
||||
*
|
||||
* 行为树的根节点,简单地激活第一个子节点
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '根节点',
|
||||
category: '根节点',
|
||||
type: NodeType.Composite,
|
||||
icon: 'TreePine',
|
||||
description: '行为树的根节点',
|
||||
color: '#FFD700'
|
||||
})
|
||||
@ECSComponent('RootNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class RootNode extends CompositeNodeComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.compositeType = CompositeType.Sequence;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { NodeType, CompositeType, AbortType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { CompositeNodeComponent } from '../CompositeNodeComponent';
|
||||
|
||||
/**
|
||||
* 选择节点
|
||||
*
|
||||
* 按顺序执行子节点,任一成功则成功
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '选择',
|
||||
category: '组合',
|
||||
type: NodeType.Composite,
|
||||
icon: 'GitBranch',
|
||||
description: '按顺序执行子节点,任一成功则成功',
|
||||
color: '#8BC34A'
|
||||
})
|
||||
@ECSComponent('SelectorNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class SelectorNode extends CompositeNodeComponent {
|
||||
@BehaviorProperty({
|
||||
label: '中止类型',
|
||||
type: 'select',
|
||||
description: '条件变化时的中止行为',
|
||||
options: [
|
||||
{ label: '无', value: 'none' },
|
||||
{ label: '自身', value: 'self' },
|
||||
{ label: '低优先级', value: 'lower-priority' },
|
||||
{ label: '两者', value: 'both' }
|
||||
]
|
||||
})
|
||||
@Serialize()
|
||||
abortType: AbortType = AbortType.None;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.compositeType = CompositeType.Selector;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { NodeType, CompositeType, AbortType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { CompositeNodeComponent } from '../CompositeNodeComponent';
|
||||
|
||||
/**
|
||||
* 序列节点
|
||||
*
|
||||
* 按顺序执行所有子节点,全部成功才成功
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '序列',
|
||||
category: '组合',
|
||||
type: NodeType.Composite,
|
||||
icon: 'List',
|
||||
description: '按顺序执行子节点,全部成功才成功',
|
||||
color: '#4CAF50'
|
||||
})
|
||||
@ECSComponent('SequenceNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class SequenceNode extends CompositeNodeComponent {
|
||||
@BehaviorProperty({
|
||||
label: '中止类型',
|
||||
type: 'select',
|
||||
description: '条件变化时的中止行为',
|
||||
options: [
|
||||
{ label: '无', value: 'none' },
|
||||
{ label: '自身', value: 'self' },
|
||||
{ label: '低优先级', value: 'lower-priority' },
|
||||
{ label: '两者', value: 'both' }
|
||||
]
|
||||
})
|
||||
@Serialize()
|
||||
abortType: AbortType = AbortType.None;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.compositeType = CompositeType.Sequence;
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import { ECSComponent, Serializable, Serialize, Entity } from '@esengine/ecs-framework';
|
||||
import { CompositeNodeComponent } from '../CompositeNodeComponent';
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
|
||||
/**
|
||||
* SubTree 节点 - 引用其他行为树作为子树
|
||||
*
|
||||
* 允许将其他行为树嵌入到当前树中,实现行为树的复用和模块化。
|
||||
*
|
||||
* 注意:SubTreeNode 是一个特殊的叶子节点,它不会执行编辑器中静态连接的子节点,
|
||||
* 只会执行从 assetId 动态加载的外部行为树文件。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const subTree = entity.addComponent(SubTreeNode);
|
||||
* subTree.assetId = 'patrol';
|
||||
* subTree.inheritParentBlackboard = true;
|
||||
* ```
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '子树',
|
||||
category: '组合',
|
||||
type: NodeType.Composite,
|
||||
icon: 'GitBranch',
|
||||
description: '引用并执行外部行为树文件(不支持静态子节点)',
|
||||
color: '#FF9800',
|
||||
requiresChildren: false
|
||||
})
|
||||
@ECSComponent('SubTreeNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class SubTreeNode extends CompositeNodeComponent {
|
||||
/**
|
||||
* 引用的子树资产ID
|
||||
* 逻辑标识符,例如 'patrol' 或 'ai/patrol'
|
||||
* 实际的文件路径由 AssetLoader 决定
|
||||
*/
|
||||
@BehaviorProperty({
|
||||
label: '资产ID',
|
||||
type: 'asset',
|
||||
description: '要引用的行为树资产ID'
|
||||
})
|
||||
@Serialize()
|
||||
assetId: string = '';
|
||||
|
||||
/**
|
||||
* 是否将父黑板传递给子树
|
||||
*
|
||||
* - true: 子树可以访问和修改父树的黑板变量
|
||||
* - false: 子树使用独立的黑板实例
|
||||
*/
|
||||
@BehaviorProperty({
|
||||
label: '继承父黑板',
|
||||
type: 'boolean',
|
||||
description: '子树是否可以访问父树的黑板变量'
|
||||
})
|
||||
@Serialize()
|
||||
inheritParentBlackboard: boolean = true;
|
||||
|
||||
/**
|
||||
* 子树执行失败时是否传播失败状态
|
||||
*
|
||||
* - true: 子树失败时,SubTree 节点返回 Failure
|
||||
* - false: 子树失败时,SubTree 节点返回 Success(忽略失败)
|
||||
*/
|
||||
@BehaviorProperty({
|
||||
label: '传播失败',
|
||||
type: 'boolean',
|
||||
description: '子树失败时是否传播失败状态'
|
||||
})
|
||||
@Serialize()
|
||||
propagateFailure: boolean = true;
|
||||
|
||||
/**
|
||||
* 是否在行为树启动时预加载子树
|
||||
*
|
||||
* - true: 在根节点开始执行前预加载此子树,确保执行时子树已就绪
|
||||
* - false: 运行时异步加载,执行到此节点时才开始加载(可能会有延迟)
|
||||
*/
|
||||
@BehaviorProperty({
|
||||
label: '预加载',
|
||||
type: 'boolean',
|
||||
description: '在行为树启动时预加载子树,避免运行时加载延迟'
|
||||
})
|
||||
@Serialize()
|
||||
preload: boolean = true;
|
||||
|
||||
/**
|
||||
* 子树的根实体(运行时)
|
||||
* 在执行时动态创建,执行结束后销毁
|
||||
*/
|
||||
private subTreeRoot?: Entity;
|
||||
|
||||
/**
|
||||
* 子树是否已完成
|
||||
*/
|
||||
private subTreeCompleted: boolean = false;
|
||||
|
||||
/**
|
||||
* 子树的最终状态
|
||||
*/
|
||||
private subTreeResult: TaskStatus = TaskStatus.Invalid;
|
||||
|
||||
/**
|
||||
* 获取子树根实体
|
||||
*/
|
||||
getSubTreeRoot(): Entity | undefined {
|
||||
return this.subTreeRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置子树根实体(由执行系统调用)
|
||||
*/
|
||||
setSubTreeRoot(root: Entity | undefined): void {
|
||||
this.subTreeRoot = root;
|
||||
this.subTreeCompleted = false;
|
||||
this.subTreeResult = TaskStatus.Invalid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记子树完成(由执行系统调用)
|
||||
*/
|
||||
markSubTreeCompleted(result: TaskStatus): void {
|
||||
this.subTreeCompleted = true;
|
||||
this.subTreeResult = result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查子树是否已完成
|
||||
*/
|
||||
isSubTreeCompleted(): boolean {
|
||||
return this.subTreeCompleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子树执行结果
|
||||
*/
|
||||
getSubTreeResult(): TaskStatus {
|
||||
return this.subTreeResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置子树状态
|
||||
*/
|
||||
reset(): void {
|
||||
this.subTreeRoot = undefined;
|
||||
this.subTreeCompleted = false;
|
||||
this.subTreeResult = TaskStatus.Invalid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置完成状态(用于复用预加载的子树)
|
||||
* 保留子树根引用,只重置完成标记
|
||||
*/
|
||||
resetCompletionState(): void {
|
||||
this.subTreeCompleted = false;
|
||||
this.subTreeResult = TaskStatus.Invalid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置
|
||||
*/
|
||||
validate(): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!this.assetId || this.assetId.trim() === '') {
|
||||
errors.push('SubTree 节点必须指定资产ID');
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { NodeType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
|
||||
/**
|
||||
* 比较运算符
|
||||
*/
|
||||
export enum CompareOperator {
|
||||
/** 等于 */
|
||||
Equal = 'equal',
|
||||
/** 不等于 */
|
||||
NotEqual = 'notEqual',
|
||||
/** 大于 */
|
||||
Greater = 'greater',
|
||||
/** 大于等于 */
|
||||
GreaterOrEqual = 'greaterOrEqual',
|
||||
/** 小于 */
|
||||
Less = 'less',
|
||||
/** 小于等于 */
|
||||
LessOrEqual = 'lessOrEqual',
|
||||
/** 包含(字符串/数组) */
|
||||
Contains = 'contains',
|
||||
/** 正则匹配 */
|
||||
Matches = 'matches'
|
||||
}
|
||||
|
||||
/**
|
||||
* 黑板变量比较条件组件
|
||||
*
|
||||
* 比较黑板变量与指定值或另一个变量
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '比较变量',
|
||||
category: '条件',
|
||||
type: NodeType.Condition,
|
||||
icon: 'Scale',
|
||||
description: '比较黑板变量与指定值',
|
||||
color: '#2196F3'
|
||||
})
|
||||
@ECSComponent('BlackboardCompareCondition')
|
||||
@Serializable({ version: 1 })
|
||||
export class BlackboardCompareCondition extends Component {
|
||||
@BehaviorProperty({
|
||||
label: '变量名',
|
||||
type: 'variable',
|
||||
required: true
|
||||
})
|
||||
@Serialize()
|
||||
variableName: string = '';
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '运算符',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '等于', value: 'equal' },
|
||||
{ label: '不等于', value: 'notEqual' },
|
||||
{ label: '大于', value: 'greater' },
|
||||
{ label: '大于等于', value: 'greaterOrEqual' },
|
||||
{ label: '小于', value: 'less' },
|
||||
{ label: '小于等于', value: 'lessOrEqual' },
|
||||
{ label: '包含', value: 'contains' },
|
||||
{ label: '正则匹配', value: 'matches' }
|
||||
]
|
||||
})
|
||||
@Serialize()
|
||||
operator: CompareOperator = CompareOperator.Equal;
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '比较值',
|
||||
type: 'string',
|
||||
description: '可以是固定值或变量引用 {{varName}}'
|
||||
})
|
||||
@Serialize()
|
||||
compareValue: any = null;
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '反转结果',
|
||||
type: 'boolean'
|
||||
})
|
||||
@Serialize()
|
||||
invertResult: boolean = false;
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { NodeType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
|
||||
/**
|
||||
* 黑板变量存在性检查条件组件
|
||||
*
|
||||
* 检查黑板变量是否存在
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '检查变量存在',
|
||||
category: '条件',
|
||||
type: NodeType.Condition,
|
||||
icon: 'Search',
|
||||
description: '检查黑板变量是否存在',
|
||||
color: '#00BCD4'
|
||||
})
|
||||
@ECSComponent('BlackboardExistsCondition')
|
||||
@Serializable({ version: 1 })
|
||||
export class BlackboardExistsCondition extends Component {
|
||||
@BehaviorProperty({
|
||||
label: '变量名',
|
||||
type: 'variable',
|
||||
required: true
|
||||
})
|
||||
@Serialize()
|
||||
variableName: string = '';
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '检查非空',
|
||||
type: 'boolean',
|
||||
description: '检查值不为 null/undefined'
|
||||
})
|
||||
@Serialize()
|
||||
checkNotNull: boolean = false;
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '反转结果',
|
||||
type: 'boolean',
|
||||
description: '检查不存在'
|
||||
})
|
||||
@Serialize()
|
||||
invertResult: boolean = false;
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Component, ECSComponent, Entity } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
|
||||
import { NodeType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { BlackboardComponent } from '../BlackboardComponent';
|
||||
|
||||
/**
|
||||
* 自定义条件函数类型
|
||||
*/
|
||||
export type CustomConditionFunction = (
|
||||
entity: Entity,
|
||||
blackboard?: BlackboardComponent,
|
||||
deltaTime?: number
|
||||
) => boolean;
|
||||
|
||||
/**
|
||||
* 执行自定义条件组件
|
||||
*
|
||||
* 允许用户提供自定义的条件检查函数
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '自定义条件',
|
||||
category: '条件',
|
||||
type: NodeType.Condition,
|
||||
icon: 'Code',
|
||||
description: '执行自定义条件代码',
|
||||
color: '#9C27B0'
|
||||
})
|
||||
@ECSComponent('ExecuteCondition')
|
||||
@Serializable({ version: 1 })
|
||||
export class ExecuteCondition extends Component {
|
||||
@BehaviorProperty({
|
||||
label: '条件代码',
|
||||
type: 'code',
|
||||
description: 'JavaScript 代码,返回 boolean',
|
||||
required: true
|
||||
})
|
||||
@Serialize()
|
||||
conditionCode?: string;
|
||||
|
||||
@Serialize()
|
||||
parameters: Record<string, any> = {};
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '反转结果',
|
||||
type: 'boolean'
|
||||
})
|
||||
@Serialize()
|
||||
invertResult: boolean = false;
|
||||
|
||||
/** 编译后的函数(不序列化) */
|
||||
@IgnoreSerialization()
|
||||
private compiledFunction?: CustomConditionFunction;
|
||||
|
||||
/**
|
||||
* 获取或编译条件函数
|
||||
*/
|
||||
getFunction(): CustomConditionFunction | undefined {
|
||||
if (!this.compiledFunction && this.conditionCode) {
|
||||
try {
|
||||
const func = new Function(
|
||||
'entity',
|
||||
'blackboard',
|
||||
'deltaTime',
|
||||
'parameters',
|
||||
`
|
||||
try {
|
||||
${this.conditionCode}
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
this.compiledFunction = (entity, blackboard, deltaTime) => {
|
||||
return Boolean(func(entity, blackboard, deltaTime, this.parameters));
|
||||
};
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return this.compiledFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自定义函数(运行时使用)
|
||||
*/
|
||||
setFunction(func: CustomConditionFunction): void {
|
||||
this.compiledFunction = func;
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { NodeType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
|
||||
/**
|
||||
* 随机概率条件组件
|
||||
*
|
||||
* 根据概率返回成功或失败
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '随机概率',
|
||||
category: '条件',
|
||||
type: NodeType.Condition,
|
||||
icon: 'Dice',
|
||||
description: '根据概率返回成功或失败',
|
||||
color: '#E91E63'
|
||||
})
|
||||
@ECSComponent('RandomProbabilityCondition')
|
||||
@Serializable({ version: 1 })
|
||||
export class RandomProbabilityCondition extends Component {
|
||||
@BehaviorProperty({
|
||||
label: '成功概率',
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
description: '0.0 - 1.0',
|
||||
required: true
|
||||
})
|
||||
@Serialize()
|
||||
probability: number = 0.5;
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '总是重新随机',
|
||||
type: 'boolean',
|
||||
description: 'false则第一次随机后固定结果'
|
||||
})
|
||||
@Serialize()
|
||||
alwaysRandomize: boolean = true;
|
||||
|
||||
/** 缓存的随机结果(不序列化) */
|
||||
private cachedResult?: boolean;
|
||||
|
||||
/**
|
||||
* 评估随机概率
|
||||
*/
|
||||
evaluate(): boolean {
|
||||
if (this.alwaysRandomize || this.cachedResult === undefined) {
|
||||
this.cachedResult = Math.random() < this.probability;
|
||||
}
|
||||
return this.cachedResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置缓存
|
||||
*/
|
||||
reset(): void {
|
||||
this.cachedResult = undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { DecoratorType } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 装饰器节点组件基类
|
||||
*
|
||||
* 只包含通用的装饰器类型标识
|
||||
* 具体的属性由各个子类自己定义
|
||||
*/
|
||||
@ECSComponent('DecoratorNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class DecoratorNodeComponent extends Component {
|
||||
/** 装饰器类型 */
|
||||
@Serialize()
|
||||
decoratorType: DecoratorType = DecoratorType.Inverter;
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable } from '@esengine/ecs-framework';
|
||||
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
|
||||
|
||||
/**
|
||||
* 总是失败节点
|
||||
*
|
||||
* 无论子节点结果如何都返回失败
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '总是失败',
|
||||
category: '装饰器',
|
||||
type: NodeType.Decorator,
|
||||
icon: 'ThumbsDown',
|
||||
description: '无论子节点结果如何都返回失败',
|
||||
color: '#FF5722'
|
||||
})
|
||||
@ECSComponent('AlwaysFailNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class AlwaysFailNode extends DecoratorNodeComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.decoratorType = DecoratorType.AlwaysFail;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable } from '@esengine/ecs-framework';
|
||||
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
|
||||
|
||||
/**
|
||||
* 总是成功节点
|
||||
*
|
||||
* 无论子节点结果如何都返回成功
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '总是成功',
|
||||
category: '装饰器',
|
||||
type: NodeType.Decorator,
|
||||
icon: 'ThumbsUp',
|
||||
description: '无论子节点结果如何都返回成功',
|
||||
color: '#8BC34A'
|
||||
})
|
||||
@ECSComponent('AlwaysSucceedNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class AlwaysSucceedNode extends DecoratorNodeComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.decoratorType = DecoratorType.AlwaysSucceed;
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { ECSComponent, Entity } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
|
||||
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
|
||||
import { BlackboardComponent } from '../BlackboardComponent';
|
||||
|
||||
/**
|
||||
* 条件装饰器节点
|
||||
*
|
||||
* 基于条件判断是否执行子节点
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '条件装饰器',
|
||||
category: '装饰器',
|
||||
type: NodeType.Decorator,
|
||||
icon: 'Filter',
|
||||
description: '基于条件判断是否执行子节点',
|
||||
color: '#3F51B5'
|
||||
})
|
||||
@ECSComponent('ConditionalNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class ConditionalNode extends DecoratorNodeComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.decoratorType = DecoratorType.Conditional;
|
||||
}
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '条件代码',
|
||||
type: 'code',
|
||||
description: 'JavaScript 代码,返回 boolean',
|
||||
required: true
|
||||
})
|
||||
@Serialize()
|
||||
conditionCode?: string;
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '重新评估条件',
|
||||
type: 'boolean',
|
||||
description: '每次执行时是否重新评估条件'
|
||||
})
|
||||
@Serialize()
|
||||
shouldReevaluate: boolean = true;
|
||||
|
||||
/** 编译后的条件函数(不序列化) */
|
||||
@IgnoreSerialization()
|
||||
private compiledCondition?: (entity: Entity, blackboard?: BlackboardComponent) => boolean;
|
||||
|
||||
/**
|
||||
* 评估条件
|
||||
*/
|
||||
evaluateCondition(entity: Entity, blackboard?: BlackboardComponent): boolean {
|
||||
if (!this.conditionCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.compiledCondition) {
|
||||
try {
|
||||
const func = new Function(
|
||||
'entity',
|
||||
'blackboard',
|
||||
`
|
||||
try {
|
||||
return Boolean(${this.conditionCode});
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
this.compiledCondition = (entity, blackboard) => {
|
||||
return Boolean(func(entity, blackboard));
|
||||
};
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return this.compiledCondition(entity, blackboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置条件函数(运行时使用)
|
||||
*/
|
||||
setConditionFunction(func: (entity: Entity, blackboard?: BlackboardComponent) => boolean): void {
|
||||
this.compiledCondition = func;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
|
||||
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
|
||||
|
||||
/**
|
||||
* 冷却节点
|
||||
*
|
||||
* 在冷却时间内阻止子节点执行
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '冷却',
|
||||
category: '装饰器',
|
||||
type: NodeType.Decorator,
|
||||
icon: 'Timer',
|
||||
description: '在冷却时间内阻止子节点执行',
|
||||
color: '#00BCD4'
|
||||
})
|
||||
@ECSComponent('CooldownNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class CooldownNode extends DecoratorNodeComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.decoratorType = DecoratorType.Cooldown;
|
||||
}
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '冷却时间',
|
||||
type: 'number',
|
||||
min: 0,
|
||||
step: 0.1,
|
||||
description: '冷却时间(秒)',
|
||||
required: true
|
||||
})
|
||||
@Serialize()
|
||||
cooldownTime: number = 1.0;
|
||||
|
||||
/** 上次执行时间 */
|
||||
@IgnoreSerialization()
|
||||
lastExecutionTime: number = 0;
|
||||
|
||||
/**
|
||||
* 检查是否可以执行
|
||||
*/
|
||||
canExecute(currentTime: number): boolean {
|
||||
// 如果从未执行过,允许执行
|
||||
if (this.lastExecutionTime === 0) {
|
||||
return true;
|
||||
}
|
||||
return currentTime - this.lastExecutionTime >= this.cooldownTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录执行时间
|
||||
*/
|
||||
recordExecution(currentTime: number): void {
|
||||
this.lastExecutionTime = currentTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
reset(): void {
|
||||
this.lastExecutionTime = 0;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable } from '@esengine/ecs-framework';
|
||||
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
|
||||
|
||||
/**
|
||||
* 反转节点
|
||||
*
|
||||
* 反转子节点的执行结果
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '反转',
|
||||
category: '装饰器',
|
||||
type: NodeType.Decorator,
|
||||
icon: 'RotateCcw',
|
||||
description: '反转子节点的执行结果',
|
||||
color: '#607D8B'
|
||||
})
|
||||
@ECSComponent('InverterNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class InverterNode extends DecoratorNodeComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.decoratorType = DecoratorType.Inverter;
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
|
||||
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
|
||||
|
||||
/**
|
||||
* 重复节点
|
||||
*
|
||||
* 重复执行子节点指定次数
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '重复',
|
||||
category: '装饰器',
|
||||
type: NodeType.Decorator,
|
||||
icon: 'Repeat',
|
||||
description: '重复执行子节点指定次数',
|
||||
color: '#9E9E9E'
|
||||
})
|
||||
@ECSComponent('RepeaterNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class RepeaterNode extends DecoratorNodeComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.decoratorType = DecoratorType.Repeater;
|
||||
}
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '重复次数',
|
||||
type: 'number',
|
||||
min: -1,
|
||||
step: 1,
|
||||
description: '-1表示无限重复',
|
||||
required: true
|
||||
})
|
||||
@Serialize()
|
||||
repeatCount: number = 1;
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '失败时停止',
|
||||
type: 'boolean',
|
||||
description: '子节点失败时是否停止重复'
|
||||
})
|
||||
@Serialize()
|
||||
endOnFailure: boolean = false;
|
||||
|
||||
/** 当前已重复次数 */
|
||||
@IgnoreSerialization()
|
||||
currentRepeatCount: number = 0;
|
||||
|
||||
/**
|
||||
* 增加重复计数
|
||||
*/
|
||||
incrementRepeat(): void {
|
||||
this.currentRepeatCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该继续重复
|
||||
*/
|
||||
shouldContinueRepeat(): boolean {
|
||||
if (this.repeatCount === -1) {
|
||||
return true;
|
||||
}
|
||||
return this.currentRepeatCount < this.repeatCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
reset(): void {
|
||||
this.currentRepeatCount = 0;
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
|
||||
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
|
||||
|
||||
/**
|
||||
* 超时节点
|
||||
*
|
||||
* 子节点执行超时则返回失败
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '超时',
|
||||
category: '装饰器',
|
||||
type: NodeType.Decorator,
|
||||
icon: 'Clock',
|
||||
description: '子节点执行超时则返回失败',
|
||||
color: '#FF9800'
|
||||
})
|
||||
@ECSComponent('TimeoutNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class TimeoutNode extends DecoratorNodeComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.decoratorType = DecoratorType.Timeout;
|
||||
}
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '超时时间',
|
||||
type: 'number',
|
||||
min: 0,
|
||||
step: 0.1,
|
||||
description: '超时时间(秒)',
|
||||
required: true
|
||||
})
|
||||
@Serialize()
|
||||
timeoutDuration: number = 5.0;
|
||||
|
||||
/** 开始执行时间 */
|
||||
@IgnoreSerialization()
|
||||
startTime: number = 0;
|
||||
|
||||
/**
|
||||
* 记录开始时间
|
||||
*/
|
||||
recordStartTime(currentTime: number): void {
|
||||
if (this.startTime === 0) {
|
||||
this.startTime = currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否超时
|
||||
*/
|
||||
isTimeout(currentTime: number): boolean {
|
||||
if (this.startTime === 0) {
|
||||
return false;
|
||||
}
|
||||
return currentTime - this.startTime >= this.timeoutDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
reset(): void {
|
||||
this.startTime = 0;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable } from '@esengine/ecs-framework';
|
||||
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
|
||||
|
||||
/**
|
||||
* 直到失败节点
|
||||
*
|
||||
* 重复执行子节点直到失败
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '直到失败',
|
||||
category: '装饰器',
|
||||
type: NodeType.Decorator,
|
||||
icon: 'XCircle',
|
||||
description: '重复执行子节点直到失败',
|
||||
color: '#F44336'
|
||||
})
|
||||
@ECSComponent('UntilFailNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class UntilFailNode extends DecoratorNodeComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.decoratorType = DecoratorType.UntilFail;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable } from '@esengine/ecs-framework';
|
||||
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
|
||||
|
||||
/**
|
||||
* 直到成功节点
|
||||
*
|
||||
* 重复执行子节点直到成功
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '直到成功',
|
||||
category: '装饰器',
|
||||
type: NodeType.Decorator,
|
||||
icon: 'CheckCircle',
|
||||
description: '重复执行子节点直到成功',
|
||||
color: '#4CAF50'
|
||||
})
|
||||
@ECSComponent('UntilSuccessNode')
|
||||
@Serializable({ version: 1 })
|
||||
export class UntilSuccessNode extends DecoratorNodeComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.decoratorType = DecoratorType.UntilSuccess;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 日志输出组件
|
||||
*
|
||||
* 存储运行时输出的日志信息,用于在UI中显示
|
||||
*/
|
||||
@ECSComponent('LogOutput')
|
||||
export class LogOutput extends Component {
|
||||
/**
|
||||
* 日志消息列表
|
||||
*/
|
||||
messages: Array<{
|
||||
timestamp: number;
|
||||
message: string;
|
||||
level: 'log' | 'info' | 'warn' | 'error';
|
||||
}> = [];
|
||||
|
||||
/**
|
||||
* 添加日志消息
|
||||
*/
|
||||
addMessage(message: string, level: 'log' | 'info' | 'warn' | 'error' = 'log'): void {
|
||||
this.messages.push({
|
||||
timestamp: Date.now(),
|
||||
message,
|
||||
level
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空日志
|
||||
*/
|
||||
clear(): void {
|
||||
this.messages = [];
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 属性绑定组件
|
||||
* 记录节点属性到黑板变量的绑定关系
|
||||
*/
|
||||
export class PropertyBindings extends Component {
|
||||
/**
|
||||
* 属性绑定映射
|
||||
* key: 属性名称 (如 'message')
|
||||
* value: 黑板变量名 (如 'test1')
|
||||
*/
|
||||
bindings: Map<string, string> = new Map();
|
||||
|
||||
/**
|
||||
* 添加属性绑定
|
||||
*/
|
||||
addBinding(propertyName: string, blackboardKey: string): void {
|
||||
this.bindings.set(propertyName, blackboardKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性绑定的黑板变量名
|
||||
*/
|
||||
getBinding(propertyName: string): string | undefined {
|
||||
return this.bindings.get(propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查属性是否绑定到黑板变量
|
||||
*/
|
||||
hasBinding(propertyName: string): boolean {
|
||||
return this.bindings.has(propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有绑定
|
||||
*/
|
||||
clearBindings(): void {
|
||||
this.bindings.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
import { NodeTemplate, PropertyDefinition } from '../Serialization/NodeTemplates';
|
||||
import { NodeType } from '../Types/TaskStatus';
|
||||
import { getComponentTypeName } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 行为树节点元数据
|
||||
*/
|
||||
export interface BehaviorNodeMetadata {
|
||||
displayName: string;
|
||||
category: string;
|
||||
type: NodeType;
|
||||
icon?: string;
|
||||
description: string;
|
||||
color?: string;
|
||||
className?: string;
|
||||
/**
|
||||
* 是否需要子节点
|
||||
* - true: 节点需要子节点(如 SequenceNode、DecoratorNode)
|
||||
* - false: 节点不需要子节点(如 ActionNode、SubTreeNode)
|
||||
* - undefined: 根据节点类型自动判断
|
||||
*/
|
||||
requiresChildren?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点类注册表
|
||||
*/
|
||||
class NodeClassRegistry {
|
||||
private static nodeClasses = new Map<string, {
|
||||
metadata: BehaviorNodeMetadata;
|
||||
constructor: any;
|
||||
}>();
|
||||
|
||||
static registerNodeClass(constructor: any, metadata: BehaviorNodeMetadata): void {
|
||||
const key = `${metadata.category}:${metadata.displayName}`;
|
||||
this.nodeClasses.set(key, { metadata, constructor });
|
||||
}
|
||||
|
||||
static getAllNodeClasses(): Array<{ metadata: BehaviorNodeMetadata; constructor: any }> {
|
||||
return Array.from(this.nodeClasses.values());
|
||||
}
|
||||
|
||||
static getNodeClass(category: string, displayName: string): any {
|
||||
const key = `${category}:${displayName}`;
|
||||
return this.nodeClasses.get(key)?.constructor;
|
||||
}
|
||||
|
||||
static clear(): void {
|
||||
this.nodeClasses.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树节点装饰器
|
||||
*
|
||||
* 用于标注一个类是可在编辑器中使用的行为树节点
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @BehaviorNode({
|
||||
* displayName: '等待',
|
||||
* category: '动作',
|
||||
* type: NodeType.Action,
|
||||
* icon: 'Clock',
|
||||
* description: '等待指定时间',
|
||||
* color: '#9E9E9E'
|
||||
* })
|
||||
* class WaitNode extends Component {
|
||||
* @BehaviorProperty({
|
||||
* label: '持续时间',
|
||||
* type: 'number',
|
||||
* min: 0,
|
||||
* step: 0.1,
|
||||
* description: '等待时间(秒)'
|
||||
* })
|
||||
* duration: number = 1.0;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function BehaviorNode(metadata: BehaviorNodeMetadata) {
|
||||
return function <T extends { new (...args: any[]): any }>(constructor: T) {
|
||||
const metadataWithClassName = {
|
||||
...metadata,
|
||||
className: getComponentTypeName(constructor as any)
|
||||
};
|
||||
NodeClassRegistry.registerNodeClass(constructor, metadataWithClassName);
|
||||
return constructor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树属性装饰器
|
||||
*
|
||||
* 用于标注节点的可配置属性,这些属性会在编辑器中显示
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @BehaviorNode({ ... })
|
||||
* class MyNode {
|
||||
* @BehaviorProperty({
|
||||
* label: '速度',
|
||||
* type: 'number',
|
||||
* min: 0,
|
||||
* max: 100,
|
||||
* description: '移动速度'
|
||||
* })
|
||||
* speed: number = 10;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function BehaviorProperty(config: Omit<PropertyDefinition, 'name' | 'defaultValue'>) {
|
||||
return function (target: any, propertyKey: string) {
|
||||
if (!target.constructor.__nodeProperties) {
|
||||
target.constructor.__nodeProperties = [];
|
||||
}
|
||||
target.constructor.__nodeProperties.push({
|
||||
name: propertyKey,
|
||||
...config
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 使用 BehaviorProperty 代替
|
||||
*/
|
||||
export const NodeProperty = BehaviorProperty;
|
||||
|
||||
/**
|
||||
* 获取所有注册的节点模板
|
||||
*/
|
||||
export function getRegisteredNodeTemplates(): NodeTemplate[] {
|
||||
return NodeClassRegistry.getAllNodeClasses().map(({ metadata, constructor }) => {
|
||||
const propertyDefs = constructor.__nodeProperties || [];
|
||||
|
||||
const defaultConfig: any = {
|
||||
nodeType: metadata.type.toLowerCase()
|
||||
};
|
||||
|
||||
const instance = new constructor();
|
||||
const properties: PropertyDefinition[] = propertyDefs.map((prop: PropertyDefinition) => {
|
||||
const defaultValue = instance[prop.name];
|
||||
if (defaultValue !== undefined) {
|
||||
defaultConfig[prop.name] = defaultValue;
|
||||
}
|
||||
return {
|
||||
...prop,
|
||||
defaultValue: defaultValue !== undefined ? defaultValue : prop.defaultValue
|
||||
};
|
||||
});
|
||||
|
||||
switch (metadata.type) {
|
||||
case NodeType.Composite:
|
||||
defaultConfig.compositeType = metadata.displayName;
|
||||
break;
|
||||
case NodeType.Decorator:
|
||||
defaultConfig.decoratorType = metadata.displayName;
|
||||
break;
|
||||
case NodeType.Action:
|
||||
defaultConfig.actionType = metadata.displayName;
|
||||
break;
|
||||
case NodeType.Condition:
|
||||
defaultConfig.conditionType = metadata.displayName;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
type: metadata.type,
|
||||
displayName: metadata.displayName,
|
||||
category: metadata.category,
|
||||
icon: metadata.icon,
|
||||
description: metadata.description,
|
||||
color: metadata.color,
|
||||
className: metadata.className,
|
||||
componentClass: constructor,
|
||||
requiresChildren: metadata.requiresChildren,
|
||||
defaultConfig,
|
||||
properties
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有注册的节点类
|
||||
*/
|
||||
export function clearRegisteredNodes(): void {
|
||||
NodeClassRegistry.clear();
|
||||
}
|
||||
|
||||
export { NodeClassRegistry };
|
||||
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* 注册所有内置节点
|
||||
*
|
||||
* 导入所有节点类以确保装饰器被执行
|
||||
*/
|
||||
|
||||
// Actions
|
||||
import './Components/Actions/ExecuteAction';
|
||||
import './Components/Actions/WaitAction';
|
||||
import './Components/Actions/LogAction';
|
||||
import './Components/Actions/SetBlackboardValueAction';
|
||||
import './Components/Actions/ModifyBlackboardValueAction';
|
||||
|
||||
// Conditions
|
||||
import './Components/Conditions/BlackboardCompareCondition';
|
||||
import './Components/Conditions/BlackboardExistsCondition';
|
||||
import './Components/Conditions/RandomProbabilityCondition';
|
||||
import './Components/Conditions/ExecuteCondition';
|
||||
|
||||
// Composites
|
||||
import './Components/Composites/SequenceNode';
|
||||
import './Components/Composites/SelectorNode';
|
||||
import './Components/Composites/ParallelNode';
|
||||
import './Components/Composites/ParallelSelectorNode';
|
||||
import './Components/Composites/RandomSequenceNode';
|
||||
import './Components/Composites/RandomSelectorNode';
|
||||
import './Components/Composites/SubTreeNode';
|
||||
|
||||
// Decorators
|
||||
import './Components/Decorators/InverterNode';
|
||||
import './Components/Decorators/RepeaterNode';
|
||||
import './Components/Decorators/UntilSuccessNode';
|
||||
import './Components/Decorators/UntilFailNode';
|
||||
import './Components/Decorators/AlwaysSucceedNode';
|
||||
import './Components/Decorators/AlwaysFailNode';
|
||||
import './Components/Decorators/ConditionalNode';
|
||||
import './Components/Decorators/CooldownNode';
|
||||
import './Components/Decorators/TimeoutNode';
|
||||
|
||||
/**
|
||||
* 确保所有节点已注册
|
||||
*/
|
||||
export function ensureAllNodesRegistered(): void {
|
||||
// 这个函数的调用会确保上面的 import 被执行
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { BehaviorTreeData } from './BehaviorTreeData';
|
||||
import { createLogger, IService } from '@esengine/ecs-framework';
|
||||
|
||||
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}个节点)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取行为树资产
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
99
packages/behavior-tree/src/Runtime/BehaviorTreeData.ts
Normal file
99
packages/behavior-tree/src/Runtime/BehaviorTreeData.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
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;
|
||||
|
||||
/** 开始执行时间(某些节点需要) */
|
||||
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
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { EntitySystem, Matcher, Entity, Time, Core, ECSSystem } from '@esengine/ecs-framework';
|
||||
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 assetManager: BehaviorTreeAssetManager;
|
||||
private executorRegistry: NodeExecutorRegistry;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.empty().all(BehaviorTreeRuntimeComponent));
|
||||
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
this.executorRegistry = new NodeExecutorRegistry();
|
||||
this.registerBuiltInExecutors();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册所有执行器(包括内置和插件提供的)
|
||||
*/
|
||||
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.assetManager.getAsset(runtime.treeAssetId);
|
||||
if (!treeData) {
|
||||
this.logger.warn(`未找到行为树资产: ${runtime.treeAssetId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果标记了需要重置,先重置状态
|
||||
if (runtime.needsReset) {
|
||||
runtime.resetAllStates();
|
||||
runtime.needsReset = false;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
import { Component, ECSComponent } 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()
|
||||
treeAssetId: string = '';
|
||||
|
||||
/**
|
||||
* 是否自动启动
|
||||
*/
|
||||
@Serialize()
|
||||
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();
|
||||
|
||||
/**
|
||||
* 获取节点运行时状态
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取黑板值
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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'
|
||||
})
|
||||
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]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
71
packages/behavior-tree/src/Runtime/Executors/LogAction.ts
Normal file
71
packages/behavior-tree/src/Runtime/Executors/LogAction.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
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']
|
||||
}
|
||||
}
|
||||
})
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
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'
|
||||
})
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
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: '子节点失败时是否结束'
|
||||
}
|
||||
}
|
||||
})
|
||||
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]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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'
|
||||
})
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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'
|
||||
})
|
||||
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;
|
||||
}
|
||||
}
|
||||
144
packages/behavior-tree/src/Runtime/Executors/ServiceDecorator.ts
Normal file
144
packages/behavior-tree/src/Runtime/Executors/ServiceDecorator.ts
Normal 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 };
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
161
packages/behavior-tree/src/Runtime/Executors/SubTreeExecutor.ts
Normal file
161
packages/behavior-tree/src/Runtime/Executors/SubTreeExecutor.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
packages/behavior-tree/src/Runtime/Executors/WaitAction.ts
Normal file
46
packages/behavior-tree/src/Runtime/Executors/WaitAction.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
30
packages/behavior-tree/src/Runtime/Executors/index.ts
Normal file
30
packages/behavior-tree/src/Runtime/Executors/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
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';
|
||||
181
packages/behavior-tree/src/Runtime/NodeExecutor.ts
Normal file
181
packages/behavior-tree/src/Runtime/NodeExecutor.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
79
packages/behavior-tree/src/Runtime/NodeMetadata.ts
Normal file
79
packages/behavior-tree/src/Runtime/NodeMetadata.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
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 NodeMetadata {
|
||||
implementationType: string;
|
||||
nodeType: NodeType;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
configSchema?: Record<string, ConfigFieldDefinition>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点元数据注册表
|
||||
*/
|
||||
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);
|
||||
};
|
||||
}
|
||||
8
packages/behavior-tree/src/Runtime/index.ts
Normal file
8
packages/behavior-tree/src/Runtime/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export { BehaviorTreeData, BehaviorNodeData, NodeRuntimeState, createDefaultRuntimeState } from './BehaviorTreeData';
|
||||
export { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
|
||||
export { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager';
|
||||
export { INodeExecutor, NodeExecutionContext, NodeExecutorRegistry, BindingHelper } from './NodeExecutor';
|
||||
export { BehaviorTreeExecutionSystem } from './BehaviorTreeExecutionSystem';
|
||||
export { NodeMetadata, ConfigFieldDefinition, NodeMetadataRegistry, NodeExecutorMetadata } from './NodeMetadata';
|
||||
|
||||
export * from './Executors';
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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} 个绑定`);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -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' }; // 灰色
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,382 +0,0 @@
|
||||
import { Entity, IService, createLogger } from '@esengine/ecs-framework';
|
||||
import {
|
||||
LoadingState,
|
||||
LoadingTask,
|
||||
LoadingTaskHandle,
|
||||
LoadingOptions,
|
||||
LoadingProgress,
|
||||
TimeoutError,
|
||||
CircularDependencyError,
|
||||
EntityDestroyedError
|
||||
} from './AssetLoadingTypes';
|
||||
|
||||
const logger = createLogger('AssetLoadingManager');
|
||||
|
||||
/**
|
||||
* 资产加载管理器
|
||||
*
|
||||
* 统一管理行为树资产的异步加载,提供:
|
||||
* - 超时检测和自动重试
|
||||
* - 循环引用检测
|
||||
* - 实体生命周期安全
|
||||
* - 加载状态追踪
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const manager = new AssetLoadingManager();
|
||||
*
|
||||
* const handle = manager.startLoading(
|
||||
* 'patrol',
|
||||
* parentEntity,
|
||||
* () => assetLoader.loadBehaviorTree('patrol'),
|
||||
* { timeoutMs: 5000, maxRetries: 3 }
|
||||
* );
|
||||
*
|
||||
* // 在系统的 process() 中轮询检查
|
||||
* const state = handle.getState();
|
||||
* if (state === LoadingState.Loaded) {
|
||||
* const entity = await handle.promise;
|
||||
* // 使用加载的实体
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class AssetLoadingManager implements IService {
|
||||
/** 正在进行的加载任务 */
|
||||
private tasks: Map<string, LoadingTask> = new Map();
|
||||
|
||||
/** 加载栈(用于循环检测) */
|
||||
private loadingStack: Set<string> = new Set();
|
||||
|
||||
/** 默认配置 */
|
||||
private defaultOptions: Required<Omit<LoadingOptions, 'parentAssetId'>> = {
|
||||
timeoutMs: 5000,
|
||||
maxRetries: 3,
|
||||
retryDelayBase: 100,
|
||||
maxRetryDelay: 2000
|
||||
};
|
||||
|
||||
/**
|
||||
* 开始加载资产
|
||||
*
|
||||
* @param assetId 资产ID
|
||||
* @param parentEntity 父实体(用于生命周期检查)
|
||||
* @param loader 加载函数
|
||||
* @param options 加载选项
|
||||
* @returns 加载任务句柄
|
||||
*/
|
||||
startLoading(
|
||||
assetId: string,
|
||||
parentEntity: Entity,
|
||||
loader: () => Promise<Entity>,
|
||||
options: LoadingOptions = {}
|
||||
): LoadingTaskHandle {
|
||||
// 合并选项
|
||||
const finalOptions = {
|
||||
...this.defaultOptions,
|
||||
...options
|
||||
};
|
||||
|
||||
// 循环引用检测
|
||||
if (options.parentAssetId) {
|
||||
if (this.detectCircularDependency(assetId, options.parentAssetId)) {
|
||||
const error = new CircularDependencyError(
|
||||
`检测到循环引用: ${options.parentAssetId} → ${assetId}\n` +
|
||||
`加载栈: ${Array.from(this.loadingStack).join(' → ')}`
|
||||
);
|
||||
logger.error(error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否已有任务
|
||||
const existingTask = this.tasks.get(assetId);
|
||||
if (existingTask) {
|
||||
logger.debug(`资产 ${assetId} 已在加载中,返回现有任务`);
|
||||
return this.createHandle(existingTask);
|
||||
}
|
||||
|
||||
// 创建新任务
|
||||
const task: LoadingTask = {
|
||||
assetId,
|
||||
promise: null as any, // 稍后设置
|
||||
startTime: Date.now(),
|
||||
lastRetryTime: 0,
|
||||
retryCount: 0,
|
||||
maxRetries: finalOptions.maxRetries,
|
||||
timeoutMs: finalOptions.timeoutMs,
|
||||
state: LoadingState.Pending,
|
||||
parentEntityId: parentEntity.id,
|
||||
parentEntity: parentEntity,
|
||||
parentAssetId: options.parentAssetId
|
||||
};
|
||||
|
||||
// 添加到加载栈(循环检测)
|
||||
this.loadingStack.add(assetId);
|
||||
|
||||
// 创建带超时和重试的Promise
|
||||
task.promise = this.loadWithTimeoutAndRetry(task, loader, finalOptions);
|
||||
task.state = LoadingState.Loading;
|
||||
|
||||
this.tasks.set(assetId, task);
|
||||
|
||||
logger.info(`开始加载资产: ${assetId}`, {
|
||||
timeoutMs: finalOptions.timeoutMs,
|
||||
maxRetries: finalOptions.maxRetries,
|
||||
parentAssetId: options.parentAssetId
|
||||
});
|
||||
|
||||
return this.createHandle(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带超时和重试的加载
|
||||
*/
|
||||
private async loadWithTimeoutAndRetry(
|
||||
task: LoadingTask,
|
||||
loader: () => Promise<Entity>,
|
||||
options: Required<Omit<LoadingOptions, 'parentAssetId'>>
|
||||
): Promise<Entity> {
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let attempt = 0; attempt <= task.maxRetries; attempt++) {
|
||||
// 检查父实体是否还存在
|
||||
if (task.parentEntity.isDestroyed) {
|
||||
const error = new EntityDestroyedError(
|
||||
`父实体已销毁,取消加载: ${task.assetId}`
|
||||
);
|
||||
task.state = LoadingState.Cancelled;
|
||||
this.cleanup(task.assetId);
|
||||
logger.warn(error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
task.retryCount = attempt;
|
||||
task.lastRetryTime = Date.now();
|
||||
|
||||
logger.debug(`加载尝试 ${attempt + 1}/${task.maxRetries + 1}: ${task.assetId}`);
|
||||
|
||||
// 使用超时包装
|
||||
const result = await this.withTimeout(
|
||||
loader(),
|
||||
task.timeoutMs,
|
||||
`加载资产 ${task.assetId} 超时(${task.timeoutMs}ms)`
|
||||
);
|
||||
|
||||
// 加载成功
|
||||
task.state = LoadingState.Loaded;
|
||||
task.result = result;
|
||||
this.cleanup(task.assetId);
|
||||
|
||||
logger.info(`资产加载成功: ${task.assetId}`, {
|
||||
attempts: attempt + 1,
|
||||
elapsedMs: Date.now() - task.startTime
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
// 记录错误类型
|
||||
if (error instanceof TimeoutError) {
|
||||
task.state = LoadingState.Timeout;
|
||||
logger.warn(`资产加载超时: ${task.assetId} (尝试 ${attempt + 1})`);
|
||||
} else if (error instanceof EntityDestroyedError) {
|
||||
// 实体已销毁,不需要重试
|
||||
throw error;
|
||||
} else {
|
||||
logger.warn(`资产加载失败: ${task.assetId} (尝试 ${attempt + 1})`, error);
|
||||
}
|
||||
|
||||
// 最后一次尝试失败
|
||||
if (attempt === task.maxRetries) {
|
||||
task.state = LoadingState.Failed;
|
||||
task.error = lastError;
|
||||
this.cleanup(task.assetId);
|
||||
|
||||
logger.error(`资产加载最终失败: ${task.assetId}`, {
|
||||
attempts: attempt + 1,
|
||||
error: lastError.message
|
||||
});
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
// 计算重试延迟(指数退避)
|
||||
const delayMs = Math.min(
|
||||
Math.pow(2, attempt) * options.retryDelayBase,
|
||||
options.maxRetryDelay
|
||||
);
|
||||
|
||||
logger.debug(`等待 ${delayMs}ms 后重试...`);
|
||||
await this.delay(delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise 超时包装
|
||||
*/
|
||||
private withTimeout<T>(
|
||||
promise: Promise<T>,
|
||||
timeoutMs: number,
|
||||
message: string
|
||||
): Promise<T> {
|
||||
let timeoutId: NodeJS.Timeout | number;
|
||||
|
||||
const timeoutPromise = new Promise<T>((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(new TimeoutError(message));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
return Promise.race([
|
||||
promise.then(result => {
|
||||
clearTimeout(timeoutId as any);
|
||||
return result;
|
||||
}),
|
||||
timeoutPromise
|
||||
]).catch(error => {
|
||||
clearTimeout(timeoutId as any);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 循环依赖检测
|
||||
*/
|
||||
private detectCircularDependency(assetId: string, parentAssetId: string): boolean {
|
||||
// 如果父资产正在加载中,说明有循环
|
||||
if (this.loadingStack.has(parentAssetId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: 更复杂的循环检测(检查完整的依赖链)
|
||||
// 当前只检测直接循环(A→B→A)
|
||||
// 未来可以检测间接循环(A→B→C→A)
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务状态
|
||||
*/
|
||||
getTaskState(assetId: string): LoadingState {
|
||||
return this.tasks.get(assetId)?.state ?? LoadingState.Idle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务
|
||||
*/
|
||||
getTask(assetId: string): LoadingTask | undefined {
|
||||
return this.tasks.get(assetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消加载
|
||||
*/
|
||||
cancelLoading(assetId: string): void {
|
||||
const task = this.tasks.get(assetId);
|
||||
if (task) {
|
||||
task.state = LoadingState.Cancelled;
|
||||
this.cleanup(assetId);
|
||||
logger.info(`取消加载: ${assetId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理任务
|
||||
*/
|
||||
private cleanup(assetId: string): void {
|
||||
const task = this.tasks.get(assetId);
|
||||
if (task) {
|
||||
// 清除实体引用,帮助GC
|
||||
(task as any).parentEntity = null;
|
||||
}
|
||||
this.tasks.delete(assetId);
|
||||
this.loadingStack.delete(assetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟
|
||||
*/
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建任务句柄
|
||||
*/
|
||||
private createHandle(task: LoadingTask): LoadingTaskHandle {
|
||||
return {
|
||||
assetId: task.assetId,
|
||||
|
||||
getState: () => task.state,
|
||||
|
||||
getError: () => task.error,
|
||||
|
||||
getProgress: (): LoadingProgress => {
|
||||
const now = Date.now();
|
||||
const elapsed = now - task.startTime;
|
||||
const remaining = Math.max(0, task.timeoutMs - elapsed);
|
||||
|
||||
return {
|
||||
state: task.state,
|
||||
elapsedMs: elapsed,
|
||||
remainingTimeoutMs: remaining,
|
||||
retryCount: task.retryCount,
|
||||
maxRetries: task.maxRetries
|
||||
};
|
||||
},
|
||||
|
||||
cancel: () => this.cancelLoading(task.assetId),
|
||||
|
||||
promise: task.promise
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有正在加载的资产
|
||||
*/
|
||||
getLoadingAssets(): string[] {
|
||||
return Array.from(this.tasks.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加载统计信息
|
||||
*/
|
||||
getStats(): {
|
||||
totalTasks: number;
|
||||
loadingTasks: number;
|
||||
failedTasks: number;
|
||||
timeoutTasks: number;
|
||||
} {
|
||||
const tasks = Array.from(this.tasks.values());
|
||||
|
||||
return {
|
||||
totalTasks: tasks.length,
|
||||
loadingTasks: tasks.filter(t => t.state === LoadingState.Loading).length,
|
||||
failedTasks: tasks.filter(t => t.state === LoadingState.Failed).length,
|
||||
timeoutTasks: tasks.filter(t => t.state === LoadingState.Timeout).length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有任务
|
||||
*/
|
||||
clear(): void {
|
||||
logger.info('清空所有加载任务', this.getStats());
|
||||
this.tasks.clear();
|
||||
this.loadingStack.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
dispose(): void {
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
import { Entity } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 资产加载状态
|
||||
*/
|
||||
export enum LoadingState {
|
||||
/** 未开始 */
|
||||
Idle = 'idle',
|
||||
/** 即将开始 */
|
||||
Pending = 'pending',
|
||||
/** 加载中 */
|
||||
Loading = 'loading',
|
||||
/** 加载成功 */
|
||||
Loaded = 'loaded',
|
||||
/** 加载失败 */
|
||||
Failed = 'failed',
|
||||
/** 加载超时 */
|
||||
Timeout = 'timeout',
|
||||
/** 已取消 */
|
||||
Cancelled = 'cancelled'
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载任务
|
||||
*/
|
||||
export interface LoadingTask {
|
||||
/** 资产ID */
|
||||
assetId: string;
|
||||
|
||||
/** 加载Promise */
|
||||
promise: Promise<Entity>;
|
||||
|
||||
/** 开始时间 */
|
||||
startTime: number;
|
||||
|
||||
/** 上次重试时间 */
|
||||
lastRetryTime: number;
|
||||
|
||||
/** 当前重试次数 */
|
||||
retryCount: number;
|
||||
|
||||
/** 最大重试次数 */
|
||||
maxRetries: number;
|
||||
|
||||
/** 超时时间(毫秒) */
|
||||
timeoutMs: number;
|
||||
|
||||
/** 当前状态 */
|
||||
state: LoadingState;
|
||||
|
||||
/** 错误信息 */
|
||||
error?: Error;
|
||||
|
||||
/** 父实体ID */
|
||||
parentEntityId: number;
|
||||
|
||||
/** 父实体引用(需要在使用前检查isDestroyed) */
|
||||
parentEntity: Entity;
|
||||
|
||||
/** 父资产ID(用于循环检测) */
|
||||
parentAssetId?: string;
|
||||
|
||||
/** 加载结果(缓存) */
|
||||
result?: Entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载任务句柄
|
||||
*/
|
||||
export interface LoadingTaskHandle {
|
||||
/** 资产ID */
|
||||
assetId: string;
|
||||
|
||||
/** 获取当前状态 */
|
||||
getState(): LoadingState;
|
||||
|
||||
/** 获取错误信息 */
|
||||
getError(): Error | undefined;
|
||||
|
||||
/** 获取加载进度信息 */
|
||||
getProgress(): LoadingProgress;
|
||||
|
||||
/** 取消加载 */
|
||||
cancel(): void;
|
||||
|
||||
/** 加载Promise */
|
||||
promise: Promise<Entity>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载进度信息
|
||||
*/
|
||||
export interface LoadingProgress {
|
||||
/** 当前状态 */
|
||||
state: LoadingState;
|
||||
|
||||
/** 已耗时(毫秒) */
|
||||
elapsedMs: number;
|
||||
|
||||
/** 剩余超时时间(毫秒) */
|
||||
remainingTimeoutMs: number;
|
||||
|
||||
/** 当前重试次数 */
|
||||
retryCount: number;
|
||||
|
||||
/** 最大重试次数 */
|
||||
maxRetries: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载选项
|
||||
*/
|
||||
export interface LoadingOptions {
|
||||
/** 超时时间(毫秒),默认5000 */
|
||||
timeoutMs?: number;
|
||||
|
||||
/** 最大重试次数,默认3 */
|
||||
maxRetries?: number;
|
||||
|
||||
/** 父资产ID(用于循环检测) */
|
||||
parentAssetId?: string;
|
||||
|
||||
/** 重试延迟基数(毫秒),默认100 */
|
||||
retryDelayBase?: number;
|
||||
|
||||
/** 最大重试延迟(毫秒),默认2000 */
|
||||
maxRetryDelay?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 超时错误
|
||||
*/
|
||||
export class TimeoutError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'TimeoutError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 循环依赖错误
|
||||
*/
|
||||
export class CircularDependencyError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'CircularDependencyError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体已销毁错误
|
||||
*/
|
||||
export class EntityDestroyedError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'EntityDestroyedError';
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
import type { IService } from '@esengine/ecs-framework';
|
||||
import { IAssetLoader } from './IAssetLoader';
|
||||
import { BehaviorTreeAsset } from '../Serialization/BehaviorTreeAsset';
|
||||
import { BehaviorTreeAssetSerializer, DeserializationOptions } from '../Serialization/BehaviorTreeAssetSerializer';
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
|
||||
const logger = createLogger('FileSystemAssetLoader');
|
||||
|
||||
/**
|
||||
* 文件系统资产加载器配置
|
||||
*/
|
||||
export interface FileSystemAssetLoaderConfig {
|
||||
/** 资产基础路径 */
|
||||
basePath: string;
|
||||
|
||||
/** 资产格式 */
|
||||
format: 'json' | 'binary';
|
||||
|
||||
/** 文件扩展名(可选,默认根据格式自动设置) */
|
||||
extension?: string;
|
||||
|
||||
/** 是否启用缓存 */
|
||||
enableCache?: boolean;
|
||||
|
||||
/** 自定义文件读取函数(可选) */
|
||||
readFile?: (path: string) => Promise<string | Uint8Array>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件系统资产加载器
|
||||
*
|
||||
* 从文件系统加载行为树资产,支持 JSON 和 Binary 格式。
|
||||
* 提供资产缓存和预加载功能。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 创建加载器
|
||||
* const loader = new FileSystemAssetLoader({
|
||||
* basePath: 'assets/behavior-trees',
|
||||
* format: 'json',
|
||||
* enableCache: true
|
||||
* });
|
||||
*
|
||||
* // 加载资产
|
||||
* const asset = await loader.loadBehaviorTree('patrol');
|
||||
* ```
|
||||
*/
|
||||
export class FileSystemAssetLoader implements IAssetLoader, IService {
|
||||
private config: Required<FileSystemAssetLoaderConfig>;
|
||||
private cache: Map<string, BehaviorTreeAsset> = new Map();
|
||||
|
||||
constructor(config: FileSystemAssetLoaderConfig) {
|
||||
this.config = {
|
||||
basePath: config.basePath,
|
||||
format: config.format,
|
||||
extension: config.extension || (config.format === 'json' ? '.btree.json' : '.btree.bin'),
|
||||
enableCache: config.enableCache ?? true,
|
||||
readFile: config.readFile || this.defaultReadFile.bind(this)
|
||||
};
|
||||
|
||||
// 规范化路径
|
||||
this.config.basePath = this.config.basePath.replace(/\\/g, '/').replace(/\/$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载行为树资产
|
||||
*/
|
||||
async loadBehaviorTree(assetId: string): Promise<BehaviorTreeAsset> {
|
||||
// 检查缓存
|
||||
if (this.config.enableCache && this.cache.has(assetId)) {
|
||||
logger.debug(`从缓存加载资产: ${assetId}`);
|
||||
return this.cache.get(assetId)!;
|
||||
}
|
||||
|
||||
logger.info(`加载行为树资产: ${assetId}`);
|
||||
|
||||
try {
|
||||
// 构建文件路径
|
||||
const filePath = this.resolveAssetPath(assetId);
|
||||
|
||||
// 读取文件
|
||||
const data = await this.config.readFile(filePath);
|
||||
|
||||
// 反序列化(自动根据 data 类型判断格式)
|
||||
const options: DeserializationOptions = {
|
||||
validate: true,
|
||||
strict: true
|
||||
};
|
||||
|
||||
const asset = BehaviorTreeAssetSerializer.deserialize(data, options);
|
||||
|
||||
// 缓存资产
|
||||
if (this.config.enableCache) {
|
||||
this.cache.set(assetId, asset);
|
||||
}
|
||||
|
||||
logger.info(`成功加载资产: ${assetId}`);
|
||||
return asset;
|
||||
} catch (error) {
|
||||
logger.error(`加载资产失败: ${assetId}`, error);
|
||||
throw new Error(`Failed to load behavior tree asset '${assetId}': ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查资产是否存在
|
||||
*/
|
||||
async exists(assetId: string): Promise<boolean> {
|
||||
// 如果在缓存中,直接返回 true
|
||||
if (this.config.enableCache && this.cache.has(assetId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const filePath = this.resolveAssetPath(assetId);
|
||||
// 尝试读取文件(如果文件不存在会抛出异常)
|
||||
await this.config.readFile(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载资产
|
||||
*/
|
||||
async preload(assetIds: string[]): Promise<void> {
|
||||
logger.info(`预加载 ${assetIds.length} 个资产...`);
|
||||
|
||||
const promises = assetIds.map(id => this.loadBehaviorTree(id).catch(error => {
|
||||
logger.warn(`预加载资产失败: ${id}`, error);
|
||||
}));
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
logger.info(`预加载完成`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载资产
|
||||
*/
|
||||
unload(assetId: string): void {
|
||||
if (this.cache.has(assetId)) {
|
||||
this.cache.delete(assetId);
|
||||
logger.debug(`卸载资产: ${assetId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空缓存
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.cache.clear();
|
||||
logger.info('缓存已清空');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的资产数量
|
||||
*/
|
||||
getCacheSize(): number {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
dispose(): void {
|
||||
this.clearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析资产路径
|
||||
*/
|
||||
private resolveAssetPath(assetId: string): string {
|
||||
// 移除开头的斜杠
|
||||
const normalizedId = assetId.replace(/^\/+/, '');
|
||||
|
||||
// 构建完整路径
|
||||
return `${this.config.basePath}/${normalizedId}${this.config.extension}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认文件读取实现
|
||||
*
|
||||
* 注意:此实现依赖运行环境
|
||||
* - 浏览器:需要通过 fetch 或 XMLHttpRequest
|
||||
* - Node.js:需要使用 fs
|
||||
* - 游戏引擎:需要使用引擎的文件 API
|
||||
*
|
||||
* 用户应该提供自己的 readFile 实现
|
||||
*/
|
||||
private async defaultReadFile(path: string): Promise<string | Uint8Array> {
|
||||
// 检测运行环境
|
||||
if (typeof window !== 'undefined' && typeof fetch !== 'undefined') {
|
||||
// 浏览器环境
|
||||
const response = await fetch(path);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (this.config.format === 'binary') {
|
||||
const buffer = await response.arrayBuffer();
|
||||
return new Uint8Array(buffer);
|
||||
} else {
|
||||
return await response.text();
|
||||
}
|
||||
} else if (typeof require !== 'undefined') {
|
||||
// Node.js 环境
|
||||
try {
|
||||
const fs = require('fs').promises;
|
||||
if (this.config.format === 'binary') {
|
||||
const buffer = await fs.readFile(path);
|
||||
return new Uint8Array(buffer);
|
||||
} else {
|
||||
return await fs.readFile(path, 'utf-8');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read file '${path}': ${error}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
'No default file reading implementation available. ' +
|
||||
'Please provide a custom readFile function in the config.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
import { BlackboardValueType } from '../Types/TaskStatus';
|
||||
import { BlackboardVariable } from '../Components/BlackboardComponent';
|
||||
import { BlackboardValueType, BlackboardVariable } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 全局黑板配置
|
||||
@@ -43,13 +42,18 @@ export class GlobalBlackboardService implements IService {
|
||||
description?: string;
|
||||
}
|
||||
): void {
|
||||
this.variables.set(name, {
|
||||
const variable: BlackboardVariable = {
|
||||
name,
|
||||
type,
|
||||
value: initialValue,
|
||||
readonly: options?.readonly ?? false,
|
||||
description: options?.description
|
||||
});
|
||||
readonly: options?.readonly ?? false
|
||||
};
|
||||
|
||||
if (options?.description !== undefined) {
|
||||
variable.description = options.description;
|
||||
}
|
||||
|
||||
this.variables.set(name, variable);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { BehaviorTreeAsset } from '../Serialization/BehaviorTreeAsset';
|
||||
|
||||
/**
|
||||
* 资产加载器接口
|
||||
*
|
||||
* 提供可扩展的资产加载机制,允许用户自定义资产加载逻辑。
|
||||
* 支持从文件系统、网络、数据库、自定义打包格式等加载资产。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 使用默认的文件系统加载器
|
||||
* const loader = new FileSystemAssetLoader({
|
||||
* basePath: 'assets/behavior-trees',
|
||||
* format: 'json'
|
||||
* });
|
||||
* core.services.registerInstance(FileSystemAssetLoader, loader);
|
||||
*
|
||||
* // 或实现自定义加载器
|
||||
* class NetworkAssetLoader implements IAssetLoader {
|
||||
* async loadBehaviorTree(assetId: string): Promise<BehaviorTreeAsset> {
|
||||
* const response = await fetch(`/api/assets/${assetId}`);
|
||||
* return response.json();
|
||||
* }
|
||||
*
|
||||
* async exists(assetId: string): Promise<boolean> {
|
||||
* const response = await fetch(`/api/assets/${assetId}/exists`);
|
||||
* return response.json();
|
||||
* }
|
||||
* }
|
||||
* core.services.registerInstance(FileSystemAssetLoader, new NetworkAssetLoader());
|
||||
* ```
|
||||
*/
|
||||
export interface IAssetLoader {
|
||||
/**
|
||||
* 加载行为树资产
|
||||
*
|
||||
* @param assetId 资产逻辑ID,例如 'patrol' 或 'ai/patrol'
|
||||
* @returns 行为树资产对象
|
||||
* @throws 如果资产不存在或加载失败
|
||||
*/
|
||||
loadBehaviorTree(assetId: string): Promise<BehaviorTreeAsset>;
|
||||
|
||||
/**
|
||||
* 检查资产是否存在
|
||||
*
|
||||
* @param assetId 资产逻辑ID
|
||||
* @returns 资产是否存在
|
||||
*/
|
||||
exists(assetId: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 预加载资产(可选)
|
||||
*
|
||||
* 用于提前加载资产到缓存,减少运行时延迟
|
||||
*
|
||||
* @param assetIds 要预加载的资产ID列表
|
||||
*/
|
||||
preload?(assetIds: string[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* 卸载资产(可选)
|
||||
*
|
||||
* 释放资产占用的内存
|
||||
*
|
||||
* @param assetId 资产ID
|
||||
*/
|
||||
unload?(assetId: string): void;
|
||||
}
|
||||
@@ -1,355 +0,0 @@
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 资产类型
|
||||
*/
|
||||
export enum AssetType {
|
||||
BehaviorTree = 'behavior-tree',
|
||||
Blackboard = 'blackboard',
|
||||
Unknown = 'unknown'
|
||||
}
|
||||
|
||||
/**
|
||||
* 资产注册信息
|
||||
*/
|
||||
export interface AssetRegistry {
|
||||
/** 资产唯一ID */
|
||||
id: string;
|
||||
|
||||
/** 资产名称 */
|
||||
name: string;
|
||||
|
||||
/** 资产相对路径(相对于工作区根目录) */
|
||||
path: string;
|
||||
|
||||
/** 资产类型 */
|
||||
type: AssetType;
|
||||
|
||||
/** 依赖的其他资产ID列表 */
|
||||
dependencies: string[];
|
||||
|
||||
/** 最后修改时间 */
|
||||
lastModified?: number;
|
||||
|
||||
/** 资产元数据 */
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作区配置
|
||||
*/
|
||||
export interface WorkspaceConfig {
|
||||
/** 工作区名称 */
|
||||
name: string;
|
||||
|
||||
/** 工作区版本 */
|
||||
version: string;
|
||||
|
||||
/** 工作区根目录(绝对路径) */
|
||||
rootPath: string;
|
||||
|
||||
/** 资产目录配置 */
|
||||
assetPaths: {
|
||||
/** 行为树目录 */
|
||||
behaviorTrees: string;
|
||||
|
||||
/** 黑板目录 */
|
||||
blackboards: string;
|
||||
};
|
||||
|
||||
/** 资产注册表 */
|
||||
assets: AssetRegistry[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作区服务
|
||||
*
|
||||
* 管理项目的工作区配置和资产注册表,提供:
|
||||
* - 工作区配置的加载和保存
|
||||
* - 资产注册和查询
|
||||
* - 依赖关系追踪
|
||||
* - 循环依赖检测
|
||||
*/
|
||||
export class WorkspaceService implements IService {
|
||||
private config: WorkspaceConfig | null = null;
|
||||
private assetMap: Map<string, AssetRegistry> = new Map();
|
||||
private assetPathMap: Map<string, AssetRegistry> = new Map();
|
||||
|
||||
/**
|
||||
* 初始化工作区
|
||||
*/
|
||||
initialize(config: WorkspaceConfig): void {
|
||||
this.config = config;
|
||||
this.rebuildAssetMaps();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重建资产映射表
|
||||
*/
|
||||
private rebuildAssetMaps(): void {
|
||||
this.assetMap.clear();
|
||||
this.assetPathMap.clear();
|
||||
|
||||
if (!this.config) return;
|
||||
|
||||
for (const asset of this.config.assets) {
|
||||
this.assetMap.set(asset.id, asset);
|
||||
this.assetPathMap.set(asset.path, asset);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作区配置
|
||||
*/
|
||||
getConfig(): WorkspaceConfig | null {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新工作区配置
|
||||
*/
|
||||
updateConfig(config: WorkspaceConfig): void {
|
||||
this.config = config;
|
||||
this.rebuildAssetMaps();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册资产
|
||||
*/
|
||||
registerAsset(asset: AssetRegistry): void {
|
||||
if (!this.config) {
|
||||
throw new Error('工作区未初始化');
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
const existing = this.config.assets.find(a => a.id === asset.id);
|
||||
if (existing) {
|
||||
// 更新现有资产
|
||||
Object.assign(existing, asset);
|
||||
} else {
|
||||
// 添加新资产
|
||||
this.config.assets.push(asset);
|
||||
}
|
||||
|
||||
this.rebuildAssetMaps();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册资产
|
||||
*/
|
||||
unregisterAsset(assetId: string): void {
|
||||
if (!this.config) return;
|
||||
|
||||
const index = this.config.assets.findIndex(a => a.id === assetId);
|
||||
if (index !== -1) {
|
||||
this.config.assets.splice(index, 1);
|
||||
this.rebuildAssetMaps();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过ID获取资产
|
||||
*/
|
||||
getAssetById(assetId: string): AssetRegistry | undefined {
|
||||
return this.assetMap.get(assetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过路径获取资产
|
||||
*/
|
||||
getAssetByPath(path: string): AssetRegistry | undefined {
|
||||
return this.assetPathMap.get(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有资产
|
||||
*/
|
||||
getAllAssets(): AssetRegistry[] {
|
||||
return this.config?.assets || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 按类型获取资产
|
||||
*/
|
||||
getAssetsByType(type: AssetType): AssetRegistry[] {
|
||||
return this.getAllAssets().filter(a => a.type === type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取行为树资产列表
|
||||
*/
|
||||
getBehaviorTreeAssets(): AssetRegistry[] {
|
||||
return this.getAssetsByType(AssetType.BehaviorTree);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取黑板资产列表
|
||||
*/
|
||||
getBlackboardAssets(): AssetRegistry[] {
|
||||
return this.getAssetsByType(AssetType.Blackboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资产的所有依赖(递归)
|
||||
*/
|
||||
getAssetDependencies(assetId: string, visited = new Set<string>()): AssetRegistry[] {
|
||||
if (visited.has(assetId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
visited.add(assetId);
|
||||
|
||||
const asset = this.getAssetById(assetId);
|
||||
if (!asset) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dependencies: AssetRegistry[] = [];
|
||||
|
||||
for (const depId of asset.dependencies) {
|
||||
const depAsset = this.getAssetById(depId);
|
||||
if (depAsset) {
|
||||
dependencies.push(depAsset);
|
||||
// 递归获取依赖的依赖
|
||||
dependencies.push(...this.getAssetDependencies(depId, visited));
|
||||
}
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测循环依赖
|
||||
*
|
||||
* @param assetId 要检查的资产ID
|
||||
* @returns 如果存在循环依赖,返回循环路径;否则返回 null
|
||||
*/
|
||||
detectCircularDependency(assetId: string): string[] | null {
|
||||
const visited = new Set<string>();
|
||||
const path: string[] = [];
|
||||
|
||||
const dfs = (currentId: string): boolean => {
|
||||
if (path.includes(currentId)) {
|
||||
// 找到循环
|
||||
path.push(currentId);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (visited.has(currentId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
visited.add(currentId);
|
||||
path.push(currentId);
|
||||
|
||||
const asset = this.getAssetById(currentId);
|
||||
if (asset) {
|
||||
for (const depId of asset.dependencies) {
|
||||
if (dfs(depId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path.pop();
|
||||
return false;
|
||||
};
|
||||
|
||||
return dfs(assetId) ? path : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以添加依赖(不会造成循环依赖)
|
||||
*
|
||||
* @param assetId 资产ID
|
||||
* @param dependencyId 要添加的依赖ID
|
||||
* @returns 是否可以安全添加
|
||||
*/
|
||||
canAddDependency(assetId: string, dependencyId: string): boolean {
|
||||
const asset = this.getAssetById(assetId);
|
||||
if (!asset) return false;
|
||||
|
||||
// 临时添加依赖
|
||||
const originalDeps = [...asset.dependencies];
|
||||
asset.dependencies.push(dependencyId);
|
||||
|
||||
// 检测循环依赖
|
||||
const hasCircular = this.detectCircularDependency(assetId) !== null;
|
||||
|
||||
// 恢复原始依赖
|
||||
asset.dependencies = originalDeps;
|
||||
|
||||
return !hasCircular;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加资产依赖
|
||||
*/
|
||||
addAssetDependency(assetId: string, dependencyId: string): boolean {
|
||||
if (!this.canAddDependency(assetId, dependencyId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const asset = this.getAssetById(assetId);
|
||||
if (!asset) return false;
|
||||
|
||||
if (!asset.dependencies.includes(dependencyId)) {
|
||||
asset.dependencies.push(dependencyId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除资产依赖
|
||||
*/
|
||||
removeAssetDependency(assetId: string, dependencyId: string): void {
|
||||
const asset = this.getAssetById(assetId);
|
||||
if (!asset) return;
|
||||
|
||||
const index = asset.dependencies.indexOf(dependencyId);
|
||||
if (index !== -1) {
|
||||
asset.dependencies.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析资产路径(支持相对路径和绝对路径)
|
||||
*/
|
||||
resolveAssetPath(path: string): string {
|
||||
if (!this.config) return path;
|
||||
|
||||
// 如果是绝对路径,直接返回
|
||||
if (path.startsWith('/') || path.match(/^[A-Za-z]:/)) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// 相对路径,拼接工作区根目录
|
||||
return `${this.config.rootPath}/${path}`.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资产的相对路径
|
||||
*/
|
||||
getRelativePath(absolutePath: string): string {
|
||||
if (!this.config) return absolutePath;
|
||||
|
||||
const rootPath = this.config.rootPath.replace(/\\/g, '/');
|
||||
const absPath = absolutePath.replace(/\\/g, '/');
|
||||
|
||||
if (absPath.startsWith(rootPath)) {
|
||||
return absPath.substring(rootPath.length + 1);
|
||||
}
|
||||
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
dispose(): void {
|
||||
this.config = null;
|
||||
this.assetMap.clear();
|
||||
this.assetPathMap.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,704 +0,0 @@
|
||||
import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeNode } from '../Components/BehaviorTreeNode';
|
||||
import { CompositeNodeComponent } from '../Components/CompositeNodeComponent';
|
||||
import { ActiveNode } from '../Components/ActiveNode';
|
||||
import { BlackboardComponent } from '../Components/BlackboardComponent';
|
||||
import { TaskStatus, NodeType, CompositeType, AbortType } from '../Types/TaskStatus';
|
||||
import { SequenceNode } from '../Components/Composites/SequenceNode';
|
||||
import { SelectorNode } from '../Components/Composites/SelectorNode';
|
||||
import { RootNode } from '../Components/Composites/RootNode';
|
||||
import { SubTreeNode } from '../Components/Composites/SubTreeNode';
|
||||
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';
|
||||
|
||||
/**
|
||||
* 复合节点执行系统
|
||||
*
|
||||
* 负责处理所有活跃的复合节点
|
||||
* 读取子节点状态,根据复合规则决定自己的状态和激活哪些子节点
|
||||
*
|
||||
* updateOrder: 300 (在叶子节点和装饰器之后执行)
|
||||
*/
|
||||
export class CompositeExecutionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(BehaviorTreeNode, ActiveNode).exclude(RootNode, SubTreeNode));
|
||||
this.updateOrder = 300;
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const node = entity.getComponent(BehaviorTreeNode)!;
|
||||
|
||||
// 只处理复合节点
|
||||
if (node.nodeType !== NodeType.Composite) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 使用 getComponentByType 支持继承查找
|
||||
const composite = entity.getComponentByType(CompositeNodeComponent);
|
||||
|
||||
if (!composite) {
|
||||
this.logger.warn(`复合节点 ${entity.name} 没有找到复合节点组件`);
|
||||
const components = entity.components.map(c => c.constructor.name).join(', ');
|
||||
this.logger.warn(` 组件列表: ${components}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.executeComposite(entity, node, composite);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行复合节点逻辑
|
||||
*/
|
||||
private executeComposite(entity: Entity, node: BehaviorTreeNode, composite: CompositeNodeComponent): void {
|
||||
const children = entity.children;
|
||||
|
||||
if (children.length === 0) {
|
||||
node.status = TaskStatus.Success;
|
||||
this.completeNode(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据复合节点类型处理
|
||||
switch (composite.compositeType) {
|
||||
case CompositeType.Sequence:
|
||||
this.handleSequence(entity, node, children);
|
||||
break;
|
||||
|
||||
case CompositeType.Selector:
|
||||
this.handleSelector(entity, node, children);
|
||||
break;
|
||||
|
||||
case CompositeType.Parallel:
|
||||
this.handleParallel(entity, node, children);
|
||||
break;
|
||||
|
||||
case CompositeType.ParallelSelector:
|
||||
this.handleParallelSelector(entity, node, children);
|
||||
break;
|
||||
|
||||
case CompositeType.RandomSequence:
|
||||
this.handleRandomSequence(entity, node, composite, children);
|
||||
break;
|
||||
|
||||
case CompositeType.RandomSelector:
|
||||
this.handleRandomSelector(entity, node, composite, children);
|
||||
break;
|
||||
|
||||
default:
|
||||
node.status = TaskStatus.Failure;
|
||||
this.completeNode(entity);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列节点:所有子节点都成功才成功
|
||||
*/
|
||||
private handleSequence(entity: Entity, node: BehaviorTreeNode, children: readonly Entity[]): void {
|
||||
// 检查是否需要中止
|
||||
const sequenceNode = entity.getComponentByType(SequenceNode);
|
||||
if (sequenceNode && sequenceNode.abortType !== AbortType.None) {
|
||||
if (this.shouldAbort(entity, node, children, sequenceNode.abortType)) {
|
||||
this.abortExecution(entity, node, children);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查当前子节点
|
||||
if (node.currentChildIndex >= children.length) {
|
||||
// 所有子节点都成功
|
||||
node.status = TaskStatus.Success;
|
||||
node.currentChildIndex = 0; // 只重置索引,保持状态为Success
|
||||
this.completeNode(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentChild = children[node.currentChildIndex];
|
||||
const childNode = currentChild.getComponent(BehaviorTreeNode);
|
||||
|
||||
if (!childNode) {
|
||||
node.status = TaskStatus.Failure;
|
||||
this.completeNode(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果子节点还没开始执行,激活它
|
||||
if (childNode.status === TaskStatus.Invalid) {
|
||||
if (!currentChild.hasComponent(ActiveNode)) {
|
||||
currentChild.addComponent(new ActiveNode());
|
||||
}
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查子节点状态
|
||||
if (childNode.status === TaskStatus.Running) {
|
||||
node.status = TaskStatus.Running;
|
||||
} else if (childNode.status === TaskStatus.Failure) {
|
||||
// 任一失败则失败
|
||||
node.status = TaskStatus.Failure;
|
||||
node.currentChildIndex = 0; // 只重置索引,保持状态为Failure
|
||||
this.completeNode(entity);
|
||||
} else if (childNode.status === TaskStatus.Success) {
|
||||
// 成功则移动到下一个子节点
|
||||
// 重置已完成的子节点状态,以便下次行为树重新执行时从头开始
|
||||
childNode.reset();
|
||||
node.currentChildIndex++;
|
||||
// 继续保持活跃,下一帧处理下一个子节点
|
||||
node.status = TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择器节点:任一子节点成功就成功
|
||||
*/
|
||||
private handleSelector(entity: Entity, node: BehaviorTreeNode, children: readonly Entity[]): void {
|
||||
// 检查是否需要中止
|
||||
const selectorNode = entity.getComponentByType(SelectorNode);
|
||||
if (selectorNode && selectorNode.abortType !== AbortType.None) {
|
||||
if (this.shouldAbort(entity, node, children, selectorNode.abortType)) {
|
||||
this.abortExecution(entity, node, children);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查当前子节点
|
||||
if (node.currentChildIndex >= children.length) {
|
||||
// 所有子节点都失败
|
||||
node.status = TaskStatus.Failure;
|
||||
node.currentChildIndex = 0; // 只重置索引,保持状态为Failure
|
||||
this.completeNode(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentChild = children[node.currentChildIndex];
|
||||
const childNode = currentChild.getComponent(BehaviorTreeNode);
|
||||
|
||||
if (!childNode) {
|
||||
node.status = TaskStatus.Failure;
|
||||
this.completeNode(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果子节点还没开始执行,激活它
|
||||
if (childNode.status === TaskStatus.Invalid) {
|
||||
if (!currentChild.hasComponent(ActiveNode)) {
|
||||
currentChild.addComponent(new ActiveNode());
|
||||
}
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查子节点状态
|
||||
if (childNode.status === TaskStatus.Running) {
|
||||
node.status = TaskStatus.Running;
|
||||
} else if (childNode.status === TaskStatus.Success) {
|
||||
// 任一成功则成功
|
||||
node.status = TaskStatus.Success;
|
||||
node.currentChildIndex = 0; // 只重置索引,保持状态为Success
|
||||
this.completeNode(entity);
|
||||
} else if (childNode.status === TaskStatus.Failure) {
|
||||
// 失败则移动到下一个子节点
|
||||
// 重置已完成的子节点状态,以便下次行为树重新执行时从头开始
|
||||
childNode.reset();
|
||||
node.currentChildIndex++;
|
||||
// 继续保持活跃,下一帧处理下一个子节点
|
||||
node.status = TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 并行节点:所有子节点都执行,全部成功才成功
|
||||
*/
|
||||
private handleParallel(entity: Entity, node: BehaviorTreeNode, children: readonly Entity[]): void {
|
||||
let hasRunning = false;
|
||||
let hasFailed = false;
|
||||
|
||||
// 激活所有子节点
|
||||
for (const child of children) {
|
||||
if (!child.hasComponent(ActiveNode)) {
|
||||
child.addComponent(new ActiveNode());
|
||||
}
|
||||
|
||||
const childNode = child.getComponent(BehaviorTreeNode);
|
||||
if (!childNode) continue;
|
||||
|
||||
if (childNode.status === TaskStatus.Running) {
|
||||
hasRunning = true;
|
||||
} else if (childNode.status === TaskStatus.Failure) {
|
||||
hasFailed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasRunning) {
|
||||
node.status = TaskStatus.Running;
|
||||
} else if (hasFailed) {
|
||||
node.status = TaskStatus.Failure;
|
||||
node.currentChildIndex = 0; // 只重置索引,保持状态为Failure
|
||||
this.completeNode(entity);
|
||||
} else {
|
||||
// 所有子节点都成功
|
||||
node.status = TaskStatus.Success;
|
||||
node.currentChildIndex = 0; // 只重置索引,保持状态为Success
|
||||
this.completeNode(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 并行选择器:任一成功则成功
|
||||
*/
|
||||
private handleParallelSelector(entity: Entity, node: BehaviorTreeNode, children: readonly Entity[]): void {
|
||||
let hasRunning = false;
|
||||
let hasSucceeded = false;
|
||||
|
||||
// 激活所有子节点
|
||||
for (const child of children) {
|
||||
if (!child.hasComponent(ActiveNode)) {
|
||||
child.addComponent(new ActiveNode());
|
||||
}
|
||||
|
||||
const childNode = child.getComponent(BehaviorTreeNode);
|
||||
if (!childNode) continue;
|
||||
|
||||
if (childNode.status === TaskStatus.Running) {
|
||||
hasRunning = true;
|
||||
} else if (childNode.status === TaskStatus.Success) {
|
||||
hasSucceeded = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasSucceeded) {
|
||||
// 任一成功则成功
|
||||
node.status = TaskStatus.Success;
|
||||
node.currentChildIndex = 0; // 只重置索引,保持状态为Success
|
||||
// 停止所有子节点
|
||||
for (const child of children) {
|
||||
child.removeComponentByType(ActiveNode);
|
||||
}
|
||||
this.completeNode(entity);
|
||||
} else if (hasRunning) {
|
||||
node.status = TaskStatus.Running;
|
||||
} else {
|
||||
// 所有子节点都失败
|
||||
node.status = TaskStatus.Failure;
|
||||
node.currentChildIndex = 0; // 只重置索引,保持状态为Failure
|
||||
this.completeNode(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机序列
|
||||
*/
|
||||
private handleRandomSequence(
|
||||
entity: Entity,
|
||||
node: BehaviorTreeNode,
|
||||
composite: CompositeNodeComponent,
|
||||
children: readonly Entity[]
|
||||
): void {
|
||||
// 获取洗牌后的子节点索引
|
||||
const childIndex = composite.getNextChildIndex(node.currentChildIndex, children.length);
|
||||
|
||||
if (childIndex >= children.length) {
|
||||
// 所有子节点都成功
|
||||
node.status = TaskStatus.Success;
|
||||
node.currentChildIndex = 0; // 只重置索引,保持状态为Success
|
||||
composite.resetShuffle();
|
||||
this.completeNode(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentChild = children[childIndex];
|
||||
const childNode = currentChild.getComponent(BehaviorTreeNode);
|
||||
|
||||
if (!childNode) {
|
||||
node.status = TaskStatus.Failure;
|
||||
this.completeNode(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果子节点还没开始执行,激活它
|
||||
if (childNode.status === TaskStatus.Invalid) {
|
||||
if (!currentChild.hasComponent(ActiveNode)) {
|
||||
currentChild.addComponent(new ActiveNode());
|
||||
}
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查子节点状态
|
||||
if (childNode.status === TaskStatus.Running) {
|
||||
node.status = TaskStatus.Running;
|
||||
} else if (childNode.status === TaskStatus.Failure) {
|
||||
node.status = TaskStatus.Failure;
|
||||
node.currentChildIndex = 0; // 只重置索引,保持状态为Failure
|
||||
composite.resetShuffle();
|
||||
this.completeNode(entity);
|
||||
} else if (childNode.status === TaskStatus.Success) {
|
||||
// 成功则移动到下一个子节点
|
||||
// 重置已完成的子节点状态,以便下次行为树重新执行时从头开始
|
||||
childNode.reset();
|
||||
node.currentChildIndex++;
|
||||
node.status = TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机选择器
|
||||
*/
|
||||
private handleRandomSelector(
|
||||
entity: Entity,
|
||||
node: BehaviorTreeNode,
|
||||
composite: CompositeNodeComponent,
|
||||
children: readonly Entity[]
|
||||
): void {
|
||||
// 获取洗牌后的子节点索引
|
||||
const childIndex = composite.getNextChildIndex(node.currentChildIndex, children.length);
|
||||
|
||||
if (childIndex >= children.length) {
|
||||
// 所有子节点都失败
|
||||
node.status = TaskStatus.Failure;
|
||||
node.currentChildIndex = 0; // 只重置索引,保持状态为Failure
|
||||
composite.resetShuffle();
|
||||
this.completeNode(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentChild = children[childIndex];
|
||||
const childNode = currentChild.getComponent(BehaviorTreeNode);
|
||||
|
||||
if (!childNode) {
|
||||
node.status = TaskStatus.Failure;
|
||||
this.completeNode(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果子节点还没开始执行,激活它
|
||||
if (childNode.status === TaskStatus.Invalid) {
|
||||
if (!currentChild.hasComponent(ActiveNode)) {
|
||||
currentChild.addComponent(new ActiveNode());
|
||||
}
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查子节点状态
|
||||
if (childNode.status === TaskStatus.Running) {
|
||||
node.status = TaskStatus.Running;
|
||||
} else if (childNode.status === TaskStatus.Success) {
|
||||
node.status = TaskStatus.Success;
|
||||
node.currentChildIndex = 0; // 只重置索引,保持状态为Success
|
||||
composite.resetShuffle();
|
||||
this.completeNode(entity);
|
||||
} else if (childNode.status === TaskStatus.Failure) {
|
||||
// 失败则移动到下一个子节点
|
||||
// 重置已完成的子节点状态,以便下次行为树重新执行时从头开始
|
||||
childNode.reset();
|
||||
node.currentChildIndex++;
|
||||
node.status = TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该中止当前执行
|
||||
*/
|
||||
private shouldAbort(
|
||||
entity: Entity,
|
||||
node: BehaviorTreeNode,
|
||||
children: readonly Entity[],
|
||||
abortType: AbortType
|
||||
): boolean {
|
||||
const currentIndex = node.currentChildIndex;
|
||||
|
||||
// 如果还没开始执行任何子节点,不需要中止
|
||||
if (currentIndex === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Self: 检查当前执行路径中的条件节点是否失败
|
||||
if (abortType === AbortType.Self || abortType === AbortType.Both) {
|
||||
// 检查当前正在执行的分支之前的条件节点
|
||||
for (let i = 0; i < currentIndex; i++) {
|
||||
const child = children[i];
|
||||
const childNode = child.getComponent(BehaviorTreeNode);
|
||||
if (childNode && childNode.nodeType === NodeType.Condition) {
|
||||
// 如果条件节点现在失败了,应该中止
|
||||
if (childNode.status === TaskStatus.Failure) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LowerPriority: 检查高优先级分支的条件是否满足
|
||||
if (abortType === AbortType.LowerPriority || abortType === AbortType.Both) {
|
||||
// 检查当前索引之前的所有分支(优先级更高)
|
||||
for (let i = 0; i < currentIndex; i++) {
|
||||
const child = children[i];
|
||||
const childNode = child.getComponent(BehaviorTreeNode);
|
||||
if (!childNode) continue;
|
||||
|
||||
// 如果是条件节点且现在成功了
|
||||
if (childNode.nodeType === NodeType.Condition) {
|
||||
if (this.evaluateCondition(child, childNode)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 如果是复合节点,检查其第一个子节点(通常是条件)
|
||||
else if (childNode.nodeType === NodeType.Composite && child.children.length > 0) {
|
||||
const firstGrandChild = child.children[0];
|
||||
const firstGrandChildNode = firstGrandChild.getComponent(BehaviorTreeNode);
|
||||
if (firstGrandChildNode && firstGrandChildNode.nodeType === NodeType.Condition) {
|
||||
if (this.evaluateCondition(firstGrandChild, firstGrandChildNode)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估条件节点
|
||||
*/
|
||||
private evaluateCondition(entity: Entity, node: BehaviorTreeNode): boolean {
|
||||
if (node.nodeType !== NodeType.Condition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let result = false;
|
||||
|
||||
if (entity.hasComponent(BlackboardCompareCondition)) {
|
||||
result = this.evaluateBlackboardCompare(entity);
|
||||
} else if (entity.hasComponent(BlackboardExistsCondition)) {
|
||||
result = this.evaluateBlackboardExists(entity);
|
||||
} else if (entity.hasComponent(RandomProbabilityCondition)) {
|
||||
result = this.evaluateRandomProbability(entity);
|
||||
} else if (entity.hasComponent(ExecuteCondition)) {
|
||||
result = this.evaluateCustomCondition(entity);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估黑板比较条件
|
||||
*/
|
||||
private evaluateBlackboardCompare(entity: Entity): boolean {
|
||||
const condition = entity.getComponent(BlackboardCompareCondition)!;
|
||||
const blackboard = this.findBlackboard(entity);
|
||||
|
||||
if (!blackboard || !blackboard.hasVariable(condition.variableName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const value = blackboard.getValue(condition.variableName);
|
||||
let compareValue = condition.compareValue;
|
||||
|
||||
if (typeof compareValue === 'string') {
|
||||
compareValue = this.resolveVariableReferences(compareValue, blackboard);
|
||||
}
|
||||
|
||||
let result = false;
|
||||
switch (condition.operator) {
|
||||
case CompareOperator.Equal:
|
||||
result = value === compareValue;
|
||||
break;
|
||||
case CompareOperator.NotEqual:
|
||||
result = value !== compareValue;
|
||||
break;
|
||||
case CompareOperator.Greater:
|
||||
result = value > compareValue;
|
||||
break;
|
||||
case CompareOperator.GreaterOrEqual:
|
||||
result = value >= compareValue;
|
||||
break;
|
||||
case CompareOperator.Less:
|
||||
result = value < compareValue;
|
||||
break;
|
||||
case CompareOperator.LessOrEqual:
|
||||
result = value <= compareValue;
|
||||
break;
|
||||
case CompareOperator.Contains:
|
||||
if (typeof value === 'string') {
|
||||
result = value.includes(compareValue);
|
||||
} else if (Array.isArray(value)) {
|
||||
result = value.includes(compareValue);
|
||||
}
|
||||
break;
|
||||
case CompareOperator.Matches:
|
||||
if (typeof value === 'string' && typeof compareValue === 'string') {
|
||||
const regex = new RegExp(compareValue);
|
||||
result = regex.test(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return condition.invertResult ? !result : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估黑板变量存在性
|
||||
*/
|
||||
private evaluateBlackboardExists(entity: Entity): boolean {
|
||||
const condition = entity.getComponent(BlackboardExistsCondition)!;
|
||||
const blackboard = this.findBlackboard(entity);
|
||||
|
||||
if (!blackboard) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let result = blackboard.hasVariable(condition.variableName);
|
||||
|
||||
if (result && condition.checkNotNull) {
|
||||
const value = blackboard.getValue(condition.variableName);
|
||||
result = value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
return condition.invertResult ? !result : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估随机概率
|
||||
*/
|
||||
private evaluateRandomProbability(entity: Entity): boolean {
|
||||
const condition = entity.getComponent(RandomProbabilityCondition)!;
|
||||
return condition.evaluate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估自定义条件
|
||||
*/
|
||||
private evaluateCustomCondition(entity: Entity): boolean {
|
||||
const condition = entity.getComponent(ExecuteCondition)!;
|
||||
const func = condition.getFunction();
|
||||
|
||||
if (!func) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const blackboard = this.findBlackboard(entity);
|
||||
const result = func(entity, blackboard, 0);
|
||||
|
||||
return condition.invertResult ? !result : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析字符串中的变量引用
|
||||
*/
|
||||
private resolveVariableReferences(value: string, blackboard: BlackboardComponent): any {
|
||||
const pureMatch = value.match(/^{{\s*(\w+)\s*}}$/);
|
||||
if (pureMatch) {
|
||||
const varName = pureMatch[1];
|
||||
if (blackboard.hasVariable(varName)) {
|
||||
return blackboard.getValue(varName);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
return value.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
|
||||
if (blackboard.hasVariable(varName)) {
|
||||
const val = blackboard.getValue(varName);
|
||||
return val !== undefined ? String(val) : match;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找黑板组件
|
||||
*/
|
||||
private findBlackboard(entity: Entity): BlackboardComponent | undefined {
|
||||
let current: Entity | null = entity;
|
||||
|
||||
while (current) {
|
||||
const blackboard = current.getComponent(BlackboardComponent);
|
||||
if (blackboard) {
|
||||
return blackboard;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 中止当前执行
|
||||
*/
|
||||
private abortExecution(entity: Entity, node: BehaviorTreeNode, children: readonly Entity[]): void {
|
||||
// 停止当前正在执行的子节点
|
||||
const currentIndex = node.currentChildIndex;
|
||||
if (currentIndex < children.length) {
|
||||
const currentChild = children[currentIndex];
|
||||
this.deactivateNode(currentChild);
|
||||
}
|
||||
|
||||
// 重置节点状态,从头开始
|
||||
node.currentChildIndex = 0;
|
||||
node.status = TaskStatus.Running;
|
||||
|
||||
// 不需要 completeNode,因为我们要继续执行(从头开始)
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归停用节点及其所有子节点
|
||||
*/
|
||||
private deactivateNode(entity: Entity): void {
|
||||
// 移除活跃标记
|
||||
entity.removeComponentByType(ActiveNode);
|
||||
|
||||
// 重置节点状态
|
||||
const node = entity.getComponent(BehaviorTreeNode);
|
||||
if (node) {
|
||||
node.reset();
|
||||
}
|
||||
|
||||
// 递归停用所有子节点
|
||||
for (const child of entity.children) {
|
||||
this.deactivateNode(child);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归重置所有子节点的状态
|
||||
*/
|
||||
private resetAllChildren(entity: Entity): void {
|
||||
for (const child of entity.children) {
|
||||
const childNode = child.getComponent(BehaviorTreeNode);
|
||||
if (childNode) {
|
||||
childNode.reset();
|
||||
}
|
||||
// 递归重置孙子节点
|
||||
this.resetAllChildren(child);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成节点执行
|
||||
*/
|
||||
private completeNode(entity: Entity): void {
|
||||
entity.removeComponentByType(ActiveNode);
|
||||
|
||||
// 如果是复合节点完成,重置所有子节点状态
|
||||
const node = entity.getComponent(BehaviorTreeNode);
|
||||
if (node && node.nodeType === NodeType.Composite) {
|
||||
this.resetAllChildren(entity);
|
||||
}
|
||||
|
||||
// 通知父节点
|
||||
if (entity.parent && entity.parent.hasComponent(BehaviorTreeNode)) {
|
||||
if (!entity.parent.hasComponent(ActiveNode)) {
|
||||
entity.parent.addComponent(new ActiveNode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override getLoggerName(): string {
|
||||
return 'CompositeExecutionSystem';
|
||||
}
|
||||
}
|
||||
@@ -1,515 +0,0 @@
|
||||
import { EntitySystem, Matcher, Entity, Time } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeNode } from '../Components/BehaviorTreeNode';
|
||||
import { DecoratorNodeComponent } from '../Components/DecoratorNodeComponent';
|
||||
import { BlackboardComponent } from '../Components/BlackboardComponent';
|
||||
import { ActiveNode } from '../Components/ActiveNode';
|
||||
import { PropertyBindings } from '../Components/PropertyBindings';
|
||||
import { LogOutput } from '../Components/LogOutput';
|
||||
import { TaskStatus, NodeType, DecoratorType } from '../Types/TaskStatus';
|
||||
import { RepeaterNode } from '../Components/Decorators/RepeaterNode';
|
||||
import { ConditionalNode } from '../Components/Decorators/ConditionalNode';
|
||||
import { CooldownNode } from '../Components/Decorators/CooldownNode';
|
||||
import { TimeoutNode } from '../Components/Decorators/TimeoutNode';
|
||||
|
||||
/**
|
||||
* 装饰器节点执行系统
|
||||
*
|
||||
* 负责处理所有活跃的装饰器节点
|
||||
* 读取子节点状态,根据装饰器规则决定自己的状态
|
||||
*
|
||||
* updateOrder: 200 (在叶子节点之后执行)
|
||||
*/
|
||||
export class DecoratorExecutionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(BehaviorTreeNode, ActiveNode));
|
||||
this.updateOrder = 200;
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const node = entity.getComponent(BehaviorTreeNode)!;
|
||||
|
||||
// 只处理装饰器节点
|
||||
if (node.nodeType !== NodeType.Decorator) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 使用 getComponentByType 支持继承查找
|
||||
const decorator = entity.getComponentByType(DecoratorNodeComponent);
|
||||
if (!decorator) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.executeDecorator(entity, node, decorator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行装饰器逻辑
|
||||
*/
|
||||
private executeDecorator(entity: Entity, node: BehaviorTreeNode, decorator: DecoratorNodeComponent): void {
|
||||
const children = entity.children;
|
||||
|
||||
if (children.length === 0) {
|
||||
this.logger.warn('装饰器节点没有子节点');
|
||||
node.status = TaskStatus.Failure;
|
||||
this.completeNode(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
const child = children[0]; // 装饰器只有一个子节点
|
||||
const childNode = child.getComponent(BehaviorTreeNode);
|
||||
|
||||
if (!childNode) {
|
||||
node.status = TaskStatus.Failure;
|
||||
this.completeNode(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据装饰器类型处理
|
||||
switch (decorator.decoratorType) {
|
||||
case DecoratorType.Inverter:
|
||||
this.handleInverter(entity, node, child, childNode);
|
||||
break;
|
||||
|
||||
case DecoratorType.Repeater:
|
||||
this.handleRepeater(entity, node, decorator, child, childNode);
|
||||
break;
|
||||
|
||||
case DecoratorType.UntilSuccess:
|
||||
this.handleUntilSuccess(entity, node, child, childNode);
|
||||
break;
|
||||
|
||||
case DecoratorType.UntilFail:
|
||||
this.handleUntilFail(entity, node, child, childNode);
|
||||
break;
|
||||
|
||||
case DecoratorType.AlwaysSucceed:
|
||||
this.handleAlwaysSucceed(entity, node, child, childNode);
|
||||
break;
|
||||
|
||||
case DecoratorType.AlwaysFail:
|
||||
this.handleAlwaysFail(entity, node, child, childNode);
|
||||
break;
|
||||
|
||||
case DecoratorType.Conditional:
|
||||
this.handleConditional(entity, node, decorator, child, childNode);
|
||||
break;
|
||||
|
||||
case DecoratorType.Cooldown:
|
||||
this.handleCooldown(entity, node, decorator, child, childNode);
|
||||
break;
|
||||
|
||||
case DecoratorType.Timeout:
|
||||
this.handleTimeout(entity, node, decorator, child, childNode);
|
||||
break;
|
||||
|
||||
default:
|
||||
node.status = TaskStatus.Failure;
|
||||
this.completeNode(entity);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反转装饰器
|
||||
*/
|
||||
private handleInverter(entity: Entity, node: BehaviorTreeNode, child: Entity, childNode: BehaviorTreeNode): void {
|
||||
if (!child.hasComponent(ActiveNode)) {
|
||||
// 子节点未激活,激活它
|
||||
child.addComponent(new ActiveNode());
|
||||
node.status = TaskStatus.Running;
|
||||
} else {
|
||||
// 子节点正在执行
|
||||
node.status = TaskStatus.Running;
|
||||
}
|
||||
|
||||
// 如果子节点完成了
|
||||
if (childNode.status === TaskStatus.Success || childNode.status === TaskStatus.Failure) {
|
||||
// 反转结果
|
||||
node.status = childNode.status === TaskStatus.Success ? TaskStatus.Failure : TaskStatus.Success;
|
||||
this.completeNode(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重复装饰器
|
||||
*/
|
||||
private handleRepeater(
|
||||
entity: Entity,
|
||||
node: BehaviorTreeNode,
|
||||
decorator: DecoratorNodeComponent,
|
||||
child: Entity,
|
||||
childNode: BehaviorTreeNode
|
||||
): void {
|
||||
const repeater = decorator as RepeaterNode;
|
||||
|
||||
// 从 PropertyBindings 读取绑定的黑板变量值
|
||||
const repeatCount = this.resolvePropertyValue(entity, 'repeatCount', repeater.repeatCount);
|
||||
const endOnFailure = this.resolvePropertyValue(entity, 'endOnFailure', repeater.endOnFailure);
|
||||
|
||||
// 如果子节点未激活,激活它
|
||||
if (!child.hasComponent(ActiveNode)) {
|
||||
child.addComponent(new ActiveNode());
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
}
|
||||
|
||||
// 子节点正在执行
|
||||
if (childNode.status === TaskStatus.Running) {
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
}
|
||||
|
||||
// 子节点完成
|
||||
if (childNode.status === TaskStatus.Failure && endOnFailure) {
|
||||
node.status = TaskStatus.Failure;
|
||||
repeater.reset();
|
||||
this.completeNode(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
// 增加重复计数
|
||||
repeater.incrementRepeat();
|
||||
|
||||
// 检查是否继续重复(使用解析后的值)
|
||||
const shouldContinue = (repeatCount === -1) || (repeater.currentRepeatCount < repeatCount);
|
||||
if (shouldContinue) {
|
||||
// 重置子节点并继续
|
||||
childNode.invalidate();
|
||||
child.addComponent(new ActiveNode());
|
||||
node.status = TaskStatus.Running;
|
||||
} else {
|
||||
// 完成
|
||||
node.status = TaskStatus.Success;
|
||||
repeater.reset();
|
||||
this.completeNode(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 直到成功装饰器
|
||||
*/
|
||||
private handleUntilSuccess(entity: Entity, node: BehaviorTreeNode, child: Entity, childNode: BehaviorTreeNode): void {
|
||||
if (!child.hasComponent(ActiveNode)) {
|
||||
child.addComponent(new ActiveNode());
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
}
|
||||
|
||||
if (childNode.status === TaskStatus.Running) {
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
}
|
||||
|
||||
if (childNode.status === TaskStatus.Success) {
|
||||
node.status = TaskStatus.Success;
|
||||
this.completeNode(entity);
|
||||
} else {
|
||||
// 失败则重试
|
||||
childNode.invalidate();
|
||||
child.addComponent(new ActiveNode());
|
||||
node.status = TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 直到失败装饰器
|
||||
*/
|
||||
private handleUntilFail(entity: Entity, node: BehaviorTreeNode, child: Entity, childNode: BehaviorTreeNode): void {
|
||||
if (!child.hasComponent(ActiveNode)) {
|
||||
child.addComponent(new ActiveNode());
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
}
|
||||
|
||||
if (childNode.status === TaskStatus.Running) {
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
}
|
||||
|
||||
if (childNode.status === TaskStatus.Failure) {
|
||||
node.status = TaskStatus.Success;
|
||||
this.completeNode(entity);
|
||||
} else {
|
||||
// 成功则重试
|
||||
childNode.invalidate();
|
||||
child.addComponent(new ActiveNode());
|
||||
node.status = TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 总是成功装饰器
|
||||
*/
|
||||
private handleAlwaysSucceed(entity: Entity, node: BehaviorTreeNode, child: Entity, childNode: BehaviorTreeNode): void {
|
||||
if (!child.hasComponent(ActiveNode)) {
|
||||
child.addComponent(new ActiveNode());
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
}
|
||||
|
||||
if (childNode.status === TaskStatus.Running) {
|
||||
node.status = TaskStatus.Running;
|
||||
} else {
|
||||
node.status = TaskStatus.Success;
|
||||
this.completeNode(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 总是失败装饰器
|
||||
*/
|
||||
private handleAlwaysFail(entity: Entity, node: BehaviorTreeNode, child: Entity, childNode: BehaviorTreeNode): void {
|
||||
if (!child.hasComponent(ActiveNode)) {
|
||||
child.addComponent(new ActiveNode());
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
}
|
||||
|
||||
if (childNode.status === TaskStatus.Running) {
|
||||
node.status = TaskStatus.Running;
|
||||
} else {
|
||||
node.status = TaskStatus.Failure;
|
||||
this.completeNode(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 条件装饰器
|
||||
*/
|
||||
private handleConditional(
|
||||
entity: Entity,
|
||||
node: BehaviorTreeNode,
|
||||
decorator: DecoratorNodeComponent,
|
||||
child: Entity,
|
||||
childNode: BehaviorTreeNode
|
||||
): void {
|
||||
const conditional = decorator as ConditionalNode;
|
||||
|
||||
// 评估条件
|
||||
const conditionMet = conditional.evaluateCondition(entity, this.findBlackboard(entity));
|
||||
|
||||
if (!conditionMet) {
|
||||
// 条件不满足,直接失败
|
||||
node.status = TaskStatus.Failure;
|
||||
this.completeNode(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
// 条件满足,执行子节点
|
||||
if (!child.hasComponent(ActiveNode)) {
|
||||
child.addComponent(new ActiveNode());
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
}
|
||||
|
||||
node.status = childNode.status;
|
||||
|
||||
if (childNode.status !== TaskStatus.Running) {
|
||||
this.completeNode(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 冷却装饰器
|
||||
*/
|
||||
private handleCooldown(
|
||||
entity: Entity,
|
||||
node: BehaviorTreeNode,
|
||||
decorator: DecoratorNodeComponent,
|
||||
child: Entity,
|
||||
childNode: BehaviorTreeNode
|
||||
): void {
|
||||
const cooldown = decorator as CooldownNode;
|
||||
|
||||
// 从 PropertyBindings 读取绑定的黑板变量值
|
||||
const cooldownTime = this.resolvePropertyValue(entity, 'cooldownTime', cooldown.cooldownTime);
|
||||
|
||||
// 检查冷却(使用解析后的值)
|
||||
// 如果从未执行过(lastExecutionTime === 0),允许执行
|
||||
const timeSinceLastExecution = Time.totalTime - cooldown.lastExecutionTime;
|
||||
const canExecute = (cooldown.lastExecutionTime === 0) || (timeSinceLastExecution >= cooldownTime);
|
||||
|
||||
// 添加调试日志
|
||||
this.outputLog(
|
||||
entity,
|
||||
`[冷却检查] Time.totalTime=${Time.totalTime.toFixed(3)}, lastExecution=${cooldown.lastExecutionTime.toFixed(3)}, ` +
|
||||
`cooldownTime=${cooldownTime}, timeSince=${timeSinceLastExecution.toFixed(3)}, canExecute=${canExecute}, childStatus=${childNode.status}`,
|
||||
'info'
|
||||
);
|
||||
|
||||
if (!canExecute) {
|
||||
node.status = TaskStatus.Failure;
|
||||
this.completeNode(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
// 先检查子节点状态,再决定是否激活
|
||||
if (childNode.status !== TaskStatus.Invalid && childNode.status !== TaskStatus.Running) {
|
||||
// 子节点已经完成(Success 或 Failure)
|
||||
node.status = childNode.status;
|
||||
cooldown.recordExecution(Time.totalTime);
|
||||
this.outputLog(
|
||||
entity,
|
||||
`[冷却记录] 记录执行时间: ${Time.totalTime.toFixed(3)}, 下次可执行时间: ${(Time.totalTime + cooldownTime).toFixed(3)}`,
|
||||
'info'
|
||||
);
|
||||
this.completeNode(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
// 子节点还没开始或正在执行
|
||||
if (!child.hasComponent(ActiveNode)) {
|
||||
child.addComponent(new ActiveNode());
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
}
|
||||
|
||||
node.status = TaskStatus.Running;
|
||||
}
|
||||
|
||||
/**
|
||||
* 超时装饰器
|
||||
*/
|
||||
private handleTimeout(
|
||||
entity: Entity,
|
||||
node: BehaviorTreeNode,
|
||||
decorator: DecoratorNodeComponent,
|
||||
child: Entity,
|
||||
childNode: BehaviorTreeNode
|
||||
): void {
|
||||
const timeout = decorator as TimeoutNode;
|
||||
|
||||
// 从 PropertyBindings 读取绑定的黑板变量值
|
||||
const timeoutDuration = this.resolvePropertyValue(entity, 'timeoutDuration', timeout.timeoutDuration);
|
||||
|
||||
timeout.recordStartTime(Time.totalTime);
|
||||
|
||||
// 检查超时(使用解析后的值)
|
||||
const isTimeout = timeout.startTime > 0 && (Time.totalTime - timeout.startTime >= timeoutDuration);
|
||||
if (isTimeout) {
|
||||
node.status = TaskStatus.Failure;
|
||||
timeout.reset();
|
||||
// 移除子节点的活跃标记
|
||||
child.removeComponentByType(ActiveNode);
|
||||
this.completeNode(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!child.hasComponent(ActiveNode)) {
|
||||
child.addComponent(new ActiveNode());
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
}
|
||||
|
||||
node.status = childNode.status;
|
||||
|
||||
if (childNode.status !== TaskStatus.Running) {
|
||||
timeout.reset();
|
||||
this.completeNode(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成节点执行
|
||||
*/
|
||||
private completeNode(entity: Entity): void {
|
||||
entity.removeComponentByType(ActiveNode);
|
||||
|
||||
// 通知父节点
|
||||
if (entity.parent && entity.parent.hasComponent(BehaviorTreeNode)) {
|
||||
if (!entity.parent.hasComponent(ActiveNode)) {
|
||||
entity.parent.addComponent(new ActiveNode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找黑板组件(向上遍历父节点)
|
||||
*/
|
||||
private findBlackboard(entity: Entity): BlackboardComponent | undefined {
|
||||
let current: Entity | null = entity;
|
||||
|
||||
while (current) {
|
||||
const blackboard = current.getComponent(BlackboardComponent);
|
||||
if (blackboard) {
|
||||
return blackboard;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析属性值
|
||||
* 如果属性绑定到黑板变量,从黑板读取最新值
|
||||
*/
|
||||
private resolvePropertyValue(entity: Entity, propertyName: string, defaultValue: any): any {
|
||||
const bindings = entity.getComponent(PropertyBindings);
|
||||
if (!bindings || !bindings.hasBinding(propertyName)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const blackboardKey = bindings.getBinding(propertyName)!;
|
||||
const blackboard = this.findBlackboard(entity);
|
||||
|
||||
if (!blackboard || !blackboard.hasVariable(blackboardKey)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return blackboard.getValue(blackboardKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找根实体
|
||||
*/
|
||||
private findRootEntity(entity: Entity): Entity | null {
|
||||
let current: Entity | null = entity;
|
||||
while (current) {
|
||||
if (!current.parent) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的日志输出方法
|
||||
* 同时输出到控制台和LogOutput组件,确保用户在UI中能看到
|
||||
*/
|
||||
private outputLog(
|
||||
entity: Entity,
|
||||
message: string,
|
||||
level: 'log' | 'info' | 'warn' | 'error' = 'info'
|
||||
): void {
|
||||
switch (level) {
|
||||
case 'info':
|
||||
this.logger.info(message);
|
||||
break;
|
||||
case 'warn':
|
||||
this.logger.warn(message);
|
||||
break;
|
||||
case 'error':
|
||||
this.logger.error(message);
|
||||
break;
|
||||
default:
|
||||
this.logger.info(message);
|
||||
break;
|
||||
}
|
||||
|
||||
const rootEntity = this.findRootEntity(entity);
|
||||
if (rootEntity) {
|
||||
const logOutput = rootEntity.getComponent(LogOutput);
|
||||
if (logOutput) {
|
||||
logOutput.addMessage(message, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override getLoggerName(): string {
|
||||
return 'DecoratorExecutionSystem';
|
||||
}
|
||||
}
|
||||
@@ -1,603 +0,0 @@
|
||||
import { EntitySystem, Matcher, Entity, Time } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeNode } from '../Components/BehaviorTreeNode';
|
||||
import { BlackboardComponent } from '../Components/BlackboardComponent';
|
||||
import { ActiveNode } from '../Components/ActiveNode';
|
||||
import { PropertyBindings } from '../Components/PropertyBindings';
|
||||
import { LogOutput } from '../Components/LogOutput';
|
||||
import { TaskStatus, NodeType } from '../Types/TaskStatus';
|
||||
|
||||
// 导入具体的动作组件
|
||||
import { WaitAction } from '../Components/Actions/WaitAction';
|
||||
import { LogAction } from '../Components/Actions/LogAction';
|
||||
import { SetBlackboardValueAction } from '../Components/Actions/SetBlackboardValueAction';
|
||||
import { ModifyBlackboardValueAction, ModifyOperation } 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';
|
||||
|
||||
/**
|
||||
* 叶子节点执行系统
|
||||
*
|
||||
* 负责执行所有活跃的叶子节点(Action 和 Condition)
|
||||
* 只处理带有 ActiveNode 标记的节点
|
||||
*
|
||||
* updateOrder: 100 (最先执行)
|
||||
*/
|
||||
export class LeafExecutionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 只处理活跃的叶子节点
|
||||
super(Matcher.empty().all(BehaviorTreeNode, ActiveNode));
|
||||
this.updateOrder = 100;
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const node = entity.getComponent(BehaviorTreeNode)!;
|
||||
|
||||
// 只处理叶子节点
|
||||
if (node.nodeType === NodeType.Action) {
|
||||
this.executeAction(entity, node);
|
||||
} else if (node.nodeType === NodeType.Condition) {
|
||||
this.executeCondition(entity, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行动作节点
|
||||
*/
|
||||
private executeAction(entity: Entity, node: BehaviorTreeNode): void {
|
||||
let status = TaskStatus.Failure;
|
||||
|
||||
const { displayName, nodeIdShort } = this.getNodeInfo(entity);
|
||||
|
||||
// 检测实体有哪些动作组件并执行
|
||||
if (entity.hasComponent(WaitAction)) {
|
||||
status = this.executeWaitAction(entity);
|
||||
} else if (entity.hasComponent(LogAction)) {
|
||||
status = this.executeLogAction(entity);
|
||||
} else if (entity.hasComponent(SetBlackboardValueAction)) {
|
||||
status = this.executeSetBlackboardValue(entity);
|
||||
} else if (entity.hasComponent(ModifyBlackboardValueAction)) {
|
||||
status = this.executeModifyBlackboardValue(entity);
|
||||
} else if (entity.hasComponent(ExecuteAction)) {
|
||||
status = this.executeCustomAction(entity);
|
||||
} else {
|
||||
status = this.executeGenericAction(entity);
|
||||
if (status === TaskStatus.Failure) {
|
||||
this.outputLog(entity, `动作节点没有找到任何已知的动作组件`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
node.status = status;
|
||||
|
||||
// 输出节点执行后的状态
|
||||
const statusText = status === TaskStatus.Success ? 'Success' :
|
||||
status === TaskStatus.Failure ? 'Failure' :
|
||||
status === TaskStatus.Running ? 'Running' : 'Unknown';
|
||||
|
||||
if (status !== TaskStatus.Running) {
|
||||
this.outputLog(entity, `[${displayName}#${nodeIdShort}] 执行完成 -> ${statusText}`,
|
||||
status === TaskStatus.Success ? 'info' : 'warn');
|
||||
}
|
||||
|
||||
// 如果不是 Running 状态,节点执行完成
|
||||
if (status !== TaskStatus.Running) {
|
||||
this.deactivateNode(entity);
|
||||
this.notifyParent(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行等待动作
|
||||
*/
|
||||
private executeWaitAction(entity: Entity): TaskStatus {
|
||||
const waitAction = entity.getComponent(WaitAction)!;
|
||||
const node = entity.getComponent(BehaviorTreeNode);
|
||||
|
||||
const { displayName, nodeIdShort } = this.getNodeInfo(entity);
|
||||
|
||||
// 从 PropertyBindings 读取绑定的黑板变量值
|
||||
const waitTime = this.resolvePropertyValue(entity, 'waitTime', waitAction.waitTime);
|
||||
|
||||
waitAction.elapsedTime += Time.deltaTime;
|
||||
|
||||
// 输出调试信息(显示在UI中)
|
||||
this.outputLog(
|
||||
entity,
|
||||
`[${displayName}#${nodeIdShort}] deltaTime=${Time.deltaTime.toFixed(3)}s, ` +
|
||||
`elapsed=${waitAction.elapsedTime.toFixed(3)}s/${waitTime.toFixed(3)}s`,
|
||||
'info'
|
||||
);
|
||||
|
||||
if (waitAction.elapsedTime >= waitTime) {
|
||||
waitAction.reset();
|
||||
this.outputLog(entity, `[${displayName}#${nodeIdShort}] 等待完成,返回成功`, 'info');
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行日志动作
|
||||
*/
|
||||
private executeLogAction(entity: Entity): TaskStatus {
|
||||
const logAction = entity.getComponent(LogAction)!;
|
||||
const node = entity.getComponent(BehaviorTreeNode);
|
||||
|
||||
// 从 PropertyBindings 读取绑定的黑板变量值
|
||||
let message = this.resolvePropertyValue(entity, 'message', logAction.message);
|
||||
|
||||
const { displayName, nodeIdShort } = this.getNodeInfo(entity);
|
||||
|
||||
// 在消息前添加节点ID信息
|
||||
if (node) {
|
||||
message = `[${displayName}#${nodeIdShort}] ${message}`;
|
||||
}
|
||||
|
||||
if (logAction.includeEntityInfo) {
|
||||
message = `[Entity: ${entity.name}] ${message}`;
|
||||
}
|
||||
|
||||
// 输出到浏览器控制台
|
||||
switch (logAction.level) {
|
||||
case 'info':
|
||||
console.info(message);
|
||||
break;
|
||||
case 'warn':
|
||||
console.warn(message);
|
||||
break;
|
||||
case 'error':
|
||||
console.error(message);
|
||||
break;
|
||||
default:
|
||||
console.log(message);
|
||||
break;
|
||||
}
|
||||
|
||||
// 同时记录到LogOutput组件,以便在UI中显示
|
||||
const rootEntity = this.findRootEntity(entity);
|
||||
if (rootEntity) {
|
||||
const logOutput = rootEntity.getComponent(LogOutput);
|
||||
if (logOutput) {
|
||||
logOutput.addMessage(message, logAction.level);
|
||||
}
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找根实体
|
||||
*/
|
||||
private findRootEntity(entity: Entity): Entity | null {
|
||||
let current: Entity | null = entity;
|
||||
while (current) {
|
||||
if (!current.parent) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行设置黑板变量值
|
||||
*/
|
||||
private executeSetBlackboardValue(entity: Entity): TaskStatus {
|
||||
const action = entity.getComponent(SetBlackboardValueAction)!;
|
||||
const blackboard = this.findBlackboard(entity);
|
||||
|
||||
if (!blackboard) {
|
||||
this.outputLog(entity, '未找到黑板组件', 'warn');
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
let valueToSet: any;
|
||||
|
||||
// 如果指定了源变量,从中读取值
|
||||
if (action.sourceVariable) {
|
||||
if (!blackboard.hasVariable(action.sourceVariable)) {
|
||||
this.outputLog(entity, `源变量不存在: ${action.sourceVariable}`, 'warn');
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
valueToSet = blackboard.getValue(action.sourceVariable);
|
||||
} else {
|
||||
// 从 PropertyBindings 读取绑定的值
|
||||
valueToSet = this.resolvePropertyValue(entity, 'value', action.value);
|
||||
}
|
||||
|
||||
const success = blackboard.setValue(action.variableName, valueToSet, action.force);
|
||||
return success ? TaskStatus.Success : TaskStatus.Failure;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行修改黑板变量值
|
||||
*/
|
||||
private executeModifyBlackboardValue(entity: Entity): TaskStatus {
|
||||
const action = entity.getComponent(ModifyBlackboardValueAction)!;
|
||||
const blackboard = this.findBlackboard(entity);
|
||||
|
||||
if (!blackboard) {
|
||||
this.outputLog(entity, '未找到黑板组件', 'warn');
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (!blackboard.hasVariable(action.variableName)) {
|
||||
this.outputLog(entity, `变量不存在: ${action.variableName}`, 'warn');
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
let currentValue = blackboard.getValue(action.variableName);
|
||||
|
||||
// 从 PropertyBindings 读取绑定的值
|
||||
let operand = this.resolvePropertyValue(entity, 'operand', action.operand);
|
||||
|
||||
// 执行操作
|
||||
let newValue: any;
|
||||
switch (action.operation) {
|
||||
case ModifyOperation.Add:
|
||||
newValue = Number(currentValue) + Number(operand);
|
||||
break;
|
||||
case ModifyOperation.Subtract:
|
||||
newValue = Number(currentValue) - Number(operand);
|
||||
break;
|
||||
case ModifyOperation.Multiply:
|
||||
newValue = Number(currentValue) * Number(operand);
|
||||
break;
|
||||
case ModifyOperation.Divide:
|
||||
if (Number(operand) === 0) {
|
||||
this.outputLog(entity, '除数不能为0', 'warn');
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
newValue = Number(currentValue) / Number(operand);
|
||||
break;
|
||||
case ModifyOperation.Modulo:
|
||||
newValue = Number(currentValue) % Number(operand);
|
||||
break;
|
||||
case ModifyOperation.Append:
|
||||
if (Array.isArray(currentValue)) {
|
||||
newValue = [...currentValue, operand];
|
||||
} else if (typeof currentValue === 'string') {
|
||||
newValue = currentValue + operand;
|
||||
} else {
|
||||
this.outputLog(entity, `变量 ${action.variableName} 不支持 append 操作`, 'warn');
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
break;
|
||||
case ModifyOperation.Remove:
|
||||
if (Array.isArray(currentValue)) {
|
||||
newValue = currentValue.filter(item => item !== operand);
|
||||
} else {
|
||||
this.outputLog(entity, `变量 ${action.variableName} 不是数组,不支持 remove 操作`, 'warn');
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const success = blackboard.setValue(action.variableName, newValue, action.force);
|
||||
return success ? TaskStatus.Success : TaskStatus.Failure;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行自定义动作
|
||||
*/
|
||||
private executeCustomAction(entity: Entity): TaskStatus {
|
||||
const action = entity.getComponent(ExecuteAction)!;
|
||||
const func = action.getFunction();
|
||||
|
||||
if (!func) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const blackboard = this.findBlackboard(entity);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行条件节点
|
||||
*/
|
||||
private executeCondition(entity: Entity, node: BehaviorTreeNode): void {
|
||||
let result = false;
|
||||
|
||||
const { displayName, nodeIdShort } = this.getNodeInfo(entity);
|
||||
|
||||
// 检测实体有哪些条件组件并评估
|
||||
if (entity.hasComponent(BlackboardCompareCondition)) {
|
||||
result = this.evaluateBlackboardCompare(entity);
|
||||
} else if (entity.hasComponent(BlackboardExistsCondition)) {
|
||||
result = this.evaluateBlackboardExists(entity);
|
||||
} else if (entity.hasComponent(RandomProbabilityCondition)) {
|
||||
result = this.evaluateRandomProbability(entity);
|
||||
} else if (entity.hasComponent(ExecuteCondition)) {
|
||||
result = this.evaluateCustomCondition(entity);
|
||||
} else {
|
||||
this.outputLog(entity, '条件节点没有找到任何已知的条件组件', 'warn');
|
||||
}
|
||||
|
||||
node.status = result ? TaskStatus.Success : TaskStatus.Failure;
|
||||
|
||||
// 输出条件评估结果
|
||||
const statusText = result ? 'Success (true)' : 'Failure (false)';
|
||||
this.outputLog(entity, `[${displayName}#${nodeIdShort}] 条件评估 -> ${statusText}`,
|
||||
result ? 'info' : 'warn');
|
||||
|
||||
// 条件节点总是立即完成
|
||||
this.deactivateNode(entity);
|
||||
this.notifyParent(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估黑板比较条件
|
||||
*/
|
||||
private evaluateBlackboardCompare(entity: Entity): boolean {
|
||||
const condition = entity.getComponent(BlackboardCompareCondition)!;
|
||||
const blackboard = this.findBlackboard(entity);
|
||||
|
||||
if (!blackboard || !blackboard.hasVariable(condition.variableName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const value = blackboard.getValue(condition.variableName);
|
||||
|
||||
// 从 PropertyBindings 读取绑定的值
|
||||
let compareValue = this.resolvePropertyValue(entity, 'compareValue', condition.compareValue);
|
||||
|
||||
let result = false;
|
||||
switch (condition.operator) {
|
||||
case CompareOperator.Equal:
|
||||
result = value === compareValue;
|
||||
break;
|
||||
case CompareOperator.NotEqual:
|
||||
result = value !== compareValue;
|
||||
break;
|
||||
case CompareOperator.Greater:
|
||||
result = value > compareValue;
|
||||
break;
|
||||
case CompareOperator.GreaterOrEqual:
|
||||
result = value >= compareValue;
|
||||
break;
|
||||
case CompareOperator.Less:
|
||||
result = value < compareValue;
|
||||
break;
|
||||
case CompareOperator.LessOrEqual:
|
||||
result = value <= compareValue;
|
||||
break;
|
||||
case CompareOperator.Contains:
|
||||
if (typeof value === 'string') {
|
||||
result = value.includes(compareValue);
|
||||
} else if (Array.isArray(value)) {
|
||||
result = value.includes(compareValue);
|
||||
}
|
||||
break;
|
||||
case CompareOperator.Matches:
|
||||
if (typeof value === 'string' && typeof compareValue === 'string') {
|
||||
const regex = new RegExp(compareValue);
|
||||
result = regex.test(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return condition.invertResult ? !result : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估黑板变量存在性
|
||||
*/
|
||||
private evaluateBlackboardExists(entity: Entity): boolean {
|
||||
const condition = entity.getComponent(BlackboardExistsCondition)!;
|
||||
const blackboard = this.findBlackboard(entity);
|
||||
|
||||
if (!blackboard) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let result = blackboard.hasVariable(condition.variableName);
|
||||
|
||||
if (result && condition.checkNotNull) {
|
||||
const value = blackboard.getValue(condition.variableName);
|
||||
result = value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
return condition.invertResult ? !result : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估随机概率
|
||||
*/
|
||||
private evaluateRandomProbability(entity: Entity): boolean {
|
||||
const condition = entity.getComponent(RandomProbabilityCondition)!;
|
||||
|
||||
// 从 PropertyBindings 读取绑定的黑板变量值
|
||||
const probability = this.resolvePropertyValue(entity, 'probability', condition.probability);
|
||||
|
||||
// 使用解析后的概率值进行评估
|
||||
if (condition.alwaysRandomize || condition['cachedResult'] === undefined) {
|
||||
condition['cachedResult'] = Math.random() < probability;
|
||||
}
|
||||
return condition['cachedResult'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估自定义条件
|
||||
*/
|
||||
private evaluateCustomCondition(entity: Entity): boolean {
|
||||
const condition = entity.getComponent(ExecuteCondition)!;
|
||||
const func = condition.getFunction();
|
||||
|
||||
if (!func) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const blackboard = this.findBlackboard(entity);
|
||||
const result = func(entity, blackboard, Time.deltaTime);
|
||||
|
||||
return condition.invertResult ? !result : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析属性值
|
||||
* 如果属性绑定到黑板变量,从黑板读取最新值
|
||||
*/
|
||||
private resolvePropertyValue(entity: Entity, propertyName: string, defaultValue: any): any {
|
||||
// 检查实体是否有 PropertyBindings 组件
|
||||
const bindings = entity.getComponent(PropertyBindings);
|
||||
if (!bindings || !bindings.hasBinding(propertyName)) {
|
||||
// 没有绑定,返回默认值
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// 有绑定,从黑板读取值
|
||||
const blackboardKey = bindings.getBinding(propertyName)!;
|
||||
const blackboard = this.findBlackboard(entity);
|
||||
|
||||
if (!blackboard) {
|
||||
this.outputLog(entity, `[属性绑定] 未找到黑板组件,实体: ${entity.name}`, 'warn');
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (!blackboard.hasVariable(blackboardKey)) {
|
||||
this.outputLog(entity, `[属性绑定] 黑板变量不存在: ${blackboardKey}`, 'warn');
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const value = blackboard.getValue(blackboardKey);
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 移除节点的活跃标记
|
||||
*/
|
||||
private deactivateNode(entity: Entity): void {
|
||||
entity.removeComponentByType(ActiveNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知父节点子节点已完成
|
||||
*/
|
||||
private notifyParent(entity: Entity): void {
|
||||
if (entity.parent && entity.parent.hasComponent(BehaviorTreeNode)) {
|
||||
// 为父节点添加活跃标记,让它在下一帧被处理
|
||||
if (!entity.parent.hasComponent(ActiveNode)) {
|
||||
entity.parent.addComponent(new ActiveNode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找黑板组件(向上遍历父节点)
|
||||
*/
|
||||
private findBlackboard(entity: Entity): BlackboardComponent | undefined {
|
||||
let current: Entity | null = entity;
|
||||
|
||||
while (current) {
|
||||
const blackboard = current.getComponent(BlackboardComponent);
|
||||
if (blackboard) {
|
||||
return blackboard;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Entity提取节点显示名称和ID
|
||||
*/
|
||||
private getNodeInfo(entity: Entity): { displayName: string; nodeIdShort: string } {
|
||||
let displayName = 'Node';
|
||||
let nodeIdShort = '';
|
||||
|
||||
if (entity.name && entity.name.includes('#')) {
|
||||
const parts = entity.name.split('#');
|
||||
displayName = parts[0];
|
||||
nodeIdShort = parts[1];
|
||||
} else {
|
||||
nodeIdShort = entity.id.toString().substring(0, 8);
|
||||
}
|
||||
|
||||
return { displayName, nodeIdShort };
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的日志输出方法
|
||||
* 同时输出到控制台和LogOutput组件,确保用户在UI中能看到
|
||||
*/
|
||||
private outputLog(
|
||||
entity: Entity,
|
||||
message: string,
|
||||
level: 'log' | 'info' | 'warn' | 'error' = 'info'
|
||||
): void {
|
||||
// 输出到浏览器控制台(方便开发调试)
|
||||
switch (level) {
|
||||
case 'info':
|
||||
this.logger.info(message);
|
||||
break;
|
||||
case 'warn':
|
||||
this.logger.warn(message);
|
||||
break;
|
||||
case 'error':
|
||||
this.logger.error(message);
|
||||
break;
|
||||
default:
|
||||
this.logger.info(message);
|
||||
break;
|
||||
}
|
||||
|
||||
// 输出到LogOutput组件(显示在UI中)
|
||||
const rootEntity = this.findRootEntity(entity);
|
||||
if (rootEntity) {
|
||||
const logOutput = rootEntity.getComponent(LogOutput);
|
||||
if (logOutput) {
|
||||
logOutput.addMessage(message, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override getLoggerName(): string {
|
||||
return 'LeafExecutionSystem';
|
||||
}
|
||||
}
|
||||
@@ -1,388 +0,0 @@
|
||||
import { EntitySystem, Matcher, Entity, Core } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeNode } from '../Components/BehaviorTreeNode';
|
||||
import { RootNode } from '../Components/Composites/RootNode';
|
||||
import { ActiveNode } from '../Components/ActiveNode';
|
||||
import { TaskStatus, NodeType } from '../Types/TaskStatus';
|
||||
import { SubTreeNode } from '../Components/Composites/SubTreeNode';
|
||||
import { LogOutput } from '../Components/LogOutput';
|
||||
import { FileSystemAssetLoader } from '../Services/FileSystemAssetLoader';
|
||||
import { BehaviorTreeAssetLoader } from '../Serialization/BehaviorTreeAssetLoader';
|
||||
import { BehaviorTreeAssetMetadata } from '../Components/AssetMetadata';
|
||||
import { BlackboardComponent } from '../Components/BlackboardComponent';
|
||||
|
||||
/**
|
||||
* 预加载状态
|
||||
*/
|
||||
enum PreloadState {
|
||||
/** 未开始预加载 */
|
||||
NotStarted,
|
||||
/** 正在预加载 */
|
||||
Loading,
|
||||
/** 预加载完成 */
|
||||
Completed,
|
||||
/** 预加载失败 */
|
||||
Failed
|
||||
}
|
||||
|
||||
/**
|
||||
* 根节点执行系统
|
||||
*
|
||||
* 专门处理根节点的执行逻辑
|
||||
* 根节点的职责:
|
||||
* 1. 扫描并预加载所有标记为 preload=true 的子树
|
||||
* 2. 激活第一个子节点,并根据子节点的状态来设置自己的状态
|
||||
*
|
||||
* updateOrder: 350 (在所有其他执行系统之后)
|
||||
*/
|
||||
export class RootExecutionSystem extends EntitySystem {
|
||||
/** 跟踪每个根节点的预加载状态 */
|
||||
private preloadStates: Map<number, PreloadState> = new Map();
|
||||
|
||||
/** 跟踪预加载任务 */
|
||||
private preloadTasks: Map<number, Promise<void>> = new Map();
|
||||
|
||||
/** AssetLoader 实例 */
|
||||
private assetLoader?: FileSystemAssetLoader;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.empty().all(BehaviorTreeNode, ActiveNode));
|
||||
this.updateOrder = 350;
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const node = entity.getComponent(BehaviorTreeNode)!;
|
||||
|
||||
// 只处理根节点
|
||||
if (node.nodeType !== NodeType.Composite) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否是根节点
|
||||
if (!entity.hasComponent(RootNode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.executeRoot(entity, node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行根节点逻辑
|
||||
*/
|
||||
private executeRoot(entity: Entity, node: BehaviorTreeNode): void {
|
||||
// 检查预加载状态
|
||||
const preloadState = this.preloadStates.get(entity.id) || PreloadState.NotStarted;
|
||||
|
||||
if (preloadState === PreloadState.NotStarted) {
|
||||
// 开始预加载
|
||||
this.startPreload(entity, node);
|
||||
return;
|
||||
} else if (preloadState === PreloadState.Loading) {
|
||||
// 正在预加载,等待
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
} else if (preloadState === PreloadState.Failed) {
|
||||
// 预加载失败,标记为失败
|
||||
node.status = TaskStatus.Failure;
|
||||
entity.removeComponentByType(ActiveNode);
|
||||
return;
|
||||
}
|
||||
|
||||
// 预加载完成,执行正常逻辑
|
||||
const children = entity.children;
|
||||
|
||||
// 如果没有子节点,标记为成功
|
||||
if (children.length === 0) {
|
||||
node.status = TaskStatus.Success;
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取第一个子节点
|
||||
const firstChild = children[0];
|
||||
const childNode = firstChild.getComponent(BehaviorTreeNode);
|
||||
|
||||
if (!childNode) {
|
||||
node.status = TaskStatus.Failure;
|
||||
return;
|
||||
}
|
||||
|
||||
// 激活第一个子节点(如果还没激活)
|
||||
if (!firstChild.hasComponent(ActiveNode)) {
|
||||
firstChild.addComponent(new ActiveNode());
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据第一个子节点的状态来设置根节点的状态
|
||||
if (childNode.status === TaskStatus.Running) {
|
||||
node.status = TaskStatus.Running;
|
||||
} else if (childNode.status === TaskStatus.Success) {
|
||||
node.status = TaskStatus.Success;
|
||||
// 移除根节点的 ActiveNode,结束整个行为树
|
||||
entity.removeComponentByType(ActiveNode);
|
||||
} else if (childNode.status === TaskStatus.Failure) {
|
||||
node.status = TaskStatus.Failure;
|
||||
// 移除根节点的 ActiveNode,结束整个行为树
|
||||
entity.removeComponentByType(ActiveNode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始预加载子树
|
||||
*/
|
||||
private startPreload(rootEntity: Entity, node: BehaviorTreeNode): void {
|
||||
// 扫描所有需要预加载的子树节点
|
||||
const subTreeNodesToPreload = this.scanSubTreeNodes(rootEntity);
|
||||
|
||||
if (subTreeNodesToPreload.length === 0) {
|
||||
// 没有需要预加载的子树,直接标记为完成
|
||||
this.preloadStates.set(rootEntity.id, PreloadState.Completed);
|
||||
this.outputLog(rootEntity, '没有需要预加载的子树', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记为正在加载
|
||||
this.preloadStates.set(rootEntity.id, PreloadState.Loading);
|
||||
node.status = TaskStatus.Running;
|
||||
|
||||
this.outputLog(
|
||||
rootEntity,
|
||||
`开始预加载 ${subTreeNodesToPreload.length} 个子树...`,
|
||||
'info'
|
||||
);
|
||||
|
||||
// 并行加载所有子树
|
||||
const loadTask = this.preloadAllSubTrees(rootEntity, subTreeNodesToPreload);
|
||||
this.preloadTasks.set(rootEntity.id, loadTask);
|
||||
|
||||
// 异步处理加载结果
|
||||
loadTask.then(() => {
|
||||
this.preloadStates.set(rootEntity.id, PreloadState.Completed);
|
||||
this.outputLog(rootEntity, '所有子树预加载完成', 'info');
|
||||
}).catch(error => {
|
||||
this.preloadStates.set(rootEntity.id, PreloadState.Failed);
|
||||
this.outputLog(rootEntity, `子树预加载失败: ${error.message}`, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描所有需要预加载的子树节点
|
||||
*/
|
||||
private scanSubTreeNodes(entity: Entity): Array<{ entity: Entity; subTree: SubTreeNode }> {
|
||||
const result: Array<{ entity: Entity; subTree: SubTreeNode }> = [];
|
||||
|
||||
// 检查当前实体
|
||||
const subTree = entity.getComponent(SubTreeNode);
|
||||
if (subTree && subTree.preload) {
|
||||
result.push({ entity, subTree });
|
||||
}
|
||||
|
||||
// 递归扫描子节点
|
||||
for (const child of entity.children) {
|
||||
result.push(...this.scanSubTreeNodes(child));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载所有子树
|
||||
*/
|
||||
private async preloadAllSubTrees(
|
||||
rootEntity: Entity,
|
||||
subTreeNodes: Array<{ entity: Entity; subTree: SubTreeNode }>
|
||||
): Promise<void> {
|
||||
// 确保 AssetLoader 已初始化
|
||||
if (!this.assetLoader) {
|
||||
try {
|
||||
this.assetLoader = Core.services.resolve(FileSystemAssetLoader);
|
||||
} catch (error) {
|
||||
throw new Error('AssetLoader 未配置,无法预加载子树');
|
||||
}
|
||||
}
|
||||
|
||||
// 并行加载所有子树
|
||||
await Promise.all(
|
||||
subTreeNodes.map(({ entity, subTree }) =>
|
||||
this.preloadSingleSubTree(rootEntity, entity, subTree)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载单个子树
|
||||
*/
|
||||
private async preloadSingleSubTree(
|
||||
rootEntity: Entity,
|
||||
subTreeEntity: Entity,
|
||||
subTree: SubTreeNode
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.outputLog(rootEntity, `预加载子树: ${subTree.assetId}`, 'info');
|
||||
|
||||
// 加载资产
|
||||
const asset = await this.assetLoader!.loadBehaviorTree(subTree.assetId);
|
||||
|
||||
// 实例化为 Entity 树(作为子树,跳过 RootNode)
|
||||
const subTreeRoot = BehaviorTreeAssetLoader.instantiate(asset, this.scene!, {
|
||||
asSubTree: true
|
||||
});
|
||||
|
||||
// 设置子树根实体
|
||||
subTree.setSubTreeRoot(subTreeRoot);
|
||||
|
||||
// 将子树根实体设置为 SubTreeNode 的子节点,这样子树中的节点可以通过 parent 链找到主树的根节点
|
||||
subTreeEntity.addChild(subTreeRoot);
|
||||
|
||||
// 添加资产元数据
|
||||
const metadata = subTreeRoot.addComponent(new BehaviorTreeAssetMetadata());
|
||||
metadata.initialize(subTree.assetId, '1.0.0');
|
||||
|
||||
// 处理黑板继承
|
||||
if (subTree.inheritParentBlackboard) {
|
||||
this.setupBlackboardInheritance(subTreeEntity, subTreeRoot);
|
||||
}
|
||||
|
||||
// 输出子树内部结构(用于调试)
|
||||
this.outputLog(rootEntity, `=== 预加载子树 ${subTree.assetId} 的内部结构 ===`, 'info');
|
||||
this.logSubTreeStructure(rootEntity, subTreeRoot, 0);
|
||||
this.outputLog(rootEntity, `=== 预加载子树结构结束 ===`, 'info');
|
||||
|
||||
this.outputLog(rootEntity, `✓ 子树 ${subTree.assetId} 预加载完成`, 'info');
|
||||
} catch (error: any) {
|
||||
this.outputLog(
|
||||
rootEntity,
|
||||
`✗ 子树 ${subTree.assetId} 预加载失败: ${error.message}`,
|
||||
'error'
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置黑板继承
|
||||
*/
|
||||
private setupBlackboardInheritance(parentEntity: Entity, subTreeRoot: Entity): void {
|
||||
const parentBlackboard = this.findBlackboard(parentEntity);
|
||||
if (!parentBlackboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 找到子树的黑板
|
||||
const subTreeBlackboard = subTreeRoot.getComponent(BlackboardComponent);
|
||||
if (subTreeBlackboard) {
|
||||
subTreeBlackboard.setUseGlobalBlackboard(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找黑板组件
|
||||
*/
|
||||
private findBlackboard(entity: Entity): BlackboardComponent | undefined {
|
||||
let current: Entity | null = entity;
|
||||
|
||||
while (current) {
|
||||
const blackboard = current.getComponent(BlackboardComponent);
|
||||
if (blackboard) {
|
||||
return blackboard;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找根实体
|
||||
*/
|
||||
private findRootEntity(entity: Entity): Entity | null {
|
||||
let current: Entity | null = entity;
|
||||
while (current) {
|
||||
if (!current.parent) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的日志输出方法
|
||||
*/
|
||||
private outputLog(
|
||||
entity: Entity,
|
||||
message: string,
|
||||
level: 'log' | 'info' | 'warn' | 'error' = 'info'
|
||||
): void {
|
||||
// 输出到控制台
|
||||
switch (level) {
|
||||
case 'info':
|
||||
this.logger.info(message);
|
||||
break;
|
||||
case 'warn':
|
||||
this.logger.warn(message);
|
||||
break;
|
||||
case 'error':
|
||||
this.logger.error(message);
|
||||
break;
|
||||
default:
|
||||
this.logger.info(message);
|
||||
break;
|
||||
}
|
||||
|
||||
// 输出到LogOutput组件
|
||||
const rootEntity = this.findRootEntity(entity);
|
||||
if (rootEntity) {
|
||||
const logOutput = rootEntity.getComponent(LogOutput);
|
||||
if (logOutput) {
|
||||
logOutput.addMessage(message, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归打印子树结构(用于调试)
|
||||
*/
|
||||
private logSubTreeStructure(parentEntity: Entity, entity: Entity, depth: number): void {
|
||||
const indent = ' '.repeat(depth);
|
||||
const btNode = entity.getComponent(BehaviorTreeNode);
|
||||
|
||||
// 获取节点的具体类型组件
|
||||
const allComponents = entity.components.map(c => c.constructor.name);
|
||||
const nodeTypeComponent = allComponents.find(name =>
|
||||
name !== 'BehaviorTreeNode' && name !== 'ActiveNode' &&
|
||||
name !== 'BlackboardComponent' && name !== 'LogOutput' &&
|
||||
name !== 'PropertyBindings' && name !== 'BehaviorTreeAssetMetadata'
|
||||
) || 'Unknown';
|
||||
|
||||
// 构建节点显示名称
|
||||
let nodeName = entity.name;
|
||||
if (nodeTypeComponent !== 'Unknown') {
|
||||
nodeName = `${nodeName} [${nodeTypeComponent}]`;
|
||||
}
|
||||
|
||||
this.outputLog(parentEntity, `${indent}└─ ${nodeName}`, 'info');
|
||||
|
||||
// 递归打印子节点
|
||||
if (entity.children.length > 0) {
|
||||
this.outputLog(parentEntity, `${indent} 子节点数: ${entity.children.length}`, 'info');
|
||||
entity.children.forEach((child: Entity) => {
|
||||
this.logSubTreeStructure(parentEntity, child, depth + 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
protected override onDestroy(): void {
|
||||
this.preloadStates.clear();
|
||||
this.preloadTasks.clear();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
protected override getLoggerName(): string {
|
||||
return 'RootExecutionSystem';
|
||||
}
|
||||
}
|
||||
@@ -1,667 +0,0 @@
|
||||
import { EntitySystem, Matcher, Entity, Core, createLogger } from '@esengine/ecs-framework';
|
||||
import { SubTreeNode } from '../Components/Composites/SubTreeNode';
|
||||
import { ActiveNode } from '../Components/ActiveNode';
|
||||
import { BehaviorTreeNode } from '../Components/BehaviorTreeNode';
|
||||
import { TaskStatus } from '../Types/TaskStatus';
|
||||
import { IAssetLoader } from '../Services/IAssetLoader';
|
||||
import { FileSystemAssetLoader } from '../Services/FileSystemAssetLoader';
|
||||
import { BehaviorTreeAssetLoader } from '../Serialization/BehaviorTreeAssetLoader';
|
||||
import { BlackboardComponent } from '../Components/BlackboardComponent';
|
||||
import { LogOutput } from '../Components/LogOutput';
|
||||
import { AssetLoadingManager } from '../Services/AssetLoadingManager';
|
||||
import {
|
||||
LoadingState,
|
||||
LoadingTaskHandle,
|
||||
CircularDependencyError,
|
||||
EntityDestroyedError
|
||||
} from '../Services/AssetLoadingTypes';
|
||||
import { BehaviorTreeAssetMetadata } from '../Components/AssetMetadata';
|
||||
|
||||
/**
|
||||
* SubTree 执行系统
|
||||
*
|
||||
* 处理 SubTree 节点的执行,包括:
|
||||
* - 子树资产加载
|
||||
* - 子树实例化
|
||||
* - 黑板继承
|
||||
* - 子树执行和状态管理
|
||||
*
|
||||
* updateOrder: 300 (与 CompositeExecutionSystem 同级)
|
||||
*/
|
||||
export class SubTreeExecutionSystem extends EntitySystem {
|
||||
private assetLoader?: IAssetLoader;
|
||||
private assetLoaderInitialized = false;
|
||||
private hasLoggedMissingAssetLoader = false;
|
||||
private loadingManager: AssetLoadingManager;
|
||||
private loadingTasks: Map<number, LoadingTaskHandle> = new Map();
|
||||
|
||||
constructor(loadingManager?: AssetLoadingManager) {
|
||||
super(Matcher.empty().all(SubTreeNode, ActiveNode, BehaviorTreeNode));
|
||||
this.updateOrder = 300;
|
||||
this.loadingManager = loadingManager || new AssetLoadingManager();
|
||||
}
|
||||
|
||||
protected override onInitialize(): void {
|
||||
// 延迟初始化 AssetLoader,不在这里尝试获取
|
||||
// 只在第一次真正需要处理 SubTree 节点时才获取
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const subTree = entity.getComponent(SubTreeNode)!;
|
||||
const node = entity.getComponent(BehaviorTreeNode)!;
|
||||
|
||||
this.executeSubTree(entity, subTree, node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行子树节点
|
||||
*/
|
||||
private executeSubTree(
|
||||
entity: Entity,
|
||||
subTree: SubTreeNode,
|
||||
node: BehaviorTreeNode
|
||||
): void {
|
||||
// 验证配置
|
||||
const errors = subTree.validate();
|
||||
if (errors.length > 0) {
|
||||
this.logger.error(`SubTree 节点配置错误: ${errors.join(', ')}`);
|
||||
node.status = TaskStatus.Failure;
|
||||
this.completeNode(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已有子树(可能是预加载的)
|
||||
const existingSubTreeRoot = subTree.getSubTreeRoot();
|
||||
if (existingSubTreeRoot) {
|
||||
const subTreeNode = existingSubTreeRoot.getComponent(BehaviorTreeNode);
|
||||
|
||||
if (subTreeNode) {
|
||||
const statusName = TaskStatus[subTreeNode.status];
|
||||
const hasActive = existingSubTreeRoot.hasComponent(ActiveNode);
|
||||
this.outputLog(
|
||||
entity,
|
||||
`检查预加载子树 ${subTree.assetId}: status=${statusName}, hasActive=${hasActive}`,
|
||||
'info'
|
||||
);
|
||||
|
||||
// 如果子树还没开始执行(状态是 Invalid),需要激活它
|
||||
if (subTreeNode.status === TaskStatus.Invalid) {
|
||||
this.outputLog(entity, `使用预加载的子树: ${subTree.assetId}`, 'info');
|
||||
|
||||
// 检查子节点
|
||||
this.outputLog(entity, `激活前:子树根节点 ${existingSubTreeRoot.name} 有 ${existingSubTreeRoot.children.length} 个子节点`, 'info');
|
||||
if (existingSubTreeRoot.children.length > 0) {
|
||||
const firstChild = existingSubTreeRoot.children[0];
|
||||
this.outputLog(entity, ` 第一个子节点: ${firstChild.name}`, 'info');
|
||||
}
|
||||
|
||||
// 激活根节点
|
||||
if (!existingSubTreeRoot.hasComponent(ActiveNode)) {
|
||||
existingSubTreeRoot.addComponent(new ActiveNode());
|
||||
this.outputLog(entity, `为子树根节点添加 ActiveNode: ${existingSubTreeRoot.name}`, 'info');
|
||||
}
|
||||
|
||||
const subTreeRootNode = existingSubTreeRoot.getComponent(BehaviorTreeNode);
|
||||
if (subTreeRootNode) {
|
||||
this.outputLog(entity, `设置子树根节点状态: ${existingSubTreeRoot.name} -> Running`, 'info');
|
||||
subTreeRootNode.status = TaskStatus.Running;
|
||||
}
|
||||
|
||||
// 再次检查(验证激活后子节点没有丢失)
|
||||
this.outputLog(entity, `激活后:子树根节点 ${existingSubTreeRoot.name} 有 ${existingSubTreeRoot.children.length} 个子节点`, 'info');
|
||||
|
||||
this.outputLog(entity, `激活预加载的子树: ${subTree.assetId}`, 'info');
|
||||
node.status = TaskStatus.Running;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 子树已激活或已完成,更新状态
|
||||
this.updateSubTree(entity, subTree, node);
|
||||
return;
|
||||
}
|
||||
|
||||
// 子树未预加载,开始运行时加载
|
||||
this.outputLog(entity, `子树未预加载,开始运行时加载: ${subTree.assetId}`, 'info');
|
||||
this.loadAndInstantiateSubTree(entity, subTree, node);
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟初始化 AssetLoader
|
||||
*/
|
||||
private ensureAssetLoaderInitialized(): boolean {
|
||||
if (!this.assetLoaderInitialized) {
|
||||
try {
|
||||
this.assetLoader = Core.services.resolve(FileSystemAssetLoader);
|
||||
this.assetLoaderInitialized = true;
|
||||
this.logger.debug('AssetLoader 已初始化');
|
||||
} catch (error) {
|
||||
this.assetLoaderInitialized = true;
|
||||
this.assetLoader = undefined;
|
||||
|
||||
// 只在第一次失败时记录警告,避免重复日志
|
||||
if (!this.hasLoggedMissingAssetLoader) {
|
||||
this.logger.warn(
|
||||
'AssetLoader 未配置。SubTree 节点需要 AssetLoader 来加载子树资产。\n' +
|
||||
'如果您在编辑器中,请确保已打开项目并配置了项目路径。\n' +
|
||||
'如果您在运行时环境,请确保已正确注册 FileSystemAssetLoader 服务。'
|
||||
);
|
||||
this.hasLoggedMissingAssetLoader = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return this.assetLoader !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载并实例化子树(使用加载管理器)
|
||||
*/
|
||||
private loadAndInstantiateSubTree(
|
||||
parentEntity: Entity,
|
||||
subTree: SubTreeNode,
|
||||
node: BehaviorTreeNode
|
||||
): void {
|
||||
// 延迟初始化 AssetLoader
|
||||
if (!this.ensureAssetLoaderInitialized()) {
|
||||
this.logger.debug('AssetLoader 不可用,SubTree 节点执行失败');
|
||||
node.status = TaskStatus.Failure;
|
||||
this.completeNode(parentEntity);
|
||||
return;
|
||||
}
|
||||
|
||||
const assetId = subTree.assetId;
|
||||
|
||||
// 检查是否有正在进行的加载任务
|
||||
let taskHandle = this.loadingTasks.get(parentEntity.id);
|
||||
|
||||
if (taskHandle) {
|
||||
// 轮询检查状态
|
||||
const state = taskHandle.getState();
|
||||
|
||||
switch (state) {
|
||||
case LoadingState.Loading:
|
||||
case LoadingState.Pending:
|
||||
// 仍在加载中
|
||||
node.status = TaskStatus.Running;
|
||||
|
||||
// 输出进度信息
|
||||
const progress = taskHandle.getProgress();
|
||||
if (progress.elapsedMs > 1000) {
|
||||
this.logger.debug(
|
||||
`子树加载中: ${assetId} (已耗时: ${Math.round(progress.elapsedMs / 1000)}s, ` +
|
||||
`重试: ${progress.retryCount}/${progress.maxRetries})`
|
||||
);
|
||||
}
|
||||
return;
|
||||
|
||||
case LoadingState.Loaded:
|
||||
// 加载完成
|
||||
this.onLoadingComplete(parentEntity, subTree, node, taskHandle);
|
||||
return;
|
||||
|
||||
case LoadingState.Failed:
|
||||
case LoadingState.Timeout:
|
||||
// 加载失败
|
||||
const error = taskHandle.getError();
|
||||
this.outputLog(
|
||||
parentEntity,
|
||||
`子树加载失败: ${assetId} - ${error?.message || '未知错误'}`,
|
||||
'error'
|
||||
);
|
||||
node.status = TaskStatus.Failure;
|
||||
this.loadingTasks.delete(parentEntity.id);
|
||||
this.completeNode(parentEntity);
|
||||
return;
|
||||
|
||||
case LoadingState.Cancelled:
|
||||
// 已取消(实体被销毁)
|
||||
this.loadingTasks.delete(parentEntity.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 开始新的加载任务
|
||||
this.startNewLoading(parentEntity, subTree, node);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始新的加载任务
|
||||
*/
|
||||
private startNewLoading(
|
||||
parentEntity: Entity,
|
||||
subTree: SubTreeNode,
|
||||
node: BehaviorTreeNode
|
||||
): void {
|
||||
const assetId = subTree.assetId;
|
||||
|
||||
// 获取父树的资产ID(用于循环检测)
|
||||
const parentAssetId = this.getParentTreeAssetId(parentEntity);
|
||||
|
||||
try {
|
||||
// 使用加载管理器
|
||||
const taskHandle = this.loadingManager.startLoading(
|
||||
assetId,
|
||||
parentEntity,
|
||||
() => this.loadAsset(assetId),
|
||||
{
|
||||
timeoutMs: 5000,
|
||||
maxRetries: 2,
|
||||
parentAssetId: parentAssetId
|
||||
}
|
||||
);
|
||||
|
||||
this.loadingTasks.set(parentEntity.id, taskHandle);
|
||||
node.status = TaskStatus.Running;
|
||||
|
||||
this.outputLog(
|
||||
parentEntity,
|
||||
`开始加载子树: ${assetId} (父树: ${parentAssetId || 'none'})`,
|
||||
'info'
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof CircularDependencyError) {
|
||||
this.outputLog(parentEntity, `检测到循环引用: ${error.message}`, 'error');
|
||||
} else {
|
||||
this.outputLog(parentEntity, `启动加载失败: ${assetId}`, 'error');
|
||||
}
|
||||
|
||||
node.status = TaskStatus.Failure;
|
||||
this.completeNode(parentEntity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载完成时的处理
|
||||
*/
|
||||
private onLoadingComplete(
|
||||
parentEntity: Entity,
|
||||
subTree: SubTreeNode,
|
||||
node: BehaviorTreeNode,
|
||||
taskHandle: LoadingTaskHandle
|
||||
): void {
|
||||
// 获取加载结果
|
||||
taskHandle.promise.then(subTreeRoot => {
|
||||
// 再次检查实体是否存在
|
||||
if (parentEntity.isDestroyed) {
|
||||
this.logger.warn(`父实体已销毁,丢弃加载结果: ${taskHandle.assetId}`);
|
||||
subTreeRoot.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置子树
|
||||
subTree.setSubTreeRoot(subTreeRoot);
|
||||
|
||||
// 将子树根实体设置为 SubTreeNode 的子节点,这样子树中的节点可以通过 parent 链找到主树的根节点
|
||||
parentEntity.addChild(subTreeRoot);
|
||||
|
||||
// 添加资产元数据(用于循环检测)
|
||||
const metadata = subTreeRoot.addComponent(new BehaviorTreeAssetMetadata());
|
||||
metadata.initialize(taskHandle.assetId, '1.0.0');
|
||||
|
||||
// 处理黑板继承
|
||||
if (subTree.inheritParentBlackboard) {
|
||||
this.setupBlackboardInheritance(parentEntity, subTreeRoot);
|
||||
}
|
||||
|
||||
this.outputLog(parentEntity, `子树 ${taskHandle.assetId} 加载成功并激活`, 'info');
|
||||
|
||||
// 打印子树结构(用于调试)
|
||||
this.outputLog(parentEntity, `=== 子树 ${taskHandle.assetId} 内部结构 ===`, 'info');
|
||||
this.logSubTreeStructure(parentEntity, subTreeRoot, 0);
|
||||
this.outputLog(parentEntity, `=== 子树结构结束 ===`, 'info');
|
||||
|
||||
// 激活子树执行
|
||||
this.startSubTreeExecution(subTreeRoot, parentEntity);
|
||||
|
||||
// 清理任务
|
||||
this.loadingTasks.delete(parentEntity.id);
|
||||
|
||||
}).catch(error => {
|
||||
// 这里不应该到达,因为错误应该在状态机中处理了
|
||||
if (!(error instanceof EntityDestroyedError)) {
|
||||
this.logger.error('意外错误:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载资产
|
||||
*/
|
||||
private async loadAsset(assetId: string): Promise<Entity> {
|
||||
if (!this.scene) {
|
||||
throw new Error('Scene 不存在');
|
||||
}
|
||||
|
||||
// 加载资产
|
||||
const asset = await this.assetLoader!.loadBehaviorTree(assetId);
|
||||
|
||||
// 实例化为 Entity 树(作为子树,跳过 RootNode)
|
||||
const rootEntity = BehaviorTreeAssetLoader.instantiate(asset, this.scene, {
|
||||
asSubTree: true
|
||||
});
|
||||
|
||||
return rootEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置黑板继承
|
||||
*/
|
||||
private setupBlackboardInheritance(parentEntity: Entity, subTreeRoot: Entity): void {
|
||||
const parentBlackboard = this.findBlackboard(parentEntity);
|
||||
if (!parentBlackboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 找到子树的黑板
|
||||
const subTreeBlackboard = subTreeRoot.getComponent(BlackboardComponent);
|
||||
if (subTreeBlackboard) {
|
||||
// 启用全局黑板查找(这样子树可以访问父树的变量)
|
||||
subTreeBlackboard.setUseGlobalBlackboard(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找黑板
|
||||
*/
|
||||
private findBlackboard(entity: Entity): BlackboardComponent | undefined {
|
||||
let current: Entity | null = entity;
|
||||
|
||||
while (current) {
|
||||
const blackboard = current.getComponent(BlackboardComponent);
|
||||
if (blackboard) {
|
||||
return blackboard;
|
||||
}
|
||||
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始子树执行
|
||||
*/
|
||||
private startSubTreeExecution(subTreeRoot: Entity, parentEntity?: Entity): void {
|
||||
// 调试:检查子树根节点的子节点
|
||||
if (parentEntity) {
|
||||
this.outputLog(parentEntity, `子树根节点 ${subTreeRoot.name} 有 ${subTreeRoot.children.length} 个子节点`, 'info');
|
||||
}
|
||||
|
||||
// 激活根节点
|
||||
if (!subTreeRoot.hasComponent(ActiveNode)) {
|
||||
subTreeRoot.addComponent(new ActiveNode());
|
||||
if (parentEntity) {
|
||||
this.outputLog(parentEntity, `为子树根节点添加 ActiveNode: ${subTreeRoot.name}`, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
const node = subTreeRoot.getComponent(BehaviorTreeNode);
|
||||
if (node) {
|
||||
if (parentEntity) {
|
||||
this.outputLog(parentEntity, `设置子树根节点状态: ${subTreeRoot.name} -> Running`, 'info');
|
||||
}
|
||||
node.status = TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新子树状态
|
||||
*/
|
||||
private updateSubTree(
|
||||
parentEntity: Entity,
|
||||
subTree: SubTreeNode,
|
||||
node: BehaviorTreeNode
|
||||
): void {
|
||||
const subTreeRoot = subTree.getSubTreeRoot();
|
||||
if (!subTreeRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查子树是否完成
|
||||
const subTreeNode = subTreeRoot.getComponent(BehaviorTreeNode);
|
||||
if (!subTreeNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 输出子树当前状态(调试)
|
||||
const statusName = TaskStatus[subTreeNode.status];
|
||||
this.outputLog(
|
||||
parentEntity,
|
||||
`子树 ${subTree.assetId} 当前状态: ${statusName}`,
|
||||
'info'
|
||||
);
|
||||
|
||||
if (subTreeNode.status !== TaskStatus.Running) {
|
||||
// 子树完成
|
||||
this.onSubTreeCompleted(parentEntity, subTree, node, subTreeNode.status);
|
||||
} else {
|
||||
// 子树仍在运行
|
||||
node.status = TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 子树完成时的处理
|
||||
*/
|
||||
private onSubTreeCompleted(
|
||||
parentEntity: Entity,
|
||||
subTree: SubTreeNode,
|
||||
node: BehaviorTreeNode,
|
||||
subTreeStatus: TaskStatus
|
||||
): void {
|
||||
this.outputLog(parentEntity, `子树完成,状态: ${TaskStatus[subTreeStatus]}`, 'info');
|
||||
|
||||
// 检查完成前 SubTreeNode 的子节点
|
||||
this.outputLog(parentEntity, `完成前:SubTreeNode ${parentEntity.name} 有 ${parentEntity.children.length} 个子节点`, 'info');
|
||||
|
||||
// 标记子树完成
|
||||
subTree.markSubTreeCompleted(subTreeStatus);
|
||||
|
||||
// 决定父节点状态
|
||||
if (subTreeStatus === TaskStatus.Success) {
|
||||
node.status = TaskStatus.Success;
|
||||
} else if (subTreeStatus === TaskStatus.Failure) {
|
||||
if (subTree.propagateFailure) {
|
||||
node.status = TaskStatus.Failure;
|
||||
} else {
|
||||
// 忽略失败,返回成功
|
||||
node.status = TaskStatus.Success;
|
||||
}
|
||||
} else {
|
||||
node.status = subTreeStatus;
|
||||
}
|
||||
|
||||
// 清理子树
|
||||
this.cleanupSubTree(subTree);
|
||||
|
||||
// 检查清理后 SubTreeNode 的子节点
|
||||
this.outputLog(parentEntity, `清理后:SubTreeNode ${parentEntity.name} 有 ${parentEntity.children.length} 个子节点`, 'info');
|
||||
|
||||
// 完成父节点
|
||||
this.completeNode(parentEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理子树
|
||||
*/
|
||||
private cleanupSubTree(subTree: SubTreeNode): void {
|
||||
const subTreeRoot = subTree.getSubTreeRoot();
|
||||
if (!subTreeRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是预加载的子树,不销毁,只重置状态以便复用
|
||||
if (subTree.preload) {
|
||||
this.logger.debug(`重置预加载子树以便复用: ${subTree.assetId}`);
|
||||
|
||||
// 递归重置整个子树的所有节点
|
||||
this.resetSubTreeRecursively(subTreeRoot);
|
||||
|
||||
// 重置 SubTreeNode 的完成状态,但保留 subTreeRoot 引用
|
||||
subTree.resetCompletionState();
|
||||
} else {
|
||||
// 运行时加载的子树,销毁并清理
|
||||
this.logger.debug(`销毁运行时加载的子树: ${subTree.assetId}`);
|
||||
subTreeRoot.destroy();
|
||||
subTree.setSubTreeRoot(undefined);
|
||||
subTree.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归重置子树的所有节点
|
||||
*/
|
||||
private resetSubTreeRecursively(entity: Entity): void {
|
||||
// 移除 ActiveNode
|
||||
if (entity.hasComponent(ActiveNode)) {
|
||||
entity.removeComponentByType(ActiveNode);
|
||||
}
|
||||
|
||||
// 重置节点状态
|
||||
const node = entity.getComponent(BehaviorTreeNode);
|
||||
if (node) {
|
||||
node.status = TaskStatus.Invalid;
|
||||
}
|
||||
|
||||
// 递归处理子节点
|
||||
for (const child of entity.children) {
|
||||
this.resetSubTreeRecursively(child);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成节点执行
|
||||
*/
|
||||
private completeNode(entity: Entity): void {
|
||||
entity.removeComponentByType(ActiveNode);
|
||||
|
||||
// 通知父节点
|
||||
if (entity.parent && entity.parent.hasComponent(BehaviorTreeNode)) {
|
||||
if (!entity.parent.hasComponent(ActiveNode)) {
|
||||
entity.parent.addComponent(new ActiveNode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取父树的资产ID(用于循环检测)
|
||||
*/
|
||||
private getParentTreeAssetId(entity: Entity): string | undefined {
|
||||
let current: Entity | null = entity;
|
||||
|
||||
while (current) {
|
||||
// 查找带有资产元数据的组件
|
||||
const metadata = current.getComponent(BehaviorTreeAssetMetadata);
|
||||
if (metadata && metadata.assetId) {
|
||||
return metadata.assetId;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统销毁时清理
|
||||
*/
|
||||
protected override onDestroy(): void {
|
||||
// 取消所有正在加载的任务
|
||||
for (const taskHandle of this.loadingTasks.values()) {
|
||||
taskHandle.cancel();
|
||||
}
|
||||
this.loadingTasks.clear();
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找根实体
|
||||
*/
|
||||
private findRootEntity(entity: Entity): Entity | null {
|
||||
let current: Entity | null = entity;
|
||||
while (current) {
|
||||
if (!current.parent) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的日志输出方法
|
||||
* 同时输出到控制台和LogOutput组件,确保用户在UI中能看到
|
||||
*/
|
||||
private outputLog(
|
||||
entity: Entity,
|
||||
message: string,
|
||||
level: 'log' | 'info' | 'warn' | 'error' = 'info'
|
||||
): void {
|
||||
// 输出到浏览器控制台(方便开发调试)
|
||||
switch (level) {
|
||||
case 'info':
|
||||
this.logger.info(message);
|
||||
break;
|
||||
case 'warn':
|
||||
this.logger.warn(message);
|
||||
break;
|
||||
case 'error':
|
||||
this.logger.error(message);
|
||||
break;
|
||||
default:
|
||||
this.logger.info(message);
|
||||
break;
|
||||
}
|
||||
|
||||
// 输出到LogOutput组件(显示在UI中)
|
||||
const rootEntity = this.findRootEntity(entity);
|
||||
if (rootEntity) {
|
||||
const logOutput = rootEntity.getComponent(LogOutput);
|
||||
if (logOutput) {
|
||||
logOutput.addMessage(message, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归打印子树结构(用于调试)
|
||||
*/
|
||||
private logSubTreeStructure(parentEntity: Entity, entity: Entity, depth: number): void {
|
||||
const indent = ' '.repeat(depth);
|
||||
const btNode = entity.getComponent(BehaviorTreeNode);
|
||||
|
||||
// 获取节点的具体类型组件
|
||||
const allComponents = entity.components.map(c => c.constructor.name);
|
||||
const nodeTypeComponent = allComponents.find(name =>
|
||||
name !== 'BehaviorTreeNode' && name !== 'ActiveNode' &&
|
||||
name !== 'BlackboardComponent' && name !== 'LogOutput' &&
|
||||
name !== 'PropertyBindings' && name !== 'BehaviorTreeAssetMetadata'
|
||||
) || 'Unknown';
|
||||
|
||||
// 构建节点显示名称
|
||||
let nodeName = entity.name;
|
||||
if (nodeTypeComponent !== 'Unknown') {
|
||||
nodeName = `${nodeName} [${nodeTypeComponent}]`;
|
||||
}
|
||||
|
||||
this.outputLog(parentEntity, `${indent}└─ ${nodeName}`, 'info');
|
||||
|
||||
// 递归打印子节点
|
||||
if (entity.children.length > 0) {
|
||||
this.outputLog(parentEntity, `${indent} 子节点数: ${entity.children.length}`, 'info');
|
||||
entity.children.forEach((child: Entity) => {
|
||||
this.logSubTreeStructure(parentEntity, child, depth + 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override getLoggerName(): string {
|
||||
return 'SubTreeExecutionSystem';
|
||||
}
|
||||
}
|
||||
@@ -112,3 +112,14 @@ export enum BlackboardValueType {
|
||||
Object = 'object',
|
||||
Array = 'array'
|
||||
}
|
||||
|
||||
/**
|
||||
* 黑板变量定义
|
||||
*/
|
||||
export interface BlackboardVariable {
|
||||
name: string;
|
||||
type: BlackboardValueType;
|
||||
value: any;
|
||||
readonly?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@@ -1,92 +1,29 @@
|
||||
/**
|
||||
* @esengine/behavior-tree
|
||||
*
|
||||
* 完全ECS化的行为树系统
|
||||
* 行为树系统
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
// 注册所有内置节点
|
||||
import './RegisterAllNodes';
|
||||
|
||||
// 类型定义
|
||||
export * from './Types/TaskStatus';
|
||||
|
||||
// 基础组件
|
||||
export * from './Components/BehaviorTreeNode';
|
||||
export * from './Components/BlackboardComponent';
|
||||
export * from './Components/CompositeNodeComponent';
|
||||
export * from './Components/DecoratorNodeComponent';
|
||||
export * from './Components/ActiveNode';
|
||||
export * from './Components/PropertyBindings';
|
||||
export * from './Components/LogOutput';
|
||||
export * from './Components/AssetMetadata';
|
||||
|
||||
// 动作组件
|
||||
export * from './Components/Actions/WaitAction';
|
||||
export * from './Components/Actions/LogAction';
|
||||
export * from './Components/Actions/SetBlackboardValueAction';
|
||||
export * from './Components/Actions/ModifyBlackboardValueAction';
|
||||
export * from './Components/Actions/ExecuteAction';
|
||||
|
||||
// 条件组件
|
||||
export * from './Components/Conditions/BlackboardCompareCondition';
|
||||
export * from './Components/Conditions/BlackboardExistsCondition';
|
||||
export * from './Components/Conditions/RandomProbabilityCondition';
|
||||
export * from './Components/Conditions/ExecuteCondition';
|
||||
|
||||
// 组合节点
|
||||
export * from './Components/Composites/RootNode';
|
||||
export * from './Components/Composites/SequenceNode';
|
||||
export * from './Components/Composites/SelectorNode';
|
||||
export * from './Components/Composites/ParallelNode';
|
||||
export * from './Components/Composites/ParallelSelectorNode';
|
||||
export * from './Components/Composites/RandomSequenceNode';
|
||||
export * from './Components/Composites/RandomSelectorNode';
|
||||
export * from './Components/Composites/SubTreeNode';
|
||||
|
||||
// 装饰器节点
|
||||
export * from './Components/Decorators/InverterNode';
|
||||
export * from './Components/Decorators/RepeaterNode';
|
||||
export * from './Components/Decorators/UntilSuccessNode';
|
||||
export * from './Components/Decorators/UntilFailNode';
|
||||
export * from './Components/Decorators/AlwaysSucceedNode';
|
||||
export * from './Components/Decorators/AlwaysFailNode';
|
||||
export * from './Components/Decorators/ConditionalNode';
|
||||
export * from './Components/Decorators/CooldownNode';
|
||||
export * from './Components/Decorators/TimeoutNode';
|
||||
|
||||
// 系统
|
||||
export * from './Systems/RootExecutionSystem';
|
||||
export * from './Systems/LeafExecutionSystem';
|
||||
export * from './Systems/DecoratorExecutionSystem';
|
||||
export * from './Systems/CompositeExecutionSystem';
|
||||
export * from './Systems/SubTreeExecutionSystem';
|
||||
|
||||
// 服务
|
||||
export * from './Services/GlobalBlackboardService';
|
||||
export * from './Services/WorkspaceService';
|
||||
export * from './Services/IAssetLoader';
|
||||
export * from './Services/FileSystemAssetLoader';
|
||||
export * from './Services/AssetLoadingManager';
|
||||
export * from './Services/AssetLoadingTypes';
|
||||
|
||||
// 插件
|
||||
export * from './BehaviorTreePlugin';
|
||||
// Runtime
|
||||
export * from './Runtime';
|
||||
|
||||
// 辅助工具
|
||||
export * from './BehaviorTreeStarter';
|
||||
export * from './BehaviorTreeBuilder';
|
||||
|
||||
// 序列化(编辑器支持)
|
||||
export * from './Serialization/BehaviorTreePersistence';
|
||||
// 序列化
|
||||
export * from './Serialization/NodeTemplates';
|
||||
|
||||
// 资产系统(运行时)
|
||||
export * from './Serialization/BehaviorTreeAsset';
|
||||
export * from './Serialization/BehaviorTreeAssetSerializer';
|
||||
export * from './Serialization/BehaviorTreeAssetLoader';
|
||||
export * from './Serialization/EditorFormatConverter';
|
||||
export * from './Serialization/BehaviorTreeAssetSerializer';
|
||||
|
||||
// 装饰器(扩展支持)
|
||||
export * from './Decorators/BehaviorNodeDecorator';
|
||||
// 服务
|
||||
export * from './Services/GlobalBlackboardService';
|
||||
|
||||
// 插件
|
||||
export * from './BehaviorTreePlugin';
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Repeat, CheckCircle, XCircle, CheckCheck, HelpCircle, Snowflake, Timer,
|
||||
Clock, FileText, Edit, Calculator, Code,
|
||||
Equal, Dices, Settings,
|
||||
Database, AlertTriangle, Search, X,
|
||||
Database, AlertTriangle, AlertCircle, Search, X,
|
||||
LucideIcon
|
||||
} from 'lucide-react';
|
||||
import { ask } from '@tauri-apps/plugin-dialog';
|
||||
@@ -199,6 +199,17 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
isExecuting
|
||||
} = useBehaviorTreeStore();
|
||||
|
||||
// 右键菜单状态
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
visible: boolean;
|
||||
position: { x: number; y: number };
|
||||
nodeId: string | null;
|
||||
}>({
|
||||
visible: false,
|
||||
position: { x: 0, y: 0 },
|
||||
nodeId: null
|
||||
});
|
||||
|
||||
// 初始化根节点(仅在首次挂载时检查)
|
||||
useEffect(() => {
|
||||
if (nodes.length === 0) {
|
||||
@@ -212,6 +223,20 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 初始化executor用于检查执行器是否存在
|
||||
useEffect(() => {
|
||||
if (!executorRef.current) {
|
||||
executorRef.current = new BehaviorTreeExecutor();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (executorRef.current) {
|
||||
executorRef.current.destroy();
|
||||
executorRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 组件挂载和连线变化时强制更新,确保连线能正确渲染
|
||||
useEffect(() => {
|
||||
if (nodes.length > 0 || connections.length > 0) {
|
||||
@@ -223,6 +248,20 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
}
|
||||
}, [nodes.length, connections.length]);
|
||||
|
||||
// 点击其他地方关闭右键菜单
|
||||
useEffect(() => {
|
||||
const handleClick = () => {
|
||||
if (contextMenu.visible) {
|
||||
setContextMenu({ ...contextMenu, visible: false });
|
||||
}
|
||||
};
|
||||
|
||||
if (contextMenu.visible) {
|
||||
document.addEventListener('click', handleClick);
|
||||
return () => document.removeEventListener('click', handleClick);
|
||||
}
|
||||
}, [contextMenu.visible]);
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
||||
const canvasRef = useRef<HTMLDivElement>(null);
|
||||
@@ -233,11 +272,15 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
position: { x: number; y: number };
|
||||
searchText: string;
|
||||
selectedIndex: number;
|
||||
mode: 'create' | 'replace';
|
||||
replaceNodeId: string | null;
|
||||
}>({
|
||||
visible: false,
|
||||
position: { x: 0, y: 0 },
|
||||
searchText: '',
|
||||
selectedIndex: 0
|
||||
selectedIndex: 0,
|
||||
mode: 'create',
|
||||
replaceNodeId: null
|
||||
});
|
||||
const selectedNodeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -485,6 +528,83 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
onNodeSelect?.(node);
|
||||
};
|
||||
|
||||
const handleNodeContextMenu = (e: React.MouseEvent, node: BehaviorTreeNode) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 不允许对Root节点右键
|
||||
if (node.id === ROOT_NODE_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
setContextMenu({
|
||||
visible: true,
|
||||
position: { x: e.clientX, y: e.clientY },
|
||||
nodeId: node.id
|
||||
});
|
||||
};
|
||||
|
||||
const handleReplaceNode = (newTemplate: NodeTemplate) => {
|
||||
const nodeToReplace = nodes.find(n => n.id === quickCreateMenu.replaceNodeId);
|
||||
if (!nodeToReplace) return;
|
||||
|
||||
// 如果行为树正在执行,先停止
|
||||
if (executionMode !== 'idle') {
|
||||
handleStop();
|
||||
}
|
||||
|
||||
// 合并数据:新模板的默认配置 + 保留旧节点中同名属性的值
|
||||
const newData = { ...newTemplate.defaultConfig };
|
||||
|
||||
// 获取新模板的属性名列表
|
||||
const newPropertyNames = new Set(newTemplate.properties.map(p => p.name));
|
||||
|
||||
// 遍历旧节点的 data,保留新模板中也存在的属性
|
||||
for (const [key, value] of Object.entries(nodeToReplace.data)) {
|
||||
// 跳过节点类型相关的字段
|
||||
if (key === 'nodeType' || key === 'compositeType' || key === 'decoratorType' ||
|
||||
key === 'actionType' || key === 'conditionType') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果新模板也有这个属性,保留旧值(包括绑定信息)
|
||||
if (newPropertyNames.has(key)) {
|
||||
newData[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新节点,保留原节点的位置和连接
|
||||
const newNode: BehaviorTreeNode = {
|
||||
id: nodeToReplace.id,
|
||||
template: newTemplate,
|
||||
data: newData,
|
||||
position: nodeToReplace.position,
|
||||
children: nodeToReplace.children
|
||||
};
|
||||
|
||||
// 替换节点
|
||||
setNodes(nodes.map(n => n.id === newNode.id ? newNode : n));
|
||||
|
||||
// 删除所有指向该节点的属性连接,让用户重新连接
|
||||
const updatedConnections = connections.filter(conn =>
|
||||
!(conn.connectionType === 'property' && conn.to === newNode.id)
|
||||
);
|
||||
setConnections(updatedConnections);
|
||||
|
||||
// 关闭快速创建菜单
|
||||
setQuickCreateMenu({
|
||||
visible: false,
|
||||
position: { x: 0, y: 0 },
|
||||
searchText: '',
|
||||
selectedIndex: 0,
|
||||
mode: 'create',
|
||||
replaceNodeId: null
|
||||
});
|
||||
|
||||
// 显示提示
|
||||
showToast?.(`已将节点替换为 ${newTemplate.displayName}`, 'success');
|
||||
};
|
||||
|
||||
const handleNodeMouseDown = (e: React.MouseEvent, nodeId: string) => {
|
||||
// 只允许左键拖动节点
|
||||
if (e.button !== 0) return;
|
||||
@@ -703,9 +823,33 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查目标属性是否允许多个连接
|
||||
const toNode = nodes.find((n: BehaviorTreeNode) => n.id === actualTo);
|
||||
if (toNode && actualToProperty) {
|
||||
const targetProperty = toNode.template.properties.find(
|
||||
(p: PropertyDefinition) => p.name === actualToProperty
|
||||
);
|
||||
|
||||
// 如果属性不允许多个连接(默认行为)
|
||||
if (!targetProperty?.allowMultipleConnections) {
|
||||
// 检查是否已有连接到该属性
|
||||
const existingPropertyConnection = connections.find(
|
||||
(conn: Connection) =>
|
||||
conn.connectionType === 'property' &&
|
||||
conn.to === actualTo &&
|
||||
conn.toProperty === actualToProperty
|
||||
);
|
||||
|
||||
if (existingPropertyConnection) {
|
||||
showToast('该属性已有连接,请先删除现有连接', 'warning');
|
||||
clearConnecting();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 类型兼容性检查
|
||||
const fromNode = nodes.find((n: BehaviorTreeNode) => n.id === actualFrom);
|
||||
const toNode = nodes.find((n: BehaviorTreeNode) => n.id === actualTo);
|
||||
|
||||
if (fromNode && toNode && actualFromProperty && actualToProperty) {
|
||||
const isFromBlackboard = fromNode.data.nodeType === 'blackboard-variable';
|
||||
@@ -814,7 +958,9 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
y: e.clientY
|
||||
},
|
||||
searchText: '',
|
||||
selectedIndex: 0
|
||||
selectedIndex: 0,
|
||||
mode: 'create',
|
||||
replaceNodeId: null
|
||||
});
|
||||
// 清除预览连接线,但保留 connectingFrom 用于创建连接
|
||||
setConnectingToPos(null);
|
||||
@@ -876,6 +1022,13 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
};
|
||||
|
||||
const handleQuickCreateNode = (template: NodeTemplate) => {
|
||||
// 如果是替换模式,直接调用替换函数
|
||||
if (quickCreateMenu.mode === 'replace') {
|
||||
handleReplaceNode(template);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建模式:需要连接
|
||||
if (!connectingFrom) {
|
||||
return;
|
||||
}
|
||||
@@ -941,7 +1094,9 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
visible: false,
|
||||
position: { x: 0, y: 0 },
|
||||
searchText: '',
|
||||
selectedIndex: 0
|
||||
selectedIndex: 0,
|
||||
mode: 'create',
|
||||
replaceNodeId: null
|
||||
});
|
||||
clearConnecting();
|
||||
|
||||
@@ -1676,6 +1831,7 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
data-node-id={node.id}
|
||||
className={nodeClasses}
|
||||
onClick={(e) => handleNodeClick(e, node)}
|
||||
onContextMenu={(e) => handleNodeContextMenu(e, node)}
|
||||
onMouseDown={(e) => handleNodeMouseDown(e, node.id)}
|
||||
onMouseUp={(e) => handleNodeMouseUpForConnection(e, node.id)}
|
||||
style={{
|
||||
@@ -1762,12 +1918,38 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
#{node.id}
|
||||
</div>
|
||||
</div>
|
||||
{/* 缺失执行器警告 */}
|
||||
{!isRoot && node.template.className && executorRef.current && !executorRef.current.hasExecutor(node.template.className) && (
|
||||
<div
|
||||
className="bt-node-missing-executor-warning"
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'help',
|
||||
pointerEvents: 'auto',
|
||||
position: 'relative'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<AlertCircle
|
||||
size={14}
|
||||
style={{
|
||||
color: '#f44336',
|
||||
flexShrink: 0
|
||||
}}
|
||||
/>
|
||||
<div className="bt-node-missing-executor-tooltip">
|
||||
缺失执行器:找不到节点对应的执行器 "{node.template.className}"
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 未生效节点警告 */}
|
||||
{isUncommitted && (
|
||||
<div
|
||||
className="bt-node-uncommitted-warning"
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginLeft: !isRoot && node.template.className && executorRef.current && !executorRef.current.hasExecutor(node.template.className) ? '4px' : 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'help',
|
||||
@@ -1847,9 +2029,14 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
onMouseDown={(e) => handlePortMouseDown(e, node.id, prop.name)}
|
||||
onMouseUp={(e) => handlePortMouseUp(e, node.id, prop.name)}
|
||||
className={`bt-node-port bt-node-port-property ${hasConnection ? 'connected' : ''}`}
|
||||
title={`Input: ${prop.label}`}
|
||||
title={prop.description || prop.name}
|
||||
/>
|
||||
<span className="bt-node-property-label">{prop.label}:</span>
|
||||
<span
|
||||
className="bt-node-property-label"
|
||||
title={prop.description}
|
||||
>
|
||||
{prop.name}:
|
||||
</span>
|
||||
{propValue !== undefined && (
|
||||
<span className="bt-node-property-value">
|
||||
{String(propValue)}
|
||||
@@ -2212,7 +2399,9 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
visible: false,
|
||||
position: { x: 0, y: 0 },
|
||||
searchText: '',
|
||||
selectedIndex: 0
|
||||
selectedIndex: 0,
|
||||
mode: 'create',
|
||||
replaceNodeId: null
|
||||
});
|
||||
clearConnecting();
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
@@ -2251,7 +2440,9 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
visible: false,
|
||||
position: { x: 0, y: 0 },
|
||||
searchText: '',
|
||||
selectedIndex: 0
|
||||
selectedIndex: 0,
|
||||
mode: 'create',
|
||||
replaceNodeId: null
|
||||
});
|
||||
clearConnecting();
|
||||
}}
|
||||
@@ -2407,6 +2598,50 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
onSpeedChange={handleSpeedChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 右键菜单 */}
|
||||
{contextMenu.visible && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: contextMenu.position.x,
|
||||
top: contextMenu.position.y,
|
||||
backgroundColor: '#2d2d30',
|
||||
border: '1px solid #454545',
|
||||
borderRadius: '4px',
|
||||
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5)',
|
||||
zIndex: 10000,
|
||||
minWidth: '150px',
|
||||
padding: '4px 0'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
setQuickCreateMenu({
|
||||
visible: true,
|
||||
position: contextMenu.position,
|
||||
searchText: '',
|
||||
selectedIndex: 0,
|
||||
mode: 'replace',
|
||||
replaceNodeId: contextMenu.nodeId
|
||||
});
|
||||
setContextMenu({ ...contextMenu, visible: false });
|
||||
}}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
cursor: 'pointer',
|
||||
color: '#cccccc',
|
||||
fontSize: '13px',
|
||||
transition: 'background-color 0.15s'
|
||||
}}
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#094771'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
||||
>
|
||||
替换节点
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { NodeTemplates, NodeTemplate } from '@esengine/behavior-tree';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { EditorPluginManager, MessageHub } from '@esengine/editor-core';
|
||||
@@ -8,6 +8,20 @@ interface BehaviorTreeNodePaletteProps {
|
||||
onNodeSelect?: (template: NodeTemplate) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点类型对应的颜色
|
||||
*/
|
||||
const getTypeColor = (type: string): string => {
|
||||
switch (type) {
|
||||
case 'composite': return '#1976d2';
|
||||
case 'action': return '#388e3c';
|
||||
case 'condition': return '#d32f2f';
|
||||
case 'decorator': return '#fb8c00';
|
||||
case 'blackboard': return '#8e24aa';
|
||||
default: return '#7b1fa2';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 行为树节点面板
|
||||
*
|
||||
@@ -83,14 +97,18 @@ export const BehaviorTreeNodePalette: React.FC<BehaviorTreeNodePaletteProps> = (
|
||||
}, []);
|
||||
|
||||
// 按类别分组(排除根节点类别)
|
||||
const categories = ['all', ...new Set(allTemplates
|
||||
.filter(t => t.category !== '根节点')
|
||||
.map(t => t.category))];
|
||||
const categories = useMemo(() =>
|
||||
['all', ...new Set(allTemplates
|
||||
.filter(t => t.category !== '根节点')
|
||||
.map(t => t.category))]
|
||||
, [allTemplates]);
|
||||
|
||||
const filteredTemplates = (selectedCategory === 'all'
|
||||
? allTemplates
|
||||
: allTemplates.filter(t => t.category === selectedCategory))
|
||||
.filter(t => t.category !== '根节点');
|
||||
const filteredTemplates = useMemo(() =>
|
||||
(selectedCategory === 'all'
|
||||
? allTemplates
|
||||
: allTemplates.filter(t => t.category === selectedCategory))
|
||||
.filter(t => t.category !== '根节点')
|
||||
, [allTemplates, selectedCategory]);
|
||||
|
||||
const handleNodeClick = (template: NodeTemplate) => {
|
||||
onNodeSelect?.(template);
|
||||
@@ -108,17 +126,6 @@ export const BehaviorTreeNodePalette: React.FC<BehaviorTreeNodePaletteProps> = (
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeColor = (type: string): string => {
|
||||
switch (type) {
|
||||
case 'composite': return '#1976d2';
|
||||
case 'action': return '#388e3c';
|
||||
case 'condition': return '#d32f2f';
|
||||
case 'decorator': return '#fb8c00';
|
||||
case 'blackboard': return '#8e24aa';
|
||||
default: return '#7b1fa2';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
|
||||
@@ -318,29 +318,23 @@ export const BehaviorTreeNodeProperties: React.FC<BehaviorTreeNodePropertiesProp
|
||||
) : (
|
||||
template.properties.map((prop, index) => (
|
||||
<div key={index} style={{ marginBottom: '20px' }}>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
marginBottom: '8px',
|
||||
fontSize: '13px',
|
||||
fontWeight: 'bold',
|
||||
color: '#cccccc'
|
||||
}}>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
marginBottom: '8px',
|
||||
fontSize: '13px',
|
||||
fontWeight: 'bold',
|
||||
color: '#cccccc',
|
||||
cursor: prop.description ? 'help' : 'default'
|
||||
}}
|
||||
title={prop.description}
|
||||
>
|
||||
{prop.label}
|
||||
{prop.required && (
|
||||
<span style={{ color: '#f48771', marginLeft: '4px' }}>*</span>
|
||||
)}
|
||||
</label>
|
||||
{renderProperty(prop)}
|
||||
{prop.description && prop.type !== 'boolean' && (
|
||||
<div style={{
|
||||
marginTop: '5px',
|
||||
fontSize: '11px',
|
||||
color: '#666',
|
||||
lineHeight: '1.4'
|
||||
}}>
|
||||
{prop.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -140,6 +140,8 @@ const LogEntryItem = memo(({
|
||||
|
||||
LogEntryItem.displayName = 'LogEntryItem';
|
||||
|
||||
const MAX_LOGS = 1000;
|
||||
|
||||
export function ConsolePanel({ logService }: ConsolePanelProps) {
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
const [filter, setFilter] = useState('');
|
||||
@@ -157,10 +159,16 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
||||
const logContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLogs(logService.getLogs());
|
||||
setLogs(logService.getLogs().slice(-MAX_LOGS));
|
||||
|
||||
const unsubscribe = logService.subscribe((entry) => {
|
||||
setLogs(prev => [...prev, entry]);
|
||||
setLogs(prev => {
|
||||
const newLogs = [...prev, entry];
|
||||
if (newLogs.length > MAX_LOGS) {
|
||||
return newLogs.slice(-MAX_LOGS);
|
||||
}
|
||||
return newLogs;
|
||||
});
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
@@ -348,14 +356,16 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const levelCounts = {
|
||||
const levelCounts = useMemo(() => ({
|
||||
[LogLevel.Debug]: logs.filter(l => l.level === LogLevel.Debug).length,
|
||||
[LogLevel.Info]: logs.filter(l => l.level === LogLevel.Info).length,
|
||||
[LogLevel.Warn]: logs.filter(l => l.level === LogLevel.Warn).length,
|
||||
[LogLevel.Error]: logs.filter(l => l.level === LogLevel.Error || l.level === LogLevel.Fatal).length
|
||||
};
|
||||
}), [logs]);
|
||||
|
||||
const remoteLogCount = logs.filter(l => l.source === 'remote').length;
|
||||
const remoteLogCount = useMemo(() =>
|
||||
logs.filter(l => l.source === 'remote').length
|
||||
, [logs]);
|
||||
|
||||
return (
|
||||
<div className="console-panel">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
import { IEditorPlugin, EditorPluginCategory, PanelPosition, MessageHub } from '@esengine/editor-core';
|
||||
import type { MenuItem, ToolbarItem, PanelDescriptor, ISerializer } from '@esengine/editor-core';
|
||||
import { BehaviorTreePersistence } from '@esengine/behavior-tree';
|
||||
import { BehaviorTreeData } from '@esengine/behavior-tree';
|
||||
|
||||
/**
|
||||
* 行为树编辑器插件
|
||||
@@ -112,18 +112,15 @@ export class BehaviorTreePlugin implements IEditorPlugin {
|
||||
getSerializers(): ISerializer[] {
|
||||
return [
|
||||
{
|
||||
serialize: (data: any) => {
|
||||
// 使用行为树持久化工具
|
||||
const result = BehaviorTreePersistence.serialize(data.entity, data.pretty ?? true);
|
||||
if (typeof result === 'string') {
|
||||
const encoder = new TextEncoder();
|
||||
return encoder.encode(result);
|
||||
}
|
||||
return result;
|
||||
serialize: (data: BehaviorTreeData) => {
|
||||
const json = this.serializeBehaviorTreeData(data);
|
||||
const encoder = new TextEncoder();
|
||||
return encoder.encode(json);
|
||||
},
|
||||
deserialize: (data: Uint8Array) => {
|
||||
// 返回原始数据,让上层决定如何反序列化到场景
|
||||
return data;
|
||||
const decoder = new TextDecoder();
|
||||
const json = decoder.decode(data);
|
||||
return this.deserializeBehaviorTreeData(json);
|
||||
},
|
||||
getSupportedType: () => 'behavior-tree'
|
||||
}
|
||||
@@ -143,10 +140,9 @@ export class BehaviorTreePlugin implements IEditorPlugin {
|
||||
}
|
||||
|
||||
async onBeforeSave(filePath: string, data: any): Promise<void> {
|
||||
// 验证行为树数据
|
||||
if (filePath.endsWith('.behavior-tree.json')) {
|
||||
console.log('[BehaviorTreePlugin] Validating behavior tree before save');
|
||||
const isValid = BehaviorTreePersistence.validate(JSON.stringify(data));
|
||||
const isValid = this.validateBehaviorTreeData(data);
|
||||
if (!isValid) {
|
||||
throw new Error('Invalid behavior tree data');
|
||||
}
|
||||
@@ -159,25 +155,83 @@ export class BehaviorTreePlugin implements IEditorPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
|
||||
private createNewBehaviorTree(): void {
|
||||
console.log('[BehaviorTreePlugin] Creating new behavior tree');
|
||||
// TODO: 实现创建新行为树的逻辑
|
||||
}
|
||||
|
||||
private openBehaviorTree(): void {
|
||||
console.log('[BehaviorTreePlugin] Opening behavior tree');
|
||||
// TODO: 实现打开行为树的逻辑
|
||||
}
|
||||
|
||||
private saveBehaviorTree(): void {
|
||||
console.log('[BehaviorTreePlugin] Saving behavior tree');
|
||||
// TODO: 实现保存行为树的逻辑
|
||||
}
|
||||
|
||||
private validateBehaviorTree(): void {
|
||||
console.log('[BehaviorTreePlugin] Validating behavior tree');
|
||||
// TODO: 实现验证行为树的逻辑
|
||||
}
|
||||
|
||||
private serializeBehaviorTreeData(treeData: BehaviorTreeData): string {
|
||||
const serializable = {
|
||||
id: treeData.id,
|
||||
name: treeData.name,
|
||||
rootNodeId: treeData.rootNodeId,
|
||||
nodes: Array.from(treeData.nodes.entries()).map(([, node]) => ({
|
||||
...node
|
||||
})),
|
||||
blackboardVariables: treeData.blackboardVariables
|
||||
? Array.from(treeData.blackboardVariables.entries()).map(([key, value]) => ({
|
||||
key,
|
||||
value
|
||||
}))
|
||||
: []
|
||||
};
|
||||
return JSON.stringify(serializable, null, 2);
|
||||
}
|
||||
|
||||
private deserializeBehaviorTreeData(json: string): BehaviorTreeData {
|
||||
const parsed = JSON.parse(json);
|
||||
const treeData: BehaviorTreeData = {
|
||||
id: parsed.id,
|
||||
name: parsed.name,
|
||||
rootNodeId: parsed.rootNodeId,
|
||||
nodes: new Map(),
|
||||
blackboardVariables: new Map()
|
||||
};
|
||||
|
||||
if (parsed.nodes) {
|
||||
for (const node of parsed.nodes) {
|
||||
treeData.nodes.set(node.id, node);
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.blackboardVariables) {
|
||||
for (const variable of parsed.blackboardVariables) {
|
||||
treeData.blackboardVariables!.set(variable.key, variable.value);
|
||||
}
|
||||
}
|
||||
|
||||
return treeData;
|
||||
}
|
||||
|
||||
private validateBehaviorTreeData(data: any): boolean {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!data.id || !data.name || !data.rootNodeId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!data.nodes || !Array.isArray(data.nodes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rootNode = data.nodes.find((n: any) => n.id === data.rootNodeId);
|
||||
if (!rootNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EditorPluginManager } from '@esengine/editor-core';
|
||||
import { EditorPluginManager, LocaleService, MessageHub } from '@esengine/editor-core';
|
||||
import type { IEditorPlugin } from '@esengine/editor-core';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { TauriAPI } from '../api/tauri';
|
||||
|
||||
interface PluginPackageJson {
|
||||
@@ -119,6 +120,28 @@ export class PluginLoader {
|
||||
await pluginManager.installEditor(pluginInstance);
|
||||
this.loadedPluginNames.add(packageJson.name);
|
||||
|
||||
// 同步插件的语言设置
|
||||
try {
|
||||
const localeService = Core.services.resolve(LocaleService);
|
||||
const currentLocale = localeService.getCurrentLocale();
|
||||
if (pluginInstance.setLocale) {
|
||||
pluginInstance.setLocale(currentLocale);
|
||||
console.log(`[PluginLoader] Set locale for plugin ${packageJson.name}: ${currentLocale}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`[PluginLoader] Failed to set locale for plugin ${packageJson.name}:`, error);
|
||||
}
|
||||
|
||||
// 通知节点面板重新加载模板
|
||||
try {
|
||||
const messageHub = Core.services.resolve(MessageHub);
|
||||
const localeService = Core.services.resolve(LocaleService);
|
||||
messageHub.publish('locale:changed', { locale: localeService.getCurrentLocale() });
|
||||
console.log(`[PluginLoader] Published locale:changed event for plugin ${packageJson.name}`);
|
||||
} catch (error) {
|
||||
console.warn(`[PluginLoader] Failed to publish locale:changed event:`, error);
|
||||
}
|
||||
|
||||
console.log(`[PluginLoader] Successfully loaded plugin: ${packageJson.name}`);
|
||||
} catch (error) {
|
||||
console.error(`[PluginLoader] Failed to load plugin from ${pluginPath}:`, error);
|
||||
|
||||
@@ -307,3 +307,35 @@
|
||||
.bt-node-uncommitted-warning:hover .bt-node-uncommitted-tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 缺失执行器警告tooltip */
|
||||
.bt-node-missing-executor-tooltip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-bottom: 8px;
|
||||
padding: 6px 10px;
|
||||
background: rgba(244, 67, 54, 0.95);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.bt-node-missing-executor-tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 4px solid transparent;
|
||||
border-top-color: rgba(244, 67, 54, 0.95);
|
||||
}
|
||||
|
||||
.bt-node-missing-executor-warning:hover .bt-node-missing-executor-tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user