158 lines
6.2 KiB
C#
158 lines
6.2 KiB
C#
using UnityEngine;
|
||
|
||
/// <summary>
|
||
/// 敌人追踪:朝目标(玩家)移动,进入销毁半径后自我销毁。
|
||
///
|
||
/// 目标引用在 Inspector / 预制体实例里接好——预制体资产本身存不下场景对象,
|
||
/// 所以每个场景实例各自接一次;代码里不按名字 / 标签查找节点。
|
||
/// 移动直接改 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 (target == null)
|
||
Debug.LogError("[EnemyChaseController] 没有指定追踪目标(target),请在 Inspector / 预制体实例里拖入玩家。", this);
|
||
|
||
if (animator == null)
|
||
Debug.LogError("[EnemyChaseController] 没有指定 Animator(animator),死亡动画不会播。", this);
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|