클라이언트 접속 전에 스폰되어 있는 오브젝트의 경우, Ownership이 Distributable일 경우 클라이언트 접속 시점에 Ownership을 호스트로부터 분배받는다. 서버만 데이터를 수정해야 하는 환경이기 때문에 대부분 Distributable 대신 None을 사용하면 된다.
934 lines
32 KiB
C#
934 lines
32 KiB
C#
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;
|
|
|
|
[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 _corePosition;
|
|
private Vector2 _barracksPosition;
|
|
|
|
[Header("Resource Generation")]
|
|
[Tooltip("자원 프리팹")]
|
|
public GameObject resourcePrefab;
|
|
[Tooltip("기본 자원 위치 (초기 스폰 지점)")]
|
|
public Vector2 initialResourcePosition = new Vector2(0f, 5f);
|
|
[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<ObstacleEntry> obstacles = new List<ObstacleEntry>();
|
|
[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, 1000)]
|
|
[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;
|
|
|
|
[Tooltip("크립 스폰 비용 예산")]
|
|
[SerializeField] private float campCostBudget = 10f;
|
|
|
|
[Tooltip("스폰 반경 (캠프 주변)")]
|
|
[SerializeField] private float spawnRadius = 5f;
|
|
|
|
[Tooltip("최대 크립 스폰 시도 횟수")]
|
|
[SerializeField] private int maxSpawnAttempts = 50;
|
|
|
|
[Header("Generation Order")]
|
|
[Tooltip("자원 먼저 생성 후 장애물 생성 (true) 또는 그 반대 (false)")]
|
|
[SerializeField] private bool generateResourcesFirst = true;
|
|
|
|
private ResourceData[] _generatedResources;
|
|
private float _globalProductionMultiplier = 1f;
|
|
private List<Vector3> _spawnedPositions = new List<Vector3>();
|
|
private List<Vector3> _creepCampPositions = new List<Vector3>();
|
|
private List<GameObject> _creepPrefabs = new List<GameObject>();
|
|
|
|
[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 = FindFirstObjectByType<Core>();
|
|
if (core != null)
|
|
{
|
|
_corePosition = new Vector2(core.transform.position.x, core.transform.position.z);
|
|
}
|
|
else
|
|
{
|
|
_corePosition = Vector2.zero;
|
|
}
|
|
|
|
GameObject barracks = GameObject.Find("Worker Hall");
|
|
if (barracks != null)
|
|
{
|
|
_barracksPosition = new Vector2(barracks.transform.position.x, barracks.transform.position.z);
|
|
}
|
|
else
|
|
{
|
|
_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)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool success = false;
|
|
|
|
for (int attempt = 0; attempt < maxResourceGenerationAttempts; attempt++)
|
|
{
|
|
if (TryGenerateResources(out var resources))
|
|
{
|
|
_generatedResources = resources;
|
|
success = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!success)
|
|
{
|
|
GenerateResourceFallbackLayout();
|
|
}
|
|
|
|
SpawnResources();
|
|
}
|
|
|
|
private bool TryGenerateResources(out ResourceData[] resources)
|
|
{
|
|
resources = null;
|
|
int additionalResourceCount = Random.Range(minResourceCount, maxResourceCount + 1);
|
|
ResourceData[] tempResources = new ResourceData[additionalResourceCount + 1];
|
|
|
|
// Add initial resource at index 0
|
|
tempResources[0] = new ResourceData
|
|
{
|
|
position = initialResourcePosition,
|
|
baseProduction = initialResourceProduction,
|
|
qualityModifier = 0f,
|
|
finalProduction = initialResourceProduction
|
|
};
|
|
|
|
// Generate additional resources starting from index 1
|
|
for (int i = 1; i < tempResources.Length; 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;
|
|
|
|
// Initial resource uses fixed position
|
|
if (currentIndex == 0)
|
|
{
|
|
position = initialResourcePosition;
|
|
return true;
|
|
}
|
|
|
|
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 = 0f;
|
|
|
|
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 = initialResourcePosition, baseProduction = initialResourceProduction, qualityModifier = 0f, finalProduction = initialResourceProduction },
|
|
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 = 0f;
|
|
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()
|
|
{
|
|
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<Resource>();
|
|
if (resource == null)
|
|
{
|
|
Destroy(resourceObj);
|
|
continue;
|
|
}
|
|
|
|
resource.InitializeQuality(_generatedResources[i].qualityModifier);
|
|
|
|
NetworkObject networkObj = resourceObj.GetComponent<NetworkObject>();
|
|
if (networkObj != null)
|
|
{
|
|
networkObj.SpawnWithOwnership(NetworkManager.ServerClientId);
|
|
_spawnedPositions.Add(new Vector3(_generatedResources[i].position.x, 1f, _generatedResources[i].position.y));
|
|
}
|
|
else
|
|
{
|
|
Destroy(resourceObj);
|
|
}
|
|
}
|
|
}
|
|
|
|
#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)
|
|
{
|
|
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<NetworkObject>();
|
|
if (networkObj == null)
|
|
{
|
|
networkObj = obstacle.AddComponent<NetworkObject>();
|
|
}
|
|
|
|
networkObj.Spawn();
|
|
|
|
if (obstacle.GetComponent<FogOfWarVisibility>() == null)
|
|
{
|
|
var visibility = obstacle.AddComponent<FogOfWarVisibility>();
|
|
visibility.showInExploredAreas = false;
|
|
visibility.updateInterval = 0.2f;
|
|
}
|
|
|
|
_spawnedPositions.Add(randomPos);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private Vector3 GetRandomPositionInPlayableArea()
|
|
{
|
|
float x = Random.Range(-playableAreaWidth / 2f, playableAreaWidth / 2f);
|
|
float z = Random.Range(startZ, endZ);
|
|
|
|
Vector3 result = new Vector3(x, 1, 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)
|
|
{
|
|
return;
|
|
}
|
|
|
|
LoadCreepPrefabs();
|
|
|
|
if (_creepPrefabs.Count == 0)
|
|
{
|
|
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<GameObject>(assetPath);
|
|
|
|
if (prefab != null && prefab.GetComponent<CreepDataComponent>() != null)
|
|
{
|
|
_creepPrefabs.Add(prefab);
|
|
}
|
|
}
|
|
#else
|
|
// Creep prefabs not available in build - please assign manually
|
|
#endif
|
|
}
|
|
|
|
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;
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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<FogOfWarVisibility>() == null)
|
|
{
|
|
var visibility = campObj.AddComponent<FogOfWarVisibility>();
|
|
visibility.showInExploredAreas = false;
|
|
visibility.updateInterval = 0.2f;
|
|
}
|
|
|
|
CreepCamp creepCamp = campObj.GetComponent<CreepCamp>();
|
|
if (creepCamp == null)
|
|
{
|
|
Debug.LogError($"[MapGenerator] Creep camp prefab doesn't have CreepCamp component!");
|
|
Destroy(campObj);
|
|
return;
|
|
}
|
|
|
|
creepCamp.InitializeCamp(position.z, strength, campCostBudget, spawnRadius, maxSpawnAttempts);
|
|
creepCamp.SetCreepPrefabs(_creepPrefabs);
|
|
|
|
NetworkObject networkObj = campObj.GetComponent<NetworkObject>();
|
|
if (networkObj == null)
|
|
{
|
|
networkObj = campObj.AddComponent<NetworkObject>();
|
|
}
|
|
|
|
networkObj.Spawn();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Public Methods
|
|
|
|
public float GetTotalProduction()
|
|
{
|
|
if (_generatedResources == null)
|
|
return 0f;
|
|
|
|
float total = 0f;
|
|
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;
|
|
}
|
|
}
|