Files
Vision/Assets/Scripts/Gameplay/EnemyChaseController.cs
2026-10-14 19:33:29 +08:00

171 lines
6.9 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
/// <summary>
/// 敌人追踪:朝目标(玩家)移动,进入销毁半径后自我销毁。
///
/// 目标来源两种:场景里手摆的敌人在 Inspector / 预制体实例里接好(预制体资产本身存不下场景对象,
/// 所以每个场景实例各自接一次);运行时生成的敌人由生成方(PrefabSpawner)在 Instantiate 之后
/// 调 SetTarget 传进来。代码里不按名字 / 标签查找节点。
/// 移动直接改 transform,不依赖 CharacterController / Rigidbody,只在水平面内走,
/// 高度保持不变。
///
/// 死亡流程:一进入销毁半径就立刻在 dieFxPos 节点处生成死亡特效预制体(die_fx,预制体引用,
/// 允许留空)、用 Animator 播死亡动画(把 dieParamName 这个 bool 置为 true,都允许留空),
/// 再等「延迟死亡」的时间,到点销毁自己;延迟期间停止移动和转身。
/// 也就是说 die_delay 延迟的是「身体消失」,不是特效/动画的播放时机——
/// 想看清死亡动画,把 die_delay 设成动画时长即可(三个变量互不关联)。
/// </summary>
[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);
}
/// <summary>由生成方在实例化之后调用,把追踪目标接进来;场景里手摆的敌人直接在 Inspector 接。</summary>
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);
}
/// <summary>进入销毁半径:立刻生成死亡特效、播死亡动画,然后开始倒计时。</summary>
private void StartDying()
{
_dying = true;
_dieTimer = dieDelay;
SpawnDeathPrefab(dieFx);
if (animator != null && !string.IsNullOrEmpty(dieParamName))
animator.SetBool(dieParamName, true);
if (_dieTimer <= 0f)
Destroy(gameObject);
}
/// <summary>在敌人当前位置生成死亡用的预制体,并按它自己的时长兜底回收。</summary>
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));
}
/// <summary>
/// 估这个实例能播多久:粒子取「时长 + 最长粒子寿命」,旧版动画取 Clip 长度,取最长的一个。
/// 只是兜底回收,预制体自己销毁也可以(重复销毁无害)。
/// </summary>
private static float Lifespan(GameObject instance)
{
float longest = 0f;
foreach (ParticleSystem ps in instance.GetComponentsInChildren<ParticleSystem>(true))
{
ParticleSystem.MainModule main = ps.main;
longest = Mathf.Max(longest, main.duration + main.startLifetime.constantMax);
}
foreach (Animation animation in instance.GetComponentsInChildren<Animation>(true))
foreach (AnimationState state in animation)
longest = Mathf.Max(longest, state.length);
return longest > 0f ? longest : 1f; // 认不出时长时给个兜底
}
/// <summary>在场景视图里画出销毁半径,方便对着调数值。</summary>
private void OnDrawGizmosSelected()
{
Gizmos.color = new Color(1f, 0.3f, 0.3f, 0.6f);
Gizmos.DrawWireSphere(transform.position, destroyDistance);
}
}