장애물 및 장애물 생성기
Kaykit Forest Nature 추가
This commit is contained in:
8
Assets/Scripts/Editor.meta
Normal file
8
Assets/Scripts/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df9d7e473c51f4340975bc2d15064f7f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
44
Assets/Scripts/Editor/ObstacleSpawnerEditor.cs
Normal file
44
Assets/Scripts/Editor/ObstacleSpawnerEditor.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
#if UNITY_EDITOR
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Northbound
|
||||
{
|
||||
[CustomEditor(typeof(ObstacleSpawner))]
|
||||
public class ObstacleSpawnerEditor : UnityEditor.Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
|
||||
ObstacleSpawner spawner = (ObstacleSpawner)target;
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
EditorGUILayout.LabelField("에디터 도구", EditorStyles.boldLabel);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
if (GUILayout.Button("장애물 생성", GUILayout.Height(30)))
|
||||
{
|
||||
spawner.SpawnObstacles();
|
||||
EditorUtility.SetDirty(spawner);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("장애물 제거", GUILayout.Height(30)))
|
||||
{
|
||||
spawner.ClearObstacles();
|
||||
EditorUtility.SetDirty(spawner);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
EditorGUILayout.HelpBox(
|
||||
"• 장애물 생성: 설정한 밀도에 따라 랜덤 배치\n" +
|
||||
"• 장애물 제거: 생성된 모든 장애물 삭제\n" +
|
||||
"• Scene 뷰에서 초록색 영역이 배치 범위입니다",
|
||||
MessageType.Info);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
2
Assets/Scripts/Editor/ObstacleSpawnerEditor.cs.meta
Normal file
2
Assets/Scripts/Editor/ObstacleSpawnerEditor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a916ac51f838504a868f13e3766a2c5
|
||||
422
Assets/Scripts/ObstacleSpawner.cs
Normal file
422
Assets/Scripts/ObstacleSpawner.cs
Normal file
@@ -0,0 +1,422 @@
|
||||
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;
|
||||
}
|
||||
|
||||
_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
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/ObstacleSpawner.cs.meta
Normal file
2
Assets/Scripts/ObstacleSpawner.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a889d8fab7aeaa24dbbffaee2f02ba54
|
||||
Reference in New Issue
Block a user