430 lines
15 KiB
C#
430 lines
15 KiB
C#
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<ObstacleEntry> obstacles = new List<ObstacleEntry>();
|
|
|
|
[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<Vector3> _spawnedPositions = new List<Vector3>();
|
|
private Transform _obstacleParent;
|
|
|
|
public enum SpawnAreaShape
|
|
{
|
|
Rectangle,
|
|
Circle
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
if (spawnOnStart)
|
|
{
|
|
SpawnObstacles();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 장애물을 생성합니다
|
|
/// </summary>
|
|
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($"<color=cyan>[ObstacleSpawner] 장애물 생성 시작 (목표: {targetCount}개)</color>");
|
|
|
|
// 각 장애물 타입별로 최소 개수 보장
|
|
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("<color=yellow>[ObstacleSpawner] 모든 장애물이 최대 개수에 도달했습니다.</color>");
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (TrySpawnObstacle(selectedObstacle, center))
|
|
{
|
|
totalSpawned++;
|
|
consecutiveFailures = 0; // 성공 시 카운터 리셋
|
|
}
|
|
else
|
|
{
|
|
consecutiveFailures++;
|
|
}
|
|
}
|
|
|
|
if (consecutiveFailures >= maxConsecutiveFailures)
|
|
{
|
|
Debug.LogWarning($"<color=yellow>[ObstacleSpawner] 배치 공간이 부족합니다. {totalSpawned}개만 생성되었습니다.</color>");
|
|
}
|
|
|
|
Debug.Log($"<color=green>[ObstacleSpawner] 장애물 생성 완료: {totalSpawned}개</color>");
|
|
}
|
|
|
|
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($"<color=orange>[Spawn] 최소 거리 체크 실패: {randomPos}</color>");
|
|
continue;
|
|
}
|
|
|
|
// 충돌 체크
|
|
if (checkCollision && Physics.CheckSphere(randomPos, collisionCheckRadius, collisionLayers))
|
|
{
|
|
Debug.Log($"<color=orange>[Spawn] 충돌 감지: {randomPos}, 반경: {collisionCheckRadius}</color>");
|
|
continue;
|
|
}
|
|
|
|
// 지형에 맞춤
|
|
if (alignToTerrain)
|
|
{
|
|
if (Physics.Raycast(randomPos + Vector3.up * 100f, Vector3.down, out RaycastHit hit, 200f))
|
|
{
|
|
randomPos = hit.point;
|
|
Debug.Log($"<color=cyan>[Spawn] 지형 감지 성공: {hit.collider.name}</color>");
|
|
}
|
|
else
|
|
{
|
|
Debug.Log($"<color=orange>[Spawn] 지형 감지 실패: {randomPos}</color>");
|
|
// 지형이 없어도 배치 시도
|
|
}
|
|
}
|
|
|
|
// 장애물 생성
|
|
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;
|
|
}
|
|
|
|
// Add FogOfWarVisibility component to hide obstacles in unexplored areas
|
|
if (obstacle.GetComponent<FogOfWarVisibility>() == null)
|
|
{
|
|
var visibility = obstacle.AddComponent<FogOfWarVisibility>();
|
|
visibility.showInExploredAreas = false; // Obstacles hidden when not visible
|
|
visibility.updateInterval = 0.2f;
|
|
}
|
|
|
|
_spawnedPositions.Add(randomPos);
|
|
Debug.Log($"<color=green>[Spawn] 장애물 배치 성공: {obstacleEntry.prefab.name} at {randomPos}</color>");
|
|
return true;
|
|
}
|
|
|
|
Debug.LogWarning($"<color=red>[Spawn] {obstacleEntry.prefab.name} 배치 실패 (시도: {maxAttempts}회)</color>");
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 생성된 장애물을 모두 제거합니다
|
|
/// </summary>
|
|
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
|
|
}
|
|
}
|
|
} |