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("Creep Camp Settings")] [Tooltip("크립 캠프 프리팹")] public GameObject creepCampPrefab; [Tooltip("크립 캠프 간 최소 거리")] [SerializeField] private float minDistanceBetweenCamps = 30f; [Tooltip("코어와의 최소 거리")] [SerializeField] private float minDistanceFromCoreCamps = 30f; [Tooltip("막사와의 최소 거리")] [SerializeField] private float minDistanceFromBarracksCamps = 30f; [Tooltip("초기 자원과의 최소 거리")] [SerializeField] private float minDistanceFromInitialResource = 30f; [Tooltip("추가 크립 캠프 개수")] [Range(0, 10)] [SerializeField] private int additionalCreepCampCount = 5; [Tooltip("크립 캠프 강도 기본값")] [SerializeField] private float baseCampStrength = 1f; [Tooltip("Z 위치에 따른 강도 증가율 (Z 100당)")] [SerializeField] private float strengthIncreasePerZ100 = 0.2f; [Tooltip("자원 보호 캠프 강도 보너스 (far camp보다 얼마나 강할지)")] [SerializeField] private float resourceCampStrengthBonus = 0.5f; [Header("Generation Order")] [Tooltip("자원 먼저 생성 후 장애물 생성 (true) 또는 그 반대 (false)")] [SerializeField] private bool generateResourcesFirst = true; private ResourceData[] _generatedResources; private float _globalProductionMultiplier = 1f; private List _spawnedPositions = new List(); private List _creepCampPositions = new List(); private List _creepPrefabs = new List(); [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); } 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); foreach (var resource in resources) { resource.InitializeQuality(0f); } } 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); } else { Debug.LogWarning("[MapGenerator] Worker Hall not found in scene!"); _barracksPosition = Vector2.zero; } } private void GenerateMap() { _spawnedPositions.Clear(); _creepCampPositions.Clear(); if (generateResourcesFirst) { GenerateResources(); GenerateObstacles(); } else { GenerateObstacles(); GenerateResources(); } GenerateCreepCamps(); } #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] Normal resource generation succeeded 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(); Debug.Log($"[MapGenerator] Fallback layout X coordinates: {string.Join(", ", System.Array.ConvertAll(_generatedResources, r => r.position.x.ToString("F1")))}"); } 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; Debug.Log($"[MapGenerator] Generated valid resource position: X={position.x:F2} (range: {-playableAreaWidth/2f:F2} to {playableAreaWidth/2f:F2})"); 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(30, 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(20, 400), baseProduction = additionalResourceBaseProduction, qualityModifier = 0f, finalProduction = additionalResourceBaseProduction }, new ResourceData { position = new Vector2(0, 500), baseProduction = additionalResourceBaseProduction, qualityModifier = 0f, finalProduction = additionalResourceBaseProduction }, new ResourceData { position = new Vector2(-35, 600), baseProduction = additionalResourceBaseProduction, qualityModifier = 0f, finalProduction = additionalResourceBaseProduction }, new ResourceData { position = new Vector2(35, 700), baseProduction = additionalResourceBaseProduction, qualityModifier = 0f, finalProduction = additionalResourceBaseProduction }, new ResourceData { position = new Vector2(-15, 150), baseProduction = additionalResourceBaseProduction, qualityModifier = 0f, finalProduction = additionalResourceBaseProduction }, new ResourceData { position = new Vector2(15, 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; } } 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); NetworkObject networkObj = resourceObj.GetComponent(); if (networkObj != null) { networkObj.Spawn(); } 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; } } #endregion #region Obstacle Generation private void GenerateObstacles() { if (obstacles.Count == 0) { return; } int totalSpawned = 0; int targetCount = Mathf.RoundToInt(maxTotalObstacles * 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++; } } } 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++; } } } 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 (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); Vector3 result = new Vector3(x, center.y, z); return result; } 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) { return 0; } #endregion #region Creep Camp Generation private void GenerateCreepCamps() { if (creepCampPrefab == null) { Debug.LogWarning("[MapGenerator] Creep camp prefab not assigned, skipping creep camp generation."); return; } LoadCreepPrefabs(); if (_creepPrefabs.Count == 0) { Debug.LogWarning("[MapGenerator] No creep prefabs found in Assets/Prefabs/Creep/, skipping creep camp generation."); return; } int totalCampsSpawned = 0; for (int i = 0; i < _generatedResources.Length; i++) { Vector3 campPosition = FindValidCampPositionForResource(_generatedResources[i].position); if (campPosition != Vector3.zero) { float strength = CalculateCampStrength(_generatedResources[i].position.y, isResourceCamp: true); SpawnCreepCamp(campPosition, strength); _creepCampPositions.Add(campPosition); totalCampsSpawned++; } } int additionalSpawned = 0; for (int i = 0; i < additionalCreepCampCount; i++) { Vector3 campPosition = FindValidCampPosition(); if (campPosition != Vector3.zero) { float strength = CalculateCampStrength(campPosition.z, isResourceCamp: false); SpawnCreepCamp(campPosition, strength); _creepCampPositions.Add(campPosition); additionalSpawned++; } } } private void LoadCreepPrefabs() { if (_creepPrefabs.Count > 0) { return; } #if UNITY_EDITOR string[] prefabGuids = UnityEditor.AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/Prefabs/Creep" }); _creepPrefabs.Clear(); foreach (string guid in prefabGuids) { string assetPath = UnityEditor.AssetDatabase.GUIDToAssetPath(guid); GameObject prefab = UnityEditor.AssetDatabase.LoadAssetAtPath(assetPath); if (prefab != null && prefab.GetComponent() != null) { _creepPrefabs.Add(prefab); } } #else Debug.LogWarning("[MapGenerator] Creep prefabs not loaded in build. Please assign creep prefabs manually in Inspector."); #endif if (_creepPrefabs.Count == 0) { Debug.LogWarning("[MapGenerator] No creep prefabs loaded!"); } } private Vector3 FindValidCampPosition() { int maxAttempts = 200; for (int attempt = 0; attempt < maxAttempts; attempt++) { Vector3 candidatePosition = GetRandomPositionInPlayableArea(); if (IsValidCreepCampPosition(candidatePosition)) { return candidatePosition; } } return Vector3.zero; } private bool IsValidCreepCampPosition(Vector3 position) { if (Vector2.Distance(new Vector2(position.x, position.z), _corePosition) < minDistanceFromCoreCamps) { return false; } if (Vector2.Distance(new Vector2(position.x, position.z), _barracksPosition) < minDistanceFromBarracksCamps) { return false; } if (Vector2.Distance(new Vector2(position.x, position.z), _initialResourcePosition) < minDistanceFromInitialResource) { return false; } foreach (var campPos in _creepCampPositions) { if (Vector3.Distance(position, campPos) < minDistanceBetweenCamps) { return false; } } if (position.x < -playableAreaWidth / 2f || position.x > playableAreaWidth / 2f) { return false; } if (position.z < startZ || position.z > endZ) { return false; } return true; } private Vector3 FindValidCampPositionForResource(Vector2 resourcePosition) { Vector3 bestPosition = Vector3.zero; float bestDistance = float.MaxValue; int maxAttempts = 50; Debug.Log($"[MapGenerator] FindValidCampPositionForResource: Resource at {resourcePosition}, bounds: X:[{-playableAreaWidth/2f}, {playableAreaWidth/2f}], Z:[{startZ}, {endZ}]"); for (int attempt = 0; attempt < maxAttempts; attempt++) { float angle = Random.Range(0f, 360f); float distance = Random.Range(8f, 20f); Vector3 candidatePosition = new Vector3( resourcePosition.x + Mathf.Cos(angle * Mathf.Deg2Rad) * distance, 1f, resourcePosition.y + Mathf.Sin(angle * Mathf.Deg2Rad) * distance ); if (IsValidCreepCampPosition(candidatePosition)) { if (Vector3.Distance(candidatePosition, new Vector3(resourcePosition.x, 0, resourcePosition.y)) < bestDistance) { bestDistance = Vector3.Distance(candidatePosition, new Vector3(resourcePosition.x, 0, resourcePosition.y)); bestPosition = candidatePosition; } } } if (bestPosition == Vector3.zero) { Debug.LogWarning($"[MapGenerator] Failed to find camp position for resource at {resourcePosition} after {maxAttempts} attempts"); } return bestPosition; } private float CalculateCampStrength(float zPosition, bool isResourceCamp = false) { float normalizedZ = (zPosition - startZ) / (endZ - startZ); float strengthMultiplier = baseCampStrength + (normalizedZ * (strengthIncreasePerZ100 * (endZ - startZ) / 100f)); if (isResourceCamp) { strengthMultiplier *= resourceCampStrengthBonus; } return strengthMultiplier; } private void SpawnCreepCamp(Vector3 position, float strength) { GameObject campObj = Instantiate(creepCampPrefab, position, Quaternion.identity); if (campObj.GetComponent() == null) { var visibility = campObj.AddComponent(); visibility.showInExploredAreas = false; visibility.updateInterval = 0.2f; } CreepCamp creepCamp = campObj.GetComponent(); if (creepCamp == null) { Debug.LogError($"[MapGenerator] Creep camp prefab doesn't have CreepCamp component!"); Destroy(campObj); return; } creepCamp.InitializeCamp(position.z, strength); creepCamp.SetCreepPrefabs(_creepPrefabs); Debug.Log($"[MapGenerator] Camp initialized with {_creepPrefabs.Count} creep prefabs"); NetworkObject networkObj = campObj.GetComponent(); if (networkObj == null) { networkObj = campObj.AddComponent(); } networkObj.Spawn(); } #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() { _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); } if (creepCampPrefab != null) { Gizmos.color = Color.red; foreach (var campPos in _creepCampPositions) { Gizmos.DrawWireSphere(campPos, 8f); } } #endif } #endregion } public struct ResourceData { public Vector2 position; public float baseProduction; public float qualityModifier; public float finalProduction; } }