using UnityEngine; using UnityEngine.UI; public class WorldHealthBar : MonoBehaviour { // 最大生命值。 [Min(1f)] public float maxHealth = 100f; // 当前生命值,范围会自动限制在 0 到 maxHealth 之间。 [Min(0f)] public float currentHealth = 100f; // 技能能量上限,所有单位默认使用100点。 [Header("技能能量")] [Min(1f)] public float maxEnergy = 100f; // 当前技能能量,范围会自动限制在 0 到 maxEnergy 之间。 [Min(0f)] public float currentEnergy = 0f; 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); // 血条 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 TotalPixelHeight = PixelHeight + BarGap + EnergyPixelHeight; // 世界空间 Canvas,用来承载血条 UI。 private Canvas canvas; // 血条整体的 RectTransform。 private RectTransform barRect; // 绿色血量填充 Image。 private Image fillImage; // 金色技能能量填充 Image。 private Image energyFillImage; // 血条相对于单位的本地位置。 private Vector3 localBarPosition; // 血条最终使用的世界宽度。 private float worldWidth; // 所有血条共用的圆角 Sprite。 private static Sprite roundedSprite; // 防止死亡逻辑重复执行。 private bool isDead; // 初始化血量和血条 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(); UpdateFill(); UpdateEnergyFill(); } // 每帧让血条保持在单位头顶,并朝向主摄像机。 private void LateUpdate() { if (barRect == null) return; barRect.localPosition = GetBarRootPosition(); // 优先使用场景中名称完全为 Main Camera 的摄像机。 Camera camera = FindMainCamera(); if (camera == null) return; canvas.worldCamera = camera; if (camera.gameObject.name == "Main Camera") { // 使用摄像机的上方向,保证血条在摄像机画面中保持水平。 barRect.rotation = Quaternion.LookRotation( camera.transform.position - barRect.position, camera.transform.up); } else { // 找不到 Main Camera 时,保留原来的展示逻辑。 barRect.rotation = Quaternion.LookRotation( camera.transform.position - barRect.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(); } } // 让单位受到伤害。 public void TakeDamage(float amount) => SetHealth(currentHealth - amount); // 让单位恢复生命值。 public void Heal(float amount) => SetHealth(currentHealth + amount); // 增加技能能量,并返回本次增加后是否达到满能量。 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() { if (isDead) { return; } currentEnergy = 0f; 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); 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)); localBarPosition = new Vector3(0f, top.y + heightOffset, 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, TotalPixelHeight); barRect.localScale = Vector3.one * (worldWidth / PixelWidth); barRect.localPosition = GetBarRootPosition(); RectTransform healthRow = CreateRow(barObject.transform, "HealthBar", PixelWidth, PixelHeight, 0); Image background = CreateImage("Border", healthRow, RoundedSprite(), borderColor); SetFullRect(background.rectTransform); Image inner = CreateImage("Fill", healthRow, RoundedSprite(), GetHealthFillColor()); 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; RectTransform energyRow = CreateRow( barObject.transform, "EnergyBar", EnergyPixelWidth, EnergyPixelHeight, 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; } // 返回整体 Canvas 的位置,使血条中心保持原来的高度。 private Vector3 GetBarRootPosition() { float pixelToWorld = worldWidth / PixelWidth; float energyOffset = (TotalPixelHeight - PixelHeight) * 0.5f * pixelToWorld; return localBarPosition - Vector3.up * energyOffset; } // 创建一行固定像素高度的世界空间 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() { return CompareTag("Enemy") ? new Color(0.88f, 0.12f, 0.12f, 1f) : new Color(0.1f, 0.8f, 0.18f, 1f); } private Color GetEnergyFillColor() { return new Color(1f, 0.72f, 0.08f, 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); } } // 创建并缓存一个带圆角的白色 Sprite,供边框和填充复用。 private static Sprite RoundedSprite() { if (roundedSprite != null) return roundedSprite; const int width = 64; const int height = 16; const float radius = 5f; 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++) { float dx = Mathf.Max(Mathf.Max(radius - x, 0f), x - (width - 1 - radius)); float dy = Mathf.Max(Mathf.Max(radius - y, 0f), y - (height - 1 - radius)); bool inside = dx * dx + dy * dy <= radius * radius; texture.SetPixel(x, y, inside ? Color.white : Color.clear); } } 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; } }