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.65f; // 所有单位使用统一的血条世界宽度。 [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(); } // 自动同步 UnitBase 上的等级设置 UnitBase unit = GetComponent(); if (unit != null) { SetLevel(unit.level); } } // 每帧让血条保持在单位头顶,并朝向主摄像机。 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(); OnHealthChanged?.Invoke(currentHealth, maxHealth); if (currentHealth <= 0f) { Die(); } } /// 生命值变化事件:参数为 (当前血量, 最大血量) public System.Action OnHealthChanged; /// 护盾吸收伤害事件:参数为实际吸收的伤害数值 public System.Action OnShieldAbsorbedDamage; // 让单位受到伤害,并弹出伤害数字。 // 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(); OnShieldAbsorbedDamage?.Invoke(absorbed); 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(); canvas.renderMode = RenderMode.WorldSpace; canvas.sortingOrder = 100; barObject.GetComponent().dynamicPixelsPerUnit = 10f; barRect = barObject.GetComponent(); barRect.sizeDelta = new Vector2(PixelWidth, showEnergyBar ? TotalPixelHeight : (ShieldPixelHeight + PixelHeight)); barRect.localScale = Vector3.one * (worldWidth / PixelWidth); barRect.localPosition = GetBarRootPosition(); ApplyScaleCompensation(); // 1. 顶层独立护盾条 (ShieldBar) shieldRow = CreateRow(barObject.transform, "ShieldBar", PixelWidth, ShieldPixelHeight, 0); Image shieldBackground = CreateImage("ShieldBorder", shieldRow, RoundedSprite(), borderColor); // 加回黑框 SetFullRect(shieldBackground.rectTransform); Image shieldInner = CreateImage("ShieldFill", shieldRow, RoundedSprite(), new Color(0f, 0.88f, 1f, 0.95f)); // 亮天蓝色 shieldInner.rectTransform.anchorMin = Vector2.zero; shieldInner.rectTransform.anchorMax = Vector2.one; shieldInner.rectTransform.offsetMin = new Vector2(2f, 1f); shieldInner.rectTransform.offsetMax = new Vector2(-2f, -1f); shieldInner.type = Image.Type.Filled; shieldInner.fillMethod = Image.FillMethod.Horizontal; shieldInner.fillOrigin = 0; shieldFillImage = shieldInner; shieldRow.gameObject.SetActive(false); // 默认没有护盾时整体收起隐藏 // 2. 中层主血条 (HealthBar) healthRow = CreateRow(barObject.transform, "HealthBar", PixelWidth, PixelHeight, ShieldPixelHeight + BarGap); Image background = CreateImage("Border", healthRow, RoundedSprite(), borderColor); SetFullRect(background.rectTransform); Image inner = CreateImage("Fill", healthRow, RoundedSprite(), Color.green); inner.rectTransform.anchorMin = Vector2.zero; inner.rectTransform.anchorMax = Vector2.one; inner.rectTransform.offsetMin = new Vector2(2f, 2f); inner.rectTransform.offsetMax = new Vector2(-2f, -2f); inner.type = Image.Type.Filled; inner.fillMethod = Image.FillMethod.Horizontal; inner.fillOrigin = 0; fillImage = inner; // 3. 底层技能能量条 (EnergyBar) if (!showEnergyBar) { energyFillImage = null; CreateLevelBadge(barObject.transform); return; } energyRow = CreateRow( barObject.transform, "EnergyBar", EnergyPixelWidth, EnergyPixelHeight, ShieldPixelHeight + BarGap + PixelHeight + BarGap); Image energyBackground = CreateImage("EnergyBorder", energyRow, RoundedSprite(), borderColor); SetFullRect(energyBackground.rectTransform); Image energyInner = CreateImage( "EnergyFill", energyRow, RoundedSprite(), GetEnergyFillColor()); energyInner.rectTransform.anchorMin = Vector2.zero; energyInner.rectTransform.anchorMax = Vector2.one; energyInner.rectTransform.offsetMin = new Vector2(2f, 1f); energyInner.rectTransform.offsetMax = new Vector2(-2f, -1f); energyInner.type = Image.Type.Filled; energyInner.fillMethod = Image.FillMethod.Horizontal; energyInner.fillOrigin = 0; energyFillImage = energyInner; CreateLevelBadge(barObject.transform); } private void ApplyScaleCompensation() { Vector3 parentScale = transform.lossyScale; float targetScale = worldWidth / PixelWidth; 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); } // 返回整体 Canvas 的位置,使血条中心保持原来的高度。 private Vector3 GetBarRootPosition() { float pixelToWorld = worldWidth / PixelWidth; int barHeight = showEnergyBar ? TotalPixelHeight : PixelHeight; float worldEnergyOffset = (barHeight - PixelHeight) * 0.5f * pixelToWorld; float localEnergyOffset = transform.lossyScale.y != 0f ? worldEnergyOffset / transform.lossyScale.y : worldEnergyOffset; return localBarPosition - Vector3.up * localEnergyOffset; } // 创建一行固定像素高度的世界空间 UI。 private static RectTransform CreateRow( Transform parent, string name, int width, int height, int topOffset) { GameObject rowObject = new GameObject(name, typeof(RectTransform)); rowObject.transform.SetParent(parent, false); RectTransform rowRect = rowObject.GetComponent(); rowRect.anchorMin = new Vector2(0.5f, 1f); rowRect.anchorMax = new Vector2(0.5f, 1f); rowRect.pivot = new Vector2(0.5f, 1f); rowRect.sizeDelta = new Vector2(width, height); rowRect.anchoredPosition = new Vector2(0f, -topOffset); return rowRect; } private Color GetHealthFillColor() { // 使用阵营 ID 判断敌我(支持 PVP) var unitTeam = GetComponent(); if (unitTeam != null && unitTeam.IsLocalPlayerTeam()) { return new Color(0.1f, 0.8f, 0.18f, 1f); // 绿色(我方) } else { return new Color(0.88f, 0.12f, 0.12f, 1f); // 红色(敌方) } } private Color GetEnergyFillColor() { return new Color(1f, 0.72f, 0.08f, 1f); } // 创建圆形 Sprite 用于等级徽章背景。 private static Sprite CircleSprite() { if (circleSprite != null) return circleSprite; Texture2D texture = new Texture2D(64, 64, TextureFormat.RGBA32, false); float radius = 30f; Vector2 center = new Vector2(31.5f, 31.5f); for (int y = 0; y < 64; y++) { for (int x = 0; x < 64; x++) { float dist = Vector2.Distance(new Vector2(x, y), center); if (dist > radius) { texture.SetPixel(x, y, Color.clear); } else { float alpha = Mathf.Clamp01(radius + 0.5f - dist); texture.SetPixel(x, y, new Color(1f, 1f, 1f, alpha)); } } } texture.Apply(); circleSprite = Sprite.Create(texture, new Rect(0, 0, 64, 64), new Vector2(0.5f, 0.5f)); return circleSprite; } // 创建血条左侧的圆圈等级徽章 private void CreateLevelBadge(Transform parent) { int badgeSize = 39; // 调整为原来的 1.5 倍 (从 26 增大至 39) GameObject badgeObj = new GameObject("LevelBadge", typeof(RectTransform)); badgeObj.transform.SetParent(parent, false); RectTransform badgeRect = badgeObj.GetComponent(); badgeRect.anchorMin = new Vector2(0f, 1f); badgeRect.anchorMax = new Vector2(0f, 1f); badgeRect.pivot = new Vector2(1f, 0.5f); // 右边缘切齐,紧贴血条左边缘 badgeRect.sizeDelta = new Vector2(badgeSize, badgeSize); badgeRect.anchoredPosition = new Vector2(2f, -15f); // 居中对齐主血条中心 (posY = -15f) // 外层黑边圆圈 Image outerCircle = CreateImage("OuterCircle", badgeRect, CircleSprite(), new Color(0.05f, 0.05f, 0.05f, 0.95f)); SetFullRect(outerCircle.rectTransform); // 内层深色底板 Image innerCircle = CreateImage("InnerCircle", outerCircle.transform, CircleSprite(), new Color(0.12f, 0.18f, 0.15f, 1f)); innerCircle.rectTransform.anchorMin = Vector2.zero; innerCircle.rectTransform.anchorMax = Vector2.one; innerCircle.rectTransform.offsetMin = new Vector2(2f, 2f); innerCircle.rectTransform.offsetMax = new Vector2(-2f, -2f); levelBadgeBg = innerCircle; // 居中阿拉伯数字 Text GameObject textObj = new GameObject("LevelText", typeof(RectTransform), typeof(Text)); textObj.transform.SetParent(innerCircle.transform, false); RectTransform textRect = textObj.GetComponent(); SetFullRect(textRect); levelText = textObj.GetComponent(); levelText.text = unitLevel.ToString(); levelText.font = GetBuiltinFont(); levelText.fontSize = 26; // 字体同步放大 levelText.fontStyle = FontStyle.Bold; levelText.alignment = TextAnchor.MiddleCenter; levelText.color = new Color(0.95f, 0.98f, 0.95f, 1f); // 加上外发光/描边效果 var shadow = textObj.AddComponent(); shadow.effectColor = new Color(0f, 0f, 0f, 0.9f); shadow.effectDistance = new Vector2(1f, -1f); } // 创建一个用于边框或填充的 UI Image。 private static Image CreateImage(string name, Transform parent, Sprite sprite, Color color) { GameObject imageObject = new GameObject(name, typeof(RectTransform), typeof(Image)); imageObject.transform.SetParent(parent, false); Image image = imageObject.GetComponent(); image.sprite = sprite; image.color = color; image.raycastTarget = false; return image; } // 让 UI 元素铺满父级 RectTransform。 private static void SetFullRect(RectTransform rect) { rect.anchorMin = Vector2.zero; rect.anchorMax = Vector2.one; rect.offsetMin = Vector2.zero; rect.offsetMax = Vector2.zero; } // 根据当前生命值更新绿色填充比例。 private void UpdateFill() { if (fillImage != null) { fillImage.fillAmount = Mathf.Clamp01(currentHealth / maxHealth); } } // 根据当前技能能量更新金色填充比例。 private void UpdateEnergyFill() { if (energyFillImage != null) { energyFillImage.fillAmount = Mathf.Clamp01(currentEnergy / maxEnergy); } } // 根据当前护盾值更新天蓝色填充比例与显隐。 private void UpdateShieldFill() { if (shieldRow != null && shieldFillImage != null) { if (currentShield > 0f) { shieldRow.gameObject.SetActive(true); // 1. 护盾条左对齐:锚点改为左上角 shieldRow.anchorMin = new Vector2(0f, 1f); shieldRow.anchorMax = new Vector2(0f, 1f); shieldRow.pivot = new Vector2(0f, 1f); shieldRow.anchoredPosition = new Vector2(0f, 0f); // 2. 护盾条的物理宽度按 (maxShield / maxHealth) 比例,代表护盾容量占血条的比例 float shieldRatio = maxHealth > 0f ? Mathf.Clamp01(maxShield / maxHealth) : 1.0f; float targetWidth = PixelWidth * Mathf.Max(0.05f, shieldRatio); shieldRow.sizeDelta = new Vector2(targetWidth, ShieldPixelHeight); // 3. 天蓝色填充条按 (currentShield / maxShield) 动态扣减 float fillRatio = maxShield > 0f ? Mathf.Clamp01(currentShield / maxShield) : 1.0f; shieldFillImage.fillAmount = fillRatio; shieldFillImage.SetVerticesDirty(); } else { shieldRow.gameObject.SetActive(false); } } } // 创建并缓存一个直角矩形白色 Sprite,供边框和填充复用。 private static Sprite RoundedSprite() { if (roundedSprite != null) return roundedSprite; const int width = 64; const int height = 16; Texture2D texture = new Texture2D(width, height, TextureFormat.RGBA32, false); texture.wrapMode = TextureWrapMode.Clamp; texture.filterMode = FilterMode.Bilinear; texture.hideFlags = HideFlags.HideAndDontSave; // 填充纯白色,无圆角 for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { texture.SetPixel(x, y, Color.white); } } texture.Apply(); roundedSprite = Sprite.Create(texture, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f), width); roundedSprite.hideFlags = HideFlags.HideAndDontSave; return roundedSprite; } // 在单位头顶生成一个漂浮伤害数字;数字独立于单位存活,死亡单位也能看到最后一击。 // TextMesh 不支持原生描边,用 4 个偏移的黑色副本 + 1 个白色主字组合出黑边效果。 private void SpawnDamageNumber(float amount, bool isHeal, bool isVictimFriend = false) { if (DamageNumber.ActiveCount >= DamageNumber.MaxActive) { return; } GameObject rootObject = new GameObject("DamageNumber"); Vector3 spawnPosition = transform.position + Vector3.up * damageNumberSpawnHeight; spawnPosition.x += Random.Range(-0.3f, 0.3f); rootObject.transform.position = spawnPosition; string text = Mathf.RoundToInt(amount).ToString(); float outline = DamageNumberOutlineWidth; // 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(); number.duration = damageNumberDuration; // 友方受伤向下浮动(负值),敌方受伤向上浮动(正值) number.riseHeight = isVictimFriend ? -damageNumberRiseHeight : damageNumberRiseHeight; } // 创建一个配置好的数字 TextMesh 子物体。 private static TextMesh CreateNumberText(Transform parent, string text, Vector3 localOffset, Color color, float characterSize, int sortingOrder) { GameObject textObject = new GameObject("Text"); textObject.transform.SetParent(parent, false); textObject.transform.localPosition = localOffset; Font font = GetBuiltinFont(); TextMesh textMesh = textObject.AddComponent(); textMesh.text = text; textMesh.font = font; textMesh.fontSize = 40; textMesh.characterSize = characterSize; textMesh.anchor = TextAnchor.MiddleCenter; textMesh.alignment = TextAlignment.Center; textMesh.fontStyle = FontStyle.Bold; textMesh.richText = false; textMesh.color = color; MeshRenderer textRenderer = textObject.GetComponent(); if (font != null) { textRenderer.sharedMaterial = font.material; } // 渲染在单位之上、血条(100)之下;白主字必须比黑边高一级, // 同材质同 sortingOrder 的渲染顺序不确定,后画的黑边会盖住白字。 textRenderer.sortingOrder = sortingOrder; return textMesh; } // 缓存引擎内置字体;Tuanjie 2022.3 使用 LegacyRuntime,旧版本回退 Arial。 private static Font cachedBuiltinFont; private static Font GetBuiltinFont() { if (cachedBuiltinFont == null) { try { cachedBuiltinFont = Resources.GetBuiltinResource("LegacyRuntime.ttf"); } catch { cachedBuiltinFont = null; } if (cachedBuiltinFont == null) { try { cachedBuiltinFont = Resources.GetBuiltinResource("Arial.ttf"); } catch { cachedBuiltinFont = null; } } } return cachedBuiltinFont; } } // 漂浮伤害数字:上升、缩放回弹、淡出后自动销毁。 // 独立于所属单位存活,即使单位死亡仍会完整播放动画。 public class DamageNumber : MonoBehaviour { // 全场同时存在的伤害数字上限,防止极端情况下刷屏。 public const int MaxActive = 60; public static int ActiveCount { get; private set; } public float duration = 0.8f; public float riseHeight = 1.2f; // 一个数字由主字 + 8 方向描边副本共 9 个 TextMesh 组成,统一驱动透明度。 private TextMesh[] textMeshes; private Color[] startColors; private Vector3 startPosition; private Camera targetCamera; private float elapsed; private void Awake() { ActiveCount++; textMeshes = GetComponentsInChildren(); startColors = new Color[textMeshes.Length]; for (int i = 0; i < textMeshes.Length; i++) { startColors[i] = textMeshes[i].color; } startPosition = transform.position; } private void Start() { GameObject mainCameraObject = GameObject.Find("Main Camera"); if (mainCameraObject != null) { targetCamera = mainCameraObject.GetComponent(); } if (targetCamera == null) { targetCamera = Camera.main; } } private void Update() { elapsed += Time.deltaTime; if (elapsed >= duration) { Destroy(gameObject); return; } float t = elapsed / duration; // 匀加速上升。 Vector3 position = startPosition; position.y += riseHeight * Mathf.SmoothStep(0f, 1f, t); transform.position = position; // 前20%从1.35倍缩放回弹到1倍。 float scale = t < 0.2f ? Mathf.Lerp(1.35f, 1f, t / 0.2f) : 1f; transform.localScale = Vector3.one * scale; // 后30%淡出。 float alpha = t < 0.7f ? 1f : 1f - (t - 0.7f) / 0.3f; for (int i = 0; i < textMeshes.Length; i++) { if (textMeshes[i] != null) { textMeshes[i].color = new Color(startColors[i].r, startColors[i].g, startColors[i].b, alpha); } } // 每帧朝向摄像机。TextMesh 可读面朝本地-Z,所以 +Z 要背向摄像机, // 否则文字从背面被看到会左右镜像。 if (targetCamera != null) { transform.rotation = Quaternion.LookRotation( transform.position - targetCamera.transform.position, targetCamera.transform.up); } } private void OnDestroy() { ActiveCount--; } }