수정사항

This commit is contained in:
2026-08-06 20:49:36 +09:00
parent 6d6d452922
commit 60c1488716
36 changed files with 1451 additions and 76 deletions

View File

@@ -23,6 +23,10 @@ public class DialogPlayer : MonoBehaviour
// 대화 중 제스처/표정을 재생한 Animator들의 원래 상태 — 대화 종료 시 전부 복원.
// (끼어든 다른 NPC의 Animator도 포함되므로 딕셔너리로 추적한다)
private readonly Dictionary<Animator, (int gestureHash, int expressionHash, bool hasExpression)> _touchedAnimators = new();
// 대화 중 포즈를 바꾼 캐릭터들의 원래 포즈 — 대화 종료 시 전부 복원.
// (끼어든 다른 NPC의 포즈도 포함되므로 Animator와 같은 방식으로 딕셔너리에 추적한다)
private readonly Dictionary<CharacterPoses, Pose> _touchedPoses = new();
public bool IsPlaying { get; private set; }
// 지금 실제 대사를 재생 중인 DialogPlayer (전역 1개) — 이때 다른 NPC의 Play()는 무시된다.
@@ -72,6 +76,10 @@ private void OnDestroy()
// 선택 메뉴를 띄우지 않는다 — 메뉴는 플레이어가 "말을 건" 경우의 UI다.
public void PlayAuto() => _ = Play(autoSelect: true);
// 「이야기한다」 버튼용 — 선택 메뉴를 띄우고 대화를 시작한다.
// PlayAuto와 달리 후보가 여럿이면 DialogEnterHud로 고르게 한다. (UnityEvent 연결용 void)
public void Talk() => _ = Play();
// autoSelect가 true면 후보가 여럿이어도 메뉴 없이 최상단(우선순위 1위) 비트를 바로 재생한다.
public async Awaitable Play(bool autoSelect = false)
{
@@ -202,12 +210,6 @@ private async Awaitable PlayBeat(StoryBeat beat)
// 도달한 종료 노드가 정한다. 중간에 끊기면(씬 전환 등) false로 남아 장소 BGM으로 복귀한다.
bool keepBgmOnEnd = false;
// 대화 전에 떠 있던 화면 그림 — 끝나면 이 상태로 되돌린다.
// 제스처·표정 Animator를 되돌리는 것과 같은 취지다: 대화는 장소 화면을 잠시 빌려 쓸 뿐이라
// 장소에 서 있던 인물이 대화 한 번 했다고 사라지거나 표정이 굳어 있으면 안 된다.
var frontBefore = FrontSprite.Instance != null ? FrontSprite.Instance.Current : null;
var sideBefore = SideSprite.Instance != null ? SideSprite.Instance.Current : null;
try
{
var node = beat.Group.StartNode;
@@ -291,10 +293,11 @@ private async Awaitable PlayBeat(StoryBeat beat)
if (SoundManager.Instance != null && !keepBgmOnEnd)
SoundManager.Instance.ClearOverrideBGM();
// 화면 그림 복원 — Show(null)은 Hide()와 같으므로 "대화 전에 아무것도 없었다"도 그대로 처리된
if (FrontSprite.Instance != null) FrontSprite.Instance.Show(frontBefore);
if (SideSprite.Instance != null) SideSprite.Instance.Show(sideBefore);
// 앞/옆 그림은 대화 전용 오버레이라 대화가 끝나면 무조건 걷는
if (FrontSprite.Instance != null) FrontSprite.Instance.Hide();
if (SideSprite.Instance != null) SideSprite.Instance.Hide();
RestoreDefaultPoses();
RestoreDefaultAnimations();
}
}
@@ -351,8 +354,8 @@ private async Awaitable<bool> PlayNode(DialogNode node)
if (node.Sfx != null && SoundManager.Instance != null)
SoundManager.Instance.PlaySFX(node.Sfx);
// 화면 그림(앞/옆) — BGM과 같은 규칙으로 비우면 변경 없음
ApplySprites(node);
// 포즈 + 화면 그림
ApplyStageVisuals(node);
// 보이스 재생
if (node.Voice != null && node.Speaker != null)
@@ -389,21 +392,53 @@ private async Awaitable<bool> PlayNode(DialogNode node)
return false;
}
// 노드가 지정한 화면 그림을 반영한다.
// 비어 있는 지정은 "변경 없음"이라(BGM과 같은 규칙) 인물이 서 있는 동안 매 대사에 다시 꽂을 필요가 없다.
// 지우기를 먼저, 지정을 나중에 처리한다 — 한 노드에서 둘 다 걸면 새 그림이 이긴다.
private static void ApplySprites(DialogNode node)
// 노드가 지정한 무대 연출(포즈 + 앞/옆 그림)을 반영한다. 셋의 수명 규칙이 각각 다르다:
// Pose — 지속. '변경 없음'이 기본이라 인물이 서 있는 동안 다시 꽂을 필요가 없다
// FrontSprite — 1회성. 지정 안 한 노드에선 사라진다 ("이의있음!" 같은 번쩍 연출)
// SideSprite — 지정한 대사에서만. 비우면 사라진다
// 뒤의 둘은 Show(null)이 곧 Hide라 그대로 넘기면 두 경우가 한 줄로 처리된다.
private void ApplyStageVisuals(DialogNode node)
{
var front = FrontSprite.Instance;
var side = SideSprite.Instance;
bool hasFront = front != null;
bool hasSide = side != null;
if (FrontSprite.Instance != null) FrontSprite.Instance.Show(node.FrontSprite);
if (SideSprite.Instance != null) SideSprite.Instance.Show(node.SideSprite);
if (hasFront && node.ClearSprite is SpriteClear.Front or SpriteClear.Both) front.Hide();
if (hasSide && node.ClearSprite is SpriteClear.Side or SpriteClear.Both) side.Hide();
if (node.Pose == Pose.Keep) return; // 변경 없음 — 지금 포즈를 그대로 둔다
if (hasFront && node.FrontSprite != null) front.Show(node.FrontSprite);
if (hasSide && node.SideSprite != null) side.Show(node.SideSprite);
// 대상을 안 정했으면 아무 일도 하지 않는다.
// 화자나 장소 인물로 자동 유추할 수도 있지만, 그러면 "누가 바뀌는지"가 노드만 봐서는
// 보이지 않는다. 말하는 사람과 화면에 비치는 사람이 다를 수 있는 이상 명시를 요구하는 편이 낫다.
if (node.PoseTarget == null)
{
Debug.LogWarning($"[DialogPlayer] Pose({node.Pose})를 지정했으나 Pose Target이 비어 있음 — 무시");
return;
}
var poses = FindPoses(node.PoseTarget);
if (poses == null)
{
Debug.LogWarning($"[DialogPlayer] {node.PoseTarget.name}의 CharacterPoses를 찾지 못함 — 포즈 무시");
return;
}
// 대화 종료 시 복원할 원래 포즈를 처음 건드릴 때만 기억한다
if (!_touchedPoses.ContainsKey(poses)) _touchedPoses[poses] = poses.Current;
poses.Apply(node.Pose);
}
// 포즈를 적용할 대상의 CharacterPoses. 그 캐릭터가 지금 씬에 없으면 null.
// 제스처/표정과 달리 화자를 전혀 보지 않는다 — 대상은 노드가 명시한 것 하나뿐이다.
private static CharacterPoses FindPoses(CharacterData target)
{
var obj = CharacterVoiceObject.Find(target);
return obj != null ? obj.GetComponentInChildren<CharacterPoses>(includeInactive: true) : null;
}
// 대화 중 포즈를 바꾼 모든 캐릭터(끼어든 NPC 포함)를 원래 포즈로 복원
private void RestoreDefaultPoses()
{
foreach (var kvp in _touchedPoses)
if (kvp.Key != null) kvp.Key.Apply(kvp.Value);
_touchedPoses.Clear();
}
// 호감도 분기 노드의 조건식 평가.