Unity C#进阶:用状态模式与FSM优雅管理复杂敌人AI,告别Spaghetti Code!(Day32)

Langchain系列文章目录

01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘

PyTorch系列文章目录

Python系列文章目录

C#系列文章目录

01-C#与游戏开发的初次见面:从零开始的Unity之旅
02-C#入门:从变量与数据类型开始你的游戏开发之旅
03-C#运算符与表达式:从入门到游戏伤害计算实践
04-从零开始学C#:用if-else和switch打造智能游戏逻辑
05-掌握C#循环:for、while、break与continue详解及游戏案例
06-玩转C#函数:参数、返回值与游戏中的攻击逻辑封装
07-Unity游戏开发入门:用C#控制游戏对象移动
08-C#面向对象编程基础:类的定义、属性与字段详解
09-C#封装与访问修饰符:保护数据安全的利器
10-如何用C#继承提升游戏开发效率?Enemy与Boss案例解析
11-C#多态性入门:从零到游戏开发实战
12-C#接口王者之路:从入门到Unity游戏开发实战 (IAttackable案例详解)
13-C#静态成员揭秘:共享数据与方法的利器
14-Unity 面向对象实战:掌握组件化设计与脚本通信,构建玩家敌人交互
15-C#入门 Day15:彻底搞懂数组!从基础到游戏子弹管理实战
16-C# List 从入门到实战:掌握动态数组,轻松管理游戏敌人列表 (含代码示例)
17-C# 字典 (Dictionary) 完全指南:从入门到游戏属性表实战 (Day 17)
18-C#游戏开发【第18天】 | 深入理解队列(Queue)与栈(Stack):从基础到任务队列实战
19-【C# 进阶】深入理解枚举 Flags 属性:游戏开发中多状态组合的利器
20-C#结构体(Struct)深度解析:轻量数据容器与游戏开发应用 (Day 20)
21-Unity数据持久化进阶:告别硬编码,用ScriptableObject优雅管理游戏配置!(Day 21)
22-Unity C# 健壮性编程:告别崩溃!掌握异常处理与调试的 4 大核心技巧 (Day 22)
23-C#代码解耦利器:委托与事件(Delegate & Event)从入门到实践 (Day 23)
24-Unity脚本通信终极指南:从0到1精通UnityEvent与事件解耦(Day 24)
25-精通C# Lambda与LINQ:Unity数据处理效率提升10倍的秘诀! (Day 25)
26-# Unity C#进阶:掌握泛型编程,告别重复代码,编写优雅复用的通用组件!(Day26)
27-Unity协程从入门到精通:告别卡顿,用Coroutine优雅处理异步与时序任务 (Day 27)
28-搞定玩家控制!Unity输入系统、物理引擎、碰撞检测实战指南 (Day 28)
29-# Unity动画控制核心:Animator状态机与C#脚本实战指南 (Day 29)
30-Unity UI 从零到精通 (第30天): Canvas、布局与C#交互实战 (Day 30)
31-Unity性能优化利器:彻底搞懂对象池技术(附C#实现与源码解析)
32-Unity C#进阶:用状态模式与FSM优雅管理复杂敌人AI,告别Spaghetti Code!(Day32)


文章目录

  • Langchain系列文章目录
  • PyTorch系列文章目录
  • Python系列文章目录
  • C#系列文章目录
  • 前言
  • 一、为什么需要状态管理?
    • 1.1 复杂行为带来的挑战
      • 1.1.1 “意大利面条代码”的困境
      • 1.1.2 一个简单的例子
    • 1.2 状态模式与FSM:结构化的解决方案
      • 1.2.1 状态模式的核心思想
      • 1.2.2 FSM作为具体模型
  • 二、状态模式详解
    • 2.1 核心概念
    • 2.2 组成元素
    • 2.3 工作原理
    • 2.4 伪代码示例(C#结构)
  • 三、有限状态机(FSM)基础
    • 3.1 FSM定义与特点
    • 3.2 FSM核心三要素
      • 3.2.1 状态(States)
      • 3.2.2 事件/触发器(Events/Triggers)
      • 3.2.3 转换(Transitions)
    • 3.3 可视化FSM:状态图
  • 四、在Unity中实现FSM
    • 4.1 基于枚举的简单FSM
      • 4.1.1 实现思路
      • 4.1.2 优缺点分析
      • 4.1.3 代码示例
    • 4.2 基于类的FSM(更接近状态模式)
      • 4.2.1 实现思路
      • 4.2.2 优缺点分析
      • 4.2.3 代码结构示例
  • 五、Animator状态机 vs 代码状态机
    • 5.1 Unity Animator状态机
      • 5.1.1 简介
      • 5.1.2 优点
      • 5.1.3 缺点
    • 5.2 代码状态机(基于类或枚举)
      • 5.2.1 简介
      • 5.2.2 优点
      • 5.2.3 缺点
    • 5.3 应用场景选择:何时使用哪种?
  • 六、实践:实现敌人AI状态机
    • 6.1 需求定义
    • 6.2 状态设计
    • 6.3 实现巡逻状态 (PatrolState)
    • 6.4 实现追击状态 (ChaseState)
    • 6.5 实现攻击状态 (AttackState)
    • 6.6 状态切换逻辑
    • 6.7 完整设置与运行
  • 七、总结


前言

欢迎来到《C# for Unity开发者50天掌握》专栏的第32天!在前几周的学习中,我们掌握了C#的基础语法、面向对象编程以及常用的数据结构。今天,我们将深入探讨一个在游戏开发中至关重要的设计模式——状态模式,以及它的常见应用形式——有限状态机(FSM)

想象一下,游戏中的一个敌人AI需要根据不同情况(如玩家距离、自身血量)做出不同的行为(巡逻、追击、攻击、逃跑)。如果使用大量的if-elseswitch语句来管理这些行为,代码很快会变得臃肿、难以理解和维护,形成所谓的“意大利面条代码”(Spaghetti Code)。状态模式和FSM正是解决这类问题的利器,它们能帮助我们以结构化、可扩展的方式管理复杂对象的行为。本文将带你从概念到实践,彻底搞懂状态模式与FSM,并动手为敌人AI实现一个简单的状态机。

一、为什么需要状态管理?

在进入具体的技术细节之前,我们先来理解引入状态管理的必要性。

1.1 复杂行为带来的挑战

随着游戏逻辑复杂度的提升,一个游戏对象(如玩家角色、NPC、敌人)可能拥有多种不同的状态,并且需要在不同状态下表现出不同的行为。

1.1.1 “意大利面条代码”的困境

当状态数量增多时,如果仅仅依赖条件判断语句(if-elseswitch)来决定当前行为,会导致:

  • 可读性差: 大量的嵌套条件判断使得代码逻辑难以追踪。
  • 维护困难: 增加或修改一个状态,可能需要改动多处代码,容易引入错误。
  • 扩展性受限: 添加新状态或新行为变得非常复杂和痛苦。
  • 职责不清: 对象的核心逻辑与状态判断逻辑紧密耦合在一起。

1.1.2 一个简单的例子

想象一个简单的门,它有“打开”和“关闭”两种状态。我们可以用一个布尔变量isOpen来管理。但如果这个门还有“正在打开”、“正在关闭”、“已锁定”等状态,并且每个状态下对“开门”、“关门”、“锁门”等操作的响应都不同,简单的条件判断就会迅速膨胀。

1.2 状态模式与FSM:结构化的解决方案

为了应对上述挑战,开发者们总结出了**状态模式(State Pattern)有限状态机(Finite State Machine, FSM)**这两种强大的设计思想。

1.2.1 状态模式的核心思想

状态模式允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。它将与特定状态相关的行为局部化,并且将不同状态的行为分割开来。

1.2.2 FSM作为具体模型

有限状态机是一种数学计算模型,也是实现状态转换逻辑的一种常用方式。它定义了一组有限的状态、状态之间的转换规则以及触发这些转换的事件。在游戏开发中,FSM是状态模式的一种非常直观和实用的实现方式。

二、状态模式详解

状态模式是GoF(Gang of Four)经典设计模式之一,旨在解决对象状态变化与行为耦合的问题。

2.1 核心概念

状态模式的意图是:允许一个对象在其内部状态改变时改变其行为。对象看起来似乎修改了它的类。

其本质是将每个状态的行为封装到独立的状态类中,并将对象(称为“上下文”)的行为委托给当前的状态对象。当对象的状态改变时,只需改变其引用的状态对象即可。

2.2 组成元素

一个典型的状态模式实现包含以下角色:

  • Context(上下文): 拥有状态的对象,它维护一个当前状态对象的引用,并将与状态相关的行为委托给当前状态对象处理。它通常提供一个方法来改变当前状态。
  • State(状态接口/抽象类): 定义一个接口或抽象类,用于封装与Context某个状态相关的行为。通常包含处理请求的方法(如HandleInput(), UpdateState()等)以及进入/退出该状态的方法(Enter(), Exit())。
  • ConcreteState(具体状态类): 实现State接口或继承State抽象类,具体实现该状态下的行为逻辑。在处理完请求后,可能需要决定并告知Context转换到下一个状态。

2.3 工作原理

  1. 封装状态行为: 将不同状态下的行为分别封装在不同的ConcreteState类中。
  2. 委托行为: Context对象持有一个指向当前State对象的引用。当Context需要执行与状态相关的操作时,它调用当前State对象的相应方法。
  3. 状态转换: 当某个条件满足(如接收到特定事件或内部逻辑判断)时,当前ConcreteState对象可以决定切换到下一个状态,并通过调用Context提供的方法来更新Context持有的State对象引用,从而完成状态转换。

2.4 伪代码示例(C#结构)

// 状态接口
public interface IState
{
    void EnterState(Context context); // 进入状态时的逻辑
    void UpdateState(Context context); // 状态激活时的更新逻辑
    void ExitState(Context context);  // 退出状态时的逻辑
    void HandleInput(Context context, InputType input); // 处理输入或事件
}

// 具体状态A
public class ConcreteStateA : IState
{
    public void EnterState(Context context) { /* 初始化状态A */ }
    public void UpdateState(Context context) { /* 状态A的持续行为 */ }
    public void ExitState(Context context) { /* 清理状态A */ }
    public void HandleInput(Context context, InputType input)
    {
        if (input == InputType.SomeTrigger)
        {
            // 条件满足,切换到状态B
            context.TransitionToState(new ConcreteStateB());
        }
    }
}

// 具体状态B (类似实现)
public class ConcreteStateB : IState { /* ... */ }

// 上下文类
public class Context
{
    private IState _currentState;

    public Context(IState initialState)
    {
        _currentState = initialState;
        _currentState.EnterState(this);
    }

    // 切换状态的方法
    public void TransitionToState(IState newState)
    {
        _currentState.ExitState(this); // 执行旧状态的退出逻辑
        _currentState = newState;       // 更新当前状态
        _currentState.EnterState(this); // 执行新状态的进入逻辑
    }

    // 将行为委托给当前状态
    public void RequestUpdate()
    {
        _currentState.UpdateState(this);
    }

    public void ProcessInput(InputType input)
    {
        _currentState.HandleInput(this, input);
    }
}

public enum InputType { Default, SomeTrigger }

三、有限状态机(FSM)基础

有限状态机(FSM)是状态模式在实践中非常常用的一种具体实现模型,尤其在游戏AI、动画控制、流程管理等领域。

3.1 FSM定义与特点

FSM可以看作是一个抽象机器,它在任何给定时间只能处于有限个状态中的一个。当接收到特定的**事件(Event)或满足某些条件(Condition)时,它会从当前状态转换(Transition)**到另一个状态。

  • 有限性: 状态的数量是预先定义好的、有限的。
  • 确定性/非确定性: 在确定性FSM(Deterministic FSM)中,给定当前状态和输入事件,下一个状态是唯一确定的。非确定性FSM(Non-deterministic FSM)则可能转换到多个状态之一。游戏开发中常用的是确定性FSM。
  • 结构化: FSM提供了一种清晰的方式来组织和管理状态及其转换逻辑。

3.2 FSM核心三要素

理解FSM的关键在于掌握它的三个核心组成部分:

3.2.1 状态(States)

代表对象在某个时间点所处的具体情况或行为模式。例如,敌人的状态可以是:

  • Patrol(巡逻)
  • Chase(追击)
  • Attack(攻击)
  • Idle(待机)
  • Flee(逃跑)

3.2.2 事件/触发器(Events/Triggers)

导致状态发生变化的原因或信号。这些可以是外部输入(如玩家进入视野)、内部条件变化(如生命值低于阈值)、时间流逝等。例如:

  • PlayerDetected(探测到玩家)
  • PlayerLost(丢失玩家目标)
  • ReachedAttackRange(进入攻击范围)
  • HealthLow(生命值低)
  • TimerExpired(计时器到期)

3.2.3 转换(Transitions)

定义了从一个状态迁移到另一个状态的规则。通常表示为:(当前状态 + 事件/条件) => 新状态。例如:

  • (Patrol + PlayerDetected) => Chase
  • (Chase + ReachedAttackRange) => Attack
  • (Attack + PlayerOutOfRange) => Chase
  • (Chase + PlayerLost) => Patrol

3.3 可视化FSM:状态图

FSM通常使用**状态图(State Diagram)**进行可视化表示,这极大地增强了理解和沟通效率。状态图用圆圈(或方框)表示状态,用带箭头的线表示转换,线上标注触发转换的事件或条件。

下面是一个简单敌人AI状态机的流程图示例:

PlayerDetected
PlayerLost
ReachedAttackRange
PlayerOutOfRange
PlayerKilled
HealthLow
SafeDistance
Patrol
Chase
Attack
Dead
Flee

(注意:Mermaid图在某些Markdown编辑器或平台(如CSDN)可能需要特定语法或插件支持才能正确渲染)

四、在Unity中实现FSM

在Unity C#项目中,我们可以通过多种方式实现FSM。下面介绍两种常见的方法:基于枚举的简单FSM和基于类的FSM。

4.1 基于枚举的简单FSM

这种方法适用于状态相对较少、逻辑不复杂的情况。

4.1.1 实现思路

  1. 定义一个枚举(enum)来表示所有可能的状态。
  2. 在需要管理状态的脚本(通常是MonoBehaviour)中,持有一个该枚举类型的变量,表示当前状态。
  3. Update()方法或一个专门的状态处理方法中,使用switch语句根据当前状态变量的值来执行相应的行为逻辑和状态转换检查。

4.1.2 优缺点分析

  • 优点: 实现简单快捷,代码量少,易于理解(对于小型FSM)。
  • 缺点:
    • 当状态增多或状态逻辑变复杂时,switch语句会变得庞大且难以维护。
    • 状态行为逻辑、状态转换逻辑以及状态进入/退出的逻辑容易混杂在一起,违反单一职责原则。
    • 扩展性较差,添加新状态需要修改enumswitch语句。

4.1.3 代码示例

using UnityEngine;

public class SimpleEnemyAI_Enum : MonoBehaviour
{
    // 定义状态枚举
    public enum EnemyState { Patrol, Chase, Attack }

    // 当前状态
    public EnemyState currentState = EnemyState.Patrol;

    public float patrolSpeed = 2f;
    public float chaseSpeed = 5f;
    public float attackRange = 1.5f;
    public Transform player; // 假设已赋值

    void Update()
    {
        // 根据当前状态执行行为和检查转换
        switch (currentState)
        {
            case EnemyState.Patrol:
                PatrolBehavior();
                CheckForPlayer();
                break;

            case EnemyState.Chase:
                ChaseBehavior();
                CheckAttackRange();
                CheckPlayerLost();
                break;

            case EnemyState.Attack:
                AttackBehavior();
                CheckPlayerOutOfRange();
                break;
        }
    }

    void PatrolBehavior()
    {
        // 简单的巡逻逻辑: 在某个区域来回移动 (示例简化)
        transform.Translate(Vector3.forward * patrolSpeed * Time.deltaTime);
        Debug.Log("Patrolling...");
    }

    void ChaseBehavior()
    {
        // 朝玩家移动
        if (player != null)
        {
            Vector3 direction = (player.position - transform.position).normalized;
            transform.position += direction * chaseSpeed * Time.deltaTime;
            transform.LookAt(player); // 面向玩家
            Debug.Log("Chasing Player!");
        }
    }

    void AttackBehavior()
    {
        // 简单的攻击逻辑 (示例简化)
        Debug.Log("Attacking Player!");
        // 可能包含攻击动画触发、伤害计算等
    }

    // --- 状态转换检查 ---

    void CheckForPlayer()
    {
        // 简单的探测逻辑 (示例简化)
        if (player != null && Vector3.Distance(transform.position, player.position) < 10f) // 假设探测范围10米
        {
            Debug.Log("Player Detected! Switching to Chase.");
            currentState = EnemyState.Chase;
        }
    }

    void CheckAttackRange()
    {
        if (player != null && Vector3.Distance(transform.position, player.position) <= attackRange)
        {
            Debug.Log("Reached Attack Range! Switching to Attack.");
            currentState = EnemyState.Attack;
        }
    }

    void CheckPlayerLost()
    {
        // 简单的丢失逻辑 (示例简化)
        if (player == null || Vector3.Distance(transform.position, player.position) > 15f) // 假设丢失距离15米
        {
            Debug.Log("Player Lost! Switching back to Patrol.");
            currentState = EnemyState.Patrol;
            // 可能需要返回巡逻点
        }
    }

    void CheckPlayerOutOfRange()
    {
        if (player == null || Vector3.Distance(transform.position, player.position) > attackRange)
        {
            Debug.Log("Player out of Attack Range! Switching to Chase.");
            currentState = EnemyState.Chase;
        }
    }
}

4.2 基于类的FSM(更接近状态模式)

这种方法更符合面向对象的设计原则,尤其适合状态较多、逻辑复杂的场景。它实现了状态模式的核心思想。

4.2.1 实现思路

  1. 定义一个State基类(或接口IState),包含所有状态共有的方法,如Enter()(进入状态时调用)、Execute()(状态激活时每帧调用)和Exit()(退出状态时调用)。
  2. 为每个具体状态(如PatrolState, ChaseState, AttackState)创建单独的类,继承自State基类并实现其方法,封装该状态的行为逻辑和转换条件检查。
  3. 在需要管理状态的脚本(EnemyController)中,持有一个对当前State对象的引用。
  4. EnemyControllerUpdate()方法调用当前State对象的Execute()方法。
  5. 状态转换由当前的ConcreteState对象内部逻辑决定,并通过调用EnemyController提供的状态切换方法来完成。

4.2.2 优缺点分析

  • 优点:
    • 高内聚,低耦合: 每个状态的逻辑封装在自己的类中,职责清晰。
    • 易于扩展: 添加新状态只需创建新的State子类,对现有代码影响小。
    • 可维护性好: 修改某个状态的行为只需修改对应的State类。
    • 遵循开闭原则: 对扩展开放,对修改关闭。
  • 缺点:
    • 实现相对枚举方式更复杂,需要定义更多的类。
    • 状态类之间可能需要共享数据,需要通过Context(EnemyController)传递或使用其他共享机制。

4.2.3 代码结构示例

using UnityEngine;

// 状态机控制器
public class EnemyController_Class : MonoBehaviour
{
    public State currentState;
    public PatrolState patrolState = new PatrolState();
    public ChaseState chaseState = new ChaseState();
    public AttackState attackState = new AttackState();

    // 共享数据 (或通过构造函数传入State)
    public Transform player;
    public float patrolSpeed = 2f;
    public float chaseSpeed = 5f;
    public float attackRange = 1.5f;
    public float detectionRange = 10f;
    public float loseRange = 15f;
    public Animator animator; // 可选,用于动画控制

    void Start()
    {
        // 初始化状态
        TransitionToState(patrolState);
    }

    void Update()
    {
        // 委托给当前状态执行
        if (currentState != null)
        {
            currentState.Execute(this);
        }
    }

    // 状态切换方法
    public void TransitionToState(State nextState)
    {
        if (currentState != null)
        {
            currentState.Exit(this); // 执行退出逻辑
        }
        currentState = nextState;
        currentState.Enter(this);   // 执行进入逻辑
    }
}

// --- 状态基类 ---
public abstract class State
{
    public abstract void Enter(EnemyController_Class enemy);
    public abstract void Execute(EnemyController_Class enemy);
    public abstract void Exit(EnemyController_Class enemy);
}

// --- 具体状态类 ---

public class PatrolState : State
{
    public override void Enter(EnemyController_Class enemy)
    {
        Debug.Log("Entering Patrol State");
        // enemy.animator?.SetBool("IsPatrolling", true);
        // 初始化巡逻路径等
    }

    public override void Execute(EnemyController_Class enemy)
    {
        // 执行巡逻逻辑
        // transform.Translate(...)
        Debug.Log("Patrolling...");

        // 检查转换条件
        if (enemy.player != null && Vector3.Distance(enemy.transform.position, enemy.player.position) < enemy.detectionRange)
        {
            enemy.TransitionToState(enemy.chaseState);
        }
    }

    public override void Exit(EnemyController_Class enemy)
    {
        Debug.Log("Exiting Patrol State");
        // enemy.animator?.SetBool("IsPatrolling", false);
    }
}

public class ChaseState : State
{
    public override void Enter(EnemyController_Class enemy)
    {
        Debug.Log("Entering Chase State");
        // enemy.animator?.SetBool("IsChasing", true);
    }

    public override void Execute(EnemyController_Class enemy)
    {
        // 执行追击逻辑
        if (enemy.player != null)
        {
            // Move towards player...
            Debug.Log("Chasing Player!");

            // 检查转换条件
            if (Vector3.Distance(enemy.transform.position, enemy.player.position) <= enemy.attackRange)
            {
                enemy.TransitionToState(enemy.attackState);
            }
            else if (Vector3.Distance(enemy.transform.position, enemy.player.position) > enemy.loseRange)
            {
                 enemy.TransitionToState(enemy.patrolState);
            }
        }
        else // 如果玩家对象消失
        {
            enemy.TransitionToState(enemy.patrolState);
        }
    }

    public override void Exit(EnemyController_Class enemy)
    {
         Debug.Log("Exiting Chase State");
         // enemy.animator?.SetBool("IsChasing", false);
    }
}

public class AttackState : State
{
     private float attackTimer = 0f;
     private float attackCooldown = 1f; // 攻击间隔

    public override void Enter(EnemyController_Class enemy)
    {
        Debug.Log("Entering Attack State");
        // enemy.animator?.SetTrigger("Attack");
        attackTimer = 0f; // 重置攻击计时器
    }

    public override void Execute(EnemyController_Class enemy)
    {
        // 执行攻击逻辑
        Debug.Log("Attacking...");
        // 可能需要面向玩家
        // enemy.transform.LookAt(enemy.player);

        attackTimer += Time.deltaTime;
        if (attackTimer >= attackCooldown)
        {
             Debug.Log("Perform Attack Action!");
             // enemy.animator?.SetTrigger("Attack"); // 再次触发攻击动画或逻辑
             attackTimer = 0f; // 重置计时器
        }


        // 检查转换条件
        if (enemy.player == null || Vector3.Distance(enemy.transform.position, enemy.player.position) > enemy.attackRange)
        {
            enemy.TransitionToState(enemy.chaseState);
        }
    }

    public override void Exit(EnemyController_Class enemy)
    {
        Debug.Log("Exiting Attack State");
    }
}

五、Animator状态机 vs 代码状态机

Unity自身提供了一个强大的可视化状态机系统——Animator Controller,它主要用于控制动画,但也可以用来管理游戏逻辑状态。那么,何时使用Animator状态机,何时使用我们前面讨论的代码状态机呢?

5.1 Unity Animator状态机

5.1.1 简介

Animator Controller是Unity内置的一个可视化工具,允许开发者通过拖拽和连接节点来创建状态机。节点代表动画片段(Animation Clip)或子状态机(Sub-State Machine),连线代表过渡(Transition),可以通过参数(Parameters)和条件(Conditions)来控制过渡的触发。

5.1.2 优点

  • 可视化: 状态和转换关系直观明了,易于设计和调试。
  • 动画集成: 与动画系统无缝集成,轻松实现状态切换与动画播放的同步。
  • 过渡效果: 支持动画混合(Blending)和平滑过渡。
  • 内置功能: 提供了如Any State、Layers、Behaviours(State Machine Behaviours)等高级功能,可以附加脚本到特定状态或过渡上。

5.1.3 缺点

  • 逻辑与动画耦合: 主要设计目标是动画控制,将复杂的非动画游戏逻辑放入其中可能导致状态机过于臃肿,或使逻辑依赖于动画结构。
  • 可视化复杂性: 对于非常复杂的状态机,图形界面可能变得难以管理和导航。
  • 代码交互相对繁琐: 通过代码控制Animator参数(SetFloat, SetBool, SetTrigger)来驱动状态转换,不如直接调用方法直观。
  • 不易进行单元测试: 依赖于Unity引擎环境,对纯逻辑状态机进行单元测试较困难。

5.2 代码状态机(基于类或枚举)

5.2.1 简介

指完全通过C#脚本实现的FSM,如前文所述的基于枚举或基于类的实现方式。

5.2.2 优点

  • 高度灵活性与控制力: 可以完全根据需求定制状态逻辑、转换条件和数据共享方式。
  • 逻辑与表现分离: 可以将核心AI逻辑与动画播放解耦(例如,状态机决定行为,然后通知动画系统播放相应动画)。
  • 易于集成: 更容易与其他纯代码系统(如寻路、技能系统)集成。
  • 可测试性强: 对于基于类的FSM,状态逻辑封装在独立类中,更容易进行单元测试。
  • 不受限于动画: 非常适合管理非动画相关的状态,如游戏流程状态(菜单、加载、游戏中、暂停)。

5.2.3 缺点

  • 缺乏可视化: 没有内置的图形界面,状态关系需要通过代码或文档来理解。
  • 需要手动实现: 状态转换、进入/退出逻辑等都需要自己编写代码实现。
  • 动画同步需额外处理: 如果需要根据状态播放动画,需要在状态逻辑中显式调用Animator的方法。

5.3 应用场景选择:何时使用哪种?

选择Animator状态机还是代码状态机,取决于具体需求:

  • 优先选择Animator状态机的情况:

    • 状态转换与角色动画播放紧密相关(如行走、跑步、跳跃、攻击动画切换)。
    • 需要利用Animator的动画混合、层级、IK等高级动画特性。
    • 团队中有美术或动画师需要参与状态机逻辑的调整。
  • 优先选择代码状态机的情况:

    • 管理复杂的游戏逻辑、AI行为,且这些行为不一定直接对应某个特定动画(或者一个行为涉及多个动画/无动画)。
    • 需要将状态逻辑与动画系统解耦。
    • 需要对状态机逻辑进行单元测试。
    • 管理游戏全局状态、UI流程等非角色行为的状态。
  • 混合使用:

    • 在实践中,两者经常结合使用。例如,用代码状态机管理高层AI决策(巡逻、追击),在每个状态内部,再通过代码控制Animator播放相应的动画(如在ChaseState中设置Animator的IsRunning参数为true)。

总结表格:

特性 Animator状态机 代码状态机 (基于类)
可视化 (内置图形界面) (依赖代码/文档)
动画集成 (无缝) 需要手动编码
灵活性 中等 (受限于Animator结构) (完全自定义)
逻辑耦合 较高 (与动画系统) (可与表现层分离)
可测试性 较弱 (依赖引擎) 较强 (纯C#逻辑)
实现复杂度 中等 (图形操作+少量代码) 较高 (纯代码实现)
适用场景 动画驱动的状态、美术参与度高 复杂AI逻辑、非动画状态、需解耦

六、实践:实现敌人AI状态机

现在,让我们结合所学,使用基于类的状态模式为敌人AI实现一个简单的状态机,包含巡逻(Patrol)、**追击(Chase)攻击(Attack)**三种状态。我们将使用上一节的EnemyController_Class结构。

6.1 需求定义

  • Patrol State: 敌人在预设的路径点之间来回移动。如果探测到玩家进入视野范围,切换到Chase状态。
  • Chase State: 敌人快速朝玩家位置移动。如果进入攻击范围,切换到Attack状态。如果丢失玩家(玩家移出丢失范围),切换回Patrol状态。
  • Attack State: 敌人停止移动,执行攻击动作(此处简化为打印日志和设置冷却)。如果玩家移出攻击范围,切换回Chase状态。

6.2 状态设计

我们已经有了EnemyController_ClassState基类以及PatrolStateChaseStateAttackState三个具体状态类的框架。接下来需要填充它们的具体逻辑。

(注意:以下代码是基于4.2.3节示例的进一步完善,假设EnemyController_Class已挂载到敌人游戏对象上,并已在Inspector中设置好player引用及相关参数。)

6.3 实现巡逻状态 (PatrolState)

using UnityEngine;

public class PatrolState : State
{
    private int currentWaypointIndex = 0;
    public Transform[] waypoints; // 在EnemyController中定义并赋值

    public override void Enter(EnemyController_Class enemy)
    {
        Debug.Log("Entering Patrol State");
        enemy.GetComponent<UnityEngine.AI.NavMeshAgent>().speed = enemy.patrolSpeed; // (可选) 使用NavMeshAgent
        // 如果需要路径点, 在 EnemyController 中定义 waypoints 数组
        // waypoints = enemy.patrolWaypoints;
        // GoToNextWaypoint(enemy);
    }

    public override void Execute(EnemyController_Class enemy)
    {
        // 简单的巡逻逻辑: 假设在路点间移动
        // if (waypoints != null && waypoints.Length > 0)
        // {
        //     if (Vector3.Distance(enemy.transform.position, waypoints[currentWaypointIndex].position) < 1.0f)
        //     {
        //         GoToNextWaypoint(enemy);
        //     }
        // } else {
             Debug.Log("Patrolling... (No waypoints defined, implement movement logic)");
        // }


        // 检查是否探测到玩家
        Collider[] hits = Physics.OverlapSphere(enemy.transform.position, enemy.detectionRange, LayerMask.GetMask("Player")); // 假设玩家在Player层
        if (hits.Length > 0)
        {
             enemy.player = hits[0].transform; // 获取玩家引用
             Debug.Log("Player detected!");
             enemy.TransitionToState(enemy.chaseState);
        }
    }

    // private void GoToNextWaypoint(EnemyController_Class enemy)
    // {
    //     if (waypoints.Length == 0) return;
    //     enemy.GetComponent().destination = waypoints[currentWaypointIndex].position;
    //     currentWaypointIndex = (currentWaypointIndex + 1) % waypoints.Length;
    // }

    public override void Exit(EnemyController_Class enemy)
    {
        Debug.Log("Exiting Patrol State");
        // enemy.GetComponent().ResetPath(); // 停止导航
    }
}
  • 注意: 上述代码需要配合UnityEngine.AI.NavMeshAgent组件或自定义移动逻辑。waypoints需要在EnemyController_Class中定义并赋值。探测玩家使用了Physics.OverlapSphere,请确保玩家对象有正确的Layer。

6.4 实现追击状态 (ChaseState)

using UnityEngine;

public class ChaseState : State
{
    public override void Enter(EnemyController_Class enemy)
    {
        Debug.Log("Entering Chase State");
        enemy.GetComponent<UnityEngine.AI.NavMeshAgent>().speed = enemy.chaseSpeed;
        // enemy.animator?.SetBool("IsChasing", true);
    }

    public override void Execute(EnemyController_Class enemy)
    {
        if (enemy.player != null)
        {
            // 使用NavMeshAgent追击玩家
            enemy.GetComponent<UnityEngine.AI.NavMeshAgent>().destination = enemy.player.position;
            Debug.Log("Chasing Player!");

            float distanceToPlayer = Vector3.Distance(enemy.transform.position, enemy.player.position);

            // 检查是否进入攻击范围
            if (distanceToPlayer <= enemy.attackRange)
            {
                 Debug.Log("Reached attack range!");
                 enemy.TransitionToState(enemy.attackState);
            }
            // 检查是否丢失玩家
            else if (distanceToPlayer > enemy.loseRange)
            {
                 Debug.Log("Player lost!");
                 enemy.player = null; // 清除玩家引用
                 enemy.TransitionToState(enemy.patrolState);
            }
            // 可选: 添加视线检查 Physics.Linecast
        }
        else // 如果意外丢失player引用
        {
             Debug.Log("Player reference lost unexpectedly!");
             enemy.TransitionToState(enemy.patrolState);
        }
    }

    public override void Exit(EnemyController_Class enemy)
    {
        Debug.Log("Exiting Chase State");
        // enemy.GetComponent().ResetPath(); // 可选,看Attack状态是否需要移动
        // enemy.animator?.SetBool("IsChasing", false);
    }
}

6.5 实现攻击状态 (AttackState)

using UnityEngine;

public class AttackState : State
{
    private float attackTimer = 0f;
    private float attackCooldown = 1.5f; // 攻击间隔

    public override void Enter(EnemyController_Class enemy)
    {
        Debug.Log("Entering Attack State");
        // 停止移动 (如果使用NavMeshAgent)
        enemy.GetComponent<UnityEngine.AI.NavMeshAgent>().isStopped = true;
        // enemy.GetComponent().velocity = Vector3.zero; // 确保速度归零
        // enemy.transform.LookAt(enemy.player); // 面向玩家
        // enemy.animator?.SetTrigger("Attack"); // 触发一次攻击动画
        attackTimer = attackCooldown; // 进入时即可攻击一次或等待冷却
    }

    public override void Execute(EnemyController_Class enemy)
    {
        if (enemy.player != null)
        {
            // 保持面向玩家
            enemy.transform.LookAt(enemy.player.position);

             attackTimer += Time.deltaTime;
             if (attackTimer >= attackCooldown)
             {
                 PerformAttack(enemy);
                 attackTimer = 0f; // 重置计时器
             }

            // 检查玩家是否移出攻击范围
            if (Vector3.Distance(enemy.transform.position, enemy.player.position) > enemy.attackRange)
            {
                Debug.Log("Player moved out of attack range!");
                enemy.TransitionToState(enemy.chaseState);
            }
        }
        else // 玩家消失
        {
            Debug.Log("Target player lost during attack!");
            enemy.TransitionToState(enemy.patrolState); // 或 Chase? 取决于设计
        }
    }

    private void PerformAttack(EnemyController_Class enemy)
    {
        Debug.Log($"Enemy attacks Player! Timestamp: {Time.time}");
        // 在这里实现实际的伤害逻辑, 比如调用玩家身上的受击方法
        // enemy.player.GetComponent()?.TakeDamage(10);
        // enemy.animator?.SetTrigger("Attack"); // 再次触发攻击动画
    }

    public override void Exit(EnemyController_Class enemy)
    {
        Debug.Log("Exiting Attack State");
        // 恢复移动 (如果使用NavMeshAgent)
        enemy.GetComponent<UnityEngine.AI.NavMeshAgent>().isStopped = false;
        // enemy.animator?.ResetTrigger("Attack"); // 重置攻击触发器
    }
}

6.6 状态切换逻辑

状态切换的逻辑已经包含在每个ConcreteStateExecute方法中。当满足特定条件时,它们会调用enemy.TransitionToState()方法来改变EnemyController_Class中的currentStateTransitionToState方法负责调用旧状态的Exit和新状态的Enter

6.7 完整设置与运行

  1. 创建一个敌人游戏对象。
  2. 为其添加NavMeshAgent组件(如果使用导航网格)。
  3. EnemyController_Class脚本添加到敌人对象。
  4. EnemyController_Class的Inspector面板中:
    • 拖拽玩家对象(或包含玩家的Prefab)到Player字段(可以在运行时动态查找)。
    • 设置好Patrol Speed, Chase Speed, Attack Range, Detection Range, Lose Range等参数。
    • (如果使用路点巡逻)创建空对象作为路点,并将它们拖拽到EnemyController_Class中一个public Transform[] waypoints;字段(需要在EnemyController_ClassPatrolState中添加此字段并传递)。
    • (如果使用动画)将Animator组件添加到敌人对象,创建好对应的动画状态和参数,并在EnemyController_Class中获取Animator引用,在各状态的Enter/Exit/Execute中调用animator.Set...方法。
  5. 确保场景中有NavMesh(如果使用NavMeshAgent)。
  6. 确保玩家对象有"Player" Layer(如果探测代码中指定了)。
  7. 运行游戏,观察敌人AI的行为和控制台输出的日志。

这个实践环节展示了如何使用基于类的状态模式构建一个模块化、可扩展的敌人AI。你可以基于此框架轻松添加更多状态(如Flee、Stunned)或修改现有状态的行为。

七、总结

今天,我们深入探讨了在Unity C#开发中管理复杂对象行为的利器——状态模式有限状态机(FSM)。核心要点回顾:

  1. 问题的提出: 传统if-elseswitch管理多状态行为会导致代码难以维护和扩展(意大利面条代码)。
  2. 状态模式: 通过将每个状态的行为封装到独立的类中,并将对象的行为委托给当前状态对象,实现了行为与状态的解耦。主要组成部分为Context、State接口/基类、ConcreteState类。
  3. 有限状态机(FSM): 状态模式的一种常用实现模型,包含状态(States)、**事件/触发器(Events/Triggers)转换(Transitions)**三个核心要素。状态图是其有力的可视化工具。
  4. Unity中的实现:
    • 基于枚举: 简单快捷,适合状态少、逻辑简单的场景,但扩展性差。
    • 基于类(状态模式): 更符合OOP,结构清晰,易于扩展和维护,适合复杂状态逻辑,但实现稍复杂。包含Enter, Execute, Exit生命周期方法是常见实践。
  5. Animator状态机 vs 代码状态机:
    • Animator: 优点在于可视化、与动画系统深度集成;缺点是逻辑与动画耦合,复杂逻辑管理不便。
    • 代码FSM: 优点在于灵活性高、逻辑与表现分离、易于测试;缺点是缺乏可视化、需要手动实现。
    • 选择取决于应用场景,通常与动画紧密相关的用Animator,复杂AI或非动画逻辑用代码FSM,两者也可结合使用。
  6. 实践应用: 我们通过一个敌人AI(巡逻、追击、攻击)的实例,演示了如何使用基于类的状态模式(代码FSM)构建结构清晰、易于管理的行为逻辑。

掌握状态模式与FSM,能显著提升你构建复杂游戏系统(尤其是AI、角色控制器、游戏流程管理)的能力,编写出更健壮、更易于维护和扩展的代码。希望今天的学习能为你打下坚实的基础!


你可能感兴趣的:(C#编程从入门到进阶,unity,c#,状态模式,AI,游戏引擎,游戏开发,c语言)