히든분기

This commit is contained in:
2026-07-23 10:50:22 +09:00
parent 3db450e78d
commit e1c43c9b3d
6 changed files with 228 additions and 12 deletions

View File

@@ -269,17 +269,41 @@ private async Awaitable PlayEntry(DialogEntry entry)
var node = entry.Group.StartNode;
while (node != null)
{
await PlayNode(node);
// 이 노드가 히든 분기를 가지면, 노드가 재생되는 동안 제스처 감시를 무장한다.
// (무장 안 된 노드는 아래 대기/선택이 기존과 완전히 동일하게 동작)
bool armed = node.HiddenBranch != null;
if (armed) HiddenBranchResolver.Arm(node.HiddenGestureKey);
try
{
bool diverted = await PlayNode(node); // 대사 표시 + 대기(무장 시 제스처 감시 포함)
if (node.Choices != null && node.Choices.Count > 0)
{
int picked = await WaitForChoice(node);
RecordChoice(node, picked);
node = node.Choices[picked].DestinationNode;
if (diverted)
{
RecordHiddenChoice(node);
node = node.HiddenBranch; // 대사 도중 제스처 발동 → 몰래 분기
}
else if (node.Choices != null && node.Choices.Count > 0)
{
int picked = await WaitForChoice(node); // 메뉴(무장 시 제스처 감시 포함)
if (picked == DivertIndex)
{
RecordHiddenChoice(node);
node = node.HiddenBranch; // 메뉴 도중 제스처 발동 → 메뉴엔 없던 히든 분기
}
else
{
RecordChoice(node, picked);
node = node.Choices[picked].DestinationNode;
}
}
else
{
node = node.Next;
}
}
else
finally
{
node = node.Next;
if (armed) HiddenBranchResolver.Disarm();
}
}
@@ -433,7 +457,8 @@ private void LateUpdate()
}
}
private async Awaitable PlayNode(DialogNode node)
// 반환: true = 대기 도중 히든 제스처가 발동됨(→ HiddenBranch로 분기), false = 평범하게 진행
private async Awaitable<bool> PlayNode(DialogNode node)
{
// 화자 옆 DialogHud에 대사 표시
// (배치는 화자의 DialogHudPlacement 담당, 없으면 DialogHud 기본값. 이름은 노드 오버라이드 우선)
@@ -528,6 +553,11 @@ private async Awaitable PlayNode(DialogNode node)
}
// 진행 방식 결정
// 히든 분기가 무장된 노드는 대기 중 제스처를 함께 감시(레이스)한다.
// 무장 안 된 노드는 아래 기존 대기 그대로 → 동작 100% 동일, 반환 false.
if (HiddenBranchResolver.Armed)
return await WaitAdvanceOrDivert(node);
if (node.WaitForInput)
{
await WaitForAdvanceInput(); // B버튼 입력이 있어야만 다음으로
@@ -543,6 +573,7 @@ private async Awaitable PlayNode(DialogNode node)
else
await WaitForAdvanceInput(); // 지정 시간이 없으면 입력으로 진행
}
return false;
}
// 노드의 EventKey와 같은 Key를 가진 이벤트들을 호출
@@ -564,17 +595,102 @@ private void RecordChoice(DialogNode node, int index)
StoryManager.Instance.RecordChoice(code);
}
// 히든 분기 기록: 노드의 HiddenCode를 선택지 Code와 동일하게 StoryState에 영구 기록
private void RecordHiddenChoice(DialogNode node)
{
string code = DialogVariables.Format(node.HiddenCode); // {token} 치환
if (!string.IsNullOrEmpty(code))
StoryManager.Instance.RecordChoice(code);
}
// 히든 분기 신호값 — 선택지 인덱스(0..n-1)와 절대 겹치지 않는 값.
private const int DivertIndex = -1;
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);
// 무장 안 된 노드는 기존과 동일 — 그냥 메뉴 선택을 기다린다.
if (!HiddenBranchResolver.Armed)
return await ChoiceHud.Instance.Show(node.ChoiceQuestion, node.Choices);
// 무장된 노드: 메뉴가 떠 있는 동안 제스처가 들어오면 메뉴를 취소하고 히든으로 분기.
if (HiddenBranchResolver.Fired)
return DivertIndex; // 메뉴가 뜨기 전에 이미 발동한 경우
void OnFired() => ChoiceHud.Instance.CancelPending(); // 제스처 → 메뉴 즉시 취소
HiddenBranchResolver.FiredEvent += OnFired;
try
{
return await ChoiceHud.Instance.Show(node.ChoiceQuestion, node.Choices);
}
catch (OperationCanceledException)
{
// 우리가 제스처로 취소한 것이면 히든 분기, 아니면(씬 전환 등) 위로 전파
if (HiddenBranchResolver.Fired) return DivertIndex;
throw;
}
finally
{
HiddenBranchResolver.FiredEvent -= OnFired;
}
}
// 노드 대기 + 히든 제스처 감시(레이스). 무장된 노드에서만 호출된다.
// 반환: true = 대기 도중 제스처 발동(→히든 분기), false = 평범하게 진행(입력/시간)
private async Awaitable<bool> WaitAdvanceOrDivert(DialogNode node)
{
// 기존 PlayNode 대기 규칙 그대로 계산 — 입력 대기냐, 시간 대기냐
bool waitForInput;
float timeoutSeconds;
if (node.WaitForInput)
{
waitForInput = true; timeoutSeconds = -1f;
}
else
{
float wait = (node.Voice != null && node.Voice.Clip != null)
? node.Voice.Clip.length
: node.LineDuration;
if (wait > 0f) { waitForInput = false; timeoutSeconds = wait; }
else { waitForInput = true; timeoutSeconds = -1f; }
}
var im = InputManager.Instance;
bool advance = false;
void OnAdvance() => advance = true;
if (waitForInput && im != null) im.OnDialogNext_Event += OnAdvance;
// 입력 대기인데 InputManager가 없으면 기존 WaitForAdvanceInput처럼 1초 후 진행
float timeLeft = timeoutSeconds;
if (waitForInput && im == null) timeLeft = 1f;
try
{
while (true)
{
if (HiddenBranchResolver.Fired) return true; // 제스처 발동 → 히든 분기
if (waitForInput && advance) return false; // B버튼 → 평범하게 진행
if (timeLeft >= 0f)
{
timeLeft -= Time.deltaTime;
if (timeLeft <= 0f) return false; // 시간 경과 → 평범하게 진행
}
await Awaitable.NextFrameAsync(destroyCancellationToken);
}
}
catch (OperationCanceledException)
{
return false; // 대기 중 파괴/씬 전환 — 기존과 동일하게 조용히 종료
}
finally
{
if (waitForInput && im != null) im.OnDialogNext_Event -= OnAdvance;
}
}
// 대화 진행 입력(OnDialogNext = VR B버튼) 한 번을 대기