This commit is contained in:
2026-06-23 17:21:20 +09:00
48 changed files with 2436 additions and 84 deletions

View File

@@ -5,4 +5,5 @@ public class DialogChoice
{
public DialogNode DestinationNode;
public string ChoiceText;
public string Code; // 선택 시 기록/식별용 코드 (선택 입력, 영문 권장)
}

View File

@@ -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의 이벤트를 호출
}

View File

@@ -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;
}
}
//테스트용

View File

@@ -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;
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6f182cc352a11ed48b27e690bdb10520

View 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();
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f6bb90f809bd62e409383e02949f32c3

View File

@@ -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
});
}

View File

@@ -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} →");
}
}

View File

@@ -0,0 +1,107 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
// 게임 클리어 시 처리:
// - NPC(오브젝트)들을 지정 위치로 재배치
// - 대화 존(DialogRegion)들을 활성/비활성 전환
// 리듬게임의 On Cleared 같은 UnityEvent에 OnGameClear()를 연결하면 한 번에 처리된다.
public class GameClear : MonoBehaviour
{
[SerializeField] private RoomClearGateController _clearGate;
// 옮길 오브젝트 ↔ 목적지(빈 Transform) 한 쌍
[System.Serializable]
public struct Relocation
{
public GameObject Target; // 옮길 오브젝트 (NPC 등)
public Transform Destination; // 옮길 위치/회전 기준
}
// 클리어 시 적용할 존 상태 한 쌍
[System.Serializable]
public struct ZoneState
{
public DialogRegion Zone; // 대상 존
public bool Active; // 이 상태로 전환
}
[Header("NPC 재배치")]
[SerializeField] private List<Relocation> _relocations = new();
[Header("대화 존 상태")]
[SerializeField] private List<ZoneState> _zoneStates = new();
// ── 클리어 시 한 번에 (UnityEvent 연결용) ─────────────────────
public void OnGameClear()
{
if(_clearGate != null)
{
//_clearGate.OpenClearGate();
_clearGate.MarkRoomCleared();
}
Relocate();
ApplyZoneStates();
SetClearDialogParameter();
}
// ── NPC 재배치 ───────────────────────────────────────────────
// 리스트의 각 오브젝트를 지정 위치(위치+회전)로 이동.
public void Relocate()
{
foreach (var r in _relocations)
Relocate(r.Target, r.Destination);
}
// 단일 오브젝트 재배치. NavMeshAgent가 있으면 Warp로 옮겨야 경로/내부상태가 안 깨진다.
public void Relocate(GameObject target, Transform destination)
{
if (target == null || destination == null) return;
// 따라다니던 중이면 멈춰서 재배치 위치에 머물게 (다시 플레이어를 향해 가지 않도록)
if (target.TryGetComponent(out FollowObject follow) && follow.FollowEnabled)
follow.DisableFollow();
if (target.TryGetComponent(out NavMeshAgent agent) && agent.isOnNavMesh)
{
agent.Warp(destination.position);
target.transform.rotation = destination.rotation;
}
else
{
target.transform.SetPositionAndRotation(destination.position, destination.rotation);
}
}
// ── 대화 존 활성/비활성 ──────────────────────────────────────
// 인스펙터에 설정한 _zoneStates를 한 번에 적용.
public void ApplyZoneStates()
{
foreach (var z in _zoneStates)
SetZoneActive(z.Zone, z.Active);
}
// 특정 존을 활성/비활성 (존 오브젝트째로 토글 → 트리거도 같이 꺼짐).
public void SetZoneActive(DialogRegion zone, bool active)
{
if (zone != null)
zone.gameObject.SetActive(active);
}
public void SetClearDialogParameter()
{
//DialogVariables.Set("SpaceSceneName1", RandomSceneRouteManager.Instance.GetNextSceneName1());
//DialogVariables.Set("SpaceSceneCode1", RandomSceneRouteManager.Instance.GetNextSceneCode1());
//DialogVariables.Set("SpaceSceneName2", RandomSceneRouteManager.Instance.GetNextSceneName2());
//DialogVariables.Set("SpaceSceneCode2", RandomSceneRouteManager.Instance.GetNextSceneCode2());
//테스트용
DialogVariables.Set("SpaceSceneName1", "블랙잭");
DialogVariables.Set("SpaceSceneCode1", "blackjack");
DialogVariables.Set("SpaceSceneName2", "미로방");
DialogVariables.Set("SpaceSceneCode2", "MazeRoom");
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 22b271cf4093f3e458ae92b4a993272b

View File

@@ -12,6 +12,7 @@ public class InputManager : MonoBehaviour, GameInput.IPlayerActions
// ─── 입력 이벤트들 (PlayerController 등이 구독) ──────────────────────
public event Action OnJump_Event; // 한 번씩 (눌렀을 때)
public event Action OnInteract_Event; // 상호작용 키 (앉기 등) — 눌렀을 때 한 번씩
public event Action OnDialogNext_Event; // 대화 다음 진행 (VR B버튼) — 눌렀을 때 한 번씩
//키보드로 테스트용
public event Action OnKey_Left_Event;
@@ -51,6 +52,12 @@ public void OnInteract(InputAction.CallbackContext ctx)
OnInteract_Event?.Invoke();
}
public void OnDialogNext(InputAction.CallbackContext ctx)
{
if (ctx.phase == InputActionPhase.Started)
OnDialogNext_Event?.Invoke();
}
public void OnKey_Left(InputAction.CallbackContext ctx)
{
if (ctx.phase == InputActionPhase.Started)

View File

@@ -2,7 +2,7 @@
public class RoomClearGateController : MonoBehaviour
{
[Header("방 클리어 후 열릴 게이트")]
[Header("<EFBFBD><EFBFBD> Ŭ<><C5AC><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>Ʈ")]
[SerializeField] private RoomExitGate exitGate;
private bool isRoomCleared = false;
@@ -10,20 +10,20 @@ public class RoomClearGateController : MonoBehaviour
public bool IsRoomCleared => isRoomCleared;
// 블랙잭 최종 승리 후 호출
// 이 함수는 게이트를 바로 열지 않고, "방 클리어 완료" 상태만 저장함
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20>¸<EFBFBD> <20><> ȣ<><C8A3>
// <EFBFBD><EFBFBD> <20>Լ<EFBFBD><D4BC><EFBFBD> <20><><EFBFBD><EFBFBD>Ʈ<EFBFBD><C6AE> <20>ٷ<EFBFBD> <20><><EFBFBD><EFBFBD> <20>ʰ<EFBFBD>, "<22><> Ŭ<><C5AC><EFBFBD><EFBFBD> <20>Ϸ<EFBFBD>" <20><><EFBFBD>¸<EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
public void MarkRoomCleared()
{
isRoomCleared = true;
Debug.Log("방 클리어 완료. 이제 오픈존에 들어가면 게이트가 열립니다.");
Debug.Log("<EFBFBD><EFBFBD> Ŭ<><C5AC><EFBFBD><EFBFBD> <20>Ϸ<EFBFBD>. <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EEB0A1> <20><><EFBFBD><EFBFBD>Ʈ<EFBFBD><C6AE> <20><><EFBFBD><EFBFBD><EFBFBD>ϴ<EFBFBD>.");
}
// 오픈존에 들어갔을 때 호출
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EEB0AC> <20><> ȣ<><C8A3>
public void OpenClearGate()
{
if (!isRoomCleared)
{
Debug.Log("아직 방 클리어 전이라 게이트를 열 수 없습니다.");
Debug.Log("<EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> Ŭ<><C5AC><EFBFBD><EFBFBD> <20><><EFBFBD>̶<EFBFBD> <20><><EFBFBD><EFBFBD>Ʈ<EFBFBD><C6AE> <20><> <20><> <20><><EFBFBD><EFBFBD><EFBFBD>ϴ<EFBFBD>.");
return;
}
@@ -37,11 +37,11 @@ public void OpenClearGate()
if (exitGate != null)
{
exitGate.OpenGate();
Debug.Log("방 클리어 게이트 오픈");
Debug.Log("<EFBFBD><EFBFBD> Ŭ<><C5AC><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>Ʈ <20><><EFBFBD><EFBFBD>");
}
else
{
Debug.LogWarning("Exit Gate가 연결되지 않았습니다.");
Debug.LogWarning("Exit Gate<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ʾҽ<CABE><D2BD>ϴ<EFBFBD>.");
}
}
@@ -50,4 +50,10 @@ public void ResetClearState()
isRoomCleared = false;
gateOpened = false;
}
public void OpenDoor(string code)
{
Debug.Log($"다음씬코드 : {code}");
}
}

View File

@@ -3,9 +3,13 @@
// 대상(보통 플레이어)을 NavMesh 위에서 따라다니는 간단한 동행 스크립트.
// 속도/가속/높이(Base Offset) 등은 NavMeshAgent 컴포넌트에서 설정한다.
// 추적은 _followEnabled로 동적으로 켜고/끌 수 있다.
[RequireComponent(typeof(NavMeshAgent))]
public class FollowObject : MonoBehaviour
{
[Header("Enable")]
[SerializeField] private bool _followEnabled = true; // 추적 on/off (런타임에 동적 변경 가능)
[Header("Target")]
[SerializeField] private Transform _target; // 비워두면 Camera.main 사용
@@ -20,14 +24,23 @@ public class FollowObject : MonoBehaviour
private NavMeshAgent _agent;
private float _repathTimer;
public bool FollowEnabled
{
get => _followEnabled;
set => SetFollowEnabled(value);
}
private void Awake()
{
_agent = GetComponent<NavMeshAgent>();
_agent.stoppingDistance = _followDistance;
ApplyAgentStopped();
}
private void Update()
{
if (!_followEnabled) return;
var target = ResolveTarget();
if (target == null || !_agent.isOnNavMesh) return;
@@ -45,6 +58,28 @@ private void Update()
FaceTarget(target);
}
// ── 동적 on/off ──────────────────────────────────────────────
public void SetFollowEnabled(bool on)
{
_followEnabled = on;
if (on) _repathTimer = 0f; // 켜면 즉시 경로 재계산
ApplyAgentStopped();
}
public void EnableFollow() => SetFollowEnabled(true); // UnityEvent 연결용(무인자)
public void DisableFollow() => SetFollowEnabled(false);
// 끄면 즉시 멈추고 남은 경로 제거(잔여 이동 방지), 켜면 이동 재개
private void ApplyAgentStopped()
{
if (_agent == null || !_agent.isActiveAndEnabled || !_agent.isOnNavMesh) return;
_agent.isStopped = !_followEnabled;
if (!_followEnabled)
_agent.ResetPath();
}
// ─────────────────────────────────────────────────────────────
private void FaceTarget(Transform target)
{
Vector3 dir = target.position - transform.position;

View File

@@ -47,7 +47,7 @@ public async Awaitable<int> Show(string choiceQuestion, List<DialogChoice> choic
for (int i = 0; i < choices.Count; i++)
{
var row = Instantiate(_rowPrefab, _rowContainer);
row.Bind(i, choices[i].ChoiceText);
row.Bind(i, DialogVariables.Format(choices[i].ChoiceText)); // {key} 토큰 치환
row.OnClicked += HandleClicked;
_rows.Add(row);
}
@@ -85,8 +85,9 @@ private void Hide()
private void SetQuestion(string question)
{
if (ChoiceQuestion == null) return;
bool hasQuestion = !string.IsNullOrWhiteSpace(question);
ChoiceQuestion.text = hasQuestion ? question : string.Empty;
string text = DialogVariables.Format(question); // {key} 토큰 치환
bool hasQuestion = !string.IsNullOrWhiteSpace(text);
ChoiceQuestion.text = hasQuestion ? text : string.Empty;
ChoiceQuestion.gameObject.SetActive(hasQuestion);
}

View File

@@ -53,9 +53,9 @@ public void Show(CharacterData speaker, string text, float chestHeight, float fo
_activeLateralOffset = lateralOffset;
if (_speakerName != null)
_speakerName.text = speaker != null ? speaker.Name : string.Empty;
_speakerName.text = speaker != null ? DialogVariables.Format(speaker.Name) : string.Empty;
if (_dialogueText != null)
_dialogueText.text = text;
_dialogueText.text = DialogVariables.Format(text); // {key} 토큰 치환
if (_panel != null) _panel.SetActive(true);
}