using System.Collections.Generic; using UnityEngine; using Random = UnityEngine.Random; // 발판을 생성하고 주기적으로 재배치하는 스크립트 public class PlatformSpawner : MonoBehaviour { private class PendingTutorialGuide { public Transform target; public GameManager.TutorialGuideType guideType; public string title; public string message; public float triggerX; } public GameObject platformPrefab; // 계속 이어지는 기본 발판 프리팹 public GameObject jumpObstaclePrefab; // 낮은 점프 장애물 프리팹 public GameObject slideObstaclePrefab; // 위쪽 슬라이딩 장애물 프리팹 public int count = 12; // 기본 발판 풀 개수 public bool usePatternCourse = true; // 검증된 발판 패턴 코스를 사용 public bool tutorialMode = true; // 처음에는 튜토리얼 패턴을 먼저 배치 public CourseStep[] tutorialSteps; // 튜토리얼 발판 순서 public CourseStep[] stagePatternSteps; // 튜토리얼 이후 반복할 검증된 발판 순서 public bool keepWorldSpacingBySpeed = true; // 게임 속도가 빨라져도 발판 사이 실제 거리를 유지 public int continuousCoursePoolCount = 12; // 기본 발판을 촘촘히 깔 때 필요한 최소 풀 크기 public int obstaclePoolCount = 6; // 점프/슬라이드 장애물별 재사용 풀 크기 public float timeBetSpawnMin = 1.25f; // 다음 배치까지의 시간 간격 최솟값 public float timeBetSpawnMax = 2.25f; // 다음 배치까지의 시간 간격 최댓값 public float tutorialSpawnInterval = 2.2f; // 튜토리얼 발판 배치 간격 private float timeBetSpawn; // 다음 배치까지의 시간 간격 private const float FallbackLandingSpacing = 0.64f; public float yMin = -3.5f; // 배치할 위치의 최소 y값 public float yMax = 1.5f; // 배치할 위치의 최대 y값 public float tutorialGuideTriggerX = 5f; // 발판/장애물이 이 위치까지 오면 설명을 띄운다. private float xPos = 20f; // 배치할 위치의 x 값 private GameObject[] platforms; // 기본 발판 풀 private GameObject[] jumpObstacles; // 점프 장애물 풀 private GameObject[] slideObstacles; // 슬라이드 장애물 풀 private int platformIndex = 0; // 기본 발판 풀 순번 private int jumpObstacleIndex = 0; // 점프 장애물 풀 순번 private int slideObstacleIndex = 0; // 슬라이드 장애물 풀 순번 private int courseStepIndex = 0; // 진행 중인 패턴 코스 순번 private Platform.PlatformSkin mapPlatformSkin = Platform.PlatformSkin.Airport; // 현재 맵에서 사용할 발판 스킨 private readonly List pendingTutorialGuides = new List(); // 실제 체험 직전에 띄울 설명들 private GameObject activeFinishLandingPlatform; // 도착 게이트가 공중에 뜨지 않도록 깔아두는 전용 발판 private bool courseSpawningStopped = false; // 도착 게이트 뒤로 새 발판/장애물이 나오지 않게 막는다. private Vector2 poolPosition = new Vector2(0, -25); // 초반에 생성된 발판들을 화면 밖에 숨겨둘 위치 private float lastSpawnTime; // 마지막 배치 시점 public void ApplyMap(MapDefinition mapDefinition) { if(mapDefinition == null) { return; } usePatternCourse = true; tutorialMode = mapDefinition.isTutorial; mapPlatformSkin = mapDefinition.platformSkin; tutorialSteps = mapDefinition.GetOpeningSteps(); stagePatternSteps = mapDefinition.GetLoopSteps(); courseStepIndex = 0; courseSpawningStopped = false; pendingTutorialGuides.Clear(); ClearFinishLandingPlatform(); ResetPoolIndices(); lastSpawnTime = Time.time; timeBetSpawn = 0f; count = Mathf.Max(count, continuousCoursePoolCount); EnsurePlatformPool(); DeactivateAllPlatformPools(); } public void SpawnFinishLandingPlatform(float spawnX, float groundY) { if(platformPrefab == null) { return; } ClearFinishLandingPlatform(); activeFinishLandingPlatform = Instantiate(platformPrefab, poolPosition, Quaternion.identity); activeFinishLandingPlatform.SetActive(false); CourseStep landingStep = CreateStep( Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, Mathf.Clamp(groundY, yMin, yMax), FallbackLandingSpacing, "", "", Platform.ItemPattern.None, CoursePlatformType.Basic); ConfigurePlatform(activeFinishLandingPlatform, landingStep); activeFinishLandingPlatform.transform.position = new Vector2(spawnX, landingStep.yPosition); activeFinishLandingPlatform.SetActive(true); } public void ClearFinishLandingPlatform() { if(activeFinishLandingPlatform != null) { Destroy(activeFinishLandingPlatform); activeFinishLandingPlatform = null; } } public void StopCourseSpawningAfterFinishGate() { courseSpawningStopped = true; pendingTutorialGuides.Clear(); } void Start() { EnsurePatternSteps(); if(usePatternCourse && tutorialMode) { count = Mathf.Max(count, continuousCoursePoolCount); } // 변수들을 초기화하고 사용할 발판들을 미리 생성 EnsurePlatformPool(); // 마지막 배치 시점 초기화 lastSpawnTime = 0f; // 다음번 배치까지의 시간 간격을 0으로 초기화 timeBetSpawn = 0f; } private void EnsurePlatformPool() { int basicCount = usePatternCourse ? Mathf.Max(count, continuousCoursePoolCount) : Mathf.Max(1, count); int obstacleCount = Mathf.Max(1, obstaclePoolCount); EnsurePool(ref platforms, basicCount, platformPrefab); EnsurePool(ref jumpObstacles, obstacleCount, jumpObstaclePrefab); EnsurePool(ref slideObstacles, obstacleCount, slideObstaclePrefab); count = platforms != null ? platforms.Length : count; } private void EnsurePool(ref GameObject[] pool, int requiredCount, GameObject prefab) { if(prefab == null) { return; } if(pool != null && pool.Length >= requiredCount) { return; } GameObject[] nextPool = new GameObject[requiredCount]; if(pool != null) { for(int i = 0; i < pool.Length; i++) { nextPool[i] = pool[i]; } } for(int i = 0; i < nextPool.Length; i++) { if(nextPool[i] == null) { nextPool[i] = Instantiate(prefab, poolPosition, Quaternion.identity); nextPool[i].SetActive(false); } } pool = nextPool; } private void ResetPoolIndices() { platformIndex = 0; jumpObstacleIndex = 0; slideObstacleIndex = 0; } private void DeactivateAllPlatformPools() { DeactivatePool(platforms); DeactivatePool(jumpObstacles); DeactivatePool(slideObstacles); } private void DeactivatePool(GameObject[] pool) { if(pool == null) { return; } for(int i = 0; i < pool.Length; i++) { if(pool[i] != null) { pool[i].SetActive(false); pool[i].transform.position = poolPosition; } } } void Update() { // 순서를 돌아가며 주기적으로 발판을 배치 if(GameManager.instance == null || GameManager.instance.isGameover || !GameFlowManager.IsGameplayActive) { return; } if(courseSpawningStopped) { return; } UpdatePendingTutorialGuides(); if(GameManager.instance != null && GameManager.instance.IsTutorialGuidePaused) { return; } EnsurePlatformPool(); if(Time.time >= lastSpawnTime + timeBetSpawn) { lastSpawnTime = Time.time; CourseStep step = usePatternCourse ? GetCurrentCourseStep() : null; timeBetSpawn = step != null ? GetStepSpawnInterval(step) : Random.Range(timeBetSpawnMin, timeBetSpawnMax); float yPos = step != null ? Mathf.Clamp(step.yPosition, yMin, yMax) : Random.Range(yMin, yMax); GameObject platformObject = TakeFromPool(platforms, ref platformIndex); if(platformObject == null) { return; } platformObject.SetActive(false); ConfigurePlatform(platformObject, step); platformObject.SetActive(true); platformObject.transform.position = new Vector2(xPos, yPos); SpawnCourseObstacles(step, yPos); RegisterTutorialGuide(platformObject.transform, step); if(usePatternCourse) { courseStepIndex++; } } } private void UpdatePendingTutorialGuides() { if(GameManager.instance == null || pendingTutorialGuides.Count == 0 || GameManager.instance.IsTutorialGuidePaused) { return; } for(int i = 0; i < pendingTutorialGuides.Count; i++) { PendingTutorialGuide guide = pendingTutorialGuides[i]; if(guide == null || guide.target == null || !guide.target.gameObject.activeInHierarchy) { pendingTutorialGuides.RemoveAt(i); i--; continue; } if(guide.target.position.x <= guide.triggerX) { GameManager.instance.SetTutorialGuide(guide.guideType, guide.title, guide.message); pendingTutorialGuides.RemoveAt(i); return; } } } private void RegisterTutorialGuide(Transform target, CourseStep step) { if(target == null || step == null || step.guideType == GameManager.TutorialGuideType.None || (string.IsNullOrEmpty(step.title) && string.IsNullOrEmpty(step.message))) { return; } pendingTutorialGuides.Add(new PendingTutorialGuide { target = target, guideType = step.guideType, title = step.title, message = step.message, triggerX = GetTutorialGuideTriggerX(step) }); } private float GetTutorialGuideTriggerX(CourseStep step) { if(step != null && step.guideType == GameManager.TutorialGuideType.DoubleJump) { return -2f; } return tutorialGuideTriggerX; } private void SpawnCourseObstacles(CourseStep step, float yPos) { if(step == null) { return; } if(step.coursePlatformType == CoursePlatformType.ForceHit || step.platformPattern == Platform.PlatformPattern.ForceHit) { SpawnJumpObstacle(Platform.PlatformPattern.LowMid, yPos); return; } if(step.coursePlatformType == CoursePlatformType.Jump || IsJumpPattern(step.platformPattern)) { Platform.PlatformPattern jumpPattern = IsJumpPattern(step.platformPattern) ? step.platformPattern : Platform.PlatformPattern.LowMid; SpawnJumpObstacle(jumpPattern, yPos); return; } if(step.coursePlatformType == CoursePlatformType.Slide || step.platformPattern == Platform.PlatformPattern.Slide) { SpawnSlideObstacle(step.slideLayout, yPos); } } private void SpawnJumpObstacle(Platform.PlatformPattern platformPattern, float yPos) { GameObject obstacleObject = TakeFromPool(jumpObstacles, ref jumpObstacleIndex); if(obstacleObject == null) { return; } obstacleObject.SetActive(false); ConfigureObstacle(obstacleObject, platformPattern, SlideObstaclePattern.SlideLayout.Random); obstacleObject.transform.position = new Vector2(xPos, yPos); obstacleObject.SetActive(true); } private void SpawnSlideObstacle(SlideObstaclePattern.SlideLayout slideLayout, float yPos) { GameObject obstacleObject = TakeFromPool(slideObstacles, ref slideObstacleIndex); if(obstacleObject == null) { return; } obstacleObject.SetActive(false); ConfigureObstacle(obstacleObject, Platform.PlatformPattern.Slide, slideLayout); obstacleObject.transform.position = new Vector2(xPos, yPos); obstacleObject.SetActive(true); } private void ConfigureObstacle( GameObject obstacleObject, Platform.PlatformPattern platformPattern, SlideObstaclePattern.SlideLayout slideLayout) { CourseObstacle obstacle = obstacleObject.GetComponent(); if(obstacle != null) { obstacle.Configure(mapPlatformSkin, platformPattern, slideLayout); } } private bool IsJumpPattern(Platform.PlatformPattern platformPattern) { return platformPattern == Platform.PlatformPattern.LowLeft || platformPattern == Platform.PlatformPattern.LowMid || platformPattern == Platform.PlatformPattern.LowRight; } private GameObject TakeFromPool(GameObject[] pool, ref int poolIndex) { if(pool == null || pool.Length == 0) { return null; } if(poolIndex < 0 || poolIndex >= pool.Length) { poolIndex = 0; } GameObject platformObject = pool[poolIndex]; poolIndex++; if(poolIndex >= pool.Length) { poolIndex = 0; } return platformObject; } private void ConfigurePlatform(GameObject platformObject, CourseStep step) { Platform platform = platformObject.GetComponent(); if(platform == null) { return; } if(step == null) { platform.ConfigureForRandom(mapPlatformSkin); return; } platform.ConfigureForTutorial(step.mileagePattern, step.itemPattern, mapPlatformSkin); // 설명은 발판 생성 시점이 아니라 실제 체험 직전에 RegisterTutorialGuide가 띄운다. } private float GetStepSpawnInterval(CourseStep step) { float spawnInterval; if(step != null && step.spawnInterval > 0f) { spawnInterval = step.spawnInterval; } else { spawnInterval = tutorialSpawnInterval; } if(keepWorldSpacingBySpeed && GameManager.instance != null) { spawnInterval /= Mathf.Max(0.1f, GameManager.instance.gameSpeed); } return Mathf.Max(0.05f, spawnInterval); } private CourseStep 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() { CourseProfile fallbackProfile = tutorialMode ? FallbackCourseProfile.CreateTutorialFallback() : FallbackCourseProfile.CreateGeneralFallback(); if(tutorialSteps == null || tutorialSteps.Length == 0) { tutorialSteps = fallbackProfile.GetOpeningSteps(); } if(stagePatternSteps == null || stagePatternSteps.Length == 0) { stagePatternSteps = fallbackProfile.GetLoopSteps(); } } private CourseStep 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, CoursePlatformType coursePlatformType = CoursePlatformType.Basic) { CourseStep step = new CourseStep(); step.coursePlatformType = coursePlatformType; 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; } }