Files
daluandou/Assets/Script/UnitBase.cs
T
huadexuan f06a1d8f92 ```
feat(gameplay): 实现召唤师对决模式核心功能

- 新增召唤师对决管理模式,支持点卡片直接召唤单位
- 添加召唤师点地移动控制,移动时不攻击、站定时自动攻击
- 实现召唤师模式开关,单位不再自动追击远处敌人
- 更新卡牌按钮逻辑,对决模式下按能量消耗直接召唤
- 修改单位基础行为,支持移动中不攻击的召唤师模式
- 更新核心玩法文档,从建筑生产模式转向召唤师对决模式
- 添加召唤师对决测试场景,整合新的战斗逻辑
```
2026-09-16 18:16:13 +08:00

1120 lines
32 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;
//重力加速度
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;
[Header("召唤师模式")]
// 召唤师:不自动追击远处敌人,只在站定后攻击射程内的目标;移动由 SummonerClickMovement 控制。
public bool summonerMode = false;
//召唤师模式下允许自动攻击的开关(移动时由操控脚本关闭,站定时开启)。
private bool summonerAttackEnabled = true;
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)
{
// 召唤师模式:不主动追击,站定且攻击开关开启时才攻击射程内目标。
if (summonerMode)
{
if (!IsWithinAttackReach(target) || !summonerAttackEnabled)
{
state = State.Alert;
// 动画由 SummonerClickMovement 控制,这里不设置
applyGravity();
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);
}
applyGravity();
}
public GameObject Target => target;
public string OpponentTag => GetOpponentTag();
// 召唤师模式下控制"移动中不攻击":移动时 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;
}
//根据自身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);
}
}