using UnityEngine;
using UnityEngine.UI;
public class WorldHealthBar : MonoBehaviour
{
// 最大生命值。
[Min(1f)] public float maxHealth = 100f;
// 当前生命值,范围会自动限制在 0 到 maxHealth 之间。
[Min(0f)] public float currentHealth = 100f;
[Header("护盾")]
public float maxShield = 0f;
public float currentShield = 0f;
///
/// 为单位添加护盾
///
public void AddShield(float amount)
{
if (amount <= 0f || isDead) return;
if (currentShield <= 0f)
{
maxShield = amount;
}
else
{
maxShield += amount;
}
currentShield += amount;
UpdateFill();
UpdateShieldFill();
Debug.Log($"{gameObject.name} 获得护盾 +{amount:F1},当前护盾: {currentShield:F1}/{maxShield:F1}");
}
///
/// 减少或清空护盾
///
public void RemoveShield(float amount)
{
if (amount <= 0f || currentShield <= 0f) return;
currentShield = Mathf.Max(0f, currentShield - amount);
if (currentShield <= 0f)
{
maxShield = 0f;
}
UpdateShieldFill();
}
// 技能能量上限,英雄默认使用100点;没有技能的小怪可关闭能量条。
[Header("技能能量")]
public bool showEnergyBar = true;
[Min(1f)] public float maxEnergy = 100f;
// 当前技能能量,范围会自动限制在 0 到 maxEnergy 之间。
[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;
// 血条相对于单位头顶的额外高度。
[Min(0f)] public float heightOffset = 0.25f;
// 所有单位使用统一的血条世界宽度。
[Min(0.1f)] public float barWidth = 1.2f;
// 血条边框颜色。
public Color borderColor = new Color(0.03f, 0.03f, 0.03f, 0.95f);
// 死亡特效持续时间,单位为秒。
[Min(0.1f)] public float deathEffectDuration = 1.1f;
// 死亡时使用的浅蓝色粒子颜色。
public Color deathEffectColor = new Color(0.55f, 0.9f, 1f, 1f);
[Header("复活设置")]
[Tooltip("是否可复活(友方英雄)")]
public bool canRevive = false;
[Header("伤害数字")]
public bool showDamageNumbers = true;
[Min(0.1f)] public float damageNumberDuration = 0.8f;
[Min(0f)] public float damageNumberRiseHeight = 1.2f;
[Min(0f)] public float damageNumberSpawnHeight = 1.5f;
[Min(0.01f)] public float damageNumberSize = 0.09f;
// 描边宽度设为私有常量,所有角色统一使用
private const float DamageNumberOutlineWidth = 0.025f; // 从 0.015 增加到 0.025,描边更粗
// 护盾条高 10 像素,置于血条最上方。
private const int ShieldPixelHeight = 10;
private const int PixelWidth = 100;
private const int PixelHeight = 14;
// 能量条紧贴血条下方,宽度更窄,高度更小。
private const int EnergyPixelWidth = 100; // 改为和血条一样宽
private const int EnergyPixelHeight = 12; // 从 8 增加到 12,更明显
private const int BarGap = 0; // 改为 0,让血条和能量条紧贴在一起
private const int TotalPixelHeight = ShieldPixelHeight + BarGap + PixelHeight + BarGap + EnergyPixelHeight;
// 世界空间 Canvas,用来承载血条 UI。
private Canvas canvas;
// 血条整体的 RectTransform。
private RectTransform barRect;
// 整体 UI 行节点引用
private RectTransform shieldRow;
private RectTransform healthRow;
private RectTransform energyRow;
// 绿色血量填充 Image。
private Image fillImage;
// 浅蓝色护盾填充 Image。
private Image shieldFillImage;
// 金色技能能量填充 Image。
private Image energyFillImage;
// 血条相对于单位的本地位置。
private Vector3 localBarPosition;
// 血条最终使用的世界宽度。
private float worldWidth;
[Header("等级显示")]
public int unitLevel = 1;
// 所有血条共用的圆角 Sprite 和圆形 Sprite。
private static Sprite roundedSprite;
private static Sprite circleSprite;
private Text levelText;
private Image levelBadgeBg;
// 防止死亡逻辑重复执行。
private bool isDead;
// 公开死亡状态供外部查询(用于胜负判定)
public bool IsDead => isDead;
public void SetLevel(int level)
{
unitLevel = Mathf.Max(1, level);
if (levelText != null)
{
levelText.text = unitLevel.ToString();
}
}
// 初始化血量和血条 UI。
private void Awake()
{
maxHealth = Mathf.Max(1f, maxHealth);
currentHealth = Mathf.Clamp(currentHealth, 0f, maxHealth);
maxEnergy = Mathf.Max(1f, maxEnergy);
_currentEnergy = Mathf.Clamp(_currentEnergy, 0f, maxEnergy);
CalculateLayout();
CreateBar(); // 创建血条 UI 结构;无技能单位只创建血条
UpdateFill();
UpdateEnergyFill();
}
// 在 Start() 中根据最终标签设置血条颜色,确保 Instantiate 后的标签修改生效。
private void Start()
{
if (fillImage != null)
{
fillImage.color = GetHealthFillColor();
}
}
// 每帧让血条保持在单位头顶,并朝向主摄像机。
private void LateUpdate()
{
if (barRect == null) return;
barRect.localPosition = GetBarRootPosition();
ApplyScaleCompensation();
// 优先使用场景中名称完全为 Main Camera 的摄像机。
Camera camera = FindMainCamera();
if (camera == null) return;
canvas.worldCamera = camera;
if (camera.gameObject.name == "Main Camera")
{
// 面向摄像机背面,保证 UI 左右方向不发生镜像反转
barRect.rotation = Quaternion.LookRotation(
barRect.position - camera.transform.position,
camera.transform.up);
}
else
{
// 找不到 Main Camera 时,保留原来的展示逻辑。
barRect.rotation = Quaternion.LookRotation(
barRect.position - camera.transform.position,
Vector3.up);
}
}
// 查找场景中名称为 Main Camera 的摄像机;找不到时回退到 Camera.main。
private Camera FindMainCamera()
{
GameObject mainCameraObject = GameObject.Find("Main Camera");
if (mainCameraObject != null)
{
Camera namedCamera = mainCameraObject.GetComponent();
if (namedCamera != null && namedCamera.isActiveAndEnabled)
{
return namedCamera;
}
}
return Camera.main;
}
// 直接设置当前生命值,并刷新绿色填充比例。
public void SetHealth(float value)
{
if (isDead) return;
currentHealth = Mathf.Clamp(value, 0f, maxHealth);
UpdateFill();
if (currentHealth <= 0f)
{
Die();
}
}
// 让单位受到伤害,并弹出伤害数字。
// attacker: 攻击者,用于判断受击者(自己)是敌是友,决定数字颜色和浮动方向
public void TakeDamage(float amount, GameObject attacker = null)
{
bool wasDead = isDead;
float originalDamage = amount;
// 优先消耗护盾抵扣伤害
if (currentShield > 0f && amount > 0f)
{
float absorbed = Mathf.Min(currentShield, amount);
currentShield -= absorbed;
amount -= absorbed;
UpdateShieldFill();
Debug.Log($"{gameObject.name} 护盾吸收了 {absorbed:F1} 伤害,剩余护盾: {currentShield:F1}");
}
if (amount > 0f)
{
SetHealth(currentHealth - amount);
}
if (showDamageNumbers && originalDamage > 0f && !wasDead)
{
// 判断受击者(自己)是否是友方(Friend tag)
bool isVictimFriend = gameObject.CompareTag("Friend");
SpawnDamageNumber(originalDamage, false, isVictimFriend);
}
}
// 不再需要这个方法,直接用 CompareTag 判断
// 让单位恢复生命值,并弹出绿色治疗数字。
public void Heal(float amount)
{
bool wasDead = isDead;
float healed = Mathf.Min(amount, maxHealth - currentHealth);
SetHealth(currentHealth + amount);
if (showDamageNumbers && healed > 0f && !wasDead)
{
SpawnDamageNumber(healed, true);
}
}
///
/// 复活单位(用于友方英雄复活系统)
///
public void Revive(float healthAmount)
{
if (!canRevive)
{
Debug.LogWarning($"{gameObject.name} 不可复活(canRevive=false)");
return;
}
// 重置死亡状态
isDead = false;
// 恢复生命值
currentHealth = Mathf.Clamp(healthAmount, 0f, maxHealth);
UpdateFill();
Debug.Log($"{gameObject.name} 已复活,生命值: {currentHealth}/{maxHealth}");
}
// 增加技能能量,并返回本次增加后是否达到满能量。
public bool AddEnergy(float amount)
{
if (isDead || amount <= 0f)
{
return false;
}
float previousEnergy = currentEnergy;
currentEnergy = Mathf.Clamp(currentEnergy + amount, 0f, maxEnergy);
UpdateEnergyFill();
return previousEnergy < maxEnergy && currentEnergy >= maxEnergy;
}
// 清空技能能量并刷新能量条。
public void ResetEnergy()
{
SetEnergy(0f);
}
// 设置下方条的当前填充值;单位用于技能能量,建筑用于生产进度。
public void SetEnergy(float value)
{
if (isDead)
{
return;
}
currentEnergy = Mathf.Clamp(value, 0f, maxEnergy);
UpdateEnergyFill();
}
// 处理单位死亡:隐藏模型和血条,播放蓝色向上爆散特效。
private void Die()
{
isDead = true;
currentHealth = 0f;
UpdateFill();
foreach (Renderer renderer in GetComponentsInChildren())
{
renderer.enabled = false;
}
// 立即从敌我扫描中移除,避免死亡等待期间继续被攻击。
gameObject.SetActive(false);
if (barRect != null)
{
barRect.gameObject.SetActive(false);
}
GameObject effect = CreateDeathEffect();
Destroy(effect, deathEffectDuration);
// 如果可以复活(友方英雄),不销毁GameObject
if (!canRevive)
{
Destroy(gameObject, deathEffectDuration);
}
}
// 创建蓝色粒子爆散特效。
private GameObject CreateDeathEffect()
{
GameObject effect = new GameObject("DeathEffect");
effect.transform.position = transform.position + Vector3.up * 1.35f;
ParticleSystem particles = effect.AddComponent();
particles.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
var main = particles.main;
main.playOnAwake = false;
main.duration = deathEffectDuration;
main.startLifetime = new ParticleSystem.MinMaxCurve(0.9f, deathEffectDuration);
main.startSpeed = new ParticleSystem.MinMaxCurve(0.05f, 0.18f);
main.startSize = new ParticleSystem.MinMaxCurve(0.09f, 0.2f);
main.startRotation3D = true;
main.startRotationX = new ParticleSystem.MinMaxCurve(0f, 6.28f);
main.startRotationY = new ParticleSystem.MinMaxCurve(0f, 6.28f);
main.startRotationZ = new ParticleSystem.MinMaxCurve(0f, 6.28f);
main.startColor = deathEffectColor;
main.maxParticles = 32;
main.gravityModifier = new ParticleSystem.MinMaxCurve(1.6f);
main.simulationSpace = ParticleSystemSimulationSpace.World;
main.stopAction = ParticleSystemStopAction.None;
var emission = particles.emission;
emission.rateOverTime = 0f;
emission.SetBursts(new[] { new ParticleSystem.Burst(0f, 18) });
var shape = particles.shape;
shape.shapeType = ParticleSystemShapeType.Sphere;
shape.radius = 0.12f;
// 只保留轻微水平散落,主要依靠重力向下坠落。
var velocity = particles.velocityOverLifetime;
velocity.enabled = true;
velocity.space = ParticleSystemSimulationSpace.World;
velocity.x = new ParticleSystem.MinMaxCurve(-0.08f, 0.08f);
velocity.y = new ParticleSystem.MinMaxCurve(-0.25f, -0.05f);
velocity.z = new ParticleSystem.MinMaxCurve(-0.08f, 0.08f);
var collision = particles.collision;
collision.enabled = true;
collision.type = ParticleSystemCollisionType.World;
collision.mode = ParticleSystemCollisionMode.Collision3D;
collision.bounce = 0f;
collision.lifetimeLoss = 0.15f;
var size = particles.sizeOverLifetime;
size.enabled = true;
AnimationCurve fade = new AnimationCurve(
new Keyframe(0f, 1f),
new Keyframe(0.75f, 1f),
new Keyframe(1f, 0f));
size.size = new ParticleSystem.MinMaxCurve(1f, fade);
var renderer = effect.GetComponent();
renderer.renderMode = ParticleSystemRenderMode.Mesh;
renderer.mesh = CreateCrystalMesh();
renderer.material = CreateParticleMaterial();
particles.Play();
return effect;
}
// 创建低模菱形水晶碎片 Mesh。
private Mesh CreateCrystalMesh()
{
Mesh mesh = new Mesh { name = "DeathCrystal" };
mesh.vertices = new[]
{
new Vector3(0f, 0.5f, 0f), new Vector3(0.5f, 0f, 0f),
new Vector3(0f, 0f, 0.5f), new Vector3(-0.5f, 0f, 0f),
new Vector3(0f, 0f, -0.5f), new Vector3(0f, -0.5f, 0f)
};
mesh.triangles = new[]
{
0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 1,
5, 2, 1, 5, 3, 2, 5, 4, 3, 5, 1, 4
};
mesh.RecalculateNormals();
mesh.RecalculateBounds();
return mesh;
}
// 创建带浅蓝发光效果的水晶材质。
private Material CreateParticleMaterial()
{
Shader shader = Shader.Find("Standard");
Material material = new Material(shader);
material.color = deathEffectColor;
material.SetFloat("_Metallic", 0.15f);
material.SetFloat("_Glossiness", 0.85f);
material.SetColor("_EmissionColor", deathEffectColor * 1.6f);
material.EnableKeyword("_EMISSION");
return material;
}
// 根据单位模型或碰撞体计算血条位置和宽度。
private void CalculateLayout()
{
Bounds bounds = new Bounds(transform.position, new Vector3(1f, 2f, 1f));
bool found = false;
foreach (Renderer renderer in GetComponentsInChildren(true))
{
if (!found)
{
bounds = renderer.bounds;
found = true;
}
else
{
bounds.Encapsulate(renderer.bounds);
}
}
if (!found)
{
CharacterController controller = GetComponent();
if (controller != null) bounds = controller.bounds;
}
// 血条尺寸固定,不随单位模型大小变化;只根据模型顶部调整位置。
worldWidth = barWidth;
// 水平方向固定对齐单位根节点,只使用模型顶部计算血条高度。
Vector3 top = transform.InverseTransformPoint(new Vector3(transform.position.x, bounds.max.y, transform.position.z));
float localHeightOffset = transform.lossyScale.y != 0f
? heightOffset / transform.lossyScale.y
: heightOffset;
localBarPosition = new Vector3(0f, top.y + localHeightOffset, 0f);
}
// 创建世界空间 Canvas、边框和绿色血量填充。
private void CreateBar()
{
GameObject barObject = new GameObject("WorldHealthBar", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler));
barObject.transform.SetParent(transform, false);
barObject.layer = gameObject.layer;
canvas = barObject.GetComponent