``` chore(config): 更新配置文件 移除不必要的配置项并优化项目设置 ``` 注意:由于未提供具体的代码差异信息,此提交消息为占位符。 请提供实际的代码变更详情以生成准确的提交消息。
881 lines
24 KiB
C#
881 lines
24 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
[RequireComponent(typeof(StatusEffectController))]
|
|
public class UnitBase : MonoBehaviour
|
|
{
|
|
|
|
//移动速度
|
|
public float speed = 5f;
|
|
|
|
//重力加速度
|
|
public float gravity = -20f;
|
|
|
|
//活动范围
|
|
public float range = 500f;
|
|
|
|
//兼容旧场景配置,目标搜索不再受此范围限制
|
|
public float detectRange = 200f;
|
|
|
|
//攻击范围
|
|
public float attackRange = 5f;
|
|
|
|
//模型自身前方相对逻辑前方的水平修正角度。
|
|
[Header("模型朝向修正")]
|
|
public float modelFacingOffset = 0f;
|
|
|
|
//是否保留动画根运动;需要攻击动作带动整体躯干时开启。
|
|
[Header("动画根运动")]
|
|
public bool useRootMotion = false;
|
|
|
|
//基础攻击力,实际伤害结算逻辑后续接入
|
|
[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;
|
|
|
|
//角色状态
|
|
private State state = State.Alert;
|
|
|
|
//攻击目标
|
|
public GameObject target;
|
|
|
|
//当前目标点
|
|
private Vector3 targetPos;
|
|
|
|
//初始位置坐标
|
|
private Vector3 startPos;
|
|
|
|
//动画控制器
|
|
private Animator animator;
|
|
|
|
// 角色控制器
|
|
private CharacterController controller;
|
|
|
|
// 自身的血条组件,同时负责显示技能能量条。
|
|
private WorldHealthBar worldHealthBar;
|
|
|
|
// 通用状态效果组件。
|
|
private StatusEffectController statusEffectController;
|
|
|
|
//垂直速度
|
|
private float verticalVelocity;
|
|
|
|
// 攻击动画的有效经过时间;眩晕期间不增加。
|
|
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
|
|
void Start()
|
|
{
|
|
//初始化
|
|
animator = GetComponent<Animator>();
|
|
controller = GetComponent<CharacterController>();
|
|
worldHealthBar = GetComponent<WorldHealthBar>();
|
|
statusEffectController = EnsureStatusEffectController();
|
|
unitSkill = GetComponent<UnitSkill>();
|
|
|
|
// 大多数角色由 CharacterController 位移;特殊攻击动作可保留动画根运动。
|
|
animator.applyRootMotion = useRootMotion;
|
|
//记录初始坐标
|
|
startPos = transform.position;
|
|
randomTargetPos();
|
|
}
|
|
|
|
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);
|
|
applyGravity();
|
|
return;
|
|
}
|
|
|
|
//攻击状态下等待Animator报告攻击动画播放完成。
|
|
if (state == State.Attack)
|
|
{
|
|
animator.SetInteger("speed", 0);
|
|
attackElapsedTime += Time.deltaTime;
|
|
applyGravity();
|
|
CheckAttackAnimationFinished();
|
|
return;
|
|
}
|
|
|
|
if (isPlayingSkill)
|
|
{
|
|
animator.SetInteger("speed", 0);
|
|
applyGravity();
|
|
CheckSkillAnimationFinished();
|
|
return;
|
|
}
|
|
|
|
//查找场景中距离自己最近的敌对目标
|
|
target = FindNearestOpponent();
|
|
if (target != null)
|
|
{
|
|
state = State.Track;
|
|
|
|
//目标在攻击范围外则朝目标移动,否则开始攻击
|
|
if (!IsWithinAttackReach(target))
|
|
{
|
|
move(target.transform.position - transform.position);
|
|
}
|
|
else
|
|
{
|
|
attack(target);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//场上没有敌人时进入待机,不再巡逻移动
|
|
state = State.Alert;
|
|
animator.SetInteger("speed", 0);
|
|
applyGravity();
|
|
}
|
|
|
|
public GameObject Target => target;
|
|
|
|
public string OpponentTag => GetOpponentTag();
|
|
|
|
public float AttackSpeedMultiplier => attackSpeedMultiplier;
|
|
|
|
public bool IsStunned => statusEffectController != null && statusEffectController.IsStunned;
|
|
|
|
public void ApplyStun(float duration)
|
|
{
|
|
EnsureStatusEffectController().ApplyStun(duration);
|
|
}
|
|
|
|
//根据自身Tag找到敌对Tag
|
|
private string GetOpponentTag()
|
|
{
|
|
if (CompareTag("Friend"))
|
|
{
|
|
return "Enemy";
|
|
}
|
|
|
|
if (CompareTag("Enemy"))
|
|
{
|
|
return "Friend";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
//查找距离自己最近的敌对目标
|
|
private GameObject FindNearestOpponent()
|
|
{
|
|
string opponentTag = GetOpponentTag();
|
|
if (string.IsNullOrEmpty(opponentTag))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
GameObject nearestOpponent = null;
|
|
float nearestSqrDistance = float.PositiveInfinity;
|
|
GameObject[] opponents = GameObject.FindGameObjectsWithTag(opponentTag);
|
|
|
|
foreach (var opponent in opponents)
|
|
{
|
|
//排除自己以及自己所在层级中的对象
|
|
if (opponent == gameObject || opponent.transform.root == transform.root)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
float sqrDistance = (opponent.transform.position - transform.position).sqrMagnitude;
|
|
if (sqrDistance < nearestSqrDistance)
|
|
{
|
|
nearestSqrDistance = sqrDistance;
|
|
nearestOpponent = opponent;
|
|
}
|
|
}
|
|
|
|
return nearestOpponent;
|
|
}
|
|
|
|
//判断是否进入可攻击距离
|
|
private bool IsWithinAttackReach(GameObject opponent)
|
|
{
|
|
if (opponent == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// 将双方控制器半径计入判定,避免碰撞体互相阻挡时无法进入攻击距离。
|
|
float selfRadius = GetHorizontalCollisionRadius(controller);
|
|
CharacterController opponentController = opponent.GetComponent<CharacterController>();
|
|
float opponentRadius = GetHorizontalCollisionRadius(opponentController);
|
|
float reachDistance = selfRadius + opponentRadius + attackRange;
|
|
return Vector3.Distance(transform.position, opponent.transform.position) <= reachDistance;
|
|
}
|
|
|
|
private float GetHorizontalCollisionRadius(CharacterController characterController)
|
|
{
|
|
if (characterController == null)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
Bounds bounds = characterController.bounds;
|
|
return Mathf.Max(bounds.extents.x, bounds.extents.z);
|
|
}
|
|
|
|
//查找当前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;
|
|
|
|
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)
|
|
{
|
|
WorldHealthBar healthBar = opponent.GetComponentInParent<WorldHealthBar>();
|
|
if (healthBar == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
healthBar.TakeDamage(attackPower);
|
|
RegisterAttackHit();
|
|
}
|
|
|
|
public void ApplySkillHit(float damageMultiplier, bool registerEnergy)
|
|
{
|
|
if (IsStunned || target == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
WorldHealthBar healthBar = target.GetComponentInParent<WorldHealthBar>();
|
|
if (healthBar == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
healthBar.TakeDamage(attackPower * Mathf.Max(0f, damageMultiplier));
|
|
if (registerEnergy)
|
|
{
|
|
RegisterAttackHit();
|
|
}
|
|
}
|
|
|
|
public void ApplyAreaSkillDamage(float radius, float damage, bool registerEnergy)
|
|
{
|
|
if (worldHealthBar == null || damage <= 0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string opponentTag = GetOpponentTag();
|
|
if (string.IsNullOrEmpty(opponentTag))
|
|
{
|
|
return;
|
|
}
|
|
|
|
float clampedRadius = Mathf.Max(0f, radius);
|
|
float radiusSquared = clampedRadius * clampedRadius;
|
|
GameObject[] opponents = GameObject.FindGameObjectsWithTag(opponentTag);
|
|
HashSet<WorldHealthBar> hitHealthBars = new HashSet<WorldHealthBar>();
|
|
|
|
foreach (GameObject opponent in opponents)
|
|
{
|
|
if (opponent == null || opponent == gameObject || opponent.transform.root == transform.root)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Vector3 offset = opponent.transform.position - transform.position;
|
|
offset.y = 0f;
|
|
if (offset.sqrMagnitude > radiusSquared)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
WorldHealthBar healthBar = opponent.GetComponentInParent<WorldHealthBar>();
|
|
if (healthBar == null || !hitHealthBars.Add(healthBar))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
healthBar.TakeDamage(damage);
|
|
}
|
|
|
|
if (registerEnergy)
|
|
{
|
|
RegisterAttackHit();
|
|
}
|
|
}
|
|
|
|
public void ApplyAreaStun(float radius, float duration)
|
|
{
|
|
if (duration <= 0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string opponentTag = GetOpponentTag();
|
|
if (string.IsNullOrEmpty(opponentTag))
|
|
{
|
|
return;
|
|
}
|
|
|
|
float clampedRadius = Mathf.Max(0f, radius);
|
|
float radiusSquared = clampedRadius * clampedRadius;
|
|
GameObject[] opponents = GameObject.FindGameObjectsWithTag(opponentTag);
|
|
HashSet<UnitBase> hitUnits = new HashSet<UnitBase>();
|
|
|
|
foreach (GameObject opponent in opponents)
|
|
{
|
|
if (opponent == null || opponent == gameObject || opponent.transform.root == transform.root)
|
|
{
|
|
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))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
unit.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,
|
|
projectileSpawnPoint.rotation);
|
|
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,
|
|
projectileSpawnPoint.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;
|
|
}
|
|
|
|
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;
|
|
|
|
//攻击动画结束后,目标仍在攻击范围内则立即进行下一次攻击。
|
|
target = FindNearestOpponent();
|
|
if (target != null && IsWithinAttackReach(target))
|
|
{
|
|
// 攻击结束后可能已切换到新的近距离目标,先更新朝向再开始下一次攻击。
|
|
FaceTarget(target);
|
|
attack(target);
|
|
return;
|
|
}
|
|
|
|
//目标离开攻击范围,回到待机逻辑。
|
|
state = State.Alert;
|
|
animator.SetInteger("speed", 0);
|
|
applyGravity();
|
|
}
|
|
|
|
|
|
|
|
//角色移动,持续到目标点
|
|
void move(Vector3 movement)
|
|
{
|
|
movement.y = 0;
|
|
if (movement == Vector3.zero)
|
|
{
|
|
animator.SetInteger("speed", 0);
|
|
applyGravity();
|
|
return;
|
|
}
|
|
|
|
//改变动画控制器的参数
|
|
animator.SetInteger("speed", 1);
|
|
//向目标点移动,同时应用垂直重力
|
|
Vector3 velocity = movement.normalized * speed;
|
|
velocity.y = getVerticalVelocity();
|
|
controller.Move(velocity * Time.deltaTime);
|
|
//移动时模型正面朝向移动方向;模型朝向修正只用于攻击动作。
|
|
Quaternion movementRotation = Quaternion.LookRotation(movement, Vector3.up);
|
|
transform.rotation = Quaternion.Euler(0f, movementRotation.eulerAngles.y, 0f);
|
|
}
|
|
|
|
|
|
private float getVerticalVelocity()
|
|
{
|
|
if (controller.isGrounded && verticalVelocity < 0)
|
|
{
|
|
verticalVelocity = -2f;
|
|
}
|
|
else
|
|
{
|
|
verticalVelocity += gravity * Time.deltaTime;
|
|
}
|
|
|
|
return verticalVelocity;
|
|
}
|
|
|
|
private void applyGravity()
|
|
{
|
|
controller.Move(Vector3.up * getVerticalVelocity() * Time.deltaTime);
|
|
}
|
|
|
|
// 在 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);
|
|
}
|
|
|
|
//随机目标点
|
|
private void randomTargetPos()
|
|
{
|
|
//以startPos为中心,在水平圆形范围内随机一个点
|
|
Vector2 randomOffset = Random.insideUnitCircle * range;
|
|
targetPos = startPos + new Vector3(randomOffset.x, 0, randomOffset.y);
|
|
}
|
|
}
|
|
|