chore(plastic): 更新 Plastic SCM 配置文件 更新了 .plastic/plastic.changes 和 .plastic/plastic.wktree 二进制配置文件以同步工作区状态。 ```
366 lines
13 KiB
C#
366 lines
13 KiB
C#
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
public class WorldHealthBar : MonoBehaviour
|
|
{
|
|
// 最大生命值。
|
|
[Min(1f)] public float maxHealth = 100f;
|
|
|
|
// 当前生命值,范围会自动限制在 0 到 maxHealth 之间。
|
|
[Min(0f)] public float currentHealth = 100f;
|
|
|
|
// 血条相对于单位头顶的额外高度。
|
|
[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);
|
|
|
|
// 血条内部填充颜色。
|
|
public Color fillColor = new Color(0.1f, 0.8f, 0.18f, 1f);
|
|
|
|
// 死亡特效持续时间,单位为秒。
|
|
[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;
|
|
|
|
// 世界空间 Canvas,用来承载血条 UI。
|
|
private Canvas canvas;
|
|
|
|
// 血条整体的 RectTransform。
|
|
private RectTransform barRect;
|
|
|
|
// 绿色血量填充 Image。
|
|
private Image fillImage;
|
|
|
|
// 血条相对于单位的本地位置。
|
|
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);
|
|
CalculateLayout();
|
|
CreateBar();
|
|
UpdateFill();
|
|
}
|
|
|
|
// 每帧让血条保持在单位头顶,并朝向主摄像机。
|
|
private void LateUpdate()
|
|
{
|
|
if (barRect == null) return;
|
|
barRect.localPosition = localBarPosition;
|
|
|
|
// 优先使用场景中名称完全为 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<Camera>();
|
|
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);
|
|
|
|
// 处理单位死亡:隐藏模型和血条,播放蓝色向上爆散特效。
|
|
private void Die()
|
|
{
|
|
isDead = true;
|
|
currentHealth = 0f;
|
|
UpdateFill();
|
|
|
|
foreach (Renderer renderer in GetComponentsInChildren<Renderer>())
|
|
{
|
|
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<ParticleSystem>();
|
|
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<ParticleSystemRenderer>();
|
|
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<Renderer>(true))
|
|
{
|
|
if (!found)
|
|
{
|
|
bounds = renderer.bounds;
|
|
found = true;
|
|
}
|
|
else
|
|
{
|
|
bounds.Encapsulate(renderer.bounds);
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
{
|
|
CharacterController controller = GetComponent<CharacterController>();
|
|
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>();
|
|
canvas.renderMode = RenderMode.WorldSpace;
|
|
canvas.sortingOrder = 100;
|
|
barObject.GetComponent<CanvasScaler>().dynamicPixelsPerUnit = 10f;
|
|
barRect = barObject.GetComponent<RectTransform>();
|
|
barRect.sizeDelta = new Vector2(PixelWidth, PixelHeight);
|
|
barRect.localScale = Vector3.one * (worldWidth / PixelWidth);
|
|
barRect.localPosition = localBarPosition;
|
|
|
|
Image background = CreateImage("Border", barObject.transform, RoundedSprite(), borderColor);
|
|
SetFullRect(background.rectTransform);
|
|
|
|
Image inner = CreateImage("Fill", barObject.transform, RoundedSprite(), fillColor);
|
|
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;
|
|
}
|
|
|
|
// 创建一个用于边框或填充的 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>();
|
|
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);
|
|
}
|
|
}
|
|
|
|
// 创建并缓存一个带圆角的白色 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;
|
|
}
|
|
} |