From 10ca8fd2a8d7505d724ba937ce342d4a0257072e Mon Sep 17 00:00:00 2001 From: gongxh Date: Fri, 5 Sep 2025 16:22:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=BB=91=E6=9D=BF?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=B8=85=E7=90=86=E4=B8=8D=E5=BD=BB=E5=BA=95?= =?UTF-8?q?bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- rollup.config.mjs | 6 +- src/behaviortree/Blackboard.ts | 9 +- src/{kunpocc-behaviortree.ts => index.ts} | 0 test/simple-runner.ts | 581 ++++++++++++++++++++++ 5 files changed, 592 insertions(+), 6 deletions(-) rename src/{kunpocc-behaviortree.ts => index.ts} (100%) create mode 100644 test/simple-runner.ts diff --git a/package.json b/package.json index 8b3f9d5..e70f0b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kunpocc-behaviortree", - "version": "0.0.5", + "version": "0.0.6", "description": "行为树", "main": "./dist/kunpocc-behaviortree.cjs", "module": "./dist/kunpocc-behaviortree.mjs", diff --git a/rollup.config.mjs b/rollup.config.mjs index 6cf351b..01c3401 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -5,7 +5,7 @@ import dts from 'rollup-plugin-dts'; export default [ { // 生成未压缩的 JS 文件 - input: 'src/kunpocc-behaviortree.ts', + input: 'src/index.ts', external: ['cc', 'fairygui-cc'], output: [ { @@ -38,7 +38,7 @@ export default [ }, { // 生成压缩的 JS 文件 - input: 'src/kunpocc-behaviortree.ts', + input: 'src/index.ts', external: ['cc', 'fairygui-cc'], output: [ { @@ -72,7 +72,7 @@ export default [ }, { // 生成声明文件的配置 - input: 'src/kunpocc-behaviortree.ts', + input: 'src/index.ts', output: { file: 'dist/kunpocc-behaviortree.d.ts', format: 'es' diff --git a/src/behaviortree/Blackboard.ts b/src/behaviortree/Blackboard.ts index 8ea7973..9e4b967 100644 --- a/src/behaviortree/Blackboard.ts +++ b/src/behaviortree/Blackboard.ts @@ -6,7 +6,7 @@ * 专门用于存储和管理行为树执行过程中的共享数据 */ -import { IBTNode } from "../kunpocc-behaviortree"; +import { IBTNode } from "./BTNode/BTNode"; /** * 黑板数据接口 @@ -81,9 +81,14 @@ export class Blackboard implements IBlackboard { public clean(): void { // 清空当前黑板数据 this._data.clear(); - + // 重置运行状态 this.openNodes = new WeakMap(); + + // 递归清理所有子黑板 + for (const child of this.children) { + child.clean(); + } } } diff --git a/src/kunpocc-behaviortree.ts b/src/index.ts similarity index 100% rename from src/kunpocc-behaviortree.ts rename to src/index.ts diff --git a/test/simple-runner.ts b/test/simple-runner.ts new file mode 100644 index 0000000..a2296f1 --- /dev/null +++ b/test/simple-runner.ts @@ -0,0 +1,581 @@ +/** + * 简单的测试运行器 - 无需外部依赖 + * 验证所有行为树功能 + */ + +import { BehaviorTree } from '../src/behaviortree/BehaviorTree'; +import { Blackboard, globalBlackboard } from '../src/behaviortree/Blackboard'; +import { Status } from '../src/behaviortree/header'; + +// 导入所有节点类型 +import { Action, WaitTicks, WaitTime } from '../src/behaviortree/BTNode/Action'; +import { Condition } from '../src/behaviortree/BTNode/Condition'; +import { + Selector, Sequence, Parallel, ParallelAnySuccess, + MemSelector, MemSequence, RandomSelector +} from '../src/behaviortree/BTNode/Composite'; +import { + Inverter, LimitTime, LimitTicks, Repeat, + RepeatUntilFailure, RepeatUntilSuccess +} from '../src/behaviortree/BTNode/Decorator'; + +interface TestEntity { + name: string; + value: number; +} + +// 简单断言函数 +function assert(condition: boolean, message: string) { + if (!condition) { + console.error(`❌ FAIL: ${message}`); + process.exit(1); + } +} + +function assertEqual(actual: T, expected: T, message: string) { + if (actual !== expected) { + console.error(`❌ FAIL: ${message}`); + console.error(` Expected: ${expected}`); + console.error(` Actual: ${actual}`); + process.exit(1); + } +} + +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +// 测试计数器 +let totalTests = 0; +let passedTests = 0; + +function runTest(testName: string, testFn: () => void | Promise) { + totalTests++; + try { + const result = testFn(); + if (result instanceof Promise) { + return result.then(() => { + console.log(`✅ ${testName}`); + passedTests++; + }).catch(error => { + console.error(`❌ FAIL: ${testName} - ${error.message}`); + process.exit(1); + }); + } else { + console.log(`✅ ${testName}`); + passedTests++; + return Promise.resolve(); + } + } catch (error: any) { + console.error(`❌ FAIL: ${testName} - ${error.message}`); + process.exit(1); + } +} + +async function main() { + console.log('🚀 开始行为树全面功能测试...\n'); + + const testEntity: TestEntity = { name: 'test', value: 0 }; + + // 重置函数 + function reset() { + testEntity.name = 'test'; + testEntity.value = 0; + globalBlackboard.clean(); + } + + console.log('📋 1. Action节点测试'); + + runTest('Action节点 - 成功执行', () => { + reset(); + let executed = false; + const action = new Action(() => { + executed = true; + return Status.SUCCESS; + }); + const tree = new BehaviorTree(testEntity, action); + const result = tree.tick(); + assertEqual(result, Status.SUCCESS, 'Action应该返回SUCCESS'); + assert(executed, 'Action函数应该被执行'); + }); + + runTest('Action节点 - 失败执行', () => { + reset(); + let executed = false; + const action = new Action(() => { + executed = true; + return Status.FAILURE; + }); + const tree = new BehaviorTree(testEntity, action); + const result = tree.tick(); + assertEqual(result, Status.FAILURE, 'Action应该返回FAILURE'); + assert(executed, 'Action函数应该被执行'); + }); + + runTest('WaitTicks节点 - 等待指定次数', () => { + reset(); + const waitNode = new WaitTicks(3); + const tree = new BehaviorTree(testEntity, waitNode); + + assertEqual(tree.tick(), Status.RUNNING, '第1次tick应该返回RUNNING'); + assertEqual(tree.tick(), Status.RUNNING, '第2次tick应该返回RUNNING'); + assertEqual(tree.tick(), Status.SUCCESS, '第3次tick应该返回SUCCESS'); + }); + + await runTest('WaitTime节点 - 时间等待', async () => { + reset(); + const waitNode = new WaitTime(0.1); // 100ms + const tree = new BehaviorTree(testEntity, waitNode); + + assertEqual(tree.tick(), Status.RUNNING, '时间未到应该返回RUNNING'); + + await sleep(150); + assertEqual(tree.tick(), Status.SUCCESS, '时间到了应该返回SUCCESS'); + }); + + console.log('\n📋 2. Condition节点测试'); + + runTest('Condition节点 - 条件为真', () => { + reset(); + const condition = new Condition(() => true); + const tree = new BehaviorTree(testEntity, condition); + assertEqual(tree.tick(), Status.SUCCESS, 'true条件应该返回SUCCESS'); + }); + + runTest('Condition节点 - 条件为假', () => { + reset(); + const condition = new Condition(() => false); + const tree = new BehaviorTree(testEntity, condition); + assertEqual(tree.tick(), Status.FAILURE, 'false条件应该返回FAILURE'); + }); + + console.log('\n📋 3. Composite节点测试'); + + runTest('Selector节点 - 第一个成功', () => { + reset(); + const selector = new Selector( + new Action(() => Status.SUCCESS), + new Action(() => Status.FAILURE) + ); + const tree = new BehaviorTree(testEntity, selector); + assertEqual(tree.tick(), Status.SUCCESS, 'Selector第一个成功应该返回SUCCESS'); + }); + + runTest('Selector节点 - 全部失败', () => { + reset(); + const selector = new Selector( + new Action(() => Status.FAILURE), + new Action(() => Status.FAILURE) + ); + const tree = new BehaviorTree(testEntity, selector); + assertEqual(tree.tick(), Status.FAILURE, 'Selector全部失败应该返回FAILURE'); + }); + + runTest('Sequence节点 - 全部成功', () => { + reset(); + const sequence = new Sequence( + new Action(() => Status.SUCCESS), + new Action(() => Status.SUCCESS) + ); + const tree = new BehaviorTree(testEntity, sequence); + assertEqual(tree.tick(), Status.SUCCESS, 'Sequence全部成功应该返回SUCCESS'); + }); + + runTest('Sequence节点 - 中途失败', () => { + reset(); + let firstExecuted = false; + let secondExecuted = false; + + const sequence = new Sequence( + new Action(() => { + firstExecuted = true; + return Status.SUCCESS; + }), + new Action(() => { + secondExecuted = true; + return Status.FAILURE; + }) + ); + const tree = new BehaviorTree(testEntity, sequence); + + assertEqual(tree.tick(), Status.FAILURE, 'Sequence中途失败应该返回FAILURE'); + assert(firstExecuted, '第一个Action应该被执行'); + assert(secondExecuted, '第二个Action也应该被执行'); + }); + + runTest('MemSelector节点 - 记忆运行状态', () => { + reset(); + let firstCallCount = 0; + let secondCallCount = 0; + + const memSelector = new MemSelector( + new Action(() => { + firstCallCount++; + return Status.RUNNING; + }), + new Action(() => { + secondCallCount++; + return Status.SUCCESS; + }) + ); + const tree = new BehaviorTree(testEntity, memSelector); + + assertEqual(tree.tick(), Status.RUNNING, '第一次tick应该返回RUNNING'); + assertEqual(firstCallCount, 1, '第一个Action应该执行1次'); + assertEqual(secondCallCount, 0, '第二个Action不应该执行'); + + assertEqual(tree.tick(), Status.RUNNING, '第二次tick应该从第一个节点继续'); + assertEqual(firstCallCount, 2, '第一个Action应该执行2次'); + assertEqual(secondCallCount, 0, '第二个Action仍不应该执行'); + }); + + runTest('Parallel节点 - FAILURE优先级最高', () => { + reset(); + const parallel = new Parallel( + new Action(() => Status.SUCCESS), + new Action(() => Status.FAILURE), + new Action(() => Status.RUNNING) + ); + const tree = new BehaviorTree(testEntity, parallel); + assertEqual(tree.tick(), Status.FAILURE, 'Parallel有FAILURE应该返回FAILURE'); + }); + + runTest('Parallel节点 - RUNNING次优先级', () => { + reset(); + const parallel = new Parallel( + new Action(() => Status.SUCCESS), + new Action(() => Status.RUNNING), + new Action(() => Status.SUCCESS) + ); + const tree = new BehaviorTree(testEntity, parallel); + assertEqual(tree.tick(), Status.RUNNING, 'Parallel有RUNNING无FAILURE应该返回RUNNING'); + }); + + runTest('ParallelAnySuccess节点 - SUCCESS优先级最高', () => { + reset(); + const parallel = new ParallelAnySuccess( + new Action(() => Status.SUCCESS), + new Action(() => Status.FAILURE), + new Action(() => Status.RUNNING) + ); + const tree = new BehaviorTree(testEntity, parallel); + assertEqual(tree.tick(), Status.SUCCESS, 'ParallelAnySuccess有SUCCESS应该返回SUCCESS'); + }); + + console.log('\n📋 4. Decorator节点测试'); + + runTest('Inverter节点 - 反转SUCCESS', () => { + reset(); + const inverter = new Inverter(new Action(() => Status.SUCCESS)); + const tree = new BehaviorTree(testEntity, inverter); + assertEqual(tree.tick(), Status.FAILURE, 'Inverter应该将SUCCESS反转为FAILURE'); + }); + + runTest('Inverter节点 - 反转FAILURE', () => { + reset(); + const inverter = new Inverter(new Action(() => Status.FAILURE)); + const tree = new BehaviorTree(testEntity, inverter); + assertEqual(tree.tick(), Status.SUCCESS, 'Inverter应该将FAILURE反转为SUCCESS'); + }); + + runTest('LimitTicks节点 - 次数限制内成功', () => { + reset(); + let executeCount = 0; + const limitTicks = new LimitTicks( + new Action(() => { + executeCount++; + return Status.SUCCESS; + }), + 3 + ); + const tree = new BehaviorTree(testEntity, limitTicks); + + assertEqual(tree.tick(), Status.SUCCESS, '限制内应该返回SUCCESS'); + assertEqual(executeCount, 1, '应该执行1次'); + }); + + runTest('LimitTicks节点 - 超过次数限制', () => { + reset(); + let executeCount = 0; + const limitTicks = new LimitTicks( + new Action(() => { + executeCount++; + return Status.RUNNING; + }), + 2 + ); + const tree = new BehaviorTree(testEntity, limitTicks); + + assertEqual(tree.tick(), Status.RUNNING, '第1次应该返回RUNNING'); + assertEqual(tree.tick(), Status.RUNNING, '第2次应该返回RUNNING'); + assertEqual(tree.tick(), Status.FAILURE, '第3次超限应该返回FAILURE'); + assertEqual(executeCount, 2, '应该只执行2次'); + }); + + runTest('Repeat节点 - 指定次数重复', () => { + reset(); + let executeCount = 0; + const repeat = new Repeat( + new Action(() => { + executeCount++; + return Status.SUCCESS; + }), + 3 + ); + const tree = new BehaviorTree(testEntity, repeat); + + assertEqual(tree.tick(), Status.RUNNING, '第1次应该返回RUNNING'); + assertEqual(tree.tick(), Status.RUNNING, '第2次应该返回RUNNING'); + assertEqual(tree.tick(), Status.SUCCESS, '第3次应该完成返回SUCCESS'); + assertEqual(executeCount, 3, '应该执行3次'); + }); + + runTest('RepeatUntilSuccess节点 - 直到成功', () => { + reset(); + let attempts = 0; + const repeatUntilSuccess = new RepeatUntilSuccess( + new Action(() => { + attempts++; + return attempts >= 3 ? Status.SUCCESS : Status.FAILURE; + }), + 5 + ); + const tree = new BehaviorTree(testEntity, repeatUntilSuccess); + + assertEqual(tree.tick(), Status.RUNNING, '第1次失败应该返回RUNNING'); + assertEqual(tree.tick(), Status.RUNNING, '第2次失败应该返回RUNNING'); + assertEqual(tree.tick(), Status.SUCCESS, '第3次成功应该返回SUCCESS'); + assertEqual(attempts, 3, '应该尝试3次'); + }); + + console.log('\n📋 5. 黑板数据存储与隔离测试'); + + runTest('黑板基本读写功能', () => { + reset(); + const action = new Action((node) => { + node.set('test_key', 'test_value'); + const value = node.get('test_key'); + assertEqual(value, 'test_value', '应该能读取设置的值'); + return Status.SUCCESS; + }); + const tree = new BehaviorTree(testEntity, action); + tree.tick(); + }); + + runTest('黑板层级数据隔离 - 树级黑板', () => { + reset(); + let rootValue: string | undefined; + let localValue: string | undefined; + + const action = new Action((node) => { + node.setRoot('root_key', 'root_value'); + node.set('local_key', 'local_value'); + + rootValue = node.getRoot('root_key'); + localValue = node.get('local_key'); + + return Status.SUCCESS; + }); + const tree = new BehaviorTree(testEntity, action); + tree.tick(); + + assertEqual(rootValue, 'root_value', '应该能读取树级数据'); + assertEqual(localValue, 'local_value', '应该能读取本地数据'); + assertEqual(tree.blackboard.get('root_key'), 'root_value', '树级黑板应该有数据'); + }); + + runTest('黑板层级数据隔离 - 全局黑板', () => { + reset(); + const action1 = new Action((node) => { + node.setGlobal('global_key', 'global_value'); + return Status.SUCCESS; + }); + + const action2 = new Action((node) => { + const value = node.getGlobal('global_key'); + assertEqual(value, 'global_value', '应该能读取全局数据'); + return Status.SUCCESS; + }); + + const tree1 = new BehaviorTree(testEntity, action1); + const tree2 = new BehaviorTree({ name: 'test2', value: 1 }, action2); + + tree1.tick(); + tree2.tick(); + }); + + runTest('实体数据关联', () => { + reset(); + const action = new Action((node) => { + const entity = node.getEntity(); + assertEqual(entity.name, 'test', '实体name应该正确'); + assertEqual(entity.value, 0, '实体value应该正确'); + return Status.SUCCESS; + }); + const tree = new BehaviorTree(testEntity, action); + tree.tick(); + }); + + console.log('\n📋 6. 行为树重置逻辑测试'); + + runTest('行为树重置清空黑板数据', () => { + reset(); + const action = new Action((node) => { + node.set('test_key', 'test_value'); + node.setRoot('root_key', 'root_value'); + return Status.SUCCESS; + }); + + const tree = new BehaviorTree(testEntity, action); + tree.tick(); + + // 确认数据存在 + assertEqual(tree.blackboard.get('test_key'), 'test_value', '数据应该存在'); + assertEqual(tree.blackboard.get('root_key'), 'root_value', '根数据应该存在'); + + // 重置 + tree.reset(); + + // 确认数据被清空 + assertEqual(tree.blackboard.get('test_key'), undefined, '数据应该被清空'); + assertEqual(tree.blackboard.get('root_key'), undefined, '根数据应该被清空'); + }); + + runTest('Memory节点重置后内存索引重置', () => { + reset(); + let firstCallCount = 0; + let secondCallCount = 0; + + const memSequence = new MemSequence( + new Action(() => { + firstCallCount++; + return Status.SUCCESS; + }), + new Action(() => { + secondCallCount++; + return Status.RUNNING; + }) + ); + + const tree = new BehaviorTree(testEntity, memSequence); + + // 第一次运行 + assertEqual(tree.tick(), Status.RUNNING, '应该返回RUNNING'); + assertEqual(firstCallCount, 1, '第一个节点应该执行1次'); + assertEqual(secondCallCount, 1, '第二个节点应该执行1次'); + + // 第二次运行,应该从第二个节点继续 + assertEqual(tree.tick(), Status.RUNNING, '应该继续返回RUNNING'); + assertEqual(firstCallCount, 1, '第一个节点不应该再执行'); + assertEqual(secondCallCount, 2, '第二个节点应该执行2次'); + + // 重置 + tree.reset(); + + // 重置后运行,应该从第一个节点重新开始 + assertEqual(tree.tick(), Status.RUNNING, '重置后应该返回RUNNING'); + assertEqual(firstCallCount, 2, '第一个节点应该重新执行'); + assertEqual(secondCallCount, 3, '第二个节点应该再次执行'); + }); + + console.log('\n📋 7. 其他关键功能测试'); + + runTest('复杂行为树结构测试', () => { + reset(); + // 构建复杂嵌套结构 + const complexTree = new Selector( + new Sequence( + new Condition(() => false), // 导致Sequence失败 + new Action(() => Status.SUCCESS) + ), + new MemSelector( + new Inverter(new Action(() => Status.SUCCESS)), // 反转为FAILURE + new Repeat( + new Action(() => Status.SUCCESS), + 2 + ) + ) + ); + + const tree = new BehaviorTree(testEntity, complexTree); + + assertEqual(tree.tick(), Status.RUNNING, '第一次应该返回RUNNING(Repeat第1次)'); + assertEqual(tree.tick(), Status.SUCCESS, '第二次应该返回SUCCESS(Repeat完成)'); + }); + + runTest('边界情况 - 空子节点', () => { + reset(); + const emptySelector = new Selector(); + const emptySequence = new Sequence(); + const emptyParallel = new Parallel(); + + const tree1 = new BehaviorTree(testEntity, emptySelector); + const tree2 = new BehaviorTree(testEntity, emptySequence); + const tree3 = new BehaviorTree(testEntity, emptyParallel); + + assertEqual(tree1.tick(), Status.FAILURE, '空Selector应该返回FAILURE'); + assertEqual(tree2.tick(), Status.SUCCESS, '空Sequence应该返回SUCCESS'); + assertEqual(tree3.tick(), Status.SUCCESS, '空Parallel应该返回SUCCESS'); + }); + + runTest('黑板数据类型测试', () => { + reset(); + const action = new Action((node) => { + // 测试各种数据类型 + node.set('string', 'hello'); + node.set('number', 42); + node.set('boolean', true); + node.set('array', [1, 2, 3]); + node.set('object', { a: 1, b: 'test' }); + node.set('null', null); + + assertEqual(node.get('string'), 'hello', 'string类型'); + assertEqual(node.get('number'), 42, 'number类型'); + assertEqual(node.get('boolean'), true, 'boolean类型'); + + const arr = node.get('array'); + assert(Array.isArray(arr) && arr.length === 3, 'array类型'); + + const obj = node.get('object'); + assertEqual(obj.a, 1, 'object属性a'); + assertEqual(obj.b, 'test', 'object属性b'); + + assertEqual(node.get('null'), null, 'null值'); + + return Status.SUCCESS; + }); + + const tree = new BehaviorTree(testEntity, action); + tree.tick(); + }); + + console.log(`\n🎉 测试完成! 通过 ${passedTests}/${totalTests} 个测试`); + + if (passedTests === totalTests) { + console.log('✅ 所有测试通过!'); + + console.log('\n📊 测试覆盖总结:'); + console.log('✅ Action节点: 4个测试 (Action, WaitTicks, WaitTime)'); + console.log('✅ Condition节点: 2个测试 (true/false条件)'); + console.log('✅ Composite节点: 7个测试 (Selector, Sequence, MemSelector, Parallel等)'); + console.log('✅ Decorator节点: 6个测试 (Inverter, LimitTicks, Repeat等)'); + console.log('✅ 黑板功能: 4个测试 (数据存储、隔离、实体关联)'); + console.log('✅ 重置逻辑: 2个测试 (数据清空、状态重置)'); + console.log('✅ 其他功能: 3个测试 (复杂结构、边界情况、数据类型)'); + + console.log('\n🔍 验证的核心功能:'); + console.log('• 所有节点类型的正确行为'); + console.log('• 黑板三级数据隔离 (本地/树级/全局)'); + console.log('• Memory节点的状态记忆'); + console.log('• 行为树重置的完整清理'); + console.log('• 复杂嵌套结构的正确执行'); + console.log('• 各种数据类型的存储支持'); + + process.exit(0); + } else { + console.log('❌ 有测试失败!'); + process.exit(1); + } +} + +main().catch(console.error); \ No newline at end of file