using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Playables; using UnityEngine.XR.Interaction.Toolkit.Locomotion; // 트리거 존(타임라인): Player 태그가 들어오면 등록된 타임라인을 재생. // 옵션으로 재생 동안 플레이어 로코모션(이동/회전)을 잠글 수 있다. // (UnityEvent를 실행하는 존은 EventZoneTrigger) [RequireComponent(typeof(Collider))] public class EventZoneTimeline : 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; // 어떤 EventZoneTimeline이든 타임라인(컷씬)이 재생 중이면 true. // 컷씬이 NPC의 루트를 직접 움직일 수 있으므로, 재생 중엔 대화 시작을 막는 데 쓴다. public static bool IsAnyPlaying => _playingCount > 0; private static int _playingCount; // 존이 실제로 발동(기록)되는 순간 ZoneId와 함께 호출 (SceneBgm 등에서 구독) public static event Action OnZoneTriggered; public string ZoneId => string.IsNullOrEmpty(_zoneId) ? name : _zoneId; // 잠글 때 켜져 있던 프로바이더만 기록해서, 해제 시 원래 꺼져 있던 것까지 켜지 않도록 한다. private readonly List _lockedProviders = new(); private void Reset() { GetComponent().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; OnZoneTriggered?.Invoke(ZoneId); if (_timeline == null) { Debug.LogWarning($"[EventZoneTimeline] 타임라인이 등록되지 않음 (기록만 됨): {name}"); return; } _isPlaying = true; _playingCount++; if (_lockPlayerControl) LockPlayer(other.transform); _timeline.stopped += OnTimelineStopped; _timeline.Play(); } // 타임라인이 끝(또는 외부에서 Stop)나면 호출됨. Extrapolation이 Hold면 stopped가 오지 않으니 None 권장. private void OnTimelineStopped(PlayableDirector director) { director.stopped -= OnTimelineStopped; if (_isPlaying) { _isPlaying = false; _playingCount--; } UnlockPlayer(); } // Player 태그 루트(XR Origin) 아래의 로코모션 프로바이더를 전부 꺼서 이동/회전을 막는다. private void LockPlayer(Transform playerRoot) { _lockedProviders.Clear(); foreach (var provider in playerRoot.GetComponentsInChildren()) { 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; // 존이 재생 중에 파괴돼도 전역 카운트/플레이어 잠금이 남지 않도록 if (_isPlaying) { _isPlaying = false; _playingCount--; } UnlockPlayer(); } }