FSM有限状态机详解
FSM的定义
有限的多个状态在不同条件下相互转换的流程控制系统
- 状态:物体表现出的行为(巡逻状态,追击状态,攻击状态等等)
- 进入状态时的行为
- 处于状态时的行为
- 离开状态时的行为
- 条件:状态切换的依据(发现敌人,血量归0等等)
- 状态机:管理所有状态,组织状态的切换。
FSM的适用性
有限状态机适合做流程控制的AI
- 游戏NPC的制作
- 有时游戏流程复杂时也可以考虑使用FSM来进行控制灵活性拓展性强
FSM的设计分析
状态转换表的使用
上图是一个状态转换表的例子,定义了一个敌人NPC的流程,各个状态的转换条件和目标状态,状态转换表根据具体需求而定
-
状态和条件需要唯一的标识Id,不能直接用上述的中文
- 考虑为每个状态和条件定义枚举Id
-
每个状态下都有着若干条件,对应着若干目标状态
- 一个状态内应存有所有转换条件和目标状态的Id,不能存有具体的其它状态对象
- 状态机掌管所有的状态,实际具体的状态切换应该由状态机实现
状态和条件的标识符Id
状态编号 FSMStateID
条件编号 FSMTriggerID
每个状态和条件都必须唯一的对应一个ID标识符!
Id标识符对应到程序上就是一个Enum枚举变量
条件基类的设计 FSMTrigger
-
必须的属性和字段
- 其必须有一个唯一的标识,
FSMTriggerID TriggerID;
- 其必须有一个唯一的标识,
-
必须的方法行为
- 其必须提供一个方法判断条件是否满足,
public abstract bool HandleTrigger(FSMBase fsm);
- 条件的判断逻辑多种多样,条件基类应设计成抽象类,子类需要实现具体的条件判断逻辑。
- 其必须提供一个方法判断条件是否满足,
状态基类的设计 FSMState
- 必须的属性和字段
- 状态也必须有一个唯一的标识,
FSMTriggerID StateID;
- 根据状态转换表的分析,每个状态应保存自己的状态转换表即
FSMTriggerId
到FSMStateId
的映射,可以考虑用Dictionary存储 - 除了保存条件Id,因为要每帧监听条件是否满足并做相应的切换状态处理,还应保存所有的条件对象
List<FSMTrigger>
- 状态也必须有一个唯一的标识,
- 必须的方法行为
- 状态转换表的Dictionary和条件对象的List需要根据状态转换表的配置文件进行配置,由状态机实际解析和为状态进行配置,状态里只需要实现操作相应集合的方法即可
AddMap(FSMTriggerID triggerID, FSMStateID stateID);
- 提供方法检测条件切换状态的检测方法,切换状态应在状态机中做,检测方法应该放在状态中。
- 最后还应提供可选的三个行为,具体状态子类根据逻辑选择性的重写
- 进入状态的virtual函数
EnterState(FSMBase fsmBase);
- 处于状态的virtual函数
ActionState(FSMBase fsmBase);
- 离开状态的virtual函数
ExitState(FSMBase fsmBase);
- 进入状态的virtual函数
- 状态转换表的Dictionary和条件对象的List需要根据状态转换表的配置文件进行配置,由状态机实际解析和为状态进行配置,状态里只需要实现操作相应集合的方法即可
状态机的设计 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;
}
}