feat(buildings): 添加金矿建筑功能并优化建筑健康系统

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

No files matched your search

+84
View File
@@ -0,0 +1,84 @@
using UnityEngine;
using System;
/// <summary>
/// 金币管理器,全局单例
/// </summary>
public class GoldManager : MonoBehaviour
{
public static GoldManager Instance { get; private set; }
[Header("金币设置")]
[SerializeField] private int initialGold = 500;
[SerializeField] private int currentGold = 500;
/// <summary>
/// 当前金币数量(只读)
/// </summary>
public int CurrentGold => currentGold;
/// <summary>
/// 金币变化事件(参数:新的金币数量)
/// </summary>
public event Action<int> OnGoldChanged;
private void Awake()
{
// 单例模式
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
}
private void Start()
{
currentGold = initialGold;
OnGoldChanged?.Invoke(currentGold);
}
/// <summary>
/// 增加金币
/// </summary>
public void AddGold(int amount)
{
if (amount <= 0) return;
currentGold += amount;
OnGoldChanged?.Invoke(currentGold);
Debug.Log($"获得金币 +{amount},当前金币: {currentGold}");
}
/// <summary>
/// 尝试消费金币,如果金币足够则扣除并返回 true,否则返回 false
/// </summary>
public bool TrySpendGold(int amount)
{
if (amount <= 0)
{
Debug.LogWarning("消费金币数量必须大于0");
return false;
}
if (currentGold < amount)
{
Debug.LogWarning($"金币不足!需要 {amount},当前只有 {currentGold}");
return false;
}
currentGold -= amount;
OnGoldChanged?.Invoke(currentGold);
Debug.Log($"消费金币 -{amount},当前金币: {currentGold}");
return true;
}
/// <summary>
/// 检查是否有足够的金币
/// </summary>
public bool HasEnoughGold(int amount)
{
return currentGold >= amount;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: DShMsX/+Ui0ZvxHNgg0oANG590a65IynIxC0Pd5T3n/riVwBUL38R7Q=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+59 -17
View File
@@ -1,36 +1,78 @@
using UnityEngine;
/// <summary>
/// 金矿建筑,定时产出金币
/// </summary>
public class GoldMine : MonoBehaviour
{
[Header("产出配置")]
[Min(0)] public int goldPerTick = 5;
[Min(0.1f)] public float productionInterval = 3f;
[Header("金币产出")]
[Tooltip("每次产出的金币数量")]
[SerializeField] private int goldPerProduction = 50;
private float timer;
private BuildingHealth buildingHealth;
[Tooltip("产出间隔(秒)")]
[SerializeField] private float productionInterval = 5f;
[Header("可选:血条和进度条")]
[SerializeField] private WorldHealthBar healthBar;
private float productionTimer = 0f;
private void Awake()
{
buildingHealth = GetComponent<BuildingHealth>();
// 如果没有手动指定,尝试自动获取
if (healthBar == null)
{
healthBar = GetComponent<WorldHealthBar>();
}
}
private void Start()
{
// 初始化进度条(使用能量条显示产出进度)
if (healthBar != null)
{
healthBar.maxEnergy = 100f;
healthBar.currentEnergy = 0f;
}
}
private void Update()
{
if (GoldSystem.Instance == null)
if (GoldManager.Instance == null) return;
productionTimer += Time.deltaTime;
// 更新进度条
if (healthBar != null)
{
return;
float progress = Mathf.Clamp01(productionTimer / productionInterval);
healthBar.currentEnergy = progress * 100f;
}
timer += Time.deltaTime;
if (buildingHealth != null)
// 达到产出时间
if (productionTimer >= productionInterval)
{
buildingHealth.SetProductionProgress(timer / productionInterval);
}
while (timer >= productionInterval)
{
timer -= productionInterval;
GoldSystem.Instance.AddGold(goldPerTick);
ProduceGold();
productionTimer = 0f;
}
}
private void ProduceGold()
{
if (GoldManager.Instance != null)
{
GoldManager.Instance.AddGold(goldPerProduction);
Debug.Log($"{gameObject.name} 产出金币 +{goldPerProduction}");
// 可以在这里播放特效和音效
// TODO: 添加金币产出特效
}
}
// 在编辑器中显示信息
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position + Vector3.up * 2f, 0.5f);
}
}
+57
View File
@@ -0,0 +1,57 @@
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
/// <summary>
/// 金币 UI 显示,显示在屏幕右上角
/// </summary>
[RequireComponent(typeof(Text))]
public class GoldUI : MonoBehaviour
{
private Text goldText;
private bool isSubscribed = false;
private void Awake()
{
goldText = GetComponent<Text>();
}
private void Start()
{
// 延迟订阅,确保 GoldManager 已初始化
StartCoroutine(SubscribeToGoldManager());
}
private IEnumerator SubscribeToGoldManager()
{
// 等待 GoldManager 初始化
while (GoldManager.Instance == null)
{
yield return null;
}
// 订阅事件
GoldManager.Instance.OnGoldChanged += UpdateGoldDisplay;
isSubscribed = true;
// 立即更新显示
UpdateGoldDisplay(GoldManager.Instance.CurrentGold);
}
private void OnDestroy()
{
// 取消订阅
if (isSubscribed && GoldManager.Instance != null)
{
GoldManager.Instance.OnGoldChanged -= UpdateGoldDisplay;
}
}
private void UpdateGoldDisplay(int newGold)
{
if (goldText != null)
{
goldText.text = $"金币: {newGold}";
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: Wy5O5y38VnjSuIhFnocQ+yIk0/PO6tI7xIs2xwwd3tamOqHsfmlJPMA=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+68
View File
@@ -0,0 +1,68 @@
using UnityEngine;
/// <summary>
/// 主角金币产出组件,挂载到主角单位上
/// </summary>
public class HeroGoldProducer : MonoBehaviour
{
[Header("金币产出")]
[Tooltip("每次产出的金币数量")]
[SerializeField] private int goldPerProduction = 30;
[Tooltip("产出间隔(秒)")]
[SerializeField] private float productionInterval = 8f;
[Header("可选:血条和进度条")]
[SerializeField] private WorldHealthBar healthBar;
private float productionTimer = 0f;
private void Awake()
{
// 如果没有手动指定,尝试自动获取
if (healthBar == null)
{
healthBar = GetComponent<WorldHealthBar>();
}
}
private void Start()
{
// 初始化进度条(使用能量条显示产出进度)
if (healthBar != null)
{
healthBar.maxEnergy = 100f;
healthBar.currentEnergy = 0f;
}
}
private void Update()
{
if (GoldManager.Instance == null) return;
productionTimer += Time.deltaTime;
// 更新进度条
if (healthBar != null)
{
float progress = Mathf.Clamp01(productionTimer / productionInterval);
healthBar.currentEnergy = progress * 100f;
}
// 达到产出时间
if (productionTimer >= productionInterval)
{
ProduceGold();
productionTimer = 0f;
}
}
private void ProduceGold()
{
if (GoldManager.Instance != null)
{
GoldManager.Instance.AddGold(goldPerProduction);
Debug.Log($"主角 {gameObject.name} 产出金币 +{goldPerProduction}");
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: CHtLsyOqAHO9Tb+uQSnXcIZq0ZVdLqUhEvDUjf4JqQrm9JlgUFne/xk=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+87 -7
View File
@@ -86,12 +86,19 @@ public class UnitBase : MonoBehaviour
//初始位置坐标
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;
@@ -154,10 +161,27 @@ 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
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;
//记录初始坐标
@@ -194,6 +218,12 @@ public class UnitBase : MonoBehaviour
//攻击状态下等待Animator报告攻击动画播放完成。
if (state == State.Attack)
{
// 攻击时直接禁用 NavMeshAgent,彻底停止移动
if (navAgent != null && navAgent.enabled)
{
navAgent.enabled = false;
}
animator.SetInteger("speed", 0);
attackElapsedTime += Time.deltaTime;
applyGravity();
@@ -270,7 +300,7 @@ public class UnitBase : MonoBehaviour
return false;
}
healthBar.TakeDamage(ModifyIncomingDamage(damage, attacker));
healthBar.TakeDamage(ModifyIncomingDamage(damage, attacker), attacker);
return true;
}
@@ -333,11 +363,10 @@ public class UnitBase : MonoBehaviour
return false;
}
// 将双方控制器半径计入判定,避免碰撞体互相阻挡时无法进入攻击距离。
float selfRadius = GetHorizontalCollisionRadius(controller);
CharacterController opponentController = opponent.GetComponent<CharacterController>();
float opponentRadius = GetHorizontalCollisionRadius(opponentController);
float reachDistance = selfRadius + opponentRadius + attackRange;
// NavMeshAgent 会保持一定距离,所以增加额外的攻击范围缓冲
// 原来的逻辑:selfRadius + opponentRadius + attackRange
// 现在:直接使用 attackRange,并增加 1.5 米的缓冲,让单位在更远距离就能攻击
float reachDistance = attackRange + 1.5f;
return Vector3.Distance(transform.position, opponent.transform.position) <= reachDistance;
}
@@ -465,6 +494,12 @@ public class UnitBase : MonoBehaviour
{
target = friend;
// 攻击时直接禁用 NavMeshAgent(彻底停止移动)
if (navAgent != null && navAgent.enabled)
{
navAgent.enabled = false;
}
FaceTarget(friend);
if (TryReleaseSkill())
@@ -893,6 +928,47 @@ public class UnitBase : MonoBehaviour
{
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)
@@ -973,7 +1049,11 @@ public class UnitBase : MonoBehaviour
private void applyGravity()
{
controller.Move(Vector3.up * getVerticalVelocity() * Time.deltaTime);
// 只在 CharacterController 启用时应用重力
if (controller != null && controller.enabled)
{
controller.Move(Vector3.up * getVerticalVelocity() * Time.deltaTime);
}
}
// 在 Scene 视图中标记箭的发射位置和方向,便于调整空物体。
+45 -19
View File
@@ -14,7 +14,19 @@ public class WorldHealthBar : MonoBehaviour
[Min(1f)] public float maxEnergy = 100f;
// 当前技能能量,范围会自动限制在 0 到 maxEnergy 之间。
[Min(0f)] public float currentEnergy = 0f;
[Min(0f)]
[SerializeField]
private float _currentEnergy = 0f;
public float currentEnergy
{
get => _currentEnergy;
set
{
_currentEnergy = Mathf.Clamp(value, 0f, maxEnergy);
UpdateEnergyFill();
}
}
public bool IsEnergyFull => !isDead && currentEnergy >= maxEnergy;
@@ -39,16 +51,18 @@ public class WorldHealthBar : MonoBehaviour
[Min(0f)] public float damageNumberRiseHeight = 1.2f;
[Min(0f)] public float damageNumberSpawnHeight = 1.5f;
[Min(0.01f)] public float damageNumberSize = 0.09f;
[Min(0f)] public float damageNumberOutlineWidth = 0.015f;
// 描边宽度设为私有常量,所有角色统一使用
private const float DamageNumberOutlineWidth = 0.025f; // 从 0.015 增加到 0.025,描边更粗
// 血条 UI 的像素尺寸,实际显示大小由 worldWidth 控制。
private const int PixelWidth = 100;
private const int PixelHeight = 14;
// 能量条紧贴血条下方,宽度更窄,高度更小。
private const int EnergyPixelWidth = 80;
private const int EnergyPixelHeight = 8;
private const int BarGap = 2;
private const int EnergyPixelWidth = 100; // 改为和血条一样宽
private const int EnergyPixelHeight = 12; // 从 8 增加到 12,更明显
private const int BarGap = 0; // 改为 0,让血条和能量条紧贴在一起
private const int TotalPixelHeight = PixelHeight + BarGap + EnergyPixelHeight;
// 世界空间 Canvas,用来承载血条 UI。
@@ -81,7 +95,7 @@ public class WorldHealthBar : MonoBehaviour
maxHealth = Mathf.Max(1f, maxHealth);
currentHealth = Mathf.Clamp(currentHealth, 0f, maxHealth);
maxEnergy = Mathf.Max(1f, maxEnergy);
currentEnergy = Mathf.Clamp(currentEnergy, 0f, maxEnergy);
_currentEnergy = Mathf.Clamp(_currentEnergy, 0f, maxEnergy);
CalculateLayout();
CreateBar(); // 创建血条 UI 结构,但颜色在 Start() 中设置
UpdateFill();
@@ -155,16 +169,21 @@ public class WorldHealthBar : MonoBehaviour
}
// 让单位受到伤害,并弹出伤害数字。
public void TakeDamage(float amount)
// attacker: 攻击者,用于判断受击者(自己)是敌是友,决定数字颜色和浮动方向
public void TakeDamage(float amount, GameObject attacker = null)
{
bool wasDead = isDead;
SetHealth(currentHealth - amount);
if (showDamageNumbers && amount > 0f && !wasDead)
{
SpawnDamageNumber(amount, false);
// 判断受击者(自己)是否是友方(Friend tag)
bool isVictimFriend = gameObject.CompareTag("Friend");
SpawnDamageNumber(amount, false, isVictimFriend);
}
}
// 不再需要这个方法,直接用 CompareTag 判断
// 让单位恢复生命值,并弹出绿色治疗数字。
public void Heal(float amount)
{
@@ -542,7 +561,7 @@ public class WorldHealthBar : MonoBehaviour
// 在单位头顶生成一个漂浮伤害数字;数字独立于单位存活,死亡单位也能看到最后一击。
// TextMesh 不支持原生描边,用 4 个偏移的黑色副本 + 1 个白色主字组合出黑边效果。
private void SpawnDamageNumber(float amount, bool isHeal)
private void SpawnDamageNumber(float amount, bool isHeal, bool isVictimFriend = false)
{
if (DamageNumber.ActiveCount >= DamageNumber.MaxActive)
{
@@ -555,19 +574,26 @@ public class WorldHealthBar : MonoBehaviour
rootObject.transform.position = spawnPosition;
string text = Mathf.RoundToInt(amount).ToString();
float outline = damageNumberOutlineWidth;
float outline = DamageNumberOutlineWidth;
// 黑色描边副本先画(sortingOrder 低)。
CreateNumberText(rootObject.transform, text, new Vector3(-outline, 0f, 0f), Color.black, damageNumberSize, 60);
CreateNumberText(rootObject.transform, text, new Vector3(outline, 0f, 0f), Color.black, damageNumberSize, 60);
CreateNumberText(rootObject.transform, text, new Vector3(0f, -outline, 0f), Color.black, damageNumberSize, 60);
CreateNumberText(rootObject.transform, text, new Vector3(0f, outline, 0f), Color.black, damageNumberSize, 60);
// 白色主字最后画(sortingOrder 高),覆盖在黑边之上。
CreateNumberText(rootObject.transform, text, Vector3.zero, Color.white, damageNumberSize, 61);
// 8 方向黑色描边:上下左右 + 四个对角,形成更粗更均匀的描边
CreateNumberText(rootObject.transform, text, new Vector3(-outline, 0f, 0f), Color.black, damageNumberSize, 60); // 左
CreateNumberText(rootObject.transform, text, new Vector3(outline, 0f, 0f), Color.black, damageNumberSize, 60); // 右
CreateNumberText(rootObject.transform, text, new Vector3(0f, -outline, 0f), Color.black, damageNumberSize, 60); // 下
CreateNumberText(rootObject.transform, text, new Vector3(0f, outline, 0f), Color.black, damageNumberSize, 60); // 上
CreateNumberText(rootObject.transform, text, new Vector3(-outline, -outline, 0f), Color.black, damageNumberSize, 60); // 左下
CreateNumberText(rootObject.transform, text, new Vector3(outline, -outline, 0f), Color.black, damageNumberSize, 60); // 右下
CreateNumberText(rootObject.transform, text, new Vector3(-outline, outline, 0f), Color.black, damageNumberSize, 60); // 左上
CreateNumberText(rootObject.transform, text, new Vector3(outline, outline, 0f), Color.black, damageNumberSize, 60); // 右上
// 主字颜色:治疗=绿色,友方受伤=红色,敌方受伤=白色
Color mainColor = isHeal ? Color.green : (isVictimFriend ? Color.red : Color.white);
CreateNumberText(rootObject.transform, text, Vector3.zero, mainColor, damageNumberSize, 61);
DamageNumber number = rootObject.AddComponent<DamageNumber>();
number.duration = damageNumberDuration;
number.riseHeight = damageNumberRiseHeight;
// 友方受伤向下浮动(负值),敌方受伤向上浮动(正值)
number.riseHeight = isVictimFriend ? -damageNumberRiseHeight : damageNumberRiseHeight;
}
// 创建一个配置好的数字 TextMesh 子物体。
@@ -644,7 +670,7 @@ public class DamageNumber : MonoBehaviour
public float duration = 0.8f;
public float riseHeight = 1.2f;
// 一个数字由主字+描边副本共5个TextMesh组成,统一驱动透明度。
// 一个数字由主字 + 8 方向描边副本共 9 个 TextMesh 组成,统一驱动透明度。
private TextMesh[] textMeshes;
private Color[] startColors;
private Vector3 startPosition;