Unity下FSM有限状态机详解

FSM有限状态机详解

FSM的定义

有限的多个状态在不同条件下相互转换的流程控制系统

  • 状态:物体表现出的行为(巡逻状态,追击状态,攻击状态等等)
    • 进入状态时的行为
    • 处于状态时的行为
    • 离开状态时的行为
  • 条件:状态切换的依据(发现敌人,血量归0等等)
  • 状态机:管理所有状态,组织状态的切换。

FSM的适用性

有限状态机适合做流程控制的AI

  • 游戏NPC的制作
  • 有时游戏流程复杂时也可以考虑使用FSM来进行控制灵活性拓展性强

FSM的设计分析

状态转换表的使用

在这里插入图片描述

上图是一个状态转换表的例子,定义了一个敌人NPC的流程,各个状态的转换条件和目标状态,状态转换表根据具体需求而定

  • 状态和条件需要唯一的标识Id,不能直接用上述的中文

    • 考虑为每个状态和条件定义枚举Id
  • 每个状态下都有着若干条件,对应着若干目标状态

    • 一个状态内应存有所有转换条件和目标状态的Id,不能存有具体的其它状态对象
    • 状态机掌管所有的状态,实际具体的状态切换应该由状态机实现

状态和条件的标识符Id

状态编号 FSMStateID

在这里插入图片描述

条件编号 FSMTriggerID

扫描二维码关注公众号,回复: 14629815 查看本文章

在这里插入图片描述

每个状态和条件都必须唯一的对应一个ID标识符!

Id标识符对应到程序上就是一个Enum枚举变量

条件基类的设计 FSMTrigger

  • 必须的属性和字段

    • 其必须有一个唯一的标识,FSMTriggerID TriggerID;
  • 必须的方法行为

    • 其必须提供一个方法判断条件是否满足,public abstract bool HandleTrigger(FSMBase fsm);
    • 条件的判断逻辑多种多样,条件基类应设计成抽象类,子类需要实现具体的条件判断逻辑。

状态基类的设计 FSMState

  • 必须的属性和字段
    • 状态也必须有一个唯一的标识,FSMTriggerID StateID;
    • 根据状态转换表的分析,每个状态应保存自己的状态转换表即FSMTriggerIdFSMStateId的映射,可以考虑用Dictionary存储
    • 除了保存条件Id,因为要每帧监听条件是否满足并做相应的切换状态处理,还应保存所有的条件对象List<FSMTrigger>
  • 必须的方法行为
    • 状态转换表的Dictionary和条件对象的List需要根据状态转换表的配置文件进行配置,由状态机实际解析和为状态进行配置,状态里只需要实现操作相应集合的方法即可AddMap(FSMTriggerID triggerID, FSMStateID stateID);
    • 提供方法检测条件切换状态的检测方法,切换状态应在状态机中做,检测方法应该放在状态中。
    • 最后还应提供可选的三个行为,具体状态子类根据逻辑选择性的重写
      • 进入状态的virtual函数EnterState(FSMBase fsmBase);
      • 处于状态的virtual函数ActionState(FSMBase fsmBase);
      • 离开状态的virtual函数ExitState(FSMBase fsmBase);

状态机的设计 FSMBase

  • 必须的属性和字段
    • 状态机负责管理所有的状态,其必须存有所有的状态List<FSMState> states;
    • 应该提供一个默认状态作为初始的状态FSMStateID defaultStateID;
    • 应该保存一个当前状态的对象,所有的状态切换都是基于此对象,FSMState currentState;
  • 必须的方法行为
    • 关键方法:配置状态机,需要根据配置文件动态创建状态对象,并配置每个状态对象对应的状态转换表,利用反射技术void ConfigFSM();
    • Update(); 每帧轮询监听状态检测条件,每帧调用当前状态的ActionState函数
    • 提供状态的切换函数,负责切换状态,触发上一个状态的离开行为和下一个状态的进入行为。ChangeActiveState(FSMStateID stateID);
    • PS:因为状态和条件类都不是Mono脚本,很多功能普通C#类无法直接实现,这里考虑将FSMBase作为参数传递给条件和状态类的相关方法,一些需要获取的属性都放在FSMBase中使用,虽然违反了一定的开闭原则,但大大简化了实现流程。

FSM核心源码

FSMTrigger.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum FSMTriggerID
{
    
    
    FindTarget,
    NoHealth,
    KilledTarget,
}

/// <summary>
/// FSM条件类
/// </summary>
public abstract class FSMTrigger
{
    
    
    //编号 后代必须给其赋值
    public FSMTriggerID TriggerID {
    
     get; set; }

    public FSMTrigger()
    {
    
    
        Init();
    }

    //子类必须初始化条件,为编号赋值
    protected abstract void Init();
    
    //条件逻辑处理
    public abstract bool HandleTrigger(FSMBase fsm);
}

源码的抽象Init为了提醒子类重写时必须要为FSMTriggerID赋值,其余逻辑和上述设计思路一致。

FSMState.cs

using System;
using System.Collections.Generic;

public enum FSMStateID
{
    
    
    Moving,
    Attacking,
    Dead,
    Idle,
}

/// <summary>
/// 状态类
/// </summary>
public abstract class FSMState
{
    
    
    public FSMStateID StateID {
    
     get; set; }
    private Dictionary<FSMTriggerID, FSMStateID> map;
    private List<FSMTrigger> Triggers;

    public FSMState()
    {
    
    
        map = new Dictionary<FSMTriggerID, FSMStateID>();
        Triggers = new List<FSMTrigger>();
        Init();
    }

    //要求实现类必须初始化状态类,为编号赋值
    public abstract void Init();

    //配置状态,由状态机调用
    public void AddMap(FSMTriggerID triggerID, FSMStateID stateID)
    {
    
    
        map.Add(triggerID,stateID);
        //创建条件对象 反射创建
        //命名规范  条件枚举+Trigger
        Type type = Type.GetType(triggerID + "Trigger");
        FSMTrigger trigger = Activator.CreateInstance(type) as FSMTrigger;
        Triggers.Add(trigger);
    }

    //检测当前状态的切换条件是否满足
    public void Reason(FSMBase fsm)
    {
    
    
        for (int i = 0; i < Triggers.Count; i++)
        {
    
    
            //发现条件满足
            if (Triggers[i].HandleTrigger(fsm))
            {
    
    
                FSMStateID stateID = map[Triggers[i].TriggerID];
                //切换状态
                fsm.ChangeActiveState(stateID);
                return;
            }
        }
    }
    
    
    //为具体状态提供可选事件方法
    public virtual void EnterState(FSMBase fsmBase){
    
    }
    
    public virtual void ActionState(FSMBase fsmBase){
    
    }
    
    public virtual void ExitState(FSMBase fsmBase){
    
    }
    
    
}

源码和上述设计思路基本一致,注意AddMap是用来供状态机调用的配置函数,需要用到反射创建条件对象

在介绍FSMBase.cs之前,先介绍几个工具类,用来读取和解析状态转换表的配置文件

ConfigReader.cs

cusing System.IO;
using UnityEngine;
using UnityEngine.Networking;
using System;
namespace Common
{
    
    
    ///<summary>
    ///负责读取配置文件并提供解析行
    ///<summary>
    public class ConfigReader
    {
    
    
        /// <summary>
        /// 加载(获取)配置文件
        /// </summary>
        /// <param name="fileName">文件名</param>
        /// <returns>获取的字符串(待解析)</returns>
        public static string GetConfigFile(string fileName)
        {
    
    
            string url;
            //在移动端通过Application.StreamingAssets不靠谱可能会出错 应用以下方法
            //url根据不同平台有不同的路径,利用宏标签在编译期间运行,根据所处平台选择哪条语句
            //发布后相当于就选择了一条合适的语句url=xxxx  
            //if(Application.platform == RuntimePlatform.Android) 性能稍差
#if UNITY_EDITOR || UNITY_STANDALONE
            url = "file://" + Application.dataPath + "/StreamingAssets/" + fileName;
#elif UNITY_IPHONE
            url = "file://" + Application.dataPath + "/Raw/"+fileName;
#elif UNITY_ANDROID
            url = "jar:file://" + Application.dataPath + "!/assets/"+fileName;
#endif


            //移动端根据url加载文件资源最终返回一个string
            UnityWebRequest www = UnityWebRequest.Get(url);
            www.SendWebRequest();
            while (true)
            {
    
    
                if (www.downloadHandler.isDone)
                    return www.downloadHandler.text;
            }
        }

        public static void Reader(string fileContent,Action<string> handler)
        {
    
    

            //读出来的string   "xxxName=xxxPath/r/nxxxName=xxxPath/r/n....
            //解析字符串,利用StringReader字符串读取器,流用完必须释放内存
            //using 代码块结束自动释放资源
            using (StringReader reader = new StringReader(fileContent))
            {
    
    
                string line;
                while ((line = reader.ReadLine()) != null) //逐行获取字符串
                {
    
    
                    //解析方法
                    handler(line);
                }
            }
        }
    }
}
  • 此函数提供了从StreamingAssets文件夹下读取文件的方法,读取出一串字符串string GetConfigFile(string fileName);
  • GetConfigFile读取出来的字符串是原始的字符串,此工具类还提供了按行解析的Reader函数,只需要将解析的逻辑以委托的形式传给handler即可。

配置表以txt的形式存储,具体格式如以下例子,下面的类就是基于此种格式进行的解析

在这里插入图片描述

FSMConfigReader.cs

using System.Collections;
using System.Collections.Generic;
using Common;
using UnityEngine;

/// <summary>
/// FSM配置文件读取器
/// </summary>
public class FSMConfigReader
{
    
    
    //状态a  下 有条件1->状态b  条件2->状态c
    //大字典 key : 状态   value:映射
    //小字典 key:  条件ID   value:状态ID
    public Dictionary<string, Dictionary<string, string>> Map {
    
     get; private set; }
    private string currentState;
    
    public FSMConfigReader(string fileName)
    {
    
    
        Map = new Dictionary<string, Dictionary<string, string>>();
        
        //读取配置文件
        string configFile = ConfigReader.GetConfigFile(fileName);

        //解析配置文件
        ConfigReader.Reader(configFile,BuildMap);
    }

    //每一行string
    private void BuildMap(string line)
    {
    
    
        //去除空白
        line = line.Trim();
        if (string.IsNullOrEmpty(line)) return;
        if (line.StartsWith("["))
        {
    
    
            currentState = line.Substring(1, line.Length - 2);
            Map.Add(currentState,new Dictionary<string, string>());
        }
        else
        {
    
    
            string[] keyValue = line.Split('>');
            //映射
            Map[currentState].Add(keyValue[0],keyValue[1]);
        }
    }
}

此类依赖于ConfigReader,读取出来配置表后,将其按行解析,然后存入到一个字典中,具体逻辑请查看详细注释

还有一个配置工厂类,主要防止每个NPC都要读取一次配置文件,而是只读取一次然后缓存起来,可有可无,大家自行决定

FSMConfigReaderFactory.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// AI配置文件读取器工厂
/// </summary>
public class FSMConfigReaderFactory : MonoBehaviour
{
    
    
    private static Dictionary<string, FSMConfigReader> cache = new Dictionary<string, FSMConfigReader>();
    

    public static Dictionary<string, Dictionary<string, string>> GetMap(string fileName)
    {
    
    
        if (!cache.ContainsKey(fileName))
        {
    
    
            cache.Add(fileName,new FSMConfigReader(fileName));
        }

        return cache[fileName].Map;
    }
}

接下来就是状态机类的实现过程

FSMBase.cs

using System;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 状态机
/// </summary>
public class FSMBase : MonoBehaviour
{
    
    

    #region Core
    
    //状态列表
    private List<FSMState> states;

    //默认状态ID
    public FSMStateID defaultStateID;
    
    //当前状态
    public FSMState currentState;
    
    //配置文件名称
    public string fileName;
    
    private void Start()
    {
    
    
        //配置状态机
        ConfigFSM();
    	//初始化默认状态
        InitDefaultState();
    }

    private void InitDefaultState()
    {
    
    
        FSMState defaultState = states.Find(s => s.StateID == defaultStateID);
        
        currentState = defaultState;
        //进入状态
        currentState.EnterState(this);
    }
    
    //配置状态机 -- 创建状态对象 设置状态AddMap
    private void ConfigFSM()
    {
    
    
        states = new List<FSMState>();

        var map = FSMConfigReaderFactory.GetMap(fileName);

        //大字典 -> 状态  小字典 -> 映射
        foreach (var state in map)
        {
    
    
            //所有的状态子类命名规范要严格  xxxState 且其枚举Id必须和xxx一致
            Type type = Type.GetType(state.Key + "State");
            FSMState stateObj = Activator.CreateInstance(type) as FSMState;
            states.Add(stateObj); //状态对象加到列表中
            foreach (var item in state.Value)
            {
    
    
                //字符串转枚举
                FSMTriggerID triggerID = (FSMTriggerID)Enum.Parse(typeof(FSMTriggerID), item.Key);
                FSMStateID stateID = (FSMStateID)Enum.Parse(typeof(FSMStateID), item.Value);
                stateObj.AddMap(triggerID,stateID);
            }
        }
        
    }
    
    //Update 每帧监控
    private void Update()
    {
    
    
        //每帧检测状态条件
        currentState.Reason(this);

        currentState.ActionState(this);
    }

    //切换状态
    public void ChangeActiveState(FSMStateID stateID)
    {
    
    
        //离开上一个状态
        currentState.ExitState(this);
        //切换当前状态
        currentState = states.Find(s => s.StateID == stateID);
        //进入下一个状态
        currentState.EnterState(this);
    }

}

需要着重说明的几点

  • 因为FSM利用了反射动态读取配置文件,创建相应的状态对象和条件对象,对状态子类的名称和条件子类的名称有严格的规范
    • 状态子类必须继承FSMState,且名称必须为xxxState,xxx必须和FSMStateID里的一致
    • 条件子类必须继承FSMTrigger,且名称必须为xxxTrigger,xxx必须和FSMTriggerID里的一致

FSMDemo 塔防实例

效果展示

请添加图片描述

状态转换表及配置文件

防御塔状态转换表

在这里插入图片描述

在这里插入图片描述

敌人状态转换表

在这里插入图片描述

在这里插入图片描述

FSMState和FSMTrigger和关键源码完全一致下面就不再给出,直接给出FSMBase

FSMBase 状态机基础类

FSMBase是Mono脚本会作为参数传递给条件和状态来完成一些只有Mono脚本才能完成的事情。下面给出其源码,核心代码和上方设计一致,剩余的方法或属性是在具体的State或Trigger中可能会使用的。

  • **注意:**这里不再直接公开字符串变量在编辑器中,而是以ScriptableObject数据类的形式进行配置。

FSMDataSO.cs 状态机Data配置

using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu()]
public class FSMDataSO : ScriptableObject
{
    
    
    [Tooltip("FSM配置文件名称")]
    public string fileName;

    [Tooltip("目标标签")] 
    public List<string> targetTags;
    
}

FSMBase.cs

using System;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 状态机
/// </summary>
public class FSMBase : MonoBehaviour
{
    
    

    #region Core
    
    //状态列表
    private List<FSMState> states;

    //默认状态ID
    public FSMStateID defaultStateID;
    
    //当前状态
    public FSMState currentState;
    
    private void Start()
    {
    
    
        ConfigFSM();
        //获取角色状态类
        status = GetComponent<CharacterStatus>();
        //获取攻击系统类-用于发射子弹
        attackSystem = GetComponent<AttackSystem>();
        //获取角色的马达类
        motor = GetComponent<CharacterMotor>();
        //获取主基地的Transform
        mainTF = GameObject.FindGameObjectWithTag("MainStorage").transform;
        InitDefaultState();
    }

    private void InitDefaultState()
    {
    
    
        FSMState defaultState = states.Find(s => s.StateID == defaultStateID);
        
        currentState = defaultState;
        //进入状态
        currentState.EnterState(this);
    }
    
    //配置状态机 -- 创建状态对象 设置状态AddMap
    private void ConfigFSM()
    {
    
    
        states = new List<FSMState>();

        var map = FSMConfigReaderFactory.GetMap(DataSo.fileName);

        //大字典 -> 状态  小字典 -> 映射
        foreach (var state in map)
        {
    
    
            Type type = Type.GetType(state.Key + "State");
            FSMState stateObj = Activator.CreateInstance(type) as FSMState;
            states.Add(stateObj); //状态对象加到列表中
            foreach (var item in state.Value)
            {
    
    
                //字符串转枚举
                FSMTriggerID triggerID = (FSMTriggerID)Enum.Parse(typeof(FSMTriggerID), item.Key);
                FSMStateID stateID = (FSMStateID)Enum.Parse(typeof(FSMStateID), item.Value);
                stateObj.AddMap(triggerID,stateID);
            }
        }
        
    }
    
    //Update 每帧监控
    private void Update()
    {
    
    
        //每帧检测状态条件
        currentState.Reason(this);

        currentState.ActionState(this);
    }

    //切换状态
    public void ChangeActiveState(FSMStateID stateID)
    {
    
    
        //离开上一个状态
        currentState.ExitState(this);
        //切换当前状态
        currentState = states.Find(s => s.StateID == stateID);
        //进入下一个状态
        currentState.EnterState(this);
    }
    #endregion

    #region 具体逻辑
    
    //只读
    public FSMDataSO DataSo;

    [HideInInspector]
    public GameObject currentTarget = null;
    [HideInInspector]
    public CharacterStatus status;
    [HideInInspector]
    public AttackSystem attackSystem;
    [HideInInspector]
    public CharacterMotor motor;
    [HideInInspector]
    public Transform mainTF;
    
    public bool FindTarget()
    {
    
    
        //获取首个被射线击中的target物体
        Collider[] hits = Physics.OverlapSphere(transform.position, attackSystem.AttackData.AttackDistance);
        foreach (Collider target in hits)
        {
    
    
            if (DataSo.targetTags.Contains(target.tag))
            {
    
    
                //防止找到尸体
                if(target.transform.GetComponent<CharacterStatus>().CharacterData.HP <= 0) continue;
                currentTarget = target.gameObject;
                return true;
            }
        }

        currentTarget = null;
        return false;
    }

    #endregion

}

FSMState状态子类

AttackingState.cs
using UnityEngine;

public class AttackingState : FSMState
{
    
    
    private float tmpTime = 0;
    public override void Init()
    {
    
    
        StateID = FSMStateID.Attacking;
    }

    public override void EnterState(FSMBase fsmBase)
    {
    
    
        base.EnterState(fsmBase);
        Debug.Log(fsmBase.gameObject.name + "进入攻击状态");
    }

    public override void ActionState(FSMBase fsmBase)
    {
    
    
        base.ActionState(fsmBase);
        //丢失目标就停止
        if (fsmBase.currentTarget == null) return;
        
        //转向目标
        fsmBase.motor.LookAtTarget(fsmBase.currentTarget.transform);
        
        //每隔一段时间攻击一次
        tmpTime -= Time.deltaTime;
        if (tmpTime <= 0)
        {
    
    
            //攻击!
            fsmBase.attackSystem.Fire(fsmBase.currentTarget.transform);
            //进入冷却
            tmpTime += fsmBase.attackSystem.AttackData.AtkInterval;
        }

    }

    public override void ExitState(FSMBase fsmBase)
    {
    
    
        base.ExitState(fsmBase);
        Debug.Log("退出攻击状态");
    }
}
MovingState.cs
using UnityEngine;

public class MovingState : FSMState
{
    
    
    public override void Init()
    {
    
    
        StateID = FSMStateID.Moving;
    }

    public override void EnterState(FSMBase fsmBase)
    {
    
    
        base.EnterState(fsmBase);
        fsmBase.motor.StartMove();
    }

    public override void ActionState(FSMBase fsm)
    {
    
    
        base.ActionState(fsm);
        if (fsm.mainTF == null) 
            fsm.mainTF = GameObject.FindGameObjectWithTag("MainStorage").transform;
        else
        {
    
    
            //朝向主基地移动
            fsm.motor.MoveToTarget(fsm.mainTF,fsm.attackSystem.AttackData.AttackDistance-1f);
            //移动时检测是否经过普通建筑,直接摧毁 -- 有可能敌人不在格子内
            PlacedObject placedObject =
                GridBuildingSystem.Instance.GetGridObject(fsm.transform.position)?.GetPlacedObject();
            //TODO:判断不能被摧毁的类型
            if(placedObject!=null)
                placedObject.DestroySelf();
        }
    }

    public override void ExitState(FSMBase fsmBase)
    {
    
    
        base.ExitState(fsmBase);
        fsmBase.motor.StopMove();
    }
}
IdleState.cs
public class IdleState : FSMState
{
    
    
    public override void Init()
    {
    
    
        StateID = FSMStateID.Idle;
    }
}
DeadState.cs
using UnityEngine;

public class DeadState : FSMState
{
    
    
    public override void Init()
    {
    
    
        StateID = FSMStateID.Dead;
    }

    public override void EnterState(FSMBase fsmBase)
    {
    
    
        base.EnterState(fsmBase);
        Debug.Log("进入死亡状态");
        //进入死亡状态调用死亡方法
        fsmBase.status.Dead();
        //重置CurrentTarget
        fsmBase.currentTarget = null;
    }
}

FSMTrigger条件子类

FSMTrigger.cs
public class FindTargetTrigger : FSMTrigger
{
    
    
    protected override void Init()
    {
    
    
        TriggerID = FSMTriggerID.FindTarget;
    }

    public override bool HandleTrigger(FSMBase fsm)
    {
    
    
        return fsm.FindTarget();
    }
}
KilledTargetTrigger.cs
public class KilledTargetTrigger : FSMTrigger
{
    
    
    protected override void Init()
    {
    
    
        TriggerID = FSMTriggerID.KilledTarget;
    }

    public override bool HandleTrigger(FSMBase fsm)
    {
    
    
        //有可能是击杀后其被销毁 判空即可
        //有可能是其被主动销毁
        if (fsm.currentTarget == null) return true;
        return fsm.currentTarget.GetComponent<CharacterStatus>().CharacterData.HP <= 0;
    }
}
NoHealthTrigger.cs
public class NoHealthTrigger : FSMTrigger
{
    
    
    protected override void Init()
    {
    
    
        TriggerID = FSMTriggerID.NoHealth;
    }

    public override bool HandleTrigger(FSMBase fsm)
    {
    
    
        return fsm.GetComponent<CharacterStatus>().CharacterData.HP <= 0;
    }
}

角色状态类,攻击系统等非关键源码

针对角色状态类和攻击系统,角色数据,攻击数据等笔者这里只给出简易的Demo源码,数据均采用的ScriptableObject,仅供参考。

CharacterStatus.cs
using System;
using Common;
using UnityEngine;

public abstract class CharacterStatus : MonoBehaviour
{
    
    
    public CharacterDataSO templateDataSO;
    private CharacterDataSO characterData;
    public Transform damagePoint;

    private void Awake()
    {
    
    
        damagePoint = transform.FindChildByName("DamagePoint");
    }

    public CharacterDataSO CharacterData {
    
    
        get
        {
    
    
            if (characterData == null)
            {
    
    
                characterData = Instantiate(templateDataSO);
            }

            return characterData;
        }
    }

    public event Action<float> OnHPChange;
    

    //受伤的方法
    public void TakeDamage(int damage)
    {
    
    
        Debug.Log("当前血量"+CharacterData.HP);
        
        CharacterData.HP -= damage;
        
        OnHPChange?.Invoke((float)CharacterData.HP/CharacterData.MaxHP);
    }
    
    //死亡的销毁方法不尽相同
    public abstract void Dead();
}
CharacterDataSO.cs
using UnityEngine;

[CreateAssetMenu()]
public class CharacterDataSO : ScriptableObject
{
    
    
    public int MaxHP;
    public int HP;
    public float moveSpeed;
    public float rotateSpeed;
}
CharacterMotor.cs
using UnityEngine;
using UnityEngine.AI;

public class CharacterMotor : MonoBehaviour
{
    
    
    //角色数据
    private CharacterDataSO characterData;
    private NavMeshAgent agent;
    private void Awake()
    {
    
    
        characterData = GetComponent<CharacterStatus>().CharacterData;
        agent = GetComponent<NavMeshAgent>();
        if (agent != null)
        {
    
    
            SetMoveSpeed(characterData.moveSpeed);
            SetRotateSpeed(characterData.rotateSpeed);
        }
    }

    public void SetMoveSpeed(float speed)
    {
    
    
        agent.speed = speed;
    }

    public void SetRotateSpeed(float rotSpeed)
    {
    
    
        agent.angularSpeed = rotSpeed;
    }
    
    //提供向目标位置移动的方法
    public void MoveToTarget(Transform targetPos,float stopDistance = 0.5f)
    {
    
    
        agent.stoppingDistance = stopDistance;
        agent.SetDestination(targetPos.position);
    }

    public void StopMove()
    {
    
    
        agent.isStopped = true;
    }

    public void StartMove()
    {
    
    
        agent.isStopped = false;
    }

    //面向目标
    public void LookAtTarget(Transform targetTF)
    {
    
    
        Vector3 lookDir = targetTF.position - transform.position;
        Quaternion qDir = Quaternion.LookRotation(lookDir);
        transform.rotation = Quaternion.Lerp(transform.rotation,qDir,Time.deltaTime * characterData.rotateSpeed);
    }


}
AttackSystem.cs
using Common;
using UnityEngine;

/// <summary>
/// 简易攻击系统
/// </summary>
public class AttackSystem : MonoBehaviour
{
    
    
    public AttackDataSO templateSO;
    private AttackDataSO attackData;
    private Transform firePoint;

    public AttackDataSO AttackData
    {
    
    
        get
        {
    
    
            if (attackData == null)
            {
    
    
                attackData = Instantiate(templateSO);
            }

            return attackData;
        }
    }

    private void Start()
    {
    
    
        firePoint = transform.FindChildByName("FirePoint");
    }

    //提供向目标发射子弹的方法
    public void Fire(Transform target)
    {
    
    
        if (target == null) return;
        //向目标发射快速的子弹,扣目标的血量
        GameObject bullet = GameObjectPool.Instance.CreateObject
            (attackData.BulletPrefab.name,attackData.BulletPrefab,firePoint.position,Quaternion.identity);
        //注册子弹到达目标点的造成伤害事件
        bullet.GetComponent<BulletController>().OnArriveTarget += () =>
        {
    
    
            target.GetComponent<CharacterStatus>()?.TakeDamage(attackData.Atk);
        };
        Vector3 targetPos = target.GetComponent<CharacterStatus>().damagePoint.position;
        
        bullet.GetComponent<BulletController>().FlyToTarget(targetPos,attackData.bulletSpeed);
    }
}
AttackDataSO.cs
using UnityEngine;

[CreateAssetMenu()]
public class AttackDataSO : ScriptableObject
{
    
    
    [Tooltip("攻击力")]
    public int Atk;

    [Tooltip("攻击间隔")]
    public float AtkInterval;

    [Tooltip("子弹预制体")]
    public GameObject BulletPrefab;

    [Tooltip("攻击范围")]
    public float AttackDistance;

    [Tooltip("子弹速度")]
    public float bulletSpeed;

}
BulletController.cs
using System;
using System.Collections;
using Common;
using UnityEngine;

/// <summary>
/// 子弹控制器
/// </summary>
public class BulletController : MonoBehaviour
{
    
    

    public event Action OnArriveTarget;
    
    //提供飞向目标后销毁的方法
    public void FlyToTarget(Vector3 targetPos,float speed)
    {
    
    
        StartCoroutine(MoveToTarget(targetPos, speed));
    }

    private IEnumerator MoveToTarget(Vector3 targetPos, float speed)
    {
    
    
        while (Vector3.Distance(transform.position, targetPos) > 1f)
        {
    
    
            transform.position = Vector3.MoveTowards(transform.position, targetPos, speed * Time.deltaTime);
            yield return null;
        }
        //对象池回收
        GameObjectPool.Instance.CollectObject(gameObject);
        OnArriveTarget?.Invoke();
        OnArriveTarget = null;
    }
}

猜你喜欢

转载自blog.csdn.net/Q540670228/article/details/124222488
今日推荐