# 观前提醒
项目会在 GitHub 中开源,链接:https://github.com/Maikire/Unity/tree/main/UnityFramework/FSM
# 有限状态机(Finite State Machine)
有限状态机是一种用来模拟系统行为的数学模型,通常被用于处理离散事件的控制问题。它由一组有限状态、转移函数和输入输出等组件构成,一个有限状态机在任一时刻只能处于一种状态。有限状态机能够对角色的行为进行抽象和编排,使得游戏角色能够根据当前状态自动切换到相应的行为,并且可以通过规定的条件进行状态的转换。这样一来,角色在游戏中的表现更加智能、灵活,同时也增强了游戏的可玩性。
可以将它理解为一个流程控制系统。
# 状态机设计
有限状态机的实现方式有很多种,这主要取决于具体的需求是什么,本文给出一个例子作为参考。
# 需求分析
需求:实现游戏中敌人(NPC)的基本功能,具体功能请看下面的配置表
# 设计
- 根据配置表可以得到三个主要的类
- 条件类(抽象类):逻辑的处理,判断条件是否达成,比如:生命值是否为 0、是否发现目标......
- 状态类(抽象类):储存条件到状态的映射,处理进入状态、在状态中、退出状态的情况
- 状态机(脚本):储存所有的状态,切换状态
- 为了方便储存映射,条件类和状态类需要有 ID 做标识
- 状态机有很多数据,可以将一部分数据封装起来
- 根据配置表配置状态机
# 代码
# 数据类
条件的 ID
namespace AI.FSM | |
{ | |
/// <summary> | |
/// TriggerID | |
/// </summary> | |
public enum FSMTriggerID | |
{ | |
/// <summary> | |
/// 没有生命 | |
/// </summary> | |
NoHealth, | |
/// <summary> | |
/// 发现目标 | |
/// </summary> | |
FoundTarget, | |
/// <summary> | |
/// 到达目标 | |
/// </summary> | |
ReachTarget, | |
/// <summary> | |
/// 击杀目标 | |
/// </summary> | |
KilledTarget, | |
/// <summary> | |
/// 超出攻击范围 | |
/// </summary> | |
OutOfAttackRange, | |
/// <summary> | |
/// 丢失目标 | |
/// </summary> | |
LostTarget, | |
/// <summary> | |
/// 触发巡逻状态 | |
/// </summary> | |
Patrol, | |
/// <summary> | |
/// 完成巡逻 | |
/// </summary> | |
CompletePatrol, | |
/// <summary> | |
/// 复活 | |
/// </summary> | |
Revive, | |
} | |
} |
状态的 ID
using UnityEngine; | |
namespace AI.FSM | |
{ | |
/// <summary> | |
/// StateID | |
/// </summary> | |
public enum FSMStateID | |
{ | |
[Tooltip("默认")] | |
Default, | |
[Tooltip("死亡")] | |
Dead, | |
[Tooltip("闲置")] | |
Idle, | |
[Tooltip("追逐")] | |
Pursuit, | |
[Tooltip("攻击")] | |
Attack, | |
[Tooltip("巡逻")] | |
Patrol, | |
} | |
} |
状态机脚本的数据类
using System; | |
using UnityEngine; | |
namespace AI.FSM | |
{ | |
[Serializable] | |
/// <summary> | |
/// 有限状态机数据类 | |
/// </summary> | |
public class FSMData | |
{ | |
[Tooltip("状态配置表(在StreamingAssets中的路径)")] | |
public string FSMConfigPath; | |
[Tooltip("移动速度")] | |
public float MoveSpeed; | |
[Tooltip("视野距离")] | |
public float ViewDistance; | |
[Tooltip("视野角度")] | |
public float ViewAngle; | |
[Tooltip("可以被发现的目标的标签")] | |
public string[] TargetTags = { "Player" }; | |
[Tooltip("进入攻击状态的距离")] | |
public float AttackStateDistance; | |
[Tooltip("攻击时间间隔")] | |
public float AttackTimeInterval; | |
[Tooltip("攻击状态下释放技能的距离的偏移量")] | |
public float SkillDistanceEffect; | |
[Tooltip("触发巡逻的概率,每秒判定一次,取值范围:0-1")] | |
public float PatrolProbability; | |
[Tooltip("判断是否到达路点的偏移量")] | |
public float PatrolEffect; | |
[Tooltip("寻路模式")] | |
public FSMPatrolModes PatrolMode; | |
[Tooltip("巡逻的路点")] | |
public Transform[] WayPoints; | |
[HideInInspector] | |
[Tooltip("完成巡逻")] | |
public bool CompletePatrol; | |
} | |
} |
自动寻路的模式
using System; | |
using UnityEngine; | |
namespace AI.FSM | |
{ | |
[Serializable] | |
/// <summary> | |
/// 寻路模式 | |
/// </summary> | |
public enum FSMPatrolModes | |
{ | |
[Tooltip("无(在巡逻状态下,触发 完成巡逻)")] | |
None, | |
[Tooltip("单次")] | |
Once, | |
[Tooltip("循环")] | |
Loop, | |
[Tooltip("往返")] | |
RoundTrip, | |
} | |
} |
# 条件类
- ID
- 提供用于判断是否满足条件的方法
实现类必须给 ID 赋值
namespace AI.FSM | |
{ | |
/// <summary> | |
/// FSM 条件类 | |
/// </summary> | |
public abstract class FSMTrigger | |
{ | |
/// <summary> | |
/// TriggerID | |
/// 子类必须给 TriggerID 赋值 | |
/// </summary> | |
public abstract FSMTriggerID TriggerID { get; } | |
public FSMTrigger() | |
{ | |
Init(); | |
} | |
/// <summary> | |
/// 初始化 | |
/// </summary> | |
public virtual void Init() { } | |
/// <summary> | |
/// 是否满足条件 | |
/// </summary> | |
/// <returns></returns> | |
public abstract bool HandleTrigger(FSMBase fsm); | |
} | |
} |
# 状态类
- ID
- 储存某状态拥有的所有的条件
- 储存条件到状态的映射表
- 提供切换状态的方法
- 提供进入状态、在状态中、退出状态的方法
- 添加条件对象的时候会用到 创建工厂(FSMCreateFactory),这个会在后面讲到
代码如下
using System.Collections.Generic; | |
namespace AI.FSM | |
{ | |
/// <summary> | |
/// FSM 状态类 | |
/// </summary> | |
public abstract class FSMState | |
{ | |
/// <summary> | |
/// StateID | |
/// 子类必须给 StateID 赋值 | |
/// </summary> | |
public abstract FSMStateID StateID { get; } | |
/// <summary> | |
///true: 搜索目标的时候忽略墙体 | |
/// </summary> | |
public virtual bool IgnoreWalls { get => false; } | |
/// <summary> | |
/// 条件列表 | |
/// </summary> | |
private List<FSMTrigger> Triggers; | |
/// <summary> | |
/// 映射表:条件 —— 目标状态 | |
/// </summary> | |
private Dictionary<FSMTriggerID, FSMStateID> Map; | |
public FSMState() | |
{ | |
Map = new Dictionary<FSMTriggerID, FSMStateID>(); | |
Triggers = new List<FSMTrigger>(); | |
Init(); | |
} | |
/// <summary> | |
/// 初始化 | |
/// </summary> | |
public virtual void Init() { } | |
/// <summary> | |
/// 添加映射、添加 FSMTrigger 对象 | |
/// 由状态机调用 | |
/// </summary> | |
/// <param name="triggerID"></param> | |
/// <param name="stateID"></param> | |
public void Add(FSMTriggerID triggerID, FSMStateID stateID) | |
{ | |
Map.Add(triggerID, stateID); | |
Triggers.Add(FSMCreateFactory.CreateTrigger(triggerID)); | |
} | |
/// <summary> | |
/// 判断当前状态的条件是否满足 | |
/// 如果条件满足,则切换状态 | |
/// </summary> | |
public void JudgeState(FSMBase fsm) | |
{ | |
foreach (FSMTrigger trigger in Triggers) | |
{ | |
if (trigger.HandleTrigger(fsm)) | |
{ | |
fsm.ChangeActiveState(Map[trigger.TriggerID]); | |
return; | |
} | |
} | |
} | |
/// <summary> | |
/// 进入状态 | |
/// </summary> | |
/// <param name="fsm"> 状态机 & lt;/param> | |
public virtual void EnterState(FSMBase fsm) { } | |
/// <summary> | |
/// 在状态中 | |
/// </summary> | |
/// <param name="fsm"> 状态机 & lt;/param> | |
public virtual void ActionState(FSMBase fsm) { } | |
/// <summary> | |
/// 退出状态 | |
/// </summary> | |
/// <param name="fsm"> 状态机 & lt;/param> | |
public virtual void ExitState(FSMBase fsm) { } | |
} | |
} |
# 状态机脚本
- 状态机会用到角色系统、技能系统、动画系统,本文借用这个角色框架和这个技能框架为例
- 储存所有的状态
- 切换状态
- 为状态和条件提供成员
- 利用变换组件助手类持续搜索目标
- 为状态提供基础的方法,例如:自动寻路
- 利用创建工厂(FSMCreateFactory)和配置文件读取工厂(FSMConfigReaderFactory)处理配置表,这两个会在后面讲到
代码如下
using ARPGDemo.Character; | |
using ARPGDemo.Skill; | |
using Common; | |
using System; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.AI; | |
namespace AI.FSM | |
{ | |
[RequireComponent(typeof(NavMeshAgent), typeof(CharacterStatus), typeof(CharacterSkillSystemNPC))] | |
/// <summary> | |
/// 状态机 | |
/// </summary> | |
public class FSMBase : MonoBehaviour | |
{ | |
[Tooltip("默认状态")] | |
public FSMStateID DefaultStateID = FSMStateID.Idle; | |
[Tooltip("有限状态机数据")] | |
public FSMData Data; | |
/// <summary> | |
/// 状态表 | |
/// </summary> | |
private List<FSMState> States; | |
/// <summary> | |
/// 当前状态 | |
/// </summary> | |
private FSMState CurrentState; | |
/// <summary> | |
/// 导航 | |
/// </summary> | |
private NavMeshAgent Navigation; | |
#region 为状态和条件提供的成员 | |
[HideInInspector] | |
[Tooltip("角色的信息")] | |
public CharacterStatus Character; | |
[HideInInspector] | |
[Tooltip("角色的动画")] | |
public Animator Anim; | |
[HideInInspector] | |
[Tooltip("发现的目标")] | |
public Transform FoundTarget; | |
[HideInInspector] | |
[Tooltip("技能系统")] | |
public CharacterSkillSystemNPC NPCSkillSystem; | |
[HideInInspector] | |
[Tooltip("当前的技能")] | |
public SkillData CurrentSkill; | |
#endregion | |
private void Awake() | |
{ | |
Character = this.GetComponent<CharacterStatus>(); | |
Anim = this.GetComponentInChildren<Animator>(); | |
Navigation = GetComponent<NavMeshAgent>(); | |
NPCSkillSystem = this.GetComponent<CharacterSkillSystemNPC>(); | |
} | |
private void Start() | |
{ | |
SetParameters(); | |
ConfigFSM(); | |
InitDefaultState(); | |
} | |
private void Update() | |
{ | |
SearchTarget(); // 搜索目标 | |
CurrentState.JudgeState(this); // 判断当前状态的条件 | |
CurrentState.ActionState(this); // 执行当前状态的逻辑 | |
} | |
/// <summary> | |
/// 设置参数 | |
/// </summary> | |
private void SetParameters() | |
{ | |
FoundTarget = null; | |
CurrentSkill = null; | |
} | |
/// <summary> | |
/// 配置状态机 | |
/// </summary> | |
private void ConfigFSM() | |
{ | |
States = new List<FSMState>(); | |
var map = FSMConfigReaderFactory.GetConfig(Data.FSMConfigPath); | |
foreach (var state in map) | |
{ | |
FSMState tempState = FSMCreateFactory.CreateState((FSMStateID)Enum.Parse(typeof(FSMStateID), state.Key)); | |
States.Add(tempState); | |
foreach (var item in state.Value) | |
{ | |
FSMTriggerID tempTriggerID = (FSMTriggerID)Enum.Parse(typeof(FSMTriggerID), item.Key); | |
FSMStateID tempStateID = (FSMStateID)Enum.Parse(typeof(FSMStateID), item.Value); | |
tempState.Add(tempTriggerID, tempStateID); | |
} | |
} | |
} | |
/// <summary> | |
/// 初始化默认状态 | |
/// </summary> | |
private void InitDefaultState() | |
{ | |
CurrentState = States.Find(s => s.StateID == DefaultStateID); | |
CurrentState.EnterState(this); | |
} | |
/// <summary> | |
/// 改变状态 | |
/// </summary> | |
/// <param name="stateID"></param> | |
public void ChangeActiveState(FSMStateID stateID) | |
{ | |
CurrentState.ExitState(this); | |
if (stateID == FSMStateID.Default) | |
{ | |
CurrentState = States.Find(s => s.StateID == DefaultStateID); | |
} | |
else | |
{ | |
CurrentState = States.Find(s => s.StateID == stateID); | |
} | |
CurrentState.EnterState(this); | |
} | |
/// <summary> | |
/// 搜索目标 | |
/// </summary> | |
public void SearchTarget() | |
{ | |
// 查找目标 | |
List<Transform> targets = this.transform.SelectTargets(Data.ViewDistance, Data.ViewAngle, Data.TargetTags); | |
// 选取活动的目标 | |
targets = targets.FindAll(t => t.GetComponent<CharacterStatus>().HP > 0); | |
if (targets.Count == 0) | |
{ | |
FoundTarget = null; | |
return; | |
} | |
// 是否忽略墙体 | |
if (CurrentState.IgnoreWalls) | |
{ | |
FoundTarget = targets[0]; | |
return; | |
} | |
// 利用射线判断是否有墙体 | |
RaycastHit hit; | |
Physics.Raycast(this.transform.position, targets[0].position + Vector3.up * 0.5f - this.transform.position, out hit, Data.ViewDistance); | |
if (hit.transform != null && hit.transform.name == targets[0].name) | |
{ | |
FoundTarget = targets[0]; | |
} | |
else | |
{ | |
FoundTarget = null; | |
} | |
} | |
/// <summary> | |
/// 自动寻路 | |
/// </summary> | |
/// <param name="targetPosition"> 目标位置 & lt;/param> | |
/// <param name="stoppingDistance"> 停止移动的距离 & lt;/param> | |
/// <param name="speed"> 移动速度 & lt;/param> | |
public void MoveToTarget(Vector3 targetPosition, float stoppingDistance, float speed) | |
{ | |
Navigation.stoppingDistance = stoppingDistance; | |
Navigation.speed = speed; | |
Navigation.SetDestination(targetPosition); | |
} | |
} | |
} |
# 读取配置表
- 利用配置文件读取器处理配置表
- 储存配置表的内容,一个配置表只需要解析一次
FSM 配置文件读取器
using Common; | |
using System.Collections.Generic; | |
namespace AI.FSM | |
{ | |
/// <summary> | |
/// FSM 配置文件读取器 | |
/// </summary> | |
public class FSMConfigReader | |
{ | |
/// <summary> | |
/// 大字典:状态 -- 映射 | |
/// 小字典:条件编号 -- 状态编号 | |
/// </summary> | |
public Dictionary<string, Dictionary<string, string>> Map | |
{ | |
get; | |
private set; | |
} | |
/// <summary> | |
/// 大字典的键值 | |
/// </summary> | |
private string MainKey; | |
public FSMConfigReader(string path) | |
{ | |
Map = new Dictionary<string, Dictionary<string, string>>(); | |
string configfile = ConfigReader.GetConfigFile(path); | |
ConfigReader.ReadConfigFile(configfile, BulidLine); | |
} | |
/// <summary> | |
/// 读取一行 | |
/// </summary> | |
/// <param name="line"></param> | |
private void BulidLine(string line) | |
{ | |
// 去除空白 | |
line = line.Trim(); | |
// 判断字符串是否为空,有两种方法 | |
//if (line == "" || line == null) return; | |
if (string.IsNullOrEmpty(line)) return; | |
if (line.StartsWith("[")) | |
{ | |
MainKey = line.Substring(1, line.Length - 2); | |
Map.Add(MainKey, new Dictionary<string, string>()); | |
} | |
else | |
{ | |
string[] values = line.Split("->"); | |
Map[MainKey].Add(values[0], values[1]); | |
} | |
} | |
} | |
} |
FSM 配置文件读取工厂
using System.Collections.Generic; | |
namespace AI.FSM | |
{ | |
/// <summary> | |
/// FSM 配置文件读取工厂 | |
/// </summary> | |
public class FSMConfigReaderFactory | |
{ | |
/// <summary> | |
/// 储存已生成的对象,循环利用 | |
/// </summary> | |
private static Dictionary<string, Dictionary<string, Dictionary<string, string>>> Cache = new Dictionary<string, Dictionary<string, Dictionary<string, string>>>(); | |
/// <summary> | |
/// 获取配置表 | |
/// </summary> | |
/// <param name="path"> 路径 & lt;/param> | |
/// <returns></returns> | |
public static Dictionary<string, Dictionary<string, string>> GetConfig(string path) | |
{ | |
if (Cache.ContainsKey(path)) | |
{ | |
return Cache[path]; | |
} | |
else | |
{ | |
FSMConfigReader configReader = new FSMConfigReader(path); | |
Cache.Add(path, configReader.Map); | |
return configReader.Map; | |
} | |
} | |
} | |
} |
# 创建工厂
- 根据指定的命名规则,使用反射创建对象
- 条件类的对象只涉及各种情况的触发判定,所以可以循环利用
代码如下
using System; | |
using System.Collections.Generic; | |
namespace AI.FSM | |
{ | |
/// <summary> | |
/// FSM 创建工厂 | |
/// 创建 FSMTrigger 和 FSMState 对象 | |
/// </summary> | |
public class FSMCreateFactory | |
{ | |
/// <summary> | |
/// 储存已生成的 FSMTrigger 对象,循环利用 | |
/// </summary> | |
private static Dictionary<string, FSMTrigger> Cache = new Dictionary<string, FSMTrigger>(); | |
/// <summary> | |
/// 创建 FSMTrigger 对象 | |
/// </summary> | |
/// <param name="triggerID">ID</param> | |
/// <returns></returns> | |
public static FSMTrigger CreateTrigger(FSMTriggerID triggerID) | |
{ | |
// 命名规则:AI.FSM. + triggerID + Trigger | |
string className = String.Format("AI.FSM.{0}Trigger", triggerID); | |
if (Cache.ContainsKey(className)) | |
{ | |
return Cache[className]; | |
} | |
else | |
{ | |
Type type = Type.GetType(className); | |
FSMTrigger temp = Activator.CreateInstance(type) as FSMTrigger; | |
Cache.Add(className, temp); | |
return temp; | |
} | |
} | |
/// <summary> | |
/// 创建 FSMState 对象 | |
/// </summary> | |
/// <param name="stateID">ID</param> | |
/// <returns></returns> | |
public static FSMState CreateState(FSMStateID stateID) | |
{ | |
// 命名规则:AI.FSM. + stateID + State | |
Type type = Type.GetType(String.Format("AI.FSM.{0}State", stateID)); | |
object temp = Activator.CreateInstance(type); | |
return temp as FSMState; | |
} | |
} | |
} |
# 具体的条件类
- 继承抽象类,处理对应的逻辑
完成巡逻
namespace AI.FSM | |
{ | |
/// <summary> | |
/// 完成巡逻 | |
/// </summary> | |
public class CompletePatrolTrigger : FSMTrigger | |
{ | |
public override FSMTriggerID TriggerID => FSMTriggerID.CompletePatrol; | |
public override bool HandleTrigger(FSMBase fsm) | |
{ | |
return fsm.Data.CompletePatrol; | |
} | |
} | |
} |
发现目标
namespace AI.FSM | |
{ | |
/// <summary> | |
/// 发现目标 | |
/// </summary> | |
public class FoundTargetTrigger : FSMTrigger | |
{ | |
public override FSMTriggerID TriggerID => FSMTriggerID.FoundTarget; | |
public override bool HandleTrigger(FSMBase fsm) | |
{ | |
return fsm.FoundTarget != null; | |
} | |
} | |
} |
击杀目标
using ARPGDemo.Character; | |
namespace AI.FSM | |
{ | |
/// <summary> | |
/// 击杀目标 | |
/// </summary> | |
public class KilledTargetTrigger : FSMTrigger | |
{ | |
public override FSMTriggerID TriggerID => FSMTriggerID.KilledTarget; | |
public override bool HandleTrigger(FSMBase fsm) | |
{ | |
return fsm.FoundTarget.GetComponent<CharacterStatus>().HP <= 0; | |
} | |
} | |
} |
丢失目标
namespace AI.FSM | |
{ | |
/// <summary> | |
/// 丢失目标 | |
/// </summary> | |
public class LostTargetTrigger : FSMTrigger | |
{ | |
public override FSMTriggerID TriggerID => FSMTriggerID.LostTarget; | |
public override bool HandleTrigger(FSMBase fsm) | |
{ | |
return fsm.FoundTarget == null; | |
} | |
} | |
} |
没有生命
namespace AI.FSM | |
{ | |
/// <summary> | |
/// 没有生命 | |
/// </summary> | |
public class NoHealthTrigger : FSMTrigger | |
{ | |
public override FSMTriggerID TriggerID => FSMTriggerID.NoHealth; | |
public override bool HandleTrigger(FSMBase fsm) | |
{ | |
return fsm.Character.HP <= 0; | |
} | |
} | |
} |
巡逻
using UnityEngine; | |
namespace AI.FSM | |
{ | |
/// <summary> | |
/// 巡逻 | |
/// </summary> | |
public class PatrolTrigger : FSMTrigger | |
{ | |
/// <summary> | |
/// 计时器 | |
/// </summary> | |
private float Timer; | |
public override FSMTriggerID TriggerID => FSMTriggerID.Patrol; | |
public override void Init() | |
{ | |
Timer = 0; | |
} | |
public override bool HandleTrigger(FSMBase fsm) | |
{ | |
if (fsm.Data.PatrolMode == FSMPatrolModes.None) | |
{ | |
return false; | |
} | |
Timer += Time.deltaTime; | |
if (Timer >= 1) | |
{ | |
if (Random.Range(0, 1f) <= fsm.Data.PatrolProbability) | |
{ | |
Timer = 0; | |
return true; | |
} | |
Timer = 0; | |
} | |
return false; | |
} | |
} | |
} |
到达目标
using UnityEngine; | |
namespace AI.FSM | |
{ | |
/// <summary> | |
/// 到达目标 | |
/// </summary> | |
public class ReachTargetTrigger : FSMTrigger | |
{ | |
public override FSMTriggerID TriggerID => FSMTriggerID.ReachTarget; | |
public override bool HandleTrigger(FSMBase fsm) | |
{ | |
return Vector3.Distance(fsm.transform.position, fsm.FoundTarget.position) <= fsm.Data.AttackStateDistance; | |
} | |
} | |
} |
复活
namespace AI.FSM | |
{ | |
/// <summary> | |
/// 复活 | |
/// </summary> | |
public class ReviveTrigger : FSMTrigger | |
{ | |
public override FSMTriggerID TriggerID => FSMTriggerID.Revive; | |
public override bool HandleTrigger(FSMBase fsm) | |
{ | |
return fsm.Character.HP > 0; | |
} | |
} | |
} |
# 具体的状态类
- 继承抽象类,处理对应的逻辑
闲置状态
namespace AI.FSM | |
{ | |
/// <summary> | |
/// 闲置状态 | |
/// </summary> | |
public class IdleState : FSMState | |
{ | |
public override FSMStateID StateID => FSMStateID.Idle; | |
public override bool IgnoreWalls => false; | |
public override void EnterState(FSMBase fsm) | |
{ | |
base.EnterState(fsm); | |
fsm.Anim.SetBool(fsm.Character.PlayerAnimationParameter.Revive, true); | |
fsm.Anim.SetBool(fsm.Character.PlayerAnimationParameter.Idle, true); | |
} | |
} | |
} |
死亡状态
namespace AI.FSM | |
{ | |
/// <summary> | |
/// 死亡状态 | |
/// </summary> | |
public class DeadState : FSMState | |
{ | |
public override FSMStateID StateID => FSMStateID.Dead; | |
public override bool IgnoreWalls => false; | |
} | |
} |
追逐状态
namespace AI.FSM | |
{ | |
/// <summary> | |
/// 追逐状态 | |
/// </summary> | |
public class PursuitState : FSMState | |
{ | |
public override FSMStateID StateID => FSMStateID.Pursuit; | |
public override bool IgnoreWalls => true; | |
public override void EnterState(FSMBase fsm) | |
{ | |
base.EnterState(fsm); | |
fsm.Anim.SetBool(fsm.Character.PlayerAnimationParameter.Run, true); | |
} | |
public override void ActionState(FSMBase fsm) | |
{ | |
base.ActionState(fsm); | |
fsm.MoveToTarget(fsm.FoundTarget.position, fsm.Data.AttackStateDistance, fsm.Data.MoveSpeed); | |
} | |
public override void ExitState(FSMBase fsm) | |
{ | |
base.ExitState(fsm); | |
fsm.MoveToTarget(fsm.transform.position, 1, fsm.Data.MoveSpeed); | |
} | |
} | |
} |
巡逻状态
using System; | |
using UnityEngine; | |
namespace AI.FSM | |
{ | |
/// <summary> | |
/// 巡逻状态 | |
/// </summary> | |
public class PatrolState : FSMState | |
{ | |
public override FSMStateID StateID => FSMStateID.Patrol; | |
public override bool IgnoreWalls => false; | |
/// <summary> | |
/// 路点数组的索引 | |
/// </summary> | |
private int Index; | |
public override void Init() | |
{ | |
Index = 0; | |
} | |
public override void EnterState(FSMBase fsm) | |
{ | |
base.EnterState(fsm); | |
fsm.Data.CompletePatrol = false; | |
} | |
public override void ActionState(FSMBase fsm) | |
{ | |
base.ActionState(fsm); | |
switch (fsm.Data.PatrolMode) | |
{ | |
case FSMPatrolModes.None: | |
NoneMode(fsm); | |
break; | |
case FSMPatrolModes.Once: | |
OnceMode(fsm); | |
break; | |
case FSMPatrolModes.Loop: | |
LoopMode(fsm); | |
break; | |
case FSMPatrolModes.RoundTrip: | |
RoundTripMode(fsm); | |
break; | |
} | |
} | |
public override void ExitState(FSMBase fsm) | |
{ | |
base.ExitState(fsm); | |
fsm.MoveToTarget(fsm.transform.position, 1, fsm.Data.MoveSpeed); | |
} | |
/// <summary> | |
/// 无巡逻模式 | |
/// </summary> | |
private void NoneMode(FSMBase fsm) | |
{ | |
fsm.Data.CompletePatrol = true; | |
} | |
/// <summary> | |
/// 单次模式 | |
/// </summary> | |
private void OnceMode(FSMBase fsm) | |
{ | |
if (Vector3.Distance(fsm.transform.position, fsm.Data.WayPoints[Index].position) <= fsm.Data.PatrolEffect) | |
{ | |
Index++; | |
} | |
if (Index >= fsm.Data.WayPoints.Length) | |
{ | |
fsm.Data.CompletePatrol = true; | |
return; | |
} | |
fsm.MoveToTarget(fsm.Data.WayPoints[Index].position, 0, fsm.Data.MoveSpeed); | |
} | |
/// <summary> | |
/// 循环模式 | |
/// </summary> | |
private void LoopMode(FSMBase fsm) | |
{ | |
if (Vector3.Distance(fsm.transform.position, fsm.Data.WayPoints[Index].position) <= fsm.Data.PatrolEffect) | |
{ | |
Index = (Index + 1) % fsm.Data.WayPoints.Length; | |
} | |
fsm.MoveToTarget(fsm.Data.WayPoints[Index].position, 0, fsm.Data.MoveSpeed); | |
} | |
/// <summary> | |
/// 往返模式 | |
/// </summary> | |
private void RoundTripMode(FSMBase fsm) | |
{ | |
if (Vector3.Distance(fsm.transform.position, fsm.Data.WayPoints[Index].position) <= fsm.Data.PatrolEffect) | |
{ | |
if (Index >= fsm.Data.WayPoints.Length - 1) | |
{ | |
Array.Reverse(fsm.Data.WayPoints); | |
Index++; | |
} | |
Index = (Index + 1) % fsm.Data.WayPoints.Length; | |
} | |
fsm.MoveToTarget(fsm.Data.WayPoints[Index].position, 0, fsm.Data.MoveSpeed); | |
} | |
} | |
} |
攻击状态
using Common; | |
using UnityEngine; | |
namespace AI.FSM | |
{ | |
/// <summary> | |
/// 攻击状态 | |
/// </summary> | |
public class AttackState : FSMState | |
{ | |
public override FSMStateID StateID => FSMStateID.Attack; | |
public override bool IgnoreWalls => true; | |
/// <summary> | |
/// 计时器 | |
/// </summary> | |
private float Timer; | |
/// <summary> | |
/// 用于判断攻击距离 | |
/// </summary> | |
private float JudgeAttackDistance; | |
public override void EnterState(FSMBase fsm) | |
{ | |
base.EnterState(fsm); | |
Timer = 0; | |
JudgeAttackDistance = 0; | |
GetSkill(fsm); | |
} | |
public override void ActionState(FSMBase fsm) | |
{ | |
base.ActionState(fsm); | |
Attack(fsm); | |
} | |
public override void ExitState(FSMBase fsm) | |
{ | |
base.ExitState(fsm); | |
fsm.MoveToTarget(fsm.transform.position, 1, fsm.Data.MoveSpeed); | |
} | |
/// <summary> | |
/// 获取技能 | |
/// </summary> | |
/// <param name="fsm"></param> | |
private void GetSkill(FSMBase fsm) | |
{ | |
fsm.CurrentSkill = fsm.NPCSkillSystem.GetRandomSkillData(); | |
if (fsm.CurrentSkill.MoveDistance != 0) | |
{ | |
JudgeAttackDistance = fsm.CurrentSkill.MoveDistance; | |
} | |
else | |
{ | |
JudgeAttackDistance = fsm.CurrentSkill.AttackDistance; | |
} | |
} | |
/// <summary> | |
/// 攻击 | |
/// </summary> | |
/// <param name="fsm"></param> | |
private void Attack(FSMBase fsm) | |
{ | |
// 看向目标 | |
fsm.transform.LookAtTarget(fsm.FoundTarget); | |
Timer += Time.deltaTime; | |
if (Timer > fsm.Data.AttackTimeInterval) | |
{ | |
if (fsm.CurrentSkill != null && Vector3.Distance(fsm.transform.position, fsm.FoundTarget.position) <= JudgeAttackDistance) | |
{ | |
// 放技能 | |
fsm.NPCSkillSystem.UseSkill(fsm.CurrentSkill.SkillID); | |
SetPosition(fsm); | |
GetSkill(fsm); | |
Timer = 0; | |
} | |
else | |
{ | |
// 靠近目标,达到能释放技能的位置 | |
fsm.MoveToTarget(fsm.FoundTarget.position, JudgeAttackDistance * fsm.Data.SkillDistanceEffect, fsm.Data.MoveSpeed); | |
} | |
} | |
} | |
/// <summary> | |
/// 设置释放位置 | |
/// </summary> | |
/// <param name="fsm"></param> | |
private void SetPosition(FSMBase fsm) | |
{ | |
if (fsm.NPCSkillSystem.Skill != null) | |
{ | |
Transform owner = fsm.NPCSkillSystem.Skill.Owner.transform; | |
fsm.NPCSkillSystem.SetPosition(owner.position, owner.rotation); | |
} | |
} | |
} | |
} |
# 类图
最后给出类图:
# 测试
添加组件
设置参数
结果演示