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

Unity状态机Enemy AI异常:跳过Chase直接进入Attack且无法切换状态

游戏状态机问题修复

核心问题分析

问题1:敌人发现玩家后直接进入Attack状态,无移动行为

  • ChaseState中的ChasePlayer()方法未被调用,导致进入Chase状态后不会执行移动逻辑
  • 若玩家初始就在ChaseState的攻击范围内,ChaseState会立刻切换到AttackState,表现为Idle直接跳转Attack

问题2:玩家离开攻击范围后,敌人无法切回Chase状态

  • AttackState使用了带参数的构造函数,而Unity MonoBehaviour组件不能通过自定义构造函数初始化,导致chaseState引用为空
  • AttackState的攻击范围是硬编码固定值,与ChaseState的攻击范围参数未统一
  • 状态切换逻辑仅针对"Enemy_Apple"标签,不具备通用性

修改后的代码实现

1. State基类(无修改)

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

public abstract class State : MonoBehaviour
{
    public abstract State RunCurrentState();
}

2. StateManager(无修改)

using System.Collections;
using UnityEngine;

public class StateManager : MonoBehaviour
{
    [SerializeField] private State currentState;

    void Update()
    {
        RunStateMachine();
    }

    private void RunStateMachine()
    {
        State nextState = currentState?.RunCurrentState();

        if (nextState != null)
        {
            SwitchToTheNextState(nextState);
        }
    }

    private void SwitchToTheNextState(State nextState)
    {
        currentState = nextState;
    }
}

3. IdleState(优化目标检测与状态切换逻辑)

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

public class IdleState : State
{
    public ChaseState ChaseState;
    [SerializeField] private GameObject _target;
    [SerializeField][Range(0, 100)] private float _seenRange;
    private bool _isSpotted;
    private float _delta;

    private void Start()
    {
        _target = GameObject.Find("Player");
        _delta = _target.transform.localScale.x / 2f;
        StartCoroutine(CheckForTargets());
    }

    private IEnumerator CheckForTargets()
    {
        while (true)
        {
            yield return new WaitForSeconds(1);
            if (_target == null)
            {
                Debug.Log("No target...");
                _isSpotted = false;
                continue;
            }
            
            _isSpotted = false;
            RaycastHit2D[] seenEntities = Physics2D.RaycastAll(transform.position, _target.transform.position - transform.position, _seenRange);

            foreach (var ent in seenEntities)
            {
                if (ent.transform.CompareTag(_target.tag))
                {
                    var ray2d = new Ray2D(_target.transform.position, transform.position - _target.transform.position);
                    _isSpotted = true;
                    for (float i = 0; i < Vector3.Distance(_target.transform.position, transform.position); i += _delta)
                    {
                        var tile = MapManager.Instance.SteppedOnTile(Vector3Int.FloorToInt(ray2d.GetPoint(i)));
                        if (!tile.isWalkable)
                        {
                            _isSpotted = false;
                            break;
                        }
                    }
                    break; // 找到目标后终止遍历,减少冗余计算
                }
            }

            GetComponent<SpriteRenderer>().color = _isSpotted ? Color.red : Color.white;
        }
    }

    public override State RunCurrentState()
    {
        if (_isSpotted)
        {
            StopAllCoroutines();
            // 延迟重置标志位,避免状态切换后立刻切回Idle
            StartCoroutine(ResetSpottedFlag());
            return ChaseState;
        }
        return this;
    }

    private IEnumerator ResetSpottedFlag()
    {
        yield return null;
        _isSpotted = false;
    }
}

修改点:

  • 改用CompareTag判断标签,性能更优
  • 找到目标后终止遍历循环,减少无效计算
  • 延迟重置_isSpotted标志位,避免状态切换后立刻触发Idle判断

4. ChaseState(添加移动逻辑,统一攻击范围)

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

public class ChaseState : State
{
    [SerializeField] private AttackState attackState;
    [Range(0,100)] [SerializeField] private float attackRange = 10.0f;
    [SerializeField] private GameObject _target;
    private MovementComponent _movementScript;

    private void Start()
    {
        _movementScript = GetComponent<MovementComponent>();
        _target = _target ?? GameObject.Find("Player");
        // 将攻击范围同步给AttackState,确保逻辑统一
        if(attackState != null)
        {
            attackState.SetAttackRange(attackRange);
        }
    }

    private void FixedUpdate()
    {
        if(_target == null) return;
        float distance = Vector3.Distance(transform.position, _target.transform.position);
        // 仅在非攻击范围内执行移动
        if(distance > attackRange)
        {
            ChasePlayer();
        }
    }

    public override State RunCurrentState()
    {
        if(_target == null) return this;
        bool isInAttackRange = Vector3.Distance(transform.position, _target.transform.position) <= attackRange;
        return isInAttackRange ? attackState : this;
    }

    private void ChasePlayer()
    {
        Vector3 direction = _target.transform.position - transform.position;
        _movementScript.Move(direction);
    }

    // 给AttackState提供自身有效引用
    public ChaseState GetSelf()
    {
        return this;
    }
}

修改点:

  • 在FixedUpdate中调用ChasePlayer(),确保进入Chase状态后执行移动
  • 仅当玩家不在攻击范围内时才移动,避免攻击范围内无效移动
  • 添加方法同步攻击范围给AttackState,避免数值不一致
  • 添加GetSelf()方法,让AttackState可以获取有效引用

5. AttackState(修复状态切换逻辑,移除无效构造函数)

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

public class AttackState : State
{
    [SerializeField] private Transform _target;
    private float attackRange;
    [SerializeField] private ChaseState chaseState;
    private AttackComponent _attackScript;

    private void Start()
    {
        _target = GameObject.Find("Player")?.transform;
        _attackScript = GetComponent<AttackComponent>();
        // 自动获取ChaseState引用,避免序列化赋值遗漏
        if(chaseState == null)
        {
            chaseState = GetComponent<ChaseState>();
        }
    }

    public override State RunCurrentState()
    {
        if (_target == null) return this;
        
        float distance = Vector3.Distance(transform.position, _target.position);
        if (distance <= attackRange)
        {
            _attackScript.Attack(transform.position - _target.position);
            return this;
        }
        // 玩家离开攻击范围,直接切回Chase状态
        return chaseState;
    }

    // 接收ChaseState传递的攻击范围
    public void SetAttackRange(float range)
    {
        attackRange = range;
    }
}

修改点:

  • 移除带参数的构造函数,改用序列化字段或自动获取chaseState引用
  • 统一使用ChaseState传递的攻击范围,避免硬编码不一致
  • 简化状态切换逻辑,玩家离开攻击范围直接切回ChaseState,不再限制标签

额外优化建议

  • 所有状态中的_target可以改为通过StateManager统一获取,避免重复查找
  • 给状态添加OnEnterState()和OnExitState()方法,处理状态切换时的初始化/清理逻辑
  • 攻击状态添加攻击冷却逻辑,避免连续攻击

内容的提问来源于stack exchange,提问作者Emilhhh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 14:37:01