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
相关产品推荐
相关产品推荐

