히든분기
This commit is contained in:
25
Assets/02_Scripts/Communication/Dialog/DialogGestureZone.cs
Normal file
25
Assets/02_Scripts/Communication/Dialog/DialogGestureZone.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using UnityEngine;
|
||||
|
||||
// 히든 분기를 물리 접촉으로 발동시키는 트리거 존 (KimchiHitZone과 같은 방식).
|
||||
//
|
||||
// 예) NPC 얼굴에 IsTrigger 콜라이더를 붙이고 GestureKey="slap", RequiredTag="PlayerHand".
|
||||
// 대화 노드의 HiddenBranch가 설정되고 HiddenGestureKey="slap"인 순간에만
|
||||
// 이 존을 손으로 치면 히든 분기를 탄다. 그 외 시간엔 쳐도 아무 일도 안 일어난다.
|
||||
//
|
||||
// 콜라이더는 항상 켜 둬도 된다 — 발동 여부는 DialogPlayer의 무장 상태가 결정하므로,
|
||||
// 노드마다 존을 껐다 켰다 할 필요가 없다.
|
||||
public class DialogGestureZone : MonoBehaviour
|
||||
{
|
||||
[Tooltip("이 존이 발동시킬 제스처 키. 대화 노드의 HiddenGestureKey와 일치해야 한다. " +
|
||||
"노드 쪽 키가 비어 있으면 아무 존이나 발동 가능")]
|
||||
[SerializeField] private string _gestureKey = "slap";
|
||||
|
||||
[Tooltip("이 태그를 가진 콜라이더가 들어와야 발동 (예: 손 컨트롤러). 비우면 태그 무시")]
|
||||
[SerializeField] private string _requiredTag = "PlayerHand";
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_requiredTag) && !other.CompareTag(_requiredTag)) return;
|
||||
HiddenBranchResolver.Fire(_gestureKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 408261957835e514186f6a6f82e367e1
|
||||
@@ -40,6 +40,19 @@ public class DialogNode : ScriptableObject
|
||||
public DialogNode Next; // 선택지 없을 때 자동으로 갈 노드
|
||||
public List<DialogChoice> Choices; // 있으면 플레이어 선택 대기
|
||||
|
||||
[Header("Hidden Branch")]
|
||||
[Tooltip("이 대사가 재생되는 동안 특정 행동(제스처)을 하면 Next/선택지 대신 이 노드로 몰래 분기. " +
|
||||
"비우면 히든 분기 없음 — 평소엔 존재하지 않는 것처럼 보인다")]
|
||||
public DialogNode HiddenBranch;
|
||||
|
||||
[Tooltip("히든 분기를 여는 제스처 키 (DialogGestureZone의 Gesture Key와 일치). " +
|
||||
"비우면 아무 제스처 존이나 발동 가능")]
|
||||
public string HiddenGestureKey;
|
||||
|
||||
[Tooltip("히든 분기를 탔을 때 기록할 선택지 Code (선택지의 Code와 동일한 용도). " +
|
||||
"비우면 기록 안 함. 이후 대화 조건(RequiredChoiceCodes)에서 검사 가능")]
|
||||
public string HiddenCode;
|
||||
|
||||
[Header("ChoiceQuestion")]
|
||||
[TextArea(2,5)] public string ChoiceQuestion;
|
||||
|
||||
|
||||
@@ -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버튼) 한 번을 대기
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
// 히든 분기 감시 허브 (씬 오브젝트 불필요, 순수 static).
|
||||
//
|
||||
// 대화 노드가 재생되는 동안 DialogPlayer가 Arm(key)로 무장하고, 끝나면 Disarm()한다.
|
||||
// 그 창(무장 구간) 안에서 DialogGestureZone이 Fire(key)를 호출하면 Fired가 켜지고,
|
||||
// DialogPlayer가 이를 감지해 Next(또는 선택지) 대신 노드의 HiddenBranch로 몰래 분기한다.
|
||||
//
|
||||
// 무장돼 있지 않으면 어떤 Fire도 무시된다 → 평소엔 존재하지 않는 것처럼 동작(=히든).
|
||||
public static class HiddenBranchResolver
|
||||
{
|
||||
private static string _armedKey;
|
||||
|
||||
// 지금 히든 분기를 받을 수 있는 상태인가 (노드 재생 중)
|
||||
public static bool Armed { get; private set; }
|
||||
|
||||
// 이번 무장 구간에서 제스처가 발동했는가
|
||||
public static bool Fired { get; private set; }
|
||||
|
||||
// 발동 즉시 알림 — 떠 있는 선택지 메뉴를 그 자리에서 취소시키는 용도
|
||||
public static event Action FiredEvent;
|
||||
|
||||
// Enter Play Mode(도메인 리로드 off)에서 이전 상태가 안 남게
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ResetStatics()
|
||||
{
|
||||
_armedKey = null;
|
||||
Armed = false;
|
||||
Fired = false;
|
||||
FiredEvent = null;
|
||||
}
|
||||
|
||||
// 노드 진입 시 무장. key가 비면 아무 제스처 존이나 발동시킬 수 있다.
|
||||
public static void Arm(string key)
|
||||
{
|
||||
_armedKey = key;
|
||||
Armed = true;
|
||||
Fired = false;
|
||||
}
|
||||
|
||||
// 노드 종료 시 해제.
|
||||
public static void Disarm()
|
||||
{
|
||||
Armed = false;
|
||||
_armedKey = null;
|
||||
Fired = false;
|
||||
}
|
||||
|
||||
// 제스처 존이 호출. 무장 중이고 key가 맞을 때만 1회 발동.
|
||||
public static void Fire(string key)
|
||||
{
|
||||
if (!Armed || Fired) return;
|
||||
if (!string.IsNullOrEmpty(_armedKey) && _armedKey != key) return;
|
||||
Fired = true;
|
||||
FiredEvent?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1622ac8790b08674cbb2164b2a8dd1f7
|
||||
Reference in New Issue
Block a user