概述 第 01 篇我们实现了基础状态机,用于管理玩家行为。但对于复杂的游戏 AI——比如敌人有巡逻、追踪、攻击、撤退等多种状态,且每种状态下又有子状态——单一层的状态机会导致状态膨胀。
分层状态机 将状态组织成层级结构,父状态控制子状态的切换规则。行为树 则用树状节点组合来描述决策逻辑,是工业级 AI 的主流方案。
1. 分层状态机(Hierarchical State Machine) 1.1 为什么需要分层 考虑一个敌人 AI:
1 2 3 4 5 6 7 8 9 10 - 空闲 - 站岗 - 巡逻 - 警觉 - 追踪 - 搜索 - 战斗 - 攻击 - 闪避 - 施放技能
如果用平面状态机,”空闲”下的站岗和巡逻共享相同的前提条件(未发现玩家),但每种子状态的 Update 逻辑不同。分层状态机让父状态管理共有逻辑,子状态管理差异化逻辑。
1.2 实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 public interface IState { void Enter () ; void Update () ; void FixedUpdate () ; void Exit () ; } public class StateMachine { public IState CurrentState { get ; private set ; } private IState _globalState; public void Initialize (IState startState, IState globalState = null ) { _globalState = globalState; CurrentState = startState; CurrentState.Enter(); } public void ChangeState (IState newState ) { CurrentState?.Exit(); CurrentState = newState; CurrentState.Enter(); } public void Update () { _globalState?.Update(); CurrentState?.Update(); } public void FixedUpdate () { _globalState?.FixedUpdate(); CurrentState?.FixedUpdate(); } } public class EnemyIdleState : IState { private readonly Enemy _enemy; private readonly StateMachine _subMachine = new (); private float _patrolTimer; public EnemyIdleState (Enemy enemy ) { _enemy = enemy; } public void Enter () { var startSub = Random.value > 0.5f ? new EnemyStandGuardState(_enemy, this ) : new EnemyPatrolState(_enemy, this ) as IState; _subMachine.Initialize(startSub); _patrolTimer = 0f ; } public void Update () { _subMachine.Update(); if (_enemy.DetectionSystem.IsPlayerDetected()) { _enemy.StateMachine.ChangeState(new EnemyAlertState(_enemy)); } } public void FixedUpdate () => _subMachine.FixedUpdate(); public void Exit () { } }
1.3 子状态 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class EnemyPatrolState : IState { private readonly Enemy _enemy; private readonly EnemyIdleState _parent; private int _currentWaypoint; public EnemyPatrolState (Enemy enemy, EnemyIdleState parent ) { _enemy = enemy; _parent = parent; } public void Enter () { _enemy.Animator.Play("Walk" ); _currentWaypoint = 0 ; } public void Update () { var waypoint = _enemy.PatrolRoute[_currentWaypoint]; if (Vector3.Distance(_enemy.transform.position, waypoint.position) < 0.5f ) { _currentWaypoint = (_currentWaypoint + 1 ) % _enemy.PatrolRoute.Length; _parent.SwitchSubState(new EnemyStandGuardState(_enemy, _parent)); } } public void FixedUpdate () { var waypoint = _enemy.PatrolRoute[_currentWaypoint]; _enemy.Movement.MoveTowards(waypoint.position, _enemy.PatrolSpeed); } public void Exit () { } }
父状态通过方法暴露子状态切换接口,子状态在满足条件时回调父状态切换。
2. 行为树(Behavior Tree) 行为树是游戏 AI 的工业标准方案——用树的节点组合来描述行为逻辑,直观、可扩展、可可视化编辑。
2.1 核心概念
节点类型
作用
返回值
Root
树入口
-
Sequence(顺序节点)
依次执行子节点,全部成功才算成功
Success / Failure
Selector(选择节点)
依次执行子节点,任一成功则返回
Success / Failure
Condition(条件节点)
检查条件是否满足
Success / Failure
Action(动作节点)
执行具体行为
Success / Failure / Running
Decorator(装饰节点)
修饰子节点行为(取反、循环、限时)
依赖子节点
2.2 基础节点实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public enum NodeState{ Success, Failure, Running } public abstract class BTNode { public abstract NodeState Evaluate () ; } public class Sequence : BTNode { private readonly List<BTNode> _children = new (); public Sequence (params BTNode[] children ) { _children.AddRange(children); } public override NodeState Evaluate () { foreach (var child in _children) { var result = child.Evaluate(); if (result == NodeState.Failure) return NodeState.Failure; } return NodeState.Success; } } public class Selector : BTNode { private readonly List<BTNode> _children = new (); public Selector (params BTNode[] children ) { _children.AddRange(children); } public override NodeState Evaluate () { foreach (var child in _children) { var result = child.Evaluate(); if (result == NodeState.Success) return NodeState.Success; } return NodeState.Failure; } }
2.3 AI 条件与动作节点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 public class CheckPlayerInRange : BTNode { private readonly Transform _self; private readonly Transform _player; private readonly float _range; public CheckPlayerInRange (Transform self, Transform player, float range ) { _self = self; _player = player; _range = range; } public override NodeState Evaluate () { var distance = Vector3.Distance(_self.position, _player.position); return distance <= _range ? NodeState.Success : NodeState.Failure; } } public class MoveToPlayer : BTNode { private readonly Enemy _enemy; private readonly Transform _player; private readonly float _speed; public MoveToPlayer (Enemy enemy, Transform player, float speed ) { _enemy = enemy; _player = player; _speed = speed; } public override NodeState Evaluate () { var direction = (_player.position - _enemy.transform.position).normalized; _enemy.Rigidbody.velocity = direction * _speed; if (Vector3.Distance(_enemy.transform.position, _player.position) < 1.5f ) return NodeState.Success; return NodeState.Running; } } public class AttackPlayer : BTNode { private readonly Enemy _enemy; private readonly float _attackCooldown; private float _lastAttackTime; public AttackPlayer (Enemy enemy, float cooldown ) { _enemy = enemy; _attackCooldown = cooldown; } public override NodeState Evaluate () { if (Time.time - _lastAttackTime < _attackCooldown) return NodeState.Running; _lastAttackTime = Time.time; _enemy.Animator.SetTrigger("Attack" ); return NodeState.Success; } } public class Inverter : BTNode { private readonly BTNode _child; public Inverter (BTNode child ) => _child = child; public override NodeState Evaluate () { return _child.Evaluate() switch { NodeState.Success => NodeState.Failure, NodeState.Failure => NodeState.Success, _ => NodeState.Running }; } }
2.4 组合行为树 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class EnemyAI : MonoBehaviour { private BTNode _root; private Transform _player; private void Start () { _player = GameObject.FindGameObjectWithTag("Player" ).transform; _root = new Selector( new Sequence( new CheckPlayerInRange(transform, _player, 2f ), new AttackPlayer(this , 1.5f ) ), new Sequence( new CheckPlayerInRange(transform, _player, 10f ), new MoveToPlayer(this , _player, 3.5f ) ), new PatrolRoute(this ) ); } private void Update () { _root.Evaluate(); } }
2.5 Running 状态的重要性 对比状态机,行为树的独特之处在于 Running 状态。MoveToPlayer 在”靠近中”返回 Running,Selector 知道这个节点还在执行中,不会跳到下一个。状态机需要专门的状态类来持有移动逻辑,而行为树通过 Running 加逐帧 Evaluate 自然实现。
3. 状态机 vs 行为树
维度
分层状态机
行为树
状态数量
少到中等(<20 个)
可支持数百节点
复用性
状态类可复用,但组合方式固定
节点可任意组合,复用性更高
可视化
需额外工具
天然适合可视化编辑
调试
需要 Log 当前状态
节点返回值清晰,可视化工具直接显示
Running 状态
通过 Update 隐式实现
作为一等公民(NodeState.Running)
性能
极轻量
节点多时有 GC 压力(可用对象池优化)
适合场景
玩家状态、简单 AI
复杂 AI、BOSS 行为、NPC 日常
建议 1 2 3 主角行为 -> 分层状态机(少状态,高实时性) 敌人 AI -> 行为树(多决策,易扩展) NPC 日常 -> 行为树(巡逻+响应事件+对话)
总结
分层状态机 用嵌套的子状态机管理同一父状态下的差异化行为,适合玩家状态管理
行为树 用 Sequence/Selector 组合描述决策逻辑,Running 状态让长时行为自然表达
小型项目手写行为树即可,大型 AI 考虑 Behavior Designer 等可视化工具
下一篇将探讨 UniTask 与高级异步编程 ,彻底告别协程的局限性。