2026-04-24 NPC 음성 다이얼로그 시스템

This commit is contained in:
2026-04-24 16:23:55 +09:00
parent 3ea92ba3f9
commit 305a911524
69 changed files with 1064 additions and 181 deletions

View File

@@ -1,24 +1,99 @@
using System.Collections.Generic;
using UnityEngine.InputSystem;
using UnityEngine;
[RequireComponent(typeof(CharacterVoiceObject))]
public class DialogPlayer : MonoBehaviour
{
[SerializeField] private List<DialogGroup> _dialogGroups;
private Dictionary<string, DialogGroup> _dialogGroupMap;
private Animator _animator;
private void Awake()
{
_dialogGroupMap = new Dictionary<string, DialogGroup>();
foreach (var g in _dialogGroups)
{
_dialogGroupMap[g.DialogGroupName] = g;
}
_animator = GetComponentInChildren<Animator>();
}
public void VoicePlay(DialogNode node)
// 임시 테스트용
private void Update()
{
//CharacterVoiceObject speakerVoiceObject = FindSpeakerVoiceObject(node.Speaker);
//speakerVoiceObject.Play(node.Voice.Clip);
if (Keyboard.current != null && Keyboard.current.tKey.wasPressedThisFrame)
_ = Play("BlackDialogGroup1");
}
public async Awaitable Play(string groupName)
{
if (!_dialogGroupMap.TryGetValue(groupName, out var group))
{
Debug.LogWarning($"[DialogPlayer] 그룹 없음: {groupName}");
return;
}
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;
}
}
Debug.Log("[DialogPlayer] 대화 종료");
}
private async Awaitable PlayNode(DialogNode node)
{
var speakerName = node.Speaker != null ? node.Speaker.Name : "(누구?)";
Debug.Log($"[{speakerName}] {node.TalkText}");
// 보이스 재생
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.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)
{
// TODO: 선택지 UI 띄우기. 일단은 첫 번째 선택지 자동 선택 (테스트용)
Debug.Log("[DialogPlayer] 선택지 대기 — 일단 0번 자동 선택");
await Awaitable.WaitForSecondsAsync(0.5f);
return 0;
}
private async Awaitable WaitForAdvanceInput()
{
// TODO: VR 컨트롤러 버튼 입력 대기. 일단은 1초 대기
await Awaitable.WaitForSecondsAsync(1f);
}
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using UnityEngine;
public class CharacterVoiceObject : MonoBehaviour
@@ -5,5 +6,13 @@ public class CharacterVoiceObject : MonoBehaviour
public CharacterData Character;
public AudioSource VoiceSource;
private static readonly Dictionary<CharacterData, CharacterVoiceObject> _registry = new();
private void OnEnable() => _registry[Character] = this;
private void OnDisable() => _registry.Remove(Character);
public static CharacterVoiceObject Find(CharacterData data)
=> _registry.TryGetValue(data, out var obj) ? obj : null;
public void Play(AudioClip clip) => VoiceSource.PlayOneShot(clip);
}

View File

@@ -6,5 +6,5 @@ public class ExpressionData : ScriptableObject
[HideInInspector] public int AnimationLayer = 1;
public string StateName;
public float CrossFadeDuration = 0.2f;
public AnimationClip PreviewClip;
public AnimationClip AnimClip;
}

View File

@@ -6,5 +6,5 @@ public class GestureData : ScriptableObject
[HideInInspector] public int AnimationLayer = 0;
public string StateName;
public float CrossFadeDuration = 0.2f;
public AnimationClip PreviewClip;
public AnimationClip AnimClip;
}