2026-07-05 대화시스템 수정
This commit is contained in:
@@ -7,30 +7,31 @@
|
||||
[RequireComponent(typeof(CharacterVoiceObject))]
|
||||
public class DialogPlayer : MonoBehaviour
|
||||
{
|
||||
// 대화 후보 하나. 리스트에서 위에 있을수록 우선순위가 높다.
|
||||
// (스토리 대화를 위에, 조건 없는 기본 잡담을 맨 아래에 두는 것을 권장)
|
||||
[System.Serializable]
|
||||
public struct RegionGroup
|
||||
public struct DialogEntry
|
||||
{
|
||||
public string Region; // 영역 이름 (NPC마다 자유롭게 지정 — 그룹 이름과 무관)
|
||||
public DialogGroup Group;
|
||||
public DialogCondition Condition;
|
||||
|
||||
[Tooltip("켜면 완료 후에도 반복 재생 가능(잡담용). 끄면 1회성(스토리 대화)")]
|
||||
public bool Repeatable;
|
||||
|
||||
[Tooltip("이 대화를 처음 완료하면 메인 진행도 +N (필수 대화가 아니면 0)")]
|
||||
[Min(0)] public int ProgressOnComplete;
|
||||
}
|
||||
|
||||
// 마지막 선택지 코드(LastChoiceCode)를 인자로 넘기는 UnityEvent (인스펙터 노출용 구체 타입)
|
||||
[System.Serializable]
|
||||
public class ChoiceCodeEvent : UnityEvent<string> { }
|
||||
|
||||
// 노드의 EventKey ↔ 그 노드 재생 시 호출할 이벤트. 인자로 LastChoiceCode가 전달됨.
|
||||
// 노드의 EventKey ↔ 그 노드 재생 시 호출할 이벤트.
|
||||
[System.Serializable]
|
||||
public struct NodeEvent
|
||||
{
|
||||
public string Key;
|
||||
public ChoiceCodeEvent Event;
|
||||
public UnityEvent Event;
|
||||
}
|
||||
|
||||
[Tooltip("영역 이름 ↔ 그 영역에서 재생할 DialogGroup")]
|
||||
[SerializeField] private List<RegionGroup> _regionGroups;
|
||||
|
||||
[Header("Region")]
|
||||
[SerializeField] private string _currentRegion; // 현재 영역 이름. DialogRegion 트리거가 갱신
|
||||
[Tooltip("이 NPC의 대화 후보들. 위에서부터 조건을 검사해 첫 번째로 만족하는 대화를 재생")]
|
||||
[SerializeField] private List<DialogEntry> _dialogs = new();
|
||||
|
||||
[Header("Dialog HUD Placement")] // 씬에서 캐릭터 위치/주변(벽 등)에 맞춰 조절
|
||||
[SerializeField] private float _hudChestHeight = 1.2f; // 화자 발 기준 가슴 높이
|
||||
@@ -41,7 +42,7 @@ public struct NodeEvent
|
||||
[Tooltip("노드의 Event Key와 같은 Key가 그 노드 재생 시 호출됨")]
|
||||
[SerializeField] private List<NodeEvent> _nodeEvents = new();
|
||||
|
||||
private Dictionary<string, DialogGroup> _regionMap;
|
||||
private CharacterVoiceObject _voice; // 이 NPC의 캐릭터 정보 (호감도 조건 대상)
|
||||
private Animator _animator;
|
||||
private int _initialGestureHash;
|
||||
private int _initialExpressionHash;
|
||||
@@ -49,17 +50,9 @@ public struct NodeEvent
|
||||
private readonly Dictionary<Transform, Quaternion> _originalRotations = new();
|
||||
public bool IsPlaying { get; private set; }
|
||||
|
||||
// 마지막으로 고른 선택지 (인덱스/코드). DialogVariables에도 lastChoiceIndex / lastChoiceCode 로 저장됨
|
||||
public int LastChoiceIndex { get; private set; } = -1;
|
||||
public string LastChoiceCode { get; private set; }
|
||||
public event Action<int, string> OnChoiceSelected; // (index, code)
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_regionMap = new Dictionary<string, DialogGroup>();
|
||||
foreach (var e in _regionGroups)
|
||||
if (e.Group != null) _regionMap[e.Region] = e.Group;
|
||||
|
||||
_voice = GetComponent<CharacterVoiceObject>();
|
||||
_animator = GetComponentInChildren<Animator>();
|
||||
|
||||
if (_animator != null)
|
||||
@@ -74,38 +67,38 @@ private void Awake()
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
int index = FindPlayableIndex();
|
||||
if (index < 0)
|
||||
{
|
||||
Debug.LogWarning($"[DialogPlayer] 영역 대화 없음: {region}");
|
||||
Debug.Log($"[DialogPlayer] 조건에 맞는 대화가 없음: {name}");
|
||||
return;
|
||||
}
|
||||
await PlayEntry(_dialogs[index]);
|
||||
}
|
||||
|
||||
// 리스트 순서 = 우선순위. 조건을 만족하고 (반복 가능하거나 아직 안 한) 첫 대화.
|
||||
private int FindPlayableIndex()
|
||||
{
|
||||
for (int i = 0; i < _dialogs.Count; i++)
|
||||
{
|
||||
var entry = _dialogs[i];
|
||||
if (entry.Group == null) continue;
|
||||
if (!entry.Repeatable && StoryState.IsDialogCompleted(entry.Group.name)) continue;
|
||||
if (entry.Condition != null && !entry.Condition.IsMet(_voice.Character)) continue;
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private async Awaitable PlayEntry(DialogEntry entry)
|
||||
{
|
||||
IsPlaying = true;
|
||||
try
|
||||
{
|
||||
var node = group.StartNode;
|
||||
var node = entry.Group.StartNode;
|
||||
while (node != null)
|
||||
{
|
||||
await PlayNode(node);
|
||||
@@ -121,6 +114,15 @@ public async Awaitable Play(string region)
|
||||
node = node.Next;
|
||||
}
|
||||
}
|
||||
|
||||
// 여기까지 왔으면 자연 종료(끝까지 재생) — 이때만 완료로 기록한다.
|
||||
// (중간에 오브젝트 파괴 등으로 끊기면 예외로 빠져나가 기록되지 않음)
|
||||
bool firstTime = StoryState.MarkDialogCompleted(entry.Group.name);
|
||||
if (firstTime && entry.ProgressOnComplete > 0)
|
||||
StoryState.MainProgress += entry.ProgressOnComplete;
|
||||
StoryState.Save();
|
||||
|
||||
Debug.Log($"[DialogPlayer] 대화 종료: {entry.Group.name}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -130,8 +132,6 @@ public async Awaitable Play(string region)
|
||||
RestoreDefaultAnimations();
|
||||
RestoreRotations();
|
||||
}
|
||||
|
||||
Debug.Log("[DialogPlayer] 대화 종료");
|
||||
}
|
||||
|
||||
private void RestoreDefaultAnimations()
|
||||
@@ -242,37 +242,31 @@ private void RaiseNodeEvent(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) return;
|
||||
foreach (var e in _nodeEvents)
|
||||
if (e.Key == key) e.Event?.Invoke(LastChoiceCode); // 마지막 선택지 코드를 인자로 전달
|
||||
if (e.Key == key) e.Event?.Invoke();
|
||||
}
|
||||
|
||||
// 선택 결과 기록: 인덱스/코드를 프로퍼티 + DialogVariables에 저장하고 이벤트 발행
|
||||
// 선택 기록: 선택지 Code를 StoryState에 영구 기록 (대화 활성화 조건 판정에 사용)
|
||||
private void RecordChoice(DialogNode node, int index)
|
||||
{
|
||||
string code = (node.Choices != null && index >= 0 && index < node.Choices.Count)
|
||||
? node.Choices[index].Code : null;
|
||||
code = DialogVariables.Format(code); // {token} 치환 → 동적으로 생성된 코드 반영
|
||||
|
||||
LastChoiceIndex = index;
|
||||
LastChoiceCode = code;
|
||||
|
||||
DialogVariables.Set("lastChoiceIndex", index.ToString());
|
||||
if (!string.IsNullOrEmpty(code))
|
||||
DialogVariables.Set("lastChoiceCode", code);
|
||||
|
||||
OnChoiceSelected?.Invoke(index, code);
|
||||
StoryState.RecordChoice(code);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 대화 진행 입력(OnDialogNext = VR B버튼) 한 번을 대기
|
||||
|
||||
Reference in New Issue
Block a user