在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逻辑的标准落地姿势。