using System.Collections; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.SceneManagement; using UnityEngine.UI; public class GameFlowManager : MonoBehaviour { public enum FlowScreen { Intro, Game, Result, DutyFreeShop, MapPreview } public static GameFlowManager Instance { get; private set; } public static bool IsGameplayActive { get { return Instance == null || Instance.gameplayActive; } } public GameSaveData SaveData { get { return saveData; } } public MapDefinition CurrentMap { get { return currentMap; } } private const string IntroSceneName = "Intro"; private const string GameSceneName = "Main"; private const string ResultSceneName = "Result"; private const string DutyFreeShopSceneName = "DutyFreeShop"; private const string MapPreviewSceneName = "MapPreview"; private const int ShieldCost = 120; private const int LunchboxCost = 80; private const int SpeedShoesCost = 140; private const int MileageCardCost = 160; private GameSaveData saveData; private MapDefinition currentMap; private MapDefinition completedMap; private FlowScreen currentScreen = FlowScreen.Intro; private bool gameplayActive = false; private bool pendingStartGameplay = false; private bool lastResultCleared = false; private int lastResultScore = 0; private int lastResultMileage = 0; private float lastResultTime = 0f; private float lastResultDistance = 0f; private Canvas overlayCanvas; private RectTransform overlayRoot; private TMP_FontAsset runtimeFont; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Bootstrap() { if(Instance != null) { return; } GameObject flowObject = new GameObject("Game Flow Manager"); flowObject.AddComponent(); DontDestroyOnLoad(flowObject); } private void Awake() { if(Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); saveData = GameSaveManager.Load(); currentMap = MapDatabase.GetMapByIndex(saveData.currentMapIndex); SceneManager.sceneLoaded += OnSceneLoaded; } private void Start() { ShowIntro(); } private void OnDestroy() { if(Instance == this) { SceneManager.sceneLoaded -= OnSceneLoaded; Instance = null; } } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { EnsureOverlay(); if(pendingStartGameplay) { StartCoroutine(StartGameplayAfterSceneLoad()); return; } if(currentScreen != FlowScreen.Game) { RefreshCurrentScreen(); } } public void SetTotalMileage(int totalMileage) { if(saveData == null) { return; } saveData.totalMileage = Mathf.Max(0, totalMileage); GameSaveManager.Save(saveData); } public void AddMileage(int amount) { if(saveData == null) { return; } GameSaveManager.AddMileage(saveData, amount); } public void CompleteMap(bool cleared, int score, int stageMileage, float elapsedTime, float distance) { if(!gameplayActive && currentScreen == FlowScreen.Result) { return; } gameplayActive = false; currentScreen = FlowScreen.Result; completedMap = currentMap; lastResultCleared = cleared; lastResultScore = score; lastResultMileage = stageMileage; lastResultTime = elapsedTime; lastResultDistance = distance; if(cleared) { saveData.bestScore = Mathf.Max(saveData.bestScore, score); GameSaveManager.AddMileage(saveData, completedMap.clearMileageReward); if(completedMap.hasNextMap) { GameSaveManager.UnlockMap(saveData, completedMap.nextMapId); } else { GameSaveManager.Save(saveData); } } ShowResult(); } private void StartNewJourney() { saveData = GameSaveManager.ResetSave(); currentMap = MapDatabase.GetMapByIndex(saveData.currentMapIndex); completedMap = null; ShowMapPreview(); } private void ContinueJourney() { saveData = GameSaveManager.Load(); currentMap = MapDatabase.GetMapByIndex(saveData.currentMapIndex); completedMap = null; ShowMapPreview(); } private void RetryCurrentMap() { currentMap = MapDatabase.GetMapByIndex(saveData.currentMapIndex); LoadGameSceneForCurrentMap(); } private void LoadGameSceneForCurrentMap() { currentScreen = FlowScreen.Game; gameplayActive = false; pendingStartGameplay = true; ShowLoading("탑승 준비 중", currentMap.displayName + " 코스를 불러오는 중입니다."); if(Application.CanStreamedLevelBeLoaded(GameSceneName)) { SceneManager.LoadScene(GameSceneName); return; } Scene activeScene = SceneManager.GetActiveScene(); if(!string.IsNullOrEmpty(activeScene.name)) { SceneManager.LoadScene(activeScene.name); } } private IEnumerator StartGameplayAfterSceneLoad() { yield return null; pendingStartGameplay = false; currentScreen = FlowScreen.Game; gameplayActive = true; if(overlayCanvas != null) { overlayCanvas.gameObject.SetActive(false); } if(GameManager.instance != null) { GameManager.instance.BeginMap(currentMap); } } private void ShowIntro() { currentScreen = FlowScreen.Intro; gameplayActive = false; pendingStartGameplay = false; completedMap = null; if(TryLoadFlowScene(IntroSceneName)) { return; } EnsureOverlay(); ClearOverlay(); SetOverlayVisible(true); AddTitle("Uni-Run"); AddBody("여권을 챙기고 공항으로 출발합니다.\n진행도는 자동 저장됩니다."); AddBody("현재 경로: " + currentMap.displayName + "\n해금: " + MapDatabase.GetMapByIndex(saveData.unlockedMapIndex).displayName + "\n마일리지: " + saveData.totalMileage); AddButton("이어하기", ContinueJourney); AddButton("새 여행", StartNewJourney); AddButton("저장 초기화", ResetAndStayIntro); } private void ShowResult() { if(TryLoadFlowScene(ResultSceneName)) { return; } EnsureOverlay(); ClearOverlay(); SetOverlayVisible(true); string title = lastResultCleared ? "맵 클리어" : "일정 실패"; string body = completedMap.displayName + "\n점수: " + lastResultScore + "\n획득 마일리지: " + lastResultMileage + "\n거리: " + Mathf.FloorToInt(lastResultDistance) + "m / " + Mathf.FloorToInt(completedMap.targetDistance) + "m" + "\n시간: " + lastResultTime.ToString("0.0") + "s"; if(lastResultCleared) { body += "\n클리어 보상: " + completedMap.clearMileageReward + " 마일리지"; } AddTitle(title); AddBody(body); if(lastResultCleared) { AddButton("면세점으로", ShowDutyFreeShop); } else { AddButton("다시 도전", RetryCurrentMap); } AddButton("인트로", ShowIntro); } private void ShowDutyFreeShop() { currentScreen = FlowScreen.DutyFreeShop; gameplayActive = false; pendingStartGameplay = false; if(TryLoadFlowScene(DutyFreeShopSceneName)) { return; } EnsureOverlay(); ClearOverlay(); SetOverlayVisible(true); AddTitle("면세점"); AddBody("보유 마일리지: " + saveData.totalMileage + "\n도시락: " + saveData.lunchboxStock + " / 방어막: " + saveData.shieldStock + "\n운동화: " + saveData.speedShoesStock + " / 마일리지 카드: " + saveData.mileageCardStock); AddButton("도시락 구매 " + LunchboxCost, BuyLunchbox); AddButton("방어막 구매 " + ShieldCost, BuyShield); AddButton("운동화 구매 " + SpeedShoesCost, BuySpeedShoes); AddButton("마일리지 카드 구매 " + MileageCardCost, BuyMileageCard); AddButton("다음 맵 보기", ShowMapPreview); } private void ShowMapPreview() { currentScreen = FlowScreen.MapPreview; gameplayActive = false; pendingStartGameplay = false; if(completedMap != null && lastResultCleared && completedMap.hasNextMap) { currentMap = MapDatabase.GetMap(completedMap.nextMapId); } else { currentMap = MapDatabase.GetMapByIndex(saveData.currentMapIndex); } if(TryLoadFlowScene(MapPreviewSceneName)) { return; } EnsureOverlay(); ClearOverlay(); SetOverlayVisible(true); AddTitle("다음 경로"); AddBody(currentMap.displayName + "\n" + currentMap.routeName + "\n목표 거리: " + Mathf.FloorToInt(currentMap.targetDistance) + "m" + "\n제한 시간: " + currentMap.timeLimit.ToString("0") + "s"); AddButton("출발", ConfirmMapAndStart); AddButton("인트로", ShowIntro); } private void ConfirmMapAndStart() { saveData.currentMapIndex = MapDatabase.GetIndex(currentMap.mapId); saveData.unlockedMapIndex = Mathf.Max(saveData.unlockedMapIndex, saveData.currentMapIndex); GameSaveManager.Save(saveData); LoadGameSceneForCurrentMap(); } private void RefreshCurrentScreen() { switch(currentScreen) { case FlowScreen.Result: ShowResult(); break; case FlowScreen.DutyFreeShop: ShowDutyFreeShop(); break; case FlowScreen.MapPreview: ShowMapPreview(); break; case FlowScreen.Intro: default: ShowIntro(); break; } } private void ResetAndStayIntro() { saveData = GameSaveManager.ResetSave(); currentMap = MapDatabase.GetMapByIndex(saveData.currentMapIndex); ShowIntro(); } private void BuyLunchbox() { if(TrySpendMileage(LunchboxCost)) { saveData.lunchboxStock++; GameSaveManager.Save(saveData); } ShowDutyFreeShop(); } private void BuyShield() { if(TrySpendMileage(ShieldCost)) { saveData.shieldStock++; GameSaveManager.Save(saveData); } ShowDutyFreeShop(); } private void BuySpeedShoes() { if(TrySpendMileage(SpeedShoesCost)) { saveData.speedShoesStock++; GameSaveManager.Save(saveData); } ShowDutyFreeShop(); } private void BuyMileageCard() { if(TrySpendMileage(MileageCardCost)) { saveData.mileageCardStock++; GameSaveManager.Save(saveData); } ShowDutyFreeShop(); } private bool TrySpendMileage(int cost) { if(saveData.totalMileage < cost) { return false; } saveData.totalMileage -= cost; return true; } private void ShowLoading(string title, string body) { EnsureOverlay(); ClearOverlay(); SetOverlayVisible(true); AddTitle(title); AddBody(body); } private bool TryLoadFlowScene(string sceneName) { Scene activeScene = SceneManager.GetActiveScene(); if(activeScene.name == sceneName) { return false; } if(!Application.CanStreamedLevelBeLoaded(sceneName)) { return false; } SceneManager.LoadScene(sceneName); return true; } private void EnsureOverlay() { if(overlayCanvas != null) { return; } EnsureEventSystem(); GameObject canvasObject = new GameObject("Flow Overlay", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster)); DontDestroyOnLoad(canvasObject); overlayCanvas = canvasObject.GetComponent(); overlayCanvas.renderMode = RenderMode.ScreenSpaceOverlay; overlayCanvas.sortingOrder = 100; CanvasScaler scaler = canvasObject.GetComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1280f, 720f); scaler.matchWidthOrHeight = 0.5f; GameObject rootObject = new GameObject("Flow Overlay Root", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); rootObject.transform.SetParent(canvasObject.transform, false); overlayRoot = rootObject.GetComponent(); overlayRoot.anchorMin = Vector2.zero; overlayRoot.anchorMax = Vector2.one; overlayRoot.offsetMin = Vector2.zero; overlayRoot.offsetMax = Vector2.zero; Image background = rootObject.GetComponent(); background.color = new Color(0.02f, 0.035f, 0.045f, 0.92f); background.raycastTarget = true; runtimeFont = CreateKoreanFontAsset(); } private void EnsureEventSystem() { if(EventSystem.current != null) { return; } GameObject eventSystemObject = new GameObject("EventSystem", typeof(EventSystem), typeof(StandaloneInputModule)); DontDestroyOnLoad(eventSystemObject); } private void ClearOverlay() { if(overlayRoot == null) { return; } for(int i = overlayRoot.childCount - 1; i >= 0; i--) { Destroy(overlayRoot.GetChild(i).gameObject); } } private void SetOverlayVisible(bool visible) { if(overlayCanvas != null) { overlayCanvas.gameObject.SetActive(visible); } } private void AddTitle(string text) { TextMeshProUGUI title = AddText("Title", text, 34f, FontStyles.Bold); RectTransform rectTransform = title.rectTransform; rectTransform.anchoredPosition = new Vector2(0f, 160f); rectTransform.sizeDelta = new Vector2(760f, 62f); } private void AddBody(string text) { TextMeshProUGUI body = AddText("Body", text, 17f, FontStyles.Normal); RectTransform rectTransform = body.rectTransform; rectTransform.anchoredPosition = new Vector2(0f, 52f - (GetBodyCount() * 82f)); rectTransform.sizeDelta = new Vector2(760f, 96f); } private void AddButton(string label, UnityEngine.Events.UnityAction action) { int buttonIndex = GetButtonCount(); GameObject buttonObject = new GameObject("Button " + label, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button)); buttonObject.transform.SetParent(overlayRoot, false); RectTransform buttonRect = buttonObject.GetComponent(); buttonRect.anchorMin = new Vector2(0.5f, 0.5f); buttonRect.anchorMax = new Vector2(0.5f, 0.5f); buttonRect.pivot = new Vector2(0.5f, 0.5f); buttonRect.anchoredPosition = new Vector2(0f, -82f - (buttonIndex * 46f)); buttonRect.sizeDelta = new Vector2(260f, 34f); Image buttonImage = buttonObject.GetComponent(); buttonImage.color = new Color(0.92f, 0.96f, 1f, 0.95f); Button button = buttonObject.GetComponent