diff --git a/.changeset/behavior-tree-builder-action.md b/.changeset/behavior-tree-builder-action.md new file mode 100644 index 00000000..f38c36e5 --- /dev/null +++ b/.changeset/behavior-tree-builder-action.md @@ -0,0 +1,35 @@ +--- +"@esengine/behavior-tree": minor +--- + +feat: add action() and condition() methods to BehaviorTreeBuilder + +Added new methods to support custom executor types directly in the builder: + +- `action(implementationType, name?, config?)` - Use custom action executors registered via `@NodeExecutorMetadata` +- `condition(implementationType, name?, config?)` - Use custom condition executors + +This provides a cleaner API for using custom node executors compared to the existing `executeAction()` which only supports blackboard functions. + +Example: +```typescript +// Define custom executor +@NodeExecutorMetadata({ + implementationType: 'AttackAction', + nodeType: NodeType.Action, + displayName: 'Attack', + category: 'Combat' +}) +class AttackAction implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + return TaskStatus.Success; + } +} + +// Use in builder +const tree = BehaviorTreeBuilder.create('AI') + .selector('Root') + .action('AttackAction', 'Attack', { damage: 50 }) + .end() + .build(); +``` diff --git a/docs/src/content/docs/en/modules/behavior-tree/custom-actions.md b/docs/src/content/docs/en/modules/behavior-tree/custom-actions.md index 0c231556..9ff5bc65 100644 --- a/docs/src/content/docs/en/modules/behavior-tree/custom-actions.md +++ b/docs/src/content/docs/en/modules/behavior-tree/custom-actions.md @@ -182,6 +182,70 @@ export class IsHealthLow implements INodeExecutor { } ``` +## Using Custom Executors in BehaviorTreeBuilder + +After defining a custom executor with `@NodeExecutorMetadata`, use the `.action()` method in the builder: + +```typescript +import { BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree'; + +// Use custom executor in behavior tree +const tree = BehaviorTreeBuilder.create('CombatAI') + .defineBlackboardVariable('health', 100) + .defineBlackboardVariable('target', null) + .selector('Root') + .sequence('AttackSequence') + // Use custom action - matches implementationType in decorator + .action('AttackAction', 'Attack', { damage: 25 }) + .action('MoveToTarget', 'Chase') + .end() + .action('WaitAction', 'Idle', { duration: 1000 }) + .end() + .build(); + +// Start the behavior tree +const entity = scene.createEntity('Enemy'); +BehaviorTreeStarter.start(entity, tree); +``` + +### Builder Methods for Custom Nodes + +| Method | Description | +|--------|-------------| +| `.action(type, name?, config?)` | Add custom action node | +| `.condition(type, name?, config?)` | Add custom condition node | +| `.executeAction(name)` | Use blackboard function `action_{name}` | +| `.executeCondition(name)` | Use blackboard function `condition_{name}` | + +### Complete Example + +```typescript +// 1. Define custom executor +@NodeExecutorMetadata({ + implementationType: 'AttackAction', + nodeType: NodeType.Action, + displayName: 'Attack', + category: 'Combat', + configSchema: { + damage: { type: 'number', default: 10, supportBinding: true } + } +}) +class AttackAction implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + const damage = BindingHelper.getValue(context, 'damage', 10); + console.log(`Attacking with ${damage} damage!`); + return TaskStatus.Success; + } +} + +// 2. Build and use +const tree = BehaviorTreeBuilder.create('AI') + .selector('Root') + .action('AttackAction', 'Attack', { damage: 50 }) + .end() + .build(); +``` + ## Registering Custom Executors Executors are auto-registered via the decorator. To manually register: diff --git a/docs/src/content/docs/modules/behavior-tree/custom-actions.md b/docs/src/content/docs/modules/behavior-tree/custom-actions.md index 3f0bbadf..119698d7 100644 --- a/docs/src/content/docs/modules/behavior-tree/custom-actions.md +++ b/docs/src/content/docs/modules/behavior-tree/custom-actions.md @@ -606,6 +606,107 @@ export class RetryDecorator implements INodeExecutor { } ``` +## 在代码中使用自定义执行器 + +定义了自定义执行器后,可以通过 `BehaviorTreeBuilder` 的 `.action()` 和 `.condition()` 方法在代码中使用: + +### 使用 action() 方法 + +```typescript +import { BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree'; + +// 使用自定义执行器构建行为树 +const tree = BehaviorTreeBuilder.create('CombatAI') + .defineBlackboardVariable('health', 100) + .defineBlackboardVariable('target', null) + .selector('Root') + .sequence('AttackSequence') + // 使用自定义动作 - implementationType 匹配装饰器中的定义 + .action('AttackAction', 'Attack', { damage: 25 }) + .action('MoveToPosition', 'Chase', { speed: 10 }) + .end() + .action('DelayAction', 'Idle', { duration: 1.0 }) + .end() + .build(); + +// 启动行为树 +const entity = scene.createEntity('Enemy'); +BehaviorTreeStarter.start(entity, tree); +``` + +### 使用 condition() 方法 + +```typescript +const tree = BehaviorTreeBuilder.create('AI') + .selector('Root') + .sequence('AttackBranch') + // 使用自定义条件 + .condition('CheckHealth', 'IsHealthy', { threshold: 50, operator: 'greater' }) + .action('AttackAction', 'Attack') + .end() + .end() + .build(); +``` + +### Builder 方法对照表 + +| 方法 | 说明 | 使用场景 | +|------|------|----------| +| `.action(type, name?, config?)` | 使用自定义动作执行器 | 自定义 Action 类 | +| `.condition(type, name?, config?)` | 使用自定义条件执行器 | 自定义 Condition 类 | +| `.executeAction(name)` | 调用黑板函数 `action_{name}` | 简单逻辑、快速原型 | +| `.executeCondition(name)` | 调用黑板函数 `condition_{name}` | 简单条件判断 | + +### 完整示例 + +```typescript +import { + BehaviorTreeBuilder, + BehaviorTreeStarter, + NodeExecutorMetadata, + INodeExecutor, + NodeExecutionContext, + TaskStatus, + NodeType, + BindingHelper +} from '@esengine/behavior-tree'; + +// 1. 定义自定义执行器 +@NodeExecutorMetadata({ + implementationType: 'AttackAction', + nodeType: NodeType.Action, + displayName: '攻击', + category: 'Combat', + configSchema: { + damage: { type: 'number', default: 10, supportBinding: true } + } +}) +class AttackAction implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + const damage = BindingHelper.getValue(context, 'damage', 10); + console.log(`执行攻击,造成 ${damage} 点伤害!`); + return TaskStatus.Success; + } +} + +// 2. 构建行为树 +const enemyAI = BehaviorTreeBuilder.create('EnemyAI') + .defineBlackboardVariable('health', 100) + .defineBlackboardVariable('target', null) + .selector('MainBehavior') + .sequence('AttackBranch') + .condition('CheckHealth', 'HasEnoughHealth', { threshold: 20, operator: 'greater' }) + .action('AttackAction', 'Attack', { damage: 50 }) + .end() + .log('逃跑', 'Flee') + .end() + .build(); + +// 3. 启动行为树 +const entity = scene.createEntity('Enemy'); +BehaviorTreeStarter.start(entity, enemyAI); +``` + ## 注册执行器 ### 自动注册 diff --git a/packages/framework/behavior-tree/package.json b/packages/framework/behavior-tree/package.json index 0be3ee99..b5b8fc19 100644 --- a/packages/framework/behavior-tree/package.json +++ b/packages/framework/behavior-tree/package.json @@ -29,7 +29,8 @@ "clean": "rimraf dist tsconfig.tsbuildinfo", "build": "tsup", "build:watch": "tsup --watch", - "type-check": "tsc --noEmit" + "type-check": "tsc --noEmit", + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }, "author": "yhh", "license": "MIT", diff --git a/packages/framework/behavior-tree/src/BehaviorTreeBuilder.ts b/packages/framework/behavior-tree/src/BehaviorTreeBuilder.ts index 869cff58..5bb9bfbc 100644 --- a/packages/framework/behavior-tree/src/BehaviorTreeBuilder.ts +++ b/packages/framework/behavior-tree/src/BehaviorTreeBuilder.ts @@ -181,12 +181,73 @@ export class BehaviorTreeBuilder { } /** - * 添加执行动作 + * 添加执行动作(通过黑板函数) + * + * @zh 使用黑板中的 action_{actionName} 函数执行动作 + * @en Execute action using action_{actionName} function from blackboard + * + * @example + * ```typescript + * BehaviorTreeBuilder.create("AI") + * .defineBlackboardVariable("action_Attack", (entity) => TaskStatus.Success) + * .selector("Root") + * .executeAction("Attack") + * .end() + * .build(); + * ``` */ executeAction(actionName: string, name?: string): BehaviorTreeBuilder { return this.addActionNode('ExecuteAction', name || 'ExecuteAction', { actionName }); } + /** + * 添加自定义动作节点 + * + * @zh 直接使用注册的执行器类型(通过 @NodeExecutorMetadata 装饰器注册的类) + * @en Use a registered executor type directly (class registered via @NodeExecutorMetadata decorator) + * + * @param implementationType - 执行器类型名称(@NodeExecutorMetadata 中的 implementationType) + * @param name - 节点显示名称 + * @param config - 节点配置参数 + * + * @example + * ```typescript + * // 1. 定义自定义执行器 + * @NodeExecutorMetadata({ + * implementationType: 'AttackAction', + * nodeType: NodeType.Action, + * displayName: '攻击动作', + * category: 'Action' + * }) + * class AttackAction implements INodeExecutor { + * execute(context: NodeExecutionContext): TaskStatus { + * console.log("执行攻击!"); + * return TaskStatus.Success; + * } + * } + * + * // 2. 在行为树中使用 + * BehaviorTreeBuilder.create("AI") + * .selector("Root") + * .action("AttackAction", "Attack") + * .end() + * .build(); + * ``` + */ + action(implementationType: string, name?: string, config?: Record): BehaviorTreeBuilder { + return this.addActionNode(implementationType, name || implementationType, config || {}); + } + + /** + * 添加自定义条件节点 + * + * @zh 直接使用注册的条件执行器类型 + * @en Use a registered condition executor type directly + */ + condition(implementationType: string, name?: string, config?: Record): BehaviorTreeBuilder { + return this.addConditionNode(implementationType, name || implementationType, config || {}); + } + /** * 添加黑板比较条件 */