Add travel map flow and update docs
This commit is contained in:
@@ -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: []
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b0f46aa63d14f40a4976dc22e6710a8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9cd5d89d88554d7ea48124a4f1780e0b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24f0b6a8d1474f81a6d1a3b9a07e5c43
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b552d8cc0244e64bcf24dfac5a7df5e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 194fc0ddfc534d08801e82235a731527
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 65f34cfe92f647c39ff63111a09a3da8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,6 @@
|
||||
public enum MapId {
|
||||
TutorialIncheon = 0,
|
||||
Japan = 1,
|
||||
China = 2,
|
||||
America = 3
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c10415d13fe84f39923b0f88bce0d90c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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: []
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a42f98ff13294fa49d287efb9f2beff3
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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: []
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aff80e568b654a5d8754f971f666d1a5
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -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: []
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5b58cb3097b46029246676c7008b727
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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: []
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 67f5f56056654fdcbaeeee78967f8c1d
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -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. 다음 작업은 면세점 구매 아이템을 장착 슬롯과 실제 플레이 효과에 연결하는 것이다.
|
||||
|
||||
## 최종 목표
|
||||
|
||||
|
||||
@@ -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`의 맵 시작/클리어/실패 처리
|
||||
- 다음 우선순위는 면세점 구매 아이템을 실제 장착/소모 구조와 연결하는 것이다.
|
||||
|
||||
Reference in New Issue
Block a user