502 lines
12 KiB
Markdown
502 lines
12 KiB
Markdown
# Cocos Creator 集成
|
||
|
||
本教程将引导你在 Cocos Creator 项目中集成和使用行为树系统。
|
||
|
||
## 前置要求
|
||
|
||
- Cocos Creator 3.x 或更高版本
|
||
- 基本的 TypeScript 知识
|
||
- 已完成[快速开始](./getting-started.md)教程
|
||
|
||
## 安装
|
||
|
||
### 步骤1:安装依赖
|
||
|
||
在你的 Cocos Creator 项目根目录下:
|
||
|
||
```bash
|
||
npm install @esengine/ecs-framework @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/ecs-framework';
|
||
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/ecs-framework';
|
||
import {
|
||
BehaviorTreeAssetSerializer,
|
||
BehaviorTreeAssetLoader,
|
||
BehaviorTreeStarter,
|
||
BlackboardComponent
|
||
} from '@esengine/behavior-tree';
|
||
import { resources } from 'cc';
|
||
|
||
const { ccclass, property } = _decorator;
|
||
|
||
@ccclass('EnemyAIComponent')
|
||
export class EnemyAIComponent extends Component {
|
||
@property
|
||
behaviorTreeAsset: string = 'behaviors/enemy-ai.btree';
|
||
|
||
private aiEntity: Entity | null = null;
|
||
|
||
async start() {
|
||
// 加载行为树资产
|
||
await this.loadBehaviorTree();
|
||
}
|
||
|
||
private async loadBehaviorTree() {
|
||
try {
|
||
// 获取Core管理的场景
|
||
const scene = Core.scene;
|
||
if (!scene) {
|
||
console.error('场景未初始化');
|
||
return;
|
||
}
|
||
|
||
// 从 resources 加载JSON资产
|
||
resources.load(this.behaviorTreeAsset, (err, jsonAsset: any) => {
|
||
if (err) {
|
||
console.error('加载行为树失败:', err);
|
||
return;
|
||
}
|
||
|
||
// 获取JSON字符串
|
||
const jsonString = jsonAsset.json ? JSON.stringify(jsonAsset.json) : jsonAsset.text;
|
||
|
||
// 反序列化
|
||
const btAsset = BehaviorTreeAssetSerializer.deserialize(jsonString);
|
||
|
||
// 实例化
|
||
this.aiEntity = BehaviorTreeAssetLoader.instantiate(
|
||
btAsset,
|
||
scene,
|
||
{
|
||
namePrefix: this.node.name
|
||
}
|
||
);
|
||
|
||
// 设置黑板初始值
|
||
const blackboard = this.aiEntity.getComponent(BlackboardComponent);
|
||
if (blackboard) {
|
||
// 可以在这里设置引用到 Cocos 节点
|
||
blackboard.setValue('cocosNode', this.node);
|
||
blackboard.setValue('position', this.node.position.clone());
|
||
}
|
||
|
||
// 启动 AI
|
||
BehaviorTreeStarter.start(this.aiEntity);
|
||
|
||
console.log('敌人 AI 已启动');
|
||
});
|
||
} catch (error) {
|
||
console.error('初始化行为树失败:', error);
|
||
}
|
||
}
|
||
|
||
onDestroy() {
|
||
// 停止 AI
|
||
if (this.aiEntity) {
|
||
BehaviorTreeStarter.stop(this.aiEntity);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
## 与 Cocos 节点交互
|
||
|
||
### 在编辑器ExecuteAction节点中编写代码
|
||
|
||
在行为树编辑器中,可以使用 `Execute Action` 节点,并编写代码:
|
||
|
||
```javascript
|
||
// 获取 Cocos 节点
|
||
const cocosNode = blackboard.getValue('cocosNode');
|
||
|
||
// 播放攻击动画
|
||
const animation = cocosNode.getComponent('Animation');
|
||
animation.play('attack');
|
||
|
||
return TaskStatus.Success;
|
||
```
|
||
|
||
|
||
## 完整示例:敌人 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 { BlackboardComponent } 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 blackboard: BlackboardComponent | null = null;
|
||
|
||
start() {
|
||
// 假设AI组件在同一节点上
|
||
const aiComponent = this.node.getComponent('EnemyAIComponent') as any;
|
||
if (aiComponent && aiComponent.aiEntity) {
|
||
this.blackboard = aiComponent.aiEntity.getComponent(BlackboardComponent);
|
||
}
|
||
}
|
||
|
||
update(deltaTime: number) {
|
||
if (!this.blackboard || !this.player) {
|
||
return;
|
||
}
|
||
|
||
// 计算距离
|
||
const distance = Vec3.distance(this.node.position, this.player.position);
|
||
|
||
// 更新黑板
|
||
this.blackboard.setValue('playerNode', this.player);
|
||
this.blackboard.setValue('playerInRange', distance <= this.detectionRange);
|
||
this.blackboard.setValue('distanceToPlayer', distance);
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
## 资源管理
|
||
|
||
### 预加载行为树资产
|
||
|
||
在游戏启动时预加载所有行为树资产:
|
||
|
||
```typescript
|
||
import { resources } from 'cc';
|
||
|
||
async function preloadBehaviorTrees() {
|
||
const assets = [
|
||
'behaviors/enemy-ai',
|
||
'behaviors/boss-ai',
|
||
'behaviors/patrol'
|
||
];
|
||
|
||
for (const path of assets) {
|
||
await new Promise((resolve, reject) => {
|
||
resources.preload(path, (err) => {
|
||
if (err) reject(err);
|
||
else resolve(null);
|
||
});
|
||
});
|
||
}
|
||
|
||
console.log('行为树资产预加载完成');
|
||
}
|
||
```
|
||
|
||
### 使用 AssetManager
|
||
|
||
对于动态加载,可以使用 Cocos 的 AssetManager:
|
||
|
||
```typescript
|
||
import { assetManager } from 'cc';
|
||
|
||
assetManager.loadBundle('behaviors', (err, bundle) => {
|
||
if (err) {
|
||
console.error('加载 bundle 失败:', err);
|
||
return;
|
||
}
|
||
|
||
bundle.load('enemy-ai', (err, asset) => {
|
||
if (!err) {
|
||
// 使用资产
|
||
}
|
||
});
|
||
});
|
||
```
|
||
|
||
## 调试
|
||
|
||
### 可视化调试信息
|
||
|
||
创建调试组件显示 AI 状态:
|
||
|
||
```typescript
|
||
import { _decorator, Component, Label } from 'cc';
|
||
import { BlackboardComponent } from '@esengine/behavior-tree';
|
||
|
||
const { ccclass, property } = _decorator;
|
||
|
||
@ccclass('AIDebugger')
|
||
export class AIDebugger extends Component {
|
||
@property(Label)
|
||
debugLabel: Label = null;
|
||
|
||
private blackboard: BlackboardComponent | null = null;
|
||
|
||
start() {
|
||
const aiComponent = this.node.getComponent('EnemyAIComponent') as any;
|
||
if (aiComponent && aiComponent.aiEntity) {
|
||
this.blackboard = aiComponent.aiEntity.getComponent(BlackboardComponent);
|
||
}
|
||
}
|
||
|
||
update() {
|
||
if (!this.blackboard || !this.debugLabel) {
|
||
return;
|
||
}
|
||
|
||
const health = this.blackboard.getValue('health');
|
||
const state = this.blackboard.getValue('currentState');
|
||
|
||
this.debugLabel.string = `Health: ${health}\nState: ${state}`;
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
## 性能优化
|
||
|
||
### 1. 使用对象池
|
||
|
||
为 AI 实体使用对象池:
|
||
|
||
```typescript
|
||
class AIEntityPool {
|
||
private pool: Entity[] = [];
|
||
private scene: Scene;
|
||
|
||
constructor(scene: Scene) {
|
||
this.scene = scene;
|
||
}
|
||
|
||
acquire(behaviorTreeAsset: any): Entity {
|
||
if (this.pool.length > 0) {
|
||
const entity = this.pool.pop()!;
|
||
BehaviorTreeStarter.restart(entity);
|
||
return entity;
|
||
}
|
||
|
||
return BehaviorTreeAssetLoader.instantiate(behaviorTreeAsset, this.scene);
|
||
}
|
||
|
||
release(entity: Entity) {
|
||
BehaviorTreeStarter.stop(entity);
|
||
this.pool.push(entity);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 限制更新频率
|
||
|
||
对于远离相机的敌人,可以在行为树内部使用节流机制:
|
||
|
||
```typescript
|
||
// 在行为树的Action节点中实现节流
|
||
function throttledAction(entity, blackboard, deltaTime) {
|
||
let lastUpdate = blackboard?.getValue('lastUpdateTime') || 0;
|
||
const currentTime = Date.now();
|
||
|
||
// 根据距离决定更新间隔
|
||
const distance = getDistanceToCamera();
|
||
const updateInterval = distance < 10 ? 0 : 200; // 远处敌人200ms更新一次
|
||
|
||
if (currentTime - lastUpdate < updateInterval) {
|
||
return TaskStatus.Running;
|
||
}
|
||
|
||
blackboard?.setValue('lastUpdateTime', currentTime);
|
||
|
||
// 执行实际逻辑
|
||
performAILogic();
|
||
return TaskStatus.Success;
|
||
}
|
||
```
|
||
|
||
### 3. 使用二进制格式
|
||
|
||
在构建时将 JSON 转换为二进制格式以减小包体:
|
||
|
||
```bash
|
||
# 在构建脚本中
|
||
node scripts/convert-bt-to-binary.js
|
||
```
|
||
|
||
## 多平台发布
|
||
|
||
### Web 平台
|
||
|
||
在 Web 平台,确保资源路径正确:
|
||
|
||
```typescript
|
||
// 使用相对路径
|
||
const assetPath = 'behaviors/enemy-ai';
|
||
```
|
||
|
||
### 原生平台
|
||
|
||
原生平台可以使用二进制格式以获得更好的性能:
|
||
|
||
```typescript
|
||
// 检测平台
|
||
if (sys.isNative) {
|
||
// 加载二进制格式
|
||
assetPath = 'behaviors/enemy-ai.btree.bin';
|
||
} else {
|
||
// 加载 JSON 格式
|
||
assetPath = 'behaviors/enemy-ai.btree.json';
|
||
}
|
||
```
|
||
|
||
## 常见问题
|
||
|
||
### 行为树无法加载?
|
||
|
||
检查:
|
||
1. 资源路径是否正确(相对于 `resources` 目录)
|
||
2. 文件是否已添加到项目中
|
||
3. 检查控制台错误信息
|
||
|
||
### AI 不执行?
|
||
|
||
确保:
|
||
1. `Main` 组件的 `update` 方法被调用
|
||
2. `Scene.update()` 在每帧被调用
|
||
3. 行为树已通过 `BehaviorTreeStarter.start()` 启动
|
||
|
||
### 黑板变量不更新?
|
||
|
||
检查:
|
||
1. 变量名拼写是否正确
|
||
2. 是否在正确的时机更新变量
|
||
3. 使用 `BlackboardComponent.getValue()` 和 `setValue()` 方法
|
||
|
||
## 下一步
|
||
|
||
- 查看[高级用法](./advanced-usage.md)了解子树和异步加载
|
||
- 学习[最佳实践](./best-practices.md)优化你的 AI
|