226 lines
12 KiB
C#
226 lines
12 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
|
// WLTextFxUtil.cs — 전투 텍스트 연출 3종이 공유하는 소소한 UI 조립 헬퍼 (WL-813g · #813)
|
|
//
|
|
// KillChainText · LootToast · StatPopup 이 자기 노드를 스스로 만든다(BuildIfNeeded 패턴 · 813c BossHpBar 선례).
|
|
// 프리팹에는 **빈 노드 1개 + 컴포넌트**만 들어가고 자식·좌표는 런타임에 만든다 →
|
|
// NewGameUI.prefab diff 를 "노드 추가"로만 유지한다(발주서 §2 ⓒ).
|
|
//
|
|
// 값은 하나도 들고 있지 않다(전부 WLCombatTextSettings). 여기 있는 숫자는 Unity 기본값(앵커 0/1)뿐이다.
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
using System.Collections.Generic;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
|
|
namespace WL.UI
|
|
{
|
|
internal static class WLTextFxUtil
|
|
{
|
|
internal const int UILayer = 5; // 813c 와 같은 UI 레이어 인덱스
|
|
|
|
/// <summary>이름으로 자식을 찾고 없으면 RectTransform 자식을 만든다.</summary>
|
|
internal static RectTransform NewChild(RectTransform parent, string name)
|
|
{
|
|
var t = parent.Find(name) as RectTransform;
|
|
if (t != null) return t;
|
|
var go = new GameObject(name, typeof(RectTransform));
|
|
go.layer = UILayer;
|
|
t = (RectTransform)go.transform;
|
|
t.SetParent(parent, false);
|
|
t.localScale = Vector3.one;
|
|
t.localRotation = Quaternion.identity;
|
|
return t;
|
|
}
|
|
|
|
/// <summary>부모를 가득 채우도록 늘린다.</summary>
|
|
internal static void Stretch(RectTransform rt)
|
|
{
|
|
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
|
|
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
|
|
rt.pivot = new Vector2(0.5f, 0.5f);
|
|
}
|
|
|
|
/// <summary>자식 TextMeshProUGUI 를 만들거나 찾는다(레이캐스트 off · 폰트는 있으면 주입).</summary>
|
|
internal static TextMeshProUGUI NewText(RectTransform parent, string name, TMP_FontAsset font, TextAlignmentOptions align)
|
|
{
|
|
var t = NewChild(parent, name);
|
|
var label = t.GetComponent<TextMeshProUGUI>();
|
|
if (label == null) label = t.gameObject.AddComponent<TextMeshProUGUI>();
|
|
label.raycastTarget = false;
|
|
label.alignment = align;
|
|
// 줄바꿈·오버플로 API 는 TMP 버전마다 이름이 갈려(enableWordWrapping ↔ textWrappingMode) 건드리지 않는다 — 기본값 유지.
|
|
if (font != null) label.font = font;
|
|
return label;
|
|
}
|
|
|
|
/// <summary>px → 캔버스 유닛 계수(802b 규약 · 캔버스 폭 / 기준 폭). 캔버스가 없으면 1.</summary>
|
|
internal static float UnitsPerPx(Component c, WLCombatTextSettings s)
|
|
{
|
|
if (s == null) return 1f;
|
|
var canvas = c != null ? c.GetComponentInParent<Canvas>() : null;
|
|
return s.UnitsPerPx(canvas != null ? canvas.rootCanvas : null);
|
|
}
|
|
|
|
/// <summary>0 → 1 로 지난 비율(life 는 남은 시간).</summary>
|
|
internal static float Passed(float life, float lifeMax)
|
|
{
|
|
return lifeMax > 0f ? 1f - Mathf.Clamp01(life / lifeMax) : 1f;
|
|
}
|
|
|
|
/// <summary>fadeStart 비율 이후에만 1 → 0 으로 떨어지는 알파.</summary>
|
|
internal static float FadeAlpha(float passed, float fadeStart)
|
|
{
|
|
fadeStart = Mathf.Clamp01(fadeStart);
|
|
if (passed < fadeStart) return 1f;
|
|
if (fadeStart >= 1f) return 0f;
|
|
return 1f - Mathf.Clamp01((passed - fadeStart) / (1f - fadeStart));
|
|
}
|
|
|
|
/// <summary>등장 펀치 스케일(punchSeconds 안에 punchScale → 1 로 수렴).</summary>
|
|
internal static float PunchScale(float elapsed, float punchScale, float punchSeconds)
|
|
{
|
|
if (punchSeconds <= 0f || punchScale <= 1f) return 1f;
|
|
float k = Mathf.Clamp01(elapsed / punchSeconds);
|
|
return Mathf.Lerp(punchScale, 1f, k * k);
|
|
}
|
|
|
|
// ── 아웃라인 · 그림자 (WL-813y · Q3 결함 D-6) ─────────────────────────
|
|
// uGUI `Outline`/`Shadow`(BaseMeshEffect)는 **TextMeshPro 에 적용되지 않는다** —
|
|
// TMP_Text 는 IMeshModifier 를 부르지 않고 자체 메시를 만든다(실측 근거는 완료보고).
|
|
// 그래서 TMP 의 정식 경로인 **머티리얼 아웃라인(_OutlineWidth/_OutlineColor)** +
|
|
// **언더레이 그림자(UNDERLAY_ON · _UnderlayOffsetX/Y · _UnderlaySoftness)** 를 쓴다. 포스트 0.
|
|
// 라벨마다 인스턴스 머티리얼을 만들면 배칭이 깨지므로, (원본 머티리얼 + 값) 조합마다
|
|
// **변형 머티리얼 1장**을 캐시해 `fontSharedMaterial` 로 공유한다.
|
|
|
|
private static readonly Dictionary<string, Material> s_edgeMats = new Dictionary<string, Material>();
|
|
private static readonly Dictionary<Material, Material> s_edgeOrigin = new Dictionary<Material, Material>();
|
|
|
|
/// <summary>지금까지 만든 아웃라인 변형 머티리얼 수(진단).</summary>
|
|
internal static int EdgeMaterialCount { get { return s_edgeMats.Count; } }
|
|
|
|
/// <summary>TMP 라벨에 아웃라인 + 언더레이 그림자를 입힌다. 적용 결과 문자열(실측)을 돌려준다.</summary>
|
|
internal static string ApplyEdge(TMP_Text label, WLTextEdge edge)
|
|
{
|
|
if (label == null) return "label=null";
|
|
|
|
Material src = label.fontSharedMaterial;
|
|
if (src == null) return "sharedMaterial=null";
|
|
Material origin;
|
|
if (s_edgeOrigin.TryGetValue(src, out origin)) src = origin; // 이미 우리 변형이면 원본으로 되돌아가서 다시 만든다
|
|
|
|
if (!edge.enabled || (edge.outlineWidth <= 0f && edge.shadowColor.a <= 0f))
|
|
{
|
|
if (!ReferenceEquals(label.fontSharedMaterial, src)) { label.fontSharedMaterial = src; label.UpdateMeshPadding(); }
|
|
return "edge off → 원본 머티리얼(" + src.name + ")";
|
|
}
|
|
|
|
string key = src.GetInstanceID() + "|" + edge.Key();
|
|
Material mat;
|
|
if (!s_edgeMats.TryGetValue(key, out mat) || mat == null)
|
|
{
|
|
mat = new Material(src);
|
|
mat.name = src.name + " (WL813y Edge)";
|
|
mat.hideFlags = HideFlags.DontSave;
|
|
Write(mat, edge);
|
|
s_edgeMats[key] = mat;
|
|
s_edgeOrigin[mat] = src;
|
|
}
|
|
if (!ReferenceEquals(label.fontSharedMaterial, mat))
|
|
{
|
|
label.fontSharedMaterial = mat;
|
|
label.UpdateMeshPadding(); // 아웃라인이 글리프 밖으로 나가므로 패딩을 다시 잡는다
|
|
}
|
|
return Describe(mat);
|
|
}
|
|
|
|
private static void Write(Material m, WLTextEdge e)
|
|
{
|
|
if (m.HasProperty(ShaderUtilities.ID_OutlineWidth)) m.SetFloat(ShaderUtilities.ID_OutlineWidth, Mathf.Clamp01(e.outlineWidth));
|
|
if (m.HasProperty(ShaderUtilities.ID_OutlineColor)) m.SetColor(ShaderUtilities.ID_OutlineColor, e.outlineColor);
|
|
if (m.HasProperty(ShaderUtilities.ID_FaceDilate)) m.SetFloat(ShaderUtilities.ID_FaceDilate, Mathf.Clamp(e.faceDilate, -1f, 1f));
|
|
|
|
bool shadow = e.shadowColor.a > 0f && (Mathf.Abs(e.shadowOffset.x) > 0f || Mathf.Abs(e.shadowOffset.y) > 0f || e.shadowSoftness > 0f);
|
|
if (m.HasProperty(ShaderUtilities.ID_UnderlayColor))
|
|
{
|
|
m.SetColor(ShaderUtilities.ID_UnderlayColor, shadow ? e.shadowColor : new Color(0f, 0f, 0f, 0f));
|
|
if (m.HasProperty(ShaderUtilities.ID_UnderlayOffsetX)) m.SetFloat(ShaderUtilities.ID_UnderlayOffsetX, e.shadowOffset.x);
|
|
if (m.HasProperty(ShaderUtilities.ID_UnderlayOffsetY)) m.SetFloat(ShaderUtilities.ID_UnderlayOffsetY, e.shadowOffset.y);
|
|
if (m.HasProperty(ShaderUtilities.ID_UnderlaySoftness)) m.SetFloat(ShaderUtilities.ID_UnderlaySoftness, Mathf.Clamp01(e.shadowSoftness));
|
|
if (m.HasProperty(ShaderUtilities.ID_UnderlayDilate)) m.SetFloat(ShaderUtilities.ID_UnderlayDilate, 0f);
|
|
if (shadow) m.EnableKeyword("UNDERLAY_ON"); else m.DisableKeyword("UNDERLAY_ON");
|
|
}
|
|
if (m.HasProperty(ShaderUtilities.ID_OutlineWidth) && e.outlineWidth > 0f) m.EnableKeyword("OUTLINE_ON");
|
|
}
|
|
|
|
/// <summary>머티리얼에 실제로 들어간 값(실측 덤프용).</summary>
|
|
internal static string Describe(Material m)
|
|
{
|
|
if (m == null) return "(머티리얼 없음)";
|
|
var sb = new System.Text.StringBuilder();
|
|
sb.Append(m.name).Append(" shader=").Append(m.shader != null ? m.shader.name : "-");
|
|
sb.Append(" outlineW=").Append(m.HasProperty(ShaderUtilities.ID_OutlineWidth) ? m.GetFloat(ShaderUtilities.ID_OutlineWidth).ToString("F3") : "없음");
|
|
sb.Append(" outlineC=").Append(m.HasProperty(ShaderUtilities.ID_OutlineColor) ? ColorUtility.ToHtmlStringRGBA(m.GetColor(ShaderUtilities.ID_OutlineColor)) : "없음");
|
|
if (m.HasProperty(ShaderUtilities.ID_UnderlayColor))
|
|
{
|
|
sb.Append(" underlay=").Append(ColorUtility.ToHtmlStringRGBA(m.GetColor(ShaderUtilities.ID_UnderlayColor)));
|
|
sb.Append(" off(").Append(m.GetFloat(ShaderUtilities.ID_UnderlayOffsetX).ToString("F2")).Append(',')
|
|
.Append(m.GetFloat(ShaderUtilities.ID_UnderlayOffsetY).ToString("F2")).Append(')');
|
|
sb.Append(" soft=").Append(m.GetFloat(ShaderUtilities.ID_UnderlaySoftness).ToString("F2"));
|
|
sb.Append(" UNDERLAY_ON=").Append(m.IsKeywordEnabled("UNDERLAY_ON"));
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>라벨을 원본 머티리얼로 되돌린다(프로브·롤백).</summary>
|
|
internal static string ClearEdge(TMP_Text label)
|
|
{
|
|
if (label == null) return "label=null";
|
|
Material origin;
|
|
if (label.fontSharedMaterial != null && s_edgeOrigin.TryGetValue(label.fontSharedMaterial, out origin))
|
|
{
|
|
label.fontSharedMaterial = origin;
|
|
label.UpdateMeshPadding();
|
|
return "원복 → " + origin.name;
|
|
}
|
|
return "변형 아님 — 그대로";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 텍스트 테두리·그림자 값 묶음 (WL-813y · C8/C45 — 코드 상수 0 · 전부 SO).
|
|
/// TMP 머티리얼 규격 그대로다: outlineWidth 0~1 · shadowOffset 은 폰트 단위(±1) · softness 0~1.
|
|
/// </summary>
|
|
[System.Serializable]
|
|
public struct WLTextEdge
|
|
{
|
|
[Tooltip("이 텍스트에 아웃라인/그림자를 입힐지")]
|
|
public bool enabled;
|
|
[Tooltip("아웃라인 두께 (TMP _OutlineWidth · 0~1 · 0.2~0.3 이 육안으로 또렷)")]
|
|
[Range(0f, 1f)] public float outlineWidth;
|
|
[Tooltip("아웃라인 색")]
|
|
public Color outlineColor;
|
|
[Tooltip("그림자(언더레이) 오프셋 — 폰트 단위(±1). 예 (0.6, -0.6) = 우하단")]
|
|
public Vector2 shadowOffset;
|
|
[Tooltip("그림자 색 — 알파 0 이면 그림자 없음")]
|
|
public Color shadowColor;
|
|
[Tooltip("그림자 번짐 (TMP _UnderlaySoftness · 0~1)")]
|
|
[Range(0f, 1f)] public float shadowSoftness;
|
|
[Tooltip("글자 두께 보정 (TMP _FaceDilate · -1~1). 아웃라인이 글자를 먹으면 살짝 올린다")]
|
|
[Range(-1f, 1f)] public float faceDilate;
|
|
|
|
internal string Key()
|
|
{
|
|
return (enabled ? "1" : "0") + outlineWidth.ToString("F3") + outlineColor + shadowOffset + shadowColor +
|
|
shadowSoftness.ToString("F3") + faceDilate.ToString("F3");
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return "on=" + enabled + " w=" + outlineWidth.ToString("F2") +
|
|
" oc=#" + ColorUtility.ToHtmlStringRGBA(outlineColor) +
|
|
" sh" + shadowOffset + " sc=#" + ColorUtility.ToHtmlStringRGBA(shadowColor) +
|
|
" soft=" + shadowSoftness.ToString("F2") + " dilate=" + faceDilate.ToString("F2");
|
|
}
|
|
}
|
|
}
|