资产管理
本文介绍如何加载、管理和复用行为树资产。
为什么需要资产管理?
Section titled “为什么需要资产管理?”在实际游戏开发中,你可能会遇到以下场景:
- 多个实体共享同一个行为树 - 100个敌人使用同一套AI逻辑
- 动态加载行为树 - 从JSON文件加载行为树配置
- 子树复用 - 将常用的行为片段(如”巡逻”、“追击”)做成独立的子树
- 运行时切换行为树 - 敌人在不同阶段使用不同的行为树
BehaviorTreeAssetManager
Section titled “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
Section titled “资产管理器 API”// 加载资产assetManager.loadAsset(treeData);
// 获取资产const tree = assetManager.getAsset('TreeID');
// 检查资产是否存在if (assetManager.hasAsset('TreeID')) { // ...}
// 卸载资产assetManager.unloadAsset('TreeID');
// 获取所有资产IDconst allIds = assetManager.getAllAssetIds();
// 清空所有资产assetManager.clearAll();从文件加载行为树
Section titled “从文件加载行为树”JSON 格式
Section titled “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 文件
Section titled “加载 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)
Section titled “子树(SubTree)”子树允许你将常用的行为片段做成独立的树,然后在其他树中引用。
为什么使用子树?
Section titled “为什么使用子树?”- 代码复用 - 避免重复定义相同的行为
- 模块化 - 将复杂的行为树拆分成小的可管理单元
- 团队协作 - 不同成员可以独立开发不同的子树
// 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 配置选项
Section titled “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();运行时切换行为树
Section titled “运行时切换行为树”敌人在不同阶段使用不同的行为树:
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. 共享行为树数据
Section titled “1. 共享行为树数据”// 好的做法:100个敌人共享1份BehaviorTreeDataconst sharedTree = assetManager.getAsset('EnemyAI');
for (let i = 0; i < 100; i++) { const enemy = scene.createEntity(`Enemy${i}`); BehaviorTreeStarter.start(enemy, sharedTree!); // 共享数据}
// 不好的做法:每个敌人创建独立的BehaviorTreeDatafor (let i = 0; i < 100; i++) { const enemy = scene.createEntity(`Enemy${i}`); const tree = BehaviorTreeBuilder.create('EnemyAI') // 重复创建 // ... 节点定义 .build(); BehaviorTreeStarter.start(enemy, tree);}2. 及时卸载不用的资产
Section titled “2. 及时卸载不用的资产”// 关卡结束时卸载该关卡的AIfunction onLevelEnd() { assetManager.unloadAsset('Level1BossAI'); assetManager.unloadAsset('Level1EnemyAI');}
// 加载新关卡的AIfunction onLevelStart() { const boss2AI = await loadTreeFromFile('/assets/level2-boss.btree.json'); assetManager.loadAsset(boss2AI);}完整示例:多敌人类型的游戏
Section titled “完整示例:多敌人类型的游戏”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();如何检查资产是否已加载?
Section titled “如何检查资产是否已加载?”const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
if (!assetManager.hasAsset('EnemyAI')) { // 加载资产 const tree = await loadTreeFromFile('/assets/enemy-ai.btree.json'); assetManager.loadAsset(tree);}子树找不到怎么办?
Section titled “子树找不到怎么办?”确保子树已经加载到资产管理器中:
// 1. 先加载子树const subTree = BehaviorTreeBuilder.create('SubTreeID') // ... .build();assetManager.loadAsset(subTree);
// 2. 再加载使用子树的主树const mainTree = BehaviorTreeBuilder.create('MainTree') .subTree('SubTreeID') .build();如何导出行为树为 JSON?
Section titled “如何导出行为树为 JSON?”import { BehaviorTreeAssetSerializer } from '@esengine/behavior-tree';
const tree = BehaviorTreeBuilder.create('MyTree') // ... 节点定义 .build();
// 序列化为JSON字符串const json = BehaviorTreeAssetSerializer.serialize(tree);
// 保存到文件或发送到服务器console.log(json);- 学习Cocos Creator 集成了解如何在游戏引擎中加载资源
- 查看自定义节点执行器创建自定义行为
- 阅读最佳实践优化你的行为树设计