Files
kunpocc-behaviortree/docs/BehaviorTree.md
2025-09-29 10:14:04 +08:00

214 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 硬核游戏开发 - 使用BehaviorTree实现游戏AI决策的开发详解
## 一个让我头疼三个月的BOSS
去年在公司做一个RPG项目的时候遇到了一个特别头疼的问题。策划给我扔了个需求做一个龙王BOSS要求这货能巡逻、能战斗、血少了会发疯、快死了还会逃跑。
听起来挺简单的不就是几个状态切换嘛。我当时想都没想直接上手就是一顿if-else
按策划的文档这个BOSS需要
- 平时在那巡逻,看到玩家就冲过去
- 根据距离选择近战还是远程攻击
- 血量掉到30%以下就进入狂暴模式
- 受到重击时会短暂防御
- 血量太低就开始逃跑
看起来逻辑很清楚,我就写了一大堆判断:
```javascript
function updateBoss() {
if (boss.hp < 0.1 * boss.maxHp) {
if (canEscape()) {
escape();
} else if (canDefend()) {
defend();
} else {
attack();
}
} else if (boss.hp < 0.3 * boss.maxHp) {
if (playerInRange()) {
berserkerAttack();
} else {
moveToPlayer();
}
} else if (playerDetected()) {
if (playerDistance() < 5) {
meleeAttack();
} else {
rangedAttack();
}
} else {
patrol();
}
}
```
结果呢?完全是个灾难。
BOSS的表现简直让人抓狂有时候明明看到玩家了突然又开始巡逻血都快没了还在那疯狂攻击更离谱的是防御和攻击状态之间来回切换看起来像个神经病。
策划每天都来找我:"这BOSS怎么这么蠢"我也很绝望啊每次改一个地方其他地方就出新问题。最后代码写成了800多行的意大利面条连我自己都不敢动了。
后来一个老同事看不下去了,跟我说:"你试试行为树吧,专门解决这种问题的。"
花了差不多一周时间学习和重构用行为树重新写了这个BOSS
```
龙王BOSS行为树
根节点(选择器)
├── 逃跑分支(血量 < 10%
│ ├── 寻找掩体
│ └── 快速移动
├── 防御分支(受到重击)
│ ├── 播放防御动画
│ └── 恢复少量血量
├── 血怒分支(血量 < 30%
│ ├── 进入狂暴状态
│ └── 疯狂攻击
├── 战斗分支(发现玩家)
│ ├── 距离判断
│ ├── 近战攻击 OR 远程攻击
│ └── 追击玩家
└── 巡逻分支(默认行为)
├── 沿路径移动
└── 警戒四周
```
效果立竿见影!
BOSS终于不再像个智障了行为逻辑变得很清晰。最关键的是策划现在能直接看懂这个结构提需求的时候也更明确了。代码从800行缩减到200多行维护起来轻松了不少。
最让我印象深刻的是,后来策划突然说要加个"血量50%时召唤小怪"的功能我只是在行为树里插了个新分支十几分钟就搞定了。要是放在以前的if-else结构里估计又得折腾好几天。
---
这就是我第一次接触行为树的经历。
如果你也在做游戏开发特别是涉及到AI逻辑的部分可能也遇到过类似的问题。怪物AI、NPC行为、甚至一些复杂的游戏机制用传统的if-else或者状态机来实现总是容易变成一团乱麻。
行为树提供了一个更好的解决方案。下面我就把这段时间学到的东西整理一下,希望能帮到有同样困扰的朋友。
## 什么是行为树
简单来说行为树就是一种树形结构用来描述AI的决策逻辑。
它的基本思路是这样的:从根节点开始,通过不同类型的节点来控制执行流程。比如"选择器"节点会依次尝试它的子节点,直到有一个成功为止;"序列"节点则要求所有子节点都成功才算成功。最底层的叶子节点负责执行具体的动作或者判断条件。
相比传统的状态机,行为树最大的优势是更容易组合和复用。你可以很自然地表达"先试试A不行就试B"或者"按顺序执行1、2、3"这样的逻辑,而且特别适合做可视化编辑器。
## 行为树是怎么工作的
为了更好理解我们来看一个简单的例子。假设你在做一个哥布林守卫的AI它需要在洞穴门口巡逻。
### 节点的三种状态
行为树中的每个节点在执行时只会返回三种状态之一:
* **成功** - 任务完成了
* **失败** - 这条路走不通
* **运行中** - 还在执行中,需要等待
这个概念其实很好理解,就像你在做任何事情时的状态一样:要么做完了,要么做不了,要么还在做。
### 几种常用的节点类型
#### 选择器节点Selector
这个节点的逻辑是"依次尝试,直到成功为止"。
比如哥布林饿了,它会:
1. 先去厨房找剩菜
2. 厨房没有就去花园抓虫子
3. 还是没有就啃树皮
选择器会从左到右依次执行子节点,只要有一个成功了就停止,返回成功。如果所有子节点都失败了,它才返回失败。
这种模式在游戏AI中特别常用比如敌人的攻击选择优先使用技能技能冷却中就普通攻击连普通攻击都不行就移动到攻击范围内。
#### 序列节点Sequence
这个节点的逻辑是"按顺序执行,全部成功才算成功"。
比如哥布林要做一顿饭:
1. 先洗手
2. 准备食材
3. 开火做饭
4. 享用美食
序列节点会严格按顺序执行,只有当前步骤成功了才会进行下一步。任何一步失败,整个序列就失败。
这种模式适合那些有明确步骤的任务,比如开门(检查是否有钥匙 → 走到门前 → 使用钥匙 → 推开门)。
#### 动作节点Action
这些是真正执行具体任务的节点:
- "巡逻10秒钟"
- "攻击敌人"
- "播放死亡动画"
动作节点会立即开始执行任务,然后根据情况返回对应的状态。有些动作是瞬时的(比如播放音效),有些需要持续一段时间(比如移动到目标点)。
#### 条件节点Condition
这些节点负责检查当前的环境状态:
- "敌人在视野内吗?"
- "血量低于30%吗?"
- "身上有钥匙吗?"
条件节点通常执行很快,瞬间返回成功或失败,不会有"运行中"的状态。
### 一个完整的例子
现在我们把这些节点组合起来,看看一个哥布林守卫的行为树:
```
哥布林守卫行为树:
根节点(选择器)
├── 战斗分支(序列)
│ ├── 条件:发现敌人?
│ ├── 动作:冲向敌人
│ └── 动作:攻击
├── 巡逻分支(序列)
│ ├── 条件:在巡逻路径上?
│ └── 动作:继续巡逻
└── 待机分支
└── 动作:原地等待
```
执行过程是这样的:
1. **平时状态**
- 根选择器首先尝试战斗分支
- 检查"发现敌人?" → 没有敌人,条件失败
- 尝试巡逻分支 → 检查"在巡逻路径上?" → 是的,开始巡逻
2. **发现敌人时**
- 根选择器重新开始评估
- 战斗分支:检查"发现敌人?" → 有敌人!条件成功
- 执行"冲向敌人" → 成功后执行"攻击"
这就是行为树的核心思想:每一帧都从根节点重新开始评估,根据当前情况选择最合适的行为。
### 执行机制
行为树有个很重要的特点:它每一帧都会从根节点重新开始执行。
![image](../image/maunal/flow.png)
这张图展示了我们刚才说的哥布林行为树的结构。
![image](../image/maunal/flow2.png)
执行过程就像这样:
1. 从根节点开始
2. 根据节点类型决定如何执行子节点
3. 叶子节点返回结果,层层向上传递
4. 一帧结束,下一帧重新开始
这种每帧重新评估的机制有几个好处:
- 能够实时响应环境变化
- 优先级明确,重要的行为总是先被考虑
- 调试时可以清楚看到决策过程
- 性能还不错,只执行必要的节点
当然这种机制也有一些需要注意的地方。比如如果你的行为树很复杂每帧都完整执行一遍可能会有性能问题。不过对于大部分游戏AI来说这都不是问题。