Files
Travel_Run/Assets/Scripts/Flow/GameFlowManager.cs
T
2026-07-09 02:50:48 +09:00

2067 lines
75 KiB
C#

using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
#if UNITY_EDITOR
using UnityEditor;
#endif
public partial class GameFlowManager : MonoBehaviour {
public enum FlowScreen {
Intro,
OpeningStory,
Game,
Result,
DutyFreeShop,
MapPreview,
DebugMenu
}
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 string DepartureStampResourcePath = "UI_DepartureStamp_Red";
private const string ResultPassportPanelResourcePath = "UI_ResultBoardingPassPanel_New";
private const string ResultPassportPanelAssetPath = "Assets/Resources/UI_ResultBoardingPassPanel_New.png";
private const string ResultAirportBackgroundResourcePath = "Map_Tutorial_Incheon_Airport";
private const string ResultButtonResourcePath = "UI_TutorialNextButton";
private const string ResultButtonAssetPath = "Assets/Resources/UI_TutorialNextButton.png";
private const string MileageIconAssetPath = "Assets/Sprites/UIIcons/UI_Mileage.png";
private const string TimerIconAssetPath = "Assets/Sprites/UIIcons/UI_Timer.png";
private const string CheckpointIconAssetPath = "Assets/Sprites/UIIcons/UI_Checkpoint.png";
private const string ClearRankIconAssetPath = "Assets/Sprites/UIIcons/UI_ClearRank.png";
private const string DutyFreeIconAssetPath = "Assets/Sprites/UIIcons/UI_DutyFreeShop.png";
private const string AirplaneTravelIconAssetPath = "Assets/Sprites/UIIcons/UI_AirplaneTravel.png";
private const string DutyFreeBackgroundResourcePath = "UI_DutyFreeShopInteriorPanel";
private const string DutyFreeBackgroundAssetPath = "Assets/Resources/UI_DutyFreeShopInteriorPanel.png";
private const string DutyFreeWindowPanelResourcePath = "UI_DutyFreeShopWindowPanel";
private const string DutyFreeWindowPanelAssetPath = "Assets/Resources/UI_DutyFreeShopWindowPanel.png";
private const string DutyFreeLunchboxAssetPath = "Assets/Sprites/DutyFree/DutyFree_Normal_Lunchbox.png";
private const string DutyFreeMileageCardAssetPath = "Assets/Sprites/DutyFree/DutyFree_Normal_MileageCard.png";
private const string DutyFreeSupplementAssetPath = "Assets/Sprites/DutyFree/DutyFree_Normal_Supplement.png";
private const string DutyFreeBackpackAssetPath = "Assets/Sprites/DutyFree/DutyFree_Normal_Backpack.png";
private const string DutyFreeProductCardBrownResourcePath = "UI_DutyFreeProductCard_Brown_Clean";
private const string DutyFreeProductCardGreenResourcePath = "UI_DutyFreeProductCard_Green_Clean";
private const string DutyFreeProductCardBlueResourcePath = "UI_DutyFreeProductCard_Blue_Clean";
private const string DutyFreeProductCardPurpleResourcePath = "UI_DutyFreeProductCard_Purple_Clean";
private const string DutyFreeEquipSlotPanelResourcePath = "UI_DutyFreeBagSlotPanel";
private const string DutyFreeActiveSlotResourcePath = "UI_DutyFreeBagSlot_Active";
private const string DutyFreeInactiveSlotResourcePath = "UI_DutyFreeBagSlot_Inactive";
private const string DutyFreeOccupiedSlotResourcePath = "UI_DutyFreeBagSlot_Occupied";
private const string DutyFreeButtonPrimaryResourcePath = "UI_DutyFreeButton_PrimaryYellow";
private const string DutyFreeButtonDisabledResourcePath = "UI_DutyFreeButton_DisabledGray";
private const string DutyFreeDirectionSignResourcePath = "UI_DutyFreeAirportDirectionSign";
private const string DutyFreePriceTagResourcePath = "UI_DutyFreePriceTag_LightBlue";
private const string DutyFreeProductPriceTagResourcePath = "UI_DutyFreeProductPriceTag";
private const string SouvenirCollectionIconAssetPath = "Assets/Sprites/UIIcons/UI_SouvenirCollection.png";
private const string AirportSouvenirAssetPath = "Assets/Sprites/Souvenirs/Souvenir_AirportMagnet.png";
private const string JapanSouvenirAssetPath = "Assets/Sprites/Souvenirs/Souvenir_Japan_Magnet.png";
private const string ChinaSouvenirAssetPath = "Assets/Sprites/Souvenirs/Souvenir_TempleCharm.png";
private const string AmericaSouvenirAssetPath = "Assets/Sprites/Souvenirs/Souvenir_SkylineMagnet.png";
private const int LunchboxCost = 80;
private const int MileageCardCost = 160;
private const int SupplementCost = 220;
private const int BaseMaxLife = 3;
private const int MaxSupplementPurchases = 4;
private const int MaxLifeWithSupplements = 5;
private const int BagSlotUpgradeCost = 260;
private static readonly string[] OpeningStoryPageAssetPaths = {
"Assets/ConceptArt/Opening/opening_cutscene_comic_jj_03_no_bubbles.png",
"Assets/ConceptArt/Opening/opening_cutscene_comic_jj_04_prepare_no_bubbles.png",
"Assets/ConceptArt/Opening/opening_cutscene_final_departure_jj_06_street_strict_stylematch.png"
};
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 int openingStoryPageIndex = 0;
private Canvas overlayCanvas;
private RectTransform overlayRoot;
private Image overlayBackground;
private Camera flowCamera;
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();
}
#if UNITY_EDITOR || DEVELOPMENT_BUILD
private void Update() {
if(Input.GetKeyDown(KeyCode.Escape))
{
ShowDebugMenu();
return;
}
HandleDebugMenuShortcutInput();
}
private void HandleDebugMenuShortcutInput() {
if(currentScreen != FlowScreen.DebugMenu)
{
return;
}
if(Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1))
{
ShowDebugIntroPreview();
}
else if(Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2))
{
ShowDebugGameStartPreview();
}
else if(Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3))
{
ShowDebugResultPreview(false);
}
else if(Input.GetKeyDown(KeyCode.Alpha4) || Input.GetKeyDown(KeyCode.Keypad4))
{
ShowDebugResultPreview(true);
}
else if(Input.GetKeyDown(KeyCode.Alpha5) || Input.GetKeyDown(KeyCode.Keypad5))
{
ShowDebugDutyFreeShopPreview();
}
else if(Input.GetKeyDown(KeyCode.Alpha6) || Input.GetKeyDown(KeyCode.Keypad6))
{
ShowDebugMapPreview();
}
else if(Input.GetKeyDown(KeyCode.Alpha7) || Input.GetKeyDown(KeyCode.Keypad7))
{
ShowDebugMapGameStartPreview(MapId.Japan);
}
else if(Input.GetKeyDown(KeyCode.Alpha8) || Input.GetKeyDown(KeyCode.Keypad8))
{
ShowDebugMapResultPreview(MapId.Japan, true);
}
else if(Input.GetKeyDown(KeyCode.Alpha9) || Input.GetKeyDown(KeyCode.Keypad9))
{
ShowDebugMapGameStartPreview(MapId.China);
}
else if(Input.GetKeyDown(KeyCode.Alpha0) || Input.GetKeyDown(KeyCode.Keypad0))
{
ShowDebugMapResultPreview(MapId.China, true);
}
else if(Input.GetKeyDown(KeyCode.F1))
{
ShowDebugMapGameStartPreview(MapId.America);
}
else if(Input.GetKeyDown(KeyCode.F2))
{
ShowDebugMapResultPreview(MapId.America, true);
}
}
#endif
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;
openingStoryPageIndex = 0;
ShowOpeningStory();
}
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;
SetOverlayVisible(false);
if(GameManager.instance != null)
{
GameManager.instance.BeginMap(currentMap);
}
}
private void ShowOpeningStory() {
currentScreen = FlowScreen.OpeningStory;
gameplayActive = false;
pendingStartGameplay = false;
if(TryLoadFlowScene(IntroSceneName))
{
return;
}
EnsureOverlay();
ClearOverlay();
SetOverlayVisible(true);
BuildOpeningStoryScreen();
}
private void ShowIntro() {
currentScreen = FlowScreen.Intro;
gameplayActive = false;
pendingStartGameplay = false;
completedMap = null;
if(TryLoadFlowScene(IntroSceneName))
{
return;
}
EnsureOverlay();
ClearOverlay();
SetOverlayVisible(true);
BuildIntroScreen();
}
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);
BuildMapPreviewScreen();
}
private void ConfirmMapAndStart() {
saveData.currentMapIndex = MapDatabase.GetIndex(currentMap.mapId);
saveData.unlockedMapIndex = Mathf.Max(saveData.unlockedMapIndex, saveData.currentMapIndex);
GameSaveManager.Save(saveData);
LoadGameSceneForCurrentMap();
}
private void BuildMapPreviewScreen() {
if(overlayBackground != null)
{
overlayBackground.sprite = null;
overlayBackground.preserveAspect = false;
overlayBackground.color = Color.clear;
}
if(flowCamera != null)
{
flowCamera.backgroundColor = new Color(0.006f, 0.022f, 0.035f, 1f);
}
MapId destinationMapId = currentMap != null ? currentMap.mapId : MapId.TutorialIncheon;
MapId originMapId = GetMapPreviewOriginMapId(destinationMapId);
RectTransform mapRect = AddIntroWorldMap();
AddMapPreviewRoute(mapRect, originMapId, destinationMapId);
AddMapPreviewTravelPlane(mapRect, originMapId, destinationMapId);
AddMapPreviewActionDock();
}
private MapId GetMapPreviewOriginMapId(MapId destinationMapId) {
if(completedMap != null && lastResultCleared && completedMap.hasNextMap)
{
return completedMap.mapId;
}
int destinationIndex = MapDatabase.GetIndex(destinationMapId);
if(destinationIndex > 0)
{
return MapDatabase.GetMapByIndex(destinationIndex - 1).mapId;
}
return destinationMapId;
}
private void AddMapPreviewRoute(RectTransform mapRect, MapId originMapId, MapId destinationMapId) {
Vector2[] routePoints = GetIntroRoutePoints();
Color inactiveRouteColor = new Color(0.72f, 0.92f, 1f, 0.22f);
Color activeRouteColor = new Color(1f, 0.86f, 0.28f, 0.88f);
for(int i = 0; i < routePoints.Length - 1; i++)
{
AddIntroRouteSegment(mapRect, routePoints[i], routePoints[i + 1], inactiveRouteColor, 3f);
}
int originIndex = GetIntroRouteIndex(originMapId);
int destinationIndex = GetIntroRouteIndex(destinationMapId);
if(originIndex != destinationIndex)
{
int step = originIndex < destinationIndex ? 1 : -1;
for(int i = originIndex; i != destinationIndex; i += step)
{
AddIntroRouteSegment(mapRect, routePoints[i], routePoints[i + step], activeRouteColor, 6f);
}
}
AddMapPreviewRouteMarker(mapRect, MapId.TutorialIncheon, originMapId, destinationMapId, 0f);
AddMapPreviewRouteMarker(mapRect, MapId.Japan, originMapId, destinationMapId, 0.4f);
AddMapPreviewRouteMarker(mapRect, MapId.China, originMapId, destinationMapId, 0.8f);
AddMapPreviewRouteMarker(mapRect, MapId.America, originMapId, destinationMapId, 1.2f);
}
private void AddMapPreviewRouteMarker(RectTransform mapRect, MapId markerMapId, MapId originMapId, MapId destinationMapId, float phase) {
bool isOrigin = markerMapId == originMapId;
bool isDestination = markerMapId == destinationMapId;
Vector2 position = GetIntroRoutePoint(markerMapId);
GameObject markerObject = CreateUiObject("Map Preview Route Marker " + markerMapId, mapRect, typeof(Image));
RectTransform markerRect = markerObject.GetComponent<RectTransform>();
markerRect.anchorMin = new Vector2(0.5f, 0.5f);
markerRect.anchorMax = new Vector2(0.5f, 0.5f);
markerRect.pivot = new Vector2(0.5f, 0.5f);
markerRect.anchoredPosition = position;
markerRect.sizeDelta = isDestination
? new Vector2(28f, 28f)
: isOrigin
? new Vector2(21f, 21f)
: new Vector2(15f, 15f);
Image markerImage = markerObject.GetComponent<Image>();
markerImage.color = isDestination
? new Color(1f, 0.86f, 0.26f, 0.96f)
: isOrigin
? new Color(0.82f, 0.96f, 1f, 0.90f)
: new Color(0.56f, 0.78f, 0.86f, 0.54f);
markerImage.raycastTarget = false;
FlowIntroFloatMotion markerMotion = markerObject.AddComponent<FlowIntroFloatMotion>();
markerMotion.scalePulse = isDestination ? 0.18f : isOrigin ? 0.10f : 0.05f;
markerMotion.frequency = new Vector2(0f, 0.72f);
markerMotion.phase = phase;
TextMeshProUGUI labelText = AddIntroMapLabel(
mapRect,
"Map Preview Marker Label " + markerMapId,
GetIntroRouteLabel(markerMapId),
position + GetIntroRouteLabelOffset(markerMapId),
new Vector2(94f, 20f),
isDestination ? 10.5f : 9.5f);
labelText.color = isDestination
? new Color(1f, 0.92f, 0.34f, 0.94f)
: isOrigin
? new Color(0.80f, 0.95f, 1f, 0.84f)
: new Color(0.68f, 0.86f, 0.92f, 0.54f);
}
private void AddMapPreviewTravelPlane(RectTransform mapRect, MapId originMapId, MapId destinationMapId) {
Sprite planeSprite = LoadUiSprite("", AirplaneTravelIconAssetPath);
GameObject planeObject = CreateUiObject("Map Preview Airplane", overlayRoot, typeof(Image));
RectTransform planeRect = planeObject.GetComponent<RectTransform>();
planeRect.anchorMin = new Vector2(0.5f, 0.5f);
planeRect.anchorMax = new Vector2(0.5f, 0.5f);
planeRect.pivot = new Vector2(0.5f, 0.5f);
planeRect.anchoredPosition = GetIntroRoutePoint(originMapId) + mapRect.anchoredPosition;
planeRect.sizeDelta = new Vector2(66f, 66f);
Image planeImage = planeObject.GetComponent<Image>();
planeImage.sprite = planeSprite;
planeImage.color = planeSprite != null
? new Color(1f, 1f, 1f, 0.98f)
: new Color(0.9f, 0.97f, 1f, 0.92f);
planeImage.preserveAspect = true;
planeImage.raycastTarget = false;
FlowIntroRouteMotion motion = planeObject.AddComponent<FlowIntroRouteMotion>();
motion.points = GetMapPreviewTravelPoints(originMapId, destinationMapId);
motion.offset = mapRect.anchoredPosition;
motion.duration = 3.8f;
}
private Vector2[] GetMapPreviewTravelPoints(MapId originMapId, MapId destinationMapId) {
if(originMapId == destinationMapId)
{
Vector2 destinationPoint = GetIntroRoutePoint(destinationMapId);
return new Vector2[] {
destinationPoint + new Vector2(-70f, -12f),
destinationPoint
};
}
Vector2[] routePoints = GetIntroRoutePoints();
int originIndex = GetIntroRouteIndex(originMapId);
int destinationIndex = GetIntroRouteIndex(destinationMapId);
int step = originIndex < destinationIndex ? 1 : -1;
int pointCount = Mathf.Abs(destinationIndex - originIndex) + 1;
Vector2[] travelPoints = new Vector2[pointCount];
int writeIndex = 0;
for(int i = originIndex; i != destinationIndex + step; i += step)
{
travelPoints[writeIndex] = routePoints[i];
writeIndex++;
}
return travelPoints;
}
private void AddMapPreviewActionDock() {
GameObject dockObject = CreateUiObject("Map Preview Button Dock", overlayRoot);
RectTransform dockRect = dockObject.GetComponent<RectTransform>();
dockRect.anchorMin = new Vector2(0.5f, 0.5f);
dockRect.anchorMax = new Vector2(0.5f, 0.5f);
dockRect.pivot = new Vector2(0.5f, 0.5f);
dockRect.anchoredPosition = new Vector2(328f, -236f);
dockRect.sizeDelta = new Vector2(424f, 136f);
AddIntroButton(dockRect, "출발", new Vector2(45f, 0f), ConfirmMapAndStart, true, 0);
AddIntroButton(dockRect, "인트로", new Vector2(110f, -76f), ShowIntro, false, 1);
}
private void RefreshCurrentScreen() {
switch(currentScreen)
{
case FlowScreen.Result:
ShowResult();
break;
case FlowScreen.DutyFreeShop:
ShowDutyFreeShop();
break;
case FlowScreen.MapPreview:
ShowMapPreview();
break;
case FlowScreen.OpeningStory:
ShowOpeningStory();
break;
#if UNITY_EDITOR || DEVELOPMENT_BUILD
case FlowScreen.DebugMenu:
ShowDebugMenu();
break;
#endif
case FlowScreen.Intro:
default:
ShowIntro();
break;
}
}
private void ResetAndStayIntro() {
saveData = GameSaveManager.ResetSave();
currentMap = MapDatabase.GetMapByIndex(saveData.currentMapIndex);
ShowIntro();
}
private void BuyLunchbox() {
EnsureDutyFreeStagePurchaseLimits();
PrepareOrBuyDutyFreeItem(DutyFreeItemType.Lunchbox, LunchboxCost);
ShowDutyFreeShop();
}
private void BuyMileageCard() {
EnsureDutyFreeStagePurchaseLimits();
PrepareOrBuyDutyFreeItem(DutyFreeItemType.MileageCard, MileageCardCost);
ShowDutyFreeShop();
}
private void BuySupplement() {
EnsureDutyFreeStagePurchaseLimits();
if(CanBuySupplement() && TrySpendMileage(SupplementCost))
{
saveData.supplementPurchases++;
saveData.purchasedSupplementThisStage = true;
GameSaveManager.Save(saveData);
}
ShowDutyFreeShop();
}
private void BuyBagSlotUpgrade() {
if(saveData.bagSlots < 3 && TrySpendMileage(BagSlotUpgradeCost))
{
saveData.bagSlots++;
GameSaveManager.Save(saveData);
}
ShowDutyFreeShop();
}
private void PrepareOrBuyDutyFreeItem(DutyFreeItemType itemType, int cost) {
if(TryEquipOwnedDutyFreeItem(itemType))
{
GameSaveManager.Save(saveData);
return;
}
if(!CanBuyDutyFreeItem(itemType, cost) || !TrySpendMileage(cost))
{
return;
}
AddDutyFreeStock(itemType, 1);
if(itemType == DutyFreeItemType.MileageCard)
{
saveData.purchasedMileageCardThisStage = true;
}
TryEquipOwnedDutyFreeItem(itemType);
GameSaveManager.Save(saveData);
}
private bool TryEquipOwnedDutyFreeItem(DutyFreeItemType itemType) {
if(itemType == DutyFreeItemType.None || !HasFreeDutyFreeSlot())
{
return false;
}
if(GetEquippedDutyFreeItemCount(itemType) >= GetDutyFreeStock(itemType))
{
return false;
}
for(int i = 0; i < saveData.bagSlots; i++)
{
if(GetEquippedDutyFreeItem(i) != DutyFreeItemType.None)
{
continue;
}
SetEquippedDutyFreeItem(i, itemType);
return true;
}
return false;
}
private void UnequipDutyFreeSlot(int slotIndex) {
SetEquippedDutyFreeItem(slotIndex, DutyFreeItemType.None);
GameSaveManager.Save(saveData);
ShowDutyFreeShop();
}
public void ConsumeEquippedDutyFreeItem(int slotIndex, DutyFreeItemType itemType) {
if(saveData == null
|| itemType == DutyFreeItemType.None
|| GetEquippedDutyFreeItem(slotIndex) != itemType)
{
return;
}
SetEquippedDutyFreeItem(slotIndex, DutyFreeItemType.None);
AddDutyFreeStock(itemType, -1);
GameSaveManager.Save(saveData);
}
private bool TrySpendMileage(int cost) {
if(saveData.totalMileage < cost)
{
return false;
}
saveData.totalMileage -= cost;
return true;
}
private bool HasFreeDutyFreeSlot() {
for(int i = 0; i < saveData.bagSlots; i++)
{
if(GetEquippedDutyFreeItem(i) == DutyFreeItemType.None)
{
return true;
}
}
return false;
}
private DutyFreeItemType GetEquippedDutyFreeItem(int slotIndex) {
switch(slotIndex)
{
case 0:
return saveData.equippedSlot1;
case 1:
return saveData.equippedSlot2;
case 2:
return saveData.equippedSlot3;
default:
return DutyFreeItemType.None;
}
}
private void SetEquippedDutyFreeItem(int slotIndex, DutyFreeItemType itemType) {
switch(slotIndex)
{
case 0:
saveData.equippedSlot1 = itemType;
break;
case 1:
saveData.equippedSlot2 = itemType;
break;
case 2:
saveData.equippedSlot3 = itemType;
break;
}
}
private int GetEquippedDutyFreeItemCount(DutyFreeItemType itemType) {
int count = 0;
for(int i = 0; i < saveData.bagSlots; i++)
{
if(GetEquippedDutyFreeItem(i) == itemType)
{
count++;
}
}
return count;
}
private int GetDutyFreeStock(DutyFreeItemType itemType) {
switch(itemType)
{
case DutyFreeItemType.Lunchbox:
return saveData.lunchboxStock;
case DutyFreeItemType.MileageCard:
return saveData.mileageCardStock;
default:
return 0;
}
}
private void AddDutyFreeStock(DutyFreeItemType itemType, int amount) {
switch(itemType)
{
case DutyFreeItemType.Lunchbox:
saveData.lunchboxStock = Mathf.Max(0, saveData.lunchboxStock + amount);
break;
case DutyFreeItemType.MileageCard:
saveData.mileageCardStock = Mathf.Max(0, saveData.mileageCardStock + amount);
break;
}
}
private int GetDutyFreeLimitStageIndex() {
if(completedMap != null && lastResultCleared && completedMap.hasNextMap)
{
return MapDatabase.GetIndex(completedMap.nextMapId);
}
if(currentMap != null)
{
return MapDatabase.GetIndex(currentMap.mapId);
}
return saveData != null
? Mathf.Clamp(saveData.currentMapIndex, 0, MapDatabase.LastMapIndex)
: 0;
}
private void EnsureDutyFreeStagePurchaseLimits() {
if(saveData == null)
{
return;
}
int stageIndex = GetDutyFreeLimitStageIndex();
if(saveData.dutyFreeLimitStageIndex == stageIndex)
{
return;
}
saveData.dutyFreeLimitStageIndex = stageIndex;
saveData.purchasedMileageCardThisStage = false;
saveData.purchasedSupplementThisStage = false;
GameSaveManager.Save(saveData);
}
private bool HasPurchasedMileageCardInDutyFreeStage() {
return saveData != null
&& saveData.dutyFreeLimitStageIndex == GetDutyFreeLimitStageIndex()
&& saveData.purchasedMileageCardThisStage;
}
private bool HasPurchasedSupplementInDutyFreeStage() {
return saveData != null
&& saveData.dutyFreeLimitStageIndex == GetDutyFreeLimitStageIndex()
&& saveData.purchasedSupplementThisStage;
}
private bool CanBuyDutyFreeItem(DutyFreeItemType itemType, int cost) {
if(saveData == null || itemType == DutyFreeItemType.None || saveData.totalMileage < cost)
{
return false;
}
if(!HasFreeDutyFreeSlot() || GetOwnedDutyFreeItemCount() >= Mathf.Clamp(saveData.bagSlots, 1, 3))
{
return false;
}
if(itemType == DutyFreeItemType.Lunchbox && GetDutyFreeStock(DutyFreeItemType.Lunchbox) >= GetDutyFreeMaxLife())
{
return false;
}
if(itemType == DutyFreeItemType.MileageCard && HasPurchasedMileageCardInDutyFreeStage())
{
return false;
}
return true;
}
private bool CanBuySupplement() {
return saveData != null
&& saveData.supplementPurchases < MaxSupplementPurchases
&& !HasPurchasedSupplementInDutyFreeStage()
&& saveData.totalMileage >= SupplementCost;
}
private int GetDutyFreeMaxLife() {
int supplementPurchases = saveData != null
? Mathf.Clamp(saveData.supplementPurchases, 0, MaxSupplementPurchases)
: 0;
return Mathf.Min(MaxLifeWithSupplements, BaseMaxLife + (supplementPurchases / 2));
}
private int GetOwnedDutyFreeItemCount() {
return saveData != null
? Mathf.Max(0, saveData.lunchboxStock) + Mathf.Max(0, saveData.mileageCardStock)
: 0;
}
private string GetDutyFreePurchaseLabel(DutyFreeItemType itemType, bool canEquip, bool canBuy, bool hasFreeSlot, int stock, int equippedCount) {
if(canEquip)
{
return "장착";
}
if(canBuy)
{
return "구매";
}
if(itemType == DutyFreeItemType.Lunchbox && stock >= GetDutyFreeMaxLife())
{
return "MAX";
}
if(itemType == DutyFreeItemType.MileageCard && HasPurchasedMileageCardInDutyFreeStage())
{
return "1회 완료";
}
if(stock > equippedCount && !hasFreeSlot)
{
return "슬롯 가득";
}
if(!hasFreeSlot || GetOwnedDutyFreeItemCount() >= Mathf.Clamp(saveData.bagSlots, 1, 3))
{
return "슬롯 가득";
}
return "부족";
}
private string GetDutyFreeSupplementPurchaseLabel(bool canBuy, bool isMax, bool alreadyPurchasedThisStage) {
if(isMax)
{
return "MAX";
}
if(alreadyPurchasedThisStage)
{
return "1회 완료";
}
return canBuy ? "구매" : "부족";
}
private Sprite GetDutyFreeItemSprite(DutyFreeItemType itemType) {
switch(itemType)
{
case DutyFreeItemType.Lunchbox:
return LoadUiSprite("", DutyFreeLunchboxAssetPath);
case DutyFreeItemType.MileageCard:
return LoadUiSprite("", DutyFreeMileageCardAssetPath);
case DutyFreeItemType.Supplement:
return LoadUiSprite("", DutyFreeSupplementAssetPath);
default:
return null;
}
}
private Sprite LoadDutyFreeResourceSprite(string resourcePath) {
return LoadUiSprite(resourcePath, "Assets/Resources/" + resourcePath + ".png");
}
private Sprite GetDutyFreeProductCardSprite(DutyFreeItemType itemType) {
switch(itemType)
{
case DutyFreeItemType.Lunchbox:
return LoadUiSprite(DutyFreeProductCardBrownResourcePath, "Assets/Resources/UI_DutyFreeProductCard_Brown_Clean.png");
case DutyFreeItemType.Supplement:
return LoadUiSprite(DutyFreeProductCardGreenResourcePath, "Assets/Resources/UI_DutyFreeProductCard_Green_Clean.png");
case DutyFreeItemType.MileageCard:
return LoadUiSprite(DutyFreeProductCardPurpleResourcePath, "Assets/Resources/UI_DutyFreeProductCard_Purple_Clean.png");
default:
return null;
}
}
private string GetDutyFreeItemEffectText(DutyFreeItemType itemType) {
switch(itemType)
{
case DutyFreeItemType.Lunchbox:
return "HP 회복";
case DutyFreeItemType.MileageCard:
return "획득 증가";
case DutyFreeItemType.Supplement:
return "최대 HP +0.5";
default:
return "";
}
}
private string GetDutyFreeSupplementEffectText(int purchaseCount) {
int clampedCount = Mathf.Clamp(purchaseCount, 0, MaxSupplementPurchases);
return "최대 HP +0.5\n" + clampedCount + "/" + MaxSupplementPurchases;
}
private string GetDutyFreeItemShortName(DutyFreeItemType itemType) {
switch(itemType)
{
case DutyFreeItemType.Lunchbox:
return "도시락";
case DutyFreeItemType.MileageCard:
return "마일";
case DutyFreeItemType.Supplement:
return "영양";
default:
return "";
}
}
private void ShowLoading(string title, string body) {
EnsureOverlay();
ClearOverlay();
SetOverlayVisible(true);
SetDefaultOverlayBackground();
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() {
EnsureEventSystem();
EnsureFlowCamera();
if(overlayCanvas != null)
{
return;
}
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.worldCamera = null;
overlayCanvas.sortingOrder = 100;
overlayCanvas.targetDisplay = 0;
SetUiLayer(canvasObject);
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);
SetUiLayer(rootObject);
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>();
overlayBackground = background;
background.color = new Color(0.02f, 0.035f, 0.045f, 0.92f);
background.raycastTarget = true;
runtimeFont = CreateKoreanFontAsset();
}
private void SetDefaultOverlayBackground() {
if(overlayBackground != null)
{
overlayBackground.sprite = null;
overlayBackground.preserveAspect = false;
overlayBackground.color = new Color(0.02f, 0.035f, 0.045f, 0.92f);
}
if(flowCamera != null)
{
flowCamera.backgroundColor = new Color(0.02f, 0.035f, 0.045f, 1f);
}
}
private void BuildIntroScreen() {
if(overlayBackground != null)
{
overlayBackground.sprite = null;
overlayBackground.preserveAspect = false;
overlayBackground.color = Color.clear;
}
if(flowCamera != null)
{
flowCamera.backgroundColor = new Color(0.006f, 0.022f, 0.035f, 1f);
}
RectTransform mapRect = AddIntroWorldMap();
Vector2[] routePoints = GetIntroRoutePoints();
AddIntroRoute(mapRect, routePoints);
AddIntroPlane(mapRect, routePoints);
AddIntroPassportAndPhone();
AddIntroTitlePanel();
AddIntroButtonDock();
}
private void BuildOpeningStoryScreen() {
if(overlayBackground != null)
{
overlayBackground.sprite = null;
overlayBackground.preserveAspect = false;
overlayBackground.color = new Color(0.012f, 0.018f, 0.024f, 0.96f);
}
if(flowCamera != null)
{
flowCamera.backgroundColor = new Color(0.012f, 0.018f, 0.024f, 1f);
}
openingStoryPageIndex = Mathf.Clamp(openingStoryPageIndex, 0, OpeningStoryPageAssetPaths.Length - 1);
AddOpeningStoryPage();
AddOpeningStoryProgressDots();
AddOpeningStoryControls();
}
private void AddOpeningStoryPage() {
GameObject shadowObject = CreateUiObject("Opening Story Shadow", overlayRoot, typeof(Image));
RectTransform shadowRect = shadowObject.GetComponent<RectTransform>();
shadowRect.anchorMin = new Vector2(0.5f, 0.5f);
shadowRect.anchorMax = new Vector2(0.5f, 0.5f);
shadowRect.pivot = new Vector2(0.5f, 0.5f);
shadowRect.anchoredPosition = new Vector2(7f, 18f);
shadowRect.sizeDelta = new Vector2(1132f, 637f);
Image shadowImage = shadowObject.GetComponent<Image>();
shadowImage.color = new Color(0f, 0f, 0f, 0.34f);
shadowImage.raycastTarget = false;
GameObject pageObject = CreateUiObject("Opening Story Page", overlayRoot, typeof(Image), typeof(Button));
RectTransform pageRect = pageObject.GetComponent<RectTransform>();
pageRect.anchorMin = new Vector2(0.5f, 0.5f);
pageRect.anchorMax = new Vector2(0.5f, 0.5f);
pageRect.pivot = new Vector2(0.5f, 0.5f);
pageRect.anchoredPosition = new Vector2(0f, 24f);
pageRect.sizeDelta = new Vector2(1120f, 630f);
Image pageImage = pageObject.GetComponent<Image>();
pageImage.sprite = LoadUiTextureSprite(OpeningStoryPageAssetPaths[openingStoryPageIndex]);
pageImage.color = pageImage.sprite != null
? Color.white
: new Color(0.91f, 0.87f, 0.78f, 1f);
pageImage.preserveAspect = true;
Button pageButton = pageObject.GetComponent<Button>();
pageButton.transition = Selectable.Transition.None;
pageButton.onClick.AddListener(AdvanceOpeningStory);
}
private void AddOpeningStoryProgressDots() {
float startX = -((OpeningStoryPageAssetPaths.Length - 1) * 18f) * 0.5f;
for(int i = 0; i < OpeningStoryPageAssetPaths.Length; i++)
{
GameObject dotObject = CreateUiObject("Opening Story Dot " + (i + 1), overlayRoot, typeof(Image));
RectTransform dotRect = dotObject.GetComponent<RectTransform>();
dotRect.anchorMin = new Vector2(0.5f, 0.5f);
dotRect.anchorMax = new Vector2(0.5f, 0.5f);
dotRect.pivot = new Vector2(0.5f, 0.5f);
dotRect.anchoredPosition = new Vector2(startX + (i * 18f), -324f);
dotRect.sizeDelta = i == openingStoryPageIndex ? new Vector2(12f, 12f) : new Vector2(8f, 8f);
Image dotImage = dotObject.GetComponent<Image>();
dotImage.color = i == openingStoryPageIndex
? new Color(1f, 0.86f, 0.28f, 0.95f)
: new Color(0.72f, 0.84f, 0.88f, 0.5f);
dotImage.raycastTarget = false;
}
}
private void AddOpeningStoryControls() {
if(openingStoryPageIndex > 0)
{
AddOpeningStoryButton("이전", new Vector2(-450f, -324f), PreviousOpeningStoryPage);
}
string nextLabel = openingStoryPageIndex >= OpeningStoryPageAssetPaths.Length - 1 ? "게임 시작" : "다음";
AddOpeningStoryButton(nextLabel, new Vector2(450f, -324f), AdvanceOpeningStory);
}
private void AddOpeningStoryButton(string label, Vector2 position, UnityEngine.Events.UnityAction action) {
GameObject buttonObject = CreateUiObject("Opening Story Button " + label, overlayRoot, typeof(Image), typeof(Button));
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 = position;
buttonRect.sizeDelta = label == "게임 시작" ? new Vector2(178f, 44f) : new Vector2(126f, 42f);
Image buttonImage = buttonObject.GetComponent<Image>();
buttonImage.color = label == "게임 시작"
? new Color(1f, 0.86f, 0.34f, 0.96f)
: new Color(0.88f, 0.96f, 1f, 0.88f);
Button button = buttonObject.GetComponent<Button>();
button.onClick.AddListener(action);
TextMeshProUGUI buttonText = AddTextTo(buttonObject.transform, "Label", label, label == "게임 시작" ? 17f : 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.035f, 0.065f, 0.08f, 1f);
}
private void AdvanceOpeningStory() {
if(openingStoryPageIndex >= OpeningStoryPageAssetPaths.Length - 1)
{
LoadGameSceneForCurrentMap();
return;
}
openingStoryPageIndex++;
ShowOpeningStory();
}
private void PreviousOpeningStoryPage() {
if(openingStoryPageIndex <= 0)
{
return;
}
openingStoryPageIndex--;
ShowOpeningStory();
}
private RectTransform AddIntroWorldMap() {
GameObject mapObject = CreateUiObject("Intro World Map", overlayRoot, typeof(Image));
RectTransform mapRect = mapObject.GetComponent<RectTransform>();
mapRect.anchorMin = new Vector2(0.5f, 0.5f);
mapRect.anchorMax = new Vector2(0.5f, 0.5f);
mapRect.pivot = new Vector2(0.5f, 0.5f);
mapRect.anchoredPosition = Vector2.zero;
mapRect.sizeDelta = new Vector2(1280f, 720f);
Image mapImage = mapObject.GetComponent<Image>();
mapImage.sprite = LoadUiSprite("", "Assets/ConceptArt/Intro/intro_world_map_clean_01.png");
mapImage.color = mapImage.sprite != null
? Color.white
: new Color(0.015f, 0.078f, 0.112f, 0.76f);
mapImage.preserveAspect = true;
mapImage.raycastTarget = false;
GameObject vignetteObject = CreateUiObject("Intro Map Vignette", overlayRoot, typeof(Image));
RectTransform vignetteRect = vignetteObject.GetComponent<RectTransform>();
vignetteRect.anchorMin = Vector2.zero;
vignetteRect.anchorMax = Vector2.one;
vignetteRect.offsetMin = Vector2.zero;
vignetteRect.offsetMax = Vector2.zero;
Image vignetteImage = vignetteObject.GetComponent<Image>();
vignetteImage.color = new Color(0f, 0.006f, 0.012f, 0.08f);
vignetteImage.raycastTarget = false;
return mapRect;
}
private void AddIntroMapGrid(RectTransform mapRect) {
Color gridColor = new Color(0.46f, 0.86f, 1f, 0.12f);
for(int i = -5; i <= 5; i++)
{
GameObject lineObject = CreateUiObject("Intro Map Longitude " + i, mapRect, typeof(Image));
RectTransform lineRect = lineObject.GetComponent<RectTransform>();
lineRect.anchorMin = new Vector2(0.5f, 0.5f);
lineRect.anchorMax = new Vector2(0.5f, 0.5f);
lineRect.pivot = new Vector2(0.5f, 0.5f);
lineRect.anchoredPosition = new Vector2(i * 92f, 0f);
lineRect.sizeDelta = new Vector2(2f, 610f);
Image lineImage = lineObject.GetComponent<Image>();
lineImage.color = gridColor;
lineImage.raycastTarget = false;
}
for(int i = -3; i <= 3; i++)
{
GameObject lineObject = CreateUiObject("Intro Map Latitude " + i, mapRect, typeof(Image));
RectTransform lineRect = lineObject.GetComponent<RectTransform>();
lineRect.anchorMin = new Vector2(0.5f, 0.5f);
lineRect.anchorMax = new Vector2(0.5f, 0.5f);
lineRect.pivot = new Vector2(0.5f, 0.5f);
lineRect.anchoredPosition = new Vector2(0f, i * 74f);
lineRect.sizeDelta = new Vector2(1110f, 2f);
Image lineImage = lineObject.GetComponent<Image>();
lineImage.color = gridColor;
lineImage.raycastTarget = false;
}
}
private void AddIntroLandMass(RectTransform mapRect, string objectName, Vector2 position, Vector2 size, float rotation) {
GameObject landObject = CreateUiObject(objectName, mapRect, typeof(Image));
RectTransform landRect = landObject.GetComponent<RectTransform>();
landRect.anchorMin = new Vector2(0.5f, 0.5f);
landRect.anchorMax = new Vector2(0.5f, 0.5f);
landRect.pivot = new Vector2(0.5f, 0.5f);
landRect.anchoredPosition = position;
landRect.sizeDelta = size;
landRect.localRotation = Quaternion.Euler(0f, 0f, rotation);
Image landImage = landObject.GetComponent<Image>();
landImage.color = new Color(0.24f, 0.72f, 0.59f, 0.22f);
landImage.raycastTarget = false;
}
private Vector2[] GetIntroRoutePoints() {
return new Vector2[] {
new Vector2(356f, 50f),
new Vector2(405f, 58f),
new Vector2(316f, 42f),
new Vector2(-450f, 94f)
};
}
private int GetIntroRouteIndex(MapId mapId) {
switch(mapId)
{
case MapId.Japan:
return 1;
case MapId.China:
return 2;
case MapId.America:
return 3;
case MapId.TutorialIncheon:
default:
return 0;
}
}
private Vector2 GetIntroRoutePoint(MapId mapId) {
Vector2[] routePoints = GetIntroRoutePoints();
return routePoints[Mathf.Clamp(GetIntroRouteIndex(mapId), 0, routePoints.Length - 1)];
}
private string GetIntroRouteLabel(MapId mapId) {
switch(mapId)
{
case MapId.Japan:
return "JAPAN";
case MapId.China:
return "CHINA";
case MapId.America:
return "AMERICA";
case MapId.TutorialIncheon:
default:
return "INCHEON";
}
}
private Vector2 GetIntroRouteLabelOffset(MapId mapId) {
switch(mapId)
{
case MapId.Japan:
return new Vector2(28f, -20f);
case MapId.China:
return new Vector2(-18f, -24f);
case MapId.America:
return new Vector2(0f, -25f);
case MapId.TutorialIncheon:
default:
return new Vector2(-4f, -25f);
}
}
private void AddIntroRoute(RectTransform mapRect, Vector2[] routePoints) {
Color routeColor = new Color(1f, 0.86f, 0.28f, 0.82f);
for(int i = 0; i < routePoints.Length - 1; i++)
{
AddIntroRouteSegment(mapRect, routePoints[i], routePoints[i + 1], routeColor, 4f);
}
AddIntroRouteMarker(mapRect, routePoints[0], "INCHEON", true, 0f, new Vector2(-4f, -25f));
AddIntroRouteMarker(mapRect, routePoints[1], "JAPAN", false, 0.4f, new Vector2(28f, -20f));
AddIntroRouteMarker(mapRect, routePoints[2], "CHINA", false, 0.8f, new Vector2(-18f, -24f));
AddIntroRouteMarker(mapRect, routePoints[3], "AMERICA", false, 1.2f, new Vector2(0f, -25f));
}
private void AddIntroRouteSegment(RectTransform mapRect, Vector2 from, Vector2 to, Color color, float thickness) {
GameObject segmentObject = CreateUiObject("Intro Route Segment", mapRect, typeof(Image));
RectTransform segmentRect = segmentObject.GetComponent<RectTransform>();
Vector2 delta = to - from;
segmentRect.anchorMin = new Vector2(0.5f, 0.5f);
segmentRect.anchorMax = new Vector2(0.5f, 0.5f);
segmentRect.pivot = new Vector2(0.5f, 0.5f);
segmentRect.anchoredPosition = from + (delta * 0.5f);
segmentRect.sizeDelta = new Vector2(delta.magnitude, thickness);
segmentRect.localRotation = Quaternion.Euler(0f, 0f, Mathf.Atan2(delta.y, delta.x) * Mathf.Rad2Deg);
Image segmentImage = segmentObject.GetComponent<Image>();
segmentImage.color = color;
segmentImage.raycastTarget = false;
}
private void AddIntroRouteMarker(RectTransform mapRect, Vector2 position, string label, bool current, float phase, Vector2 labelOffset) {
GameObject markerObject = CreateUiObject("Intro Route Marker " + label, mapRect, typeof(Image));
RectTransform markerRect = markerObject.GetComponent<RectTransform>();
markerRect.anchorMin = new Vector2(0.5f, 0.5f);
markerRect.anchorMax = new Vector2(0.5f, 0.5f);
markerRect.pivot = new Vector2(0.5f, 0.5f);
markerRect.anchoredPosition = position;
markerRect.sizeDelta = current ? new Vector2(23f, 23f) : new Vector2(17f, 17f);
Image markerImage = markerObject.GetComponent<Image>();
markerImage.color = current
? new Color(1f, 0.92f, 0.22f, 0.95f)
: new Color(0.78f, 0.96f, 1f, 0.86f);
markerImage.raycastTarget = false;
FlowIntroFloatMotion markerMotion = markerObject.AddComponent<FlowIntroFloatMotion>();
markerMotion.scalePulse = current ? 0.18f : 0.09f;
markerMotion.frequency = new Vector2(0f, 0.72f);
markerMotion.phase = phase;
TextMeshProUGUI labelText = AddIntroMapLabel(mapRect, "Intro Marker Label " + label, label, position + labelOffset, new Vector2(84f, 20f), 9.5f);
labelText.color = current
? new Color(1f, 0.94f, 0.44f, 0.92f)
: new Color(0.78f, 0.94f, 1f, 0.72f);
}
private TextMeshProUGUI AddIntroMapLabel(RectTransform parent, string objectName, string text, Vector2 position, Vector2 size, float fontSize) {
GameObject textObject = CreateUiObject(objectName, parent);
RectTransform textRect = textObject.GetComponent<RectTransform>();
textRect.anchorMin = new Vector2(0.5f, 0.5f);
textRect.anchorMax = new Vector2(0.5f, 0.5f);
textRect.pivot = new Vector2(0.5f, 0.5f);
textRect.anchoredPosition = position;
textRect.sizeDelta = size;
TextMeshProUGUI textComponent = textObject.AddComponent<TextMeshProUGUI>();
textComponent.text = text;
textComponent.font = runtimeFont != null ? runtimeFont : textComponent.font;
textComponent.fontSize = fontSize;
textComponent.fontStyle = FontStyles.Bold;
textComponent.alignment = TextAlignmentOptions.Center;
textComponent.raycastTarget = false;
textComponent.textWrappingMode = TextWrappingModes.NoWrap;
return textComponent;
}
private void AddIntroPlane(RectTransform mapRect, Vector2[] routePoints) {
Sprite planeSprite = LoadUiSprite("", "Assets/Sprites/UIIcons/UI_AirplaneTravel.png");
GameObject planeObject = CreateUiObject("Intro Airplane", overlayRoot, typeof(Image));
RectTransform rectTransform = planeObject.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);
rectTransform.anchoredPosition = routePoints[0] + mapRect.anchoredPosition;
rectTransform.sizeDelta = new Vector2(58f, 58f);
Image image = planeObject.GetComponent<Image>();
image.sprite = planeSprite;
image.color = planeSprite != null
? new Color(1f, 1f, 1f, 0.95f)
: new Color(0.9f, 0.97f, 1f, 0.88f);
image.preserveAspect = true;
image.raycastTarget = false;
FlowIntroRouteMotion motion = planeObject.AddComponent<FlowIntroRouteMotion>();
motion.points = routePoints;
motion.offset = mapRect.anchoredPosition;
motion.duration = 7.5f;
}
private void AddIntroPassportAndPhone() {
AddIntroPassportCover();
AddIntroSmartPhone();
}
private void AddIntroPassportCover() {
GameObject passportObject = CreateUiObject("Intro Passport Cover", overlayRoot, typeof(Image));
RectTransform passportRect = passportObject.GetComponent<RectTransform>();
passportRect.anchorMin = new Vector2(0.5f, 0.5f);
passportRect.anchorMax = new Vector2(0.5f, 0.5f);
passportRect.pivot = new Vector2(0.5f, 0.5f);
passportRect.anchoredPosition = new Vector2(424f, -242f);
passportRect.sizeDelta = new Vector2(270f, 300f);
passportRect.localRotation = Quaternion.Euler(0f, 0f, -24f);
Image passportImage = passportObject.GetComponent<Image>();
passportImage.sprite = LoadUiTextureSprite("Assets/ConceptArt/Intro/UI/intro_passport_clean.png");
passportImage.color = passportImage.sprite != null
? new Color(1f, 1f, 1f, 0.98f)
: new Color(0.034f, 0.18f, 0.22f, 0.96f);
passportImage.preserveAspect = true;
passportImage.raycastTarget = false;
}
private void AddIntroSmartPhone() {
GameObject phoneObject = CreateUiObject("Intro Smart Phone", overlayRoot, typeof(Image));
RectTransform phoneRect = phoneObject.GetComponent<RectTransform>();
phoneRect.anchorMin = new Vector2(0.5f, 0.5f);
phoneRect.anchorMax = new Vector2(0.5f, 0.5f);
phoneRect.pivot = new Vector2(0.5f, 0.5f);
phoneRect.anchoredPosition = new Vector2(558f, -218f);
phoneRect.sizeDelta = new Vector2(132f, 252f);
phoneRect.localRotation = Quaternion.Euler(0f, 0f, 10f);
Image phoneImage = phoneObject.GetComponent<Image>();
phoneImage.sprite = LoadUiTextureSprite("Assets/ConceptArt/Intro/UI/intro_phone_clean.png");
phoneImage.color = phoneImage.sprite != null
? new Color(1f, 1f, 1f, 0.98f)
: new Color(0.015f, 0.018f, 0.022f, 0.98f);
phoneImage.preserveAspect = true;
phoneImage.raycastTarget = false;
FlowIntroFloatMotion phoneMotion = phoneObject.AddComponent<FlowIntroFloatMotion>();
phoneMotion.amplitude = new Vector2(0f, 5f);
phoneMotion.frequency = new Vector2(0f, 0.45f);
phoneMotion.phase = 0.6f;
}
private void AddIntroTitlePanel() {
GameObject titleObject = CreateUiObject("Intro Title Lockup", overlayRoot, typeof(Image));
RectTransform titleRect = titleObject.GetComponent<RectTransform>();
titleRect.anchorMin = new Vector2(0.5f, 0.5f);
titleRect.anchorMax = new Vector2(0.5f, 0.5f);
titleRect.pivot = new Vector2(0.5f, 0.5f);
titleRect.anchoredPosition = new Vector2(-364f, 196f);
titleRect.sizeDelta = new Vector2(448f, 166f);
Image titleImage = titleObject.GetComponent<Image>();
titleImage.sprite = LoadUiTextureSprite("Assets/ConceptArt/Intro/UI/intro_title_lockup.png");
titleImage.color = titleImage.sprite != null
? Color.white
: new Color(0.86f, 0.98f, 1f, 0.94f);
titleImage.preserveAspect = true;
titleImage.raycastTarget = false;
}
private void AddIntroButtonDock() {
GameObject dockObject = CreateUiObject("Intro Button Dock", overlayRoot);
RectTransform dockRect = dockObject.GetComponent<RectTransform>();
dockRect.anchorMin = new Vector2(0.5f, 0.5f);
dockRect.anchorMax = new Vector2(0.5f, 0.5f);
dockRect.pivot = new Vector2(0.5f, 0.5f);
dockRect.anchoredPosition = new Vector2(0f, -224f);
dockRect.sizeDelta = new Vector2(424f, 136f);
AddIntroButton(dockRect, "게임 시작", new Vector2(45f, 0f), StartNewJourney, true, 0);
AddIntroButton(dockRect, "이어하기", new Vector2(0f, -76f), ContinueJourney, false, 1);
AddIntroButton(dockRect, "설정", new Vector2(220f, -76f), ShowIntroSettingsPlaceholder, false, 2);
}
private void AddIntroButton(RectTransform parent, string label, Vector2 position, UnityEngine.Events.UnityAction action, bool primary, int buttonStyle) {
GameObject buttonObject = CreateUiObject("Button " + label, parent, typeof(Image), typeof(Button));
RectTransform buttonRect = buttonObject.GetComponent<RectTransform>();
buttonRect.anchorMin = new Vector2(0f, 1f);
buttonRect.anchorMax = new Vector2(0f, 1f);
buttonRect.pivot = new Vector2(0f, 1f);
buttonRect.anchoredPosition = position;
buttonRect.sizeDelta = primary ? new Vector2(334f, 74f) : new Vector2(204f, 58f);
Image buttonImage = buttonObject.GetComponent<Image>();
buttonImage.sprite = GetIntroButtonSprite(primary, buttonStyle);
buttonImage.color = buttonImage.sprite != null
? Color.white
: primary
? new Color(0.98f, 0.88f, 0.38f, 0.96f)
: new Color(0.88f, 0.96f, 1f, 0.9f);
buttonImage.preserveAspect = true;
buttonImage.raycastTarget = true;
Button button = buttonObject.GetComponent<Button>();
button.targetGraphic = buttonImage;
button.navigation = new Navigation {
mode = Navigation.Mode.None
};
GameObject stampObject = CreateUiObject("Departure Stamp", buttonObject.transform, typeof(Image));
RectTransform stampRect = stampObject.GetComponent<RectTransform>();
stampRect.anchorMin = new Vector2(1f, 0.5f);
stampRect.anchorMax = new Vector2(1f, 0.5f);
stampRect.pivot = new Vector2(0.5f, 0.5f);
stampRect.anchoredPosition = new Vector2(primary ? -47f : -28f, primary ? 1f : 0f);
stampRect.sizeDelta = primary ? new Vector2(58f, 58f) : new Vector2(44f, 44f);
stampRect.localRotation = Quaternion.Euler(0f, 0f, -12f);
Image stampImage = stampObject.GetComponent<Image>();
stampImage.sprite = LoadUiSprite(DepartureStampResourcePath, "Assets/Resources/UI_DepartureStamp_Red.png");
stampImage.color = stampImage.sprite != null
? new Color(1f, 1f, 1f, 0.96f)
: new Color(0.76f, 0.08f, 0.06f, 0.96f);
stampImage.preserveAspect = true;
stampImage.raycastTarget = false;
TextMeshProUGUI stampText = AddTextTo(stampObject.transform, "Stamp Label", "출국", primary ? 13f : 11f, FontStyles.Bold);
RectTransform stampTextRect = stampText.rectTransform;
stampTextRect.anchorMin = Vector2.zero;
stampTextRect.anchorMax = Vector2.one;
stampTextRect.offsetMin = Vector2.zero;
stampTextRect.offsetMax = Vector2.zero;
stampText.color = new Color(0.74f, 0.04f, 0.03f, 0.98f);
stampObject.SetActive(false);
button.onClick.AddListener(delegate {
button.interactable = false;
stampObject.SetActive(true);
FlowIntroStampMotion stampMotion = stampObject.GetComponent<FlowIntroStampMotion>();
if(stampMotion == null)
{
stampMotion = stampObject.AddComponent<FlowIntroStampMotion>();
}
stampMotion.Play();
StartCoroutine(InvokeIntroActionAfterStamp(action));
});
if(primary)
{
FlowIntroFloatMotion motion = buttonObject.AddComponent<FlowIntroFloatMotion>();
motion.scalePulse = 0.018f;
motion.frequency = new Vector2(0f, 0.55f);
}
}
private void AddIntroButtonIcon(Transform parent, int buttonStyle) {
GameObject iconObject = CreateUiObject("Button Icon", parent, typeof(Image));
RectTransform iconRect = iconObject.GetComponent<RectTransform>();
iconRect.anchorMin = new Vector2(0.5f, 0.5f);
iconRect.anchorMax = new Vector2(0.5f, 0.5f);
iconRect.pivot = new Vector2(0.5f, 0.5f);
iconRect.anchoredPosition = new Vector2(-38f, 0f);
iconRect.sizeDelta = new Vector2(30f, 30f);
Image iconImage = iconObject.GetComponent<Image>();
iconImage.sprite = GetIntroButtonIconSprite(buttonStyle);
iconImage.color = iconImage.sprite != null
? new Color(1f, 1f, 1f, 0.96f)
: new Color(0.94f, 0.98f, 1f, 0.92f);
iconImage.preserveAspect = true;
iconImage.raycastTarget = false;
}
private Sprite GetIntroButtonSprite(bool primary, int buttonStyle) {
if(primary)
{
return LoadUiTextureSprite("Assets/ConceptArt/Intro/UI/intro_button_start.png");
}
if(buttonStyle == 2)
{
return LoadUiTextureSprite("Assets/ConceptArt/Intro/UI/intro_button_settings.png");
}
return LoadUiTextureSprite("Assets/ConceptArt/Intro/UI/intro_button_continue.png");
}
private Sprite GetIntroButtonIconSprite(int buttonStyle) {
if(buttonStyle == 2)
{
return LoadUiTextureSprite("Assets/ConceptArt/Intro/Cutouts/intro_icon_settings.png");
}
return LoadUiTextureSprite("Assets/ConceptArt/Intro/Cutouts/intro_icon_continue.png");
}
private void ShowIntroSettingsPlaceholder() {
ShowIntro();
}
private IEnumerator InvokeIntroActionAfterStamp(UnityEngine.Events.UnityAction action) {
yield return new WaitForSecondsRealtime(0.32f);
if(action != null)
{
action.Invoke();
}
}
private TextMeshProUGUI AddTextToPanel(RectTransform parent, string objectName, string text, Vector2 position, Vector2 size, float fontSize, FontStyles fontStyle, TextAlignmentOptions alignment) {
GameObject textObject = CreateUiObject(objectName, parent);
RectTransform textRect = textObject.GetComponent<RectTransform>();
textRect.anchorMin = new Vector2(0f, 1f);
textRect.anchorMax = new Vector2(0f, 1f);
textRect.pivot = new Vector2(0f, 1f);
textRect.anchoredPosition = position;
textRect.sizeDelta = size;
TextMeshProUGUI textComponent = textObject.AddComponent<TextMeshProUGUI>();
textComponent.text = text;
textComponent.font = runtimeFont != null ? runtimeFont : textComponent.font;
textComponent.fontSize = fontSize;
textComponent.fontStyle = fontStyle;
textComponent.alignment = alignment;
textComponent.color = new Color(0.95f, 0.98f, 1f, 0.96f);
textComponent.raycastTarget = false;
textComponent.textWrappingMode = TextWrappingModes.Normal;
return textComponent;
}
private GameObject CreateUiObject(string objectName, Transform parent, params System.Type[] components) {
System.Type[] finalComponents;
if(components == null || components.Length == 0)
{
finalComponents = new System.Type[] {
typeof(RectTransform),
typeof(CanvasRenderer)
};
}
else
{
finalComponents = new System.Type[components.Length + 2];
finalComponents[0] = typeof(RectTransform);
finalComponents[1] = typeof(CanvasRenderer);
for(int i = 0; i < components.Length; i++)
{
finalComponents[i + 2] = components[i];
}
}
GameObject gameObject = new GameObject(objectName, finalComponents);
gameObject.transform.SetParent(parent, false);
SetUiLayer(gameObject);
return gameObject;
}
private void SetUiLayer(GameObject gameObject) {
if(gameObject != null)
{
gameObject.layer = GetUiLayer();
}
}
private int GetUiLayer() {
int uiLayer = LayerMask.NameToLayer("UI");
return uiLayer >= 0 ? uiLayer : 0;
}
private Sprite LoadUiSprite(string resourcePath, string assetPath) {
if(!string.IsNullOrEmpty(resourcePath))
{
Sprite resourceSprite = Resources.Load<Sprite>(resourcePath);
if(resourceSprite != null)
{
return resourceSprite;
}
}
#if UNITY_EDITOR
if(!string.IsNullOrEmpty(assetPath))
{
Object[] assets = AssetDatabase.LoadAllAssetRepresentationsAtPath(assetPath);
if(assets != null)
{
for(int i = 0; i < assets.Length; i++)
{
Sprite sprite = assets[i] as Sprite;
if(sprite != null)
{
return sprite;
}
}
}
Sprite directSprite = AssetDatabase.LoadAssetAtPath<Sprite>(assetPath);
if(directSprite != null)
{
return directSprite;
}
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
if(texture == null)
{
AssetDatabase.Refresh();
texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
}
if(texture != null)
{
return Sprite.Create(texture, new Rect(0f, 0f, texture.width, texture.height), new Vector2(0.5f, 0.5f), 100f);
}
}
#endif
return null;
}
private Sprite LoadUiTextureSprite(string assetPath) {
#if UNITY_EDITOR
if(string.IsNullOrEmpty(assetPath))
{
return null;
}
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
if(texture == null)
{
AssetDatabase.Refresh();
texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
}
if(texture == null)
{
return null;
}
return Sprite.Create(texture, new Rect(0f, 0f, texture.width, texture.height), new Vector2(0.5f, 0.5f), 100f);
#else
return null;
#endif
}
private Sprite LoadUiSpriteRegion(string assetPath, Rect pixelRect) {
#if UNITY_EDITOR
if(string.IsNullOrEmpty(assetPath))
{
return null;
}
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
if(texture == null)
{
AssetDatabase.Refresh();
texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
}
if(texture == null)
{
return null;
}
Rect safeRect = ClampSpriteRect(pixelRect, texture.width, texture.height);
if(safeRect.width <= 0f || safeRect.height <= 0f)
{
return null;
}
return Sprite.Create(texture, safeRect, new Vector2(0.5f, 0.5f), 100f);
#else
return null;
#endif
}
private Rect ClampSpriteRect(Rect pixelRect, int textureWidth, int textureHeight) {
float xMin = Mathf.Clamp(pixelRect.xMin, 0f, textureWidth);
float yMin = Mathf.Clamp(pixelRect.yMin, 0f, textureHeight);
float xMax = Mathf.Clamp(pixelRect.xMax, 0f, textureWidth);
float yMax = Mathf.Clamp(pixelRect.yMax, 0f, textureHeight);
if(xMax < xMin)
{
xMax = xMin;
}
if(yMax < yMin)
{
yMax = yMin;
}
return Rect.MinMaxRect(xMin, yMin, xMax, yMax);
}
private void EnsureEventSystem() {
if(EventSystem.current != null)
{
EventSystem.current.enabled = true;
EventSystem.current.gameObject.SetActive(true);
StandaloneInputModule inputModule = EventSystem.current.GetComponent<StandaloneInputModule>();
if(inputModule == null)
{
inputModule = EventSystem.current.gameObject.AddComponent<StandaloneInputModule>();
}
inputModule.enabled = true;
DontDestroyOnLoad(EventSystem.current.gameObject);
return;
}
GameObject eventSystemObject = new GameObject("EventSystem", typeof(EventSystem), typeof(StandaloneInputModule));
DontDestroyOnLoad(eventSystemObject);
}
private void EnsureFlowCamera() {
if(flowCamera != null)
{
flowCamera.enabled = true;
flowCamera.targetDisplay = 0;
return;
}
GameObject cameraObject = new GameObject("Flow UI Camera", typeof(Camera));
DontDestroyOnLoad(cameraObject);
flowCamera = cameraObject.GetComponent<Camera>();
flowCamera.transform.position = new Vector3(640f, 360f, -10f);
flowCamera.clearFlags = CameraClearFlags.SolidColor;
flowCamera.backgroundColor = new Color(0.006f, 0.022f, 0.035f, 1f);
flowCamera.cullingMask = 1 << GetUiLayer();
flowCamera.depth = 100f;
flowCamera.orthographic = true;
flowCamera.orthographicSize = 360f;
flowCamera.targetDisplay = 0;
}
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(visible)
{
EnsureFlowCamera();
}
if(overlayCanvas != null)
{
overlayCanvas.gameObject.SetActive(visible);
}
if(flowCamera != null)
{
flowCamera.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);
SetUiLayer(buttonObject);
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);
SetUiLayer(textObject);
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);
SetUiLayer(textObject);
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;
}
}