```
feat(UnitBase): 添加单位分散机制和目标选择偏移 - 新增阵型分散参数:separationRadius、separationWeight、targetSpreadBias - 实现ComputeSeparation方法计算友军分离力,避免单位扎堆 - 添加确定性分散偏移,让不同单位偏好不同目标,避免全部涌向同一敌人 - 在移动逻辑中集成分离向量计算 feat(WorldHealthBar): 实现伤害数字显示系统 - 添加伤害数字相关配置参数:showDamageNumbers、damageNumberDuration等 - 实现TakeDamage和Heal方法中的伤害数字生成逻辑 - 创建DamageNumber组件处理数字的上升、缩放、淡出动画 - 实现白字黑边效果,使用TextMesh副本组合方式实现描边 refactor(CoreBattleTest): 更新AI巡逻和检测参数 - 将clickStopDistance替换为patrolStopDistance、patrolPause、patrolRadius - 添加leashRadius和friendlyBuildingTag参数 - 为攻击单位增加detectRange检测范围参数 chore(dependencies): 更新包管理依赖配置 - 添加TJGenerators和CodelyBridge包引用 - 调整包依赖顺序,保持XR模块引用 docs(CODELY): 更新开发日志记录 - 添加建筑系统核心脚本实现说明 - 记录NavMesh烘焙问题及解决方案 - 详细说明伤害数字系统的实现要点和技术难点 ```
This commit is contained in:
1 parent
8d43961f67
commit
4e892d74c4
7 files changed
+2060
-13
No files matched your search
@@ -33,6 +33,14 @@ public class WorldHealthBar : MonoBehaviour
|
||||
// 死亡时使用的浅蓝色粒子颜色。
|
||||
public Color deathEffectColor = new Color(0.55f, 0.9f, 1f, 1f);
|
||||
|
||||
[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;
|
||||
[Min(0f)] public float damageNumberOutlineWidth = 0.015f;
|
||||
|
||||
// 血条 UI 的像素尺寸,实际显示大小由 worldWidth 控制。
|
||||
private const int PixelWidth = 100;
|
||||
private const int PixelHeight = 14;
|
||||
@@ -146,11 +154,28 @@ public class WorldHealthBar : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// 让单位受到伤害。
|
||||
public void TakeDamage(float amount) => SetHealth(currentHealth - amount);
|
||||
// 让单位受到伤害,并弹出伤害数字。
|
||||
public void TakeDamage(float amount)
|
||||
{
|
||||
bool wasDead = isDead;
|
||||
SetHealth(currentHealth - amount);
|
||||
if (showDamageNumbers && amount > 0f && !wasDead)
|
||||
{
|
||||
SpawnDamageNumber(amount, false);
|
||||
}
|
||||
}
|
||||
|
||||
// 让单位恢复生命值。
|
||||
public void Heal(float amount) => SetHealth(currentHealth + amount);
|
||||
// 让单位恢复生命值,并弹出绿色治疗数字。
|
||||
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 bool AddEnergy(float amount)
|
||||
@@ -514,4 +539,186 @@ public class WorldHealthBar : MonoBehaviour
|
||||
roundedSprite.hideFlags = HideFlags.HideAndDontSave;
|
||||
return roundedSprite;
|
||||
}
|
||||
|
||||
// 在单位头顶生成一个漂浮伤害数字;数字独立于单位存活,死亡单位也能看到最后一击。
|
||||
// TextMesh 不支持原生描边,用 4 个偏移的黑色副本 + 1 个白色主字组合出黑边效果。
|
||||
private void SpawnDamageNumber(float amount, bool isHeal)
|
||||
{
|
||||
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;
|
||||
|
||||
// 黑色描边副本先画(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);
|
||||
|
||||
DamageNumber number = rootObject.AddComponent<DamageNumber>();
|
||||
number.duration = damageNumberDuration;
|
||||
number.riseHeight = 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>();
|
||||
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<MeshRenderer>();
|
||||
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<Font>("LegacyRuntime.ttf");
|
||||
}
|
||||
catch
|
||||
{
|
||||
cachedBuiltinFont = null;
|
||||
}
|
||||
|
||||
if (cachedBuiltinFont == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
cachedBuiltinFont = Resources.GetBuiltinResource<Font>("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;
|
||||
|
||||
// 一个数字由主字+描边副本共5个TextMesh组成,统一驱动透明度。
|
||||
private TextMesh[] textMeshes;
|
||||
private Color[] startColors;
|
||||
private Vector3 startPosition;
|
||||
private Camera targetCamera;
|
||||
private float elapsed;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ActiveCount++;
|
||||
textMeshes = GetComponentsInChildren<TextMesh>();
|
||||
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<Camera>();
|
||||
}
|
||||
|
||||
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--;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user