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';
|
||||
|
||||
Reference in New Issue
Block a user