게임클리어 이벤트
This commit is contained in:
@@ -5,4 +5,5 @@ public class DialogChoice
|
||||
{
|
||||
public DialogNode DestinationNode;
|
||||
public string ChoiceText;
|
||||
public string Code; // 선택 시 기록/식별용 코드 (선택 입력, 영문 권장)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public class DialogNode : ScriptableObject
|
||||
|
||||
[Header("Behavior")]
|
||||
public bool LookAtPlayer;
|
||||
public bool WaitForInput; // true면 LineDuration 무시하고 B버튼(OnDialogNext) 입력까지 대기
|
||||
|
||||
[Header("Flow")]
|
||||
public DialogNode Next; // 선택지 없을 때 자동으로 갈 노드
|
||||
@@ -28,4 +29,7 @@ public class DialogNode : ScriptableObject
|
||||
|
||||
[Header("ChoiceQuestion")]
|
||||
[TextArea(2,5)] public string ChoiceQuestion;
|
||||
|
||||
[Header("Event")]
|
||||
public string EventKey; // 비어있지 않으면 이 노드가 재생될 때 DialogPlayer가 같은 Key의 이벤트를 호출
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -12,6 +14,18 @@ public struct RegionGroup
|
||||
public DialogGroup Group;
|
||||
}
|
||||
|
||||
// 마지막 선택지 코드(LastChoiceCode)를 인자로 넘기는 UnityEvent (인스펙터 노출용 구체 타입)
|
||||
[System.Serializable]
|
||||
public class ChoiceCodeEvent : UnityEvent<string> { }
|
||||
|
||||
// 노드의 EventKey ↔ 그 노드 재생 시 호출할 이벤트. 인자로 LastChoiceCode가 전달됨.
|
||||
[System.Serializable]
|
||||
public struct NodeEvent
|
||||
{
|
||||
public string Key;
|
||||
public ChoiceCodeEvent Event;
|
||||
}
|
||||
|
||||
[Tooltip("영역 이름 ↔ 그 영역에서 재생할 DialogGroup")]
|
||||
[SerializeField] private List<RegionGroup> _regionGroups;
|
||||
|
||||
@@ -23,6 +37,10 @@ public struct RegionGroup
|
||||
[SerializeField] private float _hudForwardOffset = 0.5f; // 화자→플레이어 방향으로 띄울 거리
|
||||
[SerializeField] private float _hudLateralOffset = 0f; // 좌우 오프셋 (+ 플레이어 시점 오른쪽)
|
||||
|
||||
[Header("Dialog Events")]
|
||||
[Tooltip("노드의 Event Key와 같은 Key가 그 노드 재생 시 호출됨")]
|
||||
[SerializeField] private List<NodeEvent> _nodeEvents = new();
|
||||
|
||||
private Dictionary<string, DialogGroup> _regionMap;
|
||||
private Animator _animator;
|
||||
private int _initialGestureHash;
|
||||
@@ -31,6 +49,11 @@ public struct RegionGroup
|
||||
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>();
|
||||
@@ -90,6 +113,7 @@ public async Awaitable Play(string region)
|
||||
if (node.Choices != null && node.Choices.Count > 0)
|
||||
{
|
||||
int picked = await WaitForChoice(node);
|
||||
RecordChoice(node, picked);
|
||||
node = node.Choices[picked].DestinationNode;
|
||||
}
|
||||
else
|
||||
@@ -169,6 +193,8 @@ private async Awaitable PlayNode(DialogNode node)
|
||||
if (DialogHud.Instance != null)
|
||||
DialogHud.Instance.Show(node.Speaker, node.TalkText, _hudChestHeight, _hudForwardOffset, _hudLateralOffset);
|
||||
|
||||
RaiseNodeEvent(node.EventKey); // EventKey 있으면 매칭 이벤트 호출
|
||||
|
||||
// 보이스 재생
|
||||
if (node.Voice != null && node.Speaker != null)
|
||||
{
|
||||
@@ -193,17 +219,47 @@ private async Awaitable PlayNode(DialogNode node)
|
||||
if (node.Expression != null)
|
||||
_animator.CrossFade(node.Expression.StateName, node.Expression.CrossFadeDuration, node.Expression.AnimationLayer);
|
||||
|
||||
// 대기 시간 결정
|
||||
float wait = 0f;
|
||||
if (node.Voice != null && node.Voice.Clip != null)
|
||||
wait = node.Voice.Clip.length;
|
||||
// 진행 방식 결정
|
||||
if (node.WaitForInput)
|
||||
{
|
||||
await WaitForAdvanceInput(); // B버튼 입력이 있어야만 다음으로
|
||||
}
|
||||
else
|
||||
wait = node.LineDuration;
|
||||
{
|
||||
float wait = (node.Voice != null && node.Voice.Clip != null)
|
||||
? node.Voice.Clip.length
|
||||
: node.LineDuration;
|
||||
|
||||
if (wait > 0f)
|
||||
await Awaitable.WaitForSecondsAsync(wait);
|
||||
else
|
||||
await WaitForAdvanceInput(); // 수동 진행
|
||||
if (wait > 0f)
|
||||
await Awaitable.WaitForSecondsAsync(wait);
|
||||
else
|
||||
await WaitForAdvanceInput(); // 지정 시간이 없으면 입력으로 진행
|
||||
}
|
||||
}
|
||||
|
||||
// 노드의 EventKey와 같은 Key를 가진 이벤트들을 호출
|
||||
private void RaiseNodeEvent(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) return;
|
||||
foreach (var e in _nodeEvents)
|
||||
if (e.Key == key) e.Event?.Invoke(LastChoiceCode); // 마지막 선택지 코드를 인자로 전달
|
||||
}
|
||||
|
||||
// 선택 결과 기록: 인덱스/코드를 프로퍼티 + DialogVariables에 저장하고 이벤트 발행
|
||||
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);
|
||||
}
|
||||
|
||||
private async Awaitable<int> WaitForChoice(DialogNode node)
|
||||
@@ -219,10 +275,33 @@ private async Awaitable<int> WaitForChoice(DialogNode node)
|
||||
|
||||
}
|
||||
|
||||
// 대화 진행 입력(OnDialogNext = VR B버튼) 한 번을 대기
|
||||
private async Awaitable WaitForAdvanceInput()
|
||||
{
|
||||
// TODO: VR 컨트롤러 버튼 입력 대기. 일단은 1초 대기
|
||||
await Awaitable.WaitForSecondsAsync(1f);
|
||||
var im = InputManager.Instance;
|
||||
if (im == null)
|
||||
{
|
||||
// 입력 매니저 없으면 안전하게 잠깐 대기 후 진행
|
||||
await Awaitable.WaitForSecondsAsync(1f);
|
||||
return;
|
||||
}
|
||||
|
||||
bool pressed = false;
|
||||
void Handler() => pressed = true;
|
||||
im.OnDialogNext_Event += Handler;
|
||||
try
|
||||
{
|
||||
while (!pressed)
|
||||
await Awaitable.NextFrameAsync(destroyCancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 대기 중 오브젝트 파괴 시 조용히 종료
|
||||
}
|
||||
finally
|
||||
{
|
||||
im.OnDialogNext_Event -= Handler;
|
||||
}
|
||||
}
|
||||
|
||||
//테스트용
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using UnityEngine;
|
||||
|
||||
// 인스펙터에 지정한 key로 DialogVariables에 값을 넣는 헬퍼.
|
||||
// 예: TMP_InputField의 On End Edit(string) → 이 컴포넌트의 Set(string) 에 연결하면
|
||||
// 플레이어가 입력한 글자가 {key} 토큰으로 대화에 들어간다.
|
||||
public class DialogVariableSetter : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private string _key;
|
||||
|
||||
public void Set(string value) => DialogVariables.Set(_key, value); // UnityEvent<string> 연결용
|
||||
public void SetKey(string key) => _key = key;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f182cc352a11ed48b27e690bdb10520
|
||||
51
Assets/02_Scripts/Communication/Dialog/DialogVariables.cs
Normal file
51
Assets/02_Scripts/Communication/Dialog/DialogVariables.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
// 대화 텍스트의 {key} 토큰을 런타임 값으로 치환하는 전역 저장소.
|
||||
// 예) DialogVariables.Set("playerName", "철수");
|
||||
// 대사 "안녕 {playerName}!" → "안녕 철수!"
|
||||
//
|
||||
// 표시 직전(DialogHud / ChoiceHud)에서 Format()을 거치므로, 그래프엔 그냥 {key}만 써두면 된다.
|
||||
public static class DialogVariables
|
||||
{
|
||||
private static readonly Dictionary<string, string> _values = new();
|
||||
|
||||
public static void Set(string key, string value) => _values[key] = value ?? string.Empty;
|
||||
public static void Remove(string key) => _values.Remove(key);
|
||||
public static void Clear() => _values.Clear();
|
||||
public static bool TryGet(string key, out string value) => _values.TryGetValue(key, out value);
|
||||
|
||||
// "{key}" 토큰을 등록된 값으로 치환. 등록 안 된 키는 그대로 둔다(빠진 값 디버깅용).
|
||||
public static string Format(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text) || text.IndexOf('{') < 0) return text;
|
||||
|
||||
var sb = new StringBuilder(text.Length);
|
||||
int i = 0;
|
||||
while (i < text.Length)
|
||||
{
|
||||
if (text[i] == '{')
|
||||
{
|
||||
int close = text.IndexOf('}', i + 1);
|
||||
if (close > i)
|
||||
{
|
||||
string key = text.Substring(i + 1, close - i - 1);
|
||||
if (_values.TryGetValue(key, out var val))
|
||||
{
|
||||
sb.Append(val);
|
||||
i = close + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.Append(text[i]);
|
||||
i++;
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// 플레이 시작마다 초기화 (Enter Play Mode에서 도메인 리로드를 꺼도 이전 값이 안 남게)
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ResetOnPlay() => _values.Clear();
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6bb90f809bd62e409383e02949f32c3
|
||||
@@ -81,6 +81,11 @@ public override void OnImportAsset(AssetImportContext ctx)
|
||||
dn.Voice = GetInputPortValue<VoiceClip>(gn.GetInputPortByName(DialogLineNode.PORT_VOICE));
|
||||
dn.LineDuration = GetInputPortValue<float>(gn.GetInputPortByName(DialogLineNode.PORT_DURATION));
|
||||
dn.LookAtPlayer = GetInputPortValue<bool>(gn.GetInputPortByName(DialogLineNode.PORT_LOOKAT));
|
||||
dn.WaitForInput = GetInputPortValue<bool>(gn.GetInputPortByName(DialogLineNode.PORT_WAITINPUT));
|
||||
|
||||
string eventKey = null;
|
||||
line.GetNodeOptionByName(DialogLineNode.OPTION_EVENT_KEY)?.TryGetValue(out eventKey);
|
||||
dn.EventKey = eventKey;
|
||||
|
||||
int choiceCount = 0;
|
||||
line.GetNodeOptionByName(DialogLineNode.OPTION_CHOICE_COUNT)?.TryGetValue(out choiceCount);
|
||||
@@ -96,10 +101,12 @@ public override void OnImportAsset(AssetImportContext ctx)
|
||||
for (int i = 0; i < choiceCount; i++)
|
||||
{
|
||||
var choiceText = GetInputPortValue<DialogText>(gn.GetInputPortByName(DialogLineNode.ChoiceTextPort(i))).Value;
|
||||
var choiceCode = GetInputPortValue<string>(gn.GetInputPortByName(DialogLineNode.ChoiceCodePort(i)));
|
||||
var dest = GetConnectedNode(gn, DialogLineNode.ChoiceOutPort(i));
|
||||
dn.Choices.Add(new DialogChoice
|
||||
{
|
||||
ChoiceText = choiceText,
|
||||
Code = choiceCode,
|
||||
DestinationNode = dest != null && map.TryGetValue(dest, out var destDn) ? destDn : null
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,12 +20,15 @@ internal class DialogLineNode : DialogGraphNode
|
||||
public const string PORT_VOICE = "Voice";
|
||||
public const string PORT_DURATION = "LineDuration";
|
||||
public const string PORT_LOOKAT = "LookAtPlayer";
|
||||
public const string PORT_WAITINPUT = "WaitForInput";
|
||||
public const string PORT_QUESTION = "ChoiceQuestion";
|
||||
|
||||
public const string OPTION_CHOICE_COUNT = "ChoiceCount";
|
||||
public const string OPTION_EVENT_KEY = "EventKey";
|
||||
|
||||
// 선택지별 포트 이름 규칙 (임포터와 공유)
|
||||
public static string ChoiceTextPort(int i) => $"Choice{i}Text";
|
||||
public static string ChoiceCodePort(int i) => $"Choice{i}Code";
|
||||
public static string ChoiceOutPort(int i) => $"Choice{i}Out";
|
||||
|
||||
protected override void OnDefineOptions(IOptionDefinitionContext context)
|
||||
@@ -35,6 +38,11 @@ protected override void OnDefineOptions(IOptionDefinitionContext context)
|
||||
.WithTooltip("0이면 선형 진행(Next), 1 이상이면 가변 N지선다 분기")
|
||||
.WithDefaultValue(0)
|
||||
.Delayed();
|
||||
|
||||
context.AddOption<string>(OPTION_EVENT_KEY)
|
||||
.WithDisplayName("Event Key")
|
||||
.WithTooltip("비우면 없음. 이 노드 재생 시 DialogPlayer의 같은 Key 이벤트 호출 (영문 키 권장)")
|
||||
.Delayed();
|
||||
}
|
||||
|
||||
protected override void OnDefinePorts(IPortDefinitionContext context)
|
||||
@@ -49,6 +57,7 @@ protected override void OnDefinePorts(IPortDefinitionContext context)
|
||||
context.AddInputPort<VoiceClip>(PORT_VOICE).WithDisplayName("Voice").Build();
|
||||
context.AddInputPort<float>(PORT_DURATION).WithDisplayName("Line Duration").Build();
|
||||
context.AddInputPort<bool>(PORT_LOOKAT).WithDisplayName("Look At Player").Build();
|
||||
context.AddInputPort<bool>(PORT_WAITINPUT).WithDisplayName("Wait For Input").Build();
|
||||
|
||||
int choiceCount = 0;
|
||||
GetNodeOptionByName(OPTION_CHOICE_COUNT)?.TryGetValue(out choiceCount);
|
||||
@@ -69,6 +78,9 @@ protected override void OnDefinePorts(IPortDefinitionContext context)
|
||||
context.AddInputPort<DialogText>(ChoiceTextPort(i))
|
||||
.WithDisplayName($"Choice {i + 1} Text")
|
||||
.Build();
|
||||
context.AddInputPort<string>(ChoiceCodePort(i))
|
||||
.WithDisplayName($"Choice {i + 1} Code")
|
||||
.Build();
|
||||
AddExecOutput(context, ChoiceOutPort(i), $"Choice {i + 1} →");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user