在Unity开发游戏时,用C#实现游戏AI常常会陷入switch和if梯子的泥潭,代码耦合度高难维护。比如做一个巡逻的守卫,靠近玩家就追击,追近了就攻击,刚开始用Update里的if判断,后来换成switch,每加一个状态就要改核心逻辑,代码像堆积木一样乱,维护起来全是坑。用状态机模式就能彻底解决这个问题,接下来就讲怎么落地,怎么让AI逻辑清晰、好扩展、不僵硬。
一、为什么游戏AI需要摆脱switch/if梯子
1.1 常见的AI状态维护痛点
很多刚接触Unity游戏开发的同学,写AI时都会犯同一个错:把所有逻辑塞在一个大Update里,用switch包裹所有状态。比如一个基础敌人的代码大概是这样的:
// 旧写法示例:Unity敌人AI的switch逻辑
void Update() {
// 记录当前状态
switch(currentState) {
case State.Patrol:
// 执行巡逻动作
Patrol();
// 检查是否进入追击范围
if(PlayerInRange()) currentState = State.Chase;
break;
case State.Chase:
// 执行追击动作
Chase();
// 超出追击范围回巡逻
if(PlayerTooFar()) currentState = State.Patrol;
// 进入攻击范围切攻击
if(PlayerAttackRange()) currentState = State.Attack;
break;
case State.Attack:
// 执行攻击动作
Attack();
// 玩家跑远切追击
if(PlayerRunAway()) currentState = State.Chase;
break;
}
}
这种写法的问题很明显:加一个“受伤”状态就要加case,改一个状态的切换条件要碰所有相关代码,新人看不懂,改个功能可能牵一发而动全身,时间久了代码变成没人敢碰的“屎山”。
1.2 状态机的核心价值
状态机的本质是把“每个状态的独立逻辑”和“状态切换的管理”分开,让每个状态成为独立的单元,加新状态不用改核心逻辑,只需要写新的状态类就行。
二、状态机模式的基础拆分方法
2.1 定义统一的状态接口
所有状态要遵循同一个规则,比如每个状态都要有「进入时的操作」「每帧更新的逻辑」「退出时的清理」,这样状态机可以统一调用,不用管具体状态是什么。
2.2 独立的状态管理类
专门写一个StateMachine类,只负责记录当前状态、切换状态,不关心每个状态的具体逻辑,相当于“交通指挥员”,管状态的流转,不管每个状态的内容。
三、Unity中C#状态机的完整实现
这里用的技术栈是Unity 2021.3 LTS + C#,所有代码都可以直接复制到Unity项目中使用。
// 1. 状态基接口:所有具体状态必须实现这个接口
public interface IState
{
// 进入当前状态时调用(比如重置数据、播放动画)
void Enter();
// 每帧更新(比如处理移动、判断切换条件)
void Update();
// 退出当前状态时调用(比如停止移动、清理临时变量)
void Exit();
}
// 2. 状态机类:专门管理状态的切换和当前状态
public class StateMachine
{
// 当前激活的状态
public IState CurrentState { get; private set; }
// 切换到新状态,会自动执行旧状态的Exit和新状态的Enter
public void ChangeState(IState newState)
{
// 先退出旧状态(如果有)
CurrentState?.Exit();
// 切换为新状态
CurrentState = newState;
// 初始化新状态
CurrentState.Enter();
}
// 每帧更新当前状态
public void Update()
{
CurrentState?.Update();
}
}
// 3. 具体状态示例:巡逻状态,负责敌人的巡逻逻辑
public class PatrolState : IState
{
// 引用敌人控制器,获取敌人的属性和方法
private readonly EnemyController enemy;
// 构造函数传入敌人对象,避免静态引用
public PatrolState(EnemyController enemy)
{
this.enemy = enemy;
}
public void Enter()
{
// 进入巡逻状态时,选下一个巡逻点
enemy.SetNextPatrolPoint();
// 停止之前的攻击动作
enemy.StopAttack();
}
public void Update()
{
// 巡逻核心逻辑:向当前巡逻点移动(这里用Unity NavMesh的方法)
enemy.MoveToPatrolPoint();
// 检查是否进入追击范围,是就切到追击状态
if (enemy.IsPlayerInChaseRange())
{
enemy.StateMachine.ChangeState(new ChaseState(enemy));
}
}
public void Exit()
{
// 退出巡逻时,停止移动,避免残留移动逻辑
enemy.StopMoving();
}
}
// 4. 具体状态示例:追击状态,负责敌人追玩家的逻辑
public class ChaseState : IState
{
private readonly EnemyController enemy;
public ChaseState(EnemyController enemy)
{
this.enemy = enemy;
}
public void Enter()
{
// 进入追击时,停止攻击
enemy.StopAttack();
}
public void Update()
{
// 追击核心逻辑:向玩家移动
enemy.MoveToPlayer();
// 玩家跑出追击范围,切回巡逻
if (enemy.IsPlayerTooFar())
{
enemy.StateMachine.ChangeState(new PatrolState(enemy));
}
// 玩家进入攻击范围,切到攻击
if (enemy.IsPlayerInAttackRange())
{
enemy.StateMachine.ChangeState(new AttackState(enemy));
}
}
public void Exit()
{
// 退出追击时,停止移动
enemy.StopMoving();
}
}
// 5. 具体状态示例:攻击状态,负责敌人攻击玩家的逻辑
public class AttackState : IState
{
private readonly EnemyController enemy;
// 攻击冷却时间,避免连续攻击
private float attackTimer;
private const float AttackCoolDown = 1.5f;
public AttackState(EnemyController enemy)
{
this.enemy = enemy;
}
public void Enter()
{
// 进入攻击时,重置计时器,播放攻击动画
attackTimer = 0;
enemy.PlayAttackAnimation();
}
public void Update()
{
// 累加时间,达到冷却就攻击
attackTimer += Time.deltaTime;
if (attackTimer >= AttackCoolDown)
{
enemy.AttackPlayer();
attackTimer = 0;
}
// 玩家跑出攻击范围,切回追击
if (!enemy.IsPlayerInAttackRange())
{
enemy.StateMachine.ChangeState(new ChaseState(enemy));
}
}
public void Exit()
{
// 攻击状态退出的清理逻辑(比如硬直结束)
}
}
// 6. 敌人控制器:挂载到Unity的敌人GameObject上,负责整合所有状态和逻辑
public class EnemyController : MonoBehaviour
{
// 外部赋值:玩家的Transform、巡逻点数组
public Transform player;
public Transform[] patrolPoints;
// 公开状态机,供状态类调用切换
public StateMachine StateMachine { get; private set; }
// 当前巡逻点的索引
private int currentPatrolIndex;
void Awake()
{
// 初始化状态机,一开始进入巡逻状态
StateMachine = new StateMachine();
StateMachine.ChangeState(new PatrolState(this));
}
void Update()
{
// 每帧更新状态机,让状态逻辑跑起来
StateMachine.Update();
}
// 以下是敌人的公共方法,供状态类调用,实现解耦
public void SetNextPatrolPoint()
{
currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length;
}
public void MoveToPatrolPoint()
{
// 实际项目中替换为NavMeshAgent的移动代码
// GetComponent<NavMeshAgent>().SetDestination(patrolPoints[currentPatrolIndex].position);
}
public bool IsPlayerInChaseRange()
{
return Vector3.Distance(transform.position, player.position) < 10f;
}
public bool IsPlayerInAttackRange()
{
return Vector3.Distance(transform.position, player.position) < 2f;
}
public bool IsPlayerTooFar()
{
return Vector3.Distance(transform.position, player.position) > 15f;
}
public void MoveToPlayer()
{
// GetComponent<NavMeshAgent>().SetDestination(player.position);
}
public void StopMoving()
{
// GetComponent<NavMeshAgent>().ResetPath();
}
public void StopAttack()
{
// 停止攻击的逻辑(比如关闭攻击碰撞体)
}
public void PlayAttackAnimation()
{
// 播放攻击动画,替换为Animator的代码
// GetComponent<Animator>().SetTrigger("Attack");
}
public void AttackPlayer()
{
// 造成伤害的逻辑,替换为实际的伤害系统
Debug.Log($"敌人{gameObject.name}攻击了玩家");
}
}
四、状态机的应用场景、优缺点与注意事项
4.1 常见应用场景
只要涉及多状态切换的游戏逻辑,都可以用状态机:RPG游戏的敌人巡逻、追击、施法、逃跑;塔防游戏的怪物移动、攻击、死亡;NPC的对话、互动、巡逻;甚至主角的状态(待机、奔跑、跳跃、受伤)也能用,彻底避免大段的if/switch。
4.2 技术优缺点
优点:解耦性极强,每个状态是独立类,加新状态只要写新类,不用碰核心逻辑;可读性高,看每个状态的代码就知道做什么,不用翻大段的switch;可维护性好,改一个状态的逻辑不会影响其他状态。缺点:如果状态太多,会产生很多小类,新手可能觉得麻烦;状态切换条件没处理好的话,容易出现循环切换的bug(比如追击→攻击→追击→攻击无限循环)。
4.3 注意事项
状态切换的边界要清晰,比如追击状态只在“敌人进入追击范围”时触发,超出范围就退出,避免残留移动或攻击逻辑;状态类尽量不要持有多余的临时变量,比如可以用对象池复用状态,减少GC(Unity里频繁new类会产生垃圾,影响性能);每个状态的Enter和Exit要对应,进入时初始化,退出时清理,避免状态之间的干扰(比如上一个状态的移动逻辑没停,导致新状态也在移动)。
4.4 总结
用状态机模式在Unity里做游戏AI,就是把原来的“大杂烩式”switch拆成一个个独立的小状态类,从根本上摆脱僵硬的条件判断,代码变得清晰好维护,加功能也快,适合所有需要多状态逻辑的游戏开发,不管是敌人还是NPC都能用,是Unity游戏开发中AI逻辑的标准落地姿势。
评论
围绕“状态机模式在Unity游戏AI中的落地姿势,C#代码如何彻底避免僵硬的switch与if梯子”参与讨论