using UnityEngine; // 게임 씬에 하나. 현재 장소 프리팹을 띄우고, 장소 안 캐릭터 슬롯들의 등장 여부를 갱신한다. // DialogPlayer가 대화 후보를 조회할 때 (Database, Current)를 여기서 가져간다. public class LocationManager : MonoBehaviour { public static LocationManager Instance { get; private set; } [SerializeField] private StoryDatabase _database; [Tooltip("장소 프리팹을 붙일 부모 (비우면 이 오브젝트)")] [SerializeField] private Transform _locationRoot; [Tooltip("게임 시작 시 입장할 장소")] [SerializeField] private LocationData _startLocation; [Tooltip("장소 비트(StoryBeat의 Character를 비워 둔 항목)를 재생할 DialogPlayer. " + "CharacterVoiceObject 없는 씬 오브젝트에 DialogPlayer만 붙여 연결한다. " + "비우면 장소 자동 진행을 쓰지 않는다")] [SerializeField] private DialogPlayer _scenePlayer; [Tooltip("이 게임에 등장하는 캐릭터 슬롯들 (씬에 상주하는 인물 명단).\n\n" + "장소 프리팹은 이동할 때마다 파괴·재생성되지만 이 슬롯들은 씬에 남는다. " + "어느 장소에 나타날지는 배치가 아니라 StoryDatabase의 비트가 정하므로, " + "캐릭터를 장소에 추가하는 일이 비트 하나 등록으로 끝나고 장소마다 복제할 필요가 없다.\n\n" + "한 캐릭터는 한 번만 등록할 것 — 같은 인물의 슬롯이 둘 이상 활성이면 " + "CharacterVoiceObject 등록이 충돌한다")] [SerializeField] private CharacterSlot[] _characterSlots; public StoryDatabase Database => _database; public LocationData Current { get; private set; } // 지금 이 장소에 있는 인물의 대화 재생기 (아무도 없으면 null). // 「이야기한다」처럼 장소 단위로 눌리는 버튼이 "누구에게" 말을 걸지 여기서 가져간다 — // 버튼이 캐릭터를 직접 참조하면 장소마다 다시 배선해야 하지만, 이 방식이면 버튼은 그대로 둔다. // // 한 장소에 한 명을 전제로 한다. 슬롯이 여럿 켜져 있으면 목록상 첫 번째가 잡힌다 // (배경 2장짜리 장소처럼 인물이 둘 이상 필요해지면 이 자리에 선택 단계를 넣으면 된다). public DialogPlayer CurrentCharacter { get; private set; } private GameObject _currentInstance; // 대화 중 진행도/트리거가 바뀌었을 때의 슬롯 갱신 예약. // 대화 도중 화자를 꺼 버리면 재생이 깨지므로, 대화가 끝난 뒤로 미룬다. private bool _refreshPending; private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; if (_locationRoot == null) _locationRoot = transform; } private void Start() { if (StoryManager.Instance != null) StoryManager.Instance.Changed += OnStoryChanged; DialogPlayer.AnyActiveChanged += OnDialogActiveChanged; if (_startLocation != null) MoveTo(_startLocation); } private void OnDestroy() { if (Instance != this) return; if (StoryManager.Instance != null) StoryManager.Instance.Changed -= OnStoryChanged; DialogPlayer.AnyActiveChanged -= OnDialogActiveChanged; Instance = null; } // 장소 이동 (이동 버튼의 UnityEvent 연결용). 대화 중에는 무시된다. public void MoveTo(LocationData location) { if (location == null || DialogPlayer.IsAnyActive) return; Current = location; // 장소 BGM = SoundManager의 기본 BGM 층. 대화용 BGM(Override)이 걷히면 이 곡으로 돌아온다. // 비어 있으면 변경하지 않는다 — 노드 BGM과 같은 규칙(비면 변경 없음)이다. if (location.Bgm != null && SoundManager.Instance != null) SoundManager.Instance.SetDefaultBGM(location.Bgm); if (_currentInstance != null) Destroy(_currentInstance); _currentInstance = location.Prefab != null ? Instantiate(location.Prefab, _locationRoot) : null; RefreshSlots(); // 장소 비트 자동 진행 — 프리팹 생성과 슬롯 갱신이 끝난 뒤에 시작한다. // 조건을 만족하는 장소 비트가 없으면 아무 일도 일어나지 않는다. // 반복 재생을 막으려면 그 비트의 OnceOnly를 켜 둘 것. if (_scenePlayer != null) _scenePlayer.PlayAuto(); } // 대화로 진행도/트리거가 바뀌면 등장 캐릭터도 달라질 수 있다 private void OnStoryChanged() { if (DialogPlayer.IsAnyActive) { _refreshPending = true; return; } RefreshSlots(); } private void OnDialogActiveChanged(bool active) { if (active || !_refreshPending) return; _refreshPending = false; RefreshSlots(); } private void RefreshSlots() { CurrentCharacter = null; // 장소를 떠났거나 아무도 안 남을 수 있으므로 항상 먼저 비운다 if (_database == null) return; // ① 씬의 인물 명단 — 어느 장소에 나타날지는 각 슬롯이 DB를 보고 스스로 정한다 if (_characterSlots != null) foreach (var slot in _characterSlots) RefreshSlot(slot); // ② 장소 프리팹 안에 슬롯을 둔 경우 (그 장소에만 나오는 단역 등). // ①과 완전히 같은 판정을 타지만, 같은 인물을 양쪽에 두면 등록이 충돌한다. if (_currentInstance != null) foreach (var slot in _currentInstance.GetComponentsInChildren(includeInactive: true)) RefreshSlot(slot); } // 슬롯 하나 갱신 + 「이야기한다」 대상 후보 판정. // 대상은 "지금 걸 대화가 있는" 첫 슬롯이다 — 동행자로만 있는 슬롯은 IsPresent가 false라 // 후보에 오르지 않고, 그 장소·인물 비트가 생기는 순간 자연히 후보가 된다. private void RefreshSlot(CharacterSlot slot) { if (slot == null) return; slot.Refresh(_database, Current); if (CurrentCharacter == null && slot.IsPresent) CurrentCharacter = slot.Player; } // 「이야기한다」 버튼용 — 지금 장소에 있는 인물에게 말을 건다. // 걸 수 있는 대화가 여럿이면 DialogPlayer가 DialogEnterHud로 고르게 한다. // 아무도 없으면 조용히 넘어간다 (버튼을 눌러도 아무 일도 일어나지 않는다). public void TalkToCurrent() { if (CurrentCharacter != null) CurrentCharacter.Talk(); } }