684 lines
18 KiB
Markdown
684 lines
18 KiB
Markdown
# Cocos Creator 集成
|
||
|
||
本教程将引导你在 Cocos Creator 项目中集成和使用行为树系统。
|
||
|
||
## 前置要求
|
||
|
||
- Cocos Creator 3.x 或更高版本
|
||
- 基本的 TypeScript 知识
|
||
- 已完成[快速开始](./getting-started.md)教程
|
||
|
||
## 安装
|
||
|
||
### 步骤1:安装依赖
|
||
|
||
在你的 Cocos Creator 项目根目录下:
|
||
|
||
```bash
|
||
npm install @esengine/esengine @esengine/behavior-tree
|
||
```
|
||
|
||
### 步骤2:配置 tsconfig.json
|
||
|
||
确保 `tsconfig.json` 中包含以下配置:
|
||
|
||
```json
|
||
{
|
||
"compilerOptions": {
|
||
"experimentalDecorators": true,
|
||
"emitDecoratorMetadata": true,
|
||
"moduleResolution": "node"
|
||
}
|
||
}
|
||
```
|
||
|
||
## 项目结构
|
||
|
||
建议的项目结构:
|
||
|
||
```
|
||
assets/
|
||
├── scripts/
|
||
│ ├── ai/
|
||
│ │ ├── EnemyAIComponent.ts # AI 组件
|
||
│ │ └── PlayerDetector.ts # 检测器
|
||
│ ├── systems/
|
||
│ │ └── BehaviorTreeSystem.ts # 行为树系统
|
||
│ └── Main.ts # 主入口
|
||
├── resources/
|
||
│ └── behaviors/
|
||
│ ├── enemy-ai.btree.json # 行为树资产
|
||
│ └── patrol.btree.json # 子树资产
|
||
└── types/
|
||
└── enemy-ai.ts # 类型定义
|
||
```
|
||
|
||
|
||
## 初始化 ECS 和行为树
|
||
|
||
### 创建主入口组件
|
||
|
||
创建 `assets/scripts/Main.ts`:
|
||
|
||
```typescript
|
||
import { _decorator, Component } from 'cc';
|
||
import { Core, Scene } from '@esengine/esengine';
|
||
import { BehaviorTreePlugin } from '@esengine/behavior-tree';
|
||
|
||
const { ccclass } = _decorator;
|
||
|
||
@ccclass('Main')
|
||
export class Main extends Component {
|
||
async onLoad() {
|
||
// 初始化 ECS Core
|
||
Core.create();
|
||
|
||
// 安装行为树插件
|
||
const behaviorTreePlugin = new BehaviorTreePlugin();
|
||
await Core.installPlugin(behaviorTreePlugin);
|
||
|
||
// 创建并设置场景
|
||
const scene = new Scene();
|
||
behaviorTreePlugin.setupScene(scene);
|
||
Core.setScene(scene);
|
||
|
||
console.log('ECS 和行为树系统初始化完成');
|
||
}
|
||
|
||
update(deltaTime: number) {
|
||
// 更新 ECS(会自动更新场景)
|
||
Core.update(deltaTime);
|
||
}
|
||
|
||
onDestroy() {
|
||
// 清理资源
|
||
Core.destroy();
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
### 添加组件到场景
|
||
|
||
1. 在场景中创建一个空节点(命名为 `GameManager`)
|
||
2. 添加 `Main` 组件到该节点
|
||
|
||
|
||
## 创建 AI 组件
|
||
|
||
创建 `assets/scripts/ai/EnemyAIComponent.ts`:
|
||
|
||
```typescript
|
||
import { _decorator, Component, Node } from 'cc';
|
||
import { Core, Entity } from '@esengine/esengine';
|
||
import {
|
||
BehaviorTreeBuilder,
|
||
BehaviorTreeStarter,
|
||
BehaviorTreeRuntimeComponent
|
||
} from '@esengine/behavior-tree';
|
||
|
||
const { ccclass, property } = _decorator;
|
||
|
||
@ccclass('EnemyAIComponent')
|
||
export class EnemyAIComponent extends Component {
|
||
private aiEntity: Entity | null = null;
|
||
|
||
async start() {
|
||
// 创建行为树
|
||
await this.createBehaviorTree();
|
||
}
|
||
|
||
private async createBehaviorTree() {
|
||
try {
|
||
// 获取Core管理的场景
|
||
const scene = Core.scene;
|
||
if (!scene) {
|
||
console.error('场景未初始化');
|
||
return;
|
||
}
|
||
|
||
// 使用Builder API创建行为树
|
||
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||
.defineBlackboardVariable('cocosNode', this.node)
|
||
.defineBlackboardVariable('health', 100)
|
||
.defineBlackboardVariable('playerNode', null)
|
||
.defineBlackboardVariable('detectionRange', 10)
|
||
.defineBlackboardVariable('attackRange', 2)
|
||
.selector('MainBehavior')
|
||
.sequence('Combat')
|
||
.blackboardExists('playerNode')
|
||
.blackboardCompare('health', 30, 'greater')
|
||
.log('攻击玩家', 'AttackPlayer')
|
||
.end()
|
||
.sequence('Flee')
|
||
.blackboardCompare('health', 30, 'lessOrEqual')
|
||
.log('逃跑', 'RunAway')
|
||
.end()
|
||
.log('巡逻', 'Patrol')
|
||
.end()
|
||
.build();
|
||
|
||
// 创建AI实体并启动
|
||
this.aiEntity = scene.createEntity(`AI_${this.node.name}`);
|
||
BehaviorTreeStarter.start(this.aiEntity, tree);
|
||
|
||
console.log('敌人 AI 已启动');
|
||
} catch (error) {
|
||
console.error('初始化行为树失败:', error);
|
||
}
|
||
}
|
||
|
||
onDestroy() {
|
||
// 停止 AI
|
||
if (this.aiEntity) {
|
||
BehaviorTreeStarter.stop(this.aiEntity);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
## 与 Cocos 节点交互
|
||
|
||
### 创建自定义执行器
|
||
|
||
要实现与Cocos节点的交互,需要创建自定义执行器:
|
||
|
||
```typescript
|
||
import {
|
||
INodeExecutor,
|
||
NodeExecutionContext,
|
||
NodeExecutorMetadata
|
||
} from '@esengine/behavior-tree';
|
||
import { TaskStatus, NodeType } from '@esengine/behavior-tree';
|
||
import { Animation } from 'cc';
|
||
|
||
@NodeExecutorMetadata({
|
||
implementationType: 'PlayAnimation',
|
||
nodeType: NodeType.Action,
|
||
displayName: '播放动画',
|
||
description: '播放Cocos节点上的动画',
|
||
category: 'Cocos',
|
||
configSchema: {
|
||
animationName: {
|
||
type: 'string',
|
||
default: 'attack'
|
||
}
|
||
}
|
||
})
|
||
export class PlayAnimationAction implements INodeExecutor {
|
||
execute(context: NodeExecutionContext): TaskStatus {
|
||
const cocosNode = context.runtime.getBlackboardValue('cocosNode');
|
||
const animationName = context.nodeData.config.animationName;
|
||
|
||
if (!cocosNode) {
|
||
return TaskStatus.Failure;
|
||
}
|
||
|
||
const animation = cocosNode.getComponent(Animation);
|
||
if (animation) {
|
||
animation.play(animationName);
|
||
return TaskStatus.Success;
|
||
}
|
||
|
||
return TaskStatus.Failure;
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
## 完整示例:敌人 AI
|
||
|
||
### 行为树设计
|
||
|
||
使用编辑器创建 `enemy-ai.btree.json`:
|
||
|
||
```
|
||
RootSelector
|
||
├── CombatSequence
|
||
│ ├── CheckPlayerInRange (Condition)
|
||
│ ├── CheckHealthGood (Condition)
|
||
│ └── AttackPlayer (Action)
|
||
├── FleeSequence
|
||
│ ├── CheckHealthLow (Condition)
|
||
│ └── RunAway (Action)
|
||
└── PatrolSequence
|
||
├── PickWaypoint (Action)
|
||
├── MoveToWaypoint (Action)
|
||
└── Wait (Action)
|
||
```
|
||
|
||
|
||
### 黑板变量
|
||
|
||
定义以下黑板变量:
|
||
|
||
- `cocosNode`:Node - Cocos 节点引用
|
||
- `health`:Number - 生命值
|
||
- `playerNode`:Object - 玩家节点引用
|
||
- `detectionRange`:Number - 检测范围
|
||
- `attackRange`:Number - 攻击范围
|
||
- `currentWaypoint`:Number - 当前路点索引
|
||
|
||
|
||
### 实现检测系统
|
||
|
||
创建 `assets/scripts/ai/PlayerDetector.ts`:
|
||
|
||
```typescript
|
||
import { _decorator, Component, Node, Vec3 } from 'cc';
|
||
import { BehaviorTreeRuntimeComponent } from '@esengine/behavior-tree';
|
||
|
||
const { ccclass, property } = _decorator;
|
||
|
||
@ccclass('PlayerDetector')
|
||
export class PlayerDetector extends Component {
|
||
@property(Node)
|
||
player: Node = null;
|
||
|
||
@property
|
||
detectionRange: number = 10;
|
||
|
||
private runtime: BehaviorTreeRuntimeComponent | null = null;
|
||
|
||
start() {
|
||
// 假设AI组件在同一节点上
|
||
const aiComponent = this.node.getComponent('EnemyAIComponent') as any;
|
||
if (aiComponent && aiComponent.aiEntity) {
|
||
this.runtime = aiComponent.aiEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||
}
|
||
}
|
||
|
||
update(deltaTime: number) {
|
||
if (!this.runtime || !this.player) {
|
||
return;
|
||
}
|
||
|
||
// 计算距离
|
||
const distance = Vec3.distance(this.node.position, this.player.position);
|
||
|
||
// 更新黑板
|
||
this.runtime.setBlackboardValue('playerNode', this.player);
|
||
this.runtime.setBlackboardValue('playerInRange', distance <= this.detectionRange);
|
||
this.runtime.setBlackboardValue('distanceToPlayer', distance);
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
## 资源管理
|
||
|
||
### 使用 BehaviorTreeAssetManager
|
||
|
||
框架提供了 `BehaviorTreeAssetManager` 来统一管理行为树资产,避免重复创建:
|
||
|
||
```typescript
|
||
import { Core } from '@esengine/esengine';
|
||
import {
|
||
BehaviorTreeAssetManager,
|
||
BehaviorTreeBuilder,
|
||
BehaviorTreeStarter
|
||
} from '@esengine/behavior-tree';
|
||
|
||
// 获取资产管理器(插件已自动注册)
|
||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||
|
||
// 创建并注册行为树(只创建一次)
|
||
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
|
||
.defineBlackboardVariable('health', 100)
|
||
.selector('MainBehavior')
|
||
.log('攻击')
|
||
.end()
|
||
.build();
|
||
|
||
assetManager.loadAsset(enemyAI);
|
||
|
||
// 为多个敌人实体使用同一份资产
|
||
for (let i = 0; i < 10; i++) {
|
||
const enemy = scene.createEntity(`Enemy${i}`);
|
||
const tree = assetManager.getAsset('EnemyAI')!;
|
||
BehaviorTreeStarter.start(enemy, tree); // 10个敌人共享1份数据
|
||
}
|
||
```
|
||
|
||
### 从 Cocos Creator 资源加载
|
||
|
||
#### 1. 将行为树 JSON 放入 resources 目录
|
||
|
||
```
|
||
assets/
|
||
└── resources/
|
||
└── behaviors/
|
||
├── enemy-ai.btree.json
|
||
└── boss-ai.btree.json
|
||
```
|
||
|
||
#### 2. 创建资源加载器
|
||
|
||
创建 `assets/scripts/BehaviorTreeLoader.ts`:
|
||
|
||
```typescript
|
||
import { resources, JsonAsset } from 'cc';
|
||
import { Core } from '@esengine/esengine';
|
||
import {
|
||
BehaviorTreeAssetManager,
|
||
BehaviorTreeAssetSerializer,
|
||
BehaviorTreeData
|
||
} from '@esengine/behavior-tree';
|
||
|
||
export class BehaviorTreeLoader {
|
||
private assetManager: BehaviorTreeAssetManager;
|
||
|
||
constructor() {
|
||
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||
}
|
||
|
||
/**
|
||
* 从 resources 目录加载行为树
|
||
* @param path 相对于 resources 的路径,不带扩展名
|
||
* @example await loader.load('behaviors/enemy-ai')
|
||
*/
|
||
async load(path: string): Promise<BehaviorTreeData | null> {
|
||
return new Promise((resolve, reject) => {
|
||
resources.load(path, JsonAsset, (err, jsonAsset) => {
|
||
if (err) {
|
||
console.error(`加载行为树失败: ${path}`, err);
|
||
reject(err);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 反序列化 JSON 为 BehaviorTreeData
|
||
const jsonStr = JSON.stringify(jsonAsset.json);
|
||
const treeData = BehaviorTreeAssetSerializer.deserialize(jsonStr);
|
||
|
||
// 加载到资产管理器
|
||
this.assetManager.loadAsset(treeData);
|
||
|
||
console.log(`行为树已加载: ${treeData.name}`);
|
||
resolve(treeData);
|
||
} catch (error) {
|
||
console.error(`解析行为树失败: ${path}`, error);
|
||
reject(error);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 预加载所有行为树
|
||
*/
|
||
async preloadAll(paths: string[]): Promise<void> {
|
||
const promises = paths.map(path => this.load(path));
|
||
await Promise.all(promises);
|
||
console.log(`已预加载 ${paths.length} 个行为树`);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3. 在游戏启动时预加载
|
||
|
||
修改 `Main.ts`:
|
||
|
||
```typescript
|
||
import { _decorator, Component } from 'cc';
|
||
import { Core, Scene } from '@esengine/esengine';
|
||
import { BehaviorTreePlugin } from '@esengine/behavior-tree';
|
||
import { BehaviorTreeLoader } from './BehaviorTreeLoader';
|
||
|
||
const { ccclass } = _decorator;
|
||
|
||
@ccclass('Main')
|
||
export class Main extends Component {
|
||
private loader: BehaviorTreeLoader | null = null;
|
||
|
||
async onLoad() {
|
||
// 初始化 ECS Core
|
||
Core.create();
|
||
|
||
// 安装行为树插件
|
||
const behaviorTreePlugin = new BehaviorTreePlugin();
|
||
await Core.installPlugin(behaviorTreePlugin);
|
||
|
||
// 创建场景
|
||
const scene = new Scene();
|
||
behaviorTreePlugin.setupScene(scene);
|
||
Core.setScene(scene);
|
||
|
||
// 创建加载器并预加载所有行为树
|
||
this.loader = new BehaviorTreeLoader();
|
||
await this.loader.preloadAll([
|
||
'behaviors/enemy-ai',
|
||
'behaviors/boss-ai',
|
||
'behaviors/patrol', // 子树
|
||
'behaviors/chase' // 子树
|
||
]);
|
||
|
||
console.log('游戏初始化完成');
|
||
}
|
||
|
||
update(deltaTime: number) {
|
||
Core.update(deltaTime);
|
||
}
|
||
|
||
onDestroy() {
|
||
Core.destroy();
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4. 在敌人组件中使用
|
||
|
||
```typescript
|
||
import { _decorator, Component } from 'cc';
|
||
import { Core, Entity } from '@esengine/esengine';
|
||
import {
|
||
BehaviorTreeAssetManager,
|
||
BehaviorTreeStarter
|
||
} from '@esengine/behavior-tree';
|
||
|
||
const { ccclass, property } = _decorator;
|
||
|
||
@ccclass('EnemyAIComponent')
|
||
export class EnemyAIComponent extends Component {
|
||
@property
|
||
aiType: string = 'enemy-ai'; // 在编辑器中配置使用哪个AI
|
||
|
||
private aiEntity: Entity | null = null;
|
||
|
||
start() {
|
||
const scene = Core.scene;
|
||
if (!scene) return;
|
||
|
||
// 从资产管理器获取已加载的行为树
|
||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||
const tree = assetManager.getAsset(this.aiType);
|
||
|
||
if (tree) {
|
||
this.aiEntity = scene.createEntity(`AI_${this.node.name}`);
|
||
BehaviorTreeStarter.start(this.aiEntity, tree);
|
||
|
||
// 设置黑板变量
|
||
const runtime = this.aiEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||
runtime?.setBlackboardValue('cocosNode', this.node);
|
||
} else {
|
||
console.error(`找不到行为树资产: ${this.aiType}`);
|
||
}
|
||
}
|
||
|
||
onDestroy() {
|
||
if (this.aiEntity) {
|
||
BehaviorTreeStarter.stop(this.aiEntity);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 调试
|
||
|
||
### 可视化调试信息
|
||
|
||
创建调试组件显示 AI 状态:
|
||
|
||
```typescript
|
||
import { _decorator, Component, Label } from 'cc';
|
||
import { BehaviorTreeRuntimeComponent } from '@esengine/behavior-tree';
|
||
|
||
const { ccclass, property } = _decorator;
|
||
|
||
@ccclass('AIDebugger')
|
||
export class AIDebugger extends Component {
|
||
@property(Label)
|
||
debugLabel: Label = null;
|
||
|
||
private runtime: BehaviorTreeRuntimeComponent | null = null;
|
||
|
||
start() {
|
||
const aiComponent = this.node.getComponent('EnemyAIComponent') as any;
|
||
if (aiComponent && aiComponent.aiEntity) {
|
||
this.runtime = aiComponent.aiEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||
}
|
||
}
|
||
|
||
update() {
|
||
if (!this.runtime || !this.debugLabel) {
|
||
return;
|
||
}
|
||
|
||
const health = this.runtime.getBlackboardValue('health');
|
||
const playerNode = this.runtime.getBlackboardValue('playerNode');
|
||
|
||
this.debugLabel.string = `Health: ${health}\nHas Target: ${playerNode ? 'Yes' : 'No'}`;
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
## 性能优化
|
||
|
||
### 1. 限制行为树数量
|
||
|
||
合理控制同时运行的行为树数量:
|
||
|
||
```typescript
|
||
class AIManager {
|
||
private activeAIs: Entity[] = [];
|
||
private maxAIs: number = 20;
|
||
|
||
addAI(entity: Entity, tree: BehaviorTreeData) {
|
||
if (this.activeAIs.length >= this.maxAIs) {
|
||
// 移除最远的AI
|
||
const furthest = this.findFurthestAI();
|
||
if (furthest) {
|
||
BehaviorTreeStarter.stop(furthest);
|
||
this.activeAIs = this.activeAIs.filter(e => e !== furthest);
|
||
}
|
||
}
|
||
|
||
BehaviorTreeStarter.start(entity, tree);
|
||
this.activeAIs.push(entity);
|
||
}
|
||
|
||
removeAI(entity: Entity) {
|
||
BehaviorTreeStarter.stop(entity);
|
||
this.activeAIs = this.activeAIs.filter(e => e !== entity);
|
||
}
|
||
|
||
private findFurthestAI(): Entity | null {
|
||
// 根据距离找到最远的AI
|
||
// 实现细节略
|
||
return this.activeAIs[0];
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 使用冷却装饰器
|
||
|
||
对于不需要每帧更新的AI,使用冷却装饰器:
|
||
|
||
```typescript
|
||
const tree = BehaviorTreeBuilder.create('ThrottledAI')
|
||
.cooldown(0.2, 'ThrottleRoot') // 每0.2秒执行一次
|
||
.selector('MainBehavior')
|
||
// AI逻辑...
|
||
.end()
|
||
.end()
|
||
.build();
|
||
```
|
||
|
||
### 3. 缓存计算结果
|
||
|
||
在自定义执行器中缓存昂贵的计算:
|
||
|
||
```typescript
|
||
export class CachedFindTarget implements INodeExecutor {
|
||
execute(context: NodeExecutionContext): TaskStatus {
|
||
const { state, runtime, totalTime } = context;
|
||
const cacheTime = state.lastFindTime || 0;
|
||
|
||
if (totalTime - cacheTime < 1.0) {
|
||
const cached = runtime.getBlackboardValue('target');
|
||
return cached ? TaskStatus.Success : TaskStatus.Failure;
|
||
}
|
||
|
||
const target = findNearestTarget();
|
||
runtime.setBlackboardValue('target', target);
|
||
state.lastFindTime = totalTime;
|
||
|
||
return target ? TaskStatus.Success : TaskStatus.Failure;
|
||
}
|
||
}
|
||
```
|
||
|
||
## 多平台注意事项
|
||
|
||
### 性能考虑
|
||
|
||
不同平台的性能差异:
|
||
|
||
- **Web平台**: 受浏览器性能限制,建议减少同时运行的AI数量
|
||
- **原生平台**: 性能较好,可以运行更多AI
|
||
- **小游戏平台**: 内存受限,注意控制行为树数量和复杂度
|
||
|
||
### 平台适配
|
||
|
||
```typescript
|
||
import { sys } from 'cc';
|
||
|
||
// 根据平台调整AI数量
|
||
const maxAIs = sys.isNative ? 50 : (sys.isBrowser ? 20 : 30);
|
||
|
||
// 根据平台调整更新频率
|
||
const updateInterval = sys.isNative ? 0.016 : 0.05;
|
||
```
|
||
|
||
## 常见问题
|
||
|
||
### 行为树无法加载?
|
||
|
||
检查:
|
||
1. 资源路径是否正确(相对于 `resources` 目录)
|
||
2. 文件是否已添加到项目中
|
||
3. 检查控制台错误信息
|
||
|
||
### AI 不执行?
|
||
|
||
确保:
|
||
1. `Main` 组件的 `update` 方法被调用
|
||
2. `Scene.update()` 在每帧被调用
|
||
3. 行为树已通过 `BehaviorTreeStarter.start()` 启动
|
||
|
||
### 黑板变量不更新?
|
||
|
||
检查:
|
||
1. 变量名拼写是否正确
|
||
2. 是否在正确的时机更新变量
|
||
3. 使用 `BehaviorTreeRuntimeComponent.getBlackboardValue()` 和 `setBlackboardValue()` 方法
|
||
|
||
## 下一步
|
||
|
||
- 查看[资产管理](./asset-management.md)了解如何加载和管理行为树资产、使用子树
|
||
- 学习[高级用法](./advanced-usage.md)了解性能优化和调试技巧
|
||
- 阅读[最佳实践](./best-practices.md)优化你的 AI
|
||
- 学习[自定义节点执行器](./custom-actions.md)创建自定义行为
|