using System.Collections.Generic; using UnityEngine.InputSystem; using UnityEngine; [RequireComponent(typeof(CharacterVoiceObject))] public class DialogPlayer : MonoBehaviour { [System.Serializable] public struct RegionGroup { public string Region; // 영역 이름 (NPC마다 자유롭게 지정 — 그룹 이름과 무관) public DialogGroup Group; } [Tooltip("영역 이름 ↔ 그 영역에서 재생할 DialogGroup")] [SerializeField] private List _regionGroups; [Header("Region")] [SerializeField] private string _currentRegion; // 현재 영역 이름. DialogRegion 트리거가 갱신 [Header("Dialog HUD Placement")] // 씬에서 캐릭터 위치/주변(벽 등)에 맞춰 조절 [SerializeField] private float _hudChestHeight = 1.2f; // 화자 발 기준 가슴 높이 [SerializeField] private float _hudForwardOffset = 0.5f; // 화자→플레이어 방향으로 띄울 거리 [SerializeField] private float _hudLateralOffset = 0f; // 좌우 오프셋 (+ 플레이어 시점 오른쪽) private Dictionary _regionMap; private Animator _animator; private int _initialGestureHash; private int _initialExpressionHash; private bool _hasInitialExpression; private readonly Dictionary _originalRotations = new(); public bool IsPlaying { get; private set; } private void Awake() { _regionMap = new Dictionary(); foreach (var e in _regionGroups) if (e.Group != null) _regionMap[e.Region] = e.Group; _animator = GetComponentInChildren(); if (_animator != null) { _initialGestureHash = _animator.GetCurrentAnimatorStateInfo(0).fullPathHash; if (_animator.layerCount > 1) { _initialExpressionHash = _animator.GetCurrentAnimatorStateInfo(1).fullPathHash; _hasInitialExpression = true; } } } public async Awaitable Play() { var region = ResolveRegion(); if (region != null) await Play(region); } // 현재 영역. 영역이 없거나 매칭 그룹이 없으면 리스트 첫 항목으로 폴백. private string ResolveRegion() { if (!string.IsNullOrEmpty(_currentRegion) && _regionMap.ContainsKey(_currentRegion)) return _currentRegion; return _regionGroups.Count > 0 ? _regionGroups[0].Region : null; } // 영역 전환 (DialogRegion 트리거가 호출). 다음 Play()부터 해당 영역 대화가 재생됨. public void SetRegion(string region) => _currentRegion = region; public string CurrentRegion => _currentRegion; public async Awaitable Play(string region) { if (IsPlaying) return; if (!_regionMap.TryGetValue(region, out var group)) { Debug.LogWarning($"[DialogPlayer] 영역 대화 없음: {region}"); return; } IsPlaying = true; try { var node = group.StartNode; while (node != null) { await PlayNode(node); if (node.Choices != null && node.Choices.Count > 0) { int picked = await WaitForChoice(node); node = node.Choices[picked].DestinationNode; } else { node = node.Next; } } } finally { IsPlaying = false; if (DialogHud.Instance != null) DialogHud.Instance.Hide(); RestoreDefaultAnimations(); RestoreRotations(); } Debug.Log("[DialogPlayer] 대화 종료"); } private void RestoreDefaultAnimations() { if (_animator == null) return; _animator.CrossFade(_initialGestureHash, 0.3f, 0, normalizedTimeOffset: 0f); if (_hasInitialExpression) _animator.CrossFade(_initialExpressionHash, 0.3f, 1, normalizedTimeOffset: 0f); } private async Awaitable RotateTowardPlayer(Transform target) { if (Camera.main == null) return; var playerCam = Camera.main.transform; float duration = 0.5f; float elapsed = 0f; while (elapsed < duration) { Vector3 dir = playerCam.position - target.position; dir.y = 0f; if (dir.sqrMagnitude > 0.0001f) { var targetRot = Quaternion.LookRotation(dir); target.rotation = Quaternion.Slerp(target.rotation, targetRot, 10f * Time.deltaTime); } elapsed += Time.deltaTime; await Awaitable.NextFrameAsync(); } } private void RestoreRotations() { foreach (var kvp in _originalRotations) { if (kvp.Key != null) _ = RotateToRotation(kvp.Key, kvp.Value); } _originalRotations.Clear(); } private async Awaitable RotateToRotation(Transform target, Quaternion targetRotation) { float duration = 0.5f; float elapsed = 0f; while (elapsed < duration) { if (target == null) return; target.rotation = Quaternion.Slerp(target.rotation, targetRotation, 10f * Time.deltaTime); elapsed += Time.deltaTime; await Awaitable.NextFrameAsync(); } } private async Awaitable PlayNode(DialogNode node) { // 화자 옆 DialogHud에 대사 표시 (배치 오프셋은 이 NPC의 설정값 사용) if (DialogHud.Instance != null) DialogHud.Instance.Show(node.Speaker, node.TalkText, _hudChestHeight, _hudForwardOffset, _hudLateralOffset); // 보이스 재생 if (node.Voice != null && node.Speaker != null) { var voiceObj = CharacterVoiceObject.Find(node.Speaker); if (voiceObj != null && node.Voice.Clip != null) voiceObj.Play(node.Voice.Clip); } // 플레이어 향해 회전 if (node.LookAtPlayer && node.Speaker != null) { var voiceObj = CharacterVoiceObject.Find(node.Speaker); if (voiceObj != null) { _originalRotations.TryAdd(voiceObj.transform, voiceObj.transform.rotation); _ = RotateTowardPlayer(voiceObj.transform); } } if (node.Gesture != null) _animator.CrossFade(node.Gesture.StateName, node.Gesture.CrossFadeDuration, node.Gesture.AnimationLayer); if (node.Expression != null) _animator.CrossFade(node.Expression.StateName, node.Expression.CrossFadeDuration, node.Expression.AnimationLayer); // 대기 시간 결정 float wait = 0f; if (node.Voice != null && node.Voice.Clip != null) wait = node.Voice.Clip.length; else wait = node.LineDuration; if (wait > 0f) await Awaitable.WaitForSecondsAsync(wait); else await WaitForAdvanceInput(); // 수동 진행 } private async Awaitable WaitForChoice(DialogNode node) { //선택을 기다리는 함수 수정해서 사용할것 if (ChoiceHud.Instance == null) { Debug.LogWarning("[DialogPlayer] ChoiceHud 없음 — 0번 자동 선택"); return 0; } return await ChoiceHud.Instance.Show(node.ChoiceQuestion, node.Choices); } private async Awaitable WaitForAdvanceInput() { // TODO: VR 컨트롤러 버튼 입력 대기. 일단은 1초 대기 await Awaitable.WaitForSecondsAsync(1f); } //테스트용 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)) { Debug.Log("캐릭터 클릭"); Play(); } } }