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

+9 -5
View File
@@ -19,18 +19,22 @@ public class ArcherHunterSkill : UnitSkill
private GameObject FindNearbyOpponent(GameObject mainTarget)
{
string opponentTag = unit.OpponentTag;
if (string.IsNullOrEmpty(opponentTag))
UnitTeam myTeam = unit.GetComponent<UnitTeam>();
if (myTeam == null)
{
return null;
}
GameObject nearbyOpponent = null;
float nearestSqrDistance = splitShotRadius * splitShotRadius;
GameObject[] opponents = GameObject.FindGameObjectsWithTag(opponentTag);
foreach (GameObject opponent in opponents)
foreach (UnitTeam opponentTeam in GameObject.FindObjectsOfType<UnitTeam>())
{
if (opponent == mainTarget || opponent == unit.gameObject || opponent.transform.root == unit.transform.root)
GameObject opponent = opponentTeam.gameObject;
if (opponent == null
|| opponent == mainTarget
|| opponent == unit.gameObject
|| opponent.transform.root == unit.transform.root
|| !myTeam.IsEnemy(opponent))
{
continue;
}
+1 -25
View File
@@ -44,30 +44,6 @@ public class ArcherSkill : UnitSkill
private GameObject FindNearestOpponent()
{
string opponentTag = unit.OpponentTag;
if (string.IsNullOrEmpty(opponentTag))
{
return null;
}
GameObject nearestOpponent = null;
float nearestSqrDistance = float.PositiveInfinity;
GameObject[] opponents = GameObject.FindGameObjectsWithTag(opponentTag);
foreach (GameObject opponent in opponents)
{
if (opponent == null || opponent.transform.root == unit.transform.root)
{
continue;
}
float sqrDistance = (opponent.transform.position - unit.transform.position).sqrMagnitude;
if (sqrDistance < nearestSqrDistance)
{
nearestSqrDistance = sqrDistance;
nearestOpponent = opponent;
}
}
return nearestOpponent;
return unit.FindNearestEnemyForSkill();
}
}
+305
View File
@@ -0,0 +1,305 @@
using UnityEngine;
using UnityEngine.EventSystems;
/// <summary>
/// 建筑攻击标志控制器(双阵营):每个阵营各自一个攻击标志,默认指向敌方主城堡。
/// 本地玩家点击建筑切换自己阵营的标志;两方标志同时显示,指向同一建筑时并排错开。
/// 单位寻敌:周围有敌人打敌人,否则直奔己方标志建筑;到达后是我方阵营(增援完成)→ 己方标志切回主城堡。
/// </summary>
public class AttackTargetController : MonoBehaviour
{
public static AttackTargetController Instance { get; private set; }
[Header("蓝方(teamId=0)攻击目标")]
public GameObject blueTarget;
[Header("红方(teamId=1)攻击目标")]
public GameObject redTarget;
[Header("标志视觉(头顶箭头)")]
[Tooltip("标志 Prefab:拖入自定义图标替换默认箭头;留空时代码生成默认箭头")]
[SerializeField] private GameObject markerPrefab;
[Tooltip("标志相对建筑包围盒顶部的额外高度")]
[SerializeField] private float markerHeightOffset = 0.8f;
[Tooltip("双方标志指向同一建筑时的并排错开距离")]
[SerializeField] private float markerSideOffset = 1.1f;
[Tooltip("建筑子级下该名字的空物体 = 标志锚点(可选,编辑器可调位置)")]
[SerializeField] private string anchorName = "标志锚点";
private Transform blueMarker;
private Transform redMarker;
private Renderer[] blueMarkerRenderers;
private Renderer[] redMarkerRenderers;
private GameObject cachedBlueEnemyCastle; // teamId 0 的敌方主城堡(红色城堡)
private GameObject cachedRedEnemyCastle; // teamId 1 的敌方主城堡(蓝色城堡)
private float nextCastleRefreshTime;
private const float CastleRefreshInterval = 0.5f;
private Camera mainCamera;
private static readonly int TintColorId = Shader.PropertyToID("_TintColor");
private static readonly int ColorId = Shader.PropertyToID("_Color");
private static readonly Color BlueColor = new Color(0.2f, 0.55f, 1f, 0.95f);
private static readonly Color RedColor = new Color(1f, 0.2f, 0.15f, 0.95f);
private MaterialPropertyBlock propertyBlock;
private void Awake()
{
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
Instance = this;
}
private void Start()
{
mainCamera = Camera.main;
CreateMarkers();
blueTarget = GetEnemyCastleOfTeam(0);
redTarget = GetEnemyCastleOfTeam(1);
Debug.Log($"攻击标志初始: 蓝方→{blueTarget?.name ?? "未找到"} 红方→{redTarget?.name ?? "未找到"}");
}
private void Update()
{
// 主城堡引用失效时定期重查(场景加载时序容错)
if ((cachedBlueEnemyCastle == null || cachedRedEnemyCastle == null) && Time.time >= nextCastleRefreshTime)
{
nextCastleRefreshTime = Time.time + CastleRefreshInterval;
cachedBlueEnemyCastle = FindBuilding("红色城堡");
cachedRedEnemyCastle = FindBuilding("蓝色城堡");
if (blueTarget == null && cachedBlueEnemyCastle != null) blueTarget = cachedBlueEnemyCastle;
if (redTarget == null && cachedRedEnemyCastle != null) redTarget = cachedRedEnemyCastle;
}
HandleClick();
}
// 点击建筑:只切换本地玩家阵营的攻击标志
private void HandleClick()
{
if (mainCamera == null) mainCamera = Camera.main;
if (mainCamera == null) return;
bool clicked = Input.GetMouseButtonDown(0)
|| (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began);
if (!clicked) return;
// 点击落在 UI 上不处理
if (EventSystem.current != null)
{
if (Input.touchCount > 0)
{
if (EventSystem.current.IsPointerOverGameObject(Input.GetTouch(0).fingerId)) return;
}
else if (EventSystem.current.IsPointerOverGameObject()) return;
}
Vector2 screenPos = Input.touchCount > 0 ? (Vector2)Input.GetTouch(0).position : Input.mousePosition;
Ray ray = mainCamera.ScreenPointToRay(screenPos);
if (Physics.Raycast(ray, out RaycastHit hit, 500f))
{
Transform building = FindBuildingInParents(hit.collider.transform);
if (building == null) return;
int localTeam = GameManager.Instance != null ? GameManager.Instance.LocalPlayerTeamId : 0;
// 点自己方主城堡无效
if (building.name == GetTeamCastleName(localTeam)) return;
if (localTeam == 0)
{
blueTarget = building.gameObject;
}
else
{
redTarget = building.gameObject;
}
Debug.Log($"{(localTeam == 0 ? "蓝方" : "红方")}攻击标志 → {building.name}");
}
}
/// <summary>获取阵营当前的攻击目标(无效时兜底敌方主城堡)。单位出生时调用一次做使命快照。</summary>
public GameObject GetTeamTarget(int teamId)
{
GameObject t = teamId == 0 ? blueTarget : redTarget;
if (t != null && t.activeInHierarchy) return t;
return GetEnemyCastleOfTeam(teamId);
}
/// <summary>获取阵营的敌方主城堡(teamId 0→红色城堡,1→蓝色城堡),带缓存。</summary>
public GameObject GetEnemyCastleOfTeam(int teamId)
{
if (teamId == 0)
{
if (cachedBlueEnemyCastle == null || !cachedBlueEnemyCastle.activeInHierarchy)
{
cachedBlueEnemyCastle = FindBuilding("红色城堡");
}
return cachedBlueEnemyCastle;
}
if (cachedRedEnemyCastle == null || !cachedRedEnemyCastle.activeInHierarchy)
{
cachedRedEnemyCastle = FindBuilding("蓝色城堡");
}
return cachedRedEnemyCastle;
}
private static GameObject FindBuilding(string name)
{
var found = GameObject.Find(name);
return found != null && found.CompareTag("Building") ? found : null;
}
private static string GetTeamCastleName(int teamId)
{
return teamId == 0 ? "蓝色城堡" : "红色城堡";
}
private static Transform FindBuildingInParents(Transform t)
{
while (t != null)
{
if (t.CompareTag("Building")) return t;
t = t.parent;
}
return null;
}
// 双标志:位置/朝向/颜色(指向同一建筑时并排)
private void LateUpdate()
{
if (mainCamera == null) mainCamera = Camera.main;
bool sameBuilding = blueTarget != null && blueTarget == redTarget;
float blueSide = sameBuilding ? markerSideOffset : 0f;
float redSide = sameBuilding ? -markerSideOffset : 0f;
UpdateMarker(blueMarker, blueMarkerRenderers, blueTarget, BlueColor, blueSide);
UpdateMarker(redMarker, redMarkerRenderers, redTarget, RedColor, redSide);
}
private void UpdateMarker(Transform marker, Renderer[] renderers, GameObject target, Color color, float sideOffset)
{
if (marker == null) return;
if (target == null)
{
marker.gameObject.SetActive(false);
return;
}
marker.gameObject.SetActive(true);
// 位置:锚点优先(可选),否则建筑包围盒顶部 + 高度偏移
Vector3 pos;
Transform anchor = target.transform.Find(anchorName);
if (anchor != null)
{
pos = anchor.position;
}
else
{
Bounds bounds = CalculateTargetBounds(target);
pos = new Vector3(target.transform.position.x, bounds.max.y + markerHeightOffset, target.transform.position.z);
}
// 指向同一建筑时沿相机右方向并排错开
if (sideOffset != 0f && mainCamera != null)
{
pos += mainCamera.transform.right * sideOffset;
}
marker.position = pos;
// billboard:箭头尖朝下指着建筑
if (mainCamera != null)
{
Vector3 toCamera = mainCamera.transform.position - marker.position;
marker.rotation = Quaternion.LookRotation(toCamera) * Quaternion.Euler(90f, 0f, 0f);
}
// 颜色按标志所属阵营
if (renderers == null) return;
propertyBlock ??= new MaterialPropertyBlock();
foreach (Renderer rend in renderers)
{
if (rend == null) continue;
rend.GetPropertyBlock(propertyBlock);
propertyBlock.SetColor(TintColorId, color);
propertyBlock.SetColor(ColorId, color);
rend.SetPropertyBlock(propertyBlock);
}
}
private static Bounds CalculateTargetBounds(GameObject target)
{
var renderers = target.GetComponentsInChildren<Renderer>();
if (renderers.Length == 0) return new Bounds(target.transform.position, Vector3.one);
Bounds b = renderers[0].bounds;
for (int i = 1; i < renderers.Length; i++) b.Encapsulate(renderers[i].bounds);
return b;
}
private void CreateMarkers()
{
if (blueMarker != null && redMarker != null) return;
blueMarker = CreateMarker("蓝方攻击标志", BlueColor);
blueMarkerRenderers = blueMarker.GetComponentsInChildren<Renderer>();
redMarker = CreateMarker("红方攻击标志", RedColor);
redMarkerRenderers = redMarker.GetComponentsInChildren<Renderer>();
}
private Transform CreateMarker(string name, Color color)
{
GameObject markerGo;
// 优先用可替换的标志 Prefab(可在 Inspector 拖入自定义图标)
if (markerPrefab != null)
{
markerGo = Instantiate(markerPrefab, transform);
markerGo.name = name;
}
else
{
// 兜底:代码生成默认倒锥箭头(尖朝下指着建筑)
markerGo = new GameObject(name, typeof(MeshFilter), typeof(MeshRenderer));
markerGo.transform.SetParent(transform, false);
markerGo.GetComponent<MeshFilter>().sharedMesh = CreateArrowMesh();
var mat = new Material(Shader.Find("Legacy Shaders/Particles/Additive"));
mat.SetColor(TintColorId, color);
markerGo.GetComponent<Renderer>().sharedMaterial = mat;
}
return markerGo.transform;
}
// 4面锥体:尖朝下,高2,底半径0.7
private static Mesh CreateArrowMesh()
{
var mesh = new Mesh();
mesh.name = "AttackMarkerArrow";
Vector3 tip = new Vector3(0f, -1.2f, 0f);
Vector3[] baseQuad = {
new Vector3(-0.7f, 0.8f, -0.7f), new Vector3(0.7f, 0.8f, -0.7f),
new Vector3(0.7f, 0.8f, 0.7f), new Vector3(-0.7f, 0.8f, 0.7f)
};
var vertices = new System.Collections.Generic.List<Vector3>();
var normals = new System.Collections.Generic.List<Vector3>();
void AddFace(Vector3 a, Vector3 b, Vector3 c, Vector3 n)
{
vertices.AddRange(new[] { a, b, c });
normals.AddRange(new[] { n, n, n });
}
AddFace(baseQuad[0], baseQuad[1], tip, new Vector3(0f, 0f, -1f)); // 后
AddFace(baseQuad[1], baseQuad[2], tip, new Vector3(1f, 0f, 0f)); // 右
AddFace(baseQuad[2], baseQuad[3], tip, new Vector3(0f, 0f, 1f)); // 前
AddFace(baseQuad[3], baseQuad[0], tip, new Vector3(-1f, 0f, 0f)); // 左
mesh.SetVertices(vertices);
mesh.SetNormals(normals);
mesh.triangles = new int[] { 0,1,2, 3,4,5, 6,7,8, 9,10,11 };
mesh.RecalculateBounds();
return mesh;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: CHlLsXyuUXPweN4lxBlvd5LSZPyvxClExfV+1fA7aZcHqIyTo65nmKw=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+113
View File
@@ -0,0 +1,113 @@
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 拆家版固定八卡组、四张手牌轮转管理器。
/// </summary>
public class BattleDeckCycle : MonoBehaviour
{
public static BattleDeckCycle Instance { get; private set; }
[SerializeField] private Transform cardRoot;
[SerializeField] private int visibleSlotCount = 4;
[SerializeField] private string[] deckDataNames =
{
"人类士兵Data", "弓箭手Data", "盾卫Data", "精锐士兵Data",
"法师Data", "狂暴狼人Data", "石巨人Data", "弓箭猎人Data"
};
private readonly List<CharacterData> deck = new List<CharacterData>();
private readonly List<CharacterData> hand = new List<CharacterData>();
private readonly Queue<CharacterData> drawQueue = new Queue<CharacterData>();
private SummonCardButton[] buttons;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
BuildDeck();
}
private void Start()
{
if (cardRoot == null)
cardRoot = GameObject.Find("角色卡片")?.transform;
buttons = cardRoot != null
? cardRoot.GetComponentsInChildren<SummonCardButton>(true)
: FindObjectsOfType<SummonCardButton>(true);
System.Array.Sort(buttons, (a, b) => a.summonSlot.CompareTo(b.summonSlot));
HideExtraSlots();
hand.Clear();
drawQueue.Clear();
for (int i = 0; i < visibleSlotCount && i < deck.Count; i++)
hand.Add(deck[i]);
for (int i = visibleSlotCount; i < deck.Count; i++)
drawQueue.Enqueue(deck[i]);
RefreshAllCards();
}
public CharacterData GetCard(int slot)
{
int index = slot - 1;
return index >= 0 && index < hand.Count ? hand[index] : null;
}
public bool TryPlay(int slot)
{
CharacterData card = GetCard(slot);
if (card == null || card.unitPrefab == null || CastleSpawnManager.Instance == null)
return false;
if (!CastleSpawnManager.Instance.TrySpawn(card, 0))
return false;
int index = slot - 1;
CharacterData played = hand[index];
hand[index] = drawQueue.Dequeue();
drawQueue.Enqueue(played);
RefreshAllCards();
return true;
}
private void BuildDeck()
{
deck.Clear();
foreach (string dataName in deckDataNames)
{
CharacterData data = Resources.Load<CharacterData>("Data/" + dataName);
if (data == null)
{
Debug.LogWarning($"卡组缺少角色数据: Resources/Data/{dataName}.asset");
continue;
}
deck.Add(data);
}
}
private void HideExtraSlots()
{
foreach (var button in buttons)
{
if (button != null && button.summonSlot > visibleSlotCount)
button.gameObject.SetActive(false);
}
}
private void RefreshAllCards()
{
if (buttons == null) return;
foreach (var button in buttons)
{
if (button == null || button.summonSlot > visibleSlotCount) continue;
button.SetDeckCharacter(GetCard(button.summonSlot));
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: XXob4XilUXnOhMYzExpqM+4zrmqzWWfAaYPtwHjVN2gIEPjdfifecQI=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+7 -9
View File
@@ -13,13 +13,8 @@ public class CastleMageSkill : UnitSkill
public override void ReleaseSkill()
{
GameObject target = unit.Target;
if (target == null)
{
return;
}
string opponentTag = unit.OpponentTag;
if (string.IsNullOrEmpty(opponentTag))
UnitTeam myTeam = unit.GetComponent<UnitTeam>();
if (target == null || myTeam == null)
{
return;
}
@@ -28,9 +23,12 @@ public class CastleMageSkill : UnitSkill
float radiusSquared = explosionRadius * explosionRadius;
HashSet<UnitBase> hitUnits = new HashSet<UnitBase>();
foreach (GameObject opponent in GameObject.FindGameObjectsWithTag(opponentTag))
foreach (UnitTeam opponentTeam in GameObject.FindObjectsOfType<UnitTeam>())
{
if (opponent == null || opponent.transform.root == unit.transform.root)
GameObject opponent = opponentTeam.gameObject;
if (opponent == null
|| opponent.transform.root == unit.transform.root
|| !myTeam.IsEnemy(opponent))
{
continue;
}
+77
View File
@@ -0,0 +1,77 @@
using UnityEngine;
/// <summary>
/// 拆家版出兵管理:从城堡前方出兵点生成单位,消耗能量。
/// 替代旧的召唤师身边召唤逻辑。
/// </summary>
public class CastleSpawnManager : MonoBehaviour
{
public static CastleSpawnManager Instance { get; private set; }
[Header("出兵点(城堡前方)")]
public Transform blueSpawnPoint;
public Transform redSpawnPoint;
private void Awake()
{
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
Instance = this;
}
/// <summary>从指定阵营城堡出兵,消耗能量。teamId: 0=蓝, 1=红。</summary>
public bool TrySpawn(CharacterData data, int teamId)
{
if (data == null || data.unitPrefab == null) return false;
if (ManaSystem.Instance == null || !ManaSystem.Instance.TryConsumeMana(data.manaCost))
{
Debug.Log($"能量不足,需要 {data.manaCost}");
return false;
}
Transform spawnPoint = teamId == 0 ? blueSpawnPoint : redSpawnPoint;
if (spawnPoint == null)
{
Debug.LogError($"找不到 teamId={teamId} 的出兵点");
return false;
}
Vector3 direction = spawnPoint.forward;
direction.y = 0f;
if (direction.sqrMagnitude < 0.001f) direction = Vector3.forward;
direction.Normalize();
GameObject unit = Instantiate(
data.unitPrefab,
spawnPoint.position,
Quaternion.LookRotation(direction, Vector3.up));
// 设置阵营
UnitTeam unitTeam = unit.GetComponent<UnitTeam>();
if (unitTeam != null)
{
unitTeam.teamId = teamId;
unit.tag = teamId == 0 ? "Friend" : "Enemy";
}
// 初始化属性(默认 1 级)
UnitBase unitBase = unit.GetComponent<UnitBase>();
if (unitBase != null)
{
PlayerCharacterData playerData = new PlayerCharacterData("castle_spawn", 1, 0);
float hp = playerData.GetMaxHealth(data);
float atk = playerData.GetAttackPower(data);
float spd = playerData.GetMoveSpeed(data);
unitBase.InitializeFromPlayerData(hp, atk, spd);
}
Debug.Log($"城堡出兵: {data.characterName} teamId={teamId} pos={spawnPoint.position}");
return true;
}
public bool CanAfford(CharacterData data)
{
return data != null
&& ManaSystem.Instance != null
&& ManaSystem.Instance.CurrentManaInt >= data.manaCost;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: CHMXsyioV3xtkiJD2efA348D6pNVylSji1x9B3EkftlvQ+9oBm5CB9Y=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+61 -13
View File
@@ -7,15 +7,23 @@ public class CrystalMineCapture : MonoBehaviour
[Range(0f, 100f)] public float blueProgress = 50f;
[Min(0f)] public float capturePerDamage = 1f;
[Tooltip("是否自动添加中立阵营(teamId=2)。拆家版关闭以避免单位自动锁敌水晶矿")]
public bool autoSetNeutralTeam = true;
[Header("水晶矿光球")]
[SerializeField] private AdjustableGlowOrb glowOrb;
[Tooltip("混沌裂缝传送门(随机出兵点),随争夺进度同步变色,可留空")]
[SerializeField] private RiftPortalVisual riftVisual;
[SerializeField] private Color neutralColor = Color.white;
[SerializeField] private Color blueColor = new Color(0.1f, 0.65f, 1f, 1f);
[SerializeField] private Color redColor = new Color(1f, 0.12f, 0.08f, 1f);
[Header("世界血条")]
[Tooltip("血条相对模型渲染包围盒顶部的额外高度")]
[Min(0f)] public float heightOffset = 1.1f;
[Min(0.1f)] public float barWidth = 1.8f;
[Min(0.1f)] public float barWidth = 5f;
private float? barHeightCache;
private Canvas canvas;
private RectTransform barRect;
@@ -28,24 +36,41 @@ public class CrystalMineCapture : MonoBehaviour
private void Awake()
{
unitTeam = GetComponent<UnitTeam>();
if (unitTeam == null)
if (autoSetNeutralTeam)
{
unitTeam = gameObject.AddComponent<UnitTeam>();
unitTeam = GetComponent<UnitTeam>();
if (unitTeam == null)
{
unitTeam = gameObject.AddComponent<UnitTeam>();
}
unitTeam.teamId = 2;
}
else
{
unitTeam = GetComponent<UnitTeam>();
}
unitTeam.teamId = 2;
if (glowOrb == null)
{
glowOrb = GetComponentInChildren<AdjustableGlowOrb>(true);
}
if (riftVisual == null)
{
riftVisual = GetComponentInChildren<RiftPortalVisual>(true);
}
CreateCaptureBar();
RefreshVisuals();
}
private void Start()
{
// 强制重置光球颜色为初始状态
if (glowOrb != null)
{
glowOrb.SetGlowColor(neutralColor);
}
RefreshVisuals();
}
@@ -53,13 +78,8 @@ public class CrystalMineCapture : MonoBehaviour
{
if (barRect == null) return;
barRect.position = transform.position + Vector3.up * heightOffset;
Vector3 parentScale = transform.lossyScale;
float targetScale = barWidth / 100f;
barRect.localScale = new Vector3(
parentScale.x != 0f ? targetScale / parentScale.x : targetScale,
parentScale.y != 0f ? targetScale / parentScale.y : targetScale,
parentScale.z != 0f ? targetScale / parentScale.z : targetScale);
barRect.position = transform.position + Vector3.up * GetBarHeight();
barRect.localScale = Vector3.one * (barWidth / 100f);
Camera camera = Camera.main;
if (camera != null)
@@ -68,6 +88,27 @@ public class CrystalMineCapture : MonoBehaviour
}
}
// 血条高度 = 所有子 Renderer 包围盒顶部 + heightOffset。
// 水晶矿模型根节点是 100 倍缩放,高度必须按世界包围盒算,否则血条会埋进模型内部被遮挡。
private float GetBarHeight()
{
if (barHeightCache.HasValue) return barHeightCache.Value;
float top = 0f;
Renderer[] renderers = GetComponentsInChildren<Renderer>();
if (renderers.Length > 0)
{
Bounds bounds = renderers[0].bounds;
for (int i = 1; i < renderers.Length; i++)
{
bounds.Encapsulate(renderers[i].bounds);
}
top = bounds.max.y;
}
barHeightCache = top - transform.position.y + heightOffset;
return barHeightCache.Value;
}
public bool ReceiveCaptureDamage(float damage, GameObject attacker)
{
if (damage <= 0f || attacker == null) return false;
@@ -109,12 +150,19 @@ public class CrystalMineCapture : MonoBehaviour
Color color = blueProgress >= 100f ? blueColor : blueProgress <= 0f ? redColor : neutralColor;
glowOrb.SetGlowColor(color);
}
if (riftVisual != null)
{
Color riftColor = blueProgress >= 100f ? blueColor : blueProgress <= 0f ? redColor : neutralColor;
riftVisual.SetRiftColor(riftColor);
}
}
private void CreateCaptureBar()
{
GameObject root = new GameObject("CrystalCaptureBar", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler));
root.transform.SetParent(transform, false);
// 血条 Canvas 不要作为水晶矿的子对象,直接放在场景根目录避免被 100 倍缩放影响
root.transform.parent = null;
canvas = root.GetComponent<Canvas>();
canvas.renderMode = RenderMode.WorldSpace;
canvas.sortingOrder = 110;
+184
View File
@@ -0,0 +1,184 @@
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 红方城堡自动出兵 AI:独立能量、三路目标评估、低费试探与高费推进。
/// </summary>
public class EnemyCastleAI : MonoBehaviour
{
[Header("AI 阵营")]
[SerializeField] private int teamId = 1;
[SerializeField] private int maxMana = 10;
[SerializeField] private float manaRegenInterval = 3f;
[SerializeField] private float decisionInterval = 1.2f;
[SerializeField] private float minimumSpawnInterval = 1.4f;
[Header("三路目标")]
[SerializeField] private float leftLaneX = -10f;
[SerializeField] private float centerLaneX = 0f;
[SerializeField] private float rightLaneX = 10f;
[SerializeField] private float laneHalfWidth = 4f;
[SerializeField] private float resourceThreatWeight = 2.5f;
[SerializeField] private float enemyUnitWeight = 1.5f;
[SerializeField] private float laneRotationWeight = 0.7f;
[Header("卡组")]
[SerializeField] private CharacterData[] deck;
private Transform redSpawnPoint;
private AttackTargetController targetController;
private float currentMana;
private float manaTimer;
private float decisionTimer;
private float spawnTimer;
private int lastLane = 1;
private int consecutiveLaneSpawns;
private bool initialized;
private void Start()
{
redSpawnPoint = FindRedSpawnPoint();
targetController = AttackTargetController.Instance;
currentMana = maxMana;
if (deck == null || deck.Length == 0)
deck = Resources.LoadAll<CharacterData>("Data");
initialized = redSpawnPoint != null && deck != null && deck.Length > 0;
if (!initialized)
Debug.LogWarning("EnemyCastleAI 初始化失败:缺少红方出兵点或角色卡组。");
}
private void Update()
{
if (!initialized) return;
manaTimer += Time.deltaTime;
while (manaTimer >= manaRegenInterval)
{
manaTimer -= manaRegenInterval;
currentMana = Mathf.Min(maxMana, currentMana + 1f);
}
decisionTimer -= Time.deltaTime;
spawnTimer -= Time.deltaTime;
if (decisionTimer > 0f || spawnTimer > 0f) return;
decisionTimer = decisionInterval;
int lane = ChooseLane();
CharacterData card = ChooseCard(lane);
if (card == null || card.manaCost > currentMana) return;
if (Spawn(card, lane))
{
currentMana -= card.manaCost;
spawnTimer = minimumSpawnInterval;
if (lane == lastLane) consecutiveLaneSpawns++;
else consecutiveLaneSpawns = 1;
lastLane = lane;
}
}
private int ChooseLane()
{
float left = ScoreLane(leftLaneX, 0);
float center = ScoreLane(centerLaneX, 1);
float right = ScoreLane(rightLaneX, 2);
if (consecutiveLaneSpawns >= 2)
{
if (lastLane == 0) left -= 3f;
if (lastLane == 1) center -= 3f;
if (lastLane == 2) right -= 3f;
}
float best = Mathf.Max(left, Mathf.Max(center, right));
if (Mathf.Approximately(best, left)) return 0;
if (Mathf.Approximately(best, right)) return 2;
return 1;
}
private float ScoreLane(float laneX, int lane)
{
float score = 1f;
score += lane == 1 ? 0.8f : 0f;
score += lane == lastLane ? laneRotationWeight : 0f;
foreach (var unit in FindObjectsOfType<UnitTeam>())
{
if (unit == null || unit.teamId != 0) continue;
Vector3 p = unit.transform.position;
if (Mathf.Abs(p.x - laneX) <= laneHalfWidth)
score += enemyUnitWeight;
}
foreach (var mine in FindObjectsOfType<CrystalMineCapture>())
{
if (mine == null) continue;
Vector3 p = mine.transform.position;
if (Mathf.Abs(p.x - laneX) > laneHalfWidth * 1.8f) continue;
UnitTeam team = mine.GetComponent<UnitTeam>();
if (team == null || team.teamId != teamId)
score += resourceThreatWeight;
}
return score;
}
private CharacterData ChooseCard(int lane)
{
CharacterData best = null;
float bestScore = float.NegativeInfinity;
foreach (var card in deck)
{
if (card == null || card.unitPrefab == null || card.manaCost > currentMana) continue;
float score = Random.Range(0f, 1f);
if (card.manaCost <= 2) score += 2f;
if (currentMana >= 7 && card.manaCost >= 5) score += 2f;
if (lane != 1 && card.manaCost <= 4) score += 0.5f;
if (score > bestScore) { bestScore = score; best = card; }
}
return best;
}
private bool Spawn(CharacterData data, int lane)
{
GameObject unit = Instantiate(data.unitPrefab, redSpawnPoint.position, redSpawnPoint.rotation);
UnitTeam team = unit.GetComponent<UnitTeam>();
if (team != null) team.teamId = teamId;
unit.tag = "Enemy";
UnitBase unitBase = unit.GetComponent<UnitBase>();
if (unitBase != null)
{
unitBase.InitializeFromPlayerData(data.GetMaxHealth(1), data.GetAttackPower(1), data.GetMoveSpeed(1));
}
if (targetController != null)
{
targetController.redTarget = FindLaneTarget(lane);
}
return true;
}
private GameObject FindLaneTarget(int lane)
{
float x = lane == 0 ? leftLaneX : lane == 2 ? rightLaneX : centerLaneX;
GameObject best = null;
float bestDistance = float.PositiveInfinity;
foreach (var mine in FindObjectsOfType<CrystalMineCapture>())
{
if (Mathf.Abs(mine.transform.position.x - x) < laneHalfWidth * 2f)
{
float d = Mathf.Abs(mine.transform.position.z - redSpawnPoint.position.z);
if (d < bestDistance) { bestDistance = d; best = mine.gameObject; }
}
}
return best != null ? best : targetController?.GetEnemyCastleOfTeam(teamId);
}
private Transform FindRedSpawnPoint()
{
var manager = GetComponent<CastleSpawnManager>();
if (manager != null && manager.redSpawnPoint != null) return manager.redSpawnPoint;
var castles = GameObject.Find("红色城堡");
return castles != null ? castles.transform.Find("出兵点") : null;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: Dnwb5iqqUSrSNOMaQ6I/KyAE6Po88URGA+4wZlnG5BICA1eFs1+ujqg=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+44 -1
View File
@@ -65,6 +65,29 @@ public class SummonCardButton : MonoBehaviour
UpdateButtonState();
}
public void SetDeckCharacter(CharacterData data)
{
characterData = data;
isOnCooldown = false;
cooldownTimer = 0f;
ClearCardUI();
UpdateCardUI();
UpdateCooldownUI();
UpdateButtonState();
}
private void ClearCardUI()
{
if (cardIcon != null) cardIcon.sprite = null;
if (characterNameText != null) characterNameText.text = "";
if (manaCostText != null) manaCostText.text = "";
if (costNumberImage != null)
{
costNumberImage.sprite = null;
costNumberImage.enabled = false;
}
}
public void OnClick()
{
if (characterData == null)
@@ -79,6 +102,20 @@ public class SummonCardButton : MonoBehaviour
return;
}
// 拆家版:固定八卡组采用四槽轮转,成功出牌后立即补下一张。
if (CastleSpawnManager.Instance != null)
{
if (BattleDeckCycle.Instance != null)
{
BattleDeckCycle.Instance.TryPlay(summonSlot);
}
else if (CastleSpawnManager.Instance.TrySpawn(characterData, 0))
{
StartCooldown();
}
return;
}
// 召唤师对决模式:点卡片直接在召唤师身边召唤,消耗能量。
if (SummonerDuelManager.Instance != null)
{
@@ -162,7 +199,7 @@ public class SummonCardButton : MonoBehaviour
private bool CanAfford()
{
if (characterData == null)
if (characterData == null || characterData.unitPrefab == null)
{
return false;
}
@@ -173,6 +210,12 @@ public class SummonCardButton : MonoBehaviour
return BattleSetupManager.Instance.CanAfford(characterData);
}
// 拆家版按能量判断。
if (CastleSpawnManager.Instance != null)
{
return CastleSpawnManager.Instance.CanAfford(characterData);
}
// 召唤师对决模式按能量判断。
if (SummonerDuelManager.Instance != null)
{
+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);
}
}