Files
daluandou/Assets/Script/UnitBase.cs
T
huadexuan fe3877dedb 根据提供的code differences信息,由于没有具体的代码变更内容,我将生成一个通用的commit message模板:
```
docs(readme): 更新文档说明

- 添加项目使用指南
- 完善API接口描述
- 修正错误的配置示例
```

注意:由于您提供的code differences部分为空,以上是一个示例模板。请提供具体的代码变更信息以便生成更准确的commit message。
2026-08-26 23:36:30 +08:00

499 lines
13 KiB
C#

using System.Collections.Generic;
using UnityEngine;
public class UnitBase : MonoBehaviour
{
//移动速度
public float speed = 5f;
//重力加速度
public float gravity = -20f;
//活动范围
public float range = 500f;
//兼容旧场景配置,目标搜索不再受此范围限制
public float detectRange = 200f;
//攻击范围
public float attackRange = 5f;
//基础攻击力,实际伤害结算逻辑后续接入
[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 float verticalVelocity;
//当前攻击开始时间,用于在没有结束 Event 时释放攻击状态。
private float attackStartTime;
//状态枚举
public enum State
{
//警戒巡逻
Alert,
//追踪状态
Track,
//攻击状态
Attack
}
// Start is called before the first frame update
void Start()
{
//初始化
animator = GetComponent<Animator>();
controller = GetComponent<CharacterController>();
worldHealthBar = GetComponent<WorldHealthBar>();
//记录初始坐标
startPos = transform.position;
randomTargetPos();
}
// Update is called once per frame
void Update()
{
//攻击状态下等待Animator报告攻击动画播放完成。
if (state == State.Attack)
{
animator.SetInteger("speed", 0);
applyGravity();
CheckAttackAnimationFinished();
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();
}
//根据自身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;
}
//根据动画名称判断是否为攻击动画
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 = 1f;
return;
}
animator.speed = animationDuration / attackDuration;
}
//攻击
private void attack(GameObject friend)
{
target = friend;
//根据目标攻击时长加速或减速完整攻击动画
SetAttackAnimationSpeed();
animator.SetInteger("speed", 0);
animator.SetTrigger("attack");
state = State.Attack;
attackStartTime = Time.time;
}
//由攻击动画中最合适的帧通过 Animation Event 调用。
public void OnAttackEffect()
{
if (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 RegisterAttackHit()
{
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);
}
}
// 普通攻击命中后积攒能量,满能量时清空能量并调用技能占位方法。
private void AddAttackEnergy()
{
if (worldHealthBar == null || energyPerAttack <= 0f)
{
return;
}
if (!worldHealthBar.AddEnergy(energyPerAttack))
{
return;
}
worldHealthBar.ResetEnergy();
ReleaseSkill();
}
// 技能占位方法,后续由具体单位脚本重写。
protected virtual void ReleaseSkill()
{
}
// 根据当前攻击动画的播放进度自动结束攻击,不依赖 Animation Event。
private void CheckAttackAnimationFinished()
{
float animationDuration = attackDuration > 0f
? attackDuration
: GetAttackAnimationDuration();
if (animationDuration > 0f && Time.time - attackStartTime >= 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;
//攻击动画结束后,目标仍在攻击范围内则立即进行下一次攻击。
target = FindNearestOpponent();
if (target != null && IsWithinAttackReach(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);
//改变角色朝向目标点,只改变y轴
transform.rotation = Quaternion.LookRotation(movement, Vector3.up);
transform.rotation = Quaternion.Euler(0, transform.rotation.eulerAngles.y, 0);
}
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);
}
//随机目标点
private void randomTargetPos()
{
//以startPos为中心,在水平圆形范围内随机一个点
Vector2 randomOffset = Random.insideUnitCircle * range;
targetPos = startPos + new Vector3(randomOffset.x, 0, randomOffset.y);
}
}