대화구조 수정
This commit is contained in:
@@ -28,6 +28,10 @@ public class DialogHud : MonoBehaviour
|
||||
private string _speakerText = string.Empty;
|
||||
private Typewriter _typewriter;
|
||||
|
||||
// 강조 단어([[...]])의 맥동 연출. 지금 대사에 강조가 있을 때만 매 프레임 다시 그린다.
|
||||
private readonly TextGlow _glow = new();
|
||||
private bool _hasEmphasis;
|
||||
|
||||
// 같은 버전으로 콜백이 중복 호출될 때 헛일을 막는다 (Unity 권장 패턴)
|
||||
private int _uiVersion = -1;
|
||||
|
||||
@@ -66,9 +70,21 @@ private void OnUIReload(PanelRenderer panelRenderer, VisualElement root, int ver
|
||||
_speakerName = root.Q<Label>("SpeakerName");
|
||||
_dialogText = root.Q<Label>("DialogText");
|
||||
|
||||
// 글자별 정점 후처리 연결 — Label이 리로드로 새로 만들어질 때마다 다시 걸어야 한다
|
||||
if (_dialogText != null)
|
||||
_dialogText.PostProcessTextVertices = _glow.Process;
|
||||
|
||||
ApplyState(); // 첫 호출에선 _visible=false라 숨김 상태로 시작한다
|
||||
}
|
||||
|
||||
// 맥동은 매 프레임 정점을 다시 만들어야 보이므로 리페인트를 요청한다.
|
||||
// 강조가 없는 대사에서는 아무 일도 하지 않는다.
|
||||
private void Update()
|
||||
{
|
||||
if (_visible && _hasEmphasis && _glow.IsAnimating && _dialogText != null)
|
||||
_dialogText.MarkDirtyRepaint();
|
||||
}
|
||||
|
||||
// 화자 이름 + 대사 표시. 대사는 style(없으면 기본 스타일)로 한 글자씩 드러난다.
|
||||
// - speakerNameOverride가 비어있지 않으면 CharacterData.Name 대신 그 이름을 표시한다 (예: "???")
|
||||
// - style은 이 대사만의 연출 (DialogNode.Typewriter). 둘 다 비면 한 번에 표시된다.
|
||||
@@ -81,15 +97,22 @@ private void OnUIReload(PanelRenderer panelRenderer, VisualElement root, int ver
|
||||
_speakerText = DialogVariables.Format(speakerName); // {key} 토큰 치환
|
||||
_visible = true;
|
||||
|
||||
var effective = style != null ? style : _defaultStyle;
|
||||
|
||||
// 토큰 치환 → 강조 표기 펼치기 순서. 토큰 값 안에 [[...]]가 있어도 강조가 적용된다.
|
||||
string body = DialogMarkup.Expand(DialogVariables.Format(text), effective);
|
||||
_hasEmphasis = body.Contains("<link=" + TextGlow.LINK_ID);
|
||||
_glow.SetStyle(effective);
|
||||
|
||||
// Begin이 진행 콜백으로 ApplyState를 부르므로 _visible을 먼저 세워 둔다
|
||||
_typewriter.Begin(DialogVariables.Format(text), style != null ? style : _defaultStyle,
|
||||
destroyCancellationToken);
|
||||
_typewriter.Begin(body, effective, destroyCancellationToken);
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
_speakerText = string.Empty;
|
||||
_visible = false;
|
||||
_hasEmphasis = false;
|
||||
_typewriter.Clear(); // 타이핑 사운드 정지 + ApplyState 호출
|
||||
}
|
||||
|
||||
@@ -97,7 +120,17 @@ public void Hide()
|
||||
// 요소가 아직 없으면(리로드 콜백 전) 조용히 넘어가고, 콜백이 오면 같은 함수로 복원된다.
|
||||
private void ApplyState()
|
||||
{
|
||||
if (_speakerName != null) _speakerName.text = _speakerText;
|
||||
if (_speakerName != null)
|
||||
{
|
||||
_speakerName.text = _speakerText;
|
||||
|
||||
// 서술(화자 없는 대사)에서는 이름표를 감추되 **자리는 그대로 남긴다**.
|
||||
// display:None은 레이아웃에서 요소를 빼버려서 DialogText(flex-grow:1)가 그 공간까지
|
||||
// 차지해 대사가 위로 올라온다. visibility:Hidden은 position이 Relative인 요소의
|
||||
// 공간을 유지하므로, 화자가 있으나 없으나 대사 영역 크기가 동일하다.
|
||||
_speakerName.style.visibility = string.IsNullOrEmpty(_speakerText)
|
||||
? Visibility.Hidden : Visibility.Visible;
|
||||
}
|
||||
|
||||
if (_dialogText != null)
|
||||
{
|
||||
|
||||
54
Assets/02_Scripts/_UI/DialogMarkup.cs
Normal file
54
Assets/02_Scripts/_UI/DialogMarkup.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
// 대사 텍스트의 저작용 단축 표기를 리치 텍스트로 펼친다.
|
||||
//
|
||||
// 그날 밤 [[피 묻은 칼]]을 봤습니다
|
||||
// ↓
|
||||
// 그날 밤 <link=glow><color=#FF3B30>피 묻은 칼</color></link>을 봤습니다
|
||||
//
|
||||
// 왜 단축 표기를 두는가: 그래프의 Talk Text 칸에 <color=#FF3B30>을 손으로 쓰면
|
||||
// 오타가 조용히 깨지고, 색을 바꿀 때 모든 대사를 찾아 고쳐야 한다.
|
||||
// 색은 TypewriterStyle.EmphasisColor 한 곳에서 관리된다.
|
||||
//
|
||||
// <link>을 같이 붙이는 이유: TextGlow가 Glyph.linkID로 강조 글자를 식별해 맥동시킨다.
|
||||
public static class DialogMarkup
|
||||
{
|
||||
public const string OPEN = "[[";
|
||||
public const string CLOSE = "]]";
|
||||
|
||||
public static string Expand(string text, TypewriterStyle style)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text) || text.IndexOf(OPEN, StringComparison.Ordinal) < 0)
|
||||
return text;
|
||||
|
||||
string hex = ColorUtility.ToHtmlStringRGB(style != null ? style.EmphasisColor : Color.red);
|
||||
|
||||
var sb = new StringBuilder(text.Length + 48);
|
||||
int i = 0;
|
||||
while (i < text.Length)
|
||||
{
|
||||
int open = text.IndexOf(OPEN, i, StringComparison.Ordinal);
|
||||
if (open < 0)
|
||||
{
|
||||
sb.Append(text, i, text.Length - i);
|
||||
break;
|
||||
}
|
||||
|
||||
int close = text.IndexOf(CLOSE, open + OPEN.Length, StringComparison.Ordinal);
|
||||
if (close < 0)
|
||||
{
|
||||
sb.Append(text, i, text.Length - i); // 닫히지 않은 표기 — 손대지 않고 그대로 둔다
|
||||
break;
|
||||
}
|
||||
|
||||
sb.Append(text, i, open - i);
|
||||
sb.Append("<link=").Append(TextGlow.LINK_ID).Append("><color=#").Append(hex).Append('>');
|
||||
sb.Append(text, open + OPEN.Length, close - open - OPEN.Length);
|
||||
sb.Append("</color></link>");
|
||||
i = close + CLOSE.Length;
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/_UI/DialogMarkup.cs.meta
Normal file
2
Assets/02_Scripts/_UI/DialogMarkup.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 880e12adc5032fa4d8771e062c033623
|
||||
58
Assets/02_Scripts/_UI/TextGlow.cs
Normal file
58
Assets/02_Scripts/_UI/TextGlow.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
// 강조 단어를 맥동시키는 글자별(정점) 효과.
|
||||
//
|
||||
// TextElement.PostProcessTextVertices에 연결하면 UI Toolkit이 글리프 정점을 만든 직후 이 콜백이
|
||||
// 불린다. Glyph.vertices는 NativeSlice<Vertex> — 실제 메시 버퍼를 가리키는 뷰라서, Glyph가
|
||||
// struct로 복사돼도 여기에 쓰면 그대로 화면에 반영된다.
|
||||
//
|
||||
// 대상은 <link=glow>로 표시된 글자뿐이다 (DialogMarkup이 붙인다).
|
||||
// 글리프 순번을 세지 않으므로 태그·공백·줄바꿈에 전혀 영향받지 않는다 —
|
||||
// Glyph에는 원본 문자 인덱스가 없어서 순번을 세는 방식은 추측에 기대야 한다.
|
||||
//
|
||||
// 중요: RGB만 밝게 올리고 **알파는 절대 건드리지 않는다.**
|
||||
// Typewriter가 아직 안 드러난 글자를 <alpha=#00>으로 숨기는데, 알파를 만지면
|
||||
// 아직 나오지 않아야 할 강조 단어가 미리 보여 버린다.
|
||||
public sealed class TextGlow
|
||||
{
|
||||
// DialogMarkup이 붙이는 <link=...> 값. 이 값으로 강조 글자를 식별한다.
|
||||
public const string LINK_ID = "glow";
|
||||
|
||||
private TypewriterStyle _style;
|
||||
|
||||
public void SetStyle(TypewriterStyle style) => _style = style;
|
||||
|
||||
// 맥동이 켜져 있는가 (매 프레임 MarkDirtyRepaint를 부를지 판단용)
|
||||
public bool IsAnimating => _style != null && _style.GlowStrength > 0f;
|
||||
|
||||
public void Process(TextElement.GlyphsEnumerable glyphs)
|
||||
{
|
||||
var style = _style;
|
||||
if (style == null || style.GlowStrength <= 0f) return;
|
||||
|
||||
// 0~1 왕복. unscaledTime이라 일시정지(timeScale 0) 중에도 맥동한다
|
||||
float wave = 0.5f - 0.5f * Mathf.Cos(Time.unscaledTime * style.GlowSpeed * 2f * Mathf.PI);
|
||||
float boost = wave * style.GlowStrength;
|
||||
|
||||
foreach (var glyph in glyphs)
|
||||
{
|
||||
if (glyph.linkID != LINK_ID) continue;
|
||||
|
||||
var verts = glyph.vertices;
|
||||
for (int i = 0; i < verts.Length; i++)
|
||||
{
|
||||
var v = verts[i];
|
||||
v.tint = Brighten(v.tint, boost);
|
||||
verts[i] = v; // NativeSlice는 실제 버퍼를 가리키므로 이 쓰기가 화면에 반영된다
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 흰색 쪽으로 boost만큼 당긴다. 알파는 그대로 유지 — 숨김 상태(alpha 0)를 보존해야 한다.
|
||||
private static Color32 Brighten(Color32 c, float boost) => new Color32(
|
||||
(byte)(c.r + (255 - c.r) * boost),
|
||||
(byte)(c.g + (255 - c.g) * boost),
|
||||
(byte)(c.b + (255 - c.b) * boost),
|
||||
c.a);
|
||||
}
|
||||
2
Assets/02_Scripts/_UI/TextGlow.cs.meta
Normal file
2
Assets/02_Scripts/_UI/TextGlow.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9031534da157fdd49b9077c83401f908
|
||||
Reference in New Issue
Block a user