feat(gameplay): 更新单位属性和材质配置

- 调整多个单位的基础生命值和攻击力数值平衡
- 修改CoreBattleGround材质的纹理缩放比例
- 更新地砖材质颜色为白色
- 优化纹理资源设置,调整最大纹理尺寸并添加PlayableAds平台支持
- 修复弓箭手技能目标查找逻辑,改用统一的敌人检测方法
- 为水晶矿添加中立阵营设置和光球视觉效果同步
- 调整预制体中的全局缩放参数
- 为狼人和石巨人添加NavMeshAgent组件支持
- 优化水晶矿血条显示位置计算方式
```
This commit is contained in:
huadexuan committed 2026-09-20 21:52:31 +08:00
1 parent 0e7de5f885
commit 53d95802e5
448 files changed
+133333 -472

No files matched your search

+165 -211
View File
@@ -8,14 +8,9 @@ public class UnitBase : MonoBehaviour
//移动速度
public float speed = 5f;
//重力加速度
public float gravity = -20f;
//活动范围
public float range = 500f;
//兼容旧场景配置,目标搜索不再受此范围限制
public float detectRange = 200f;
[Header("寻敌视野")]
[Tooltip("检测范围内优先攻击敌方战斗单位;范围外则搜索敌方建筑物")]
public float detectionRange = 25f;
//攻击范围
public float attackRange = 5f;
@@ -24,10 +19,6 @@ public class UnitBase : MonoBehaviour
[Header("模型朝向修正")]
public float modelFacingOffset = 0f;
//是否保留动画根运动;需要攻击动作带动整体躯干时开启。
[Header("动画根运动")]
public bool useRootMotion = false;
//基础攻击力,实际伤害结算逻辑后续接入
[Min(0f)]
public float attackPower = 10f;
@@ -68,8 +59,6 @@ public class UnitBase : MonoBehaviour
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("召唤师模式")]
@@ -79,19 +68,14 @@ public class UnitBase : MonoBehaviour
//召唤师模式下允许自动攻击的开关(移动时由操控脚本关闭,站定时开启)。
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;
//出生使命:出生时快照当时己方标志指向的建筑,之后标志变化不影响本单位(支持分兵)。
private GameObject missionTarget;
// NavMesh 寻路更新间隔(优化性能)
private float nextPathUpdateTime = 0f;
@@ -100,10 +84,7 @@ public class UnitBase : MonoBehaviour
//动画控制器
private Animator animator;
// 角色控制器
private CharacterController controller;
// NavMesh 寻路代理(如果有的话优先使用)
// NavMesh 寻路代理(所有单位统一走 NavMesh 寻路)
private UnityEngine.AI.NavMeshAgent navAgent;
// 自身的血条组件,同时负责显示技能能量条。
@@ -112,9 +93,6 @@ public class UnitBase : MonoBehaviour
// 通用状态效果组件。
private StatusEffectController statusEffectController;
//垂直速度
private float verticalVelocity;
// 攻击动画的有效经过时间;眩晕期间不增加。
private float attackElapsedTime;
@@ -167,13 +145,12 @@ public class UnitBase : MonoBehaviour
{
//初始化
animator = GetComponent<Animator>();
controller = GetComponent<CharacterController>();
navAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
worldHealthBar = GetComponent<WorldHealthBar>();
statusEffectController = EnsureStatusEffectController();
unitSkill = GetComponent<UnitSkill>();
// 如果有 NavMeshAgent,配置它并禁用 CharacterController
// 如果有 NavMeshAgent,配置它
if (navAgent != null)
{
navAgent.speed = speed;
@@ -181,19 +158,20 @@ public class UnitBase : MonoBehaviour
navAgent.acceleration = 8f;
navAgent.stoppingDistance = attackRange * 0.95f; // 接近攻击范围边缘时停止
navAgent.autoBraking = true;
// 禁用 CharacterController,避免冲突
if (controller != null)
{
controller.enabled = false;
}
// NavMesh 使用烘焙平面的高度,不能根据动画帧中的 Renderer 包围盒推算 baseOffset。
navAgent.baseOffset = 0f;
}
// 大多数角色由 CharacterController 位移;特殊攻击动作可保留动画根运动。
animator.applyRootMotion = useRootMotion;
//记录初始坐标
startPos = transform.position;
randomTargetPos();
// 根运动作用于所有动画:走路摇摆、攻击前冲由 OnAnimatorMove 统一协调
animator.applyRootMotion = true;
//出生使命:快照己方标志当前指向的建筑(之后玩家切换标志不影响本单位)
var myTeam = GetComponent<UnitTeam>();
if (myTeam != null && AttackTargetController.Instance != null)
{
missionTarget = AttackTargetController.Instance.GetTeamTarget(myTeam.teamId);
}
}
private StatusEffectController EnsureStatusEffectController()
@@ -218,7 +196,6 @@ public class UnitBase : MonoBehaviour
if (statusEffectController != null && statusEffectController.IsStunned)
{
animator.SetInteger("speed", 0);
applyGravity();
return;
}
@@ -233,7 +210,6 @@ public class UnitBase : MonoBehaviour
animator.SetInteger("speed", 0);
attackElapsedTime += Time.deltaTime;
applyGravity();
CheckAttackAnimationFinished();
return;
}
@@ -241,13 +217,12 @@ public class UnitBase : MonoBehaviour
if (isPlayingSkill)
{
animator.SetInteger("speed", 0);
applyGravity();
CheckSkillAnimationFinished();
return;
}
//查找场景中距离自己最近的敌对目标
target = FindNearestOpponent();
//目标决策:周围有敌人打敌人,否则直奔全局攻击标志建筑(默认敌方主城堡)
target = FindTarget();
if (target != null)
{
// 召唤师模式:不主动追击,站定且攻击开关开启时才攻击射程内目标。
@@ -257,7 +232,6 @@ public class UnitBase : MonoBehaviour
{
state = State.Alert;
// 动画由 SummonerClickMovement 控制,这里不设置
applyGravity();
return;
}
attack(target);
@@ -273,6 +247,21 @@ public class UnitBase : MonoBehaviour
}
else
{
//到达使命建筑发现是我方的(增援/占领完成):本单位使命转向敌方主城堡,不影响全局标志
if (target.CompareTag("Building") && IsFriendlyBuilding(target))
{
var myTeam = GetComponent<UnitTeam>();
GameObject castle = AttackTargetController.Instance != null && myTeam != null
? AttackTargetController.Instance.GetEnemyCastleOfTeam(myTeam.teamId)
: null;
if (castle != null)
{
missionTarget = castle; // 使命完成,改打主城堡(单位自身,一次性的)
target = castle;
move(castle.transform.position - transform.position);
return;
}
}
attack(target);
}
@@ -286,12 +275,14 @@ public class UnitBase : MonoBehaviour
{
animator.SetInteger("speed", 0);
}
applyGravity();
}
public GameObject Target => target;
public string OpponentTag => GetOpponentTag();
public GameObject FindNearestEnemyForSkill()
{
return FindNearestEnemyUnit();
}
// 召唤师模式下控制"移动中不攻击":移动时 false,站定后 true。
public void SetSummonerAttackEnabled(bool enabled)
@@ -339,17 +330,78 @@ public class UnitBase : MonoBehaviour
return true;
}
//根据自身Tag找到敌对Tag
private string GetOpponentTag()
//根运动协调(applyRootMotion=true 时由动画系统每帧回调):
//走路(agent 驱动):只叠加动画的侧向摇摆和转身差量,前向分量丢弃(防双倍移速);
//攻击/技能(agent 已禁用):root motion 全量生效(前冲、后撤、带位移的攻击动作)。
private void OnAnimatorMove()
{
// 已弃用:改用阵营 ID 系统,不再依赖 Tag 判断敌我
// 保留此方法是为了兼容旧代码,但不应再被调用
Debug.LogWarning("GetOpponentTag() 已弃用,请使用阵营 ID 系统");
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 (missionTarget != null && missionTarget.activeInHierarchy)
{
return missionTarget;
}
var myTeam = GetComponent<UnitTeam>();
if (myTeam != null && AttackTargetController.Instance != null)
{
return AttackTargetController.Instance.GetEnemyCastleOfTeam(myTeam.teamId);
}
return null;
}
//查找距离自己最近的敌对目标(使用阵营 ID 判断)
private GameObject FindNearestOpponent()
//判断建筑是否我方阵营(含主城堡和已占领资源建筑)
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)
@@ -358,38 +410,40 @@ public class UnitBase : MonoBehaviour
return null;
}
GameObject nearestOpponent = null;
float nearestSqrDistance = float.PositiveInfinity;
Vector3 myPos = transform.position;
// 查找所有 UnitTeam 组件(不再依赖 Tag)
UnitTeam[] allUnits = FindObjectsOfType<UnitTeam>();
// Phase 1: detectionRange 内的敌方战斗单位
GameObject nearestUnit = null;
float nearestUnitSqr = float.PositiveInfinity;
foreach (var unit in allUnits)
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 sqrDistance = (unit.transform.position - transform.position).sqrMagnitude;
//加入确定性分散偏移:让不同单位偏好不同目标,避免全部涌向同一个敌人
float bias = ((uint)(gameObject.GetInstanceID() * 7919) ^ (uint)(unit.gameObject.GetInstanceID())) % 10u / 10f * targetSpreadBias;
float adjustedSqrDistance = sqrDistance + bias;
if (adjustedSqrDistance < nearestSqrDistance)
float adjusted = sqrDist + bias;
if (adjusted < nearestUnitSqr)
{
nearestSqrDistance = adjustedSqrDistance;
nearestOpponent = unit.gameObject;
nearestUnitSqr = adjusted;
nearestUnit = unit.gameObject;
}
}
return nearestOpponent;
if (nearestUnit != null)
return nearestUnit;
return null;
}
//判断是否进入可攻击距离
@@ -400,22 +454,25 @@ public class UnitBase : MonoBehaviour
return false;
}
// NavMeshAgent 会保持一定距离,所以增加额外的攻击范围缓冲
// 原来的逻辑:selfRadius + opponentRadius + attackRange
// 现在:直接使用 attackRange,并增加 1.5 米的缓冲,让单位在更远距离就能攻击
float reachDistance = attackRange + 1.5f;
return Vector3.Distance(transform.position, opponent.transform.position) <= reachDistance;
}
float distance = Vector3.Distance(transform.position, opponent.transform.position);
float buffer = 1.5f;
private float GetHorizontalCollisionRadius(CharacterController characterController)
{
if (characterController == null)
// 建筑目标:中心在模型内部,用碰撞体最近点计算到建筑表面的精确距离,缓冲小让单位贴近
if (opponent.CompareTag("Building"))
{
return 0f;
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;
}
}
Bounds bounds = characterController.bounds;
return Mathf.Max(bounds.extents.x, bounds.extents.z);
// NavMeshAgent 会保持一定距离,所以增加额外的攻击范围缓冲
return distance <= attackRange + buffer;
}
//查找当前Animator中使用的攻击动画时长
@@ -1008,7 +1065,7 @@ public class UnitBase : MonoBehaviour
animator.speed = attackSpeedMultiplier;
//攻击动画结束后,目标仍在攻击范围内则立即进行下一次攻击。
target = FindNearestOpponent();
target = FindTarget();
if (target != null && IsWithinAttackReach(target))
{
// 攻击结束后可能已切换到新的近距离目标,先更新朝向再开始下一次攻击。
@@ -1020,141 +1077,46 @@ public class UnitBase : MonoBehaviour
//目标离开攻击范围,回到待机逻辑。
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 (navAgent == null) return;
// 限制寻路更新频率(性能优化)
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;
}
// 重新启用 NavMeshAgent(攻击后可能被禁用)
if (!navAgent.enabled)
{
navAgent.enabled = true;
}
// 否则使用 CharacterController(旧方式)
//友军分离力:与过近的同阵营单位互相推开,避免扎堆
Vector3 separation = ComputeSeparation();
if (separation.sqrMagnitude > 0.0001f)
if (!navAgent.isOnNavMesh) return;
// 确保没有被停止
if (navAgent.isStopped)
{
movement += separation * separationWeight;
movement.y = 0;
navAgent.isStopped = false;
}
if (movement == Vector3.zero)
// 限制寻路更新频率(性能优化)
if (Time.time >= nextPathUpdateTime)
{
animator.SetInteger("speed", 0);
applyGravity();
return;
// 设置目标位置(相对于当前位置)
Vector3 targetPosition = transform.position + movement.normalized * 10f;
navAgent.SetDestination(targetPosition);
nextPathUpdateTime = Time.time + PathUpdateInterval;
}
//改变动画控制器的参数
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)
// 根据速度设置动画
if (navAgent.velocity.magnitude > 0.1f)
{
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;
animator.SetInteger("speed", 1);
}
else
{
verticalVelocity += gravity * Time.deltaTime;
}
return verticalVelocity;
}
private void applyGravity()
{
// 只在 CharacterController 启用时应用重力
if (controller != null && controller.enabled)
{
controller.Move(Vector3.up * getVerticalVelocity() * Time.deltaTime);
animator.SetInteger("speed", 0);
}
}
@@ -1174,13 +1136,5 @@ public class UnitBase : MonoBehaviour
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);
}
}