149 lines
6.7 KiB
C#
149 lines
6.7 KiB
C#
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<CharacterSlot>(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();
|
|
}
|
|
}
|