Merge branch 'main' of https://www.nakjungit.site/sharedacc520k/WhaleAdventure_VR
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} →");
|
||||
}
|
||||
}
|
||||
|
||||
107
Assets/02_Scripts/Managers/GameClear.cs
Normal file
107
Assets/02_Scripts/Managers/GameClear.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Managers/GameClear.cs.meta
Normal file
2
Assets/02_Scripts/Managers/GameClear.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22b271cf4093f3e458ae92b4a993272b
|
||||
@@ -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)
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user