Files
daluandou/Assets/Script/UnitBase.cs
T
huadexuan eb7b424efd ```
feat(projectile): 更新法师火球材质和粒子效果

- 更换火球拖尾和核心材质的着色器为透明渲染模式
- 添加GRABPASS禁用以优化渲染性能
- 调整粒子系统的生命周期、速度和尺寸参数
- 启用粒子系统跟随变换移动功能
- 为火球预制体添加胶囊碰撞体用于触发检测

fix(unit): 优化攻击距离判断逻辑

- 在攻击状态下增加1.0f的距离容差
- 避免目标轻微移动导致攻击状态频繁切换
- 提升战斗体验的稳定性

refactor(character): 调整法师角色缩放比例

- 将法师预制体的缩放从100倍调整为正常比例(1,1,1)
- 更新攻击伤害值从默认值改为90
- 修改攻击持续时间从2秒改为2.4秒
- 切换投射物预设为新的火球预制体
```
2026-09-22 22:38:10 +08:00

1163 lines
34 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
{
//移动速度
public float speed = 5f;
[Header("寻敌视野")]
[Tooltip("检测范围内优先攻击敌方战斗单位;范围外则搜索敌方建筑物")]
public float detectionRange = 25f;
//攻击范围
public float attackRange = 5f;
//模型自身前方相对逻辑前方的水平修正角度。
[Header("模型朝向修正")]
public float modelFacingOffset = 0f;
//基础攻击力,实际伤害结算逻辑后续接入
[Min(0f)]
public float attackPower = 10f;
//完整攻击动作时长,单位为秒;设置为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;
//角色状态
private State state = State.Alert;
//攻击目标
public GameObject target;
//出生使命:出生时快照当时己方标志指向的建筑,之后标志变化不影响本单位(支持分兵)。
private GameObject missionTarget;
// 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;
//状态枚举
public enum State
{
//警戒巡逻
Alert,
//追踪状态
Track,
//攻击状态
Attack,
//技能状态
Skill
}
// Start is called before the first frame update
public void InitializeFromPlayerData(float health, float attack, float moveSpeed)
{
attackPower = attack;
speed = moveSpeed;
if (worldHealthBar == null)
{
worldHealthBar = GetComponent<WorldHealthBar>();
}
if (worldHealthBar != null)
{
worldHealthBar.maxHealth = health;
worldHealthBar.currentHealth = health;
}
}
void Start()
{
//初始化
animator = GetComponent<Animator>();
navAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
worldHealthBar = GetComponent<WorldHealthBar>();
statusEffectController = EnsureStatusEffectController();
unitSkill = GetComponent<UnitSkill>();
// 如果有 NavMeshAgent,配置它
if (navAgent != null)
{
navAgent.speed = speed;
navAgent.angularSpeed = 120f;
navAgent.acceleration = 8f;
navAgent.stoppingDistance = attackRange * 0.95f; // 接近攻击范围边缘时停止
navAgent.autoBraking = true;
// NavMesh 使用烘焙平面的高度,不能根据动画帧中的 Renderer 包围盒推算 baseOffset。
navAgent.baseOffset = 0f;
}
// 根运动作用于所有动画:走路摇摆、攻击前冲由 OnAnimatorMove 统一协调
animator.applyRootMotion = true;
//出生使命:快照己方标志当前指向的建筑(之后玩家切换标志不影响本单位)
var myTeam = GetComponent<UnitTeam>();
if (myTeam != null && AttackTargetController.Instance != null)
{
missionTarget = AttackTargetController.Instance.GetTeamTarget(myTeam.teamId);
}
}
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,彻底停止移动
if (navAgent != null && navAgent.enabled)
{
navAgent.enabled = false;
}
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();
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;
// 召唤师模式下动画由 SummonerClickMovement 控制(移动时 speed=1,站定后 speed=0)
if (!summonerMode)
{
animator.SetInteger("speed", 0);
}
}
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)
{
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;
}
healthBar.TakeDamage(ModifyIncomingDamage(damage, attacker), attacker);
return true;
}
//根运动协调(applyRootMotion=true 时由动画系统每帧回调):
//走路(agent 驱动):只叠加动画的侧向摇摆和转身差量,前向分量丢弃(防双倍移速);
//攻击/技能(agent 已禁用):root motion 全量生效(前冲、后撤、带位移的攻击动作)。
private void OnAnimatorMove()
{
if (animator == null) return;
Vector3 delta = animator.deltaPosition;
delta.y = 0f;
Quaternion yawDelta = Quaternion.Euler(0f, animator.deltaRotation.eulerAngles.y, 0f);
if (navAgent != null && navAgent.enabled && navAgent.isOnNavMesh)
{
// 移动中:侧向分量(垂直于朝向)保留 = 大摇大摆的摇摆;同步 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()
{
}
//目标决策:周围有敌人打敌人(detectionRange 内),否则直奔出生使命建筑(快照,标志变化不影响)。
private GameObject FindTarget()
{
// Phase 1: detectionRange 内的敌方战斗单位
GameObject enemy = FindNearestEnemyUnit();
if (enemy != null) return enemy;
// Phase 2: 出生使命建筑;只有仍属于敌方的使命建筑才继续攻击。
if (IsTargetValid(missionTarget))
{
return missionTarget;
}
// 使命建筑已被己方占领、失活或销毁时,改攻敌方主城堡。
var myTeam = GetComponent<UnitTeam>();
if (myTeam != null && AttackTargetController.Instance != null)
{
GameObject castle = AttackTargetController.Instance.GetEnemyCastleOfTeam(myTeam.teamId);
missionTarget = castle;
return castle;
}
return null;
}
//判断建筑是否我方阵营(含主城堡和已占领资源建筑)
private bool IsFriendlyBuilding(GameObject building)
{
var myTeam = GetComponent<UnitTeam>();
var buildingTeam = building.GetComponent<UnitTeam>();
return myTeam != null && buildingTeam != null && buildingTeam.teamId == myTeam.teamId;
}
//查找 detectionRange 内最近的敌方战斗单位
private GameObject FindNearestEnemyUnit()
{
var myTeam = GetComponent<UnitTeam>();
if (myTeam == null)
{
Debug.LogWarning($"{gameObject.name} 缺少 UnitTeam 组件,无法查找敌人");
return null;
}
Vector3 myPos = transform.position;
// Phase 1: detectionRange 内的敌方战斗单位
GameObject nearestUnit = null;
float nearestUnitSqr = float.PositiveInfinity;
UnitTeam[] allTeams = FindObjectsOfType<UnitTeam>();
foreach (var unit in allTeams)
{
if (unit.gameObject == gameObject || !myTeam.IsEnemy(unit.gameObject))
continue;
if (unit.transform.root == transform.root)
continue;
// 跳过建筑物(它们有 Building Tag),只找战斗单位
if (unit.CompareTag("Building"))
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 = 1.5f;
// 建筑目标:中心在模型内部,用碰撞体最近点计算到建筑表面的精确距离,缓冲小让单位贴近
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;
}
}
// 攻击状态下增加容差,避免目标轻微移动导致状态频繁切换
if (state == State.Attack)
{
buffer += 1.0f;
}
// 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(彻底停止移动)
if (navAgent != null && navAgent.enabled)
{
navAgent.enabled = false;
}
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)
{
UnitBase targetUnit = opponent.GetComponentInParent<UnitBase>();
if (targetUnit != null && targetUnit.ReceiveDamage(attackPower, gameObject))
{
RegisterAttackHit();
return;
}
CrystalMineCapture targetCrystalMine = opponent.GetComponentInParent<CrystalMineCapture>();
if (targetCrystalMine != null && targetCrystalMine.ReceiveCaptureDamage(attackPower, gameObject))
{
RegisterAttackHit();
return;
}
BuildingHealth targetBuilding = opponent.GetComponentInParent<BuildingHealth>();
if (targetBuilding != null && targetBuilding.ReceiveDamage(attackPower, gameObject))
{
RegisterAttackHit();
return;
}
// 召唤师等特殊单位(有 WorldHealthBar 但没有 UnitBase)
WorldHealthBar targetHealthBar = opponent.GetComponentInParent<WorldHealthBar>();
if (targetHealthBar != null)
{
targetHealthBar.TakeDamage(attackPower, gameObject);
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)
{
CrystalMineCapture targetCrystalMine = target.GetComponentInParent<CrystalMineCapture>();
hit = targetCrystalMine != null && targetCrystalMine.ReceiveCaptureDamage(damage, gameObject);
}
if (!hit)
{
BuildingHealth targetBuilding = target.GetComponentInParent<BuildingHealth>();
hit = targetBuilding != null && targetBuilding.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 || opponent == gameObject || opponent.transform.root == transform.root)
{
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>();
BuildingHealth targetBuilding = opponent.GetComponentInParent<BuildingHealth>();
WorldHealthBar healthBar = null;
if (targetUnit != null)
{
healthBar = targetUnit.GetComponent<WorldHealthBar>();
}
else if (targetBuilding != null)
{
healthBar = targetBuilding.GetComponent<WorldHealthBar>();
}
else
{
// 召唤师等特殊单位(有 WorldHealthBar 但没有 UnitBase)
healthBar = opponent.GetComponentInParent<WorldHealthBar>();
}
if (healthBar == null || !hitHealthBars.Add(healthBar))
{
continue;
}
if (targetUnit != null)
{
targetUnit.ReceiveDamage(damage, gameObject);
}
else if (targetBuilding != null)
{
targetBuilding.ReceiveDamage(damage, gameObject);
}
else if (healthBar != null)
{
// 召唤师等特殊单位
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>();
foreach (UnitTeam targetTeam in allTeams)
{
GameObject opponent = targetTeam.gameObject;
if (opponent == null || opponent == gameObject || opponent.transform.root == transform.root)
{
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)
{
statusEffect.ApplyStun(duration);
}
}
}
public void RegisterAttackHit()
{
if (IsStunned)
{
return;
}
AddAttackEnergy();
}
private void FireProjectile(GameObject opponent)
{
if (projectilePrefab == null || projectileSpawnPoint == null)
{
return;
}
GameObject projectileObject = Instantiate(
projectilePrefab,
projectileSpawnPoint.position,
Quaternion.identity);
Projectile projectile = projectileObject.GetComponent<Projectile>();
if (projectile != null)
{
projectile.Initialize(opponent, attackPower, 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;
}
GameObject projectileObject = Instantiate(
projectilePrefab,
projectileSpawnPoint.position,
Quaternion.identity);
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;
}
worldHealthBar.AddEnergy(energyPerAttack);
}
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;
state = State.Skill;
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()
{
AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);
if (!stateInfo.IsName("skill") || stateInfo.normalizedTime < 1f)
{
return;
}
isPlayingSkill = false;
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.speed = attackSpeedMultiplier;
//攻击结束后只继续攻击当前锁定目标,不在这里重新寻敌。
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;
// 确保没有被停止
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);
}
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);
}
}