You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
        }
    }      
}

问题定位

  1. Update逻辑冲突:Update中先调用StateHandler(),随后又直接触发Attack状态切换并调用伤害,导致状态频繁切换,StateHandler内的状态逻辑被干扰,Attack状态无法稳定停留。同时每帧调用takeDamage不符合游戏逻辑,且可能因状态切换失效。
  2. PlayerMovement引用错误:op = GetComponent<PlayerMovement>()是在AI自身获取组件,而该组件应该属于玩家对象,导致op为空,伤害调用失败。
  3. Attack状态行为缺失:OnEnterAttack仅设置动画参数,未停止NavMeshAgent移动,AI会继续追踪玩家;同时无攻击冷却机制,无法实现间隔性伤害。
  4. 状态切换优先级不合理:Attack状态下优先判断CanSeePlayer(),会导致AI刚进入Attack就切换到Charge状态,无法正常攻击。
  5. Tag查找错误:vaccine和targetPos使用同一Tag,导致两者指向同一对象,逻辑混乱。
  6. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 04:57:02