2026-07-08 이벤트존 추가

This commit is contained in:
2026-07-08 18:15:44 +09:00
parent f00da071ff
commit 27a0dfcd44
11 changed files with 1177 additions and 5 deletions

View File

@@ -19,6 +19,9 @@ public class DialogCondition
[Tooltip("골랐어야 하는 선택지 Code들 (전부 필요)")]
public List<string> RequiredChoiceCodes = new();
[Tooltip("밟았어야 하는 이벤트존 Id들 (전부 필요)")]
public List<string> RequiredZoneIds = new();
// affectionTarget: 호감도 조건을 검사할 캐릭터 (보통 대화를 거는 NPC 자신)
public bool IsMet(CharacterData affectionTarget)
{
@@ -38,6 +41,10 @@ public bool IsMet(CharacterData affectionTarget)
if (!string.IsNullOrEmpty(code) && !story.HasChosen(code))
return false;
foreach (var zoneId in RequiredZoneIds)
if (!string.IsNullOrEmpty(zoneId) && !story.HasTriggeredZone(zoneId))
return false;
return true;
}
}

View File

@@ -73,6 +73,15 @@ public void RecordChoice(string code)
Changed?.Invoke();
}
// ── 이벤트존 이력 ────────────────────────────────────────────
public bool HasTriggeredZone(string zoneId) => _state.TriggeredZones.Contains(zoneId);
public void RecordZoneTriggered(string zoneId)
{
if (string.IsNullOrEmpty(zoneId) || !_state.TriggeredZones.Add(zoneId)) return;
Changed?.Invoke();
}
// ── 저장 / 로드 ──────────────────────────────────────────────
// 플레이 시작 시엔 항상 빈 상태로 시작한다 (테스트 반복이 꼬이지 않게).
// 이어하기를 만들 때 타이틀 화면 등에서 Load()를 호출하면 된다.

View File

@@ -0,0 +1,104 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.XR.Interaction.Toolkit.Locomotion;
// 트리거 존: Player 태그가 들어오면 등록된 타임라인을 재생.
// 옵션으로 재생 동안 플레이어 로코모션(이동/회전)을 잠글 수 있다.
[RequireComponent(typeof(Collider))]
public class EventZone : MonoBehaviour
{
[Tooltip("플레이어 진입 시 재생할 타임라인")]
[SerializeField] private PlayableDirector _timeline;
[Tooltip("StoryState에 기록되는 Id. 비워두면 오브젝트 이름 사용 (DialogCondition의 RequiredZoneIds에서 참조)")]
[SerializeField] private string _zoneId;
[Header("옵션")]
[Tooltip("재생 동안 플레이어 조작(로코모션)을 잠글지 여부. HMD 트래킹은 유지된다")]
[SerializeField] private bool _lockPlayerControl = true;
[Tooltip("한 번만 재생. 끄면 재생이 끝난 뒤 다시 들어오면 또 재생된다")]
[SerializeField] private bool _playOnce = true;
private bool _hasPlayed;
private bool _isPlaying;
public string ZoneId => string.IsNullOrEmpty(_zoneId) ? name : _zoneId;
// 잠글 때 켜져 있던 프로바이더만 기록해서, 해제 시 원래 꺼져 있던 것까지 켜지 않도록 한다.
private readonly List<LocomotionProvider> _lockedProviders = new();
private void Reset()
{
GetComponent<Collider>().isTrigger = true;
}
private void OnTriggerEnter(Collider other)
{
if (!other.CompareTag("Player")) return;
// 이어하기 로드 후에도 한 번 밟은 존은 다시 재생되지 않도록 StoryState 기록도 함께 본다.
var story = StoryManager.Instance; // 매니저 없는 테스트 씬에서도 재생은 되도록 null 허용
if (_isPlaying || (_playOnce && (_hasPlayed || (story != null && story.HasTriggeredZone(ZoneId)))))
return;
// 밟은 사실은 대화 활성화 조건(RequiredZoneIds) 판정에 쓰이므로 타임라인 유무와 무관하게 기록
if (story != null)
story.RecordZoneTriggered(ZoneId);
_hasPlayed = true;
if (_timeline == null)
{
Debug.LogWarning($"[EventZone] 타임라인이 등록되지 않음 (기록만 됨): {name}");
return;
}
_isPlaying = true;
if (_lockPlayerControl)
LockPlayer(other.transform);
_timeline.stopped += OnTimelineStopped;
_timeline.Play();
}
// 타임라인이 끝(또는 외부에서 Stop)나면 호출됨. Extrapolation이 Hold면 stopped가 오지 않으니 None 권장.
private void OnTimelineStopped(PlayableDirector director)
{
director.stopped -= OnTimelineStopped;
_isPlaying = false;
UnlockPlayer();
}
// Player 태그 루트(XR Origin) 아래의 로코모션 프로바이더를 전부 꺼서 이동/회전을 막는다.
private void LockPlayer(Transform playerRoot)
{
_lockedProviders.Clear();
foreach (var provider in playerRoot.GetComponentsInChildren<LocomotionProvider>())
{
if (!provider.enabled) continue;
provider.enabled = false;
_lockedProviders.Add(provider);
}
}
private void UnlockPlayer()
{
foreach (var provider in _lockedProviders)
{
if (provider != null)
provider.enabled = true;
}
_lockedProviders.Clear();
}
private void OnDestroy()
{
if (_timeline != null)
_timeline.stopped -= OnTimelineStopped;
// 존이 재생 중에 파괴돼도 플레이어가 잠긴 채 남지 않도록
UnlockPlayer();
}
}

View File

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

View File

@@ -10,6 +10,7 @@ public class StoryState
public readonly Dictionary<string, int> Affection = new(); // 캐릭터 Id → 호감도
public readonly HashSet<string> CompletedDialogs = new(); // 완료한 DialogGroup 이름
public readonly HashSet<string> ChosenCodes = new(); // 골랐던 선택지 Code
public readonly HashSet<string> TriggeredZones = new(); // 밟았던 EventZone Id
public void Clear()
{
@@ -17,6 +18,7 @@ public void Clear()
Affection.Clear();
CompletedDialogs.Clear();
ChosenCodes.Clear();
TriggeredZones.Clear();
}
// ── JSON 변환 ────────────────────────────────────────────────
@@ -29,6 +31,7 @@ private class JsonData
public List<int> AffectionValues = new();
public List<string> CompletedDialogs = new();
public List<string> ChosenCodes = new();
public List<string> TriggeredZones = new();
}
public string ToJson()
@@ -41,6 +44,7 @@ public string ToJson()
}
data.CompletedDialogs.AddRange(CompletedDialogs);
data.ChosenCodes.AddRange(ChosenCodes);
data.TriggeredZones.AddRange(TriggeredZones);
return JsonUtility.ToJson(data, prettyPrint: true);
}
@@ -55,6 +59,7 @@ public static StoryState FromJson(string json)
state.Affection[data.AffectionIds[i]] = data.AffectionValues[i];
state.CompletedDialogs.UnionWith(data.CompletedDialogs);
state.ChosenCodes.UnionWith(data.ChosenCodes);
state.TriggeredZones.UnionWith(data.TriggeredZones);
return state;
}
}