using Unity.Netcode; using UnityEngine; using System.Collections.Generic; #if UNITY_EDITOR using UnityEditor; #endif namespace Northbound { public class MapGenerator : NetworkBehaviour { public static MapGenerator Instance { get; private set; } [Header("Common Settings")] [Tooltip("맵 생성 시작 시 자동 생성")] [SerializeField] private bool generateOnSpawn = true; [Tooltip("생성된 오브젝트를 부모로 그룹화")] [SerializeField] private bool groupUnderParent = true; [Header("Map Boundaries")] [Tooltip("X 범위 (-width/2 ~ +width/2)")] public float playableAreaWidth = 200f; [Tooltip("Z 시작 위치")] public float startZ = 25f; [Tooltip("Z 끝 위치")] public float endZ = 700f; private Vector2 _initialResourcePosition; private Vector2 _corePosition; private Vector2 _barracksPosition; [Header("Resource Generation")] [Tooltip("자원 프리팹")] public GameObject resourcePrefab; [Tooltip("최소 자원 개수 (초기 자원 제외)")] [Range(8, 12)] public int minResourceCount = 8; [Tooltip("최대 자원 개수 (초기 자원 제외)")] [Range(8, 12)] public int maxResourceCount = 12; [Tooltip("자원 간 최소 거리")] public float minDistanceBetweenResources = 80f; [Tooltip("코어와의 최소 거리")] public float minDistanceFromCore = 50f; [Tooltip("막사와의 최소 거리")] public float minDistanceFromBarracks = 50f; [Tooltip("초기 자원 생산량 (분당)")] public float initialResourceProduction = 50f; [Tooltip("추가 자원 기본 생산량 (분당)")] public float additionalResourceBaseProduction = 25f; [Tooltip("목표 총 생산량 (분당)")] public float targetTotalProduction = 300f; [Tooltip("생산량 허용 오차")] [Range(0f, 0.2f)] public float targetProductionTolerance = 0.05f; [Tooltip("최소 품질 보정")] public float minQualityModifier = -30f; [Tooltip("최대 품질 보정")] public float maxQualityModifier = 30f; [Tooltip("최대 생성 시도 횟수")] public int maxResourceGenerationAttempts = 10; [Header("Obstacle Settings")] [Tooltip("배치할 장애물 목록")] [SerializeField] private List obstacles = new List(); [Tooltip("장애물 밀도")] [Range(0f, 1f)] [SerializeField] private float obstacleDensity = 0.5f; [Tooltip("최대 장애물 개수")] [SerializeField] private int maxTotalObstacles = 100; [Tooltip("장애물 간 최소 거리")] [SerializeField] private float minDistanceBetweenObstacles = 2f; [Tooltip("배치 전 충돌 체크")] [SerializeField] private bool checkCollision = true; [Tooltip("충돌 체크 레이어")] [SerializeField] private LayerMask collisionLayers = -1; [Tooltip("충돌 체크 반경")] [SerializeField] private float collisionCheckRadius = 1f; [Tooltip("지형에 맞춰 배치")] [SerializeField] private bool alignToTerrain = true; [Tooltip("Y축 랜덤 회전 적용")] [SerializeField] private bool randomRotation = true; [Tooltip("크기 랜덤 변화 범위")] [Range(0f, 0.5f)] [SerializeField] private float scaleVariation = 0.1f; [Tooltip("배치 시도 최대 횟수")] [SerializeField] private int maxObstacleSpawnAttempts = 50; [Header("Generation Order")] [Tooltip("자원 먼저 생성 후 장애물 생성 (true) 또는 그 반대 (false)")] [SerializeField] private bool generateResourcesFirst = true; private ResourceData[] _generatedResources; private float _globalProductionMultiplier = 1f; private List _spawnedPositions = new List(); private Transform _objectsParent; [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; } public override void OnNetworkSpawn() { if (IsServer && generateOnSpawn) { Instance = this; FindImportantPositions(); GenerateMap(); } } private void FindImportantPositions() { Core core = FindObjectOfType(); if (core != null) { _corePosition = new Vector2(core.transform.position.x, core.transform.position.z); Debug.Log($"[MapGenerator] Found Core at {_corePosition}"); } else { Debug.LogWarning("[MapGenerator] Core not found in scene!"); _corePosition = Vector2.zero; } Resource[] resources = FindObjectsOfType(); if (resources.Length > 0) { _initialResourcePosition = new Vector2(resources[0].transform.position.x, resources[0].transform.position.z); Debug.Log($"[MapGenerator] Found initial Resource at {_initialResourcePosition}"); } else { Debug.LogWarning("[MapGenerator] No Resource found in scene!"); _initialResourcePosition = Vector2.zero; } GameObject barracks = GameObject.Find("Worker Hall"); if (barracks != null) { _barracksPosition = new Vector2(barracks.transform.position.x, barracks.transform.position.z); Debug.Log($"[MapGenerator] Found Worker Hall at {_barracksPosition}"); } else { Debug.LogWarning("[MapGenerator] Worker Hall not found in scene!"); _barracksPosition = Vector2.zero; } } private void GenerateMap() { if (groupUnderParent) { _objectsParent = new GameObject("Generated Map Objects").transform; _objectsParent.SetParent(transform); NetworkObject parentNetworkObj = _objectsParent.gameObject.GetComponent(); if (parentNetworkObj == null) { parentNetworkObj = _objectsParent.gameObject.AddComponent(); } parentNetworkObj.Spawn(); } _spawnedPositions.Clear(); if (generateResourcesFirst) { GenerateResources(); GenerateObstacles(); } else { GenerateObstacles(); GenerateResources(); } Debug.Log($"[MapGenerator] Map generation complete!"); } #region Resource Generation private void GenerateResources() { if (resourcePrefab == null) { Debug.LogError("[MapGenerator] Resource prefab not assigned!"); return; } bool success = false; for (int attempt = 0; attempt < maxResourceGenerationAttempts; attempt++) { if (TryGenerateResources(out var resources)) { _generatedResources = resources; success = true; Debug.Log($"[MapGenerator] Successfully generated resources on attempt {attempt + 1}"); break; } else { Debug.LogWarning($"[MapGenerator] Resource generation attempt {attempt + 1} failed validation"); } } if (!success) { Debug.LogWarning("[MapGenerator] All resource generation attempts failed, using fallback layout"); GenerateResourceFallbackLayout(); } SpawnResources(); } private bool TryGenerateResources(out ResourceData[] resources) { resources = null; int additionalResourceCount = Random.Range(minResourceCount, maxResourceCount + 1); ResourceData[] tempResources = new ResourceData[additionalResourceCount]; for (int i = 0; i < additionalResourceCount; i++) { Vector2 position; float qualityModifier; if (!TryFindValidResourcePosition(tempResources, i, out position)) { return false; } qualityModifier = Random.Range(minQualityModifier, maxQualityModifier); float qualityMultiplier = 1f + (qualityModifier / 100f); float finalProduction = additionalResourceBaseProduction * qualityMultiplier; tempResources[i] = new ResourceData { position = position, baseProduction = additionalResourceBaseProduction, qualityModifier = qualityModifier, finalProduction = finalProduction }; } if (!ValidateProductionRate(tempResources, out float globalMultiplier)) { return false; } _globalProductionMultiplier = globalMultiplier; for (int i = 0; i < tempResources.Length; i++) { tempResources[i].finalProduction *= _globalProductionMultiplier; } resources = tempResources; return true; } private bool TryFindValidResourcePosition(ResourceData[] existingResources, int currentIndex, out Vector2 position) { position = Vector2.zero; int maxAttempts = 100; for (int attempt = 0; attempt < maxAttempts; attempt++) { float x = Random.Range(-playableAreaWidth / 2f, playableAreaWidth / 2f); float y = Random.Range(startZ, endZ); Vector2 candidatePosition = new Vector2(x, y); if (IsValidResourcePosition(candidatePosition, existingResources, currentIndex)) { position = candidatePosition; return true; } } return false; } private bool IsValidResourcePosition(Vector2 position, ResourceData[] existingResources, int currentIndex) { if (Vector2.Distance(position, _initialResourcePosition) < minDistanceBetweenResources) { return false; } if (Vector2.Distance(position, _corePosition) < minDistanceFromCore) { return false; } if (Vector2.Distance(position, _barracksPosition) < minDistanceFromBarracks) { return false; } for (int i = 0; i < currentIndex; i++) { if (Vector2.Distance(position, existingResources[i].position) < minDistanceBetweenResources) { return false; } } return true; } private bool ValidateProductionRate(ResourceData[] additionalResources, out float globalMultiplier) { globalMultiplier = 1f; float totalProduction = initialResourceProduction; for (int i = 0; i < additionalResources.Length; i++) { totalProduction += additionalResources[i].finalProduction; } float minTarget = targetTotalProduction * (1f - targetProductionTolerance); float maxTarget = targetTotalProduction * (1f + targetProductionTolerance); if (totalProduction >= minTarget && totalProduction <= maxTarget) { globalMultiplier = 1f; return true; } globalMultiplier = targetTotalProduction / totalProduction; float adjustedTotal = totalProduction * globalMultiplier; if (adjustedTotal >= minTarget && adjustedTotal <= maxTarget) { return true; } return false; } private void GenerateResourceFallbackLayout() { _generatedResources = new ResourceData[] { new ResourceData { position = new Vector2(30, 100), baseProduction = additionalResourceBaseProduction, qualityModifier = 0f, finalProduction = additionalResourceBaseProduction }, new ResourceData { position = new Vector2(50, 200), baseProduction = additionalResourceBaseProduction, qualityModifier = 0f, finalProduction = additionalResourceBaseProduction }, new ResourceData { position = new Vector2(20, 300), baseProduction = additionalResourceBaseProduction, qualityModifier = 0f, finalProduction = additionalResourceBaseProduction }, new ResourceData { position = new Vector2(60, 400), baseProduction = additionalResourceBaseProduction, qualityModifier = 0f, finalProduction = additionalResourceBaseProduction }, new ResourceData { position = new Vector2(35, 500), baseProduction = additionalResourceBaseProduction, qualityModifier = 0f, finalProduction = additionalResourceBaseProduction }, new ResourceData { position = new Vector2(55, 600), baseProduction = additionalResourceBaseProduction, qualityModifier = 0f, finalProduction = additionalResourceBaseProduction }, new ResourceData { position = new Vector2(25, 700), baseProduction = additionalResourceBaseProduction, qualityModifier = 0f, finalProduction = additionalResourceBaseProduction }, new ResourceData { position = new Vector2(45, 150), baseProduction = additionalResourceBaseProduction, qualityModifier = 0f, finalProduction = additionalResourceBaseProduction }, new ResourceData { position = new Vector2(65, 250), baseProduction = additionalResourceBaseProduction, qualityModifier = 0f, finalProduction = additionalResourceBaseProduction } }; float totalProduction = initialResourceProduction; for (int i = 0; i < _generatedResources.Length; i++) { totalProduction += _generatedResources[i].finalProduction; } _globalProductionMultiplier = targetTotalProduction / totalProduction; for (int i = 0; i < _generatedResources.Length; i++) { _generatedResources[i].finalProduction *= _globalProductionMultiplier; } Debug.Log($"[MapGenerator] Resource fallback layout generated. Global multiplier: {_globalProductionMultiplier:F2}"); } private void SpawnResources() { float totalProduction = initialResourceProduction; for (int i = 0; i < _generatedResources.Length; i++) { GameObject resourceObj = Instantiate(resourcePrefab, new Vector3(_generatedResources[i].position.x, 1f, _generatedResources[i].position.y), Quaternion.identity); Resource resource = resourceObj.GetComponent(); if (resource == null) { Debug.LogError($"[MapGenerator] Resource prefab at index {i} doesn't have Resource component!"); Destroy(resourceObj); continue; } resource.InitializeQuality(_generatedResources[i].qualityModifier); Debug.Log($"[MapGenerator] Spawned resource at {_generatedResources[i].position} with quality: {_generatedResources[i].qualityModifier:F1}% (Actual Max: {resource.ActualMaxResources}, Actual Recharge: {resource.ActualRechargeAmount})"); NetworkObject networkObj = resourceObj.GetComponent(); if (networkObj != null) { networkObj.Spawn(); if (groupUnderParent && _objectsParent != null) { resourceObj.transform.SetParent(_objectsParent); } } else { Debug.LogError($"[MapGenerator] Resource prefab at index {i} doesn't have NetworkObject component!"); Destroy(resourceObj); } _spawnedPositions.Add(new Vector3(_generatedResources[i].position.x, 1f, _generatedResources[i].position.y)); totalProduction += _generatedResources[i].finalProduction; } Debug.Log($"[MapGenerator] Spawned {_generatedResources.Length} additional resources (plus initial at {_initialResourcePosition}). Total production: {totalProduction:F2} mana/min. Global multiplier: {_globalProductionMultiplier:F2}"); } #endregion #region Obstacle Generation private void GenerateObstacles() { if (obstacles.Count == 0) { Debug.Log("[MapGenerator] No obstacles configured, skipping obstacle generation."); return; } int totalSpawned = 0; int targetCount = Mathf.RoundToInt(maxTotalObstacles * obstacleDensity); Debug.Log($"[MapGenerator] Starting obstacle generation. Target: {targetCount}, Density: {obstacleDensity}"); foreach (var obstacle in obstacles) { if (obstacle.prefab == null) { Debug.LogWarning($"[MapGenerator] Obstacle prefab is null!"); continue; } int minRequired = obstacle.minCount; for (int i = 0; i < minRequired && totalSpawned < maxTotalObstacles; i++) { if (TrySpawnObstacle(obstacle)) { totalSpawned++; Debug.Log($"[MapGenerator] Spawned min required obstacle: {obstacle.prefab.name}"); } } } 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()) { break; } continue; } if (TrySpawnObstacle(selectedObstacle)) { totalSpawned++; consecutiveFailures = 0; } else { consecutiveFailures++; } } Debug.Log($"[MapGenerator] Spawned {totalSpawned} obstacles (target: {targetCount}). Consecutive failures: {consecutiveFailures}"); } private bool TrySpawnObstacle(ObstacleEntry obstacleEntry) { for (int attempt = 0; attempt < maxObstacleSpawnAttempts; attempt++) { Vector3 randomPos = GetRandomPositionInPlayableArea(); if (!IsValidObstaclePosition(randomPos)) { continue; } if (checkCollision && Physics.CheckSphere(randomPos, collisionCheckRadius, collisionLayers)) { continue; } if (alignToTerrain) { if (Physics.Raycast(randomPos + Vector3.up * 100f, Vector3.down, out RaycastHit hit, 200f)) { randomPos = hit.point; } } randomPos.y = 1f; Quaternion rotation = randomRotation ? Quaternion.Euler(0, Random.Range(0f, 360f), 0) : Quaternion.identity; GameObject obstacle = Instantiate(obstacleEntry.prefab, randomPos, rotation); if (scaleVariation > 0) { float scale = 1f + Random.Range(-scaleVariation, scaleVariation); obstacle.transform.localScale *= scale; } NetworkObject networkObj = obstacle.GetComponent(); if (networkObj == null) { networkObj = obstacle.AddComponent(); } networkObj.Spawn(); if (groupUnderParent && _objectsParent != null) { obstacle.transform.SetParent(_objectsParent); } if (obstacle.GetComponent() == null) { var visibility = obstacle.AddComponent(); visibility.showInExploredAreas = false; visibility.updateInterval = 0.2f; } _spawnedPositions.Add(randomPos); return true; } return false; } private Vector3 GetRandomPositionInPlayableArea() { Vector3 center = transform.position; float x = Random.Range(-playableAreaWidth / 2f, playableAreaWidth / 2f); float z = Random.Range(startZ, endZ); return new Vector3(x, center.y, z); } private bool IsValidObstaclePosition(Vector3 position) { foreach (var spawnedPos in _spawnedPositions) { if (Vector3.Distance(position, spawnedPos) < minDistanceBetweenObstacles) { return false; } } if (Vector2.Distance(new Vector2(position.x, position.z), _corePosition) < minDistanceFromCore) { return false; } if (Vector2.Distance(new Vector2(position.x, position.z), _barracksPosition) < minDistanceFromBarracks) { 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 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 int CountObstacleType(GameObject prefab) { int count = 0; if (_objectsParent != null) { foreach (Transform child in _objectsParent) { if (child.name.StartsWith(prefab.name)) { count++; } } } return count; } #endregion #region Public Methods public float GetTotalProduction() { if (_generatedResources == null) return initialResourceProduction; float total = initialResourceProduction; for (int i = 0; i < _generatedResources.Length; i++) { total += _generatedResources[i].finalProduction; } return total; } public void ClearGeneratedObjects() { if (_objectsParent != null) { NetworkObject parentNetworkObj = _objectsParent.GetComponent(); if (parentNetworkObj != null && parentNetworkObj.IsSpawned) { parentNetworkObj.Despawn(true); } foreach (Transform child in _objectsParent) { NetworkObject networkObj = child.GetComponent(); if (networkObj != null && networkObj.IsSpawned) { networkObj.Despawn(true); } } if (Application.isPlaying) { Destroy(_objectsParent.gameObject); } else { DestroyImmediate(_objectsParent.gameObject); } _objectsParent = null; } _spawnedPositions.Clear(); } public void RegenerateMap() { ClearGeneratedObjects(); GenerateMap(); } #endregion #region Editor private void OnDrawGizmos() { Vector3 center = transform.position; center.x = 0f; center.z = (startZ + endZ) / 2f; float height = endZ - startZ; Gizmos.color = new Color(0, 1, 0, 0.3f); Gizmos.DrawCube(center, new Vector3(playableAreaWidth, 0.1f, height)); Gizmos.color = Color.green; Gizmos.DrawWireCube(center, new Vector3(playableAreaWidth, 0.1f, height)); Gizmos.color = Color.cyan; Gizmos.DrawWireSphere(new Vector3(_corePosition.x, 0, _corePosition.y), minDistanceFromCore); Gizmos.color = Color.magenta; Gizmos.DrawWireSphere(new Vector3(_barracksPosition.x, 0, _barracksPosition.y), minDistanceFromBarracks); } private void OnDrawGizmosSelected() { #if UNITY_EDITOR Gizmos.color = Color.yellow; foreach (var pos in _spawnedPositions) { Gizmos.DrawWireSphere(pos, minDistanceBetweenObstacles / 2f); } if (_generatedResources != null) { Gizmos.color = Color.blue; foreach (var resource in _generatedResources) { Gizmos.DrawWireSphere(new Vector3(resource.position.x, 0, resource.position.y), 5f); } } if (resourcePrefab != null) { Gizmos.color = Color.blue; Gizmos.DrawWireSphere(new Vector3(_initialResourcePosition.x, 0, _initialResourcePosition.y), 5f); } #endif } #endregion } public struct ResourceData { public Vector2 position; public float baseProduction; public float qualityModifier; public float finalProduction; } }