636 lines
19 KiB
C#
636 lines
19 KiB
C#
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<GameFlowManager>();
|
|
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<Canvas>();
|
|
overlayCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
|
overlayCanvas.sortingOrder = 100;
|
|
|
|
CanvasScaler scaler = canvasObject.GetComponent<CanvasScaler>();
|
|
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<RectTransform>();
|
|
overlayRoot.anchorMin = Vector2.zero;
|
|
overlayRoot.anchorMax = Vector2.one;
|
|
overlayRoot.offsetMin = Vector2.zero;
|
|
overlayRoot.offsetMax = Vector2.zero;
|
|
|
|
Image background = rootObject.GetComponent<Image>();
|
|
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<RectTransform>();
|
|
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<Image>();
|
|
buttonImage.color = new Color(0.92f, 0.96f, 1f, 0.95f);
|
|
|
|
Button button = buttonObject.GetComponent<Button>();
|
|
button.onClick.AddListener(action);
|
|
|
|
TextMeshProUGUI buttonText = AddTextTo(buttonObject.transform, "Label", label, 15f, FontStyles.Bold);
|
|
RectTransform textRect = buttonText.rectTransform;
|
|
textRect.anchorMin = Vector2.zero;
|
|
textRect.anchorMax = Vector2.one;
|
|
textRect.offsetMin = Vector2.zero;
|
|
textRect.offsetMax = Vector2.zero;
|
|
buttonText.color = new Color(0.04f, 0.07f, 0.09f, 1f);
|
|
}
|
|
|
|
private int GetBodyCount() {
|
|
int count = 0;
|
|
|
|
for(int i = 0; i < overlayRoot.childCount; i++)
|
|
{
|
|
if(overlayRoot.GetChild(i).name.StartsWith("Body"))
|
|
{
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
private int GetButtonCount() {
|
|
int count = 0;
|
|
|
|
for(int i = 0; i < overlayRoot.childCount; i++)
|
|
{
|
|
if(overlayRoot.GetChild(i).name.StartsWith("Button"))
|
|
{
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
private TextMeshProUGUI AddText(string objectName, string text, float fontSize, FontStyles fontStyle) {
|
|
GameObject textObject = new GameObject(objectName + " " + GetBodyCount(), typeof(RectTransform), typeof(CanvasRenderer));
|
|
textObject.transform.SetParent(overlayRoot, false);
|
|
|
|
RectTransform rectTransform = textObject.GetComponent<RectTransform>();
|
|
rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
|
|
rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
|
|
rectTransform.pivot = new Vector2(0.5f, 0.5f);
|
|
|
|
TextMeshProUGUI label = textObject.AddComponent<TextMeshProUGUI>();
|
|
label.text = text;
|
|
label.font = runtimeFont != null ? runtimeFont : label.font;
|
|
label.fontSize = fontSize;
|
|
label.fontStyle = fontStyle;
|
|
label.alignment = TextAlignmentOptions.Center;
|
|
label.color = new Color(0.95f, 0.98f, 1f, 1f);
|
|
label.raycastTarget = false;
|
|
label.textWrappingMode = TextWrappingModes.Normal;
|
|
return label;
|
|
}
|
|
|
|
private TextMeshProUGUI AddTextTo(Transform parent, string objectName, string text, float fontSize, FontStyles fontStyle) {
|
|
GameObject textObject = new GameObject(objectName, typeof(RectTransform), typeof(CanvasRenderer));
|
|
textObject.transform.SetParent(parent, false);
|
|
TextMeshProUGUI label = textObject.AddComponent<TextMeshProUGUI>();
|
|
label.text = text;
|
|
label.font = runtimeFont != null ? runtimeFont : label.font;
|
|
label.fontSize = fontSize;
|
|
label.fontStyle = fontStyle;
|
|
label.alignment = TextAlignmentOptions.Center;
|
|
label.raycastTarget = false;
|
|
label.textWrappingMode = TextWrappingModes.NoWrap;
|
|
return label;
|
|
}
|
|
|
|
private TMP_FontAsset CreateKoreanFontAsset() {
|
|
string[] familyNames = {
|
|
"Malgun Gothic",
|
|
"맑은 고딕",
|
|
"Arial Unicode MS"
|
|
};
|
|
|
|
for(int i = 0; i < familyNames.Length; i++)
|
|
{
|
|
TMP_FontAsset fontAsset = TMP_FontAsset.CreateFontAsset(familyNames[i], "Regular", 80);
|
|
if(fontAsset != null)
|
|
{
|
|
fontAsset.name = "Runtime Flow Korean Font";
|
|
return fontAsset;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|