507 lines
13 KiB
Markdown
507 lines
13 KiB
Markdown
# 资产管理
|
||
|
||
本文介绍如何加载、管理和复用行为树资产。
|
||
|
||
## 为什么需要资产管理?
|
||
|
||
在实际游戏开发中,你可能会遇到以下场景:
|
||
|
||
1. **多个实体共享同一个行为树** - 100个敌人使用同一套AI逻辑
|
||
2. **动态加载行为树** - 从JSON文件加载行为树配置
|
||
3. **子树复用** - 将常用的行为片段(如"巡逻"、"追击")做成独立的子树
|
||
4. **运行时切换行为树** - 敌人在不同阶段使用不同的行为树
|
||
|
||
## BehaviorTreeAssetManager
|
||
|
||
框架提供了 `BehaviorTreeAssetManager` 服务来统一管理行为树资产。
|
||
|
||
### 核心概念
|
||
|
||
- **BehaviorTreeData(行为树数据)**:行为树的定义,可以被多个实体共享
|
||
- **BehaviorTreeRuntimeComponent(运行时组件)**:每个实体独立的运行时状态
|
||
- **AssetManager(资产管理器)**:统一管理所有 BehaviorTreeData
|
||
|
||
### 基本使用
|
||
|
||
```typescript
|
||
import { Core } from '@esengine/esengine';
|
||
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
|
||
|
||
```typescript
|
||
// 加载资产
|
||
assetManager.loadAsset(treeData);
|
||
|
||
// 获取资产
|
||
const tree = assetManager.getAsset('TreeID');
|
||
|
||
// 检查资产是否存在
|
||
if (assetManager.hasAsset('TreeID')) {
|
||
// ...
|
||
}
|
||
|
||
// 卸载资产
|
||
assetManager.unloadAsset('TreeID');
|
||
|
||
// 获取所有资产ID
|
||
const allIds = assetManager.getAllAssetIds();
|
||
|
||
// 清空所有资产
|
||
assetManager.clearAll();
|
||
```
|
||
|
||
## 从文件加载行为树
|
||
|
||
### JSON 格式
|
||
|
||
行为树可以导出为 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 文件
|
||
|
||
```typescript
|
||
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. **团队协作** - 不同成员可以独立开发不同的子树
|
||
|
||
### 创建子树
|
||
|
||
```typescript
|
||
// 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);
|
||
```
|
||
|
||
### 使用子树
|
||
|
||
```typescript
|
||
// 在主行为树中使用子树
|
||
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 配置选项
|
||
|
||
```typescript
|
||
.subTree('SubTreeID', {
|
||
shareBlackboard: true, // 是否共享黑板(默认true)
|
||
})
|
||
```
|
||
|
||
- **shareBlackboard: true** - 子树和父树共享黑板变量
|
||
- **shareBlackboard: false** - 子树使用独立的黑板
|
||
|
||
## 资源预加载
|
||
|
||
在游戏启动时预加载所有行为树资产:
|
||
|
||
```typescript
|
||
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();
|
||
```
|
||
|
||
## 运行时切换行为树
|
||
|
||
敌人在不同阶段使用不同的行为树:
|
||
|
||
```typescript
|
||
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. 共享行为树数据
|
||
|
||
```typescript
|
||
// 好的做法: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. 及时卸载不用的资产
|
||
|
||
```typescript
|
||
// 关卡结束时卸载该关卡的AI
|
||
function onLevelEnd() {
|
||
assetManager.unloadAsset('Level1BossAI');
|
||
assetManager.unloadAsset('Level1EnemyAI');
|
||
}
|
||
|
||
// 加载新关卡的AI
|
||
function onLevelStart() {
|
||
const boss2AI = await loadTreeFromFile('/assets/level2-boss.btree.json');
|
||
assetManager.loadAsset(boss2AI);
|
||
}
|
||
```
|
||
|
||
## 完整示例:多敌人类型的游戏
|
||
|
||
```typescript
|
||
import { Core, Scene } from '@esengine/esengine';
|
||
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();
|
||
```
|
||
|
||
## 常见问题
|
||
|
||
### 如何检查资产是否已加载?
|
||
|
||
```typescript
|
||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||
|
||
if (!assetManager.hasAsset('EnemyAI')) {
|
||
// 加载资产
|
||
const tree = await loadTreeFromFile('/assets/enemy-ai.btree.json');
|
||
assetManager.loadAsset(tree);
|
||
}
|
||
```
|
||
|
||
### 子树找不到怎么办?
|
||
|
||
确保子树已经加载到资产管理器中:
|
||
|
||
```typescript
|
||
// 1. 先加载子树
|
||
const subTree = BehaviorTreeBuilder.create('SubTreeID')
|
||
// ...
|
||
.build();
|
||
assetManager.loadAsset(subTree);
|
||
|
||
// 2. 再加载使用子树的主树
|
||
const mainTree = BehaviorTreeBuilder.create('MainTree')
|
||
.subTree('SubTreeID')
|
||
.build();
|
||
```
|
||
|
||
### 如何导出行为树为 JSON?
|
||
|
||
```typescript
|
||
import { BehaviorTreeAssetSerializer } from '@esengine/behavior-tree';
|
||
|
||
const tree = BehaviorTreeBuilder.create('MyTree')
|
||
// ... 节点定义
|
||
.build();
|
||
|
||
// 序列化为JSON字符串
|
||
const json = BehaviorTreeAssetSerializer.serialize(tree);
|
||
|
||
// 保存到文件或发送到服务器
|
||
console.log(json);
|
||
```
|
||
|
||
## 下一步
|
||
|
||
- 学习[Cocos Creator 集成](./cocos-integration.md)了解如何在游戏引擎中加载资源
|
||
- 查看[自定义节点执行器](./custom-actions.md)创建自定义行为
|
||
- 阅读[最佳实践](./best-practices.md)优化你的行为树设计
|