Unity中FSM攻击状态失效问题求助及代码优化建议
问题排查:Unity有限状态机Attack状态失效问题
问题描述
我在Unity中编写的PlagueFiniteStateMachine有限状态机(FSM)脚本里,Attack状态无法正常工作。预期效果是当Agent进入玩家攻击范围时,触发攻击动画并对玩家造成伤害,但当前无法实现该功能。我曾尝试在Update函数中加入距离检测逻辑,持续判断玩家是否在攻击范围内,但问题仍未解决。目前处于中级开发水平,希望能帮忙排查问题并提供代码优化建议。
完整脚本代码
public class PlagueFiniteStateMachine : MonoBehaviour { public enum doctorState { // define every state needed Walking, Attack, Charge, Return, Idle, Follow, } [SerializeField] float visibleDis= 15f; [SerializeField] float visibleAngle= 10f; [SerializeField] int attack= 10; [SerializeField] float distance = 5f; private Animator ani; public LayerMask ignore; [SerializeField] private GameObject player; [SerializeField] NavMeshAgent plague; [SerializeField] GameObject targetPos; [SerializeField] GameObject aiBase; [SerializeField] GameObject vaccine; [SerializeField]GameObject Indicator; PlayerMovement op; //[SerializeField]Vector3 destination; doctorState currentState; void Start() { Indicator.SetActive(false); plague = GetComponent<NavMeshAgent>(); ani = this.GetComponent<Animator>(); op = GetComponent<PlayerMovement>(); player = GameObject.FindGameObjectWithTag("Player"); targetPos = GameObject.FindGameObjectWithTag("Flag"); aiBase = GameObject.FindGameObjectWithTag("AIBase"); vaccine = GameObject.FindGameObjectWithTag("Flag"); ChangeState(doctorState.Walking); // on entry walking state is default state plague.SetDestination(targetPos.transform.position); } void Update() { float stillAttack = Vector3.Distance(player.transform.position, plague.transform.position); StateHandler(); if (stillAttack <= distance) { ChangeState(doctorState.Attack); plague.SetDestination(player.transform.position); op.takeDamage(attack); } } // method to handle behaviours between states & conditions to switch states void StateHandler() { switch (currentState) { case doctorState.Walking: if (CanSeePlayer()) { ChangeState(doctorState.Charge); } else if (CanAttack()) { ChangeState(doctorState.Attack); } else if (ReachedTarget()) { ChangeState(doctorState.Return); } break; case doctorState.Attack: if(CanSeePlayer()) { ChangeState(doctorState.Charge); } else if (!CanAttack()) { ChangeState(doctorState.Walking); } else if(ReachedTarget()) { ChangeState(doctorState.Return); } break; case doctorState.Charge: if (!CanSeePlayer()) { ChangeState(doctorState.Walking); } else if(CanAttack()) { ChangeState(doctorState.Attack); } else if (ReachedTarget()) { ChangeState(doctorState.Return); } break; case doctorState.Return: if (AgentReturnedVaccine()) { Debug.Log("Agent secured the package, We have failed."); ChangeState(doctorState.Idle); } break; } } // method to handle the behaviour in transition between states void ChangeState(doctorState newState) { if(currentState== newState) return; // to prevent unnecessary transitions; currentState= newState; //here is the enter logic for current states switch (currentState) { case doctorState.Walking: OnEnterWalking(); break; case doctorState.Charge: OnEnterCharge(); break; case doctorState.Attack: OnEnterAttack(); break; case doctorState.Idle: OnEnterIdle(); return; case doctorState.Return: OnEnterReturn(); break; } } // Enter and exit methods for each state void OnEnterWalking() { ani.SetBool("CanAttack", false); ani.SetBool("CanCharge", false); ani.SetBool("IsWalking", true); plague.SetDestination(targetPos.transform.position); Debug.Log("Current state is: " + currentState); } void OnEnterIdle() { Indicator.SetActive(false); ani.SetBool("CanAttack", false); ani.SetBool("CanCharge", false); ani.SetBool("IsWalking", false); ani.SetBool("IsIdle", true); Debug.Log("Current state is: " + currentState); } void OnEnterCharge() { Indicator.SetActive(false); ani.SetBool("IsWalking", false); ani.SetBool("CanAttack", false); ani.SetBool("CanCharge", true); plague.SetDestination(player.transform.position); Debug.Log("Current state is: " + currentState); } void OnEnterAttack() { Indicator.SetActive(false); // float stillAttack = Vector3.Distance(player.transform.position, plague.transform.position); ani.SetBool("CanCharge", false); ani.SetBool("IsWalking", false); ani.SetBool("CanAttack", true); /* if(stillAttack<= distance) { plague.SetDestination(player.transform.position); op.takeDamage(attack); } */ Debug.Log("Current state is: " + currentState); } void OnEnterReturn() { Indicator.SetActive(true); ani.SetBool("IsWalking", false); ani.SetBool("CanAttack", false); ani.SetBool("CanCharge", true); plague.SetDestination(aiBase.transform.position); Debug.Log("Current state is: " + currentState); } bool Captured(Collider other) { string thisTag = "Agent"; GameObject someObject = GameObject.FindGameObjectWithTag(thisTag); if (other.gameObject.tag == "flag" && someObject != null) { return true; } return false; } public bool CanSeePlayer() // boolean method to check if player is in range { Vector3 direction= player.transform.position- this.transform.position; float angle= Vector3.Angle(direction, this.transform.forward); if(direction.magnitude< visibleDis && angle< visibleAngle) { return true; } return false; } public bool CanAttack() { /* RaycastHit hit; int maxDis = 5; ; if(Physics.Raycast(transform.position, Vector3.forward, maxDis, ignore )) { Debug.DrawRay(transform.position, transform.forward, Color.red); Debug.Log("Hit " + player); return true; } return false; */ if(Vector3.Distance(player.transform.position,plague.transform.position) <= distance) { return true; } return false; } bool ReachedTarget() { if(Vector3.Distance(targetPos.transform.position, plague.transform.position) < distance) { return true; } return false; } bool AgentReturnedVaccine() { return Vector3.Distance(aiBase.transform.position, plague.transform.position) < distance; } void OnTriggerEnter(Collider other) { if (Captured(other)) { Debug.Log("Returning to base"); ChangeState(doctorState.Return); } } }
问题定位
- Update逻辑冲突:Update中先调用
StateHandler(),随后又直接触发Attack状态切换并调用伤害,导致状态频繁切换,StateHandler内的状态逻辑被干扰,Attack状态无法稳定停留。同时每帧调用takeDamage不符合游戏逻辑,且可能因状态切换失效。 - PlayerMovement引用错误:
op = GetComponent<PlayerMovement>()是在AI自身获取组件,而该组件应该属于玩家对象,导致op为空,伤害调用失败。 - Attack状态行为缺失:
OnEnterAttack仅设置动画参数,未停止NavMeshAgent移动,AI会继续追踪玩家;同时无攻击冷却机制,无法实现间隔性伤害。 - 状态切换优先级不合理:Attack状态下优先判断
CanSeePlayer(),会导致AI刚进入Attack就切换到Charge状态,无法正常攻击。 - Tag查找错误:
vaccine和targetPos使用同一Tag,导致两者指向同一对象,逻辑混乱。 - Captured函数逻辑错误:函数内查找"Agent"标签对象与碰撞判断无关,逻辑冗余且可能出错。
修复方案
1. 修正PlayerMovement引用
在Start()方法中,将op = GetComponent<PlayerMovement>();修改为:
op = player.GetComponent<PlayerMovement>();
2. 重构Update逻辑,统一状态管理
移除Update中的状态切换和伤害代码,仅保留状态处理:
void Update() { StateHandler(); }
3. 完善Attack状态行为
- 添加攻击冷却变量:
[SerializeField] float attackCooldown = 2f; private float lastAttackTime; - 修改
OnEnterAttack,停止AI移动:void OnEnterAttack() { Indicator.SetActive(false); ani.SetBool("CanCharge", false); ani.SetBool("IsWalking", false); ani.SetBool("CanAttack", true); plague.isStopped = true; // 停止移动,原地攻击 lastAttackTime = Time.time; // 初始化攻击时间 Debug.Log("Current state is: " + currentState); } - 在
StateHandler的Attack分支中添加冷却判断和状态恢复逻辑:case doctorState.Attack: // 间隔触发伤害 if (Time.time - lastAttackTime >= attackCooldown) { op.takeDamage(attack); lastAttackTime = Time.time; } // 调整状态切换优先级,优先判断攻击范围 if (!CanAttack()) { plague.isStopped = false; ChangeState(doctorState.Walking); } else if(CanSeePlayer()) { plague.isStopped = false; ChangeState(doctorState.Charge); } else if(ReachedTarget()) { plague.isStopped = false; ChangeState(doctorState.Return); } break;
4. 修正Tag查找错误
给vaccine设置独立Tag(如"Vaccine"),修改Start()中的查找代码:
vaccine = GameObject.FindGameObjectWithTag("Vaccine");
5. 修复Captured函数逻辑
简化碰撞判断逻辑:
bool Captured(Collider other) { return other.CompareTag("Flag"); // 根据实际需求修改目标Tag }
代码优化建议
- 状态模式重构:将每个状态拆分为独立类,实现
IState接口(包含Enter()、Update()、Exit()方法),替代臃肿的switch语句,提升代码可维护性。 - Inspector直接赋值:尽量通过
[SerializeField]在Inspector面板直接拖入player、targetPos等对象,减少FindGameObjectWithTag调用,提升性能和代码可靠性。 - 完善视野检测:在
CanSeePlayer()中添加射线检测,排除障碍物遮挡的情况:public bool CanSeePlayer() { Vector3 direction = player.transform.position - transform.position; float angle = Vector3.Angle(direction, transform.forward); if (direction.magnitude < visibleDis && angle < visibleAngle) { if (!Physics.Raycast(transform.position, direction.normalized, visibleDis, ignore)) { return true; } } return false; } - 动画参数校验:确保Animator中的参数名称与代码中完全一致,避免拼写错误导致动画不触发。
- Gizmos调试:添加
OnDrawGizmos()方法,绘制攻击范围、视野范围,方便调试:void OnDrawGizmosSelected() { // 绘制攻击范围 Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, distance); // 绘制视野范围 Gizmos.color = Color.yellow; Vector3 forward = transform.forward * visibleDis; Gizmos.DrawWireArc(transform.position, Vector3.up, Quaternion.Euler(0, -visibleAngle/2, 0)*transform.forward, visibleAngle, visibleDis); }
内容的提问来源于stack exchange,提问作者1neWrld
相关产品推荐
相关产品推荐

