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