Files
daluandou/Assets/Script/UnitBase.cs
T
huadexuan 1cdb7c3910 ```
feat(buildings): 添加金矿建筑功能并优化建筑健康系统

- 为兵营和金矿添加伤害数字显示功能,包括持续时间、上升高度等参数
- 将金矿脚本重构为GoldMine组件,实现定时产出金币机制
- 为所有单位添加NavMeshAgent组件以支持导航寻路
- 为弓箭手、法师等单位添加分离半径和目标扩散参数
- 为建筑添加碰撞体和标签设置
- 修复当前能量字段命名规范(currentEnergy -> _currentEnergy)
- 调整金矿健康值从100提升至500
- 统一伤害数字显示相关属性配置
```
2026-09-15 21:13:44 +08:00

1085 lines
30 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;
[Header("阵型分散")]
[Min(0f)] public float separationRadius = 1.5f;
[Min(0f)] public float separationWeight = 3f;
[Min(0f)] public float targetSpreadBias = 6f;
private static Collider[] s_separationBuffer = new Collider[30];
//角色状态
private State state = State.Alert;
//攻击目标
public GameObject target;
//当前目标点
private Vector3 targetPos;
//初始位置坐标
private Vector3 startPos;
// NavMesh 寻路更新间隔(优化性能)
private float nextPathUpdateTime = 0f;
private const float PathUpdateInterval = 0.5f; // 每 0.5 秒更新一次路径
//动画控制器
private Animator animator;
// 角色控制器
private CharacterController controller;
// NavMesh 寻路代理(如果有的话优先使用)
private UnityEngine.AI.NavMeshAgent navAgent;
// 自身的血条组件,同时负责显示技能能量条。
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
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>();
controller = GetComponent<CharacterController>();
navAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
worldHealthBar = GetComponent<WorldHealthBar>();
statusEffectController = EnsureStatusEffectController();
unitSkill = GetComponent<UnitSkill>();
// 如果有 NavMeshAgent,配置它并禁用 CharacterController
if (navAgent != null)
{
navAgent.speed = speed;
navAgent.angularSpeed = 120f;
navAgent.acceleration = 8f;
navAgent.stoppingDistance = attackRange * 0.95f; // 接近攻击范围边缘时停止
navAgent.autoBraking = true;
// 禁用 CharacterController,避免冲突
if (controller != null)
{
controller.enabled = false;
}
}
// 大多数角色由 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)
{
// 攻击时直接禁用 NavMeshAgent,彻底停止移动
if (navAgent != null && navAgent.enabled)
{
navAgent.enabled = false;
}
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);
}
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;
}
//根据自身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;
//加入确定性分散偏移:让不同单位偏好不同目标,避免全部涌向同一个敌人
float bias = ((uint)(gameObject.GetInstanceID() * 7919) ^ (uint)(opponent.GetInstanceID())) % 10u / 10f * targetSpreadBias;
float adjustedSqrDistance = sqrDistance + bias;
if (adjustedSqrDistance < nearestSqrDistance)
{
nearestSqrDistance = adjustedSqrDistance;
nearestOpponent = opponent;
}
}
return nearestOpponent;
}
//判断是否进入可攻击距离
private bool IsWithinAttackReach(GameObject opponent)
{
if (opponent == null)
{
return false;
}
// NavMeshAgent 会保持一定距离,所以增加额外的攻击范围缓冲
// 原来的逻辑:selfRadius + opponentRadius + attackRange
// 现在:直接使用 attackRange,并增加 1.5 米的缓冲,让单位在更远距离就能攻击
float reachDistance = attackRange + 1.5f;
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;
// 攻击时直接禁用 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;
}
BuildingHealth targetBuilding = opponent.GetComponentInParent<BuildingHealth>();
if (targetBuilding != null && targetBuilding.ReceiveDamage(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)
{
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;
}
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;
}
UnitBase targetUnit = opponent.GetComponentInParent<UnitBase>();
BuildingHealth targetBuilding = opponent.GetComponentInParent<BuildingHealth>();
WorldHealthBar healthBar = targetUnit != null
? targetUnit.GetComponent<WorldHealthBar>()
: targetBuilding != null ? targetBuilding.GetComponent<WorldHealthBar>() : null;
if (healthBar == null || !hitHealthBars.Add(healthBar))
{
continue;
}
if (targetUnit != null)
{
targetUnit.ReceiveDamage(damage, gameObject);
}
else
{
targetBuilding.ReceiveDamage(damage, gameObject);
}
}
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;
// 如果有 NavMeshAgent,使用它来移动
if (navAgent != null)
{
// 重新启用 NavMeshAgent(攻击后可能被禁用)
if (!navAgent.enabled)
{
navAgent.enabled = true;
}
if (navAgent.isOnNavMesh)
{
// 确保没有被停止
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);
}
return;
}
}
// 否则使用 CharacterController(旧方式)
//友军分离力:与过近的同阵营单位互相推开,避免扎堆
Vector3 separation = ComputeSeparation();
if (separation.sqrMagnitude > 0.0001f)
{
movement += separation * separationWeight;
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 Vector3 ComputeSeparation()
{
if (separationRadius <= 0f)
{
return Vector3.zero;
}
int hitCount = Physics.OverlapSphereNonAlloc(transform.position, separationRadius, s_separationBuffer);
Vector3 separation = Vector3.zero;
for (int i = 0; i < hitCount; i++)
{
Collider other = s_separationBuffer[i];
if (other == null || other.gameObject == gameObject)
{
continue;
}
//只推开同阵营单位,敌人间的碰撞由CharacterController自然阻挡
if (!other.CompareTag(gameObject.tag))
{
continue;
}
Vector3 diff = transform.position - other.transform.position;
diff.y = 0f;
float distance = diff.magnitude;
if (distance > 0.01f && distance < separationRadius)
{
separation += diff / distance * (separationRadius - distance);
}
}
return separation;
}
private float getVerticalVelocity()
{
if (controller.isGrounded && verticalVelocity < 0)
{
verticalVelocity = -2f;
}
else
{
verticalVelocity += gravity * Time.deltaTime;
}
return verticalVelocity;
}
private void applyGravity()
{
// 只在 CharacterController 启用时应用重力
if (controller != null && controller.enabled)
{
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);
}
}