feat(gameplay): 提升单位检测范围并添加法师属性 - 将所有单位的检测范围从默认值提升至100 - 为法师预制体添加护盾、等级等新属性 - 为法师添加暴击、吸血、伤害减免等战斗属性 - 更换法师弹丸预设并调整相关参数 - 添加锚点交战Z线控制功能 - 更新玩家数据中配备的召唤师技能 - 移除多余的纹理配置文件 ```
1445 lines
44 KiB
C#
1445 lines
44 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
[RequireComponent(typeof(StatusEffectController))]
|
||
public class UnitBase : MonoBehaviour
|
||
{
|
||
[Header("角色数据")]
|
||
[Tooltip("角色数据文件名(从 Resources/Data 加载),不填则使用 Inspector 中的默认值")]
|
||
public string characterDataName = "";
|
||
|
||
[Header("等级")]
|
||
[Tooltip("角色等级,用于从 CharacterData 读取对应等级的属性")]
|
||
public int level = 1;
|
||
|
||
//移动速度
|
||
public float speed = 5f;
|
||
|
||
[Header("寻敌视野")]
|
||
[Tooltip("只在检测范围内寻找敌方战斗单位;范围内没有敌人时原地待机")]
|
||
public float detectionRange = 30f;
|
||
|
||
//攻击范围
|
||
public float attackRange = 5f;
|
||
|
||
//模型自身前方相对逻辑前方的水平修正角度。
|
||
[Header("模型朝向修正")]
|
||
public float modelFacingOffset = 0f;
|
||
|
||
//基础攻击力,实际伤害结算逻辑后续接入
|
||
[Min(0f)]
|
||
public float attackPower = 10f;
|
||
|
||
[Header("战术增益扩展属性")]
|
||
public float critRate = 0f; // 暴击率 (如 0.15 = 15%)
|
||
public float critMultiplier = 1.5f; // 暴击伤害倍数
|
||
public float lifestealRate = 0f; // 伤害吸血比例 (如 0.08 = 8%)
|
||
public float damageReductionRate = 0f; // 伤害减免比例 (如 0.10 = 10%, 最高 75%)
|
||
public float skillChargeMultiplier = 1f; // 技能能量充能倍率 (如 1.25 = +25%)
|
||
|
||
//完整攻击动作时长,单位为秒;设置为0时使用动画真实时长
|
||
[Min(0f)]
|
||
public float attackDuration = 0f;
|
||
|
||
public enum AttackType
|
||
{
|
||
//近战锁定目标,攻击事件触发时直接扣血。
|
||
MeleeDirect,
|
||
|
||
//近战范围伤害,暂未实现。
|
||
MeleeArea,
|
||
|
||
//远程锁定目标,攻击事件触发时生成飞行物。
|
||
RangedProjectile,
|
||
|
||
//远程抛物线,不锁定目标,暂未实现。
|
||
RangedArc
|
||
}
|
||
|
||
[Header("攻击类型")]
|
||
public AttackType attackType = AttackType.MeleeDirect;
|
||
|
||
[Header("远程飞行物")]
|
||
public GameObject projectilePrefab;
|
||
public Transform projectileSpawnPoint;
|
||
[Min(0f)]
|
||
public float projectileSpeed = 20f;
|
||
[Min(0f)]
|
||
public float projectileLifetime = 5f;
|
||
|
||
//每次普通攻击命中后增加的技能能量。
|
||
[Header("技能能量")]
|
||
[Min(0f)]
|
||
public float energyPerAttack = 20f;
|
||
|
||
[Header("阵型分散")]
|
||
[Min(0f)] public float targetSpreadBias = 6f;
|
||
|
||
[Header("召唤师模式")]
|
||
// 召唤师:不自动追击远处敌人,只在站定后攻击射程内的目标;移动由 SummonerClickMovement 控制。
|
||
public bool summonerMode = false;
|
||
|
||
// 召唤师模式下允许自动攻击的开关(移动时由操控脚本关闭,站定后开启)。
|
||
private bool summonerAttackEnabled = true;
|
||
|
||
[Header("无目标回位")]
|
||
[Tooltip("开启后,当前没有敌方单位时返回生成时的初始格位。")]
|
||
public bool returnToAnchorWhenIdle = false;
|
||
[Min(0.1f)]
|
||
public float anchorArrivalDistance = 0.35f;
|
||
private Vector3 anchorPosition;
|
||
private bool anchorInitialized;
|
||
|
||
[Header("阵位守护")]
|
||
[Tooltip("开启后,只打「离自己阵位不超过 寻敌视野 detectionRange」的敌人,避免一路推到很远的地方。")]
|
||
public bool useAnchorLeash = false;
|
||
|
||
[Header("警戒线出击")]
|
||
[Tooltip("开启后,只有当敌人进入警戒线(Z <= engagementZThreshold)后才全员出击;出击后视野全开,战斗中途不回家。")]
|
||
public bool useEngagementZLine = true;
|
||
public float engagementZThreshold = 10.0f;
|
||
|
||
private static bool isHeroTeamEngaged = false;
|
||
|
||
//角色状态
|
||
private State state = State.Alert;
|
||
|
||
//攻击目标
|
||
public GameObject target;
|
||
|
||
// NavMesh 寻路更新间隔(优化性能)
|
||
private float nextPathUpdateTime = 0f;
|
||
private const float PathUpdateInterval = 0.5f; // 每 0.5 秒更新一次路径
|
||
|
||
//动画控制器
|
||
private Animator animator;
|
||
|
||
// NavMesh 寻路代理(所有单位统一走 NavMesh 寻路)
|
||
private UnityEngine.AI.NavMeshAgent navAgent;
|
||
|
||
// 自身的血条组件,同时负责显示技能能量条。
|
||
private WorldHealthBar worldHealthBar;
|
||
|
||
// 通用状态效果组件。
|
||
private StatusEffectController statusEffectController;
|
||
|
||
// 攻击动画的有效经过时间;眩晕期间不增加。
|
||
private float attackElapsedTime;
|
||
|
||
private bool isPlayingSkill;
|
||
private float attackSpeedMultiplier = 1f;
|
||
private UnitSkill unitSkill;
|
||
private System.Action skillFinishedCallback;
|
||
private float skillElapsedTime;
|
||
private float skillTimeout;
|
||
|
||
|
||
//状态枚举
|
||
public enum State
|
||
{
|
||
//警戒巡逻
|
||
Alert,
|
||
|
||
//追踪状态
|
||
Track,
|
||
|
||
//攻击状态
|
||
Attack,
|
||
|
||
//技能状态
|
||
Skill
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
// Start is called before the first frame update
|
||
public void InitializeFromPlayerData(float health, float attack, float moveSpeed, int unitLvl = 1)
|
||
{
|
||
attackPower = attack;
|
||
speed = moveSpeed;
|
||
level = Mathf.Max(1, unitLvl);
|
||
|
||
// 同步更新 NavMeshAgent 速度
|
||
if (navAgent == null)
|
||
{
|
||
navAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
|
||
}
|
||
if (navAgent != null)
|
||
{
|
||
navAgent.speed = moveSpeed;
|
||
}
|
||
|
||
if (worldHealthBar == null)
|
||
{
|
||
worldHealthBar = GetComponent<WorldHealthBar>();
|
||
}
|
||
|
||
if (worldHealthBar != null)
|
||
{
|
||
worldHealthBar.maxHealth = health;
|
||
worldHealthBar.currentHealth = health;
|
||
worldHealthBar.SetLevel(level);
|
||
}
|
||
}
|
||
|
||
void Start()
|
||
{
|
||
//初始化
|
||
animator = GetComponent<Animator>();
|
||
navAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
|
||
worldHealthBar = GetComponent<WorldHealthBar>();
|
||
statusEffectController = EnsureStatusEffectController();
|
||
unitSkill = GetComponent<UnitSkill>();
|
||
anchorPosition = transform.position;
|
||
anchorInitialized = true;
|
||
|
||
// 从 CharacterData 加载数值(如果配置了)
|
||
LoadCharacterData();
|
||
|
||
// 如果有 NavMeshAgent,配置它
|
||
if (navAgent != null)
|
||
{
|
||
navAgent.speed = speed;
|
||
navAgent.angularSpeed = 120f;
|
||
navAgent.acceleration = 8f;
|
||
navAgent.stoppingDistance = NormalStoppingDistance; // 接近攻击范围边缘时停止
|
||
navAgent.autoBraking = true;
|
||
|
||
// NavMesh 使用烘焙平面的高度,不能根据动画帧中的 Renderer 包围盒推算 baseOffset。
|
||
navAgent.baseOffset = 0f;
|
||
}
|
||
|
||
// 根运动作用于所有动画:走路摇摆、攻击前冲由 OnAnimatorMove 统一协调
|
||
animator.applyRootMotion = true;
|
||
}
|
||
|
||
private void LoadCharacterData()
|
||
{
|
||
if (string.IsNullOrEmpty(characterDataName))
|
||
{
|
||
Debug.Log($"{gameObject.name}: 未配置 characterDataName,使用 Inspector 中的默认值");
|
||
return;
|
||
}
|
||
|
||
CharacterData characterData = Resources.Load<CharacterData>($"Data/{characterDataName}");
|
||
if (characterData == null)
|
||
{
|
||
Debug.LogWarning($"{gameObject.name}: 未找到 CharacterData: Data/{characterDataName}");
|
||
return;
|
||
}
|
||
|
||
// 根据等级读取属性
|
||
float health = characterData.GetMaxHealth(level);
|
||
float attack = characterData.GetAttackPower(level);
|
||
float moveSpeed = characterData.GetMoveSpeed(level);
|
||
|
||
// 应用属性
|
||
attackPower = attack;
|
||
speed = moveSpeed;
|
||
|
||
if (worldHealthBar != null)
|
||
{
|
||
worldHealthBar.maxHealth = health;
|
||
worldHealthBar.currentHealth = health;
|
||
}
|
||
|
||
Debug.Log($"{gameObject.name} 从 CharacterData 初始化 (Level {level}): HP={health} ATK={attack} SPD={moveSpeed}");
|
||
}
|
||
|
||
private StatusEffectController EnsureStatusEffectController()
|
||
{
|
||
if (statusEffectController == null)
|
||
{
|
||
statusEffectController = GetComponent<StatusEffectController>();
|
||
}
|
||
|
||
if (statusEffectController == null)
|
||
{
|
||
statusEffectController = gameObject.AddComponent<StatusEffectController>();
|
||
}
|
||
|
||
return statusEffectController;
|
||
}
|
||
|
||
|
||
// Update is called once per frame
|
||
void Update()
|
||
{
|
||
if (statusEffectController != null && statusEffectController.IsStunned)
|
||
{
|
||
animator.SetInteger("speed", 0);
|
||
return;
|
||
}
|
||
|
||
//攻击状态下等待Animator报告攻击动画播放完成。
|
||
if (state == State.Attack)
|
||
{
|
||
// 攻击时暂停 NavMeshAgent,但保持 enabled 维持物理/寻路障碍排体
|
||
if (navAgent != null && navAgent.enabled && navAgent.isOnNavMesh)
|
||
{
|
||
if (!navAgent.isStopped)
|
||
{
|
||
navAgent.isStopped = true;
|
||
navAgent.velocity = Vector3.zero;
|
||
}
|
||
}
|
||
|
||
animator.SetInteger("speed", 0);
|
||
|
||
// 目标死亡、失活或离开攻击范围时,解除锁定并重新寻敌。
|
||
if (!IsTargetValid(target) || !IsWithinAttackReach(target))
|
||
{
|
||
target = null;
|
||
state = State.Alert;
|
||
return;
|
||
}
|
||
|
||
attackElapsedTime += Time.deltaTime;
|
||
CheckAttackAnimationFinished();
|
||
return;
|
||
}
|
||
|
||
if (isPlayingSkill)
|
||
{
|
||
animator.SetInteger("speed", 0);
|
||
CheckSkillAnimationFinished();
|
||
return;
|
||
}
|
||
|
||
// 移动/追踪状态持续重新评估目标,确保相遇时能及时攻击敌方单位。
|
||
// Attack 状态在上方单独处理,因此不会被这里打断或切换目标。
|
||
target = FindTarget();
|
||
|
||
// 阵位守护:目标离自己阵位超过寻敌视野(detectionRange)就不追了,留在阵位附近
|
||
if (target != null && useAnchorLeash && anchorInitialized
|
||
&& IsBeyondLeash(target.transform.position, detectionRange))
|
||
{
|
||
target = null;
|
||
}
|
||
|
||
if (target != null)
|
||
{
|
||
// 召唤师模式:不主动追击,站定且攻击开关开启时才攻击射程内目标。
|
||
if (summonerMode)
|
||
{
|
||
if (!IsWithinAttackReach(target) || !summonerAttackEnabled)
|
||
{
|
||
state = State.Alert;
|
||
// 动画由 SummonerClickMovement 控制,这里不设置
|
||
return;
|
||
}
|
||
attack(target);
|
||
return;
|
||
}
|
||
|
||
state = State.Track;
|
||
|
||
//目标在攻击范围外则朝目标移动,否则开始攻击
|
||
if (!IsWithinAttackReach(target))
|
||
{
|
||
move(target.transform.position - transform.position);
|
||
}
|
||
else
|
||
{
|
||
attack(target);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
//场上完全没有敌人或整波清理干净时,才进入待机回格位
|
||
state = State.Alert;
|
||
if (!HasAnyEnemyOnField())
|
||
{
|
||
isHeroTeamEngaged = false;
|
||
if (returnToAnchorWhenIdle && anchorInitialized && !IsAtAnchor())
|
||
{
|
||
ReturnToAnchor();
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 召唤师模式下动画由 SummonerClickMovement 控制(移动时 speed=1,站定后 speed=0)
|
||
if (!summonerMode)
|
||
{
|
||
animator.SetInteger("speed", 0);
|
||
animator.speed = 1f; // 重置动画播放速度(修复攻击后动画变慢的问题)
|
||
}
|
||
}
|
||
|
||
public GameObject Target => target;
|
||
|
||
private bool IsTargetValid(GameObject candidate)
|
||
{
|
||
if (candidate == null || !candidate.activeInHierarchy)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
WorldHealthBar healthBar = candidate.GetComponentInParent<WorldHealthBar>();
|
||
if (healthBar != null && healthBar.IsDead)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
UnitTeam myTeam = GetComponent<UnitTeam>();
|
||
UnitTeam targetTeam = candidate.GetComponentInParent<UnitTeam>();
|
||
return myTeam == null || targetTeam == null || myTeam.IsEnemy(targetTeam.gameObject);
|
||
}
|
||
|
||
public GameObject FindNearestEnemyForSkill()
|
||
{
|
||
return FindNearestEnemyUnit();
|
||
}
|
||
|
||
// 召唤师模式下控制"移动中不攻击":移动时 false,站定后 true。
|
||
public void SetSummonerAttackEnabled(bool enabled)
|
||
{
|
||
summonerAttackEnabled = enabled;
|
||
if (!enabled && state == State.Track)
|
||
{
|
||
state = State.Alert;
|
||
}
|
||
}
|
||
|
||
public float AttackSpeedMultiplier => attackSpeedMultiplier;
|
||
|
||
public bool IsStunned => statusEffectController != null && statusEffectController.IsStunned;
|
||
|
||
public void ApplyStun(float duration)
|
||
{
|
||
if (duration <= 0f)
|
||
{
|
||
return;
|
||
}
|
||
|
||
EnsureStatusEffectController().ApplyStun(duration);
|
||
}
|
||
|
||
public float ModifyIncomingDamage(float damage, GameObject attacker)
|
||
{
|
||
if (damage <= 0f)
|
||
{
|
||
return 0f;
|
||
}
|
||
|
||
if (unitSkill == null)
|
||
{
|
||
return damage;
|
||
}
|
||
|
||
return unitSkill.ModifyIncomingDamage(damage, attacker);
|
||
}
|
||
|
||
public bool ReceiveDamage(float damage, GameObject attacker = null)
|
||
{
|
||
WorldHealthBar healthBar = GetComponent<WorldHealthBar>();
|
||
if (healthBar == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 战术增益:伤害减免 (最高 75%)
|
||
if (damageReductionRate > 0f)
|
||
{
|
||
float cappedReduction = Mathf.Clamp(damageReductionRate, 0f, 0.75f);
|
||
damage *= (1f - cappedReduction);
|
||
}
|
||
|
||
healthBar.TakeDamage(ModifyIncomingDamage(damage, attacker), attacker);
|
||
return true;
|
||
}
|
||
|
||
//根运动协调(applyRootMotion=true 时由动画系统每帧回调):
|
||
//走路(agent 驱动):只叠加动画的侧向摇摆和转身差量,前向分量丢弃(防双倍移速);
|
||
//攻击/技能:保持 navAgent 开启作为实体阻挡,根运动全量生效,同时实时同步 navAgent.nextPosition 避免拉扯漂移。
|
||
private void OnAnimatorMove()
|
||
{
|
||
if (animator == null) return;
|
||
|
||
Vector3 delta = animator.deltaPosition;
|
||
delta.y = 0f;
|
||
Quaternion yawDelta = Quaternion.Euler(0f, animator.deltaRotation.eulerAngles.y, 0f);
|
||
|
||
bool isAttackingOrSkill = state == State.Attack || state == State.Skill || isPlayingSkill;
|
||
|
||
if (navAgent != null && navAgent.enabled && navAgent.isOnNavMesh)
|
||
{
|
||
if (isAttackingOrSkill)
|
||
{
|
||
// 攻击/技能期间:全量应用根运动位移,并实时同步 agent 位置,保持阻挡体积且不发生位移偏差
|
||
if (delta.sqrMagnitude > 1e-10f)
|
||
{
|
||
transform.position += delta;
|
||
navAgent.nextPosition = transform.position;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 移动中:侧向分量(垂直于朝向)保留 = 大摇大摆的摇摆;同步 agent 位置防漂移
|
||
if (delta.sqrMagnitude > 1e-10f)
|
||
{
|
||
Vector3 lateral = delta - Vector3.Project(delta, transform.forward);
|
||
if (lateral.sqrMagnitude > 1e-10f)
|
||
{
|
||
transform.position += lateral;
|
||
navAgent.nextPosition = transform.position;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 攻击/技能/无 agent:根运动位移全量应用(垂直分量丢弃,防漂离地面)
|
||
if (delta.sqrMagnitude > 1e-10f)
|
||
{
|
||
transform.position += delta;
|
||
}
|
||
}
|
||
|
||
// 朝向:root 旋转差量叠加(身体摇摆、攻击转身)
|
||
transform.rotation *= yawDelta;
|
||
}
|
||
|
||
private void LateUpdate()
|
||
{
|
||
}
|
||
|
||
private bool IsAtAnchor()
|
||
{
|
||
if (!anchorInitialized) return true;
|
||
Vector3 current = transform.position;
|
||
current.y = 0f;
|
||
Vector3 anchor = anchorPosition;
|
||
anchor.y = 0f;
|
||
return Vector3.Distance(current, anchor) <= anchorArrivalDistance;
|
||
}
|
||
|
||
/// <summary>某个世界坐标是否离锚点超过了指定距离(只算水平距离)。</summary>
|
||
private bool IsBeyondLeash(Vector3 worldPosition, float maxDistance)
|
||
{
|
||
if (!anchorInitialized) return false;
|
||
Vector3 anchor = anchorPosition;
|
||
anchor.y = 0f;
|
||
Vector3 point = worldPosition;
|
||
point.y = 0f;
|
||
return Vector3.Distance(anchor, point) > maxDistance;
|
||
}
|
||
|
||
/// <summary>追击/接敌时用的停靠距离(贴着攻击范围边缘停下)。</summary>
|
||
private float NormalStoppingDistance => Mathf.Max(0f, attackRange * 0.95f);
|
||
|
||
/// <summary>
|
||
/// 重置锚点位置到当前位置(用于复活等场景)
|
||
/// </summary>
|
||
public void ResetAnchorPosition()
|
||
{
|
||
anchorPosition = transform.position;
|
||
anchorInitialized = true;
|
||
}
|
||
|
||
private void ReturnToAnchor()
|
||
{
|
||
if (navAgent == null || !anchorInitialized)
|
||
{
|
||
RestoreIdleSpeed();
|
||
return;
|
||
}
|
||
|
||
if (!navAgent.enabled)
|
||
{
|
||
navAgent.enabled = true;
|
||
}
|
||
|
||
if (!navAgent.isOnNavMesh)
|
||
{
|
||
RestoreIdleSpeed();
|
||
return;
|
||
}
|
||
|
||
if (IsAtAnchor())
|
||
{
|
||
navAgent.isStopped = true;
|
||
navAgent.ResetPath();
|
||
transform.position = new Vector3(anchorPosition.x, transform.position.y, anchorPosition.z);
|
||
navAgent.nextPosition = transform.position;
|
||
RestoreIdleSpeed();
|
||
return;
|
||
}
|
||
|
||
navAgent.isStopped = false;
|
||
// 回位必须贴到锚点:攻击用的停靠距离会把 agent 挡在半路(远程角色尤其明显)
|
||
navAgent.stoppingDistance = 0f;
|
||
if (Time.time >= nextPathUpdateTime)
|
||
{
|
||
navAgent.SetDestination(anchorPosition);
|
||
nextPathUpdateTime = Time.time + PathUpdateInterval;
|
||
}
|
||
|
||
// 用正常速度走回格位
|
||
navAgent.speed = speed;
|
||
if (!summonerMode)
|
||
{
|
||
animator.SetInteger("speed", navAgent.velocity.sqrMagnitude > 0.01f ? 1 : 0);
|
||
animator.speed = 1f; // 确保走路动画速度正常
|
||
}
|
||
}
|
||
|
||
/// <summary>回位结束或无法寻路时,把速度和动画恢复成正常站定状态。</summary>
|
||
private void RestoreIdleSpeed()
|
||
{
|
||
if (navAgent != null && navAgent.enabled)
|
||
{
|
||
navAgent.speed = speed;
|
||
navAgent.stoppingDistance = NormalStoppingDistance;
|
||
}
|
||
if (!summonerMode && animator != null)
|
||
{
|
||
animator.SetInteger("speed", 0);
|
||
animator.speed = 1f;
|
||
}
|
||
}
|
||
|
||
//目标决策:只寻找 detectionRange 内的敌方战斗单位;范围内没有敌人时原地待机或回到锚点。
|
||
private GameObject FindTarget()
|
||
{
|
||
return FindNearestEnemyUnit();
|
||
}
|
||
|
||
// 判断目标是否属于本单位自身的层级树(自身或其子物体)。
|
||
// 不能用 transform.root 比较:单位和敌人常挂在同一个场景容器下,会被误判成"自己"而全部跳过。
|
||
private bool IsOwnHierarchy(Transform other)
|
||
{
|
||
if (other == null) return true;
|
||
return other == transform || other.IsChildOf(transform) || transform.IsChildOf(other);
|
||
}
|
||
|
||
/// <summary>检查全场是否还有存活的敌方单位。</summary>
|
||
private bool HasAnyEnemyOnField()
|
||
{
|
||
var myTeam = GetComponent<UnitTeam>();
|
||
if (myTeam == null) return false;
|
||
|
||
UnitTeam[] allTeams = FindObjectsOfType<UnitTeam>();
|
||
foreach (var unit in allTeams)
|
||
{
|
||
if (unit == null || !unit.gameObject.activeInHierarchy) continue;
|
||
if (!myTeam.IsEnemy(unit.gameObject)) continue;
|
||
if (IsOwnHierarchy(unit.transform)) continue;
|
||
|
||
WorldHealthBar healthBar = unit.GetComponentInParent<WorldHealthBar>();
|
||
if (healthBar != null && healthBar.IsDead) continue;
|
||
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
//查找 detectionRange 内最近的敌方战斗单位
|
||
private GameObject FindNearestEnemyUnit()
|
||
{
|
||
var myTeam = GetComponent<UnitTeam>();
|
||
if (myTeam == null)
|
||
{
|
||
Debug.LogWarning($"{gameObject.name} 缺少 UnitTeam 组件,无法查找敌人");
|
||
return null;
|
||
}
|
||
|
||
// 己方英雄团队警戒线出击逻辑:敌人没跨过警戒线(Z <= engagementZThreshold)前按兵不动
|
||
if (useEngagementZLine && (myTeam.teamId == 0 || CompareTag("Friend") || CompareTag("Untagged")))
|
||
{
|
||
if (!HasAnyEnemyOnField())
|
||
{
|
||
isHeroTeamEngaged = false;
|
||
return null;
|
||
}
|
||
|
||
if (!isHeroTeamEngaged)
|
||
{
|
||
bool enemyCrossedLine = false;
|
||
UnitTeam[] allTeamsForCheck = FindObjectsOfType<UnitTeam>();
|
||
foreach (var unit in allTeamsForCheck)
|
||
{
|
||
if (unit == null || !unit.gameObject.activeInHierarchy) continue;
|
||
if (!myTeam.IsEnemy(unit.gameObject)) continue;
|
||
if (IsOwnHierarchy(unit.transform)) continue;
|
||
|
||
WorldHealthBar healthBar = unit.GetComponentInParent<WorldHealthBar>();
|
||
if (healthBar != null && healthBar.IsDead) continue;
|
||
|
||
if (unit.transform.position.z <= engagementZThreshold)
|
||
{
|
||
enemyCrossedLine = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!enemyCrossedLine)
|
||
{
|
||
return null; // 敌人尚未到达警戒线,保持在阵位待命
|
||
}
|
||
|
||
isHeroTeamEngaged = true; // 触发全员出击!
|
||
}
|
||
}
|
||
|
||
Vector3 myPos = transform.position;
|
||
|
||
GameObject nearestUnit = null;
|
||
float nearestUnitSqr = float.PositiveInfinity;
|
||
|
||
UnitTeam[] allTeams = FindObjectsOfType<UnitTeam>();
|
||
foreach (var unit in allTeams)
|
||
{
|
||
if (!myTeam.IsEnemy(unit.gameObject))
|
||
continue;
|
||
if (IsOwnHierarchy(unit.transform))
|
||
continue;
|
||
|
||
float sqrDist = (unit.transform.position - myPos).sqrMagnitude;
|
||
if (sqrDist > detectionRange * detectionRange)
|
||
continue;
|
||
|
||
float bias = ((uint)(gameObject.GetInstanceID() * 7919) ^ (uint)(unit.gameObject.GetInstanceID())) % 10u / 10f * targetSpreadBias;
|
||
float adjusted = sqrDist + bias;
|
||
if (adjusted < nearestUnitSqr)
|
||
{
|
||
nearestUnitSqr = adjusted;
|
||
nearestUnit = unit.gameObject;
|
||
}
|
||
}
|
||
|
||
if (nearestUnit != null)
|
||
return nearestUnit;
|
||
|
||
return null;
|
||
}
|
||
|
||
//判断是否进入可攻击距离
|
||
private bool IsWithinAttackReach(GameObject opponent)
|
||
{
|
||
if (opponent == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
float distance = Vector3.Distance(transform.position, opponent.transform.position);
|
||
float buffer = 0.2f;
|
||
|
||
// 建筑目标:中心在模型内部,用碰撞体最近点计算到建筑表面的精确距离,缓冲小让单位贴近
|
||
if (opponent.CompareTag("Building"))
|
||
{
|
||
buffer = 0.2f;
|
||
var col = opponent.GetComponentInChildren<Collider>();
|
||
if (col != null)
|
||
{
|
||
Vector3 closest = col.ClosestPoint(transform.position);
|
||
Vector3 flat = closest - transform.position;
|
||
flat.y = 0f; // 忽略高度差,只算水平距离
|
||
distance = flat.magnitude;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 非建筑目标:扣除双方的阻挡半径,计算贴边距离
|
||
float myRadius = navAgent != null ? navAgent.radius : 0.5f;
|
||
var targetAgent = opponent.GetComponentInParent<UnityEngine.AI.NavMeshAgent>();
|
||
float targetRadius = targetAgent != null ? targetAgent.radius : 0.5f;
|
||
|
||
distance = Mathf.Max(0f, distance - (myRadius + targetRadius));
|
||
}
|
||
|
||
// 攻击状态下增加容差,避免目标轻微移动导致状态频繁切换
|
||
if (state == State.Attack)
|
||
{
|
||
buffer += 0.5f;
|
||
}
|
||
|
||
// NavMeshAgent 会保持一定距离,所以增加额外的攻击范围缓冲
|
||
return distance <= attackRange + buffer;
|
||
}
|
||
|
||
//查找当前Animator中使用的攻击动画时长
|
||
private float GetAttackAnimationDuration()
|
||
{
|
||
if (animator == null || animator.runtimeAnimatorController == null)
|
||
{
|
||
return 0f;
|
||
}
|
||
|
||
var overrideController = animator.runtimeAnimatorController as AnimatorOverrideController;
|
||
if (overrideController != null)
|
||
{
|
||
var overrides = new List<KeyValuePair<AnimationClip, AnimationClip>>();
|
||
overrideController.GetOverrides(overrides);
|
||
|
||
foreach (var pair in overrides)
|
||
{
|
||
if (IsAttackAnimation(pair.Key))
|
||
{
|
||
return pair.Value != null ? pair.Value.length : pair.Key.length;
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach (var clip in animator.runtimeAnimatorController.animationClips)
|
||
{
|
||
if (IsAttackAnimation(clip))
|
||
{
|
||
return clip.length;
|
||
}
|
||
}
|
||
|
||
return 0f;
|
||
}
|
||
|
||
// 查找当前 Animator 中使用的技能动画时长。
|
||
private float GetSkillAnimationDuration()
|
||
{
|
||
if (animator == null || animator.runtimeAnimatorController == null)
|
||
{
|
||
return 0f;
|
||
}
|
||
|
||
var overrideController = animator.runtimeAnimatorController as AnimatorOverrideController;
|
||
if (overrideController != null)
|
||
{
|
||
var overrides = new List<KeyValuePair<AnimationClip, AnimationClip>>();
|
||
overrideController.GetOverrides(overrides);
|
||
|
||
foreach (var pair in overrides)
|
||
{
|
||
if (IsSkillAnimation(pair.Key))
|
||
{
|
||
return pair.Value != null ? pair.Value.length : pair.Key.length;
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach (var clip in animator.runtimeAnimatorController.animationClips)
|
||
{
|
||
if (IsSkillAnimation(clip))
|
||
{
|
||
return clip.length;
|
||
}
|
||
}
|
||
|
||
return 0f;
|
||
}
|
||
|
||
// 根据动画名称判断是否为技能动画。
|
||
private bool IsSkillAnimation(AnimationClip clip)
|
||
{
|
||
if (clip == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
string clipName = clip.name.ToLowerInvariant();
|
||
return clipName.Contains("skill")
|
||
|| clipName.Contains("技能");
|
||
}
|
||
|
||
//根据动画名称判断是否为攻击动画
|
||
private bool IsAttackAnimation(AnimationClip clip)
|
||
{
|
||
if (clip == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
string clipName = clip.name.ToLowerInvariant();
|
||
return clipName.Contains("attack")
|
||
|| clipName.Contains("攻击")
|
||
|| clipName.Contains("挥剑");
|
||
}
|
||
|
||
//设置攻击动画播放速度
|
||
private void SetAttackAnimationSpeed()
|
||
{
|
||
float animationDuration = GetAttackAnimationDuration();
|
||
if (animationDuration <= 0f || attackDuration <= 0f)
|
||
{
|
||
animator.speed = attackSpeedMultiplier;
|
||
return;
|
||
}
|
||
|
||
animator.speed = animationDuration / attackDuration * attackSpeedMultiplier;
|
||
}
|
||
|
||
//攻击
|
||
private void attack(GameObject friend)
|
||
{
|
||
target = friend;
|
||
|
||
// 攻击时暂停 NavMeshAgent,但保持 enabled 维持物理/寻路障碍排体
|
||
if (navAgent != null && navAgent.enabled && navAgent.isOnNavMesh)
|
||
{
|
||
navAgent.isStopped = true;
|
||
navAgent.velocity = Vector3.zero;
|
||
}
|
||
|
||
FaceTarget(friend);
|
||
|
||
if (TryReleaseSkill())
|
||
{
|
||
return;
|
||
}
|
||
|
||
//根据目标攻击时长加速或减速完整攻击动画
|
||
SetAttackAnimationSpeed();
|
||
|
||
animator.SetInteger("speed", 0);
|
||
animator.SetTrigger("attack");
|
||
state = State.Attack;
|
||
attackElapsedTime = 0f;
|
||
}
|
||
|
||
private void FaceTarget(GameObject opponent)
|
||
{
|
||
FaceTarget(opponent, modelFacingOffset);
|
||
}
|
||
|
||
private void FaceTarget(GameObject opponent, float facingOffset)
|
||
{
|
||
if (opponent == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Vector3 direction = opponent.transform.position - transform.position;
|
||
direction.y = 0f;
|
||
if (direction.sqrMagnitude <= 0.0001f)
|
||
{
|
||
return;
|
||
}
|
||
|
||
ApplyFacingRotation(direction, facingOffset);
|
||
}
|
||
|
||
public void FaceCurrentTarget()
|
||
{
|
||
FaceTarget(target);
|
||
}
|
||
|
||
public void FaceCurrentTarget(float facingOffset)
|
||
{
|
||
FaceTarget(target, facingOffset);
|
||
}
|
||
|
||
private bool TryReleaseSkill()
|
||
{
|
||
if (worldHealthBar == null
|
||
|| !worldHealthBar.IsEnergyFull
|
||
|| unitSkill == null
|
||
|| target == null
|
||
|| !target.activeInHierarchy)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
worldHealthBar.ResetEnergy();
|
||
state = State.Alert;
|
||
animator.SetInteger("speed", 0);
|
||
unitSkill.ReleaseSkill();
|
||
return true;
|
||
}
|
||
|
||
private void ApplyFacingRotation(Vector3 direction, float facingOffset)
|
||
{
|
||
direction.y = 0f;
|
||
if (direction.sqrMagnitude <= 0.0001f)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Quaternion targetRotation = Quaternion.LookRotation(direction, Vector3.up);
|
||
transform.rotation = Quaternion.Euler(
|
||
0f,
|
||
targetRotation.eulerAngles.y + facingOffset,
|
||
0f);
|
||
}
|
||
|
||
//由攻击动画中最合适的帧通过 Animation Event 调用。
|
||
public void OnAttackEffect()
|
||
{
|
||
if (IsStunned || state != State.Attack || target == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
switch (attackType)
|
||
{
|
||
case AttackType.MeleeDirect:
|
||
ApplyDirectDamage(target);
|
||
break;
|
||
case AttackType.RangedProjectile:
|
||
FireProjectile(target);
|
||
break;
|
||
case AttackType.MeleeArea:
|
||
case AttackType.RangedArc:
|
||
break;
|
||
}
|
||
}
|
||
|
||
private void ApplyDirectDamage(GameObject opponent)
|
||
{
|
||
float damage = attackPower;
|
||
if (critRate > 0f && Random.value < critRate)
|
||
{
|
||
damage *= critMultiplier;
|
||
Debug.Log($"{gameObject.name} 触发暴击!造成 {damage:F1} 伤害");
|
||
}
|
||
|
||
UnitBase targetUnit = opponent.GetComponentInParent<UnitBase>();
|
||
if (targetUnit != null && targetUnit.ReceiveDamage(damage, gameObject))
|
||
{
|
||
if (lifestealRate > 0f && worldHealthBar != null)
|
||
{
|
||
worldHealthBar.Heal(damage * lifestealRate);
|
||
}
|
||
RegisterAttackHit();
|
||
return;
|
||
}
|
||
|
||
WorldHealthBar targetHealthBar = opponent.GetComponentInParent<WorldHealthBar>();
|
||
if (targetHealthBar != null)
|
||
{
|
||
targetHealthBar.TakeDamage(damage, gameObject);
|
||
if (lifestealRate > 0f && worldHealthBar != null)
|
||
{
|
||
worldHealthBar.Heal(damage * lifestealRate);
|
||
}
|
||
RegisterAttackHit();
|
||
}
|
||
}
|
||
|
||
public void ApplySkillHit(float damageMultiplier, bool registerEnergy)
|
||
{
|
||
if (IsStunned || target == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
float damage = attackPower * Mathf.Max(0f, damageMultiplier);
|
||
UnitBase targetUnit = target.GetComponentInParent<UnitBase>();
|
||
bool hit = targetUnit != null && targetUnit.ReceiveDamage(damage, gameObject);
|
||
|
||
if (!hit)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (registerEnergy)
|
||
{
|
||
RegisterAttackHit();
|
||
}
|
||
}
|
||
|
||
public void ApplyAreaSkillDamage(float radius, float damage, bool registerEnergy)
|
||
{
|
||
if (worldHealthBar == null || damage <= 0f)
|
||
{
|
||
return;
|
||
}
|
||
|
||
UnitTeam myTeam = GetComponent<UnitTeam>();
|
||
if (myTeam == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
float clampedRadius = Mathf.Max(0f, radius);
|
||
float radiusSquared = clampedRadius * clampedRadius;
|
||
|
||
// 查找所有带 UnitTeam 的对象(包括单位和召唤师)
|
||
UnitTeam[] allTeams = FindObjectsOfType<UnitTeam>();
|
||
HashSet<WorldHealthBar> hitHealthBars = new HashSet<WorldHealthBar>();
|
||
|
||
foreach (UnitTeam targetTeam in allTeams)
|
||
{
|
||
GameObject opponent = targetTeam.gameObject;
|
||
|
||
if (opponent == null || IsOwnHierarchy(opponent.transform))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 检查是否是敌人
|
||
if (!myTeam.IsEnemy(opponent))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
Vector3 offset = opponent.transform.position - transform.position;
|
||
offset.y = 0f;
|
||
if (offset.sqrMagnitude > radiusSquared)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
UnitBase targetUnit = opponent.GetComponentInParent<UnitBase>();
|
||
WorldHealthBar healthBar = targetUnit != null
|
||
? targetUnit.GetComponent<WorldHealthBar>()
|
||
: opponent.GetComponentInParent<WorldHealthBar>();
|
||
|
||
if (healthBar == null || !hitHealthBars.Add(healthBar))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (targetUnit != null)
|
||
{
|
||
targetUnit.ReceiveDamage(damage, gameObject);
|
||
}
|
||
else
|
||
{
|
||
healthBar.TakeDamage(damage, gameObject);
|
||
}
|
||
}
|
||
|
||
if (registerEnergy)
|
||
{
|
||
RegisterAttackHit();
|
||
}
|
||
}
|
||
|
||
public void ApplyAreaStun(float radius, float duration)
|
||
{
|
||
if (duration <= 0f)
|
||
{
|
||
return;
|
||
}
|
||
|
||
UnitTeam myTeam = GetComponent<UnitTeam>();
|
||
if (myTeam == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
float clampedRadius = Mathf.Max(0f, radius);
|
||
float radiusSquared = clampedRadius * clampedRadius;
|
||
|
||
// 查找所有带 UnitTeam 的对象(包括单位和召唤师)
|
||
UnitTeam[] allTeams = FindObjectsOfType<UnitTeam>();
|
||
HashSet<UnitBase> hitUnits = new HashSet<UnitBase>();
|
||
HashSet<StatusEffectController> hitStatusEffects = new HashSet<StatusEffectController>();
|
||
|
||
foreach (UnitTeam targetTeam in allTeams)
|
||
{
|
||
GameObject opponent = targetTeam.gameObject;
|
||
|
||
if (opponent == null || IsOwnHierarchy(opponent.transform))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 检查是否是敌人
|
||
if (!myTeam.IsEnemy(opponent))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
Vector3 offset = opponent.transform.position - transform.position;
|
||
offset.y = 0f;
|
||
if (offset.sqrMagnitude > radiusSquared)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
UnitBase unit = opponent.GetComponentInParent<UnitBase>();
|
||
if (unit != null && hitUnits.Add(unit))
|
||
{
|
||
unit.ApplyStun(duration);
|
||
continue;
|
||
}
|
||
|
||
// 召唤师等特殊单位(有 StatusEffectController 但没有 UnitBase)
|
||
StatusEffectController statusEffect = opponent.GetComponentInParent<StatusEffectController>();
|
||
if (statusEffect != null && hitStatusEffects.Add(statusEffect))
|
||
{
|
||
statusEffect.ApplyStun(duration);
|
||
}
|
||
}
|
||
}
|
||
|
||
public void RegisterAttackHit()
|
||
{
|
||
if (IsStunned)
|
||
{
|
||
return;
|
||
}
|
||
|
||
AddAttackEnergy();
|
||
}
|
||
|
||
private void FireProjectile(GameObject opponent)
|
||
{
|
||
if (projectilePrefab == null || projectileSpawnPoint == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 计算朝向目标的旋转
|
||
Vector3 direction = (opponent.transform.position - projectileSpawnPoint.position).normalized;
|
||
Quaternion rotation = Quaternion.LookRotation(direction);
|
||
|
||
GameObject projectileObject = Instantiate(
|
||
projectilePrefab,
|
||
projectileSpawnPoint.position,
|
||
rotation); // 使用正确的旋转,让投射物面向目标
|
||
Projectile projectile = projectileObject.GetComponent<Projectile>();
|
||
if (projectile != null)
|
||
{
|
||
float damage = attackPower;
|
||
if (critRate > 0f && Random.value < critRate)
|
||
{
|
||
damage *= critMultiplier;
|
||
Debug.Log($"{gameObject.name} 远程攻击触发暴击!造成 {damage:F1} 伤害");
|
||
}
|
||
projectile.Initialize(opponent, damage, gameObject, projectileSpeed, projectileLifetime);
|
||
}
|
||
}
|
||
|
||
public void FireSkillProjectile(GameObject opponent)
|
||
{
|
||
FireSkillProjectile(opponent, 1f);
|
||
}
|
||
|
||
public void FireSkillProjectile(GameObject opponent, float damageMultiplier)
|
||
{
|
||
if (opponent == null || projectilePrefab == null || projectileSpawnPoint == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 计算朝向目标的旋转
|
||
Vector3 direction = (opponent.transform.position - projectileSpawnPoint.position).normalized;
|
||
Quaternion rotation = Quaternion.LookRotation(direction);
|
||
|
||
GameObject projectileObject = Instantiate(
|
||
projectilePrefab,
|
||
projectileSpawnPoint.position,
|
||
rotation); // 使用正确的旋转,让投射物面向目标
|
||
Projectile projectile = projectileObject.GetComponent<Projectile>();
|
||
if (projectile != null)
|
||
{
|
||
// 技能箭不再次积攒能量,避免技能触发递归。
|
||
projectile.Initialize(opponent, attackPower * Mathf.Max(0f, damageMultiplier), gameObject, projectileSpeed, projectileLifetime, false);
|
||
}
|
||
}
|
||
|
||
// 普通攻击命中后只积攒能量,大招在下一次普攻开始前释放。
|
||
private void AddAttackEnergy()
|
||
{
|
||
if (worldHealthBar == null || energyPerAttack <= 0f)
|
||
{
|
||
return;
|
||
}
|
||
|
||
float actualEnergy = energyPerAttack * skillChargeMultiplier;
|
||
worldHealthBar.AddEnergy(actualEnergy);
|
||
}
|
||
|
||
private float GetSkillAnimationSpeed(float targetDuration)
|
||
{
|
||
float animationDuration = GetSkillAnimationDuration();
|
||
if (animationDuration <= 0f || targetDuration <= 0f)
|
||
{
|
||
return 1f;
|
||
}
|
||
|
||
return animationDuration / targetDuration;
|
||
}
|
||
|
||
public void PlaySkillAnimation(System.Action onFinished, float targetDuration = 0f)
|
||
{
|
||
if (isPlayingSkill)
|
||
{
|
||
return;
|
||
}
|
||
|
||
skillFinishedCallback = onFinished;
|
||
isPlayingSkill = true;
|
||
skillElapsedTime = 0f;
|
||
float animationDuration = GetSkillAnimationDuration();
|
||
skillTimeout = targetDuration > 0f
|
||
? targetDuration + 0.5f
|
||
: Mathf.Max(animationDuration + 0.5f, 2f);
|
||
state = State.Skill;
|
||
|
||
if (navAgent != null && navAgent.enabled && navAgent.isOnNavMesh)
|
||
{
|
||
navAgent.isStopped = true;
|
||
navAgent.velocity = Vector3.zero;
|
||
}
|
||
|
||
animator.speed = GetSkillAnimationSpeed(targetDuration);
|
||
animator.SetInteger("speed", 0);
|
||
animator.SetTrigger("skill");
|
||
}
|
||
|
||
public void MultiplyAttackSpeed(float multiplier, float limit)
|
||
{
|
||
attackSpeedMultiplier = Mathf.Min(
|
||
attackSpeedMultiplier * multiplier,
|
||
limit);
|
||
animator.speed = attackSpeedMultiplier;
|
||
}
|
||
|
||
private void CheckSkillAnimationFinished()
|
||
{
|
||
if (!isPlayingSkill)
|
||
{
|
||
return;
|
||
}
|
||
|
||
skillElapsedTime += Time.deltaTime;
|
||
AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);
|
||
bool animationFinished = stateInfo.IsName("skill") && stateInfo.normalizedTime >= 1f;
|
||
bool timedOut = skillElapsedTime >= skillTimeout;
|
||
if (!animationFinished && !timedOut)
|
||
{
|
||
return;
|
||
}
|
||
|
||
isPlayingSkill = false;
|
||
skillElapsedTime = 0f;
|
||
skillTimeout = 0f;
|
||
state = State.Alert;
|
||
animator.speed = attackSpeedMultiplier;
|
||
skillFinishedCallback?.Invoke();
|
||
skillFinishedCallback = null;
|
||
}
|
||
|
||
|
||
// 根据当前攻击动画的播放进度自动结束攻击,不依赖 Animation Event。
|
||
private void CheckAttackAnimationFinished()
|
||
{
|
||
float animationDuration = attackDuration > 0f
|
||
? attackDuration
|
||
: GetAttackAnimationDuration();
|
||
|
||
if (animationDuration > 0f && attackElapsedTime >= animationDuration)
|
||
{
|
||
FinishAttack();
|
||
return;
|
||
}
|
||
|
||
if (!IsPlayingAttackAnimation())
|
||
{
|
||
return;
|
||
}
|
||
|
||
AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);
|
||
if (stateInfo.normalizedTime >= 1f)
|
||
{
|
||
FinishAttack();
|
||
}
|
||
}
|
||
|
||
private bool IsPlayingAttackAnimation()
|
||
{
|
||
AnimatorClipInfo[] clipInfos = animator.GetCurrentAnimatorClipInfo(0);
|
||
foreach (AnimatorClipInfo clipInfo in clipInfos)
|
||
{
|
||
if (IsAttackAnimation(clipInfo.clip))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private void FinishAttack()
|
||
{
|
||
//攻击动画结束后恢复正常 Animator 播放速度,避免攻击动画调速污染走路动画。
|
||
animator.speed = 1f;
|
||
|
||
//攻击结束后只继续攻击当前锁定目标,不在这里重新寻敌。
|
||
if (IsTargetValid(target) && IsWithinAttackReach(target))
|
||
{
|
||
FaceTarget(target);
|
||
attack(target);
|
||
return;
|
||
}
|
||
|
||
//目标失效或离开攻击范围,解除锁定,下一帧重新寻敌。
|
||
target = null;
|
||
state = State.Alert;
|
||
animator.SetInteger("speed", 0);
|
||
}
|
||
|
||
//角色移动,持续到目标点
|
||
void move(Vector3 movement)
|
||
{
|
||
movement.y = 0;
|
||
|
||
if (navAgent == null) return;
|
||
|
||
// 重新启用 NavMeshAgent(攻击后可能被禁用)
|
||
if (!navAgent.enabled)
|
||
{
|
||
navAgent.enabled = true;
|
||
}
|
||
|
||
if (!navAgent.isOnNavMesh) return;
|
||
|
||
// 追击时用正常速度和正常停靠距离(可能刚从回位状态切回来)
|
||
navAgent.speed = speed;
|
||
navAgent.stoppingDistance = NormalStoppingDistance;
|
||
|
||
// 确保没有被停止
|
||
if (navAgent.isStopped)
|
||
{
|
||
navAgent.isStopped = false;
|
||
}
|
||
|
||
// 限制寻路更新频率(性能优化)
|
||
if (Time.time >= nextPathUpdateTime)
|
||
{
|
||
// 设置目标位置(相对于当前位置)
|
||
Vector3 targetPosition = transform.position + movement.normalized * 10f;
|
||
navAgent.SetDestination(targetPosition);
|
||
nextPathUpdateTime = Time.time + PathUpdateInterval;
|
||
}
|
||
|
||
// 根据速度设置动画
|
||
if (navAgent.velocity.magnitude > 0.1f)
|
||
{
|
||
animator.SetInteger("speed", 1);
|
||
// 走路时始终保持正常速度,不受 AttackSpeedMultiplier 影响
|
||
if (animator != null)
|
||
{
|
||
animator.speed = 1f;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
animator.SetInteger("speed", 0);
|
||
}
|
||
}
|
||
|
||
// 在 Scene 视图中标记箭的发射位置和方向,便于调整空物体。
|
||
private void OnDrawGizmos()
|
||
{
|
||
if (projectileSpawnPoint == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Vector3 position = projectileSpawnPoint.position;
|
||
Gizmos.color = Color.cyan;
|
||
Gizmos.DrawWireSphere(position, 0.08f);
|
||
Gizmos.DrawLine(position, position + projectileSpawnPoint.forward * 0.4f);
|
||
|
||
Gizmos.color = Color.green;
|
||
Gizmos.DrawLine(position, position + projectileSpawnPoint.up * 0.15f);
|
||
}
|
||
}
|
||
|