diff --git a/Assets/ConceptArt/duty_free_shop_screen_concept_01.png b/Assets/ConceptArt/duty_free_shop_screen_concept_01.png new file mode 100644 index 0000000..12f1bc2 Binary files /dev/null and b/Assets/ConceptArt/duty_free_shop_screen_concept_01.png differ diff --git a/Assets/ConceptArt/duty_free_shop_screen_concept_01.png.meta b/Assets/ConceptArt/duty_free_shop_screen_concept_01.png.meta new file mode 100644 index 0000000..8fb3936 --- /dev/null +++ b/Assets/ConceptArt/duty_free_shop_screen_concept_01.png.meta @@ -0,0 +1,100 @@ +fileFormatVersion: 2 +guid: 9c5d2e13a8f2417bb6a94e31c8d427f5 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 80 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + swizzle: 50462976 + cookieLightType: 1 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Platform.prefab b/Assets/Prefabs/Platform.prefab index 1bf9c4a..c708c32 100644 --- a/Assets/Prefabs/Platform.prefab +++ b/Assets/Prefabs/Platform.prefab @@ -451,6 +451,10 @@ MonoBehaviour: - {fileID: 1166831543635676} - {fileID: 1987654321098765} emptyPatternWeight: 2 + mileageSprite: {fileID: 21300000, guid: 8b2f1d0c3e4a5b6f9d7e0123456789ab, type: 3} + mileagePatternChance: 50 + smallMileageValue: 10 + bonusMileageValue: 30 --- !u!1 &1832961231703714 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/GameManager.cs b/Assets/Scripts/GameManager.cs index 53b4b07..b8e302b 100644 --- a/Assets/Scripts/GameManager.cs +++ b/Assets/Scripts/GameManager.cs @@ -13,8 +13,16 @@ public class GameManager : MonoBehaviour { private int score = 0; // 게임 점수 private int currentLife; // 현재 남은 목숨 + private int stageMileage = 0; // 현재 플레이 중 획득한 마일리지 + private int totalMileage = 0; // 누적 보유 마일리지 public int maxLife = 3; // 시작 목숨 + public int initialMileage = 500; // 첫 시작시 지급할 초기 마일리지 + public bool grantInitialMileage = true; // 초기 마일리지 지급 여부 + public bool saveMileageImmediately = true; // 스테이지 정산 전까지는 획득 즉시 누적한다. + + private const string TotalMileageKey = "UniRun.TotalMileage"; + private const string InitialMileageGrantedKey = "UniRun.InitialMileageGranted"; // 게임의 스피드를 올리는 변수 public float gameSpeed = 1f; @@ -44,6 +52,16 @@ public class GameManager : MonoBehaviour { private void Start() { currentLife = maxLife; + totalMileage = PlayerPrefs.GetInt(TotalMileageKey, 0); + + if(grantInitialMileage && PlayerPrefs.GetInt(InitialMileageGrantedKey, 0) == 0) + { + totalMileage += initialMileage; + PlayerPrefs.SetInt(TotalMileageKey, totalMileage); + PlayerPrefs.SetInt(InitialMileageGrantedKey, 1); + PlayerPrefs.Save(); + } + UpdateScoreUI(); } @@ -75,6 +93,26 @@ public class GameManager : MonoBehaviour { } + // 마일리지 토큰을 먹었을 때 호출되는 메서드 + public void AddMileage(int amount) { + if(isGameover) + { + return; + } + + stageMileage += amount; + score += Mathf.Max(1, amount / 10); + + if(saveMileageImmediately) + { + totalMileage += amount; + PlayerPrefs.SetInt(TotalMileageKey, totalMileage); + PlayerPrefs.Save(); + } + + UpdateScoreUI(); + } + // 플레이어가 장애물에 부딪혔을 때 목숨과 속도를 줄이는 메서드 public bool TakeDamage(int damage = 1) { if(isGameover) @@ -100,6 +138,10 @@ public class GameManager : MonoBehaviour { } private void UpdateScoreUI() { - scoreText.text = "Score : " + score + "\nLife : " + currentLife + "/" + maxLife + "\nSpeed : " + gameSpeed.ToString("0.0"); + scoreText.text = "Score : " + score + + "\nLife : " + currentLife + "/" + maxLife + + "\nMileage : " + stageMileage + + "\nTotal : " + totalMileage + + "\nSpeed : " + gameSpeed.ToString("0.0"); } } diff --git a/Assets/Scripts/MileagePickup.cs b/Assets/Scripts/MileagePickup.cs new file mode 100644 index 0000000..b9b5061 --- /dev/null +++ b/Assets/Scripts/MileagePickup.cs @@ -0,0 +1,55 @@ +using UnityEngine; + +// 플레이어가 먹을 수 있는 마일리지 토큰 +public class MileagePickup : MonoBehaviour +{ + public int mileageValue = 10; // 획득 마일리지 + + private bool collected = false; // 이미 획득했는지 여부 + private SpriteRenderer spriteRenderer; + private Collider2D pickupCollider; + + private void Awake() + { + spriteRenderer = GetComponent(); + pickupCollider = GetComponent(); + } + + public void Setup(Sprite sprite, int value) + { + mileageValue = value; + + if(spriteRenderer != null) + { + spriteRenderer.sprite = sprite; + } + } + + public void ResetPickup() + { + collected = false; + gameObject.SetActive(true); + + if(pickupCollider != null) + { + pickupCollider.enabled = true; + } + } + + private void OnTriggerEnter2D(Collider2D other) + { + if(collected || !other.CompareTag("Player")) + { + return; + } + + collected = true; + + if(GameManager.instance != null) + { + GameManager.instance.AddMileage(mileageValue); + } + + gameObject.SetActive(false); + } +} diff --git a/Assets/Scripts/MileagePickup.cs.meta b/Assets/Scripts/MileagePickup.cs.meta new file mode 100644 index 0000000..52af1e0 --- /dev/null +++ b/Assets/Scripts/MileagePickup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d68f1d3b0a9e4aef9b0a6d8f9c2e1a44 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Platform.cs b/Assets/Scripts/Platform.cs index af6b87c..06a43a0 100644 --- a/Assets/Scripts/Platform.cs +++ b/Assets/Scripts/Platform.cs @@ -1,34 +1,49 @@ -using UnityEngine; +using UnityEngine; // 발판으로서 필요한 동작을 담은 스크립트 public class Platform : MonoBehaviour { public GameObject[] obstacles; // 장애물 오브젝트들 public int emptyPatternWeight = 2; // 장애물이 없는 패턴이 선택될 가중치 + public Sprite mileageSprite; // 이 발판에서 사용할 마일리지 토큰 스프라이트 + public int mileagePatternChance = 50; // 마일리지 패턴 등장 확률 + public int smallMileageValue = 10; // 일반 토큰 가치 + public int bonusMileageValue = 30; // 어려운 루트 보너스 토큰 가치 + private bool stepped = false; // 플레이어 캐릭터가 밟았었는가 + private MileagePickup[] mileagePickups; // 런타임에 생성하는 마일리지 토큰 풀 + private const int MileagePoolSize = 5; // 컴포넌트가 활성화될때 마다 매번 실행되는 메서드 private void OnEnable() { // 발판을 리셋하는 처리 stepped = false; + EnsureMileagePickups(); for(int i = 0; i < obstacles.Length; i++) { obstacles[i].SetActive(false); } + HideMileagePickups(); + if(obstacles.Length == 0) { + TryPlaceMileagePattern(null); return; } // 낮은 장애물과 높은 장애물이 동시에 켜져 불가능한 배치가 되지 않도록 하나만 선택한다. int patternIndex = Random.Range(0, obstacles.Length + emptyPatternWeight); + GameObject activeObstacle = null; if(patternIndex < obstacles.Length) { - obstacles[patternIndex].SetActive(true); + activeObstacle = obstacles[patternIndex]; + activeObstacle.SetActive(true); } + + TryPlaceMileagePattern(activeObstacle); } void OnCollisionEnter2D(Collision2D collision) { @@ -39,4 +54,127 @@ public class Platform : MonoBehaviour { GameManager.instance.AddScore(1); } } + + private void EnsureMileagePickups() { + if(mileagePickups != null && mileagePickups.Length == MileagePoolSize) + { + return; + } + + mileagePickups = new MileagePickup[MileagePoolSize]; + + for(int i = 0; i < MileagePoolSize; i++) + { + GameObject pickupObject = new GameObject("Mileage Pickup " + (i + 1)); + pickupObject.transform.SetParent(transform); + pickupObject.transform.localScale = Vector3.one; + + SpriteRenderer spriteRenderer = pickupObject.AddComponent(); + spriteRenderer.sprite = mileageSprite; + spriteRenderer.sortingLayerName = "Foreground"; + spriteRenderer.sortingOrder = 1; + + CircleCollider2D pickupCollider = pickupObject.AddComponent(); + pickupCollider.isTrigger = true; + pickupCollider.radius = 0.35f; + + mileagePickups[i] = pickupObject.AddComponent(); + mileagePickups[i].Setup(mileageSprite, smallMileageValue); + + pickupObject.SetActive(false); + } + } + + private void HideMileagePickups() { + if(mileagePickups == null) + { + return; + } + + for(int i = 0; i < mileagePickups.Length; i++) + { + if(mileagePickups[i] != null) + { + mileagePickups[i].gameObject.SetActive(false); + } + } + } + + private void TryPlaceMileagePattern(GameObject activeObstacle) { + if(mileageSprite == null || mileagePickups == null || Random.Range(0, 100) >= mileagePatternChance) + { + return; + } + + if(activeObstacle == null) + { + PlaceSafeMileageLine(); + return; + } + + if(activeObstacle.name.Contains("Slide")) + { + PlaceSlideMileageLine(); + } + else + { + PlaceJumpMileageArc(activeObstacle.transform.localPosition.x); + } + } + + private void PlaceSafeMileageLine() { + Vector2[] positions = { + new Vector2(-2f, 2.25f), + new Vector2(-1f, 2.25f), + new Vector2(0f, 2.25f), + new Vector2(1f, 2.25f), + new Vector2(2f, 2.25f) + }; + + PlaceMileageTokens(positions, smallMileageValue); + } + + private void PlaceJumpMileageArc(float obstacleX) { + Vector2[] positions = { + new Vector2(obstacleX - 1.2f, 2.7f), + new Vector2(obstacleX, 3.45f), + new Vector2(obstacleX + 1.2f, 2.7f), + new Vector2(obstacleX + 1.9f, 2.25f) + }; + + PlaceMileageTokens(positions, smallMileageValue); + SetPickupValue(1, bonusMileageValue); + } + + private void PlaceSlideMileageLine() { + Vector2[] positions = { + new Vector2(-1.5f, 1.45f), + new Vector2(-0.5f, 1.45f), + new Vector2(0.5f, 1.45f), + new Vector2(1.5f, 1.45f) + }; + + PlaceMileageTokens(positions, smallMileageValue); + SetPickupValue(2, bonusMileageValue); + } + + private void PlaceMileageTokens(Vector2[] positions, int value) { + int count = Mathf.Min(positions.Length, mileagePickups.Length); + + for(int i = 0; i < count; i++) + { + mileagePickups[i].Setup(mileageSprite, value); + mileagePickups[i].transform.localPosition = positions[i]; + mileagePickups[i].ResetPickup(); + } + } + + private void SetPickupValue(int index, int value) { + if(index < 0 || index >= mileagePickups.Length || !mileagePickups[index].gameObject.activeSelf) + { + return; + } + + mileagePickups[index].Setup(mileageSprite, value); + } } diff --git a/desing.md b/desing.md index dd7a84d..bbbd658 100644 --- a/desing.md +++ b/desing.md @@ -311,3 +311,22 @@ UI 아이콘은 텍스트 없이 의미가 읽히는 심볼 중심으로 사용 - `Assets/ConceptArt/stage_background_concept_01.png` - `Assets/ConceptArt/stage_background_panels_preview.png` + +## 채택한 면세점 화면 콘셉트 + +면세점은 스테이지 사이에서 공용 마일리지로 준비 아이템을 구매하는 화면이다. 화면의 중심은 상품 진열과 구매이며, 다음 맵 이동은 오른쪽 표지판으로만 안내한다. + +화면 구성: + +- 왼쪽/중앙: 도시락, 운동화, 가방, 방어막 같은 구매 상품 진열 +- 상단: 보유 공용 마일리지 표시 +- 오른쪽: 비행기 아이콘과 오른쪽 방향성을 가진 다음 맵 이동 표지판 +- 왼쪽 표지판은 배치하지 않는다. +- 큰 비행기 자체를 화면 주인공으로 두지 않고, 비행기는 표지판 아이콘으로만 사용한다. +- 상품 가격은 지역 화폐가 아니라 공용 마일리지 하나로 통일한다. +- 도시락은 목숨 +1 회복이 바로 읽히도록 하트와 +1 느낌을 사용한다. +- 가방은 기본 1칸에서 최대 3칸까지 확장되는 아이템 슬롯 업그레이드로 표현한다. + +컨셉/검토 파일: + +- `Assets/ConceptArt/duty_free_shop_screen_concept_01.png`