Skip to content

高级用法

本文介绍行为树系统的高级功能和使用技巧。

子树系统

子树允许你将行为树的一部分抽取为独立的资产,实现复用和模块化。

创建子树

子树本质上就是一个独立的行为树资产:

typescript
import { BehaviorTreeBuilder, BlackboardValueType, TaskStatus } from '@esengine/behavior-tree';

// 创建一个巡逻子树
const patrolSubtree = BehaviorTreeBuilder.create(scene, 'PatrolBehavior')
    .blackboard()
        .defineVariable('patrolPoints', BlackboardValueType.Array, [])
        .defineVariable('currentIndex', BlackboardValueType.Number, 0)
    .endBlackboard()
    .sequence('PatrolSequence')
        .action('MoveToNextPoint', (entity, blackboard) => {
            const points = blackboard?.getValue('patrolPoints') || [];
            const index = blackboard?.getValue('currentIndex') || 0;

            if (points.length === 0) return TaskStatus.Failure;

            const target = points[index];
            console.log(`移动到巡逻点 ${index}:`, target);

            // 移动逻辑...

            return TaskStatus.Success;
        })
        .action('UpdateIndex', (entity, blackboard) => {
            const points = blackboard?.getValue('patrolPoints') || [];
            const index = blackboard?.getValue('currentIndex') || 0;
            const nextIndex = (index + 1) % points.length;
            blackboard?.setValue('currentIndex', nextIndex);
            return TaskStatus.Success;
        })
        .wait(1.0)
    .end()
    .build();

使用SubTree节点

在主树中引用子树:

typescript
const mainTree = BehaviorTreeBuilder.create(scene, 'EnemyAI')
    .blackboard()
        .defineVariable('health', BlackboardValueType.Number, 100)
    .endBlackboard()
    .selector('Root')
        .sequence('Combat')
            .compareBlackboardValue('health', CompareOperator.Greater, 50)
            .action('Attack', () => TaskStatus.Success)
        .end()
        // 使用子树
        .subTree(patrolSubtree, {
            inheritParentBlackboard: true,  // 继承父黑板
            propagateFailure: true          // 传播失败状态
        })
    .end()
    .build();

从资产加载子树

使用编辑器创建的子树资产:

typescript
import {
    BehaviorTreeAssetSerializer,
    BehaviorTreeAssetLoader,
    BehaviorTreeBuilder
} from '@esengine/behavior-tree';

// 加载子树资产
const subtreeJson = await loadFile('patrol-behavior.btree.json');
const subtreeAsset = BehaviorTreeAssetSerializer.deserialize(subtreeJson);

// 在主树中使用
const mainTree = BehaviorTreeBuilder.create(scene, 'MainAI')
    .selector('Root')
        .subTreeFromAsset(subtreeAsset, scene, {
            namePrefix: 'Patrol',
            inheritParentBlackboard: true
        })
    .end()
    .build();

子树黑板继承

当启用 inheritParentBlackboard 时,子树可以访问父树的黑板变量:

typescript
// 父树定义的变量
const parent = BehaviorTreeBuilder.create(scene, 'Parent')
    .blackboard()
        .defineVariable('playerPosition', BlackboardValueType.Vector3, { x: 0, y: 0, z: 0 })
    .endBlackboard()
    .subTree(childTree, { inheritParentBlackboard: true })
    .build();

// 子树可以访问父树的 playerPosition 变量
const child = BehaviorTreeBuilder.create(scene, 'Child')
    .action('UseParentData', (entity, blackboard) => {
        const playerPos = blackboard?.getValue('playerPosition');
        console.log('玩家位置:', playerPos);
        return TaskStatus.Success;
    })
    .build();

异步操作

异步动作

对于需要多帧完成的操作,返回 TaskStatus.Running

typescript
.action('LoadResource', (entity, blackboard, deltaTime) => {
    // 检查是否已开始加载
    let loadingState = blackboard?.getValue('loadingState');

    if (!loadingState) {
        // 第一次执行,开始加载
        startAsyncLoad().then(result => {
            blackboard?.setValue('loadingState', 'completed');
            blackboard?.setValue('loadedData', result);
        });
        blackboard?.setValue('loadingState', 'loading');
        return TaskStatus.Running;  // 继续等待
    }

    if (loadingState === 'loading') {
        return TaskStatus.Running;  // 仍在加载中
    }

    if (loadingState === 'completed') {
        // 加载完成
        blackboard?.setValue('loadingState', null);
        return TaskStatus.Success;
    }

    return TaskStatus.Failure;
})

超时控制

使用装饰器实现超时:

typescript
.timeout(5.0, 'LoadTimeout')  // 5秒超时
    .action('SlowOperation', () => {
        // 长时间运行的操作
        return TaskStatus.Running;
    })
.end()

全局黑板

全局黑板在所有行为树实例之间共享数据。

创建全局黑板

typescript
import { GlobalBlackboard } from '@esengine/behavior-tree';

// 设置全局变量
GlobalBlackboard.setValue('gameState', 'playing');
GlobalBlackboard.setValue('playerCount', 4);
GlobalBlackboard.setValue('difficulty', 'hard');

在行为树中访问全局黑板

typescript
.action('CheckGameState', (entity, blackboard) => {
    const gameState = GlobalBlackboard.getValue('gameState');

    if (gameState === 'paused') {
        return TaskStatus.Failure;
    }

    return TaskStatus.Success;
})

全局黑板监听

监听全局变量变化:

typescript
const unsubscribe = GlobalBlackboard.subscribe('difficulty', (newValue, oldValue) => {
    console.log(`难度从 ${oldValue} 变为 ${newValue}`);
    // 调整AI行为
});

// 取消监听
unsubscribe();

性能优化

1. 使用对象池

复用行为树实体以减少GC压力:

typescript
class BehaviorTreePool {
    private pool: Map<string, Entity[]> = new Map();
    private scene: Scene;

    constructor(scene: Scene) {
        this.scene = scene;
    }

    acquire(asset: BehaviorTreeAsset, poolKey: string): Entity {
        let pool = this.pool.get(poolKey);

        if (!pool) {
            pool = [];
            this.pool.set(poolKey, pool);
        }

        if (pool.length > 0) {
            const entity = pool.pop()!;
            BehaviorTreeStarter.restart(entity);
            return entity;
        }

        return BehaviorTreeAssetLoader.instantiate(asset, this.scene);
    }

    release(entity: Entity, poolKey: string) {
        BehaviorTreeStarter.stop(entity);

        const pool = this.pool.get(poolKey) || [];
        pool.push(entity);
        this.pool.set(poolKey, pool);
    }

    clear() {
        for (const [key, pool] of this.pool) {
            for (const entity of pool) {
                entity.destroy();
            }
        }
        this.pool.clear();
    }
}

// 使用示例
const pool = new BehaviorTreePool(scene);

// 获取AI实例
const enemyAI = pool.acquire(enemyAsset, 'enemy');

// 释放回池
pool.release(enemyAI, 'enemy');

2. 降低更新频率

对于不需要每帧更新的AI,可以在行为树内部使用节流逻辑:

typescript
// 方法1: 在行为树根节点使用Cooldown装饰器
const ai = BehaviorTreeBuilder.create(scene, 'ThrottledAI')
    .cooldown(0.1)  // 每0.1秒执行一次
        .selector()
            // AI逻辑
        .end()
    .end()
    .build();

// 方法2: 在Action中实现自定义节流
.action('ThrottledAction', (entity, blackboard, deltaTime) => {
    const lastUpdate = blackboard?.getValue('lastUpdateTime') || 0;
    const currentTime = Date.now();
    const updateInterval = 100;  // 100ms

    if (currentTime - lastUpdate < updateInterval) {
        return TaskStatus.Running;
    }

    blackboard?.setValue('lastUpdateTime', currentTime);

    // 执行实际逻辑
    performAILogic();
    return TaskStatus.Success;
})

3. 条件缓存

缓存昂贵的条件检查结果:

typescript
.action('CacheExpensiveCheck', (entity, blackboard) => {
    const cacheKey = 'expensiveCheckResult';
    const cacheTime = blackboard?.getValue('expensiveCheckTime') || 0;
    const currentTime = Date.now();

    // 如果缓存未过期(1秒内),直接使用缓存结果
    if (currentTime - cacheTime < 1000) {
        const cachedResult = blackboard?.getValue(cacheKey);
        return cachedResult ? TaskStatus.Success : TaskStatus.Failure;
    }

    // 执行昂贵的检查
    const result = performExpensiveCheck();

    // 缓存结果
    blackboard?.setValue(cacheKey, result);
    blackboard?.setValue('expensiveCheckTime', currentTime);

    return result ? TaskStatus.Success : TaskStatus.Failure;
})

4. 分帧执行

将大量计算分散到多帧:

typescript
.action('ProcessLargeDataset', (entity, blackboard, deltaTime) => {
    const data = blackboard?.getValue('dataset') || [];
    let processedIndex = blackboard?.getValue('processedIndex') || 0;

    const batchSize = 100;  // 每帧处理100个
    const endIndex = Math.min(processedIndex + batchSize, data.length);

    for (let i = processedIndex; i < endIndex; i++) {
        // 处理单个数据项
        processItem(data[i]);
    }

    blackboard?.setValue('processedIndex', endIndex);

    if (endIndex >= data.length) {
        // 全部处理完成
        blackboard?.setValue('processedIndex', 0);
        return TaskStatus.Success;
    }

    return TaskStatus.Running;  // 继续下一帧
})

序列化和反序列化

JSON格式

标准的可读格式:

typescript
import { BehaviorTreeAssetSerializer } from '@esengine/behavior-tree';

// 序列化为JSON
const asset = createBehaviorTreeAsset();
const json = BehaviorTreeAssetSerializer.serialize(asset);

// 保存到文件
await saveFile('ai-behavior.btree.json', json);

// 从JSON加载
const loadedJson = await loadFile('ai-behavior.btree.json');
const loadedAsset = BehaviorTreeAssetSerializer.deserialize(loadedJson);

二进制格式

体积更小的二进制格式(通常比JSON小60-70%):

typescript
// 序列化为二进制
const binary = BehaviorTreeAssetSerializer.serializeToBinary(asset);

// 保存二进制文件
await saveFile('ai-behavior.btree.bin', binary);

// 从二进制加载
const loadedBinary = await loadFile('ai-behavior.btree.bin');
const loadedAsset = BehaviorTreeAssetSerializer.deserializeFromBinary(loadedBinary);

格式转换

在JSON和二进制之间转换:

typescript
// JSON转二进制
const jsonData = await loadFile('tree.btree.json');
const asset = BehaviorTreeAssetSerializer.deserialize(jsonData);
const binary = BehaviorTreeAssetSerializer.serializeToBinary(asset);
await saveFile('tree.btree.bin', binary);

// 二进制转JSON
const binaryData = await loadFile('tree.btree.bin');
const asset2 = BehaviorTreeAssetSerializer.deserializeFromBinary(binaryData);
const json = BehaviorTreeAssetSerializer.serialize(asset2);
await saveFile('tree.btree.json', json);

调试技巧

1. 日志节点

在关键位置添加日志:

typescript
.log('开始战斗序列', 'info')
.sequence('Combat')
    .log('检查生命值', 'debug')
    .compareBlackboardValue('health', CompareOperator.Greater, 0)
    .log('执行攻击', 'info')
    .action('Attack', () => TaskStatus.Success)
.end()

2. 黑板快照

定期输出黑板状态:

typescript
.action('DebugBlackboard', (entity, blackboard) => {
    console.log('=== 黑板快照 ===');
    const vars = blackboard?.getAllVariables();
    for (const [key, value] of Object.entries(vars || {})) {
        console.log(`${key}:`, value);
    }
    return TaskStatus.Success;
})

3. 条件断言

验证重要条件:

typescript
.action('AssertPlayerExists', (entity, blackboard) => {
    const player = blackboard?.getValue('player');

    if (!player) {
        console.error('断言失败: 玩家不存在');
        return TaskStatus.Failure;
    }

    return TaskStatus.Success;
})

4. 性能分析

测量节点执行时间:

typescript
.action('ProfiledAction', (entity, blackboard) => {
    const startTime = performance.now();

    // 执行操作
    doSomething();

    const elapsed = performance.now() - startTime;
    console.log(`操作耗时: ${elapsed.toFixed(2)}ms`);

    return TaskStatus.Success;
})

5. 可视化调试

在编辑器中运行行为树并观察节点状态:

typescript
import { BehaviorTreeDebugger } from '@esengine/behavior-tree';

// 启用调试模式
BehaviorTreeDebugger.enable(aiEntity);

// 获取当前执行路径
const executionPath = BehaviorTreeDebugger.getExecutionPath(aiEntity);
console.log('执行路径:', executionPath);

// 获取节点状态
const nodeStatus = BehaviorTreeDebugger.getNodeStatus(aiEntity, nodeId);
console.log('节点状态:', nodeStatus);

常见模式

1. 状态机模式

使用行为树实现状态机:

typescript
const fsm = BehaviorTreeBuilder.create(scene, 'StateMachine')
    .blackboard()
        .defineVariable('currentState', BlackboardValueType.String, 'idle')
    .endBlackboard()
    .selector('StateSwitch')
        // Idle状态
        .sequence('IdleState')
            .checkBlackboardValue('currentState', 'idle')
            .action('IdleBehavior', (e, bb) => {
                console.log('执行Idle行为');
                // 状态转换条件
                if (shouldTransitionToMove()) {
                    bb?.setValue('currentState', 'move');
                }
                return TaskStatus.Success;
            })
        .end()
        // Move状态
        .sequence('MoveState')
            .checkBlackboardValue('currentState', 'move')
            .action('MoveBehavior', (e, bb) => {
                console.log('执行Move行为');
                if (shouldTransitionToAttack()) {
                    bb?.setValue('currentState', 'attack');
                }
                return TaskStatus.Success;
            })
        .end()
        // Attack状态
        .sequence('AttackState')
            .checkBlackboardValue('currentState', 'attack')
            .action('AttackBehavior', (e, bb) => {
                console.log('执行Attack行为');
                if (shouldTransitionToIdle()) {
                    bb?.setValue('currentState', 'idle');
                }
                return TaskStatus.Success;
            })
        .end()
    .end()
    .build();

2. 优先级队列模式

按优先级执行任务:

typescript
.selector('PriorityQueue')
    // 最高优先级:生存
    .sequence('Survive')
        .compareBlackboardValue('health', CompareOperator.Less, 20)
        .action('Heal', () => TaskStatus.Success)
    .end()
    // 中优先级:战斗
    .sequence('Combat')
        .checkBlackboardExists('nearbyEnemy', true)
        .action('Fight', () => TaskStatus.Success)
    .end()
    // 低优先级:收集资源
    .sequence('Gather')
        .action('CollectResources', () => TaskStatus.Success)
    .end()
.end()

3. 并行任务模式

同时执行多个任务:

typescript
.parallel(ParallelPolicy.RequireAll)  // 所有任务都要成功
    .action('PlayAnimation', () => TaskStatus.Success)
    .action('PlaySound', () => TaskStatus.Success)
    .action('SpawnParticles', () => TaskStatus.Success)
.end()

下一步

Released under the MIT License.