Add travel map flow and update docs

This commit is contained in:
jongjae0305
2026-06-24 15:34:09 +09:00
parent 8561fab0a2
commit af26dd4311
37 changed files with 3114 additions and 165 deletions
+5 -4
View File
@@ -620,7 +620,7 @@ Transform:
m_GameObject: {fileID: 1928374650918273}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: -0.99, y: 3.8399992, z: 0}
m_LocalPosition: {x: -1.35, y: 2.68, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -787,6 +787,7 @@ MonoBehaviour:
- 0.55
- 1.65
minSpacing: 1.5
layout: 0
--- !u!1 &1987654321098766
GameObject:
m_ObjectHideFlags: 0
@@ -813,8 +814,8 @@ Transform:
m_GameObject: {fileID: 1987654321098766}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0.26, y: 4.66, z: 0}
m_LocalScale: {x: 4.745, y: 1, z: 1}
m_LocalPosition: {x: -0.09999995, y: 3.5000005, z: 0}
m_LocalScale: {x: 3, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4987654321098765}
@@ -905,7 +906,7 @@ Transform:
m_GameObject: {fileID: 1987654321098767}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 1.48, y: 3.8399992, z: 0}
m_LocalPosition: {x: 1.1199999, y: 2.6800003, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
+68 -7
View File
@@ -1,6 +1,7 @@
using UnityEngine;
using UnityEngine;
// 왼쪽 끝으로 이동한 배경을 오른쪽 끝으로 재배치하는 스크립트
[ExecuteAlways]
public class BackgroundLoop : MonoBehaviour {
public Sprite[] backgroundSprites; // 순서대로 교체할 공항 배경 스프라이트
public int startSpriteIndex = 0; // 이 배경 오브젝트가 시작할 스프라이트 번호
@@ -9,19 +10,26 @@ public class BackgroundLoop : MonoBehaviour {
private float width; // 배경의 가로 길이
private int currentSpriteIndex; // 현재 표시 중인 스프라이트 번호
private SpriteRenderer spriteRenderer; // 배경을 표시하는 스프라이트 렌더러
private BoxCollider2D backgroundCollider; // 루프 폭 계산용 콜라이더
private void Awake() {
// 가로 길이를 측정하는 처리
InitializeBackground();
}
BoxCollider2D backgroundCollider = GetComponent<BoxCollider2D>();
width = backgroundCollider.size.x;
spriteRenderer = GetComponent<SpriteRenderer>();
private void OnEnable() {
InitializeBackground();
}
currentSpriteIndex = NormalizeSpriteIndex(startSpriteIndex);
ApplyBackgroundSprite(currentSpriteIndex);
private void OnValidate() {
InitializeBackground();
}
private void Update() {
if(!Application.isPlaying)
{
return;
}
// 현재 위치가 원점에서 왼쪽으로 width 이상 이동했을때 위치를 리셋
if(transform.position.x <= -width)
{
@@ -29,6 +37,55 @@ public class BackgroundLoop : MonoBehaviour {
}
}
private void InitializeBackground() {
CacheComponents();
currentSpriteIndex = NormalizeSpriteIndex(startSpriteIndex);
ApplyBackgroundSprite(currentSpriteIndex);
SyncColliderSize();
RefreshWidth();
}
private void CacheComponents() {
if(spriteRenderer == null)
{
spriteRenderer = GetComponent<SpriteRenderer>();
}
if(backgroundCollider == null)
{
backgroundCollider = GetComponent<BoxCollider2D>();
}
}
private void RefreshWidth() {
if(spriteRenderer != null && spriteRenderer.sprite != null)
{
width = spriteRenderer.bounds.size.x;
return;
}
if(backgroundCollider != null)
{
width = backgroundCollider.size.x * Mathf.Abs(transform.lossyScale.x);
return;
}
width = 0f;
}
private void SyncColliderSize() {
if(spriteRenderer == null || spriteRenderer.sprite == null || backgroundCollider == null)
{
return;
}
Vector2 spriteSize = spriteRenderer.sprite.bounds.size;
backgroundCollider.size = spriteSize;
backgroundCollider.offset = Vector2.zero;
backgroundCollider.isTrigger = true;
}
// 위치를 리셋하는 메서드
private void Reposition() {
Vector2 offset = new Vector2(width * 2f, 0);
@@ -36,6 +93,8 @@ public class BackgroundLoop : MonoBehaviour {
currentSpriteIndex = NormalizeSpriteIndex(currentSpriteIndex + spriteAdvanceOnLoop);
ApplyBackgroundSprite(currentSpriteIndex);
SyncColliderSize();
RefreshWidth();
}
private int NormalizeSpriteIndex(int index) {
@@ -64,7 +123,9 @@ public class BackgroundLoop : MonoBehaviour {
if(backgroundSprite != null)
{
spriteRenderer.enabled = true;
spriteRenderer.sprite = backgroundSprite;
spriteRenderer.color = Color.white;
}
}
}
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7b0f46aa63d14f40a4976dc22e6710a8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+635
View File
@@ -0,0 +1,635 @@
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;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8a22a48e05fb4c1aa0b7e2a4d6723a0f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+28
View File
@@ -0,0 +1,28 @@
using System;
[Serializable]
public class GameSaveData {
public int currentMapIndex;
public int unlockedMapIndex;
public int totalMileage;
public int bagSlots;
public int shieldStock;
public int lunchboxStock;
public int speedShoesStock;
public int mileageCardStock;
public int bestScore;
public static GameSaveData CreateDefault() {
return new GameSaveData {
currentMapIndex = 0,
unlockedMapIndex = 0,
totalMileage = 0,
bagSlots = 1,
shieldStock = 0,
lunchboxStock = 0,
speedShoesStock = 0,
mileageCardStock = 0,
bestScore = 0
};
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9cd5d89d88554d7ea48124a4f1780e0b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+75
View File
@@ -0,0 +1,75 @@
using UnityEngine;
public static class GameSaveManager {
private const string SaveKey = "UniRun.SaveData.v1";
private const string LegacyTotalMileageKey = "UniRun.TotalMileage";
public static GameSaveData Load() {
if(PlayerPrefs.HasKey(SaveKey))
{
string json = PlayerPrefs.GetString(SaveKey);
GameSaveData loadedData = JsonUtility.FromJson<GameSaveData>(json);
if(loadedData != null)
{
Normalize(loadedData);
return loadedData;
}
}
GameSaveData saveData = GameSaveData.CreateDefault();
saveData.totalMileage = PlayerPrefs.GetInt(LegacyTotalMileageKey, 0);
Normalize(saveData);
Save(saveData);
return saveData;
}
public static void Save(GameSaveData saveData) {
Normalize(saveData);
string json = JsonUtility.ToJson(saveData);
PlayerPrefs.SetString(SaveKey, json);
PlayerPrefs.SetInt(LegacyTotalMileageKey, saveData.totalMileage);
PlayerPrefs.Save();
}
public static GameSaveData ResetSave() {
PlayerPrefs.DeleteKey(SaveKey);
GameSaveData saveData = GameSaveData.CreateDefault();
Save(saveData);
return saveData;
}
public static void AddMileage(GameSaveData saveData, int amount) {
if(saveData == null)
{
return;
}
saveData.totalMileage = Mathf.Max(0, saveData.totalMileage + amount);
Save(saveData);
}
public static void UnlockMap(GameSaveData saveData, MapId mapId) {
if(saveData == null)
{
return;
}
int mapIndex = MapDatabase.GetIndex(mapId);
saveData.unlockedMapIndex = Mathf.Max(saveData.unlockedMapIndex, mapIndex);
Save(saveData);
}
private static void Normalize(GameSaveData saveData) {
saveData.currentMapIndex = Mathf.Clamp(saveData.currentMapIndex, 0, MapDatabase.LastMapIndex);
saveData.unlockedMapIndex = Mathf.Clamp(saveData.unlockedMapIndex, 0, MapDatabase.LastMapIndex);
saveData.currentMapIndex = Mathf.Min(saveData.currentMapIndex, saveData.unlockedMapIndex);
saveData.totalMileage = Mathf.Max(0, saveData.totalMileage);
saveData.bagSlots = Mathf.Clamp(saveData.bagSlots, 1, 3);
saveData.shieldStock = Mathf.Max(0, saveData.shieldStock);
saveData.lunchboxStock = Mathf.Max(0, saveData.lunchboxStock);
saveData.speedShoesStock = Mathf.Max(0, saveData.speedShoesStock);
saveData.mileageCardStock = Mathf.Max(0, saveData.mileageCardStock);
saveData.bestScore = Mathf.Max(0, saveData.bestScore);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b2f7f2cb496e477589a57fd237868fd3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+648 -49
View File
@@ -2,10 +2,30 @@ using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro;
using UnityEngine.UI;
#if UNITY_EDITOR
using UnityEditor;
#endif
// 게임 오버 상태를 표현하고, 게임 점수와 UI를 관리하는 게임 매니저
// 씬에는 단 하나의 게임 매니저만 존재할 수 있다.
public class GameManager : MonoBehaviour {
public enum TutorialGuideType {
None,
Jump,
DoubleJump,
Slide,
Damage,
Items
}
public enum TutorialItemType {
HealthSushi,
InvincibleRamen,
Shield,
SpeedShoes,
MileageCard
}
public static GameManager instance; // 싱글톤을 할당할 전역 변수
public bool isGameover = false; // 게임 오버 상태
@@ -16,14 +36,34 @@ public class GameManager : MonoBehaviour {
private int currentLife; // 현재 남은 목숨
private int stageMileage = 0; // 현재 플레이 중 획득한 마일리지
private int totalMileage = 0; // 누적 보유 마일리지
private TutorialGuideType tutorialGuideType = TutorialGuideType.None; // 현재 튜토리얼 안내 이미지 종류
private string tutorialTitle = ""; // 튜토리얼 안내 제목
private string tutorialMessage = ""; // 튜토리얼 안내 문구
private Image[] lifeIcons; // 왼쪽 HUD의 목숨 도장 표시
private TextMeshProUGUI tutorialText; // 화면 하단 튜토리얼 안내
private GameObject tutorialGuideObject; // 화면 하단 이미지 튜토리얼 패널
private Image tutorialMainImage; // 점프/슬라이드 대표 이미지
private TextMeshProUGUI tutorialMultiplierText; // 2단 점프 x2 표시
private TextMeshProUGUI tutorialTitleText; // 튜토리얼 제목
private TextMeshProUGUI tutorialBodyText; // 튜토리얼 설명
private Image[] tutorialItemImages; // 아이템 순서 설명 이미지
private TMP_FontAsset tutorialFontAsset; // 한글 튜토리얼 문구용 런타임 폰트
private float tutorialGuideSequenceStartTime; // 튜토리얼 안내 시작 시각
private int currentTutorialGuideSequenceIndex = -1; // 현재 표시 중인 안내 단계
private bool tutorialGuideSequenceComplete = false; // 기본 튜토리얼 안내 완료 여부
private MapDefinition currentMapDefinition; // 현재 플레이 중인 맵 데이터
private float stageElapsedTime = 0f; // 현재 맵 진행 시간
private float stageDistance = 0f; // 현재 맵 진행 거리
private bool stageCompleted = false; // 결과 화면이 이미 처리되었는지 여부
public int maxLife = 3; // 시작 목숨
public int initialMileage = 500; // 첫 시작시 지급할 초기 마일리지
public bool grantInitialMileage = true; // 초기 마일리지 지급 여부
public bool saveMileageImmediately = true; // 스테이지 정산 전까지는 획득 즉시 누적한다.
public bool playTutorialGuideSequence = false; // 시작 시 입력/아이템 안내를 시간 기준으로 보여준다.
public float itemInvincibleDuration = 2.2f; // 라멘 아이템 무적 지속 시간
public float itemSpeedBoostAmount = 0.35f; // 운동화 아이템 즉시 속도 보정량
public float mileageBonusDuration = 7f; // 마일리지 카드 보너스 지속 시간
public int mileageBonusMultiplier = 2; // 마일리지 카드 획득 배율
private const string TotalMileageKey = "UniRun.TotalMileage";
private const string InitialMileageGrantedKey = "UniRun.InitialMileageGranted";
@@ -31,6 +71,10 @@ public class GameManager : MonoBehaviour {
private const string PassportPanelResourcePath = "UI_PassportPanel";
private const string LifeStampResourcePath = "UI_LifeStamp";
private const string PassportItemSlotResourcePath = "UI_PassportItemSlot";
private const float TutorialJumpGuideDuration = 3.5f;
private const float TutorialDoubleJumpGuideDuration = 3.5f;
private const float TutorialSlideGuideDuration = 3.5f;
private const float TutorialItemGuideDuration = 5.2f;
// 게임의 스피드를 올리는 변수
public float gameSpeed = 1f;
@@ -38,6 +82,9 @@ public class GameManager : MonoBehaviour {
public float speedIncreaseRate = 0.05f;
public float maxGameSpeed = 2f;
public float hitSpeedPenalty = 0.3f;
private int shieldCharges = 0; // 방어막 아이템으로 막을 수 있는 남은 피격 횟수
private float itemInvincibleEndTime = 0f; // 라멘 무적 종료 시각
private float mileageBonusEndTime = 0f; // 마일리지 카드 보너스 종료 시각
// 게임 시작과 동시에 싱글톤을 구성
void Awake() {
@@ -60,7 +107,9 @@ public class GameManager : MonoBehaviour {
private void Start() {
currentLife = maxLife;
totalMileage = PlayerPrefs.GetInt(TotalMileageKey, 0);
totalMileage = GameFlowManager.Instance != null
? GameFlowManager.Instance.SaveData.totalMileage
: PlayerPrefs.GetInt(TotalMileageKey, 0);
if(grantInitialMileage && PlayerPrefs.GetInt(InitialMileageGrantedKey, 0) == 0)
{
@@ -71,26 +120,90 @@ public class GameManager : MonoBehaviour {
}
BuildRuntimeHUD();
StartTutorialGuideSequence();
BeginMap(GameFlowManager.Instance != null
? GameFlowManager.Instance.CurrentMap
: MapDatabase.GetMap(MapId.TutorialIncheon));
SyncTotalMileageToFlow();
UpdateScoreUI();
}
void Update() {
// 게임 오버 상태에서 게임을 재시작할 수 있게 하는 처리
if(isGameover && Input.GetMouseButtonDown(0))
if(isGameover && GameFlowManager.Instance == null && Input.GetMouseButtonDown(0))
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
if(!GameFlowManager.IsGameplayActive)
{
UpdateScoreUI();
return;
}
if(!isGameover)
{
UpdateTutorialGuideSequence();
stageElapsedTime += Time.deltaTime;
// 게임 진행시 게임 속도는 시간에 따라 증가
gameSpeed += speedIncreaseRate * Time.deltaTime;
// 게임 최대 속도는 max를 넘어서지 못하게
gameSpeed = Mathf.Min(gameSpeed, maxGameSpeed);
stageDistance += 10f * gameSpeed * Time.deltaTime;
if(currentMapDefinition != null && currentMapDefinition.targetDistance > 0f && stageDistance >= currentMapDefinition.targetDistance)
{
CompleteStage(true);
return;
}
if(currentMapDefinition != null && currentMapDefinition.timeLimit > 0f && stageElapsedTime >= currentMapDefinition.timeLimit)
{
CompleteStage(false);
return;
}
UpdateScoreUI();
}
}
public void BeginMap(MapDefinition mapDefinition) {
currentMapDefinition = mapDefinition != null
? mapDefinition
: MapDatabase.GetMap(MapId.TutorialIncheon);
score = 0;
stageMileage = 0;
currentLife = maxLife;
gameSpeed = minGameSpeed;
shieldCharges = 0;
itemInvincibleEndTime = 0f;
mileageBonusEndTime = 0f;
stageElapsedTime = 0f;
stageDistance = 0f;
stageCompleted = false;
isGameover = false;
tutorialGuideSequenceComplete = false;
currentTutorialGuideSequenceIndex = -1;
SetTutorialGuide(TutorialGuideType.None, "", "");
if(gameoverUI != null)
{
gameoverUI.SetActive(false);
}
PlatformSpawner platformSpawner = FindFirstObjectByType<PlatformSpawner>();
if(platformSpawner != null)
{
platformSpawner.ApplyMap(currentMapDefinition);
}
UpdateScoreUI();
}
// 점수를 증가시키는 메서드
public void AddScore(int newScore) {
@@ -103,10 +216,95 @@ public class GameManager : MonoBehaviour {
}
public void SetTutorialMessage(string message) {
SetTutorialGuide(TutorialGuideType.None, "", message);
}
public void SetTutorialGuide(TutorialGuideType guideType, string title, string message) {
tutorialGuideType = guideType;
tutorialTitle = title;
tutorialMessage = message;
UpdateScoreUI();
}
private void StartTutorialGuideSequence() {
if(!playTutorialGuideSequence)
{
return;
}
tutorialGuideSequenceStartTime = Time.time;
tutorialGuideSequenceComplete = false;
currentTutorialGuideSequenceIndex = -1;
UpdateTutorialGuideSequence();
}
private void UpdateTutorialGuideSequence() {
if(!playTutorialGuideSequence || tutorialGuideSequenceComplete)
{
return;
}
float elapsedTime = Time.time - tutorialGuideSequenceStartTime;
int guideIndex = GetTutorialGuideSequenceIndex(elapsedTime);
if(guideIndex == currentTutorialGuideSequenceIndex)
{
return;
}
currentTutorialGuideSequenceIndex = guideIndex;
ApplyTutorialGuideSequenceStep(guideIndex);
}
private int GetTutorialGuideSequenceIndex(float elapsedTime) {
if(elapsedTime < TutorialJumpGuideDuration)
{
return 0;
}
elapsedTime -= TutorialJumpGuideDuration;
if(elapsedTime < TutorialDoubleJumpGuideDuration)
{
return 1;
}
elapsedTime -= TutorialDoubleJumpGuideDuration;
if(elapsedTime < TutorialSlideGuideDuration)
{
return 2;
}
elapsedTime -= TutorialSlideGuideDuration;
if(elapsedTime < TutorialItemGuideDuration)
{
return 3;
}
return 4;
}
private void ApplyTutorialGuideSequenceStep(int guideIndex) {
switch(guideIndex)
{
case 0:
SetTutorialGuide(TutorialGuideType.Jump, "점프", "왼쪽 클릭으로 가볍게 뛰어오릅니다.");
break;
case 1:
SetTutorialGuide(TutorialGuideType.DoubleJump, "2단 점프", "공중에서 왼쪽 클릭을 한 번 더 누릅니다.");
break;
case 2:
SetTutorialGuide(TutorialGuideType.Slide, "슬라이드", "오른쪽 클릭을 누르고 있으면 낮게 지나갑니다.");
break;
case 3:
SetTutorialGuide(TutorialGuideType.Items, "아이템", "초밥 -> 라멘 -> 방어막 -> 운동화 -> 마일리지 카드 순서입니다.");
break;
default:
tutorialGuideSequenceComplete = true;
SetTutorialGuide(TutorialGuideType.None, "", "");
break;
}
}
// 마일리지 토큰을 먹었을 때 호출되는 메서드
public void AddMileage(int amount) {
if(isGameover)
@@ -114,14 +312,19 @@ public class GameManager : MonoBehaviour {
return;
}
stageMileage += amount;
score += Mathf.Max(1, amount / 10);
int finalAmount = IsMileageBonusActive()
? amount * Mathf.Max(1, mileageBonusMultiplier)
: amount;
stageMileage += finalAmount;
score += Mathf.Max(1, finalAmount / 10);
if(saveMileageImmediately)
{
totalMileage += amount;
totalMileage += finalAmount;
PlayerPrefs.SetInt(TotalMileageKey, totalMileage);
PlayerPrefs.Save();
SyncTotalMileageToFlow();
}
UpdateScoreUI();
@@ -129,7 +332,7 @@ public class GameManager : MonoBehaviour {
// 플레이어가 장애물에 부딪혔을 때 목숨과 속도를 줄이는 메서드
public bool TakeDamage(int damage = 1) {
if(isGameover)
if(isGameover || !GameFlowManager.IsGameplayActive)
{
return false;
}
@@ -145,18 +348,126 @@ public class GameManager : MonoBehaviour {
return currentLife <= 0;
}
public void ApplyTutorialItem(TutorialItemType itemType) {
if(isGameover || !GameFlowManager.IsGameplayActive)
{
return;
}
switch(itemType)
{
case TutorialItemType.HealthSushi:
currentLife = Mathf.Min(maxLife, currentLife + 1);
AddScore(3);
SetTutorialGuide(TutorialGuideType.Items, "초밥", "방금 잃은 목숨을 1 회복합니다.");
break;
case TutorialItemType.InvincibleRamen:
itemInvincibleEndTime = Time.time + itemInvincibleDuration;
SetTutorialGuide(TutorialGuideType.Items, "라멘", "짧은 시간 동안 다음 충돌 피해를 받지 않습니다.");
break;
case TutorialItemType.Shield:
shieldCharges = Mathf.Max(shieldCharges, 1);
SetTutorialGuide(TutorialGuideType.Items, "방어막", "다음 충돌 1회를 방어막이 대신 막습니다.");
break;
case TutorialItemType.SpeedShoes:
gameSpeed = Mathf.Min(maxGameSpeed, gameSpeed + itemSpeedBoostAmount);
SetTutorialGuide(TutorialGuideType.Items, "운동화", "속도를 즉시 회복해 늦어진 일정을 따라잡습니다.");
break;
case TutorialItemType.MileageCard:
mileageBonusEndTime = Time.time + mileageBonusDuration;
SetTutorialGuide(TutorialGuideType.Items, "마일리지 카드", "잠시 동안 먹는 마일리지가 두 배로 들어옵니다.");
break;
}
UpdateScoreUI();
}
public bool TryBlockDamageWithItem() {
if(isGameover || !GameFlowManager.IsGameplayActive)
{
return false;
}
if(IsItemInvincibleActive())
{
SetTutorialGuide(TutorialGuideType.Items, "라멘", "무적 중이라 이번 충돌은 피해 없이 지나갑니다.");
return true;
}
if(shieldCharges > 0)
{
shieldCharges--;
SetTutorialGuide(TutorialGuideType.Items, "방어막", "방어막이 충돌 피해를 한 번 막았습니다.");
UpdateScoreUI();
return true;
}
return false;
}
private bool IsItemInvincibleActive() {
return Time.time < itemInvincibleEndTime;
}
private bool IsMileageBonusActive() {
return Time.time < mileageBonusEndTime;
}
// 플레이어 캐릭터가 사망시 게임 오버를 실행하는 메서드
public void OnPlayerDead() {
isGameover = true;
if(gameoverUI != null)
{
gameoverUI.SetActive(GameFlowManager.Instance == null);
}
CompleteStage(false);
}
private void CompleteStage(bool cleared) {
if(stageCompleted)
{
return;
}
stageCompleted = true;
isGameover = true;
if(GameFlowManager.Instance != null)
{
GameFlowManager.Instance.CompleteMap(cleared, score, stageMileage, stageElapsedTime, stageDistance);
}
else if(gameoverUI != null)
{
gameoverUI.SetActive(true);
}
}
private void SyncTotalMileageToFlow() {
if(GameFlowManager.Instance != null)
{
GameFlowManager.Instance.SetTotalMileage(totalMileage);
}
}
private void UpdateScoreUI() {
if(scoreText != null)
{
scoreText.text = "SCORE " + score
string mapName = currentMapDefinition != null ? currentMapDefinition.displayName : "RUN";
string distanceText = currentMapDefinition != null
? Mathf.FloorToInt(stageDistance) + "/" + Mathf.FloorToInt(currentMapDefinition.targetDistance) + "m"
: Mathf.FloorToInt(stageDistance) + "m";
string timeText = currentMapDefinition != null && currentMapDefinition.timeLimit > 0f
? Mathf.Max(0f, currentMapDefinition.timeLimit - stageElapsedTime).ToString("0") + "s"
: stageElapsedTime.ToString("0") + "s";
scoreText.text = mapName
+ "\nSCORE " + score
+ "\nMILE " + stageMileage
+ "\nTOTAL " + totalMileage
+ "\nDIST " + distanceText
+ "\nTIME " + timeText
+ "\nSPD " + gameSpeed.ToString("0.0");
}
@@ -171,16 +482,73 @@ public class GameManager : MonoBehaviour {
bool hasLife = i < currentLife;
lifeIcons[i].color = hasLife
? Color.white
? GetActiveLifeIconColor(lifeIcons[i])
: new Color(0.34f, 0.34f, 0.34f, 0.72f);
}
}
if(tutorialText != null)
{
tutorialText.text = tutorialMessage;
tutorialText.gameObject.SetActive(!string.IsNullOrEmpty(tutorialMessage));
RefreshTutorialGuide();
}
private void RefreshTutorialGuide() {
if(tutorialGuideObject == null)
{
return;
}
bool hasGuide = tutorialGuideType != TutorialGuideType.None
|| !string.IsNullOrEmpty(tutorialTitle)
|| !string.IsNullOrEmpty(tutorialMessage);
tutorialGuideObject.SetActive(hasGuide);
if(!hasGuide)
{
return;
}
if(tutorialTitleText != null)
{
tutorialTitleText.text = tutorialTitle;
}
if(tutorialBodyText != null)
{
tutorialBodyText.text = tutorialMessage;
}
bool showItemRow = tutorialGuideType == TutorialGuideType.Items;
Sprite mainSprite = GetTutorialMainSprite(tutorialGuideType);
if(tutorialMainImage != null)
{
tutorialMainImage.gameObject.SetActive(!showItemRow && mainSprite != null);
tutorialMainImage.sprite = mainSprite;
}
if(tutorialMultiplierText != null)
{
tutorialMultiplierText.gameObject.SetActive(tutorialGuideType == TutorialGuideType.DoubleJump);
}
if(tutorialItemImages != null)
{
for(int i = 0; i < tutorialItemImages.Length; i++)
{
if(tutorialItemImages[i] != null)
{
tutorialItemImages[i].gameObject.SetActive(showItemRow && tutorialItemImages[i].sprite != null);
}
}
}
}
private Color GetActiveLifeIconColor(Image lifeIcon) {
if(lifeIcon != null && lifeIcon.sprite == null)
{
return new Color(0.94f, 0.18f, 0.12f, 0.9f);
}
return Color.white;
}
private void BuildRuntimeHUD() {
@@ -193,7 +561,7 @@ public class GameManager : MonoBehaviour {
ConfigureRightInfoText();
CreateStatusHUD(canvasTransform);
CreateTutorialText(canvasTransform);
CreateTutorialGuide(canvasTransform);
}
private void ConfigureRightInfoText() {
@@ -202,11 +570,11 @@ public class GameManager : MonoBehaviour {
rectTransform.anchorMax = new Vector2(1f, 1f);
rectTransform.pivot = new Vector2(1f, 1f);
rectTransform.anchoredPosition = new Vector2(-12f, -9f);
rectTransform.sizeDelta = new Vector2(132f, 62f);
rectTransform.sizeDelta = new Vector2(176f, 96f);
scoreText.alignment = TextAlignmentOptions.TopRight;
scoreText.fontSize = 9.5f;
scoreText.lineSpacing = -10f;
scoreText.fontSize = 8.5f;
scoreText.lineSpacing = -13f;
scoreText.color = new Color(0.94f, 0.97f, 1f, 0.92f);
scoreText.raycastTarget = false;
scoreText.textWrappingMode = TextWrappingModes.NoWrap;
@@ -220,11 +588,11 @@ public class GameManager : MonoBehaviour {
hudRect.anchorMin = new Vector2(0f, 1f);
hudRect.anchorMax = new Vector2(0f, 1f);
hudRect.pivot = new Vector2(0f, 1f);
hudRect.anchoredPosition = new Vector2(10f, -8f);
hudRect.sizeDelta = new Vector2(176f, 54f);
hudRect.anchoredPosition = new Vector2(8f, -7f);
hudRect.sizeDelta = new Vector2(108f, 34f);
Image hudBackground = hudObject.GetComponent<Image>();
hudBackground.sprite = Resources.Load<Sprite>(PassportPanelResourcePath);
hudBackground.sprite = LoadHudSprite(PassportPanelResourcePath);
hudBackground.color = hudBackground.sprite != null
? Color.white
: new Color(0.04f, 0.08f, 0.11f, 0.68f);
@@ -243,11 +611,11 @@ public class GameManager : MonoBehaviour {
frameRect.anchorMin = new Vector2(0f, 1f);
frameRect.anchorMax = new Vector2(0f, 1f);
frameRect.pivot = new Vector2(0f, 1f);
frameRect.anchoredPosition = new Vector2(9f, -10f);
frameRect.sizeDelta = new Vector2(29f, 29f);
frameRect.anchoredPosition = new Vector2(6f, -6f);
frameRect.sizeDelta = new Vector2(20f, 20f);
Image frameImage = faceFrame.GetComponent<Image>();
frameImage.color = new Color(0.84f, 0.87f, 0.80f, 0.58f);
frameImage.color = new Color(0.12f, 0.16f, 0.17f, 0.36f);
frameImage.raycastTarget = false;
GameObject faceObject = new GameObject("Character Face", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
@@ -260,13 +628,16 @@ public class GameManager : MonoBehaviour {
faceRect.offsetMax = new Vector2(-2f, -2f);
Image faceImage = faceObject.GetComponent<Image>();
faceImage.sprite = Resources.Load<Sprite>(CharacterFaceResourcePath);
faceImage.sprite = LoadHudSprite(CharacterFaceResourcePath);
faceImage.preserveAspect = true;
faceImage.raycastTarget = false;
faceImage.color = faceImage.sprite != null
? Color.white
: new Color(1f, 1f, 1f, 0f);
}
private void CreateLifeIcons(Transform parent) {
Sprite lifeSprite = Resources.Load<Sprite>(LifeStampResourcePath);
Sprite lifeSprite = LoadHudSprite(LifeStampResourcePath);
int iconCount = Mathf.Max(maxLife, 1);
lifeIcons = new Image[iconCount];
@@ -279,12 +650,14 @@ public class GameManager : MonoBehaviour {
lifeRect.anchorMin = new Vector2(0f, 1f);
lifeRect.anchorMax = new Vector2(0f, 1f);
lifeRect.pivot = new Vector2(0f, 1f);
lifeRect.anchoredPosition = new Vector2(50f + (i * 21f), -10f);
lifeRect.sizeDelta = new Vector2(18f, 18f);
lifeRect.anchoredPosition = new Vector2(33f + (i * 13f), -7f);
lifeRect.sizeDelta = new Vector2(10f, 10f);
Image lifeImage = lifeObject.GetComponent<Image>();
lifeImage.sprite = lifeSprite;
lifeImage.color = Color.white;
lifeImage.color = lifeSprite != null
? Color.white
: new Color(0.94f, 0.18f, 0.12f, 0.9f);
lifeImage.preserveAspect = true;
lifeImage.raycastTarget = false;
lifeIcons[i] = lifeImage;
@@ -292,7 +665,7 @@ public class GameManager : MonoBehaviour {
}
private void CreateItemSlots(Transform parent) {
Sprite slotSprite = Resources.Load<Sprite>(PassportItemSlotResourcePath);
Sprite slotSprite = LoadHudSprite(PassportItemSlotResourcePath);
for(int i = 0; i < 3; i++)
{
@@ -303,8 +676,8 @@ public class GameManager : MonoBehaviour {
slotRect.anchorMin = new Vector2(0f, 1f);
slotRect.anchorMax = new Vector2(0f, 1f);
slotRect.pivot = new Vector2(0f, 1f);
slotRect.anchoredPosition = new Vector2(50f + (i * 21f), -31f);
slotRect.sizeDelta = new Vector2(18f, 18f);
slotRect.anchoredPosition = new Vector2(33f + (i * 13f), -20f);
slotRect.sizeDelta = new Vector2(10f, 10f);
Image slotImage = slotObject.GetComponent<Image>();
slotImage.sprite = slotSprite;
@@ -316,25 +689,251 @@ public class GameManager : MonoBehaviour {
}
}
private void CreateTutorialText(Transform canvasTransform) {
GameObject tutorialObject = new GameObject("Tutorial Text", typeof(RectTransform), typeof(CanvasRenderer));
tutorialObject.transform.SetParent(canvasTransform, false);
private Sprite LoadHudSprite(string resourcePath) {
Sprite sprite = Resources.Load<Sprite>(resourcePath);
if(sprite != null)
{
return sprite;
}
RectTransform tutorialRect = tutorialObject.GetComponent<RectTransform>();
tutorialRect.anchorMin = new Vector2(0.5f, 0f);
tutorialRect.anchorMax = new Vector2(0.5f, 0f);
tutorialRect.pivot = new Vector2(0.5f, 0f);
tutorialRect.anchoredPosition = new Vector2(0f, 18f);
tutorialRect.sizeDelta = new Vector2(360f, 24f);
Texture2D texture = Resources.Load<Texture2D>(resourcePath);
#if UNITY_EDITOR
if(texture == null)
{
sprite = AssetDatabase.LoadAssetAtPath<Sprite>("Assets/Resources/" + resourcePath + ".png");
if(sprite != null)
{
return sprite;
}
tutorialText = tutorialObject.AddComponent<TextMeshProUGUI>();
tutorialText.font = scoreText.font;
tutorialText.alignment = TextAlignmentOptions.Center;
tutorialText.fontSize = 9.5f;
tutorialText.fontStyle = FontStyles.Bold;
tutorialText.color = new Color(0.96f, 0.98f, 1f, 0.95f);
tutorialText.raycastTarget = false;
tutorialText.textWrappingMode = TextWrappingModes.NoWrap;
tutorialText.gameObject.SetActive(false);
texture = AssetDatabase.LoadAssetAtPath<Texture2D>("Assets/Resources/" + resourcePath + ".png");
}
#endif
if(texture == null)
{
return null;
}
return Sprite.Create(
texture,
new Rect(0f, 0f, texture.width, texture.height),
new Vector2(0.5f, 0.5f),
100f);
}
private void CreateTutorialGuide(Transform canvasTransform) {
tutorialGuideObject = new GameObject("Tutorial Guide", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
tutorialGuideObject.transform.SetParent(canvasTransform, false);
RectTransform guideRect = tutorialGuideObject.GetComponent<RectTransform>();
guideRect.anchorMin = new Vector2(0.5f, 0f);
guideRect.anchorMax = new Vector2(0.5f, 0f);
guideRect.pivot = new Vector2(0.5f, 0f);
guideRect.anchoredPosition = new Vector2(18f, 12f);
guideRect.sizeDelta = new Vector2(372f, 72f);
Image guideBackground = tutorialGuideObject.GetComponent<Image>();
guideBackground.color = new Color(0.035f, 0.055f, 0.07f, 0.76f);
guideBackground.raycastTarget = false;
tutorialFontAsset = CreateKoreanTutorialFontAsset();
CreateTutorialMainImage(tutorialGuideObject.transform);
CreateTutorialItemRow(tutorialGuideObject.transform);
tutorialTitleText = CreateTutorialText(tutorialGuideObject.transform, "Tutorial Title", new Vector2(70f, -9f), new Vector2(286f, 18f), 12f, FontStyles.Bold);
tutorialBodyText = CreateTutorialText(tutorialGuideObject.transform, "Tutorial Body", new Vector2(70f, -28f), new Vector2(286f, 34f), 9f, FontStyles.Normal);
tutorialGuideObject.SetActive(false);
}
private void CreateTutorialMainImage(Transform parent) {
GameObject imageObject = new GameObject("Tutorial Main Image", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
imageObject.transform.SetParent(parent, false);
RectTransform imageRect = imageObject.GetComponent<RectTransform>();
imageRect.anchorMin = new Vector2(0f, 0.5f);
imageRect.anchorMax = new Vector2(0f, 0.5f);
imageRect.pivot = new Vector2(0f, 0.5f);
imageRect.anchoredPosition = new Vector2(14f, 0f);
imageRect.sizeDelta = new Vector2(48f, 48f);
tutorialMainImage = imageObject.GetComponent<Image>();
tutorialMainImage.preserveAspect = true;
tutorialMainImage.raycastTarget = false;
GameObject multiplierObject = new GameObject("Double Jump Marker", typeof(RectTransform), typeof(CanvasRenderer));
multiplierObject.transform.SetParent(imageObject.transform, false);
RectTransform multiplierRect = multiplierObject.GetComponent<RectTransform>();
multiplierRect.anchorMin = new Vector2(1f, 0f);
multiplierRect.anchorMax = new Vector2(1f, 0f);
multiplierRect.pivot = new Vector2(1f, 0f);
multiplierRect.anchoredPosition = new Vector2(-2f, 1f);
multiplierRect.sizeDelta = new Vector2(22f, 15f);
tutorialMultiplierText = multiplierObject.AddComponent<TextMeshProUGUI>();
tutorialMultiplierText.font = scoreText.font;
tutorialMultiplierText.text = "x2";
tutorialMultiplierText.alignment = TextAlignmentOptions.Center;
tutorialMultiplierText.fontSize = 9f;
tutorialMultiplierText.fontStyle = FontStyles.Bold;
tutorialMultiplierText.color = new Color(1f, 0.91f, 0.33f, 1f);
tutorialMultiplierText.raycastTarget = false;
tutorialMultiplierText.textWrappingMode = TextWrappingModes.NoWrap;
tutorialMultiplierText.gameObject.SetActive(false);
}
private void CreateTutorialItemRow(Transform parent) {
string[] itemAssetPaths = {
"Assets/Sprites/Items/Item_Health_Sushi.png",
"Assets/Sprites/Items/Item_Invincible_Ramen.png",
"Assets/Sprites/Items/Item_Shield.png",
"Assets/Sprites/Items/Item_Speed_Shoes.png",
"Assets/Sprites/Items/Item_Mileage_Card.png"
};
tutorialItemImages = new Image[itemAssetPaths.Length];
for(int i = 0; i < itemAssetPaths.Length; i++)
{
GameObject itemObject = new GameObject("Tutorial Item " + (i + 1), typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
itemObject.transform.SetParent(parent, false);
RectTransform itemRect = itemObject.GetComponent<RectTransform>();
itemRect.anchorMin = new Vector2(0f, 0.5f);
itemRect.anchorMax = new Vector2(0f, 0.5f);
itemRect.pivot = new Vector2(0f, 0.5f);
itemRect.anchoredPosition = new Vector2(11f + (i * 27f), 0f);
itemRect.sizeDelta = new Vector2(24f, 24f);
Image itemImage = itemObject.GetComponent<Image>();
itemImage.sprite = LoadTutorialSprite(itemAssetPaths[i]);
itemImage.preserveAspect = true;
itemImage.raycastTarget = false;
itemImage.gameObject.SetActive(false);
tutorialItemImages[i] = itemImage;
}
}
private TextMeshProUGUI CreateTutorialText(Transform parent, string objectName, Vector2 anchoredPosition, Vector2 sizeDelta, float fontSize, FontStyles fontStyle) {
GameObject textObject = new GameObject(objectName, typeof(RectTransform), typeof(CanvasRenderer));
textObject.transform.SetParent(parent, false);
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 = anchoredPosition;
textRect.sizeDelta = sizeDelta;
TextMeshProUGUI text = textObject.AddComponent<TextMeshProUGUI>();
text.font = tutorialFontAsset != null ? tutorialFontAsset : scoreText.font;
text.alignment = TextAlignmentOptions.Left;
text.fontSize = fontSize;
text.fontStyle = fontStyle;
text.color = new Color(0.96f, 0.98f, 1f, 0.96f);
text.raycastTarget = false;
text.textWrappingMode = TextWrappingModes.Normal;
return text;
}
private TMP_FontAsset CreateKoreanTutorialFontAsset() {
string[] familyNames = {
"Malgun Gothic",
"맑은 고딕",
"Arial Unicode MS"
};
string[] styleNames = {
"Regular",
"Normal"
};
for(int familyIndex = 0; familyIndex < familyNames.Length; familyIndex++)
{
for(int styleIndex = 0; styleIndex < styleNames.Length; styleIndex++)
{
TMP_FontAsset fontAsset = TMP_FontAsset.CreateFontAsset(familyNames[familyIndex], styleNames[styleIndex], 80);
if(fontAsset != null)
{
fontAsset.name = "Runtime Korean Tutorial Font";
return fontAsset;
}
}
}
return null;
}
private Sprite GetTutorialMainSprite(TutorialGuideType guideType) {
if(guideType == TutorialGuideType.Jump || guideType == TutorialGuideType.DoubleJump)
{
return LoadTutorialSprite("Assets/Sprites/JJ_Jump.png", "JJ_Jump_2");
}
if(guideType == TutorialGuideType.Slide)
{
return LoadTutorialSprite("Assets/Sprites/JJ_Slide.png", "JJ_Slide_0");
}
if(guideType == TutorialGuideType.Damage)
{
return LoadTutorialSprite("Assets/Resources/UI_LifeStamp.png");
}
return null;
}
private Sprite LoadTutorialSprite(string assetPath, string spriteName = "") {
#if UNITY_EDITOR
Object[] assets = AssetDatabase.LoadAllAssetRepresentationsAtPath(assetPath);
Sprite firstSprite = null;
if(assets != null && assets.Length > 0)
{
for(int i = 0; i < assets.Length; i++)
{
Sprite sprite = assets[i] as Sprite;
if(sprite == null)
{
continue;
}
if(firstSprite == null)
{
firstSprite = sprite;
}
if(sprite.name == spriteName)
{
return sprite;
}
}
if(firstSprite != null)
{
return firstSprite;
}
}
Sprite directSprite = AssetDatabase.LoadAssetAtPath<Sprite>(assetPath);
if(directSprite != null)
{
return directSprite;
}
Texture2D 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
}
}
+96
View File
@@ -0,0 +1,96 @@
using UnityEngine;
// 튜토리얼 중 플레이어가 직접 먹어 효과를 확인하는 필드 아이템
public class ItemPickup : MonoBehaviour
{
public GameManager.TutorialItemType itemType = GameManager.TutorialItemType.HealthSushi;
private static Sprite fallbackSprite;
private bool collected = false;
private SpriteRenderer spriteRenderer;
private Collider2D pickupCollider;
private void Awake()
{
spriteRenderer = GetComponent<SpriteRenderer>();
pickupCollider = GetComponent<Collider2D>();
}
public void Setup(Sprite sprite, GameManager.TutorialItemType nextItemType)
{
itemType = nextItemType;
if(spriteRenderer != null)
{
spriteRenderer.sprite = sprite != null ? sprite : GetFallbackSprite();
spriteRenderer.color = sprite != null ? Color.white : GetFallbackColor(nextItemType);
}
}
public void ResetPickup()
{
collected = false;
gameObject.SetActive(true);
if(pickupCollider != null)
{
pickupCollider.enabled = true;
}
}
private void OnTriggerEnter2D(Collider2D other)
{
if(collected || !other.CompareTag("Player") || !GameFlowManager.IsGameplayActive)
{
return;
}
collected = true;
if(GameManager.instance != null)
{
GameManager.instance.ApplyTutorialItem(itemType);
}
gameObject.SetActive(false);
}
private Color GetFallbackColor(GameManager.TutorialItemType fallbackItemType)
{
switch(fallbackItemType)
{
case GameManager.TutorialItemType.InvincibleRamen:
return new Color(1f, 0.58f, 0.12f, 1f);
case GameManager.TutorialItemType.Shield:
return new Color(0.32f, 0.78f, 1f, 1f);
case GameManager.TutorialItemType.SpeedShoes:
return new Color(0.35f, 1f, 0.5f, 1f);
case GameManager.TutorialItemType.MileageCard:
return new Color(1f, 0.92f, 0.26f, 1f);
case GameManager.TutorialItemType.HealthSushi:
default:
return new Color(1f, 0.34f, 0.42f, 1f);
}
}
private Sprite GetFallbackSprite()
{
if(fallbackSprite != null)
{
return fallbackSprite;
}
Texture2D texture = new Texture2D(16, 16);
Color[] pixels = new Color[16 * 16];
for(int i = 0; i < pixels.Length; i++)
{
pixels[i] = Color.white;
}
texture.SetPixels(pixels);
texture.Apply();
fallbackSprite = Sprite.Create(texture, new Rect(0f, 0f, 16f, 16f), new Vector2(0.5f, 0.5f), 16f);
return fallbackSprite;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 24f0b6a8d1474f81a6d1a3b9a07e5c43
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4b552d8cc0244e64bcf24dfac5a7df5e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+249
View File
@@ -0,0 +1,249 @@
using UnityEngine;
public static class MapDatabase {
private static MapDefinition[] maps;
public static MapDefinition[] AllMaps {
get {
EnsureMaps();
return maps;
}
}
public static MapDefinition GetMap(MapId mapId) {
EnsureMaps();
for(int i = 0; i < maps.Length; i++)
{
if(maps[i].mapId == mapId)
{
return CloneMap(maps[i]);
}
}
return CloneMap(maps[0]);
}
public static MapDefinition GetMapByIndex(int mapIndex) {
EnsureMaps();
int clampedIndex = Mathf.Clamp(mapIndex, 0, maps.Length - 1);
return CloneMap(maps[clampedIndex]);
}
public static int GetIndex(MapId mapId) {
EnsureMaps();
for(int i = 0; i < maps.Length; i++)
{
if(maps[i].mapId == mapId)
{
return i;
}
}
return 0;
}
public static int LastMapIndex {
get {
EnsureMaps();
return maps.Length - 1;
}
}
private static void EnsureMaps() {
if(maps != null && maps.Length > 0)
{
return;
}
maps = new MapDefinition[] {
CreateTutorialIncheon(),
CreateJapan(),
CreateChina(),
CreateAmerica()
};
}
private static MapDefinition CreateTutorialIncheon() {
return new MapDefinition {
mapId = MapId.TutorialIncheon,
displayName = "Tutorial / Incheon",
routeName = "인천공항 출발 준비",
isTutorial = true,
targetDistance = 850f,
timeLimit = 95f,
clearMileageReward = 150,
hasNextMap = true,
nextMapId = MapId.Japan,
openingSteps = new PlatformSpawner.TutorialStep[] {
Step(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.Jump, -2.4f, 1.05f, "점프", "가방 장애물이 보이면 왼쪽 클릭으로 뛰어넘습니다."),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 1f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.DoubleJump, -2.4f, 2.1f, "2단 점프", "다음 발판이 멀리 있으면 공중에서 한 번 더 왼쪽 클릭합니다."),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 1.1f),
Step(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.CenterPair, GameManager.TutorialGuideType.Slide, -2.4f, 1.15f, "슬라이드", "천장 표지판이 보이면 오른쪽 클릭을 누르고 지나갑니다."),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 1f),
Step(Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, GameManager.TutorialGuideType.Damage, -2.4f, 1.15f, "피격", "이번 구간은 일부러 맞아 목숨과 속도 감소를 확인합니다."),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.Items, -2.4f, 0.95f, "초밥", "초밥을 먹으면 잃은 목숨을 1 회복합니다.", Platform.ItemPattern.HealthSushi),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.Items, -2.4f, 0.9f, "라멘", "라멘을 먹고 다음 충돌을 피해 없이 지나가 봅니다.", Platform.ItemPattern.InvincibleRamen),
Step(Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, GameManager.TutorialGuideType.None, -2.4f, 1.05f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.Items, -2.4f, 0.9f, "방어막", "방어막을 먹고 다음 충돌 1회를 막아 봅니다.", Platform.ItemPattern.Shield),
Step(Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, GameManager.TutorialGuideType.None, -2.4f, 1.05f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.Items, -2.4f, 0.95f, "운동화", "운동화는 속도를 회복해 일정이 늦어지는 것을 줄입니다.", Platform.ItemPattern.SpeedShoes),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.Items, -2.4f, 0.95f, "마일리지 카드", "카드를 먹은 뒤 마일리지를 모으면 획득량이 늘어납니다.", Platform.ItemPattern.MileageCard),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 1f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 1f)
},
loopSteps = CreateEasyLoop()
};
}
private static MapDefinition CreateJapan() {
return new MapDefinition {
mapId = MapId.Japan,
displayName = "Japan",
routeName = "후쿠오카 라멘 투어",
isTutorial = false,
targetDistance = 1200f,
timeLimit = 105f,
clearMileageReward = 250,
hasNextMap = true,
nextMapId = MapId.China,
openingSteps = new PlatformSpawner.TutorialStep[] {
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.2f, 1.8f)
},
loopSteps = new PlatformSpawner.TutorialStep[] {
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.2f, 2.05f),
Step(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.3f, 2.15f),
Step(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.CenterPair, GameManager.TutorialGuideType.None, -2.25f, 2.35f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -1.95f, 2.05f),
Step(Platform.PlatformPattern.LowLeft, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.35f, 2.1f),
Step(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.WidePair, GameManager.TutorialGuideType.None, -2.3f, 2.45f)
}
};
}
private static MapDefinition CreateChina() {
return new MapDefinition {
mapId = MapId.China,
displayName = "China",
routeName = "베이징 시장길",
isTutorial = false,
targetDistance = 1450f,
timeLimit = 115f,
clearMileageReward = 350,
hasNextMap = true,
nextMapId = MapId.America,
openingSteps = new PlatformSpawner.TutorialStep[] {
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.1f, 1.9f)
},
loopSteps = new PlatformSpawner.TutorialStep[] {
Step(Platform.PlatformPattern.LowRight, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.25f, 2.0f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -1.8f, 2.05f),
Step(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.LeftPair, GameManager.TutorialGuideType.None, -2.3f, 2.35f),
Step(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 2.05f),
Step(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.RightPair, GameManager.TutorialGuideType.None, -2.25f, 2.35f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2f, 1.95f)
}
};
}
private static MapDefinition CreateAmerica() {
return new MapDefinition {
mapId = MapId.America,
displayName = "America",
routeName = "LA 공항 고속도로",
isTutorial = false,
targetDistance = 1700f,
timeLimit = 125f,
clearMileageReward = 500,
hasNextMap = false,
nextMapId = MapId.America,
openingSteps = new PlatformSpawner.TutorialStep[] {
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.2f, 1.8f)
},
loopSteps = new PlatformSpawner.TutorialStep[] {
Step(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 1.95f),
Step(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.WidePair, GameManager.TutorialGuideType.None, -2.25f, 2.25f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -1.8f, 1.9f),
Step(Platform.PlatformPattern.LowLeft, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.35f, 1.95f),
Step(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.CenterPair, GameManager.TutorialGuideType.None, -2.25f, 2.2f),
Step(Platform.PlatformPattern.LowRight, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.1f, 1.9f)
}
};
}
private static PlatformSpawner.TutorialStep[] CreateEasyLoop() {
return new PlatformSpawner.TutorialStep[] {
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 2.1f),
Step(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 2.2f),
Step(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.1f, 2.1f),
Step(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.CenterPair, GameManager.TutorialGuideType.None, -2.35f, 2.35f)
};
}
private static PlatformSpawner.TutorialStep Step(
Platform.PlatformPattern platformPattern,
Platform.MileagePattern mileagePattern,
SlideObstaclePattern.SlideLayout slideLayout,
GameManager.TutorialGuideType guideType,
float yPosition,
float spawnInterval,
string title = "",
string message = "",
Platform.ItemPattern itemPattern = Platform.ItemPattern.None) {
return new PlatformSpawner.TutorialStep {
platformPattern = platformPattern,
mileagePattern = mileagePattern,
slideLayout = slideLayout,
itemPattern = itemPattern,
guideType = guideType,
yPosition = yPosition,
spawnInterval = spawnInterval,
title = title,
message = message
};
}
private static MapDefinition CloneMap(MapDefinition source) {
return new MapDefinition {
mapId = source.mapId,
displayName = source.displayName,
routeName = source.routeName,
isTutorial = source.isTutorial,
targetDistance = source.targetDistance,
timeLimit = source.timeLimit,
clearMileageReward = source.clearMileageReward,
hasNextMap = source.hasNextMap,
nextMapId = source.nextMapId,
openingSteps = CloneSteps(source.openingSteps),
loopSteps = CloneSteps(source.loopSteps)
};
}
public static PlatformSpawner.TutorialStep[] CloneSteps(PlatformSpawner.TutorialStep[] source) {
if(source == null)
{
return new PlatformSpawner.TutorialStep[0];
}
PlatformSpawner.TutorialStep[] steps = new PlatformSpawner.TutorialStep[source.Length];
for(int i = 0; i < source.Length; i++)
{
PlatformSpawner.TutorialStep sourceStep = source[i];
steps[i] = Step(
sourceStep.platformPattern,
sourceStep.mileagePattern,
sourceStep.slideLayout,
sourceStep.guideType,
sourceStep.yPosition,
sourceStep.spawnInterval,
sourceStep.title,
sourceStep.message,
sourceStep.itemPattern);
}
return steps;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 194fc0ddfc534d08801e82235a731527
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+16
View File
@@ -0,0 +1,16 @@
using System;
[Serializable]
public class MapDefinition {
public MapId mapId;
public string displayName;
public string routeName;
public bool isTutorial;
public float targetDistance;
public float timeLimit;
public int clearMileageReward;
public bool hasNextMap;
public MapId nextMapId;
public PlatformSpawner.TutorialStep[] openingSteps;
public PlatformSpawner.TutorialStep[] loopSteps;
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 65f34cfe92f647c39ff63111a09a3da8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+6
View File
@@ -0,0 +1,6 @@
public enum MapId {
TutorialIncheon = 0,
Japan = 1,
China = 2,
America = 3
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c10415d13fe84f39923b0f88bce0d90c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+1 -1
View File
@@ -38,7 +38,7 @@ public class MileagePickup : MonoBehaviour
private void OnTriggerEnter2D(Collider2D other)
{
if(collected || !other.CompareTag("Player"))
if(collected || !other.CompareTag("Player") || !GameFlowManager.IsGameplayActive)
{
return;
}
+189 -7
View File
@@ -1,4 +1,7 @@
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
// 발판으로서 필요한 동작을 담은 스크립트
public class Platform : MonoBehaviour {
@@ -8,7 +11,8 @@ public class Platform : MonoBehaviour {
LowLeft,
LowMid,
LowRight,
Slide
Slide,
ForceHit
}
public enum MileagePattern {
@@ -19,6 +23,15 @@ public class Platform : MonoBehaviour {
SlideLine
}
public enum ItemPattern {
None,
HealthSushi,
InvincibleRamen,
Shield,
SpeedShoes,
MileageCard
}
public GameObject[] obstacles; // 장애물 오브젝트들
public int emptyPatternWeight = 2; // 장애물이 없는 패턴이 선택될 가중치
public Sprite mileageSprite; // 이 발판에서 사용할 마일리지 토큰 스프라이트
@@ -28,20 +41,34 @@ public class Platform : MonoBehaviour {
private bool stepped = false; // 플레이어 캐릭터가 밟았었는가
private MileagePickup[] mileagePickups; // 런타임에 생성하는 마일리지 토큰 풀
private ItemPickup[] itemPickups; // 런타임에 생성하는 튜토리얼 아이템 풀
private PlatformPattern reservedPattern = PlatformPattern.Random; // 다음 활성화 때 사용할 수동 패턴
private MileagePattern reservedMileagePattern = MileagePattern.Random; // 다음 활성화 때 사용할 마일리지 패턴
private SlideObstaclePattern.SlideLayout reservedSlideLayout = SlideObstaclePattern.SlideLayout.Random; // 다음 슬라이딩 장애물 배치
private ItemPattern reservedItemPattern = ItemPattern.None; // 다음 활성화 때 사용할 아이템 배치
private const int MileagePoolSize = 5;
private const int ItemPoolSize = 1;
private const float MileageTokenScale = 0.55f;
private const float MileageColliderRadius = 0.4f;
private const float ItemTokenScale = 0.68f;
private const float ItemColliderRadius = 0.46f;
public void ConfigureForTutorial(PlatformPattern pattern, MileagePattern mileagePattern) {
public void ConfigureForTutorial(
PlatformPattern pattern,
MileagePattern mileagePattern,
SlideObstaclePattern.SlideLayout slideLayout = SlideObstaclePattern.SlideLayout.Random,
ItemPattern itemPattern = ItemPattern.None) {
reservedPattern = pattern;
reservedMileagePattern = mileagePattern;
reservedSlideLayout = slideLayout;
reservedItemPattern = itemPattern;
}
public void ConfigureForRandom() {
reservedPattern = PlatformPattern.Random;
reservedMileagePattern = MileagePattern.Random;
reservedSlideLayout = SlideObstaclePattern.SlideLayout.Random;
reservedItemPattern = ItemPattern.None;
}
// 컴포넌트가 활성화될때 마다 매번 실행되는 메서드
@@ -50,6 +77,7 @@ public class Platform : MonoBehaviour {
stepped = false;
EnsureMileagePickups();
EnsureItemPickups();
for(int i = 0; i < obstacles.Length; i++)
{
@@ -57,10 +85,11 @@ public class Platform : MonoBehaviour {
}
HideMileagePickups();
HideItemPickups();
if(reservedPattern != PlatformPattern.Random)
{
ApplyTutorialPattern();
ApplyReservedPattern();
return;
}
@@ -77,6 +106,7 @@ public class Platform : MonoBehaviour {
if(patternIndex < obstacles.Length)
{
activeObstacle = obstacles[patternIndex];
ConfigureSlideObstacle(activeObstacle);
activeObstacle.SetActive(true);
}
@@ -85,7 +115,7 @@ public class Platform : MonoBehaviour {
void OnCollisionEnter2D(Collision2D collision) {
// 플레이어 캐릭터가 자신을 밟았을때 점수를 추가하는 처리
if(collision.collider.tag == "Player" && !stepped)
if(collision.collider.tag == "Player" && !stepped && GameFlowManager.IsGameplayActive)
{
stepped = true;
GameManager.instance.AddScore(1);
@@ -122,6 +152,33 @@ public class Platform : MonoBehaviour {
}
}
private void EnsureItemPickups() {
if(itemPickups != null && itemPickups.Length == ItemPoolSize)
{
return;
}
itemPickups = new ItemPickup[ItemPoolSize];
for(int i = 0; i < ItemPoolSize; i++)
{
GameObject pickupObject = new GameObject("Tutorial Item Pickup " + (i + 1));
pickupObject.transform.SetParent(transform);
pickupObject.transform.localScale = GetParentScaleCompensatedVector(ItemTokenScale);
SpriteRenderer spriteRenderer = pickupObject.AddComponent<SpriteRenderer>();
spriteRenderer.sortingLayerName = "Foreground";
spriteRenderer.sortingOrder = 2;
CircleCollider2D pickupCollider = pickupObject.AddComponent<CircleCollider2D>();
pickupCollider.isTrigger = true;
pickupCollider.radius = ItemColliderRadius;
itemPickups[i] = pickupObject.AddComponent<ItemPickup>();
pickupObject.SetActive(false);
}
}
private void HideMileagePickups() {
if(mileagePickups == null)
{
@@ -137,6 +194,21 @@ public class Platform : MonoBehaviour {
}
}
private void HideItemPickups() {
if(itemPickups == null)
{
return;
}
for(int i = 0; i < itemPickups.Length; i++)
{
if(itemPickups[i] != null)
{
itemPickups[i].gameObject.SetActive(false);
}
}
}
private void TryPlaceMileagePattern(GameObject activeObstacle) {
if(mileageSprite == null || mileagePickups == null || Random.Range(0, 100) >= mileagePatternChance)
{
@@ -159,7 +231,7 @@ public class Platform : MonoBehaviour {
}
}
private void ApplyTutorialPattern() {
private void ApplyReservedPattern() {
GameObject activeObstacle = null;
switch(reservedPattern)
@@ -176,14 +248,46 @@ public class Platform : MonoBehaviour {
case PlatformPattern.Slide:
activeObstacle = FindObstacle("Slide Obstacle Pattern");
break;
case PlatformPattern.ForceHit:
activeObstacle = FindObstacle("Obstacle Mid");
ActivateObstacle(activeObstacle);
GameObject slideObstacle = FindObstacle("Slide Obstacle Pattern");
reservedSlideLayout = SlideObstaclePattern.SlideLayout.WidePair;
ActivateObstacle(slideObstacle);
break;
}
if(activeObstacle != null)
if(reservedPattern != PlatformPattern.ForceHit && activeObstacle != null)
{
activeObstacle.SetActive(true);
ActivateObstacle(activeObstacle);
}
PlaceReservedMileagePattern(activeObstacle);
PlaceReservedItemPattern();
}
private void ActivateObstacle(GameObject obstacle) {
if(obstacle == null)
{
return;
}
ConfigureSlideObstacle(obstacle);
obstacle.SetActive(true);
}
private void ConfigureSlideObstacle(GameObject activeObstacle) {
if(activeObstacle == null || !activeObstacle.name.Contains("Slide"))
{
return;
}
SlideObstaclePattern slideObstaclePattern = activeObstacle.GetComponent<SlideObstaclePattern>();
if(slideObstaclePattern != null)
{
slideObstaclePattern.ConfigureLayout(reservedSlideLayout);
}
}
private GameObject FindObstacle(string obstacleName) {
@@ -226,6 +330,84 @@ public class Platform : MonoBehaviour {
TryPlaceMileagePattern(activeObstacle);
}
private void PlaceReservedItemPattern() {
if(reservedItemPattern == ItemPattern.None || itemPickups == null || itemPickups.Length == 0)
{
return;
}
GameManager.TutorialItemType itemType = GetTutorialItemType(reservedItemPattern);
ItemPickup pickup = itemPickups[0];
pickup.Setup(LoadItemSprite(itemType), itemType);
pickup.transform.localPosition = GetParentScaleCompensatedPosition(GetItemPosition(reservedItemPattern));
pickup.ResetPickup();
}
private GameManager.TutorialItemType GetTutorialItemType(ItemPattern itemPattern) {
switch(itemPattern)
{
case ItemPattern.InvincibleRamen:
return GameManager.TutorialItemType.InvincibleRamen;
case ItemPattern.Shield:
return GameManager.TutorialItemType.Shield;
case ItemPattern.SpeedShoes:
return GameManager.TutorialItemType.SpeedShoes;
case ItemPattern.MileageCard:
return GameManager.TutorialItemType.MileageCard;
case ItemPattern.HealthSushi:
default:
return GameManager.TutorialItemType.HealthSushi;
}
}
private Vector2 GetItemPosition(ItemPattern itemPattern) {
switch(itemPattern)
{
case ItemPattern.HealthSushi:
return new Vector2(-1.6f, 1.55f);
case ItemPattern.InvincibleRamen:
return new Vector2(-0.8f, 1.55f);
case ItemPattern.Shield:
return new Vector2(-0.2f, 1.55f);
case ItemPattern.SpeedShoes:
return new Vector2(0.7f, 1.55f);
case ItemPattern.MileageCard:
return new Vector2(1.4f, 1.55f);
default:
return new Vector2(0f, 1.55f);
}
}
private Sprite LoadItemSprite(GameManager.TutorialItemType itemType) {
#if UNITY_EDITOR
string assetPath = "";
switch(itemType)
{
case GameManager.TutorialItemType.InvincibleRamen:
assetPath = "Assets/Sprites/Items/Item_Invincible_Ramen.png";
break;
case GameManager.TutorialItemType.Shield:
assetPath = "Assets/Sprites/Items/Item_Shield.png";
break;
case GameManager.TutorialItemType.SpeedShoes:
assetPath = "Assets/Sprites/Items/Item_Speed_Shoes.png";
break;
case GameManager.TutorialItemType.MileageCard:
assetPath = "Assets/Sprites/Items/Item_Mileage_Card.png";
break;
case GameManager.TutorialItemType.HealthSushi:
default:
assetPath = "Assets/Sprites/Items/Item_Health_Sushi.png";
break;
}
return AssetDatabase.LoadAssetAtPath<Sprite>(assetPath);
#else
return null;
#endif
}
private void PlaceSafeMileageLine() {
Vector2[] positions = {
new Vector2(-2f, 1.35f),
+240 -35
View File
@@ -4,18 +4,38 @@ using Random = UnityEngine.Random;
// 발판을 생성하고 주기적으로 재배치하는 스크립트
public class PlatformSpawner : MonoBehaviour {
private enum TutorialStepKind {
None,
Jump,
DoubleJumpTakeoff,
Slide,
ForceHit,
HealthItem,
InvincibleItem,
ShieldItem,
SpeedItem,
MileageCardItem
}
[Serializable]
public class TutorialStep {
public Platform.PlatformPattern platformPattern;
public Platform.MileagePattern mileagePattern;
public SlideObstaclePattern.SlideLayout slideLayout;
public Platform.ItemPattern itemPattern;
public GameManager.TutorialGuideType guideType;
public float yPosition;
public float spawnInterval;
public string title;
public string message;
}
public GameObject platformPrefab; // 생성할 발판의 원본 프리팹
public int count = 3; // 생성할 발판의 개수
public bool tutorialMode = true; // 튜토리얼에서는 랜덤 대신 정해진 순서로 배치
public bool usePatternCourse = true; // 검증된 발판 패턴 코스를 사용
public bool tutorialMode = true; // 처음에는 튜토리얼 패턴을 먼저 배치
public TutorialStep[] tutorialSteps; // 튜토리얼 발판 순서
public TutorialStep[] stagePatternSteps; // 튜토리얼 이후 반복할 검증된 발판 순서
public float timeBetSpawnMin = 1.25f; // 다음 배치까지의 시간 간격 최솟값
public float timeBetSpawnMax = 2.25f; // 다음 배치까지의 시간 간격 최댓값
@@ -28,13 +48,46 @@ public class PlatformSpawner : MonoBehaviour {
private GameObject[] platforms; // 미리 생성한 발판들
private int currentIndex = 0; // 사용할 현재 순번의 발판
private int tutorialStepIndex = 0; // 진행 중인 튜토리얼 순번
private int courseStepIndex = 0; // 진행 중인 패턴 코스 순번
private Vector2 poolPosition = new Vector2(0, -25); // 초반에 생성된 발판들을 화면 밖에 숨겨둘 위치
private float lastSpawnTime; // 마지막 배치 시점
public void ApplyMap(MapDefinition mapDefinition) {
if(mapDefinition == null)
{
return;
}
usePatternCourse = true;
tutorialMode = mapDefinition.isTutorial;
tutorialSteps = MapDatabase.CloneSteps(mapDefinition.openingSteps);
stagePatternSteps = MapDatabase.CloneSteps(mapDefinition.loopSteps);
courseStepIndex = 0;
currentIndex = 0;
lastSpawnTime = Time.time;
timeBetSpawn = 0f;
if(platforms != null)
{
for(int i = 0; i < platforms.Length; i++)
{
if(platforms[i] != null)
{
platforms[i].SetActive(false);
platforms[i].transform.position = poolPosition;
}
}
}
}
void Start() {
EnsureTutorialSteps();
EnsurePatternSteps();
if(usePatternCourse && tutorialMode)
{
count = Mathf.Max(count, 8);
}
// 변수들을 초기화하고 사용할 발판들을 미리 생성
platforms = new GameObject[count];
@@ -53,7 +106,7 @@ public class PlatformSpawner : MonoBehaviour {
void Update() {
// 순서를 돌아가며 주기적으로 발판을 배치
if(GameManager.instance.isGameover)
if(GameManager.instance == null || GameManager.instance.isGameover || !GameFlowManager.IsGameplayActive)
{
return;
}
@@ -62,12 +115,15 @@ public class PlatformSpawner : MonoBehaviour {
{
lastSpawnTime = Time.time;
timeBetSpawn = tutorialMode ? tutorialSpawnInterval : Random.Range(timeBetSpawnMin, timeBetSpawnMax);
TutorialStep step = usePatternCourse ? GetCurrentCourseStep() : null;
timeBetSpawn = step != null ? GetStepSpawnInterval(step) : Random.Range(timeBetSpawnMin, timeBetSpawnMax);
float yPos = tutorialMode ? GetTutorialYPosition() : Random.Range(yMin, yMax);
float yPos = step != null
? Mathf.Clamp(step.yPosition, yMin, yMax)
: Random.Range(yMin, yMax);
platforms[currentIndex].SetActive(false);
ConfigurePlatform(platforms[currentIndex]);
ConfigurePlatform(platforms[currentIndex], step);
platforms[currentIndex].SetActive(true);
platforms[currentIndex].transform.position = new Vector2(xPos, yPos);
@@ -79,14 +135,14 @@ public class PlatformSpawner : MonoBehaviour {
currentIndex = 0;
}
if(tutorialMode)
if(usePatternCourse)
{
tutorialStepIndex++;
courseStepIndex++;
}
}
}
private void ConfigurePlatform(GameObject platformObject) {
private void ConfigurePlatform(GameObject platformObject, TutorialStep step) {
Platform platform = platformObject.GetComponent<Platform>();
if(platform == null)
@@ -94,37 +150,74 @@ public class PlatformSpawner : MonoBehaviour {
return;
}
if(!tutorialMode)
if(step == null)
{
platform.ConfigureForRandom();
return;
}
TutorialStep step = GetCurrentTutorialStep();
platform.ConfigureForTutorial(step.platformPattern, step.mileagePattern);
platform.ConfigureForTutorial(step.platformPattern, step.mileagePattern, step.slideLayout, step.itemPattern);
if(GameManager.instance != null)
if(GameManager.instance != null && step.guideType != GameManager.TutorialGuideType.None)
{
GameManager.instance.SetTutorialMessage(step.message);
GameManager.instance.SetTutorialGuide(step.guideType, step.title, step.message);
}
if(GameManager.instance != null
&& step.guideType == GameManager.TutorialGuideType.None
&& (!tutorialMode || courseStepIndex >= tutorialSteps.Length))
{
GameManager.instance.SetTutorialGuide(GameManager.TutorialGuideType.None, "", "");
}
}
private float GetTutorialYPosition() {
return Mathf.Clamp(GetCurrentTutorialStep().yPosition, yMin, yMax);
private float GetStepSpawnInterval(TutorialStep step) {
if(step != null && step.spawnInterval > 0f)
{
return step.spawnInterval;
}
private TutorialStep GetCurrentTutorialStep() {
return tutorialSpawnInterval;
}
private TutorialStep GetCurrentCourseStep() {
EnsurePatternSteps();
if(tutorialMode && courseStepIndex < tutorialSteps.Length)
{
return tutorialSteps[courseStepIndex];
}
int loopIndex = tutorialMode
? courseStepIndex - tutorialSteps.Length
: courseStepIndex;
if(stagePatternSteps == null || stagePatternSteps.Length == 0)
{
return CreateStep(
Platform.PlatformPattern.Empty,
Platform.MileagePattern.SafeLine,
SlideObstaclePattern.SlideLayout.Random,
GameManager.TutorialGuideType.None,
-2.2f,
tutorialSpawnInterval,
"",
"");
}
loopIndex %= stagePatternSteps.Length;
if(loopIndex < 0)
{
loopIndex += stagePatternSteps.Length;
}
return stagePatternSteps[loopIndex];
}
private void EnsurePatternSteps() {
EnsureTutorialSteps();
if(tutorialStepIndex < tutorialSteps.Length)
{
return tutorialSteps[tutorialStepIndex];
}
int reviewStartIndex = Mathf.Max(0, tutorialSteps.Length - 3);
int reviewLength = tutorialSteps.Length - reviewStartIndex;
int reviewIndex = reviewStartIndex + ((tutorialStepIndex - tutorialSteps.Length) % reviewLength);
return tutorialSteps[reviewIndex];
EnsureStagePatternSteps();
}
private void EnsureTutorialSteps() {
@@ -134,21 +227,133 @@ public class PlatformSpawner : MonoBehaviour {
}
tutorialSteps = new TutorialStep[] {
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, -2.4f, "Tutorial 1: Run forward and collect mileage."),
CreateStep(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, -2.4f, "Tutorial 2: Left click to jump over suitcase obstacles."),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, -2.4f, "Tutorial 3: Hold right click to slide under airport signs."),
CreateStep(Platform.PlatformPattern.LowLeft, Platform.MileagePattern.JumpArc, -2.1f, "Tutorial 4: Suitcases mean jump."),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, -2.2f, "Tutorial 5: Hanging signs mean slide."),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, -1.8f, "Tutorial complete: collect mileage and keep moving."),
CreateTutorialStep(TutorialStepKind.Jump, Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, -2.4f, 1.05f),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, -2.4f, 1f),
CreateTutorialStep(TutorialStepKind.DoubleJumpTakeoff, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, -2.4f, 2.10f),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, -2.4f, 1.10f),
CreateTutorialStep(TutorialStepKind.Slide, Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.CenterPair, -2.4f, 1.15f),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, -2.4f, 1f),
CreateTutorialStep(TutorialStepKind.ForceHit, Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, -2.4f, 1.15f),
CreateTutorialStep(TutorialStepKind.HealthItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, -2.4f, 0.95f, Platform.ItemPattern.HealthSushi),
CreateTutorialStep(TutorialStepKind.InvincibleItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, -2.4f, 0.9f, Platform.ItemPattern.InvincibleRamen),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, -2.4f, 1.05f),
CreateTutorialStep(TutorialStepKind.ShieldItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, -2.4f, 0.9f, Platform.ItemPattern.Shield),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, -2.4f, 1.05f),
CreateTutorialStep(TutorialStepKind.SpeedItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, -2.4f, 0.95f, Platform.ItemPattern.SpeedShoes),
CreateTutorialStep(TutorialStepKind.MileageCardItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, -2.4f, 0.95f, Platform.ItemPattern.MileageCard),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, -2.4f, 1f),
CreateTutorialStep(TutorialStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, -2.4f, 1f),
};
}
private TutorialStep CreateStep(Platform.PlatformPattern platformPattern, Platform.MileagePattern mileagePattern, float yPosition, string message) {
private void EnsureStagePatternSteps() {
if(stagePatternSteps != null && stagePatternSteps.Length > 0)
{
return;
}
stagePatternSteps = new TutorialStep[] {
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.2f, 2.25f, "", ""),
CreateStep(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.3f, 2.4f, "", ""),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -1.95f, 2.2f, "", ""),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.WidePair, GameManager.TutorialGuideType.None, -2.25f, 2.65f, "", ""),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.0f, 2.2f, "", ""),
CreateStep(Platform.PlatformPattern.LowLeft, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.35f, 2.35f, "", ""),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.LeftPair, GameManager.TutorialGuideType.None, -2.35f, 2.7f, "", ""),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.1f, 2.2f, "", ""),
CreateStep(Platform.PlatformPattern.LowRight, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.0f, 2.35f, "", ""),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -1.85f, 2.15f, "", ""),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.RightPair, GameManager.TutorialGuideType.None, -2.25f, 2.65f, "", ""),
CreateStep(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.4f, 2.35f, "", ""),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, -2.0f, 2.2f, "", ""),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.CenterPair, GameManager.TutorialGuideType.None, -2.2f, 2.7f, "", ""),
};
}
private TutorialStep CreateStep(
Platform.PlatformPattern platformPattern,
Platform.MileagePattern mileagePattern,
SlideObstaclePattern.SlideLayout slideLayout,
GameManager.TutorialGuideType guideType,
float yPosition,
float spawnInterval,
string title,
string message,
Platform.ItemPattern itemPattern = Platform.ItemPattern.None) {
TutorialStep step = new TutorialStep();
step.platformPattern = platformPattern;
step.mileagePattern = mileagePattern;
step.slideLayout = slideLayout;
step.itemPattern = itemPattern;
step.guideType = guideType;
step.yPosition = yPosition;
step.spawnInterval = spawnInterval;
step.title = title;
step.message = message;
return step;
}
private TutorialStep CreateTutorialStep(
TutorialStepKind stepKind,
Platform.PlatformPattern platformPattern,
Platform.MileagePattern mileagePattern,
SlideObstaclePattern.SlideLayout slideLayout,
float yPosition,
float spawnInterval,
Platform.ItemPattern itemPattern = Platform.ItemPattern.None) {
GameManager.TutorialGuideType guideType = GameManager.TutorialGuideType.None;
string title = "";
string message = "";
switch(stepKind)
{
case TutorialStepKind.Jump:
guideType = GameManager.TutorialGuideType.Jump;
title = "점프";
message = "가방 장애물이 보이면 왼쪽 클릭으로 뛰어넘습니다.";
break;
case TutorialStepKind.DoubleJumpTakeoff:
guideType = GameManager.TutorialGuideType.DoubleJump;
title = "2단 점프";
message = "다음 발판이 멀리 있으면 공중에서 한 번 더 왼쪽 클릭합니다.";
break;
case TutorialStepKind.Slide:
guideType = GameManager.TutorialGuideType.Slide;
title = "슬라이드";
message = "천장 표지판이 보이면 오른쪽 클릭을 누르고 지나갑니다.";
break;
case TutorialStepKind.ForceHit:
guideType = GameManager.TutorialGuideType.Damage;
title = "피격";
message = "이번 구간은 일부러 맞아 목숨과 속도 감소를 확인합니다.";
break;
case TutorialStepKind.HealthItem:
guideType = GameManager.TutorialGuideType.Items;
title = "초밥";
message = "초밥을 먹으면 잃은 목숨을 1 회복합니다.";
break;
case TutorialStepKind.InvincibleItem:
guideType = GameManager.TutorialGuideType.Items;
title = "라멘";
message = "라멘을 먹고 다음 충돌을 피해 없이 지나가 봅니다.";
break;
case TutorialStepKind.ShieldItem:
guideType = GameManager.TutorialGuideType.Items;
title = "방어막";
message = "방어막을 먹고 다음 충돌 1회를 막아 봅니다.";
break;
case TutorialStepKind.SpeedItem:
guideType = GameManager.TutorialGuideType.Items;
title = "운동화";
message = "운동화는 속도를 회복해 일정이 늦어지는 것을 줄입니다.";
break;
case TutorialStepKind.MileageCardItem:
guideType = GameManager.TutorialGuideType.Items;
title = "마일리지 카드";
message = "카드를 먹은 뒤 마일리지를 모으면 획득량이 늘어납니다.";
break;
}
return CreateStep(platformPattern, mileagePattern, slideLayout, guideType, yPosition, spawnInterval, title, message, itemPattern);
}
}
+12 -1
View File
@@ -42,7 +42,7 @@ public class PlayerController : MonoBehaviour {
}
private void Update() {
if(isDead)
if(isDead || !GameFlowManager.IsGameplayActive)
{
return;
}
@@ -108,6 +108,11 @@ public class PlayerController : MonoBehaviour {
}
private void OnTriggerEnter2D(Collider2D other) {
if(!GameFlowManager.IsGameplayActive)
{
return;
}
if(other.CompareTag("Dead") && !isDead)
{
if(other.gameObject.name == "DeadZone")
@@ -163,6 +168,12 @@ public class PlayerController : MonoBehaviour {
return;
}
if(GameManager.instance != null && GameManager.instance.TryBlockDamageWithItem())
{
StartCoroutine(InvincibleRoutine());
return;
}
bool shouldDie = GameManager.instance.TakeDamage();
if(shouldDie)
+1 -1
View File
@@ -6,7 +6,7 @@ public class ScrollingObject : MonoBehaviour {
private void Update() {
// 게임 오브젝트를 왼쪽으로 일정 속도로 평행 이동하는 처리
if(!GameManager.instance.isGameover)
if(GameManager.instance != null && !GameManager.instance.isGameover && GameFlowManager.IsGameplayActive)
{
transform.Translate(Vector3.left * speed * GameManager.instance.gameSpeed * Time.deltaTime);
}
+85
View File
@@ -3,9 +3,18 @@ using UnityEngine;
// 슬라이딩 장애물 패턴이 켜질 때마다 매달린 장애물 위치를 섞는다.
public class SlideObstaclePattern : MonoBehaviour
{
public enum SlideLayout {
Random,
LeftPair,
CenterPair,
RightPair,
WidePair
}
public Transform[] obstacles; // 위치를 바꿀 고공 장애물들
public float[] xPositions = { -1.65f, -0.55f, 0.55f, 1.65f }; // 선택 가능한 로컬 x 위치
public float minSpacing = 1.5f; // 두 장애물 사이의 최소 간격
public SlideLayout layout = SlideLayout.Random; // 패턴 스포너가 지정하는 슬라이딩 장애물 배치
private void OnEnable()
{
@@ -14,6 +23,12 @@ public class SlideObstaclePattern : MonoBehaviour
return;
}
if(layout != SlideLayout.Random)
{
PlaceLayout(layout);
return;
}
int firstIndex = Random.Range(0, xPositions.Length);
if(obstacles.Length == 1 || xPositions.Length == 1)
@@ -38,6 +53,76 @@ public class SlideObstaclePattern : MonoBehaviour
PlaceObstacle(obstacles[1], rightX);
}
public void ConfigureLayout(SlideLayout nextLayout)
{
layout = nextLayout;
}
private void PlaceLayout(SlideLayout fixedLayout)
{
if(obstacles.Length == 1)
{
PlaceObstacle(obstacles[0], PickSingleX(fixedLayout));
return;
}
float leftX;
float rightX;
switch(fixedLayout)
{
case SlideLayout.LeftPair:
leftX = xPositions[0];
rightX = xPositions[Mathf.Min(2, xPositions.Length - 1)];
break;
case SlideLayout.CenterPair:
leftX = xPositions[Mathf.Min(1, xPositions.Length - 1)];
rightX = xPositions[Mathf.Min(2, xPositions.Length - 1)];
break;
case SlideLayout.RightPair:
leftX = xPositions[Mathf.Max(0, xPositions.Length - 3)];
rightX = xPositions[xPositions.Length - 1];
break;
case SlideLayout.WidePair:
default:
leftX = xPositions[0];
rightX = xPositions[xPositions.Length - 1];
break;
}
if(Mathf.Approximately(leftX, rightX) && xPositions.Length > 1)
{
leftX = xPositions[0];
rightX = xPositions[xPositions.Length - 1];
}
if(leftX > rightX)
{
float temp = leftX;
leftX = rightX;
rightX = temp;
}
PlaceObstacle(obstacles[0], leftX);
PlaceObstacle(obstacles[1], rightX);
}
private float PickSingleX(SlideLayout fixedLayout)
{
switch(fixedLayout)
{
case SlideLayout.LeftPair:
return xPositions[0];
case SlideLayout.CenterPair:
return xPositions[Mathf.Min(1, xPositions.Length - 1)];
case SlideLayout.RightPair:
return xPositions[xPositions.Length - 1];
case SlideLayout.WidePair:
default:
return xPositions[Mathf.Min(2, xPositions.Length - 1)];
}
}
private int PickSecondIndex(int firstIndex)
{
for(int i = 0; i < 12; i++)
+125
View File
@@ -0,0 +1,125 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 3
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 0
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 2
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 1
m_PVRFilteringGaussRadiusAO: 1
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots: []
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a42f98ff13294fa49d287efb9f2beff3
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+125
View File
@@ -0,0 +1,125 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 3
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 0
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 2
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 1
m_PVRFilteringGaussRadiusAO: 1
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots: []
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: aff80e568b654a5d8754f971f666d1a5
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+13
View File
@@ -1591,8 +1591,13 @@ MonoBehaviour:
platformPrefab: {fileID: 1671242928466520, guid: f9fc9d007ca214ed9ad2949a47f59fb5,
type: 3}
count: 3
usePatternCourse: 1
tutorialMode: 1
tutorialSteps: []
stagePatternSteps: []
timeBetSpawnMin: 1.25
timeBetSpawnMax: 2.25
tutorialSpawnInterval: 2.2
yMin: -3.5
yMax: 1.5
--- !u!4 &1798326554
@@ -1643,6 +1648,14 @@ MonoBehaviour:
scoreText: {fileID: 1607182866}
gameoverUI: {fileID: 265399405}
maxLife: 3
initialMileage: 500
grantInitialMileage: 1
saveMileageImmediately: 1
playTutorialGuideSequence: 1
itemInvincibleDuration: 2.2
itemSpeedBoostAmount: 0.35
mileageBonusDuration: 7
mileageBonusMultiplier: 2
gameSpeed: 1
minGameSpeed: 1
speedIncreaseRate: 0.05
+125
View File
@@ -0,0 +1,125 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 3
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 0
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 2
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 1
m_PVRFilteringGaussRadiusAO: 1
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots: []
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b5b58cb3097b46029246676c7008b727
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+125
View File
@@ -0,0 +1,125 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 3
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 0
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 2
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 1
m_PVRFilteringGaussRadiusAO: 1
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots: []
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 67f5f56056654fdcbaeeee78967f8c1d
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+16 -1
View File
@@ -4,6 +4,21 @@
EditorBuildSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Scenes: []
m_Scenes:
- enabled: 1
path: Assets/Sence/Intro.unity
guid: aff80e568b654a5d8754f971f666d1a5
- enabled: 1
path: Assets/Sence/MapPreview.unity
guid: b5b58cb3097b46029246676c7008b727
- enabled: 1
path: Assets/Sence/Main.unity
guid: 7a36f7f921a65924697f4f2e30e7de25
- enabled: 1
path: Assets/Sence/Result.unity
guid: 67f5f56056654fdcbaeeee78967f8c1d
- enabled: 1
path: Assets/Sence/DutyFreeShop.unity
guid: a42f98ff13294fa49d287efb9f2beff3
m_configObjects: {}
m_UseUCBPForAssetBundles: 0
+59 -42
View File
@@ -12,6 +12,22 @@
## 기본 게임 흐름
현재 구현된 화면 흐름은 다음과 같습니다.
```text
Intro
-> MapPreview
-> Main(Game)
-> Result
-> DutyFreeShop
-> MapPreview
-> 다음 맵 진행
```
`Intro`, `MapPreview`, `Result`, `DutyFreeShop`은 실제 `.unity` 씬으로 분리되어 있으며, 게임 본편은 `Main` 씬에서 진행됩니다. 화면 UI는 `GameFlowManager`가 런타임에 공통 오버레이로 생성합니다.
기획상 전체 여행 흐름은 다음 구조를 목표로 합니다.
```text
한국 튜토리얼
-> 인천공항 도착
@@ -232,7 +248,7 @@
## 현재 구현 상태
현재 프로젝트는 기존 단순 러너에서 여행 튜토리얼 러너 형태로 확장 중입니다.
현재 프로젝트는 기존 단순 러너에서 여행형 맵 진행 러너 구조로 확장되었습니다.
반영된 주요 기능은 다음과 같습니다.
@@ -262,58 +278,59 @@
- 오른쪽 상단 점수/마일리지/속도 텍스트 축소 배치
- 공항 튜토리얼용 발판/천장 타일 이미지 추가
- 기존 공항 배경 느낌을 길게 만든 `Sky_AirportLong.png` 추가
- 점프, 2단 점프, 슬라이딩, 강제 피격, 아이템 사용을 순서대로 체험하는 튜토리얼 코스
- 튜토리얼 상황에 맞는 발판/장애물/아이템 배치
- `ItemPickup` 기반 필드 아이템 획득
- 초밥, 라멘, 방어막, 운동화, 마일리지 카드 효과
- 피격 방어 아이템 처리
- `MapDefinition`, `MapDatabase`, `MapId` 기반 맵 데이터 구조
- `Tutorial / Incheon`, `Japan`, `China`, `America` 맵 등록
- 맵별 목표 거리, 제한 시간, 보상 마일리지, 다음 맵 해금 정보
- 맵별 시작 패턴과 반복 패턴 분리
- `GameSaveData`, `GameSaveManager` 기반 저장/로드
- 공용 마일리지, 해금 맵, 현재 맵, 구매 아이템 수량 저장
- `GameFlowManager` 기반 전체 화면 흐름 관리
- `Intro`, `MapPreview`, `Result`, `DutyFreeShop` 씬 추가
- Build Settings 시작 씬을 `Intro`로 구성
- 게임 시작 전에는 플랫폼 스폰과 플레이어 입력이 멈추도록 처리
- 맵 클리어 시 결과 화면, 보상 지급, 다음 맵 해금, 면세점 이동 처리
- 면세점에서 도시락, 방어막, 운동화, 마일리지 카드 구매 가능
최근 커밋 기준:
## 현재 남은 작업
```text
d4ab621 Add airport tutorial assets and long background
```
현재 가장 중요한 남은 작업은 면세점 구매 아이템을 실제 플레이 준비/사용 구조와 연결하는 것입니다.
## 현재 확인된 문제
가장 먼저 확인해야 할 문제는 배경입니다.
의도한 방향은 기존 공항 사진 느낌의 `Sky.png`를 바탕으로, 반복이 덜 보이도록 긴 배경 이미지 `Assets/Sprites/Backgrounds/Sky_AirportLong.png`를 사용하는 것입니다.
디스크의 `Assets/Sence/Main.unity`에는 다음 설정이 들어가 있습니다.
- `Background/Sky``Background/Sky (1)`가 긴 배경 스프라이트를 참조
- 배경 한 장의 크기: `81.92 x 10.24`
- 두 번째 배경 위치: `x = 81.92`
- 두 배경을 루프시키기 위해 `BackgroundLoop` 사용
하지만 Unity 에디터에서 열려 있는 씬 메모리가 외부에서 수정한 씬 파일을 즉시 다시 읽지 않아, 실제 플레이 화면에서는 배경이 제대로 보이지 않는 문제가 확인되었습니다.
다음 스레드에서 먼저 할 일:
1. Unity에서 `Main` 씬을 다시 열어 디스크의 씬 변경 사항을 에디터에 반영한다.
2. `Background/Sky`, `Background/Sky (1)`의 Sprite가 `Sky_AirportLong`인지 확인한다.
3. 두 오브젝트의 BoxCollider2D 크기가 `81.92 x 10.24`인지 확인한다.
4. `Background/Sky (1)`의 위치가 `x = 81.92`인지 확인한다.
5. 게임 뷰에서 배경이 보이는지 캡처로 확인한다.
- 면세점 구매 수량은 저장되지만, 아직 스테이지 시작 전 장착 슬롯과 연결되지 않았습니다.
- 도시락, 방어막, 운동화, 마일리지 카드를 플레이 시작 전에 선택하고 소모하는 UI가 필요합니다.
- 가방 업그레이드와 아이템 슬롯 확장 기능은 아직 실제 구매 상품으로 연결되지 않았습니다.
- 맵별 배경, 발판, 장애물, 마일리지 토큰 스킨 교체는 아직 `MapDefinition`과 연결되지 않았습니다.
- `Intro`, `MapPreview`, `Result`, `DutyFreeShop` 씬은 현재 얇은 컨테이너이며, 실제 화면 UI는 런타임 오버레이로 생성됩니다.
- 기념품 수집 화면과 클리어 보상 수집 데이터는 아직 구현 전입니다.
- 일본, 중국, 미국 맵은 데이터상 분리되어 있지만, 시각적 지역 차이는 아직 적용 전입니다.
## 다음 개발 우선순위
1. 배경 표시 문제 해
2. 공항 발판/천장 타일의 크기와 콜라이더 최종 점검
3. 낮은 장애물은 점프, 매달린 표지판은 슬라이딩으로 명확히 구분
4. 마일리지 토큰 크기, 위치, 콜라이더 재점검
5. 튜토리얼 패턴의 간격과 난이도 조정
6. 목표 거리와 제한 시간 기반 클리어 구조 추가
7. 면세점 화면 실제 UI 구현
8. 구매 아이템의 보유/사용 구조 구현
9. 일본 1스테이지용 패턴과 에셋 적용
10. 기념품 수집 화면 기초 구현
1. 면세점 구매 아이템을 스테이지 시작 전 장착 슬롯과 연
2. 장착 아이템의 실제 소모 타이밍과 효과 정리
3. 가방 업그레이드로 아이템 슬롯 1칸에서 최대 3칸까지 확장
4. 맵별 배경/발판/장애물/마일리지 스킨을 `MapDefinition`에 연결
5. 일본 맵의 실제 지역 에셋과 반복 패턴 조정
6. 중국, 미국 맵 난이도와 패턴 차별화
7. 결과 화면 실제 디자인 UI로 정리
8. 면세점 화면을 실제 상품 진열 UI로 정리
9. 기념품 보상 데이터와 수집 화면 기초 구현
10. 맵 선택/해금 상태 화면 고도화
## 새 스레드 인수인계 메모
새 Codex 글에서 이어갈 때는 이 순서로 보면 됩니다.
1. `README.md``현재 확인된 문제` 먼저 읽는다.
2. `desing.md``최신 구현용 에셋 상태``튜토리얼 배경 최신 결정`을 확인한다.
3. Unity 브리지가 연결되면 `Background/Sky``Background/Sky (1)`의 현재 에디터 상태를 확인한다.
4. 에디터 상태가 씬 파일과 다르면 Unity에서 `Main` 씬을 다시 열거나, 에디터에서 배경 오브젝트 값을 직접 맞춘다.
5. 배경이 해결된 뒤에 발판/장애물/마일리지 위치 조정을 진행한다.
1. `README.md``현재 구현 상태``현재 남은 작업` 먼저 읽는다.
2. `desing.md``최신 구현용 에셋 상태``다음 이미지/시각 작업`을 확인한다.
3. `Assets/Scripts/Flow/GameFlowManager.cs`에서 화면 전환 흐름을 확인한다.
4. `Assets/Scripts/Maps/MapDatabase.cs`에서 맵별 코스, 제한 시간, 보상 데이터를 확인한다.
5. `Assets/Scripts/PlatformSpawner.cs``Assets/Scripts/Platform.cs`에서 튜토리얼 코스 배치 방식을 확인한다.
6. 다음 작업은 면세점 구매 아이템을 장착 슬롯과 실제 플레이 효과에 연결하는 것이다.
## 최종 목표
+49 -15
View File
@@ -445,10 +445,41 @@ UI 아이콘은 텍스트 없이 의미가 읽히는 심볼 중심으로 사용
- 두 오브젝트 모두 BoxCollider2D 크기는 `81.92 x 10.24`
- 두 오브젝트 모두 `BackgroundLoop`로 왼쪽 이동 후 오른쪽으로 재배치
현재 확인된 문제:
현재 상태:
- 디스크의 `Main.unity`에는 긴 배경 설정이 들어가 있으나, Unity 에디터가 열린 씬 메모리에 외부 변경을 즉시 반영하지 않아 플레이 화면에서 배경이 제대로 나오지 않을 수 있다.
- 새 스레드에서 가장 먼저 Unity의 실제 에디터 상태와 씬 파일 상태가 같은지 확인해야 한다.
- 공항 배경 적용 문제는 해결되었다.
- `Main.unity`는 공항 튜토리얼 배경과 튜토리얼 코스 플레이에 사용한다.
- `Intro`, `MapPreview`, `Result`, `DutyFreeShop` 씬이 추가되었고, 각 화면의 UI는 `GameFlowManager`가 런타임 오버레이로 생성한다.
- 추후 맵별 배경을 적용할 때는 `MapDefinition`에 배경/스킨 정보를 추가하고, `GameManager.BeginMap()` 또는 별도 맵 비주얼 매니저에서 교체하는 방식이 적합하다.
### 현재 런타임 화면 구조
실제 씬 구성:
- `Assets/Sence/Intro.unity`
- `Assets/Sence/MapPreview.unity`
- `Assets/Sence/Main.unity`
- `Assets/Sence/Result.unity`
- `Assets/Sence/DutyFreeShop.unity`
현재 화면 흐름:
```text
Intro
-> MapPreview
-> Main(Game)
-> Result
-> DutyFreeShop
-> MapPreview
```
디자인 관점에서 남은 화면 작업:
- `Intro`: 타이틀, 여행 시작, 저장 이어하기, 새 여행 선택 화면으로 고도화
- `MapPreview`: 다음 여행지, 목표 거리, 제한 시간, 해금 상태를 보여주는 출발 보드로 고도화
- `Result`: 클리어/실패, 획득 마일리지, 보상, 다음 행동 버튼을 결과표처럼 정리
- `DutyFreeShop`: 상품 진열, 가격표, 보유 수량, 장착 슬롯을 실제 상점 UI로 정리
- `Main`: 맵별 배경/발판/장애물/마일리지 토큰 스킨 적용
## 튜토리얼 배경 최신 결정
@@ -465,13 +496,15 @@ UI 아이콘은 텍스트 없이 의미가 읽히는 심볼 중심으로 사용
다음에 이어서 할 시각 작업은 다음 순서가 좋다.
1. 배경 표시 문제를 먼저 해결한다.
2. 배경이 정상 표시되면 밝기와 대비를 낮춰 눈의 피로를 줄인다.
3. 발판과 천장 타일의 크기, 반복감, 콜라이더 위치를 맞춘다.
4. 캐리어 장애물은 점프용이라는 인상이 더 강하게 보이도록 정리한다.
5. 매달린 표지판 장애물 슬라이딩용이라는 인상이 더 강하게 보이도록 정리한다.
6. 마일리지 토큰은 크기를 줄이고, 플레이어가 지나가는 루트에 맞춰 배치한다.
7. 여권 HUD는 현재 부품을 유지하되 실제 아이템 보유 상태와 연결한다.
1. 면세점 화면을 실제 상품 진열 UI로 정리한다.
2. 장착 아이템 슬롯 UI를 여권 HUD 또는 면세점 화면과 연결한다.
3. 결과 화면을 여행 정산표처럼 정리한다.
4. 맵 미리보기 화면에 국가별 분위기와 출발 정보를 넣는다.
5. 일본 맵용 배경, 발판, 낮은 장애물, 슬라이딩 장애물 스킨을 우선 적용한다.
6. 중국, 미국 맵용 스킨은 같은 충돌 크기를 유지하면서 Sprite만 교체할 수 있게 준비한다.
7. 마일리지 토큰은 맵별 Sprite를 사용하되, 게임 중 가독성이 유지되는 크기로 맞춘다.
8. 여권 HUD는 현재 부품을 유지하되 실제 아이템 보유 상태와 연결한다.
9. 기념품 수집 화면은 냉장고 자석 또는 여권 페이지 콘셉트 중 하나로 결정한다.
## 새 스레드 인수인계 메모
@@ -479,8 +512,9 @@ UI 아이콘은 텍스트 없이 의미가 읽히는 심볼 중심으로 사용
새 스레드에서 이어갈 때는 다음을 먼저 확인한다.
- `README.md``현재 확인된 문제`
- 이 파일의 `긴 공항 배경 에셋`
- `Assets/Sence/Main.unity``Background/Sky`, `Background/Sky (1)` 설정
- Unity 에디터에서 실제로 보이는 배경 상태
- `Captures/latest.png` 또는 새 게임 뷰 캡처
- `README.md``현재 구현 상태``현재 남은 작업`
- 이 파일의 `현재 런타임 화면 구조`
- `Assets/Scripts/Flow/GameFlowManager.cs`의 화면 전환 구조
- `Assets/Scripts/Maps/MapDatabase.cs`의 맵별 데이터
- `Assets/Scripts/GameManager.cs`의 맵 시작/클리어/실패 처리
- 다음 우선순위는 면세점 구매 아이템을 실제 장착/소모 구조와 연결하는 것이다.