Files
esengine/docs/guide/behavior-tree/asset-management.md
yhh ad96edfad0 fix: 恢复 @esengine/ecs-framework 包名
上一个提交错误地将 npm 包名也改了,这里恢复正确的包名。
只更新 GitHub 仓库 URL,不改变 npm 包名。
2025-12-08 21:26:35 +08:00

13 KiB
Raw Blame History

资产管理

本文介绍如何加载、管理和复用行为树资产。

为什么需要资产管理?

在实际游戏开发中,你可能会遇到以下场景:

  1. 多个实体共享同一个行为树 - 100个敌人使用同一套AI逻辑
  2. 动态加载行为树 - 从JSON文件加载行为树配置
  3. 子树复用 - 将常用的行为片段(如"巡逻"、"追击")做成独立的子树
  4. 运行时切换行为树 - 敌人在不同阶段使用不同的行为树

BehaviorTreeAssetManager

框架提供了 BehaviorTreeAssetManager 服务来统一管理行为树资产。

核心概念

  • BehaviorTreeData行为树数据:行为树的定义,可以被多个实体共享
  • BehaviorTreeRuntimeComponent运行时组件:每个实体独立的运行时状态
  • AssetManager资产管理器:统一管理所有 BehaviorTreeData

基本使用

import { Core } from '@esengine/ecs-framework';
import {
    BehaviorTreeAssetManager,
    BehaviorTreeBuilder,
    BehaviorTreeStarter
} from '@esengine/behavior-tree';

// 1. 获取资产管理器(插件已自动注册)
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);

// 2. 创建并注册行为树资产
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
    .defineBlackboardVariable('health', 100)
    .selector('MainBehavior')
        .log('攻击')
    .end()
    .build();

assetManager.loadAsset(enemyAI);

// 3. 为多个实体使用同一份资产
const enemy1 = scene.createEntity('Enemy1');
const enemy2 = scene.createEntity('Enemy2');
const enemy3 = scene.createEntity('Enemy3');

// 获取共享的资产
const sharedTree = assetManager.getAsset('EnemyAI');

if (sharedTree) {
    BehaviorTreeStarter.start(enemy1, sharedTree);
    BehaviorTreeStarter.start(enemy2, sharedTree);
    BehaviorTreeStarter.start(enemy3, sharedTree);
}

资产管理器 API

// 加载资产
assetManager.loadAsset(treeData);

// 获取资产
const tree = assetManager.getAsset('TreeID');

// 检查资产是否存在
if (assetManager.hasAsset('TreeID')) {
    // ...
}

// 卸载资产
assetManager.unloadAsset('TreeID');

// 获取所有资产ID
const allIds = assetManager.getAllAssetIds();

// 清空所有资产
assetManager.clearAll();

从文件加载行为树

JSON 格式

行为树可以导出为 JSON 格式:

{
    "version": "1.0.0",
    "metadata": {
        "name": "EnemyAI",
        "description": "敌人AI行为树"
    },
    "rootNodeId": "root-1",
    "nodes": [
        {
            "id": "root-1",
            "name": "RootSelector",
            "nodeType": "Composite",
            "data": {
                "compositeType": "Selector"
            },
            "children": ["combat-1", "patrol-1"]
        },
        {
            "id": "combat-1",
            "name": "Combat",
            "nodeType": "Action",
            "data": {
                "actionType": "LogAction",
                "message": "攻击敌人"
            },
            "children": []
        }
    ],
    "blackboard": [
        {
            "name": "health",
            "type": "number",
            "defaultValue": 100
        }
    ]
}

加载 JSON 文件

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

async function loadTreeFromFile(filePath: string) {
    const assetManager = Core.services.resolve(BehaviorTreeAssetManager);

    // 1. 读取文件内容
    const jsonContent = await fetch(filePath).then(res => res.text());

    // 2. 反序列化
    const treeData = BehaviorTreeAssetSerializer.deserialize(jsonContent);

    // 3. 加载到资产管理器
    assetManager.loadAsset(treeData);

    return treeData;
}

// 使用
const tree = await loadTreeFromFile('/assets/enemy-ai.btree.json');
BehaviorTreeStarter.start(entity, tree);

子树SubTree

子树允许你将常用的行为片段做成独立的树,然后在其他树中引用。

为什么使用子树?

  1. 代码复用 - 避免重复定义相同的行为
  2. 模块化 - 将复杂的行为树拆分成小的可管理单元
  3. 团队协作 - 不同成员可以独立开发不同的子树

创建子树

// 1. 创建巡逻子树
const patrolTree = BehaviorTreeBuilder.create('PatrolBehavior')
    .sequence('Patrol')
        .log('选择巡逻点', 'PickWaypoint')
        .log('移动到巡逻点', 'MoveToWaypoint')
        .wait(2.0, 'WaitAtWaypoint')
    .end()
    .build();

// 2. 创建追击子树
const chaseTree = BehaviorTreeBuilder.create('ChaseBehavior')
    .sequence('Chase')
        .log('锁定目标', 'LockTarget')
        .log('追击目标', 'ChaseTarget')
    .end()
    .build();

// 3. 注册子树到资产管理器
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
assetManager.loadAsset(patrolTree);
assetManager.loadAsset(chaseTree);

使用子树

// 在主行为树中使用子树
const mainTree = BehaviorTreeBuilder.create('EnemyAI')
    .defineBlackboardVariable('hasTarget', false)

    .selector('MainBehavior')
        // 条件:发现目标时执行追击子树
        .sequence('CombatBranch')
            .blackboardExists('hasTarget')
            .subTree('ChaseBehavior', { shareBlackboard: true })
        .end()

        // 默认:执行巡逻子树
        .subTree('PatrolBehavior', { shareBlackboard: true })
    .end()
    .build();

assetManager.loadAsset(mainTree);

// 启动主行为树
const enemy = scene.createEntity('Enemy');
BehaviorTreeStarter.start(enemy, mainTree);

SubTree 配置选项

.subTree('SubTreeID', {
    shareBlackboard: true,  // 是否共享黑板默认true
})
  • shareBlackboard: true - 子树和父树共享黑板变量
  • shareBlackboard: false - 子树使用独立的黑板

资源预加载

在游戏启动时预加载所有行为树资产:

class BehaviorTreePreloader {
    private assetManager: BehaviorTreeAssetManager;

    constructor() {
        this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
    }

    async preloadAll() {
        // 定义所有行为树文件
        const treeFiles = [
            '/assets/ai/enemy-ai.btree.json',
            '/assets/ai/boss-ai.btree.json',
            '/assets/ai/patrol.btree.json',
            '/assets/ai/chase.btree.json'
        ];

        // 并行加载所有文件
        const loadPromises = treeFiles.map(file => this.loadTree(file));
        await Promise.all(loadPromises);

        console.log(`已加载 ${this.assetManager.getAssetCount()} 个行为树资产`);
    }

    private async loadTree(filePath: string) {
        const jsonContent = await fetch(filePath).then(res => res.text());
        const treeData = BehaviorTreeAssetSerializer.deserialize(jsonContent);
        this.assetManager.loadAsset(treeData);
    }
}

// 游戏启动时调用
const preloader = new BehaviorTreePreloader();
await preloader.preloadAll();

运行时切换行为树

敌人在不同阶段使用不同的行为树:

class EnemyAI {
    private entity: Entity;
    private assetManager: BehaviorTreeAssetManager;

    constructor(entity: Entity) {
        this.entity = entity;
        this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
    }

    // 切换到巡逻AI
    switchToPatrol() {
        const tree = this.assetManager.getAsset('PatrolAI');
        if (tree) {
            BehaviorTreeStarter.stop(this.entity);
            BehaviorTreeStarter.start(this.entity, tree);
        }
    }

    // 切换到战斗AI
    switchToCombat() {
        const tree = this.assetManager.getAsset('CombatAI');
        if (tree) {
            BehaviorTreeStarter.stop(this.entity);
            BehaviorTreeStarter.start(this.entity, tree);
        }
    }

    // 切换到狂暴模式
    switchToBerserk() {
        const tree = this.assetManager.getAsset('BerserkAI');
        if (tree) {
            BehaviorTreeStarter.stop(this.entity);
            BehaviorTreeStarter.start(this.entity, tree);
        }
    }
}

// 使用
const enemyAI = new EnemyAI(enemyEntity);

// Boss血量低于30%时进入狂暴
const runtime = enemyEntity.getComponent(BehaviorTreeRuntimeComponent);
const health = runtime?.getBlackboardValue<number>('health');

if (health && health < 30) {
    enemyAI.switchToBerserk();
}

内存优化

1. 共享行为树数据

// 好的做法100个敌人共享1份BehaviorTreeData
const sharedTree = assetManager.getAsset('EnemyAI');

for (let i = 0; i < 100; i++) {
    const enemy = scene.createEntity(`Enemy${i}`);
    BehaviorTreeStarter.start(enemy, sharedTree!);  // 共享数据
}

// 不好的做法每个敌人创建独立的BehaviorTreeData
for (let i = 0; i < 100; i++) {
    const enemy = scene.createEntity(`Enemy${i}`);
    const tree = BehaviorTreeBuilder.create('EnemyAI')  // 重复创建
        // ... 节点定义
        .build();
    BehaviorTreeStarter.start(enemy, tree);
}

2. 及时卸载不用的资产

// 关卡结束时卸载该关卡的AI
function onLevelEnd() {
    assetManager.unloadAsset('Level1BossAI');
    assetManager.unloadAsset('Level1EnemyAI');
}

// 加载新关卡的AI
function onLevelStart() {
    const boss2AI = await loadTreeFromFile('/assets/level2-boss.btree.json');
    assetManager.loadAsset(boss2AI);
}

完整示例:多敌人类型的游戏

import { Core, Scene } from '@esengine/ecs-framework';
import {
    BehaviorTreePlugin,
    BehaviorTreeAssetManager,
    BehaviorTreeBuilder,
    BehaviorTreeStarter
} from '@esengine/behavior-tree';

async function setupGame() {
    // 1. 初始化
    Core.create();
    const plugin = new BehaviorTreePlugin();
    await Core.installPlugin(plugin);

    const scene = new Scene();
    plugin.setupScene(scene);
    Core.setScene(scene);

    const assetManager = Core.services.resolve(BehaviorTreeAssetManager);

    // 2. 创建共享的子树
    const patrolTree = BehaviorTreeBuilder.create('Patrol')
        .sequence('PatrolLoop')
            .log('巡逻')
            .wait(1.0)
        .end()
        .build();

    const combatTree = BehaviorTreeBuilder.create('Combat')
        .sequence('CombatLoop')
            .log('战斗')
        .end()
        .build();

    assetManager.loadAsset(patrolTree);
    assetManager.loadAsset(combatTree);

    // 3. 创建不同类型敌人的AI
    const meleeEnemyAI = BehaviorTreeBuilder.create('MeleeEnemyAI')
        .selector('MeleeBehavior')
            .sequence('Attack')
                .blackboardExists('target')
                .log('近战攻击')
            .end()
            .subTree('Patrol')
        .end()
        .build();

    const rangedEnemyAI = BehaviorTreeBuilder.create('RangedEnemyAI')
        .selector('RangedBehavior')
            .sequence('Attack')
                .blackboardExists('target')
                .log('远程攻击')
            .end()
            .subTree('Patrol')
        .end()
        .build();

    assetManager.loadAsset(meleeEnemyAI);
    assetManager.loadAsset(rangedEnemyAI);

    // 4. 创建多个敌人实体
    // 10个近战敌人共享同一份AI
    const meleeAI = assetManager.getAsset('MeleeEnemyAI')!;
    for (let i = 0; i < 10; i++) {
        const enemy = scene.createEntity(`MeleeEnemy${i}`);
        BehaviorTreeStarter.start(enemy, meleeAI);
    }

    // 5个远程敌人共享同一份AI
    const rangedAI = assetManager.getAsset('RangedEnemyAI')!;
    for (let i = 0; i < 5; i++) {
        const enemy = scene.createEntity(`RangedEnemy${i}`);
        BehaviorTreeStarter.start(enemy, rangedAI);
    }

    console.log(`已创建 15 个敌人,使用 ${assetManager.getAssetCount()} 个行为树资产`);

    // 5. 游戏循环
    setInterval(() => {
        Core.update(0.016);
    }, 16);
}

setupGame();

常见问题

如何检查资产是否已加载?

const assetManager = Core.services.resolve(BehaviorTreeAssetManager);

if (!assetManager.hasAsset('EnemyAI')) {
    // 加载资产
    const tree = await loadTreeFromFile('/assets/enemy-ai.btree.json');
    assetManager.loadAsset(tree);
}

子树找不到怎么办?

确保子树已经加载到资产管理器中:

// 1. 先加载子树
const subTree = BehaviorTreeBuilder.create('SubTreeID')
    // ...
    .build();
assetManager.loadAsset(subTree);

// 2. 再加载使用子树的主树
const mainTree = BehaviorTreeBuilder.create('MainTree')
    .subTree('SubTreeID')
    .build();

如何导出行为树为 JSON

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

const tree = BehaviorTreeBuilder.create('MyTree')
    // ... 节点定义
    .build();

// 序列化为JSON字符串
const json = BehaviorTreeAssetSerializer.serialize(tree);

// 保存到文件或发送到服务器
console.log(json);

下一步