From 53bad3083667419d4a6b110546980406373489a0 Mon Sep 17 00:00:00 2001 From: sharedacc520k Date: Fri, 24 Jul 2026 21:11:32 +0900 Subject: [PATCH] =?UTF-8?q?3D=20=EC=9E=94=EC=9E=AC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/01_Scenes/TestScene.unity | 3 + ...eScene.unity.meta => TestScene.unity.meta} | 0 .../Communication/Dialog/DialogGestureItem.cs | 55 - .../Dialog/DialogGestureItem.cs.meta | 2 - .../Communication/Dialog/DialogNode.cs | 14 +- .../Communication/Dialog/DialogPlayer.cs | 61 +- .../Communication/Dialog/DialogTouchZone.cs | 46 + .../Dialog/DialogTouchZone.cs.meta | 2 + .../Dialog/GraphTool/Editor/DialogGraph.cs | 4 + .../GraphTool/Editor/DialogGraphImporter.cs | 38 +- .../Dialog/GraphTool/Editor/DialogLineNode.cs | 56 +- .../Communication/Dialog/HiddenBranch.cs | 23 + .../Communication/Dialog/HiddenBranch.cs.meta | 2 + .../Dialog/HiddenBranchResolver.cs | 76 +- .../02_Scripts/Communication/Voice/LipSync.cs | 109 -- .../Communication/Voice/LipSync.cs.meta | 2 - Assets/02_Scripts/Managers/SoundManager.cs | 23 +- Assets/08_UI.meta | 8 + Assets/08_UI/DialogEnterUI.uxml | 9 + Assets/08_UI/DialogEnterUI.uxml.meta | 10 + Assets/08_UI/DialogUI.uxml | 9 + Assets/08_UI/DialogUI.uxml.meta | 10 + Assets/08_UI/GlobalStyle.uss | 9 + Assets/08_UI/GlobalStyle.uss.meta | 12 + ProjectSettings/EditorBuildSettings.asset | 4 +- UIElementsSchema/UIElements.xsd | 32 + .../Unity.GraphToolkit.Editor.xsd | 13 + .../Unity.GraphToolkit.ItemLibrary.Editor.xsd | 13 + .../Unity.Multiplayer.PlayMode.Editor.xsd | 20 + .../Unity.Profiling.Editor.UI.xsd | 52 + UIElementsSchema/Unity.Profiling.Editor.xsd | 39 + UIElementsSchema/Unity.UI.Builder.xsd | 473 ++++++ UIElementsSchema/Unity.UIToolkit.Editor.xsd | 888 ++++++++++++ .../UnityEditor.Accessibility.xsd | 43 + ...ons.AnimationWindow.TimelineFoundation.xsd | 40 + ...tor.Animations.AnimationWindow.Widgets.xsd | 68 + .../UnityEditor.Audio.UIElements.xsd | 37 + .../UnityEditor.Experimental.GraphView.xsd | 30 + ...r.Inspector.GraphicsSettingsInspectors.xsd | 12 + UIElementsSchema/UnityEditor.Inspector.xsd | 15 + UIElementsSchema/UnityEditor.Overlays.xsd | 47 + ...UnityEditor.PackageManager.UI.Internal.xsd | 290 ++++ .../UnityEditor.Rendering.Converter.xsd | 12 + UIElementsSchema/UnityEditor.Rendering.xsd | 33 + UIElementsSchema/UnityEditor.Search.xsd | 15 + .../UnityEditor.ShaderGraph.Drawing.xsd | 41 + .../UnityEditor.ShortcutManagement.xsd | 21 + UIElementsSchema/UnityEditor.Toolbars.xsd | 12 + .../UnityEditor.UIElements.Debugger.xsd | 12 + ...UnityEditor.UIElements.ProjectSettings.xsd | 52 + UIElementsSchema/UnityEditor.UIElements.xsd | 292 ++++ UIElementsSchema/UnityEditor.xsd | 14 + .../UnityEngine.Accessibility.xsd | 22 + UIElementsSchema/UnityEngine.UIElements.xsd | 1288 +++++++++++++++++ UIElementsSchema/UnityEngine.xsd | 48 + 55 files changed, 4269 insertions(+), 292 deletions(-) create mode 100644 Assets/01_Scenes/TestScene.unity rename Assets/01_Scenes/{SampleScene.unity.meta => TestScene.unity.meta} (100%) delete mode 100644 Assets/02_Scripts/Communication/Dialog/DialogGestureItem.cs delete mode 100644 Assets/02_Scripts/Communication/Dialog/DialogGestureItem.cs.meta create mode 100644 Assets/02_Scripts/Communication/Dialog/DialogTouchZone.cs create mode 100644 Assets/02_Scripts/Communication/Dialog/DialogTouchZone.cs.meta create mode 100644 Assets/02_Scripts/Communication/Dialog/HiddenBranch.cs create mode 100644 Assets/02_Scripts/Communication/Dialog/HiddenBranch.cs.meta delete mode 100644 Assets/02_Scripts/Communication/Voice/LipSync.cs delete mode 100644 Assets/02_Scripts/Communication/Voice/LipSync.cs.meta create mode 100644 Assets/08_UI.meta create mode 100644 Assets/08_UI/DialogEnterUI.uxml create mode 100644 Assets/08_UI/DialogEnterUI.uxml.meta create mode 100644 Assets/08_UI/DialogUI.uxml create mode 100644 Assets/08_UI/DialogUI.uxml.meta create mode 100644 Assets/08_UI/GlobalStyle.uss create mode 100644 Assets/08_UI/GlobalStyle.uss.meta create mode 100644 UIElementsSchema/UIElements.xsd create mode 100644 UIElementsSchema/Unity.GraphToolkit.Editor.xsd create mode 100644 UIElementsSchema/Unity.GraphToolkit.ItemLibrary.Editor.xsd create mode 100644 UIElementsSchema/Unity.Multiplayer.PlayMode.Editor.xsd create mode 100644 UIElementsSchema/Unity.Profiling.Editor.UI.xsd create mode 100644 UIElementsSchema/Unity.Profiling.Editor.xsd create mode 100644 UIElementsSchema/Unity.UI.Builder.xsd create mode 100644 UIElementsSchema/Unity.UIToolkit.Editor.xsd create mode 100644 UIElementsSchema/UnityEditor.Accessibility.xsd create mode 100644 UIElementsSchema/UnityEditor.Animations.AnimationWindow.TimelineFoundation.xsd create mode 100644 UIElementsSchema/UnityEditor.Animations.AnimationWindow.Widgets.xsd create mode 100644 UIElementsSchema/UnityEditor.Audio.UIElements.xsd create mode 100644 UIElementsSchema/UnityEditor.Experimental.GraphView.xsd create mode 100644 UIElementsSchema/UnityEditor.Inspector.GraphicsSettingsInspectors.xsd create mode 100644 UIElementsSchema/UnityEditor.Inspector.xsd create mode 100644 UIElementsSchema/UnityEditor.Overlays.xsd create mode 100644 UIElementsSchema/UnityEditor.PackageManager.UI.Internal.xsd create mode 100644 UIElementsSchema/UnityEditor.Rendering.Converter.xsd create mode 100644 UIElementsSchema/UnityEditor.Rendering.xsd create mode 100644 UIElementsSchema/UnityEditor.Search.xsd create mode 100644 UIElementsSchema/UnityEditor.ShaderGraph.Drawing.xsd create mode 100644 UIElementsSchema/UnityEditor.ShortcutManagement.xsd create mode 100644 UIElementsSchema/UnityEditor.Toolbars.xsd create mode 100644 UIElementsSchema/UnityEditor.UIElements.Debugger.xsd create mode 100644 UIElementsSchema/UnityEditor.UIElements.ProjectSettings.xsd create mode 100644 UIElementsSchema/UnityEditor.UIElements.xsd create mode 100644 UIElementsSchema/UnityEditor.xsd create mode 100644 UIElementsSchema/UnityEngine.Accessibility.xsd create mode 100644 UIElementsSchema/UnityEngine.UIElements.xsd create mode 100644 UIElementsSchema/UnityEngine.xsd diff --git a/Assets/01_Scenes/TestScene.unity b/Assets/01_Scenes/TestScene.unity new file mode 100644 index 0000000..acf4f64 --- /dev/null +++ b/Assets/01_Scenes/TestScene.unity @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10fbc32e5478e0d04827fb42b39becf4dcd0a6b3b3608c5ac2b5ea682dfe44c6 +size 15897 diff --git a/Assets/01_Scenes/SampleScene.unity.meta b/Assets/01_Scenes/TestScene.unity.meta similarity index 100% rename from Assets/01_Scenes/SampleScene.unity.meta rename to Assets/01_Scenes/TestScene.unity.meta diff --git a/Assets/02_Scripts/Communication/Dialog/DialogGestureItem.cs b/Assets/02_Scripts/Communication/Dialog/DialogGestureItem.cs deleted file mode 100644 index e235cdf..0000000 --- a/Assets/02_Scripts/Communication/Dialog/DialogGestureItem.cs +++ /dev/null @@ -1,55 +0,0 @@ -using UnityEngine; -using UnityEngine.Events; - -// 대화 중 "건네기/터치" 제스처를 수행하는 오브젝트(꽃다발·김치·손 등)에 붙인다. -// 이 오브젝트가 NPC의 받는 존(_targetTag 태그의 트리거)에 닿으면, 자신의 GestureKey로 히든 분기를 발동시킨다. -// -// 어떤 아이템인지는 GestureKey로 구분된다: -// - 노드가 HiddenGestureKey="kimchi"로 무장 → 김치(GestureKey="kimchi")만 발동, 꽃다발("bouquet")은 무시. -// 그래서 "김치를 줘야 하는데 꽃다발을 주면 이벤트가 안 진행"이 자동으로 보장된다. -// -// 콜라이더가 있는 오브젝트에 붙일 것 (잡는 아이템은 보통 XRGrabInteractable이 있는 루트). -// 존은 무장 여부와 무관하게 항상 닿을 수 있지만, 무장 안 된 순간에 닿으면 아무 일도 안 일어난다. -public class DialogGestureItem : MonoBehaviour -{ - [Tooltip("이 오브젝트의 제스처 키. 대화 노드의 HiddenGestureKey와 일치할 때만 발동 (예: kimchi, bouquet, slap)")] - [SerializeField] private string _gestureKey; - - [Tooltip("이 태그를 가진 트리거(NPC 받는 존)에 닿아야 발동. 비우면 태그 무시(아무 트리거나)")] - [SerializeField] private string _targetTag = "GiveZone"; - - [Header("Consume (건넨 뒤 사라지게)")] - [Tooltip("켜면 발동 시 이 오브젝트가 사라진다(건넨 연출). 손처럼 사라지면 안 되는 건 꺼둘 것")] - [SerializeField] private bool _consumeOnGiven = true; - - [Tooltip("사라지는 방식: 켜면 완전 파괴(Destroy), 끄면 비활성화(SetActive false — 재사용 가능)")] - [SerializeField] private bool _destroy = true; - - [Tooltip("사라지기 직전 호출 (파티클·사운드 연출용)")] - [SerializeField] private UnityEvent _onConsumed; - - private void OnTriggerEnter(Collider other) - { - if (!string.IsNullOrEmpty(_targetTag) && !other.CompareTag(_targetTag)) return; - - // 노드가 이 키로 무장 중일 때만 true — 그때만 소비한다(엉뚱한 타이밍/엉뚱한 아이템은 안 사라짐). - if (!HiddenBranchResolver.Fire(_gestureKey)) return; - - if (_consumeOnGiven) - Consume(); - } - - private void Consume() - { - // 스크립트가 자식 콜라이더에 붙어 있어도 아이템 전체(Rigidbody 루트)를 처리 - var body = GetComponentInParent(); - var target = body != null ? body.gameObject : gameObject; - - _onConsumed?.Invoke(); - - if (_destroy) - Destroy(target); // XRGrabInteractable도 파괴 시 손에서 자동 해제됨 - else - target.SetActive(false); // 비활성화도 XRI가 선택을 취소해 손에서 놓게 됨 - } -} diff --git a/Assets/02_Scripts/Communication/Dialog/DialogGestureItem.cs.meta b/Assets/02_Scripts/Communication/Dialog/DialogGestureItem.cs.meta deleted file mode 100644 index 6a934b0..0000000 --- a/Assets/02_Scripts/Communication/Dialog/DialogGestureItem.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: c11477ec44fef124bb7279924beb66af \ No newline at end of file diff --git a/Assets/02_Scripts/Communication/Dialog/DialogNode.cs b/Assets/02_Scripts/Communication/Dialog/DialogNode.cs index d0b4991..9f2387b 100644 --- a/Assets/02_Scripts/Communication/Dialog/DialogNode.cs +++ b/Assets/02_Scripts/Communication/Dialog/DialogNode.cs @@ -25,17 +25,11 @@ public class DialogNode : ScriptableObject public List Choices; // 있으면 플레이어 선택 대기 [Header("Hidden Branch")] - [Tooltip("이 대사가 재생되는 동안 특정 행동(제스처)을 하면 Next/선택지 대신 이 노드로 몰래 분기. " + + [Tooltip("이 대사가 재생되는 동안 특정 행동(부위 터치·증거품 제출 등)을 하면 " + + "Next/선택지 대신 몰래 분기한다. 키마다 다른 목적지를 줄 수 있고, " + + "Key가 빈 항목은 나머지 전부를 받는 catch-all이 된다. " + "비우면 히든 분기 없음 — 평소엔 존재하지 않는 것처럼 보인다")] - public DialogNode HiddenBranch; - - [Tooltip("히든 분기를 여는 제스처 키 (아이템의 DialogGestureItem의 Gesture Key와 일치). " + - "예: kimchi, bouquet, slap. 비우면 아무 제스처 오브젝트나 발동 가능")] - public string HiddenGestureKey; - - [Tooltip("히든 분기를 탔을 때 기록할 선택지 Code (선택지의 Code와 동일한 용도). " + - "비우면 기록 안 함")] - public string HiddenCode; + public List HiddenBranches = new(); [Header("Affection Branch")] [Tooltip("켜면 이 노드는 대사 없이 호감도 조건만 검사해 즉시 라우팅한다: " + diff --git a/Assets/02_Scripts/Communication/Dialog/DialogPlayer.cs b/Assets/02_Scripts/Communication/Dialog/DialogPlayer.cs index a9d97be..dd029a5 100644 --- a/Assets/02_Scripts/Communication/Dialog/DialogPlayer.cs +++ b/Assets/02_Scripts/Communication/Dialog/DialogPlayer.cs @@ -24,7 +24,7 @@ public class DialogPlayer : MonoBehaviour // ── 전역 대화 진행 신호 (대화 중 월드 상호작용 차단용) ──────── // 선택 메뉴 단계부터 대사 종료까지, 어느 NPC든 대화가 진행 중이면 true. - // DialogInteractionBlocker가 구독해서 인터랙터를 잠근다. + // 이동 버튼·클릭 인터랙션 등이 AnyActiveChanged를 구독해 대화 중 입력을 잠그면 된다. public static bool IsAnyActive => _activeCount > 0; public static event Action AnyActiveChanged; private static int _activeCount; @@ -103,7 +103,7 @@ public async Awaitable Play() } } - // 조건/선택 메뉴를 건너뛰고 특정 대화 그룹을 강제로 시작한다 (EventZoneTrigger 등 UnityEvent 연결용). + // 조건/선택 메뉴를 건너뛰고 특정 대화 그룹을 강제로 시작한다 (버튼·컷신 등 UnityEvent 연결용). // DB에 등록된 비트면 그 설정(진행도 보상 등)을 그대로 쓰고, 없으면 임시 비트로 재생한다. // 완료 여부는 검사하지 않으므로(강제) 1회성이 필요하면 호출하는 쪽(존의 Trigger Once)에서 보장할 것. public void PlayGroup(DialogGroup group) @@ -204,26 +204,24 @@ private async Awaitable PlayBeat(StoryBeat beat) } routingHops = 0; // 실제 대사 노드에 도달 — 카운터 리셋 - // 이 노드가 히든 분기를 가지면, 노드가 재생되는 동안 제스처 감시를 무장한다. + // 이 노드가 히든 분기를 가지면, 노드가 재생되는 동안 발동 감시를 무장한다. // (무장 안 된 노드는 아래 대기/선택이 기존과 완전히 동일하게 동작) - bool armed = node.HiddenBranch != null; - if (armed) HiddenBranchResolver.Arm(node.HiddenGestureKey); + bool armed = node.HiddenBranches != null && node.HiddenBranches.Count > 0; + if (armed) HiddenBranchResolver.Arm(node.HiddenBranches); try { - bool diverted = await PlayNode(node); // 대사 표시 + 대기(무장 시 제스처 감시 포함) + bool diverted = await PlayNode(node); // 대사 표시 + 대기(무장 시 발동 감시 포함) if (diverted) { - RecordHiddenChoice(node); - node = node.HiddenBranch; // 대사 도중 제스처 발동 → 몰래 분기 + node = TakeHiddenBranch(node); // 대사 도중 발동 → 몰래 분기 } else if (node.Choices != null && node.Choices.Count > 0) { - int picked = await WaitForChoice(node); // 메뉴(무장 시 제스처 감시 포함) + int picked = await WaitForChoice(node); // 메뉴(무장 시 발동 감시 포함) if (picked == DivertIndex) { - RecordHiddenChoice(node); - node = node.HiddenBranch; // 메뉴 도중 제스처 발동 → 메뉴엔 없던 히든 분기 + node = TakeHiddenBranch(node); // 메뉴 도중 발동 → 메뉴엔 없던 히든 분기 } else { @@ -341,7 +339,7 @@ private async Awaitable PlayNode(DialogNode node) } // 진행은 항상 플레이어 입력 대기. - // 히든 분기가 무장된 노드는 대기 중 제스처를 함께 감시(레이스)한다. + // 히든 분기가 무장된 노드는 대기 중 히든 발동을 함께 감시(레이스)한다. if (HiddenBranchResolver.Armed) return await WaitAdvanceOrDivert(); @@ -393,12 +391,26 @@ private void RecordChoice(DialogNode node, int index) StoryManager.Instance.RecordChoice(code); } - // 히든 분기 기록: 노드의 HiddenCode를 선택지 Code와 동일하게 StoryState에 영구 기록 - private void RecordHiddenChoice(DialogNode node) + // 발동한 히든 분기로 이동한다. 어느 분기가 발동했는지는 Resolver가 키를 보고 이미 골라 뒀다. + // (Disarm보다 먼저 불려야 하므로 반드시 무장 구간 안에서 호출할 것) + private DialogNode TakeHiddenBranch(DialogNode node) { - string code = DialogVariables.Format(node.HiddenCode); // {token} 치환 + int index = HiddenBranchResolver.FiredIndex; + if (node.HiddenBranches == null || index < 0 || index >= node.HiddenBranches.Count) + { + // 정상 흐름에선 올 수 없다(발동 = 매칭 성공). 와도 대화가 끊기지 않게 원래 흐름으로. + Debug.LogWarning($"[DialogPlayer] 히든 분기 인덱스가 범위를 벗어남: {index}"); + return node.Next; + } + + var branch = node.HiddenBranches[index]; + + // 선택지 Code와 동일하게 StoryState에 영구 기록 + string code = DialogVariables.Format(branch.Code); // {token} 치환 if (!string.IsNullOrEmpty(code)) StoryManager.Instance.RecordChoice(code); + + return branch.Destination; } // 히든 분기 신호값 — 선택지 인덱스(0..n-1)와 절대 겹치지 않는 값. @@ -416,11 +428,11 @@ private async Awaitable WaitForChoice(DialogNode node) if (!HiddenBranchResolver.Armed) return await ChoiceHud.Instance.Show(node.ChoiceQuestion, node.Choices); - // 무장된 노드: 메뉴가 떠 있는 동안 제스처가 들어오면 메뉴를 취소하고 히든으로 분기. + // 무장된 노드: 메뉴가 떠 있는 동안 발동이 들어오면 메뉴를 취소하고 히든으로 분기. if (HiddenBranchResolver.Fired) return DivertIndex; // 메뉴가 뜨기 전에 이미 발동한 경우 - void OnFired() => ChoiceHud.Instance.CancelPending(); // 제스처 → 메뉴 즉시 취소 + void OnFired() => ChoiceHud.Instance.CancelPending(); // 발동 → 메뉴 즉시 취소 HiddenBranchResolver.FiredEvent += OnFired; try { @@ -428,7 +440,7 @@ private async Awaitable WaitForChoice(DialogNode node) } catch (OperationCanceledException) { - // 우리가 제스처로 취소한 것이면 히든 분기, 아니면(씬 전환 등) 위로 전파 + // 우리가 히든 발동으로 취소한 것이면 히든 분기, 아니면(씬 전환 등) 위로 전파 if (HiddenBranchResolver.Fired) return DivertIndex; throw; } @@ -438,8 +450,8 @@ private async Awaitable WaitForChoice(DialogNode node) } } - // 노드 대기 + 히든 제스처 감시(레이스). 무장된 노드에서만 호출된다. - // 반환: true = 대기 도중 제스처 발동(→히든 분기), false = 평범하게 진행(입력) + // 노드 대기 + 히든 발동 감시(레이스). 무장된 노드에서만 호출된다. + // 반환: true = 대기 도중 히든 발동(→히든 분기), false = 평범하게 진행(입력) private async Awaitable WaitAdvanceOrDivert() { var im = InputManager.Instance; @@ -454,7 +466,7 @@ private async Awaitable WaitAdvanceOrDivert() { while (true) { - if (HiddenBranchResolver.Fired) return true; // 제스처 발동 → 히든 분기 + if (HiddenBranchResolver.Fired) return true; // 히든 발동 → 히든 분기 if (advance) return false; // 진행 입력 → 평범하게 진행 if (timeLeft >= 0f) { @@ -503,15 +515,16 @@ private async Awaitable WaitForAdvanceInput() } } - //테스트용 + //테스트용 — 캐릭터를 클릭하면 대화 시작 (Collider2D가 있어야 잡힌다) private void Update() { if (Mouse.current == null) return; if (!Mouse.current.leftButton.wasPressedThisFrame) return; if (Camera.main == null) return; - var ray = Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue()); - if (Physics.Raycast(ray, out var hit) && hit.transform.IsChildOf(transform)) + var world = Camera.main.ScreenToWorldPoint(Mouse.current.position.ReadValue()); + var hit = Physics2D.OverlapPoint(world); + if (hit != null && hit.transform.IsChildOf(transform)) { Debug.Log("캐릭터 클릭"); _ = Play(); // 테스트용 fire-and-forget (Update는 await 불가) diff --git a/Assets/02_Scripts/Communication/Dialog/DialogTouchZone.cs b/Assets/02_Scripts/Communication/Dialog/DialogTouchZone.cs new file mode 100644 index 0000000..dd2f8f2 --- /dev/null +++ b/Assets/02_Scripts/Communication/Dialog/DialogTouchZone.cs @@ -0,0 +1,46 @@ +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.InputSystem; + +// 캐릭터의 특정 부위(손·머리·어깨 등)에 붙이는 클릭 판정 영역. +// 대화 노드가 히든 분기로 무장한 동안 이 영역을 마우스로 누르면 그 분기가 열린다. +// +// 어느 부위인지는 Key로 구분된다: +// - 노드에 Key="head" 분기가 있으면 머리 존(Key="head")이 그 분기를 연다. +// - 어디에도 안 걸리면 노드의 catch-all 분기(Key 빈 항목)로 가고, 그것도 없으면 아무 일도 안 일어난다. +// +// 무장 중이 아니면 클릭 판정 자체를 하지 않는다 → 평소엔 존재하지 않는 것처럼 동작(=히든). +// +// 배치: Collider2D가 있는 오브젝트에 붙일 것 (보통 캐릭터의 자식). +// isTrigger 여부는 상관없다 — 충돌이 아니라 점 판정만 쓴다. +[RequireComponent(typeof(Collider2D))] +public class DialogTouchZone : MonoBehaviour +{ + [Tooltip("이 부위의 키. 대화 노드의 히든 분기 Key와 일치할 때 그 분기를 연다 (예: head, hand, cheek)")] + [SerializeField] private string _key; + + [Tooltip("실제로 분기가 열렸을 때 호출 (연출·사운드용). 발동하지 않은 클릭에는 불리지 않는다")] + [SerializeField] private UnityEvent _onTouched; + + private Collider2D _collider; + + private void Awake() => _collider = GetComponent(); + + private void Update() + { + // 무장 안 된 순간에는 클릭을 아예 보지 않는다 + if (!HiddenBranchResolver.Armed) return; + + if (Mouse.current == null || !Mouse.current.leftButton.wasPressedThisFrame) return; + + var cam = Camera.main; + if (cam == null || _collider == null) return; + + Vector2 world = cam.ScreenToWorldPoint(Mouse.current.position.ReadValue()); + if (!_collider.OverlapPoint(world)) return; + + // 이 노드에 맞는 분기가 있을 때만 true (무장 구간당 1회) + if (HiddenBranchResolver.Fire(_key)) + _onTouched?.Invoke(); + } +} diff --git a/Assets/02_Scripts/Communication/Dialog/DialogTouchZone.cs.meta b/Assets/02_Scripts/Communication/Dialog/DialogTouchZone.cs.meta new file mode 100644 index 0000000..87147dd --- /dev/null +++ b/Assets/02_Scripts/Communication/Dialog/DialogTouchZone.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4eb9e02881f5bc04c955f656e949db78 \ No newline at end of file diff --git a/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogGraph.cs b/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogGraph.cs index 0b3cd65..10be139 100644 --- a/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogGraph.cs +++ b/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogGraph.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using Unity.GraphToolkit.Editor; using UnityEditor; @@ -8,6 +9,9 @@ namespace DinoLove.Dialog.GraphTool.Editor // 기존 Communication/Dialog 시스템(DialogGroup / DialogNode / DialogChoice)을 // 노드 그래프로 저작하기 위한 에디터 전용 그래프 타입이다. // 임포트 시 DialogGraphImporter가 이 그래프를 DialogGroup 에셋으로 변환한다. + // [Serializable]: GraphToolkit이 이 그래프 인스턴스를 .dlg 안에 [SerializeReference]로 저장한다. + // 없으면 "missing the [Serializable] attribute" 경고가 뜨고, 필드를 추가해도 직렬화되지 않는다. + [Serializable] [Graph(AssetExtension)] internal class DialogGraph : Graph { diff --git a/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogGraphImporter.cs b/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogGraphImporter.cs index 419f182..54790c2 100644 --- a/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogGraphImporter.cs +++ b/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogGraphImporter.cs @@ -10,7 +10,7 @@ namespace DinoLove.Dialog.GraphTool.Editor // .dlg 그래프 에셋을 기존 런타임 타입(DialogGroup / DialogNode / DialogChoice)으로 변환한다. // 생성된 DialogNode들은 서브에셋으로, DialogGroup이 메인 에셋으로 등록된다. // 따라서 DialogPlayer는 수정 없이 임포트된 .dlg 에셋(= DialogGroup)을 그대로 사용한다. - [ScriptedImporter(15, DialogGraph.AssetExtension)] // 버전 올리면 기존 .dlg 에셋이 재임포트됨 + [ScriptedImporter(16, DialogGraph.AssetExtension)] // 버전 올리면 기존 .dlg 에셋이 재임포트됨 internal class DialogGraphImporter : ScriptedImporter { public override void OnImportAsset(AssetImportContext ctx) @@ -53,6 +53,7 @@ public override void OnImportAsset(AssetImportContext ctx) var dn = ScriptableObject.CreateInstance(); dn.Choices = new List(); + dn.HiddenBranches = new List(); map[gn] = dn; order.Add(gn); @@ -144,21 +145,19 @@ public override void OnImportAsset(AssetImportContext ctx) } } - // 히든 분기 (켜져 있으면) — 목적지 노드 + 제스처 키 + 기록 코드 - bool hasHidden = false; - line.GetNodeOptionByName(DialogLineNode.OPTION_HIDDEN_BRANCH)?.TryGetValue(out hasHidden); - if (hasHidden) + // 히든 분기 — 분기마다 [여는 키 + 목적지 노드 + 기록 코드] + int hiddenCount = 0; + line.GetNodeOptionByName(DialogLineNode.OPTION_HIDDEN_COUNT)?.TryGetValue(out hiddenCount); + + for (int i = 0; i < hiddenCount; i++) { - var hiddenDest = GetConnectedNode(gn, DialogLineNode.PORT_HIDDEN_OUT); - dn.HiddenBranch = hiddenDest != null && map.TryGetValue(hiddenDest, out var hiddenDn) ? hiddenDn : null; - - string gestureKey = null; - line.GetNodeOptionByName(DialogLineNode.OPTION_HIDDEN_GESTURE)?.TryGetValue(out gestureKey); - dn.HiddenGestureKey = gestureKey; - - string hiddenCode = null; - line.GetNodeOptionByName(DialogLineNode.OPTION_HIDDEN_CODE)?.TryGetValue(out hiddenCode); - dn.HiddenCode = hiddenCode; + var hiddenDest = GetConnectedNode(gn, DialogLineNode.HiddenOutPort(i)); + dn.HiddenBranches.Add(new HiddenBranch + { + Key = GetInputPortValue(gn.GetInputPortByName(DialogLineNode.HiddenKeyPort(i))), + Code = GetInputPortValue(gn.GetInputPortByName(DialogLineNode.HiddenCodePort(i))), + Destination = hiddenDest != null && map.TryGetValue(hiddenDest, out var hiddenDn) ? hiddenDn : null + }); } } @@ -193,10 +192,11 @@ static IEnumerable GetSuccessors(INode node) } // 히든 분기 후속도 도달 가능해야 DialogNode로 생성된다 - bool hasHidden = false; - line.GetNodeOptionByName(DialogLineNode.OPTION_HIDDEN_BRANCH)?.TryGetValue(out hasHidden); - if (hasHidden) - yield return GetConnectedNode(node, DialogLineNode.PORT_HIDDEN_OUT); + int hiddenCount = 0; + line.GetNodeOptionByName(DialogLineNode.OPTION_HIDDEN_COUNT)?.TryGetValue(out hiddenCount); + + for (int i = 0; i < hiddenCount; i++) + yield return GetConnectedNode(node, DialogLineNode.HiddenOutPort(i)); } // 출력 실행 포트에 연결된 노드 (없으면 null) diff --git a/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogLineNode.cs b/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogLineNode.cs index 62848ce..2a2716c 100644 --- a/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogLineNode.cs +++ b/Assets/02_Scripts/Communication/Dialog/GraphTool/Editor/DialogLineNode.cs @@ -24,18 +24,20 @@ internal class DialogLineNode : DialogGraphNode public const string PORT_AFFECTION = "Affection"; public const string PORT_PROGRESS = "Progress"; public const string PORT_QUESTION = "ChoiceQuestion"; - public const string PORT_HIDDEN_OUT = "HiddenOut"; - public const string OPTION_CHOICE_COUNT = "ChoiceCount"; - public const string OPTION_HIDDEN_BRANCH = "HasHiddenBranch"; - public const string OPTION_HIDDEN_GESTURE = "HiddenGestureKey"; - public const string OPTION_HIDDEN_CODE = "HiddenCode"; + public const string OPTION_CHOICE_COUNT = "ChoiceCount"; + public const string OPTION_HIDDEN_COUNT = "HiddenBranchCount"; // 선택지별 포트 이름 규칙 (임포터와 공유) public static string ChoiceTextPort(int i) => $"Choice{i}Text"; public static string ChoiceCodePort(int i) => $"Choice{i}Code"; public static string ChoiceOutPort(int i) => $"Choice{i}Out"; + // 히든 분기별 포트 이름 규칙 (임포터와 공유) + public static string HiddenKeyPort(int i) => $"Hidden{i}Key"; + public static string HiddenCodePort(int i) => $"Hidden{i}Code"; + public static string HiddenOutPort(int i) => $"Hidden{i}Out"; + protected override void OnDefineOptions(IOptionDefinitionContext context) { context.AddOption(OPTION_CHOICE_COUNT) @@ -44,20 +46,12 @@ protected override void OnDefineOptions(IOptionDefinitionContext context) .WithDefaultValue(0) .Delayed(); - context.AddOption(OPTION_HIDDEN_BRANCH) - .WithDisplayName("Hidden Branch") - .WithTooltip("켜면 맨 아래에 'Hidden →' 출력 포트가 생긴다. 이 대사 재생 중 특정 제스처(싸대기/터치)를 하면 " + - "Next/선택지 대신 그 포트로 연결된 노드로 몰래 분기한다") - .WithDefaultValue(false); - - context.AddOption(OPTION_HIDDEN_GESTURE) - .WithDisplayName("Hidden Gesture Key") - .WithTooltip("히든 분기를 여는 제스처 키 (DialogGestureZone의 Gesture Key와 일치). 비우면 아무 제스처 존이나 발동") - .Delayed(); - - context.AddOption(OPTION_HIDDEN_CODE) - .WithDisplayName("Hidden Code") - .WithTooltip("히든 분기를 탔을 때 기록할 선택지 Code. 비우면 기록 안 함") + context.AddOption(OPTION_HIDDEN_COUNT) + .WithDisplayName("Hidden Branch Count") + .WithTooltip("이 대사에 달 히든 분기 개수. 0이면 히든 분기 없음. " + + "1 이상이면 분기마다 [Key · Code 입력 포트]와 'Hidden N →' 출력 포트가 생긴다. " + + "재생 중 그 Key로 발동하면 Next/선택지 대신 해당 포트로 몰래 분기한다") + .WithDefaultValue(0) .Delayed(); } @@ -106,12 +100,24 @@ protected override void OnDefinePorts(IPortDefinitionContext context) } } - // 히든 분기 출력 — 켜져 있으면 선형/선택지 관계없이 맨 아래에 붙는다. - // 이 포트에 연결된 노드가 DialogNode.HiddenBranch가 된다. - bool hasHidden = false; - GetNodeOptionByName(OPTION_HIDDEN_BRANCH)?.TryGetValue(out hasHidden); - if (hasHidden) - AddExecOutput(context, PORT_HIDDEN_OUT, "Hidden →"); + // 히든 분기 — 선형/선택지 관계없이 맨 아래에 붙는다. + // 각 포트에 연결된 노드가 DialogNode.HiddenBranches[i].Destination이 된다. + int hiddenCount = 0; + GetNodeOptionByName(OPTION_HIDDEN_COUNT)?.TryGetValue(out hiddenCount); + + for (int i = 0; i < hiddenCount; i++) + { + context.AddInputPort(HiddenKeyPort(i)) + .WithDisplayName($"Hidden {i + 1} Key") + .WithTooltip("이 분기를 여는 키 (터치 존·증거품 등 발동시키는 쪽의 키와 일치). " + + "비우면 catch-all — 다른 키에 안 걸린 발동을 전부 받는다(오답 처리용)") + .Build(); + context.AddInputPort(HiddenCodePort(i)) + .WithDisplayName($"Hidden {i + 1} Code") + .WithTooltip("이 분기를 탔을 때 기록할 코드. 비우면 기록 안 함") + .Build(); + AddExecOutput(context, HiddenOutPort(i), $"Hidden {i + 1} →"); + } } } } diff --git a/Assets/02_Scripts/Communication/Dialog/HiddenBranch.cs b/Assets/02_Scripts/Communication/Dialog/HiddenBranch.cs new file mode 100644 index 0000000..cbb128c --- /dev/null +++ b/Assets/02_Scripts/Communication/Dialog/HiddenBranch.cs @@ -0,0 +1,23 @@ +using System; +using UnityEngine; + +// 히든 분기 하나 — "무엇을 하면(Key), 어디로 가고(Destination), 뭘 기록하는가(Code)". +// DialogChoice의 히든 버전이라고 보면 된다. 차이는 플레이어에게 목록이 보이지 않는다는 것뿐. +// +// 한 노드에 여러 개를 달 수 있다: +// Key="knife" → 진짜 증거를 들이댔을 때의 분기 +// Key="photo" → 다른 증거를 들이댔을 때의 다른 반응 +// Key="" → 위 어디에도 안 걸린 나머지 전부 (오답 페널티 등) +[Serializable] +public class HiddenBranch +{ + [Tooltip("이 분기를 여는 키 (터치 존·증거품 등 발동시키는 쪽의 키와 일치). 예: head, hand, knife. " + + "비우면 catch-all — 다른 키에 안 걸린 모든 발동을 받는다")] + public string Key; + + [Tooltip("이 분기로 들어왔을 때 갈 노드. 비어 있으면 이 분기는 발동하지 않는다")] + public DialogNode Destination; + + [Tooltip("이 분기를 탔을 때 기록할 코드 (선택지 Code와 동일한 용도). 비우면 기록 안 함")] + public string Code; +} diff --git a/Assets/02_Scripts/Communication/Dialog/HiddenBranch.cs.meta b/Assets/02_Scripts/Communication/Dialog/HiddenBranch.cs.meta new file mode 100644 index 0000000..0d8e635 --- /dev/null +++ b/Assets/02_Scripts/Communication/Dialog/HiddenBranch.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 409a7546d9339b745aced834ccd9f252 \ No newline at end of file diff --git a/Assets/02_Scripts/Communication/Dialog/HiddenBranchResolver.cs b/Assets/02_Scripts/Communication/Dialog/HiddenBranchResolver.cs index 4db4214..16f3f40 100644 --- a/Assets/02_Scripts/Communication/Dialog/HiddenBranchResolver.cs +++ b/Assets/02_Scripts/Communication/Dialog/HiddenBranchResolver.cs @@ -1,22 +1,28 @@ using System; +using System.Collections.Generic; using UnityEngine; // 히든 분기 감시 허브 (씬 오브젝트 불필요, 순수 static). // -// 대화 노드가 재생되는 동안 DialogPlayer가 Arm(key)로 무장하고, 끝나면 Disarm()한다. -// 그 창(무장 구간) 안에서 DialogGestureZone이 Fire(key)를 호출하면 Fired가 켜지고, -// DialogPlayer가 이를 감지해 Next(또는 선택지) 대신 노드의 HiddenBranch로 몰래 분기한다. +// 대화 노드가 재생되는 동안 DialogPlayer가 Arm(분기 목록)으로 무장하고, 끝나면 Disarm()한다. +// 그 창(무장 구간) 안에서 누군가 Fire(key)를 호출하면 키에 맞는 분기가 골라지고, +// DialogPlayer가 이를 감지해 Next(또는 선택지) 대신 그 분기로 몰래 이동한다. +// Fire를 호출하는 쪽은 캐릭터 부위 터치 존, 증거품 제출 UI 등 무엇이든 될 수 있다. // // 무장돼 있지 않으면 어떤 Fire도 무시된다 → 평소엔 존재하지 않는 것처럼 동작(=히든). public static class HiddenBranchResolver { - private static string _armedKey; + // 지금 무장된 노드의 히든 분기 목록 (null이면 무장 해제 상태) + private static IReadOnlyList _branches; // 지금 히든 분기를 받을 수 있는 상태인가 (노드 재생 중) - public static bool Armed { get; private set; } + public static bool Armed => _branches != null; - // 이번 무장 구간에서 제스처가 발동했는가 - public static bool Fired { get; private set; } + // 이번 무장 구간에서 발동한 분기의 인덱스 (-1이면 아직 발동 안 함) + public static int FiredIndex { get; private set; } = -1; + + // 이번 무장 구간에서 발동했는가 + public static bool Fired => FiredIndex >= 0; // 발동 즉시 알림 — 떠 있는 선택지 메뉴를 그 자리에서 취소시키는 용도 public static event Action FiredEvent; @@ -25,35 +31,59 @@ public static class HiddenBranchResolver [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void ResetStatics() { - _armedKey = null; - Armed = false; - Fired = false; + _branches = null; + FiredIndex = -1; FiredEvent = null; } - // 노드 진입 시 무장. key가 비면 아무 제스처 존이나 발동시킬 수 있다. - public static void Arm(string key) + // 노드 진입 시 무장. + public static void Arm(IReadOnlyList branches) { - _armedKey = key; - Armed = true; - Fired = false; + _branches = branches; + FiredIndex = -1; } // 노드 종료 시 해제. public static void Disarm() { - Armed = false; - _armedKey = null; - Fired = false; + _branches = null; + FiredIndex = -1; } - // 제스처 존이 호출. 무장 중이고 key가 맞을 때만 1회 발동. - // 반환: 이번 호출로 실제 발동했으면 true (호출 측이 "그때만" 아이템 소비 등에 쓸 수 있게). + // 발동시키는 쪽(터치 존·증거품 UI 등)이 호출. 무장 중이고 키가 맞을 때만 1회 발동. + // + // 매칭 규칙: 키가 정확히 일치하는 분기가 최우선, 없으면 Key가 빈 분기(catch-all). + // 덕분에 "정답 키는 각자 분기로, 나머지는 전부 오답 분기로"가 자연스럽게 나온다. + // Destination이 비어 있는 분기는 후보에서 제외한다(아무 데도 못 가는 분기로 새는 것 방지). + // + // 반환: 이번 호출로 실제 발동했으면 true (호출 측이 "그때만" 연출·아이템 소비 등에 쓸 수 있게). public static bool Fire(string key) { - if (!Armed || Fired) return false; - if (!string.IsNullOrEmpty(_armedKey) && _armedKey != key) return false; - Fired = true; + if (_branches == null || FiredIndex >= 0) return false; + + int exact = -1; + int catchAll = -1; + + for (int i = 0; i < _branches.Count; i++) + { + var branch = _branches[i]; + if (branch == null || branch.Destination == null) continue; + + if (string.IsNullOrEmpty(branch.Key)) + { + if (catchAll < 0) catchAll = i; // 첫 catch-all만 사용 + } + else if (branch.Key == key) + { + exact = i; + break; // 정확 일치는 더 볼 것 없음 + } + } + + int picked = exact >= 0 ? exact : catchAll; + if (picked < 0) return false; // 맞는 분기 없음 — 아무 일도 안 일어남 + + FiredIndex = picked; FiredEvent?.Invoke(); return true; } diff --git a/Assets/02_Scripts/Communication/Voice/LipSync.cs b/Assets/02_Scripts/Communication/Voice/LipSync.cs deleted file mode 100644 index a2debdd..0000000 --- a/Assets/02_Scripts/Communication/Voice/LipSync.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using UnityEngine; - -// 보이스 진폭에 따라 입 관련 블렌드셰이프 그룹의 weight를 직접 제어 -// LateUpdate에서 갱신해 Animator가 같은 프레임에 0으로 세팅한 값을 덮어씀 -[RequireComponent(typeof(CharacterVoiceObject))] -public class LipSync : MonoBehaviour -{ - [Serializable] - private struct LipShape - { - public string Name; - [Range(0f, 100f)] public float MaxWeight; // amplitude=1일 때 도달할 weight - } - - [Header("Refs")] - [SerializeField] private SkinnedMeshRenderer _meshRenderer; - - // BMAC_OpenMouse_Big 클립의 입 관련 셰이프 프리셋 - [Header("Mouth Preset (입 최대 시 weight)")] - [SerializeField] private LipShape[] _shapes = - { - new() { Name = "Expression_SurpriesedMouth", MaxWeight = 50f }, - new() { Name = "Expression_MouthSad_L", MaxWeight = 10f }, - new() { Name = "Expression_MouthSad_R", MaxWeight = 10f }, - new() { Name = "Expression_MouthWide_L", MaxWeight = 30f }, - new() { Name = "Expression_MouthWide_R", MaxWeight = 30f }, - new() { Name = "Expression_LipsOh", MaxWeight = 100f }, - new() { Name = "Expression_LipsO", MaxWeight = 5f }, - }; - - [Header("Tuning")] - [SerializeField, Range(0f, 20f)] private float _amplitudeScale = 6f; // RMS → 0~1 매핑 배수 - [SerializeField, Range(0f, 0.05f)] private float _noiseFloor = 0.005f; - [SerializeField, Range(0f, 30f)] private float _smoothingSpeed = 15f; - [SerializeField] private int _sampleSize = 256; - - private AudioSource _audioSource; - private int[] _indices; - private float[] _sampleBuffer; - private float _currentAmplitude; - - private void Awake() - { - var voiceObj = GetComponent(); - _audioSource = voiceObj != null ? voiceObj.VoiceSource : null; - - // 메시 자동 탐색 — 첫 번째 셰이프 이름을 가진 SkinnedMeshRenderer 사용 - if (_meshRenderer == null && _shapes.Length > 0) - { - string probe = _shapes[0].Name; - foreach (var smr in GetComponentsInChildren(true)) - { - if (smr.sharedMesh != null && smr.sharedMesh.GetBlendShapeIndex(probe) >= 0) - { - _meshRenderer = smr; - break; - } - } - } - - // 인덱스 캐시 - _indices = new int[_shapes.Length]; - if (_meshRenderer != null && _meshRenderer.sharedMesh != null) - { - var mesh = _meshRenderer.sharedMesh; - for (int i = 0; i < _shapes.Length; i++) - { - _indices[i] = mesh.GetBlendShapeIndex(_shapes[i].Name); - if (_indices[i] < 0) - Debug.LogWarning($"[LipSync] 블렌드셰이프 없음: {_shapes[i].Name}", this); - } - } - else - { - for (int i = 0; i < _indices.Length; i++) _indices[i] = -1; - } - - if (_audioSource == null) - Debug.LogWarning("[LipSync] CharacterVoiceObject.VoiceSource 미할당", this); - - _sampleBuffer = new float[_sampleSize]; - } - - private void LateUpdate() - { - if (_audioSource == null || _meshRenderer == null) return; - - // PlayOneShot도 잡히도록 항상 샘플링 — 무음은 노이즈 플로어로 컷 - _audioSource.GetOutputData(_sampleBuffer, 0); - - float sumSq = 0f; - for (int i = 0; i < _sampleBuffer.Length; i++) - sumSq += _sampleBuffer[i] * _sampleBuffer[i]; - - float rms = Mathf.Sqrt(sumSq / _sampleBuffer.Length); - rms = Mathf.Max(0f, rms - _noiseFloor); - float target = Mathf.Clamp01(rms * _amplitudeScale); - - _currentAmplitude = Mathf.Lerp(_currentAmplitude, target, Time.deltaTime * _smoothingSpeed); - - // Animator가 같은 프레임에 0으로 덮은 값을 LateUpdate에서 다시 씌움 - for (int i = 0; i < _shapes.Length; i++) - { - if (_indices[i] < 0) continue; - _meshRenderer.SetBlendShapeWeight(_indices[i], _currentAmplitude * _shapes[i].MaxWeight); - } - } -} diff --git a/Assets/02_Scripts/Communication/Voice/LipSync.cs.meta b/Assets/02_Scripts/Communication/Voice/LipSync.cs.meta deleted file mode 100644 index 580b292..0000000 --- a/Assets/02_Scripts/Communication/Voice/LipSync.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: ae0d9abbe32fe5b4e8e4886143e1e5e2 \ No newline at end of file diff --git a/Assets/02_Scripts/Managers/SoundManager.cs b/Assets/02_Scripts/Managers/SoundManager.cs index ffc6d78..64653ae 100644 --- a/Assets/02_Scripts/Managers/SoundManager.cs +++ b/Assets/02_Scripts/Managers/SoundManager.cs @@ -149,32 +149,12 @@ private async Awaitable BGMFade(float from, float to, CancellationToken token) //=========================== SFX =========================== - //2D SFX 재생 (UI 사운드 등 위치 무관) + //SFX 재생. 2D 게임이라 위치 개념 없이 항상 화면 전체에 동일하게 들린다. public void PlaySFX(AudioClip clip, float volume = 1f) { if (clip == null) return; AudioSource source = GetSfxSource(); - source.transform.localPosition = Vector3.zero; - source.spatialBlend = 0f; //2D - Play(source, clip, volume); - _ = ReturnAfterPlay(source, clip.length); //재생이 끝나면 풀에 반납 - } - - //3D SFX 재생 (VR 공간음향 - 특정 위치에서 들림) - //minDistance: 이 거리 안에선 풀 볼륨으로 들림. 하늘 높이 터지는 폭죽처럼 멀리서 나는 큰 소리는 - //크게(예: 30~50) 잡아야 거리 감쇠로 사라지지 않는다. maxDistance: 감쇠 계산 상한. - public void PlaySFXAt(AudioClip clip, Vector3 position, float volume = 1f, - float minDistance = 1f, float maxDistance = 500f) - { - if (clip == null) return; - - AudioSource source = GetSfxSource(); - source.transform.position = position; - source.spatialBlend = 1f; //3D - source.rolloffMode = AudioRolloffMode.Logarithmic; - source.minDistance = minDistance; - source.maxDistance = maxDistance; Play(source, clip, volume); _ = ReturnAfterPlay(source, clip.length); //재생이 끝나면 풀에 반납 } @@ -222,6 +202,7 @@ private AudioSource CreateSfxSource() AudioSource source = go.AddComponent(); source.outputAudioMixerGroup = _sfxGroup; source.playOnAwake = false; + source.spatialBlend = 0f; //2D — 거리 감쇠 없이 항상 동일하게 return source; } diff --git a/Assets/08_UI.meta b/Assets/08_UI.meta new file mode 100644 index 0000000..fc8deba --- /dev/null +++ b/Assets/08_UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4085cc1289072ea4f9520818a87a7122 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/08_UI/DialogEnterUI.uxml b/Assets/08_UI/DialogEnterUI.uxml new file mode 100644 index 0000000..310bec7 --- /dev/null +++ b/Assets/08_UI/DialogEnterUI.uxml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Assets/08_UI/DialogEnterUI.uxml.meta b/Assets/08_UI/DialogEnterUI.uxml.meta new file mode 100644 index 0000000..f31f23b --- /dev/null +++ b/Assets/08_UI/DialogEnterUI.uxml.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 723175cba3247b54c868395e5fe948a1 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0} diff --git a/Assets/08_UI/DialogUI.uxml b/Assets/08_UI/DialogUI.uxml new file mode 100644 index 0000000..2247949 --- /dev/null +++ b/Assets/08_UI/DialogUI.uxml @@ -0,0 +1,9 @@ + +