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(); } if (navAgent != null) { navAgent.speed = moveSpeed; } if (worldHealthBar == null) { worldHealthBar = GetComponent(); } if (worldHealthBar != null) { worldHealthBar.maxHealth = health; worldHealthBar.currentHealth = health; worldHealthBar.SetLevel(level); } } void Start() { //初始化 animator = GetComponent(); navAgent = GetComponent(); worldHealthBar = GetComponent(); statusEffectController = EnsureStatusEffectController(); unitSkill = GetComponent(); 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($"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(); } if (statusEffectController == null) { statusEffectController = gameObject.AddComponent(); } 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(); if (healthBar != null && healthBar.IsDead) { return false; } UnitTeam myTeam = GetComponent(); UnitTeam targetTeam = candidate.GetComponentInParent(); 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(); 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; } /// 某个世界坐标是否离锚点超过了指定距离(只算水平距离)。 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; } /// 追击/接敌时用的停靠距离(贴着攻击范围边缘停下)。 private float NormalStoppingDistance => Mathf.Max(0f, attackRange * 0.95f); /// /// 重置锚点位置到当前位置(用于复活等场景) /// 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; // 确保走路动画速度正常 } } /// 回位结束或无法寻路时,把速度和动画恢复成正常站定状态。 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); } /// 检查全场是否还有存活的敌方单位。 private bool HasAnyEnemyOnField() { var myTeam = GetComponent(); if (myTeam == null) return false; UnitTeam[] allTeams = FindObjectsOfType(); 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(); if (healthBar != null && healthBar.IsDead) continue; return true; } return false; } //查找 detectionRange 内最近的敌方战斗单位 private GameObject FindNearestEnemyUnit() { var myTeam = GetComponent(); 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(); 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(); 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(); 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(); 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(); 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>(); 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>(); 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(); if (targetUnit != null && targetUnit.ReceiveDamage(damage, gameObject)) { if (lifestealRate > 0f && worldHealthBar != null) { worldHealthBar.Heal(damage * lifestealRate); } RegisterAttackHit(); return; } WorldHealthBar targetHealthBar = opponent.GetComponentInParent(); 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(); 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(); if (myTeam == null) { return; } float clampedRadius = Mathf.Max(0f, radius); float radiusSquared = clampedRadius * clampedRadius; // 查找所有带 UnitTeam 的对象(包括单位和召唤师) UnitTeam[] allTeams = FindObjectsOfType(); HashSet hitHealthBars = new HashSet(); 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(); WorldHealthBar healthBar = targetUnit != null ? targetUnit.GetComponent() : opponent.GetComponentInParent(); 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(); if (myTeam == null) { return; } float clampedRadius = Mathf.Max(0f, radius); float radiusSquared = clampedRadius * clampedRadius; // 查找所有带 UnitTeam 的对象(包括单位和召唤师) UnitTeam[] allTeams = FindObjectsOfType(); HashSet hitUnits = new HashSet(); HashSet hitStatusEffects = new HashSet(); 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(); if (unit != null && hitUnits.Add(unit)) { unit.ApplyStun(duration); continue; } // 召唤师等特殊单位(有 StatusEffectController 但没有 UnitBase) StatusEffectController statusEffect = opponent.GetComponentInParent(); 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(); 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(); 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); } }