Files
Travel_Run/Assets/Scripts/PlatformSpawner.cs
T
2026-07-09 02:50:48 +09:00

519 lines
17 KiB
C#

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<PendingTutorialGuide> pendingTutorialGuides = new List<PendingTutorialGuide>(); // 실제 체험 직전에 띄울 설명들
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<CourseObstacle>();
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<Platform>();
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;
}
}