using UnityEngine; /// /// 敌人追踪:朝目标(玩家)移动,进入销毁半径后自我销毁。 /// /// 目标来源两种:场景里手摆的敌人在 Inspector / 预制体实例里接好(预制体资产本身存不下场景对象, /// 所以每个场景实例各自接一次);运行时生成的敌人由生成方(PrefabSpawner)在 Instantiate 之后 /// 调 SetTarget 传进来。代码里不按名字 / 标签查找节点。 /// 移动直接改 transform,不依赖 CharacterController / Rigidbody,只在水平面内走, /// 高度保持不变。 /// /// 死亡流程:一进入销毁半径就立刻在 dieFxPos 节点处生成死亡特效预制体(die_fx,预制体引用, /// 允许留空)、用 Animator 播死亡动画(把 dieParamName 这个 bool 置为 true,都允许留空), /// 再等「延迟死亡」的时间,到点销毁自己;延迟期间停止移动和转身。 /// 也就是说 die_delay 延迟的是「身体消失」,不是特效/动画的播放时机—— /// 想看清死亡动画,把 die_delay 设成动画时长即可(三个变量互不关联)。 /// [DisallowMultipleComponent] public class EnemyChaseController : MonoBehaviour { [Header("引用(必须在 Inspector / 预制体实例里指定)")] [Tooltip("追踪目标,拖玩家根节点进来;留空敌人不会移动")] [SerializeField] private Transform target; [Header("移动")] [Tooltip("最大移动速度(单位/秒)")] [SerializeField] private float moveSpeed = 3f; [Tooltip("加速度(单位/秒²),决定从静止加到最大速度的快慢;0 = 立刻满速")] [SerializeField] private float acceleration = 10f; [Tooltip("转身速度,越大转得越快;0 = 不转身,保持初始朝向")] [SerializeField] private float turnSpeed = 10f; [Tooltip("销毁半径:与目标的水平距离小于它时销毁自己(0 = 贴身才销毁)")] [SerializeField] private float destroyDistance = 0.8f; [Header("死亡")] [Tooltip("死亡特效预制体,允许留空。在 dieFxPos 处生成一份,按自身时长兜底回收")] [SerializeField] private GameObject dieFx; [Tooltip("死亡特效的生成位置,拖敌人下的定位节点(例如 fx_die_pos);留空则在敌人自身位置生成")] [SerializeField] private Transform dieFxPos; [Tooltip("敌人身上的 Animator,死亡动画由它播;留空则不播死亡动画")] [SerializeField] private Animator animator; [Tooltip("死亡动画的 bool 参数名,对应 Animator 里的参数;置 true 触发死亡动画,留空则不播")] [SerializeField] private string dieParamName = "is_die"; [Tooltip("延迟死亡:进入销毁半径后再等这么久才销毁自己(0 = 立刻消失,特效和动画也就看不到)")] [SerializeField] private float dieDelay = 0f; private float _currentSpeed; private bool _dying; private float _dieTimer; private void Awake() { if (animator == null) Debug.LogError("[EnemyChaseController] 没有指定 Animator(animator),死亡动画不会播。", this); } private void Start() { // 放在 Start 而不是 Awake:敌人是 Instantiate 出来的,生成方在实例化之后才调 SetTarget, // 而 Instantiate 会立刻跑 Awake,那时目标还没递进来。 if (target == null) Debug.LogError("[EnemyChaseController] 没有追踪目标(target):请在 Inspector / 预制体实例里拖入玩家," + "或由生成方(PrefabSpawner)在实例化后调 SetTarget 传入。", this); } /// 由生成方在实例化之后调用,把追踪目标接进来;场景里手摆的敌人直接在 Inspector 接。 public void SetTarget(Transform value) { target = value; } private void Update() { if (_dying) { _dieTimer -= Time.deltaTime; if (_dieTimer <= 0f) Destroy(gameObject); return; } if (target == null) return; Vector3 toTarget = target.position - transform.position; toTarget.y = 0f; // 只在水平面内追踪,高度不变 float distance = toTarget.magnitude; if (distance <= destroyDistance) { StartDying(); return; } Vector3 direction = toTarget / Mathf.Max(distance, 0.0001f); _currentSpeed = acceleration > 0f ? Mathf.MoveTowards(_currentSpeed, moveSpeed, acceleration * Time.deltaTime) : moveSpeed; transform.position += direction * (_currentSpeed * Time.deltaTime); if (turnSpeed > 0f) transform.rotation = Quaternion.Slerp( transform.rotation, Quaternion.LookRotation(direction), turnSpeed * Time.deltaTime); } /// 进入销毁半径:立刻生成死亡特效、播死亡动画,然后开始倒计时。 private void StartDying() { _dying = true; _dieTimer = dieDelay; SpawnDeathPrefab(dieFx); if (animator != null && !string.IsNullOrEmpty(dieParamName)) animator.SetBool(dieParamName, true); if (_dieTimer <= 0f) Destroy(gameObject); } /// 在敌人当前位置生成死亡用的预制体,并按它自己的时长兜底回收。 private void SpawnDeathPrefab(GameObject prefab) { if (prefab == null) return; Transform spawn = dieFxPos != null ? dieFxPos : transform; GameObject instance = Instantiate(prefab, spawn.position, spawn.rotation); Destroy(instance, Lifespan(instance)); } /// /// 估这个实例能播多久:粒子取「时长 + 最长粒子寿命」,旧版动画取 Clip 长度,取最长的一个。 /// 只是兜底回收,预制体自己销毁也可以(重复销毁无害)。 /// private static float Lifespan(GameObject instance) { float longest = 0f; foreach (ParticleSystem ps in instance.GetComponentsInChildren(true)) { ParticleSystem.MainModule main = ps.main; longest = Mathf.Max(longest, main.duration + main.startLifetime.constantMax); } foreach (Animation animation in instance.GetComponentsInChildren(true)) foreach (AnimationState state in animation) longest = Mathf.Max(longest, state.length); return longest > 0f ? longest : 1f; // 认不出时长时给个兜底 } /// 在场景视图里画出销毁半径,方便对着调数值。 private void OnDrawGizmosSelected() { Gizmos.color = new Color(1f, 0.3f, 0.3f, 0.6f); Gizmos.DrawWireSphere(transform.position, destroyDistance); } }