using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif // 발판으로서 필요한 동작을 담은 스크립트 public class Platform : MonoBehaviour { public enum PlatformPattern { Random, Empty, LowLeft, LowMid, LowRight, Slide, ForceHit } public enum MileagePattern { Random, None, SafeLine, JumpArc, SlideLine } public enum ItemPattern { None, HealthSushi, InvincibleRamen, Shield, SpeedShoes, MileageCard } public enum PlatformSkin { Airport, Japan, China, USA } 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 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 PlatformSkin reservedSkin = PlatformSkin.Airport; // 현재 맵에서 사용할 발판/장애물 스킨 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; private const float JumpMileageMinWidthScale = 1.8f; private const float JumpMileageMaxWidthScale = 2.5f; public void ConfigureForTutorial( PlatformPattern pattern, MileagePattern mileagePattern, SlideObstaclePattern.SlideLayout slideLayout = SlideObstaclePattern.SlideLayout.Random, ItemPattern itemPattern = ItemPattern.None, PlatformSkin platformSkin = PlatformSkin.Airport) { reservedPattern = pattern; reservedMileagePattern = mileagePattern; reservedSlideLayout = slideLayout; reservedItemPattern = itemPattern; reservedSkin = platformSkin; } public void ConfigureForRandom(PlatformSkin platformSkin = PlatformSkin.Airport) { reservedPattern = PlatformPattern.Random; reservedMileagePattern = MileagePattern.Random; reservedSlideLayout = SlideObstaclePattern.SlideLayout.Random; reservedItemPattern = ItemPattern.None; reservedSkin = platformSkin; } // 컴포넌트가 활성화될때 마다 매번 실행되는 메서드 private void OnEnable() { // 발판을 리셋하는 처리 stepped = false; ApplyVisualSkin(); EnsureMileagePickups(); EnsureItemPickups(); for(int i = 0; i < obstacles.Length; i++) { obstacles[i].SetActive(false); } HideMileagePickups(); HideItemPickups(); if(reservedPattern != PlatformPattern.Random) { ApplyReservedPattern(); return; } if(obstacles.Length == 0) { TryPlaceMileagePattern(null); return; } // 낮은 장애물과 높은 장애물이 동시에 켜져 불가능한 배치가 되지 않도록 하나만 선택한다. int patternIndex = Random.Range(0, obstacles.Length + emptyPatternWeight); GameObject activeObstacle = null; if(patternIndex < obstacles.Length) { activeObstacle = obstacles[patternIndex]; ConfigureSlideObstacle(activeObstacle); activeObstacle.SetActive(true); } TryPlaceMileagePattern(activeObstacle); } void OnCollisionEnter2D(Collision2D collision) { // 플레이어 캐릭터가 자신을 밟았을때 점수를 추가하는 처리 if(collision.collider.tag == "Player" && !stepped && GameFlowManager.IsGameplayActive) { stepped = true; 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 = GetParentScaleCompensatedVector(MileageTokenScale); SpriteRenderer spriteRenderer = pickupObject.AddComponent(); spriteRenderer.sprite = mileageSprite; spriteRenderer.sortingLayerName = "Foreground"; spriteRenderer.sortingOrder = 1; CircleCollider2D pickupCollider = pickupObject.AddComponent(); pickupCollider.isTrigger = true; pickupCollider.radius = MileageColliderRadius; mileagePickups[i] = pickupObject.AddComponent(); mileagePickups[i].Setup(mileageSprite, smallMileageValue); pickupObject.SetActive(false); } } 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.sortingLayerName = "Foreground"; spriteRenderer.sortingOrder = 2; CircleCollider2D pickupCollider = pickupObject.AddComponent(); pickupCollider.isTrigger = true; pickupCollider.radius = ItemColliderRadius; itemPickups[i] = pickupObject.AddComponent(); 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 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) { return; } if(activeObstacle == null) { PlaceSafeMileageLine(); return; } if(activeObstacle.name.Contains("Slide")) { PlaceSlideMileageLine(); } else { PlaceJumpMileageArc(GetWorldRelativeX(activeObstacle.transform)); } } private void ApplyReservedPattern() { GameObject activeObstacle = null; switch(reservedPattern) { case PlatformPattern.LowLeft: activeObstacle = FindObstacle("Obstacle Left"); break; case PlatformPattern.LowMid: activeObstacle = FindObstacle("Obstacle Mid"); break; case PlatformPattern.LowRight: activeObstacle = FindObstacle("Obstacle Right"); break; 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(reservedPattern != PlatformPattern.ForceHit && activeObstacle != null) { 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(); if(slideObstaclePattern != null) { slideObstaclePattern.ConfigureLayout(reservedSlideLayout); } } private GameObject FindObstacle(string obstacleName) { for(int i = 0; i < obstacles.Length; i++) { if(obstacles[i] != null && obstacles[i].name == obstacleName) { return obstacles[i]; } } return null; } private void PlaceReservedMileagePattern(GameObject activeObstacle) { if(mileageSprite == null || mileagePickups == null || reservedMileagePattern == MileagePattern.None) { return; } if(reservedMileagePattern == MileagePattern.SafeLine) { PlaceSafeMileageLine(); return; } if(reservedMileagePattern == MileagePattern.JumpArc) { float obstacleX = activeObstacle != null ? GetWorldRelativeX(activeObstacle.transform) : 0f; PlaceJumpMileageArc(obstacleX); return; } if(reservedMileagePattern == MileagePattern.SlideLine) { PlaceSlideMileageLine(); return; } 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) { const float frontX = -1.45f; switch(itemPattern) { case ItemPattern.HealthSushi: return new Vector2(frontX, 1.55f); case ItemPattern.InvincibleRamen: return new Vector2(frontX, 1.55f); case ItemPattern.Shield: return new Vector2(frontX, 1.55f); case ItemPattern.SpeedShoes: return new Vector2(frontX, 1.55f); case ItemPattern.MileageCard: return new Vector2(frontX, 1.55f); default: return new Vector2(0f, 1.55f); } } private void ApplyVisualSkin() { #if UNITY_EDITOR Sprite platformSprite = LoadSpriteAtPath(GetPlatformSkinPath(reservedSkin)); Sprite ceilingSprite = LoadSpriteAtPath(GetCeilingSkinPath(reservedSkin)); Sprite lowObstacleSprite = LoadSpriteAtPath(GetLowObstacleSkinPath(reservedSkin)); Sprite hangingObstacleSprite = LoadSpriteAtPath(GetHangingObstacleSkinPath(reservedSkin)); Sprite mileageSkinSprite = LoadSpriteAtPath(GetMileageSkinPath(reservedSkin)); ApplySprite(GetComponent(), platformSprite); Transform ceilingPlatform = FindChildRecursive(transform, "Ceiling Platform"); if(ceilingPlatform != null) { ApplySprite(ceilingPlatform.GetComponent(), ceilingSprite != null ? ceilingSprite : platformSprite); } ApplyLowObstacleSkin(lowObstacleSprite); ApplyHangingObstacleSkin(hangingObstacleSprite); if(mileageSkinSprite != null) { mileageSprite = mileageSkinSprite; } #endif } #if UNITY_EDITOR private void ApplyLowObstacleSkin(Sprite obstacleSprite) { if(obstacleSprite == null || obstacles == null) { return; } for(int i = 0; i < obstacles.Length; i++) { GameObject obstacle = obstacles[i]; if(obstacle == null || obstacle.name.Contains("Slide")) { continue; } ApplySprite(obstacle.GetComponent(), obstacleSprite); } } private void ApplyHangingObstacleSkin(Sprite obstacleSprite) { if(obstacleSprite == null) { return; } GameObject slideObstacle = FindObstacle("Slide Obstacle Pattern"); if(slideObstacle == null) { return; } SlideObstaclePattern slideObstaclePattern = slideObstacle.GetComponent(); if(slideObstaclePattern == null || slideObstaclePattern.obstacles == null) { return; } for(int i = 0; i < slideObstaclePattern.obstacles.Length; i++) { Transform hangingObstacle = slideObstaclePattern.obstacles[i]; if(hangingObstacle != null) { ApplySprite(hangingObstacle.GetComponent(), obstacleSprite); } } } private void ApplySprite(SpriteRenderer spriteRenderer, Sprite sprite) { if(spriteRenderer != null && sprite != null) { spriteRenderer.sprite = sprite; } } private Sprite LoadSpriteAtPath(string assetPath) { if(string.IsNullOrEmpty(assetPath)) { return null; } return AssetDatabase.LoadAssetAtPath(assetPath); } private Transform FindChildRecursive(Transform parent, string childName) { if(parent == null) { return null; } for(int i = 0; i < parent.childCount; i++) { Transform child = parent.GetChild(i); if(child.name == childName) { return child; } Transform match = FindChildRecursive(child, childName); if(match != null) { return match; } } return null; } private string GetPlatformSkinPath(PlatformSkin platformSkin) { switch(platformSkin) { case PlatformSkin.Japan: return "Assets/Sprites/ObstacleSkins/Skin_Platform_Japan.png"; case PlatformSkin.China: return "Assets/Sprites/ObstacleSkins/Skin_Platform_China.png"; case PlatformSkin.USA: return "Assets/Sprites/ObstacleSkins/Skin_Platform_USA.png"; case PlatformSkin.Airport: default: return "Assets/Sprites/ObstacleSkins/Skin_Platform_AirportFloorTile.png"; } } private string GetCeilingSkinPath(PlatformSkin platformSkin) { if(platformSkin == PlatformSkin.Airport) { return "Assets/Sprites/ObstacleSkins/Skin_Platform_AirportCeiling.png"; } return GetPlatformSkinPath(platformSkin); } private string GetLowObstacleSkinPath(PlatformSkin platformSkin) { switch(platformSkin) { case PlatformSkin.Japan: return "Assets/Sprites/ObstacleSkins/Skin_Obstacle_StoneLantern.png"; case PlatformSkin.China: return "Assets/Sprites/ObstacleSkins/Skin_Obstacle_MarketCrate.png"; case PlatformSkin.USA: return "Assets/Sprites/ObstacleSkins/Skin_Obstacle_Cone.png"; case PlatformSkin.Airport: default: return "Assets/Sprites/ObstacleSkins/Skin_Obstacle_Suitcases.png"; } } private string GetHangingObstacleSkinPath(PlatformSkin platformSkin) { switch(platformSkin) { case PlatformSkin.Japan: return "Assets/Sprites/ObstacleSkins/Skin_Hanging_RoadSign.png"; case PlatformSkin.China: return "Assets/Sprites/ObstacleSkins/Skin_Hanging_RedLanterns.png"; case PlatformSkin.USA: return "Assets/Sprites/ObstacleSkins/Skin_Hanging_Banner.png"; case PlatformSkin.Airport: default: return "Assets/Sprites/ObstacleSkins/Skin_Hanging_AirportSign.png"; } } private string GetMileageSkinPath(PlatformSkin platformSkin) { switch(platformSkin) { case PlatformSkin.Japan: return "Assets/Sprites/Mileage/Mileage_Japan.png"; case PlatformSkin.China: return "Assets/Sprites/Mileage/Mileage_China.png"; case PlatformSkin.USA: return "Assets/Sprites/Mileage/Mileage_USA.png"; case PlatformSkin.Airport: default: return "Assets/Sprites/Mileage/Mileage_Korea.png"; } } #endif 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(assetPath); #else return null; #endif } private void PlaceSafeMileageLine() { Vector2[] positions = { new Vector2(-2f, 1.35f), new Vector2(-1f, 1.35f), new Vector2(0f, 1.35f), new Vector2(1f, 1.35f), new Vector2(2f, 1.35f) }; PlaceMileageTokens(positions, smallMileageValue); } private void PlaceJumpMileageArc(float obstacleX) { float widthScale = GetJumpMileageArcWidthScale(); Vector2[] positions = { new Vector2(obstacleX - 2.25f * widthScale, 1.88f), new Vector2(obstacleX - 1.15f * widthScale, 2.88f), new Vector2(obstacleX, 3.58f), new Vector2(obstacleX + 1.15f * widthScale, 2.88f), new Vector2(obstacleX + 2.25f * widthScale, 1.88f) }; PlaceMileageTokens(positions, smallMileageValue); SetPickupValue(2, bonusMileageValue); } private float GetJumpMileageArcWidthScale() { if(GameManager.instance == null) { return JumpMileageMinWidthScale; } float minSpeed = Mathf.Max(0.1f, GameManager.instance.minGameSpeed); float maxSpeed = Mathf.Max(minSpeed + 0.1f, GameManager.instance.maxGameSpeed); float speedProgress = Mathf.InverseLerp(minSpeed, maxSpeed, GameManager.instance.gameSpeed); return Mathf.Lerp(JumpMileageMinWidthScale, JumpMileageMaxWidthScale, speedProgress); } private void PlaceSlideMileageLine() { Vector2[] positions = { new Vector2(-1.5f, 1.05f), new Vector2(-0.5f, 1.05f), new Vector2(0.5f, 1.05f), new Vector2(1.5f, 1.05f) }; 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 = GetParentScaleCompensatedPosition(positions[i]); mileagePickups[i].ResetPickup(); } } private Vector3 GetParentScaleCompensatedVector(float scale) { float parentScaleX = Mathf.Approximately(transform.localScale.x, 0f) ? 1f : transform.localScale.x; float parentScaleY = Mathf.Approximately(transform.localScale.y, 0f) ? 1f : transform.localScale.y; return new Vector3(scale / parentScaleX, scale / parentScaleY, 1f); } private Vector3 GetParentScaleCompensatedPosition(Vector2 position) { float parentScaleX = Mathf.Approximately(transform.localScale.x, 0f) ? 1f : transform.localScale.x; float parentScaleY = Mathf.Approximately(transform.localScale.y, 0f) ? 1f : transform.localScale.y; return new Vector3(position.x / parentScaleX, position.y / parentScaleY, 0f); } private float GetWorldRelativeX(Transform child) { return child.localPosition.x * transform.localScale.x; } private void SetPickupValue(int index, int value) { if(index < 0 || index >= mileagePickups.Length || !mileagePickups[index].gameObject.activeSelf) { return; } mileagePickups[index].Setup(mileageSprite, value); } }