using UnityEngine; using System.Collections.Generic; #if UNITY_EDITOR using UnityEditor; #endif namespace Northbound { public class ObstacleSpawner : MonoBehaviour { [System.Serializable] public class ObstacleEntry { [Tooltip("배치할 장애물 프리팹")] public GameObject prefab; [Tooltip("이 장애물의 스폰 가중치 (높을수록 자주 등장)")] [Range(1, 100)] public int spawnWeight = 50; [Tooltip("최소 스폰 개수")] public int minCount = 0; [Tooltip("최대 스폰 개수")] public int maxCount = 10; } [Header("Spawn Area Settings")] [Tooltip("장애물을 배치할 영역의 중심점 (비어있으면 현재 위치 사용)")] [SerializeField] private Transform spawnCenter; [Tooltip("배치 영역의 크기")] [SerializeField] private Vector2 spawnAreaSize = new Vector2(50f, 50f); [Tooltip("배치 영역의 형태")] [SerializeField] private SpawnAreaShape areaShape = SpawnAreaShape.Rectangle; [Header("Obstacle Settings")] [Tooltip("배치할 장애물 목록")] [SerializeField] private List obstacles = new List(); [Tooltip("장애물 밀도 (0.0 ~ 1.0, 높을수록 많이 배치)")] [Range(0f, 1f)] [SerializeField] private float density = 0.5f; [Tooltip("최대 장애물 개수")] [SerializeField] private int maxTotalObstacles = 100; [Header("Placement Rules")] [Tooltip("장애물 간 최소 거리")] [SerializeField] private float minDistanceBetweenObstacles = 2f; [Tooltip("지형에 맞춰 배치")] [SerializeField] private bool alignToTerrain = true; [Tooltip("Y축 랜덤 회전 적용")] [SerializeField] private bool randomRotation = true; [Tooltip("크기 랜덤 변화 범위 (0이면 변화 없음)")] [Range(0f, 0.5f)] [SerializeField] private float scaleVariation = 0.1f; [Tooltip("배치 시도 최대 횟수")] [SerializeField] private int maxAttempts = 50; [Header("Collision Check")] [Tooltip("배치 전 충돌 체크")] [SerializeField] private bool checkCollision = true; [Tooltip("충돌 체크 레이어")] [SerializeField] private LayerMask collisionLayers = -1; [Tooltip("충돌 체크 반경")] [SerializeField] private float collisionCheckRadius = 1f; [Header("Runtime Settings")] [Tooltip("게임 시작 시 자동 생성")] [SerializeField] private bool spawnOnStart = true; [Tooltip("생성된 장애물을 부모로 그룹화")] [SerializeField] private bool groupUnderParent = true; private List _spawnedPositions = new List(); private Transform _obstacleParent; public enum SpawnAreaShape { Rectangle, Circle } private void Start() { if (spawnOnStart) { SpawnObstacles(); } } /// /// 장애물을 생성합니다 /// public void SpawnObstacles() { ClearObstacles(); _spawnedPositions.Clear(); if (obstacles.Count == 0) { Debug.LogWarning("[ObstacleSpawner] 배치할 장애물이 없습니다!"); return; } if (groupUnderParent) { _obstacleParent = new GameObject("Generated Obstacles").transform; _obstacleParent.SetParent(transform); } Vector3 center = transform.position; int totalSpawned = 0; int targetCount = Mathf.RoundToInt(maxTotalObstacles * density); Debug.Log($"[ObstacleSpawner] 장애물 생성 시작 (목표: {targetCount}개)"); // 각 장애물 타입별로 최소 개수 보장 foreach (var obstacle in obstacles) { if (obstacle.prefab == null) continue; int minRequired = obstacle.minCount; for (int i = 0; i < minRequired && totalSpawned < maxTotalObstacles; i++) { if (TrySpawnObstacle(obstacle, center)) { totalSpawned++; } } } // 나머지를 가중치에 따라 랜덤 배치 int consecutiveFailures = 0; int maxConsecutiveFailures = 50; // 연속 실패 허용 횟수 while (totalSpawned < targetCount && consecutiveFailures < maxConsecutiveFailures) { ObstacleEntry selectedObstacle = SelectRandomObstacle(); if (selectedObstacle == null || selectedObstacle.prefab == null) { consecutiveFailures++; continue; } // 최대 개수 체크 int currentCount = CountObstacleType(selectedObstacle.prefab); if (currentCount >= selectedObstacle.maxCount) { consecutiveFailures++; // 모든 장애물이 최대치에 도달했는지 확인 if (AllObstaclesAtMax()) { Debug.Log("[ObstacleSpawner] 모든 장애물이 최대 개수에 도달했습니다."); break; } continue; } if (TrySpawnObstacle(selectedObstacle, center)) { totalSpawned++; consecutiveFailures = 0; // 성공 시 카운터 리셋 } else { consecutiveFailures++; } } if (consecutiveFailures >= maxConsecutiveFailures) { Debug.LogWarning($"[ObstacleSpawner] 배치 공간이 부족합니다. {totalSpawned}개만 생성되었습니다."); } Debug.Log($"[ObstacleSpawner] 장애물 생성 완료: {totalSpawned}개"); } private bool AllObstaclesAtMax() { foreach (var obstacle in obstacles) { if (obstacle.prefab == null) continue; int currentCount = CountObstacleType(obstacle.prefab); if (currentCount < obstacle.maxCount) { return false; } } return true; } private bool TrySpawnObstacle(ObstacleEntry obstacleEntry, Vector3 center) { for (int attempt = 0; attempt < maxAttempts; attempt++) { Vector3 randomPos = GetRandomPositionInArea(center); // 최소 거리 체크 if (!IsValidPosition(randomPos)) { Debug.Log($"[Spawn] 최소 거리 체크 실패: {randomPos}"); continue; } // 충돌 체크 if (checkCollision && Physics.CheckSphere(randomPos, collisionCheckRadius, collisionLayers)) { Debug.Log($"[Spawn] 충돌 감지: {randomPos}, 반경: {collisionCheckRadius}"); continue; } // 지형에 맞춤 if (alignToTerrain) { if (Physics.Raycast(randomPos + Vector3.up * 100f, Vector3.down, out RaycastHit hit, 200f)) { randomPos = hit.point; Debug.Log($"[Spawn] 지형 감지 성공: {hit.collider.name}"); } else { Debug.Log($"[Spawn] 지형 감지 실패: {randomPos}"); // 지형이 없어도 배치 시도 } } // 장애물 생성 Quaternion rotation = randomRotation ? Quaternion.Euler(0, Random.Range(0f, 360f), 0) : Quaternion.identity; GameObject obstacle = Instantiate(obstacleEntry.prefab, randomPos, rotation); if (groupUnderParent && _obstacleParent != null) { obstacle.transform.SetParent(_obstacleParent); } // 크기 변화 if (scaleVariation > 0) { float scale = 1f + Random.Range(-scaleVariation, scaleVariation); obstacle.transform.localScale *= scale; } _spawnedPositions.Add(randomPos); Debug.Log($"[Spawn] 장애물 배치 성공: {obstacleEntry.prefab.name} at {randomPos}"); return true; } Debug.LogWarning($"[Spawn] {obstacleEntry.prefab.name} 배치 실패 (시도: {maxAttempts}회)"); return false; } private Vector3 GetRandomPositionInArea(Vector3 center) { Vector3 randomPos = center; switch (areaShape) { case SpawnAreaShape.Rectangle: randomPos.x += Random.Range(-spawnAreaSize.x / 2, spawnAreaSize.x / 2); randomPos.z += Random.Range(-spawnAreaSize.y / 2, spawnAreaSize.y / 2); break; case SpawnAreaShape.Circle: Vector2 randomCircle = Random.insideUnitCircle * spawnAreaSize.x / 2; randomPos.x += randomCircle.x; randomPos.z += randomCircle.y; break; } return randomPos; } private bool IsValidPosition(Vector3 position) { foreach (var spawnedPos in _spawnedPositions) { if (Vector3.Distance(position, spawnedPos) < minDistanceBetweenObstacles) { return false; } } return true; } private ObstacleEntry SelectRandomObstacle() { if (obstacles.Count == 0) return null; int totalWeight = 0; foreach (var obstacle in obstacles) { if (obstacle.prefab != null) { totalWeight += obstacle.spawnWeight; } } if (totalWeight == 0) return null; int randomValue = Random.Range(0, totalWeight); int currentWeight = 0; foreach (var obstacle in obstacles) { if (obstacle.prefab == null) continue; currentWeight += obstacle.spawnWeight; if (randomValue < currentWeight) { return obstacle; } } return obstacles[0]; } private int CountObstacleType(GameObject prefab) { int count = 0; if (_obstacleParent != null) { foreach (Transform child in _obstacleParent) { if (child.name.StartsWith(prefab.name)) { count++; } } } return count; } /// /// 생성된 장애물을 모두 제거합니다 /// public void ClearObstacles() { if (_obstacleParent != null) { if (Application.isPlaying) { Destroy(_obstacleParent.gameObject); } else { DestroyImmediate(_obstacleParent.gameObject); } _obstacleParent = null; } _spawnedPositions.Clear(); } private void OnDrawGizmos() { Vector3 center = transform.position; Gizmos.color = new Color(0, 1, 0, 0.3f); switch (areaShape) { case SpawnAreaShape.Rectangle: Gizmos.DrawCube(center, new Vector3(spawnAreaSize.x, 0.1f, spawnAreaSize.y)); break; case SpawnAreaShape.Circle: DrawCircle(center, spawnAreaSize.x / 2, 32); break; } Gizmos.color = Color.green; Gizmos.DrawWireCube(center, new Vector3(spawnAreaSize.x, 0.1f, spawnAreaSize.y)); } private void DrawCircle(Vector3 center, float radius, int segments) { float angleStep = 360f / segments; Vector3 prevPoint = center + new Vector3(radius, 0, 0); for (int i = 1; i <= segments; i++) { float angle = i * angleStep * Mathf.Deg2Rad; Vector3 newPoint = center + new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius); Gizmos.DrawLine(prevPoint, newPoint); prevPoint = newPoint; } } private void OnDrawGizmosSelected() { #if UNITY_EDITOR Vector3 center = spawnCenter != null ? spawnCenter.position : transform.position; // 밀도 정보 표시 int targetCount = Mathf.RoundToInt(maxTotalObstacles * density); Handles.Label(center + Vector3.up * 2f, $"Obstacle Spawner\n목표: {targetCount}개 / 최대: {maxTotalObstacles}개\n밀도: {density:P0}"); // 생성된 장애물 위치 표시 Gizmos.color = Color.yellow; foreach (var pos in _spawnedPositions) { Gizmos.DrawWireSphere(pos, minDistanceBetweenObstacles / 2f); } #endif } } }