구닥다리 제거

This commit is contained in:
2026-07-30 10:49:41 +09:00
parent fc4093ee93
commit 28f014152c
8 changed files with 108 additions and 66 deletions

Binary file not shown.

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 489593e55eab6a8489fae884d1d22ad2
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -2,7 +2,7 @@
`Communication/Dialog` 시스템(`DialogGroup` / `DialogNode` / `DialogChoice`)을 `Communication/Dialog` 시스템(`DialogGroup` / `DialogNode` / `DialogChoice`)을
**노드 그래프로 저작**하기 위한 에디터 전용 도구입니다. **노드 그래프로 저작**하기 위한 에디터 전용 도구입니다.
Unity GraphToolkit(`com.unity.graphtoolkit`, experimental) 기반. Unity GraphToolkit(`Unity.GraphToolkit.Editor`) 기반 — Unity 6000.4부터 **에디터 내장 모듈**입니다.
## 동작 개요 ## 동작 개요
- `.dlg` 그래프 에셋을 노드로 편집 → 저장하면 `DialogGraphImporter` - `.dlg` 그래프 에셋을 노드로 편집 → 저장하면 `DialogGraphImporter`
@@ -30,8 +30,11 @@ Unity GraphToolkit(`com.unity.graphtoolkit`, experimental) 기반.
- `DialogGraphNode.cs` — 공통 베이스(실행 포트 헬퍼) - `DialogGraphNode.cs` — 공통 베이스(실행 포트 헬퍼)
- `DialogStartNode.cs` — 진입 노드 - `DialogStartNode.cs` — 진입 노드
- `DialogLineNode.cs` — 대사 + 가변 N지선다 노드 - `DialogLineNode.cs` — 대사 + 가변 N지선다 노드
- `DialogAffectionNode.cs`— 호감도 조건식만 검사해 True/False로 즉시 라우팅하는 노드
- `DialogText.cs` — TalkText 멀티라인 입력용 래퍼 타입 - `DialogText.cs` — TalkText 멀티라인 입력용 래퍼 타입
- `DialogTextDrawer.cs` — DialogText를 여러 줄 TextField로 그리는 CustomPropertyDrawer - `DialogTextDrawer.cs` — DialogText를 여러 줄 TextField로 그리는 CustomPropertyDrawer
- `DialogShortText.cs` — 한 줄 텍스트 포트용 래퍼 타입 (이름 오버라이드 등)
- `DialogShortTextDrawer.cs` — DialogShortText를 한 줄 TextField로 그리는 CustomPropertyDrawer
- `DialogGraphImporter.cs`— .dlg → DialogGroup/DialogNode 변환 - `DialogGraphImporter.cs`— .dlg → DialogGroup/DialogNode 변환
## TalkText 멀티라인 ## TalkText 멀티라인
@@ -42,6 +45,12 @@ Unity GraphToolkit(`com.unity.graphtoolkit`, experimental) 기반.
- 높이를 더 키우려면 `DialogTextDrawer``minHeight` 값을 조정. - 높이를 더 키우려면 `DialogTextDrawer``minHeight` 값을 조정.
## 메모 ## 메모
- GraphToolkit experimental(0.4.0-exp.2)이라 API가 바뀔 수 있습니다. - GraphToolkit API는 여전히 experimental이라 바뀔 수 있습니다. 단, **버전을 결정하는 것은
`Packages/manifest.json`의 핀이 아니라 에디터 버전**입니다 — Unity 6000.4부터 GraphToolkit이
에디터 내장 모듈(`UnityEditor.GraphToolkitModule`)이 되었고, `com.unity.graphtoolkit` 패키지는
코드가 없는 shim(`"type": "shim"`)일 뿐입니다. 즉 manifest가 그대로여도 **에디터를 올리면
API가 바뀔 수 있으므로**, 에디터 업그레이드 후엔 이 폴더가 컴파일되는지와
`Assets/07_Data/DialogGraph/*.dlg`가 정상 임포트되는지를 확인할 것.
(패키지 버전을 내려도 API는 고정되지 않습니다 — shim엔 코드가 없음)
- 분기 출력이 비어 있으면 해당 선택지의 `DestinationNode`는 null이 되어 대화가 종료됩니다. - 분기 출력이 비어 있으면 해당 선택지의 `DestinationNode`는 null이 되어 대화가 종료됩니다.
- 여러 경로에서 같은 노드로 연결하면(루프 포함) 하나의 `DialogNode`로 합쳐집니다. - 여러 경로에서 같은 노드로 연결하면(루프 포함) 하나의 `DialogNode`로 합쳐집니다.

View File

@@ -6,30 +6,43 @@
// DialogEnterUI.uxml의 ListView로 후보를 띄우고, 행을 클릭하면 그 인덱스를 반환한다. // DialogEnterUI.uxml의 ListView로 후보를 띄우고, 행을 클릭하면 그 인덱스를 반환한다.
// 대사 도중의 분기 선택지(ChoiceHud)와는 별개다 — 이건 대화를 시작하기 전 단계의 UI. // 대사 도중의 분기 선택지(ChoiceHud)와는 별개다 — 이건 대화를 시작하기 전 단계의 UI.
// DialogPlayer.SelectBeat가 재생 가능한 비트가 여럿일 때 사용한다. // DialogPlayer.SelectBeat가 재생 가능한 비트가 여럿일 때 사용한다.
[RequireComponent(typeof(UIDocument))] //
// UIDocument의 후속인 PanelRenderer를 쓴다. 요소 참조는 UI 리로드 콜백으로 받는다:
// - rootVisualElement가 준비됐는지 매번 확인하던 지연 초기화(EnsureRefs)가 사라진다.
// - 리로드되면 ListView 인스턴스 자체가 새로 만들어지므로, 콜백에서 매번 다시 연결한다.
[RequireComponent(typeof(PanelRenderer))]
public class DialogEnterHud : MonoBehaviour public class DialogEnterHud : MonoBehaviour
{ {
public static DialogEnterHud Instance { get; private set; } public static DialogEnterHud Instance { get; private set; }
private UIDocument _document; private PanelRenderer _panelRenderer;
private VisualElement _root; // 전체 토글 대상(#Body) private VisualElement _root; // 전체 토글 대상(#Body)
private ListView _listView; private ListView _listView;
private bool _ready;
// 같은 버전으로 콜백이 중복 호출될 때 헛일을 막는다 (Unity 권장 패턴)
private int _uiVersion = -1;
private List<DialogChoice> _options; private List<DialogChoice> _options;
private AwaitableCompletionSource<int> _completion; private AwaitableCompletionSource<int> _completion;
// 현재 표시 상태. 리로드로 요소가 새로 만들어졌을 때 이 값으로 복원한다
// (메뉴를 띄운 채 리로드돼도 대기 중인 Show가 그대로 살아 있다).
private bool _visible;
private void Awake() private void Awake()
{ {
if (Instance != null && Instance != this) { Destroy(gameObject); return; } if (Instance != null && Instance != this) { Destroy(gameObject); return; }
Instance = this; Instance = this;
_document = GetComponent<UIDocument>();
}
private void Start() => Hide(); _panelRenderer = GetComponent<PanelRenderer>();
// root가 이미 준비돼 있으면 즉시 호출되고, 이후 UI가 리로드될 때마다 다시 호출된다
_panelRenderer.RegisterUIReloadCallback(OnUIReload);
}
private void OnDestroy() private void OnDestroy()
{ {
if (_panelRenderer != null)
_panelRenderer.UnregisterUIReloadCallback(OnUIReload);
if (Instance == this) Instance = null; if (Instance == this) Instance = null;
} }
@@ -40,12 +53,11 @@ private void OnDisable()
_completion = null; _completion = null;
} }
// rootVisualElement가 준비된 뒤 한 번만 요소를 캐싱하고 ListView 콜백을 다. // UI가 (재)구성될 때마다 요소를 다시 잡고 ListView 콜백을 걸고 현재 상태를 되돌린다.
private bool EnsureRefs() private void OnUIReload(PanelRenderer panelRenderer, VisualElement root, int version)
{ {
if (_ready) return true; if (_uiVersion == version) return;
var root = _document != null ? _document.rootVisualElement : null; _uiVersion = version;
if (root == null) return false;
_root = root.Q<VisualElement>("Body"); _root = root.Q<VisualElement>("Body");
_listView = root.Q<ListView>(); _listView = root.Q<ListView>();
@@ -55,8 +67,8 @@ private bool EnsureRefs()
_listView.bindItem = BindRow; // item-template(DialogEnterRow)이 makeItem을 담당, 바인딩만 우리가 _listView.bindItem = BindRow; // item-template(DialogEnterRow)이 makeItem을 담당, 바인딩만 우리가
_listView.selectionChanged += OnSelectionChanged; _listView.selectionChanged += OnSelectionChanged;
} }
_ready = true;
return true; ApplyState(); // 첫 호출에선 _visible=false라 숨김 상태로 시작한다
} }
// 대기 중인 선택을 취소하고 메뉴를 닫는다 (다른 NPC와 대화를 새로 시작할 때 등). // 대기 중인 선택을 취소하고 메뉴를 닫는다 (다른 NPC와 대화를 새로 시작할 때 등).
@@ -67,17 +79,14 @@ private bool EnsureRefs()
public async Awaitable<int> Show(List<DialogChoice> options) public async Awaitable<int> Show(List<DialogChoice> options)
{ {
if (options == null || options.Count == 0) return 0; if (options == null || options.Count == 0) return 0;
if (!EnsureRefs()) return 0; if (_root == null || _listView == null) return 0; // UI가 아직 준비되지 않음
// 이전 호출이 아직 대기 중이면 먼저 취소한다 (마지막 호출이 이긴다). // 이전 호출이 아직 대기 중이면 먼저 취소한다 (마지막 호출이 이긴다).
CancelPending(); CancelPending();
_options = options; _options = options;
_listView.itemsSource = options; _visible = true;
_listView.ClearSelection(); ApplyState();
_listView.Rebuild();
_root.style.display = DisplayStyle.Flex;
var completion = new AwaitableCompletionSource<int>(); var completion = new AwaitableCompletionSource<int>();
_completion = completion; _completion = completion;
@@ -119,9 +128,22 @@ private void OnSelectionChanged(IEnumerable<object> _)
private void Hide() private void Hide()
{ {
if (!EnsureRefs()) return; _visible = false;
_listView.itemsSource = null; _options = null;
_listView.Rebuild(); ApplyState();
_root.style.display = DisplayStyle.None; }
// 캐시된 상태를 실제 요소에 반영한다.
// 요소가 아직 없으면(리로드 콜백 전) 조용히 넘어가고, 콜백이 오면 같은 함수로 복원된다.
private void ApplyState()
{
if (_listView != null)
{
_listView.itemsSource = _visible ? _options : null;
_listView.ClearSelection(); // 이전 선택이 남아 즉시 확정되는 것을 막는다
_listView.Rebuild();
}
if (_root != null)
_root.style.display = _visible ? DisplayStyle.Flex : DisplayStyle.None;
} }
} }

View File

@@ -3,67 +3,88 @@
// UI Toolkit 버전 대사 HUD. DialogUI.uxml의 요소(#SpeakerName / #DialogText / #DialogField)를 잡아 // UI Toolkit 버전 대사 HUD. DialogUI.uxml의 요소(#SpeakerName / #DialogText / #DialogField)를 잡아
// 화자 이름 + 대사를 표시한다. // 화자 이름 + 대사를 표시한다.
// 공개 API(Instance / Show / Hide)는 기존 uGUI 버전과 동일 — DialogPlayer는 수정 없이 그대로 쓴다. // 공개 API(Instance / Show / Hide)는 기존과 동일 — DialogPlayer는 수정 없이 그대로 쓴다.
[RequireComponent(typeof(UIDocument))] //
// UIDocument의 후속인 PanelRenderer를 쓴다. 요소 참조는 UI 리로드 콜백으로 받는다:
// - rootVisualElement가 준비됐는지 매번 확인하던 지연 초기화(EnsureRefs)가 사라진다.
// - 플레이 중 UXML을 수정해 UI가 리로드돼도 참조가 자동으로 다시 연결된다.
[RequireComponent(typeof(PanelRenderer))]
public class DialogHud : MonoBehaviour public class DialogHud : MonoBehaviour
{ {
public static DialogHud Instance { get; private set; } public static DialogHud Instance { get; private set; }
private UIDocument _document; private PanelRenderer _panelRenderer;
private VisualElement _panel; // 대사 패널(#DialogField) — 토글 대상 private VisualElement _panel; // 대사 패널(#DialogField) — 토글 대상
private Label _speakerName; private Label _speakerName;
private Label _dialogText; private Label _dialogText;
private bool _ready;
// 현재 표시 상태. 리로드로 요소가 새로 만들어졌을 때 이 값으로 복원한다.
private bool _visible;
private string _speakerText = string.Empty;
private string _bodyText = string.Empty;
// 같은 버전으로 콜백이 중복 호출될 때 헛일을 막는다 (Unity 권장 패턴)
private int _uiVersion = -1;
private void Awake() private void Awake()
{ {
if (Instance != null && Instance != this) { Destroy(gameObject); return; } if (Instance != null && Instance != this) { Destroy(gameObject); return; }
Instance = this; Instance = this;
_document = GetComponent<UIDocument>();
}
// 시작 시 숨김 (이 시점엔 UIDocument의 visual tree가 준비돼 있다) _panelRenderer = GetComponent<PanelRenderer>();
private void Start() => Hide(); // root가 이미 준비돼 있으면 즉시 호출되고, 이후 UI가 리로드될 때마다 다시 호출된다
_panelRenderer.RegisterUIReloadCallback(OnUIReload);
}
private void OnDestroy() private void OnDestroy()
{ {
if (_panelRenderer != null)
_panelRenderer.UnregisterUIReloadCallback(OnUIReload);
if (Instance == this) Instance = null; if (Instance == this) Instance = null;
} }
// rootVisualElement가 준비된 뒤 한 번만 요소를 캐싱한다. // UI가 (재)구성될 때마다 요소를 다시 잡고 현재 표시 상태를 그대로 되돌린다.
// (스크립트 실행 순서상 UIDocument보다 먼저 OnEnable이 돌 수 있어 지연 초기화로 안전하게 처리) private void OnUIReload(PanelRenderer panelRenderer, VisualElement root, int version)
private bool EnsureRefs()
{ {
if (_ready) return true; if (_uiVersion == version) return;
var root = _document != null ? _document.rootVisualElement : null; _uiVersion = version;
if (root == null) return false;
_panel = root.Q<VisualElement>("DialogField"); _panel = root.Q<VisualElement>("DialogField");
_speakerName = root.Q<Label>("SpeakerName"); _speakerName = root.Q<Label>("SpeakerName");
_dialogText = root.Q<Label>("DialogText"); _dialogText = root.Q<Label>("DialogText");
_ready = true;
return true; ApplyState(); // 첫 호출에선 _visible=false라 숨김 상태로 시작한다
} }
// 화자 이름 + 대사 표시. // 화자 이름 + 대사 표시.
// - speakerNameOverride가 비어있지 않으면 CharacterData.Name 대신 그 이름을 표시한다 (예: "???") // - speakerNameOverride가 비어있지 않으면 CharacterData.Name 대신 그 이름을 표시한다 (예: "???")
public void Show(CharacterData speaker, string text, string speakerNameOverride = null) public void Show(CharacterData speaker, string text, string speakerNameOverride = null)
{ {
if (!EnsureRefs()) return;
string speakerName = !string.IsNullOrEmpty(speakerNameOverride) ? speakerNameOverride string speakerName = !string.IsNullOrEmpty(speakerNameOverride) ? speakerNameOverride
: speaker != null ? speaker.Name : string.Empty; : speaker != null ? speaker.Name : string.Empty;
if (_speakerName != null) _speakerName.text = DialogVariables.Format(speakerName); // {key} 토큰 치환
if (_dialogText != null) _dialogText.text = DialogVariables.Format(text);
if (_panel != null) _panel.style.display = DisplayStyle.Flex; _speakerText = DialogVariables.Format(speakerName); // {key} 토큰 치환
_bodyText = DialogVariables.Format(text);
_visible = true;
ApplyState();
} }
public void Hide() public void Hide()
{ {
if (!EnsureRefs()) return; _speakerText = string.Empty;
if (_speakerName != null) _speakerName.text = string.Empty; _bodyText = string.Empty;
if (_dialogText != null) _dialogText.text = string.Empty; _visible = false;
if (_panel != null) _panel.style.display = DisplayStyle.None; ApplyState();
}
// 캐시된 상태를 실제 요소에 반영한다.
// 요소가 아직 없으면(리로드 콜백 전) 조용히 넘어가고, 콜백이 오면 같은 함수로 복원된다.
private void ApplyState()
{
if (_speakerName != null) _speakerName.text = _speakerText;
if (_dialogText != null) _dialogText.text = _bodyText;
if (_panel != null)
_panel.style.display = _visible ? DisplayStyle.Flex : DisplayStyle.None;
} }
} }