diff --git a/Assets/01_Scenes/SampleScene.unity.meta b/Assets/01_Scenes/SampleScene.unity.meta new file mode 100644 index 0000000..946c9bb --- /dev/null +++ b/Assets/01_Scenes/SampleScene.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 489593e55eab6a8489fae884d1d22ad2 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/01_Scenes/TestScene.unity b/Assets/01_Scenes/TestScene.unity index acf4f64..67a7604 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:10fbc32e5478e0d04827fb42b39becf4dcd0a6b3b3608c5ac2b5ea682dfe44c6 -size 15897 +oid sha256:f4edbc8daf52ee01600ad166b48fda5206798482438aac254024d86d9b5918c5 +size 19782 diff --git a/Assets/02_Scripts/Communication/Dialog/DialogPlayer.cs b/Assets/02_Scripts/Communication/Dialog/DialogPlayer.cs index dd029a5..ab14948 100644 --- a/Assets/02_Scripts/Communication/Dialog/DialogPlayer.cs +++ b/Assets/02_Scripts/Communication/Dialog/DialogPlayer.cs @@ -68,10 +68,10 @@ public async Awaitable Play() // 다른 NPC가 실제 대사를 재생 중이면 무시 — 대화 중 다른 NPC 상호작용 차단 if (_entryInProgress != null) return; - // 다른 NPC의 선택 메뉴가 떠 있으면 먼저 취소한다. + // 다른 NPC의 대화 선택 메뉴가 떠 있으면 먼저 취소한다. // (취소된 쪽의 Play가 이 자리에서 HUD 숨김 등 정리를 마친 뒤에 이쪽이 시작된다) - if (ChoiceHud.Instance != null) - ChoiceHud.Instance.CancelPending(); + if (DialogEnterHud.Instance != null) + DialogEnterHud.Instance.CancelPending(); // 선택 메뉴가 떠 있는 동안에도 재진입을 막아야 하므로 여기서 잠근다. IsPlaying = true; @@ -116,9 +116,9 @@ private async Awaitable PlayGroupForced(DialogGroup group) if (group == null || IsPlaying) return; if (_entryInProgress != null) return; // 다른 NPC가 대사 재생 중 - // 다른 NPC의 선택 메뉴가 떠 있으면 먼저 취소 - if (ChoiceHud.Instance != null) - ChoiceHud.Instance.CancelPending(); + // 다른 NPC의 대화 선택 메뉴가 떠 있으면 먼저 취소 + if (DialogEnterHud.Instance != null) + DialogEnterHud.Instance.CancelPending(); IsPlaying = true; PushActive(); @@ -156,10 +156,10 @@ private List FindPlayableBeats() return lm.Database.GetPlayableBeats(lm.Current, _voice.Character); } - // 재생 가능한 대화가 여럿일 때 ChoiceHud로 플레이어에게 고르게 한다. 취소되면 null. + // 재생 가능한 대화가 여럿일 때 DialogEnterHud로 플레이어에게 고르게 한다. 취소되면 null. private async Awaitable SelectBeat(List playable) { - if (ChoiceHud.Instance == null) + if (DialogEnterHud.Instance == null) return playable[0]; // 선택 UI가 없으면 기존처럼 최상단 우선 var options = new List(playable.Count); @@ -173,12 +173,12 @@ private async Awaitable SelectBeat(List playable) try { - int picked = await ChoiceHud.Instance.Show(null, options); + int picked = await DialogEnterHud.Instance.Show(options); return playable[picked]; } catch (OperationCanceledException) { - // 대기 중 ChoiceHud가 비활성화됨(씬 전환 등) — 재생하지 않음 + // 대기 중 DialogEnterHud가 비활성화됨(씬 전환 등) — 재생하지 않음 return null; } } diff --git a/Assets/02_Scripts/_UI/Communication/DialogEnterHud.cs b/Assets/02_Scripts/_UI/Communication/DialogEnterHud.cs new file mode 100644 index 0000000..62ca1bb --- /dev/null +++ b/Assets/02_Scripts/_UI/Communication/DialogEnterHud.cs @@ -0,0 +1,128 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UIElements; + +// "어떤 대화를 걸지" 고르는 메뉴 (역전재판식 "대화하기" — 후보가 여럿이면 골라서 시작). +// DialogEnterUI.uxml의 ListView로 후보를 띄우고, 행을 클릭하면 그 인덱스를 반환한다. +// 대사 도중의 분기 선택지(ChoiceHud)와는 별개다 — 이건 대화를 시작하기 전 단계의 UI. +// DialogPlayer.SelectBeat가 재생 가능한 비트가 여럿일 때 사용한다. +[RequireComponent(typeof(UIDocument))] +public class DialogEnterHud : MonoBehaviour +{ + public static DialogEnterHud Instance { get; private set; } + + private UIDocument _document; + private VisualElement _root; // 전체 토글 대상(#Body) + private ListView _listView; + private bool _ready; + + private List _options; + private AwaitableCompletionSource _completion; + + private void Awake() + { + if (Instance != null && Instance != this) { Destroy(gameObject); return; } + Instance = this; + _document = GetComponent(); + } + + private void Start() => Hide(); + + private void OnDestroy() + { + if (Instance == this) Instance = null; + } + + private void OnDisable() + { + // 씬 전환 등으로 비활성화될 때 진행 중 대기 정리 + _completion?.TrySetCanceled(); + _completion = null; + } + + // rootVisualElement가 준비된 뒤 한 번만 요소를 캐싱하고 ListView 콜백을 건다. + private bool EnsureRefs() + { + if (_ready) return true; + var root = _document != null ? _document.rootVisualElement : null; + if (root == null) return false; + + _root = root.Q("Body"); + _listView = root.Q(); + if (_listView != null) + { + _listView.selectionType = SelectionType.Single; + _listView.fixedItemHeight = 40; // 행 높이 (필요하면 인스펙터의 Fixed Item Height로 조정) + _listView.bindItem = BindRow; // item-template(DialogEnterRow)이 makeItem을 담당, 바인딩만 우리가 + _listView.selectionChanged += OnSelectionChanged; + } + _ready = true; + return true; + } + + // 대기 중인 선택을 취소하고 메뉴를 닫는다 (다른 NPC와 대화를 새로 시작할 때 등). + // 취소된 쪽의 await Show(...)는 OperationCanceledException으로 빠져나간다. + public void CancelPending() => _completion?.TrySetCanceled(); + + // 후보 목록을 띄우고 고른 인덱스를 반환한다. 취소되면 OperationCanceledException. + public async Awaitable Show(List options) + { + if (options == null || options.Count == 0) return 0; + if (!EnsureRefs()) return 0; + + // 이전 호출이 아직 대기 중이면 먼저 취소한다 (마지막 호출이 이긴다). + CancelPending(); + + _options = options; + _listView.itemsSource = options; + _listView.ClearSelection(); + _listView.Rebuild(); + + _root.style.display = DisplayStyle.Flex; + + var completion = new AwaitableCompletionSource(); + _completion = completion; + + int result; + try + { + result = await completion.Awaitable; + } + finally + { + // 내가 아직 현재 세션일 때만 정리 — 취소 직후 다른 NPC가 새로 띄운 메뉴를 지우면 안 된다 + if (_completion == completion) + { + _completion = null; + Hide(); + } + } + return result; + } + + // ListView 행(item-template = DialogEnterRow.uxml)에 후보 이름을 채운다. + // 행 템플릿의 Label엔 name이 없어 타입으로 찾는다. + private void BindRow(VisualElement element, int i) + { + var label = element.Q