Files
WhaleAdventure_VR/Assets/02_Scripts/Communication/Dialog/DialogPlayer.cs

243 lines
8.1 KiB
C#

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<RegionGroup> _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<string, DialogGroup> _regionMap;
private Animator _animator;
private int _initialGestureHash;
private int _initialExpressionHash;
private bool _hasInitialExpression;
private readonly Dictionary<Transform, Quaternion> _originalRotations = new();
public bool IsPlaying { get; private set; }
private void Awake()
{
_regionMap = new Dictionary<string, DialogGroup>();
foreach (var e in _regionGroups)
if (e.Group != null) _regionMap[e.Region] = e.Group;
_animator = GetComponentInChildren<Animator>();
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<int> 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();
}
}
}