Files
daluandou/Assets/Scripts/角色脚本/UnitBase.cs
T
huadexuan 23c559d9f7 ```
feat(gameplay): 提升单位检测范围并添加法师属性

- 将所有单位的检测范围从默认值提升至100
- 为法师预制体添加护盾、等级等新属性
- 为法师添加暴击、吸血、伤害减免等战斗属性
- 更换法师弹丸预设并调整相关参数
- 添加锚点交战Z线控制功能
- 更新玩家数据中配备的召唤师技能
- 移除多余的纹理配置文件
```
2026-10-07 23:59:17 +08:00

1445 lines
44 KiB
C#
Raw 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 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);
}
}