mirror of
https://github.com/gongxh0901/kunpocc-behaviortree.git
synced 2025-12-26 16:48:56 +00:00
添加根据数据创建行为树的方法
This commit is contained in:
@@ -64,6 +64,11 @@ export namespace BT {
|
|||||||
maxChildren: number;
|
maxChildren: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册节点名 到 节点构造函数的映射
|
||||||
|
*/
|
||||||
|
const NODE_NAME_TO_CONSTRUCTOR_MAP = new Map<string, any>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 节点元数据存储
|
* 节点元数据存储
|
||||||
*/
|
*/
|
||||||
@@ -111,6 +116,7 @@ export namespace BT {
|
|||||||
parameters
|
parameters
|
||||||
};
|
};
|
||||||
NODE_METADATA_MAP.set(constructor, fullMetadata);
|
NODE_METADATA_MAP.set(constructor, fullMetadata);
|
||||||
|
NODE_NAME_TO_CONSTRUCTOR_MAP.set(name, constructor);
|
||||||
return constructor;
|
return constructor;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -131,6 +137,7 @@ export namespace BT {
|
|||||||
parameters
|
parameters
|
||||||
};
|
};
|
||||||
NODE_METADATA_MAP.set(constructor, fullMetadata);
|
NODE_METADATA_MAP.set(constructor, fullMetadata);
|
||||||
|
NODE_NAME_TO_CONSTRUCTOR_MAP.set(name, constructor);
|
||||||
return constructor;
|
return constructor;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -151,6 +158,7 @@ export namespace BT {
|
|||||||
parameters
|
parameters
|
||||||
};
|
};
|
||||||
NODE_METADATA_MAP.set(constructor, fullMetadata);
|
NODE_METADATA_MAP.set(constructor, fullMetadata);
|
||||||
|
NODE_NAME_TO_CONSTRUCTOR_MAP.set(name, constructor);
|
||||||
return constructor;
|
return constructor;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -171,6 +179,7 @@ export namespace BT {
|
|||||||
parameters
|
parameters
|
||||||
};
|
};
|
||||||
NODE_METADATA_MAP.set(constructor, fullMetadata);
|
NODE_METADATA_MAP.set(constructor, fullMetadata);
|
||||||
|
NODE_NAME_TO_CONSTRUCTOR_MAP.set(name, constructor);
|
||||||
return constructor;
|
return constructor;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -181,6 +190,20 @@ export namespace BT {
|
|||||||
export function getAllNodeMetadata(): Map<any, NodeMetadata> {
|
export function getAllNodeMetadata(): Map<any, NodeMetadata> {
|
||||||
return new Map(NODE_METADATA_MAP);
|
return new Map(NODE_METADATA_MAP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过节点名 获取 节点构造函数
|
||||||
|
*/
|
||||||
|
export function getNodeConstructor(name: string): any {
|
||||||
|
return NODE_NAME_TO_CONSTRUCTOR_MAP.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过节点构造函数 找到节点元数据
|
||||||
|
*/
|
||||||
|
export function getNodeMetadata(ctor: any): NodeMetadata {
|
||||||
|
return NODE_METADATA_MAP.get(ctor)!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _global = globalThis || window || global;
|
let _global = globalThis || window || global;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export class WaitTime extends LeafNode {
|
|||||||
private _value: number = 0;
|
private _value: number = 0;
|
||||||
constructor(duration: number = 0) {
|
constructor(duration: number = 0) {
|
||||||
super();
|
super();
|
||||||
this._max = duration * 1000;
|
this._max = duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override open(): void {
|
protected override open(): void {
|
||||||
@@ -61,7 +61,7 @@ export class WaitTime extends LeafNode {
|
|||||||
|
|
||||||
public tick(): Status {
|
public tick(): Status {
|
||||||
const currTime = new Date().getTime();
|
const currTime = new Date().getTime();
|
||||||
if (currTime - this._value >= this._max) {
|
if (currTime - this._value >= this._max * 1000) {
|
||||||
return Status.SUCCESS;
|
return Status.SUCCESS;
|
||||||
}
|
}
|
||||||
return Status.RUNNING;
|
return Status.RUNNING;
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class LimitTime extends NumericDecorator {
|
|||||||
*/
|
*/
|
||||||
constructor(child: IBTNode, max: number = 1) {
|
constructor(child: IBTNode, max: number = 1) {
|
||||||
super(child);
|
super(child);
|
||||||
this._max = max * 1000;
|
this._max = max;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override open(): void {
|
protected override open(): void {
|
||||||
@@ -65,7 +65,7 @@ export class LimitTime extends NumericDecorator {
|
|||||||
|
|
||||||
public tick(): Status {
|
public tick(): Status {
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
if (currentTime - this._value > this._max) {
|
if (currentTime - this._value > this._max * 1000) {
|
||||||
return Status.FAILURE;
|
return Status.FAILURE;
|
||||||
}
|
}
|
||||||
return this.children[0]!._execute();
|
return this.children[0]!._execute();
|
||||||
|
|||||||
71
src/behaviortree/Factory.ts
Normal file
71
src/behaviortree/Factory.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* @Author: Gongxh
|
||||||
|
* @Date: 2025-09-16
|
||||||
|
* @Description: 根据数据创建一颗行为树
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BehaviorTree } from "./BehaviorTree";
|
||||||
|
import { BT } from "./BT";
|
||||||
|
import { IBTNode } from "./BTNode/BTNode";
|
||||||
|
|
||||||
|
interface INodeConfig {
|
||||||
|
id: string,
|
||||||
|
className: string,
|
||||||
|
parameters: Record<string, any>,
|
||||||
|
children: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据节点配置递归创建节点
|
||||||
|
* @param info 节点配置
|
||||||
|
* @param nodeMap 所有节点配置的映射表
|
||||||
|
* @returns 创建的节点实例
|
||||||
|
*/
|
||||||
|
function createNodeRecursively(info: INodeConfig, nodeMap: Map<string, INodeConfig>): IBTNode {
|
||||||
|
// 获取节点构造函数
|
||||||
|
const ctor = BT.getNodeConstructor(info.className);
|
||||||
|
if (!ctor) {
|
||||||
|
throw new Error(`未找到节点【${info.className}】的构造函数`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归创建子节点
|
||||||
|
const childNodes: IBTNode[] = [];
|
||||||
|
for (const childId of info.children || []) {
|
||||||
|
const childInfo = nodeMap.get(childId);
|
||||||
|
if (!childInfo) {
|
||||||
|
throw new Error(`未找到子节点【${childId}】,行为树配置导出信息错误`);
|
||||||
|
}
|
||||||
|
const childNode = createNodeRecursively(childInfo, nodeMap);
|
||||||
|
childNodes.push(childNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建节点实例
|
||||||
|
let btnode: IBTNode;
|
||||||
|
const metadata = BT.getNodeMetadata(ctor);
|
||||||
|
if (metadata.type === BT.Type.Action || metadata.type === BT.Type.Condition) {
|
||||||
|
btnode = new ctor();
|
||||||
|
} else if (metadata.type === BT.Type.Decorator) {
|
||||||
|
btnode = new ctor(childNodes[0]!);
|
||||||
|
} else {
|
||||||
|
btnode = new ctor(...childNodes);
|
||||||
|
}
|
||||||
|
// 设置节点参数
|
||||||
|
for (const key in info.parameters) {
|
||||||
|
(btnode as any)[key] = info.parameters[key];
|
||||||
|
}
|
||||||
|
return btnode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBehaviorTree<T>(config: INodeConfig[], entity: T): BehaviorTree<T> {
|
||||||
|
// 验证配置
|
||||||
|
if (!config || !Array.isArray(config) || config.length === 0) {
|
||||||
|
throw new Error("Config is empty or invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建配置映射表
|
||||||
|
const nodeMap = new Map<string, INodeConfig>();
|
||||||
|
for (const info of config) {
|
||||||
|
nodeMap.set(info.id, info);
|
||||||
|
}
|
||||||
|
return new BehaviorTree(entity, createNodeRecursively(config[0]!, nodeMap));
|
||||||
|
}
|
||||||
@@ -2,11 +2,14 @@
|
|||||||
/** 行为树 */
|
/** 行为树 */
|
||||||
export { BehaviorTree } from "./behaviortree/BehaviorTree";
|
export { BehaviorTree } from "./behaviortree/BehaviorTree";
|
||||||
export { Blackboard } from "./behaviortree/Blackboard";
|
export { Blackboard } from "./behaviortree/Blackboard";
|
||||||
|
export { } from "./behaviortree/BT";
|
||||||
export * from "./behaviortree/BTNode/AbstractNodes";
|
export * from "./behaviortree/BTNode/AbstractNodes";
|
||||||
export * from "./behaviortree/BTNode/Action";
|
export * from "./behaviortree/BTNode/Action";
|
||||||
export { IBTNode } from "./behaviortree/BTNode/BTNode";
|
export { IBTNode } from "./behaviortree/BTNode/BTNode";
|
||||||
export * from "./behaviortree/BTNode/Composite";
|
export * from "./behaviortree/BTNode/Composite";
|
||||||
export * from "./behaviortree/BTNode/Decorator";
|
export * from "./behaviortree/BTNode/Decorator";
|
||||||
export { Status } from "./behaviortree/header";
|
export { createBehaviorTree } from "./behaviortree/Factory";
|
||||||
|
|
||||||
|
|
||||||
|
// 导出装饰器内容
|
||||||
|
import { BT } from "./behaviortree/BT";
|
||||||
|
export const { ActionNode, ConditionNode, CompositeNode, DecoratorNode, prop } = BT;
|
||||||
@@ -1,581 +0,0 @@
|
|||||||
/**
|
|
||||||
* 简单的测试运行器 - 无需外部依赖
|
|
||||||
* 验证所有行为树功能
|
|
||||||
*/
|
|
||||||
|
|
||||||
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);
|
|
||||||
Reference in New Issue
Block a user