8. 状态机与行为树
2026/4/14大约 11 分钟
状态机与行为树
你有没有注意到,游戏里的敌人不是像无头苍蝇一样乱跑的?守卫会在固定路线上巡逻,发现你之后追过来,打不过就跑回去求援,你躲起来之后又会回到巡逻状态。
这些"聪明"的行为,背后就是 AI 决策架构 在控制。最常见的两种架构是有限状态机(FSM)和行为树(Behavior Tree)。本章从零开始教你理解和实现它们。
为什么需要 AI 决策架构
想象一下,如果不用任何架构,直接用 if-else 来写敌人 AI:
// 反面教材:用一堆 if-else 写 AI,越写越乱
void Update()
{
if (看到玩家 && 距离 < 5)
{
攻击();
}
else if (看到玩家 && 血量 < 30)
{
逃跑();
}
else if (看到玩家)
{
追逐();
}
else if (血量 < 50 && 巡逻到回血点)
{
回血();
}
else if (巡逻时间到了)
{
换巡逻点();
}
else
{
巡逻();
}
}这段代码现在看起来还能看懂,但如果再加 10 种行为、20 种条件呢?你会得到几百个 if-else 嵌套在一起——谁也看不懂、谁也不敢改。
AI 决策架构的作用就是把复杂的行为逻辑整理成清晰的结构,让代码好写、好读、好改。
有限状态机(FSM)——最经典的 AI 架构
什么是状态机
有限状态机(Finite State Machine,简称 FSM)的核心思想非常简单:一个实体在任意时刻只能处于一种状态,不同状态之间可以切换。
打个比方:你的一天可以分成几个"状态":
[睡觉] --闹钟响了--> [起床洗漱] --出门--> [上班/上学]
[上班] --到饭点了--> [吃饭] --吃完--> [继续上班]
[上班] --下班了--> [回家] --到点了--> [睡觉]你在任意时刻只处于一个状态(不可能同时在睡觉和上班),而且状态之间有明确的切换条件。这就是状态机。
状态机的三个要素
| 要素 | 说明 | 举例 |
|---|---|---|
| 状态(State) | 实体当前在做什么 | 巡逻、追逐、攻击、逃跑 |
| 转移(Transition) | 从一个状态切换到另一个状态 | 发现玩家:巡逻 -> 追逐 |
| 条件(Condition) | 什么情况下可以转移 | 距离玩家 < 10 且看到玩家 |
画一个状态图
敌人的 AI 可以用这样的状态图表示:
┌────────── 发现玩家 ──────────┐
│ │
▼ │
┌───────┐ 玩家跑远了 ┌───────┐
│ 巡逻 │ ◄──────────── │ 追逐 │
└───────┘ └───────┘
│ │
│ 血量低 │ 到攻击范围
▼ ▼
┌───────┐ 血量恢复 ┌───────┐
│ 回血 │ ──────────► │ 攻击 │
└───────┘ └───────┘
▲ │
│ 血量低 │ 玩家跑远
└──────────────────────┘用代码实现状态机
下面我们用 Godot 实现一个完整的敌人 AI 状态机。
C
using Godot;
using System.Collections.Generic;
// ========== 状态基类 ==========
// 所有具体状态都继承这个基类
public abstract class EnemyState
{
// 状态名字,方便调试
public abstract string Name { get; }
// 进入状态时调用(初始化)
public abstract void Enter(EnemyController enemy);
// 每帧调用(更新逻辑)
public abstract void Update(EnemyController enemy, double delta);
// 离开状态时调用(清理)
public abstract void Exit(EnemyController enemy);
}
// ========== 巡逻状态 ==========
public class PatrolState : EnemyState
{
public override string Name => "巡逻";
private int _currentWaypointIndex = 0;
public override void Enter(EnemyController enemy)
{
GD.Print($"{enemy.Name} 开始巡逻");
_currentWaypointIndex = 0;
}
public override void Update(EnemyController enemy, double delta)
{
// 如果没有巡逻点,就站在原地
if (enemy.Waypoints.Count == 0) return;
// 朝当前巡逻点移动
var target = enemy.Waypoints[_currentWaypointIndex];
enemy.MoveToward(target, enemy.PatrolSpeed, delta);
// 到达巡逻点后,切换到下一个
if (enemy.GlobalPosition.DistanceTo(target) < 10)
{
_currentWaypointIndex = (_currentWaypointIndex + 1) % enemy.Waypoints.Count;
}
// 检测玩家:如果看到玩家且在追逐范围内,切换到追逐状态
if (enemy.CanSeePlayer() && enemy.DistanceToPlayer() < enemy.ChaseRange)
{
enemy.ChangeState(new ChaseState());
}
}
public override void Exit(EnemyController enemy)
{
// 巡逻结束,可以播放停止动画
enemy.StopMovement();
}
}
// ========== 追逐状态 ==========
public class ChaseState : EnemyState
{
public override string Name => "追逐";
public override void Enter(EnemyController enemy)
{
GD.Print($"{enemy.Name} 发现了玩家!开始追逐!");
}
public override void Update(EnemyController enemy, double delta)
{
// 血量太低,逃跑
if (enemy.Hp < enemy.MaxHp * 0.2f)
{
enemy.ChangeState(new FleeState());
return;
}
// 玩家跑远了,回到巡逻
if (enemy.DistanceToPlayer() > enemy.ChaseRange * 1.5f)
{
enemy.ChangeState(new PatrolState());
return;
}
// 到了攻击范围,开始攻击
if (enemy.DistanceToPlayer() < enemy.AttackRange)
{
enemy.ChangeState(new AttackState());
return;
}
// 追逐玩家
enemy.MoveToward(enemy.PlayerPosition, enemy.ChaseSpeed, delta);
}
public override void Exit(EnemyController enemy) { }
}
// ========== 攻击状态 ==========
public class AttackState : EnemyState
{
public override string Name => "攻击";
private double _attackTimer = 0;
public override void Enter(EnemyController enemy)
{
GD.Print($"{enemy.Name} 进入攻击状态!");
_attackTimer = 0;
}
public override void Update(EnemyController enemy, double delta)
{
// 血量太低,逃跑
if (enemy.Hp < enemy.MaxHp * 0.2f)
{
enemy.ChangeState(new FleeState());
return;
}
// 玩家跑出攻击范围,继续追逐
if (enemy.DistanceToPlayer() > enemy.AttackRange * 1.2f)
{
enemy.ChangeState(new ChaseState());
return;
}
// 面向玩家
enemy.LookAt(enemy.PlayerPosition);
// 攻击冷却
_attackTimer += delta;
if (_attackTimer >= enemy.AttackCooldown)
{
_attackTimer = 0;
enemy.PerformAttack();
}
}
public override void Exit(EnemyController enemy) { }
}
// ========== 逃跑状态 ==========
public class FleeState : EnemyState
{
public override string Name => "逃跑";
public override void Enter(EnemyController enemy)
{
GD.Print($"{enemy.Name} 血量太低了,开始逃跑!");
}
public override void Update(EnemyController enemy, double delta)
{
// 血量恢复到安全水平,回到巡逻
if (enemy.Hp > enemy.MaxHp * 0.5f)
{
enemy.ChangeState(new PatrolState());
return;
}
// 往远离玩家的方向跑
var awayDirection = (enemy.GlobalPosition - enemy.PlayerPosition).Normalized();
var fleeTarget = enemy.GlobalPosition + awayDirection * 100;
enemy.MoveToward(fleeTarget, enemy.ChaseSpeed * 1.2f, delta);
}
public override void Exit(EnemyController enemy) { }
}GDScript
extends Node
# ========== 状态基类 ==========
class_name EnemyState
# 状态名字,方便调试
var state_name: String = ""
# 进入状态时调用
func enter(_enemy: CharacterBody2D) -> void:
pass
# 每帧调用
func update(_enemy: CharacterBody2D, _delta: float) -> void:
pass
# 离开状态时调用
func exit(_enemy: CharacterBody2D) -> void:
pass
# ========== 巡逻状态 ==========
class PatrolState extends EnemyState:
var _current_waypoint_index: int = 0
func _init():
state_name = "巡逻"
func enter(enemy: CharacterBody2D) -> void:
print("%s 开始巡逻" % enemy.name)
_current_waypoint_index = 0
func update(enemy: CharacterBody2D, delta: float) -> void:
var waypoints = enemy.get("waypoints") as Array
if waypoints.is_empty():
return
# 朝当前巡逻点移动
var target: Vector2 = waypoints[_current_waypoint_index]
var direction := (target - enemy.global_position).normalized()
enemy.velocity = direction * enemy.get("patrol_speed")
enemy.move_and_slide()
# 到达巡逻点,切换到下一个
if enemy.global_position.distance_to(target) < 10:
_current_waypoint_index = (_current_waypoint_index + 1) % waypoints.size()
# 检测玩家
if enemy.get("can_see_player") and enemy.get("distance_to_player") < enemy.get("chase_range"):
enemy.get("state_machine").change_state(ChaseState.new())
func exit(enemy: CharacterBody2D) -> void:
enemy.velocity = Vector2.ZERO
# ========== 追逐状态 ==========
class ChaseState extends EnemyState:
func _init():
state_name = "追逐"
func enter(enemy: CharacterBody2D) -> void:
print("%s 发现了玩家!开始追逐!" % enemy.name)
func update(enemy: CharacterBody2D, delta: float) -> void:
# 血量太低,逃跑
if enemy.get("hp") < enemy.get("max_hp") * 0.2:
enemy.get("state_machine").change_state(FleeState.new())
return
# 玩家跑远了
if enemy.get("distance_to_player") > enemy.get("chase_range") * 1.5:
enemy.get("state_machine").change_state(PatrolState.new())
return
# 到了攻击范围
if enemy.get("distance_to_player") < enemy.get("attack_range"):
enemy.get("state_machine").change_state(AttackState.new())
return
# 追逐玩家
var player_pos: Vector2 = enemy.get("player_position")
var direction := (player_pos - enemy.global_position).normalized()
enemy.velocity = direction * enemy.get("chase_speed")
enemy.move_and_slide()
func exit(_enemy: CharacterBody2D) -> void:
pass
# ========== 攻击状态 ==========
class AttackState extends EnemyState:
var _attack_timer: float = 0.0
func _init():
state_name = "攻击"
func enter(enemy: CharacterBody2D) -> void:
print("%s 进入攻击状态!" % enemy.name)
_attack_timer = 0.0
func update(enemy: CharacterBody2D, delta: float) -> void:
# 血量太低
if enemy.get("hp") < enemy.get("max_hp") * 0.2:
enemy.get("state_machine").change_state(FleeState.new())
return
# 玩家跑出攻击范围
if enemy.get("distance_to_player") > enemy.get("attack_range") * 1.2:
enemy.get("state_machine").change_state(ChaseState.new())
return
# 攻击冷却
_attack_timer += delta
if _attack_timer >= enemy.get("attack_cooldown"):
_attack_timer = 0.0
enemy.perform_attack()
func exit(_enemy: CharacterBody2D) -> void:
pass
# ========== 逃跑状态 ==========
class FleeState extends EnemyState:
func _init():
state_name = "逃跑"
func enter(enemy: CharacterBody2D) -> void:
print("%s 血量太低了,开始逃跑!" % enemy.name)
func update(enemy: CharacterBody2D, delta: float) -> void:
# 血量恢复
if enemy.get("hp") > enemy.get("max_hp") * 0.5:
enemy.get("state_machine").change_state(PatrolState.new())
return
# 往远离玩家的方向跑
var player_pos: Vector2 = enemy.get("player_position")
var away_dir := (enemy.global_position - player_pos).normalized()
var flee_target := enemy.global_position + away_dir * 100
var direction := (flee_target - enemy.global_position).normalized()
enemy.velocity = direction * enemy.get("chase_speed") * 1.2
enemy.move_and_slide()
func exit(_enemy: CharacterBody2D) -> void:
pass状态机控制器
有了各个状态类,还需要一个"控制器"来管理当前处于哪个状态,以及状态之间的切换:
C
using Godot;
public partial class EnemyController : CharacterBody2D
{
// 属性
[Export] public float PatrolSpeed { get; set; } = 100f;
[Export] public float ChaseSpeed { get; set; } = 200f;
[Export] public float ChaseRange { get; set; } = 200f;
[Export] public float AttackRange { get; set; } = 50f;
[Export] public float AttackCooldown { get; set; } = 1.0f;
[Export] public float Hp { get; set; } = 100f;
[Export] public float MaxHp { get; set; } = 100f;
[Export] public float DetectionRange { get; set; } = 300f;
// 巡逻点
public Godot.Collections.Array<Vector2> Waypoints { get; set; } = new();
// 状态机
private EnemyState _currentState;
public string CurrentStateName => _currentState?.Name ?? "无";
// 玩家引用
private CharacterBody2D _player;
public override void _Ready()
{
// 设置初始状态为巡逻
ChangeState(new PatrolState());
}
public override void _PhysicsProcess(double delta)
{
// 更新玩家引用
_player = GetTree().GetFirstNodeInGroup("player") as CharacterBody2D;
// 更新当前状态
_currentState?.Update(this, delta);
}
// 切换状态
public void ChangeState(EnemyState newState)
{
if (_currentState != null)
{
GD.Print($"{Name} 状态切换:{_currentState.Name} -> {newState.Name}");
_currentState.Exit(this);
}
_currentState = newState;
_currentState.Enter(this);
}
// 辅助方法
public bool CanSeePlayer()
{
if (_player == null) return false;
return DistanceToPlayer() <= DetectionRange;
}
public float DistanceToPlayer()
{
return _player == null ? float.MaxValue : GlobalPosition.DistanceTo(_player.GlobalPosition);
}
public Vector2 PlayerPosition => _player?.GlobalPosition ?? GlobalPosition;
public void PerformAttack()
{
GD.Print($"{Name} 发动攻击!");
// 这里添加实际攻击逻辑
}
public void MoveToward(Vector2 target, float speed, double delta)
{
var direction = (target - GlobalPosition).Normalized();
Velocity = direction * speed;
MoveAndSlide();
}
public void StopMovement()
{
Velocity = Vector2.Zero;
}
}GDScript
extends CharacterBody2D
class_name EnemyController
# 属性
@export var patrol_speed: float = 100.0
@export var chase_speed: float = 200.0
@export var chase_range: float = 200.0
@export var attack_range: float = 50.0
@export var attack_cooldown: float = 1.0
@export var hp: float = 100.0
@export var max_hp: float = 100.0
@export var detection_range: float = 300.0
# 巡逻点
@export var waypoints: Array[Vector2] = []
# 状态机
var _current_state: EnemyState = null
var current_state_name: String = "无"
# 玩家引用
var _player: CharacterBody2D = null
func _ready() -> void:
# 设置初始状态为巡逻
change_state(PatrolState.new())
func _physics_process(delta: float) -> void:
# 更新玩家引用
_player = get_tree().get_first_node_in_group("player") as CharacterBody2D
# 更新当前状态
if _current_state:
_current_state.update(self, delta)
# 切换状态
func change_state(new_state: EnemyState) -> void:
if _current_state:
print("%s 状态切换:%s -> %s" % [name, _current_state.state_name, new_state.state_name])
_current_state.exit(self)
_current_state = new_state
_current_state.enter(self)
current_state_name = _current_state.state_name
# 辅助方法
func can_see_player() -> bool:
if not _player:
return false
return distance_to_player() <= detection_range
func distance_to_player() -> float:
if not _player:
return INF
return global_position.distance_to(_player.global_position)
var player_position: Vector2:
get:
return _player.global_position if _player else global_position
func perform_attack() -> void:
print("%s 发动攻击!" % name)
# 这里添加实际攻击逻辑状态机的优缺点
优点
| 优点 | 说明 |
|---|---|
| 简单直观 | 状态图一目了然,容易理解和讨论 |
| 容易调试 | 打印当前状态就能知道 AI 在做什么 |
| 性能好 | 每帧只执行一个状态的逻辑,开销很小 |
| 适合简单 AI | 巡逻、追逐、攻击这种简单的行为模式非常合适 |
缺点
| 缺点 | 说明 |
|---|---|
| 状态爆炸 | 行为多了之后,状态数量会急剧增长,状态图变得像一团乱麻 |
| 难以复用 | "追着玩家打"这个行为,追逐状态和攻击状态可能都要写一遍 |
| 缺乏层次 | 不能表达"先做 A,如果 A 失败就做 B,否则做 C"这种复杂逻辑 |
| 扩展困难 | 加一个新状态可能要修改很多现有状态的转移条件 |
行为树(Behavior Tree)——更强大的 AI 架构
当 AI 行为变得复杂(比如 RTS 游戏里的士兵,需要同时考虑资源采集、战斗、建造、逃跑等多种因素),状态机就不够用了。这时候就需要行为树。
行为树的核心概念
行为树就像一棵倒挂的树,从上往下执行。树的每个节点代表一个"任务"或"判断"。
打个比方:行为树就像你每天早上的决策流程:
起床了
├── 今天是工作日吗?
│ ├── 是 → 准备上班
│ │ ├── 天气好吗?
│ │ │ ├── 是 → 骑自行车去
│ │ │ └── 否 → 打车去
│ │ └── 到公司
│ └── 否 → 今天是休息日
│ ├── 想出去玩吗?
│ │ ├── 是 → 出门逛街
│ │ └── 否 → 在家打游戏
│ └── ...行为树的节点类型
行为树有四种基本节点类型:
| 节点类型 | 作用 | 返回值 | 举例 |
|---|---|---|---|
| Sequence(顺序) | 依次执行子节点,一个失败就全部失败 | 子节点全部成功才成功 | 先走到门边,再开门,再走出去 |
| Selector(选择) | 依次尝试子节点,一个成功就全部成功 | 子节点有一个成功就成功 | 优先攻击,不行就逃跑,再不行就待着 |
| Condition(条件) | 判断某个条件是否成立 | 成功或失败 | 玩家在视野内?血量低于30%? |
| Action(动作) | 执行具体的行为 | 成功或失败 | 移动、攻击、播放动画 |
行为树 vs 状态机怎么选
| 对比维度 | 状态机(FSM) | 行为树(BT) |
|---|---|---|
| 复杂度 | 简单 | 中等 |
| 适合行为数 | 3-8 个状态 | 10+ 个行为 |
| 调试难度 | 容易(打印当前状态) | 中等(需要追踪执行路径) |
| 复用性 | 低 | 高(子树可以复用) |
| 学习曲线 | 平缓 | 较陡 |
| 典型应用 | 平台跳跃敌人、射击游戏小怪 | RTS 单位、RPG NPC、复杂 Boss |
选择建议
- 初学者和简单 AI:用状态机,够用且好懂
- 复杂 AI(需要同时考虑多个因素):用行为树
- 大型项目:两者结合——行为树做高层决策,状态机做底层行为执行
实战案例:用 FSM 做敌人 AI
上面的代码已经是一个完整的 FSM 敌人 AI 实现了。让我们来看看如何在实际场景中使用它。
场景设置
- 创建一个
EnemyController场景(CharacterBody2D) - 添加
Sprite2D、CollisionShape2D等子节点 - 设置 Waypoints(巡逻点)——可以在编辑器中手动放置几个 Marker2D 节点
- 把玩家节点加入 "player" 组
运行效果
敌人会自动:
- 在巡逻点之间来回走动
- 发现玩家后追过去
- 到了攻击范围就停下来攻击
- 血量低了就逃跑
- 玩家跑远后回到巡逻
扩展思路
你可以在上面基础上继续扩展:
- 添加搜索状态:玩家消失后去最后看到的位置搜索
- 添加警戒状态:听到声音但没看到人,提高警惕
- 添加呼叫支援状态:发现玩家后通知附近的同伴
- 添加动画混合:不同状态播放不同动画
- 添加音效:发现玩家时播放警报声
最终建议
- 从状态机开始学,它是 AI 编程的基础
- 把每个状态写得小而清晰,一个状态只做一件事
- 状态切换时要有明确的进入/退出逻辑(比如切换动画)
- 用日志打印状态切换信息,方便调试
- 等你发现状态机不够用了,再学行为树
