From 28f014152c658e0924320e1c25b095b2909135cc Mon Sep 17 00:00:00 2001 From: nakjun Date: Thu, 30 Jul 2026 10:49:41 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B5=AC=EB=8B=A5=EB=8B=A4=EB=A6=AC=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/01_Scenes/SampleScene.unity | 3 - Assets/01_Scenes/SampleScene.unity.meta | 7 -- Assets/01_Scenes/TestScene.unity | 4 +- .../Communication/Dialog/GraphTool/README.md | 13 +++- .../_UI/Communication/DialogEnterHud.cs | 68 ++++++++++++------ .../02_Scripts/_UI/Communication/DialogHud.cs | 71 ++++++++++++------- .../ProjectConfig/Mobile_Renderer.asset | 4 +- .../ProjectConfig/PC_Renderer.asset | 4 +- 8 files changed, 108 insertions(+), 66 deletions(-) delete mode 100644 Assets/01_Scenes/SampleScene.unity delete mode 100644 Assets/01_Scenes/SampleScene.unity.meta diff --git a/Assets/01_Scenes/SampleScene.unity b/Assets/01_Scenes/SampleScene.unity deleted file mode 100644 index 42200ef..0000000 --- a/Assets/01_Scenes/SampleScene.unity +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a0e30a6453a81773203498f22f8b671fb2dc5e1d19692aacb68a51afd3dd0294 -size 15897 diff --git a/Assets/01_Scenes/SampleScene.unity.meta b/Assets/01_Scenes/SampleScene.unity.meta deleted file mode 100644 index 946c9bb..0000000 --- a/Assets/01_Scenes/SampleScene.unity.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 489593e55eab6a8489fae884d1d22ad2 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/01_Scenes/TestScene.unity b/Assets/01_Scenes/TestScene.unity index a9324ac..61d6bcb 100644 --- a/Assets/01_Scenes/TestScene.unity +++ b/Assets/01_Scenes/TestScene.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77bf19d97db8f543f37c057eadf59caacde7b253fd9c5d67a447d63e8d3cda04 -size 21069 +oid sha256:86d391b69a080b393e583dc40c20fb41a4bf94dc05dec47d5305480c6b9c78e2 +size 19502 diff --git a/Assets/02_Scripts/Communication/Dialog/GraphTool/README.md b/Assets/02_Scripts/Communication/Dialog/GraphTool/README.md index 0417f9d..acc80b9 100644 --- a/Assets/02_Scripts/Communication/Dialog/GraphTool/README.md +++ b/Assets/02_Scripts/Communication/Dialog/GraphTool/README.md @@ -2,7 +2,7 @@ `Communication/Dialog` 시스템(`DialogGroup` / `DialogNode` / `DialogChoice`)을 **노드 그래프로 저작**하기 위한 에디터 전용 도구입니다. -Unity GraphToolkit(`com.unity.graphtoolkit`, experimental) 기반. +Unity GraphToolkit(`Unity.GraphToolkit.Editor`) 기반 — Unity 6000.4부터 **에디터 내장 모듈**입니다. ## 동작 개요 - `.dlg` 그래프 에셋을 노드로 편집 → 저장하면 `DialogGraphImporter`가 @@ -30,8 +30,11 @@ Unity GraphToolkit(`com.unity.graphtoolkit`, experimental) 기반. - `DialogGraphNode.cs` — 공통 베이스(실행 포트 헬퍼) - `DialogStartNode.cs` — 진입 노드 - `DialogLineNode.cs` — 대사 + 가변 N지선다 노드 +- `DialogAffectionNode.cs`— 호감도 조건식만 검사해 True/False로 즉시 라우팅하는 노드 - `DialogText.cs` — TalkText 멀티라인 입력용 래퍼 타입 - `DialogTextDrawer.cs` — DialogText를 여러 줄 TextField로 그리는 CustomPropertyDrawer +- `DialogShortText.cs` — 한 줄 텍스트 포트용 래퍼 타입 (이름 오버라이드 등) +- `DialogShortTextDrawer.cs` — DialogShortText를 한 줄 TextField로 그리는 CustomPropertyDrawer - `DialogGraphImporter.cs`— .dlg → DialogGroup/DialogNode 변환 ## TalkText 멀티라인 @@ -42,6 +45,12 @@ Unity GraphToolkit(`com.unity.graphtoolkit`, experimental) 기반. - 높이를 더 키우려면 `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이 되어 대화가 종료됩니다. - 여러 경로에서 같은 노드로 연결하면(루프 포함) 하나의 `DialogNode`로 합쳐집니다. diff --git a/Assets/02_Scripts/_UI/Communication/DialogEnterHud.cs b/Assets/02_Scripts/_UI/Communication/DialogEnterHud.cs index f3c4b8c..974fc0e 100644 --- a/Assets/02_Scripts/_UI/Communication/DialogEnterHud.cs +++ b/Assets/02_Scripts/_UI/Communication/DialogEnterHud.cs @@ -6,30 +6,43 @@ // DialogEnterUI.uxml의 ListView로 후보를 띄우고, 행을 클릭하면 그 인덱스를 반환한다. // 대사 도중의 분기 선택지(ChoiceHud)와는 별개다 — 이건 대화를 시작하기 전 단계의 UI. // DialogPlayer.SelectBeat가 재생 가능한 비트가 여럿일 때 사용한다. -[RequireComponent(typeof(UIDocument))] +// +// UIDocument의 후속인 PanelRenderer를 쓴다. 요소 참조는 UI 리로드 콜백으로 받는다: +// - rootVisualElement가 준비됐는지 매번 확인하던 지연 초기화(EnsureRefs)가 사라진다. +// - 리로드되면 ListView 인스턴스 자체가 새로 만들어지므로, 콜백에서 매번 다시 연결한다. +[RequireComponent(typeof(PanelRenderer))] public class DialogEnterHud : MonoBehaviour { public static DialogEnterHud Instance { get; private set; } - private UIDocument _document; + private PanelRenderer _panelRenderer; private VisualElement _root; // 전체 토글 대상(#Body) private ListView _listView; - private bool _ready; + + // 같은 버전으로 콜백이 중복 호출될 때 헛일을 막는다 (Unity 권장 패턴) + private int _uiVersion = -1; private List _options; private AwaitableCompletionSource _completion; + // 현재 표시 상태. 리로드로 요소가 새로 만들어졌을 때 이 값으로 복원한다 + // (메뉴를 띄운 채 리로드돼도 대기 중인 Show가 그대로 살아 있다). + private bool _visible; + private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; - _document = GetComponent(); - } - private void Start() => Hide(); + _panelRenderer = GetComponent(); + // root가 이미 준비돼 있으면 즉시 호출되고, 이후 UI가 리로드될 때마다 다시 호출된다 + _panelRenderer.RegisterUIReloadCallback(OnUIReload); + } private void OnDestroy() { + if (_panelRenderer != null) + _panelRenderer.UnregisterUIReloadCallback(OnUIReload); if (Instance == this) Instance = null; } @@ -40,12 +53,11 @@ private void OnDisable() _completion = null; } - // rootVisualElement가 준비된 뒤 한 번만 요소를 캐싱하고 ListView 콜백을 건다. - private bool EnsureRefs() + // UI가 (재)구성될 때마다 요소를 다시 잡고 ListView 콜백을 걸고 현재 상태를 되돌린다. + private void OnUIReload(PanelRenderer panelRenderer, VisualElement root, int version) { - if (_ready) return true; - var root = _document != null ? _document.rootVisualElement : null; - if (root == null) return false; + if (_uiVersion == version) return; + _uiVersion = version; _root = root.Q("Body"); _listView = root.Q(); @@ -55,8 +67,8 @@ private bool EnsureRefs() _listView.bindItem = BindRow; // item-template(DialogEnterRow)이 makeItem을 담당, 바인딩만 우리가 _listView.selectionChanged += OnSelectionChanged; } - _ready = true; - return true; + + ApplyState(); // 첫 호출에선 _visible=false라 숨김 상태로 시작한다 } // 대기 중인 선택을 취소하고 메뉴를 닫는다 (다른 NPC와 대화를 새로 시작할 때 등). @@ -67,17 +79,14 @@ private bool EnsureRefs() public async Awaitable Show(List options) { if (options == null || options.Count == 0) return 0; - if (!EnsureRefs()) return 0; + if (_root == null || _listView == null) return 0; // UI가 아직 준비되지 않음 // 이전 호출이 아직 대기 중이면 먼저 취소한다 (마지막 호출이 이긴다). CancelPending(); _options = options; - _listView.itemsSource = options; - _listView.ClearSelection(); - _listView.Rebuild(); - - _root.style.display = DisplayStyle.Flex; + _visible = true; + ApplyState(); var completion = new AwaitableCompletionSource(); _completion = completion; @@ -119,9 +128,22 @@ private void OnSelectionChanged(IEnumerable _) private void Hide() { - if (!EnsureRefs()) return; - _listView.itemsSource = null; - _listView.Rebuild(); - _root.style.display = DisplayStyle.None; + _visible = false; + _options = null; + ApplyState(); + } + + // 캐시된 상태를 실제 요소에 반영한다. + // 요소가 아직 없으면(리로드 콜백 전) 조용히 넘어가고, 콜백이 오면 같은 함수로 복원된다. + 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; } } diff --git a/Assets/02_Scripts/_UI/Communication/DialogHud.cs b/Assets/02_Scripts/_UI/Communication/DialogHud.cs index 162a26f..46249d1 100644 --- a/Assets/02_Scripts/_UI/Communication/DialogHud.cs +++ b/Assets/02_Scripts/_UI/Communication/DialogHud.cs @@ -3,67 +3,88 @@ // UI Toolkit 버전 대사 HUD. DialogUI.uxml의 요소(#SpeakerName / #DialogText / #DialogField)를 잡아 // 화자 이름 + 대사를 표시한다. -// 공개 API(Instance / Show / Hide)는 기존 uGUI 버전과 동일 — DialogPlayer는 수정 없이 그대로 쓴다. -[RequireComponent(typeof(UIDocument))] +// 공개 API(Instance / Show / Hide)는 기존과 동일 — DialogPlayer는 수정 없이 그대로 쓴다. +// +// UIDocument의 후속인 PanelRenderer를 쓴다. 요소 참조는 UI 리로드 콜백으로 받는다: +// - rootVisualElement가 준비됐는지 매번 확인하던 지연 초기화(EnsureRefs)가 사라진다. +// - 플레이 중 UXML을 수정해 UI가 리로드돼도 참조가 자동으로 다시 연결된다. +[RequireComponent(typeof(PanelRenderer))] public class DialogHud : MonoBehaviour { public static DialogHud Instance { get; private set; } - private UIDocument _document; + private PanelRenderer _panelRenderer; + private VisualElement _panel; // 대사 패널(#DialogField) — 토글 대상 private Label _speakerName; 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() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; - _document = GetComponent(); - } - // 시작 시 숨김 (이 시점엔 UIDocument의 visual tree가 준비돼 있다) - private void Start() => Hide(); + _panelRenderer = GetComponent(); + // root가 이미 준비돼 있으면 즉시 호출되고, 이후 UI가 리로드될 때마다 다시 호출된다 + _panelRenderer.RegisterUIReloadCallback(OnUIReload); + } private void OnDestroy() { + if (_panelRenderer != null) + _panelRenderer.UnregisterUIReloadCallback(OnUIReload); if (Instance == this) Instance = null; } - // rootVisualElement가 준비된 뒤 한 번만 요소를 캐싱한다. - // (스크립트 실행 순서상 UIDocument보다 먼저 OnEnable이 돌 수 있어 지연 초기화로 안전하게 처리) - private bool EnsureRefs() + // UI가 (재)구성될 때마다 요소를 다시 잡고 현재 표시 상태를 그대로 되돌린다. + private void OnUIReload(PanelRenderer panelRenderer, VisualElement root, int version) { - if (_ready) return true; - var root = _document != null ? _document.rootVisualElement : null; - if (root == null) return false; + if (_uiVersion == version) return; + _uiVersion = version; _panel = root.Q("DialogField"); _speakerName = root.Q