대화구조 수정

This commit is contained in:
2026-07-30 13:20:13 +09:00
parent 2413483853
commit 519e351fa5
15 changed files with 778 additions and 17 deletions

View File

@@ -3,16 +3,23 @@
using UnityEngine.InputSystem;
using UnityEngine;
[RequireComponent(typeof(CharacterVoiceObject))]
public class DialogPlayer : MonoBehaviour
{
// 대화 후보는 인스펙터가 아니라 StoryDatabase에서 온다 —
// 현재 장소(LocationManager.Current) + 이 캐릭터의 비트 중 조건을 만족하는 것들.
// 여럿이면 선택 메뉴가 뜨고, DB 목록에서 위에 있을수록 우선순위가 높다.
//
// CharacterVoiceObject는 선택이다:
// 있으면 → NPC 플레이어. 그 캐릭터의 비트(말을 걸어야 시작되는 대화)를 재생한다.
// 없으면 → 씬 플레이어. Character가 빈 "장소 비트"를 재생한다 (서술·진입 연출 등).
// LocationManager가 장소 입장 직후 자동으로 호출한다.
private CharacterVoiceObject _voice; // 이 NPC의 캐릭터 정보 (호감도 조건 대상)
private CharacterVoiceObject _voice; // 이 NPC의 캐릭터 정보 (없으면 씬 플레이어)
private Animator _animator;
// 이 플레이어가 담당하는 캐릭터. 씬 플레이어는 null이고, 그게 곧 "장소 비트" 조회 키가 된다.
private CharacterData OwnerCharacter => _voice != null ? _voice.Character : null;
// 대화 중 제스처/표정을 재생한 Animator들의 원래 상태 — 대화 종료 시 전부 복원.
// (끼어든 다른 NPC의 Animator도 포함되므로 딕셔너리로 추적한다)
private readonly Dictionary<Animator, (int gestureHash, int expressionHash, bool hasExpression)> _touchedAnimators = new();
@@ -61,7 +68,12 @@ private void OnDestroy()
if (_entryInProgress == this) _entryInProgress = null;
}
public async Awaitable Play()
// 장소 비트를 자동으로 시작한다 (LocationManager가 장소 입장 직후 호출).
// 선택 메뉴를 띄우지 않는다 — 메뉴는 플레이어가 "말을 건" 경우의 UI다.
public void PlayAuto() => _ = Play(autoSelect: true);
// autoSelect가 true면 후보가 여럿이어도 메뉴 없이 최상단(우선순위 1위) 비트를 바로 재생한다.
public async Awaitable Play(bool autoSelect = false)
{
if (IsPlaying) return;
@@ -85,7 +97,9 @@ public async Awaitable Play()
return;
}
StoryBeat beat = playable.Count == 1 ? playable[0] : await SelectBeat(playable);
StoryBeat beat = playable.Count == 1 || autoSelect
? playable[0]
: await SelectBeat(playable);
if (beat == null) return; // 선택 대기 중 취소됨 (다른 NPC와 대화 시작, 씬 전환 등)
_entryInProgress = this; // 여기서부터 실제 대사 재생 — 끝날 때까지 다른 NPC 상호작용 무시
@@ -153,7 +167,7 @@ private List<StoryBeat> FindPlayableBeats()
Debug.LogWarning($"[DialogPlayer] LocationManager 또는 StoryDatabase가 없음: {name}");
return new List<StoryBeat>();
}
return lm.Database.GetPlayableBeats(lm.Current, _voice.Character);
return lm.Database.GetPlayableBeats(lm.Current, OwnerCharacter);
}
// 재생 가능한 대화가 여럿일 때 DialogEnterHud로 플레이어에게 고르게 한다. 취소되면 null.
@@ -310,7 +324,7 @@ private async Awaitable<bool> PlayNode(DialogNode node)
// 호감도 증감 — 화자 기준, 화자가 비어 있으면 대화 주인 NPC
if (node.Affection != 0)
{
var affectionTarget = node.Speaker != null ? node.Speaker : _voice.Character;
var affectionTarget = node.Speaker != null ? node.Speaker : OwnerCharacter;
StoryManager.Instance.AddAffection(affectionTarget, node.Affection);
}
@@ -380,7 +394,7 @@ private bool IsAffectionMet(DialogNode node)
var req = requirements[i];
if (req == null) continue;
var target = req.Character != null ? req.Character : _voice.Character;
var target = req.Character != null ? req.Character : OwnerCharacter;
group &= req.IsMet(story.GetAffection(target));
// 다음 연결자가 OR이거나 마지막이면 AND 그룹을 닫고 OR로 합친다
@@ -556,6 +570,7 @@ private async Awaitable WaitForAdvanceInput()
//테스트용 — 캐릭터를 클릭하면 대화 시작 (Collider2D가 있어야 잡힌다)
private void Update()
{
if (_voice == null) return; //씬 플레이어(장소 비트 담당)는 클릭 대상이 아니다
if (Mouse.current == null) return;
if (!Mouse.current.leftButton.wasPressedThisFrame) return;
if (Camera.main == null) return;

View File

@@ -8,8 +8,18 @@ public class CharacterVoiceObject : MonoBehaviour
private static readonly Dictionary<CharacterData, CharacterVoiceObject> _registry = new();
private void OnEnable() => _registry[Character] = this;
private void OnDisable() => _registry.Remove(Character);
// Character를 비워 두면 Dictionary 널 키로 예외가 나므로 등록하지 않는다.
// (화자 없는 대화는 CharacterVoiceObject 자체를 붙이지 않는 씬 플레이어로 처리한다)
private void OnEnable()
{
if (Character != null) _registry[Character] = this;
else Debug.LogWarning($"[CharacterVoiceObject] Character가 비어 있어 등록하지 않음: {name}");
}
private void OnDisable()
{
if (Character != null) _registry.Remove(Character);
}
public static CharacterVoiceObject Find(CharacterData data)
=> _registry.TryGetValue(data, out var obj) ? obj : null;