fix: 修复黑板数据清理不彻底bug

This commit is contained in:
gongxh
2025-09-05 16:22:12 +08:00
parent 2ab47b2a7b
commit 10ca8fd2a8
5 changed files with 592 additions and 6 deletions

View File

@@ -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",

View File

@@ -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'

View File

@@ -6,7 +6,7 @@
* 专门用于存储和管理行为树执行过程中的共享数据
*/
import { IBTNode } from "../kunpocc-behaviortree";
import { IBTNode } from "./BTNode/BTNode";
/**
* 黑板数据接口
@@ -84,6 +84,11 @@ export class Blackboard implements IBlackboard {
// 重置运行状态
this.openNodes = new WeakMap<IBTNode, boolean>();
// 递归清理所有子黑板
for (const child of this.children) {
child.clean();
}
}
}

581
test/simple-runner.ts Normal file
View File

@@ -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<T>(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<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 测试计数器
let totalTests = 0;
let passedTests = 0;
function runTest(testName: string, testFn: () => void | Promise<void>) {
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<string>('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<string>('root_key');
localValue = node.get<string>('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<string>('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<string>('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<TestEntity>();
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<string>('test_key'), 'test_value', '数据应该存在');
assertEqual(tree.blackboard.get<string>('root_key'), 'root_value', '根数据应该存在');
// 重置
tree.reset();
// 确认数据被清空
assertEqual(tree.blackboard.get<string>('test_key'), undefined, '数据应该被清空');
assertEqual(tree.blackboard.get<string>('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>('string'), 'hello', 'string类型');
assertEqual(node.get<number>('number'), 42, 'number类型');
assertEqual(node.get<boolean>('boolean'), true, 'boolean类型');
const arr = node.get<number[]>('array');
assert(Array.isArray(arr) && arr.length === 3, 'array类型');
const obj = node.get<any>('object');
assertEqual(obj.a, 1, 'object属性a');
assertEqual(obj.b, 'test', 'object属性b');
assertEqual(node.get<any>('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);