using UnityEngine; using System.Collections.Generic; public class TerrainGenerator { private NoiseGenerator noiseGenerator; private List originalRiverPositions; public TerrainGenerator() { noiseGenerator = new NoiseGenerator(); originalRiverPositions = new List(); } public void GenerateTerrain(MapData mapData) { // Clear previous river positions originalRiverPositions.Clear(); // Generate base heightmap GenerateHeightmap(mapData); // Generate water bodies first (they take priority) GenerateOceans(mapData); GenerateLakes(mapData); GenerateRivers(mapData); // Store river positions after generation but before forests StoreRiverPositions(mapData); // Generate mountains (avoid water areas) GenerateMountains(mapData); // Generate forests (can overlap with rivers initially) GenerateForests(mapData); // Create river paths through forests (this cuts paths after forests are placed) CreateRiverPathsThroughForests(mapData); // Generate shores (around water) GenerateShores(mapData); } private void GenerateHeightmap(MapData mapData) { for (int x = 0; x < mapData.Width; x++) { for (int y = 0; y < mapData.Height; y++) { float height = noiseGenerator.GetNoise(x, y, 0.1f); mapData.GetTile(x, y).height = height; } } } private void GenerateOceans(MapData mapData) { // Generate one large ocean that extends to map edge int edgeChoice = Random.Range(0, 4); // 0=left, 1=right, 2=top, 3=bottom int oceanX, oceanY, oceanWidth, oceanHeight; switch (edgeChoice) { case 0: // Left edge ocean - cover entire left edge oceanX = 0; oceanY = 0; oceanWidth = Random.Range(40, 70); oceanHeight = mapData.Height; break; case 1: // Right edge ocean - cover entire right edge oceanWidth = Random.Range(40, 70); oceanX = mapData.Width - oceanWidth; oceanY = 0; oceanHeight = mapData.Height; break; case 2: // Top edge ocean - cover entire top edge oceanX = 0; oceanHeight = Random.Range(40, 70); oceanY = mapData.Height - oceanHeight; oceanWidth = mapData.Width; break; default: // Bottom edge ocean - cover entire bottom edge oceanX = 0; oceanY = 0; oceanWidth = mapData.Width; oceanHeight = Random.Range(40, 70); break; } // Create the main ocean shape List oceanTiles = new List(); for (int x = oceanX; x < oceanX + oceanWidth && x < mapData.Width; x++) { for (int y = oceanY; y < oceanY + oceanHeight && y < mapData.Height; y++) { // Calculate distance from the map edge that should be ocean float distanceFromOceanEdge = 0f; if (edgeChoice == 0) distanceFromOceanEdge = x; // Left edge else if (edgeChoice == 1) distanceFromOceanEdge = mapData.Width - 1 - x; // Right edge else if (edgeChoice == 2) distanceFromOceanEdge = mapData.Height - 1 - y; // Top edge else distanceFromOceanEdge = y; // Bottom edge // Create probability that decreases from edge with some noise float maxDepth = Mathf.Min(oceanWidth, oceanHeight) * 0.8f; float edgeProbability = 1f - (distanceFromOceanEdge / maxDepth); // Add noise for coastline variation, but keep it more cohesive float noise = noiseGenerator.GetNoise(x, y, 0.1f) * 0.2f; float finalProbability = Mathf.Clamp01(edgeProbability + noise); // Ensure edge tiles are always ocean for proper ocean coverage if (distanceFromOceanEdge < 3 || finalProbability > 0.4f) { oceanTiles.Add(new Vector2Int(x, y)); } } } // Apply ocean tiles foreach (var pos in oceanTiles) { mapData.GetTile(pos.x, pos.y).terrainType = TerrainType.Ocean; mapData.GetTile(pos.x, pos.y).isWalkable = false; } // Fill gaps more aggressively to reduce scattered cells FillTerrainGaps(mapData, TerrainType.Ocean, 3); } private void GenerateLakes(MapData mapData) { int lakeCount = Random.Range(3, 6); for (int i = 0; i < lakeCount; i++) { int attempts = 0; while (attempts < 20) { attempts++; int lakeX = Random.Range(10, mapData.Width - 10); int lakeY = Random.Range(10, mapData.Height - 10); int lakeSize = Random.Range(4, 10); // Check if area is suitable for a lake (no water nearby) if (IsAreaSuitable(mapData, lakeX, lakeY, lakeSize * 2, TerrainType.Plains)) { CreateLake(mapData, lakeX, lakeY, lakeSize); break; } } } } private void CreateLake(MapData mapData, int centerX, int centerY, int size) { List lakeTiles = new List(); for (int x = centerX - size; x <= centerX + size; x++) { for (int y = centerY - size; y <= centerY + size; y++) { if (mapData.IsValidPosition(x, y)) { float distance = Vector2.Distance(new Vector2(x, y), new Vector2(centerX, centerY)); if (distance <= size * Random.Range(0.8f, 1.2f)) { lakeTiles.Add(new Vector2Int(x, y)); } } } } // Apply lake tiles foreach (var pos in lakeTiles) { mapData.GetTile(pos.x, pos.y).terrainType = TerrainType.Lake; mapData.GetTile(pos.x, pos.y).isWalkable = false; } // Fill gaps FillTerrainGaps(mapData, TerrainType.Lake, 1); } private void GenerateRivers(MapData mapData) { var riverGenerator = new RiverGenerator(); riverGenerator.GenerateRivers(mapData); } private void GenerateForests(MapData mapData) { // Generate one huge forest first (if possible) GenerateHugeForest(mapData); // Generate medium forests int mediumForestCount = Random.Range(3, 6); for (int i = 0; i < mediumForestCount; i++) { int attempts = 0; while (attempts < 20) { attempts++; int forestX = Random.Range(10, mapData.Width - 10); int forestY = Random.Range(10, mapData.Height - 10); int forestSize = Random.Range(8, 18); if (IsAreaSuitableForForest(mapData, forestX, forestY, forestSize)) { CreateForest(mapData, forestX, forestY, forestSize); break; } } } // Generate small forests and forest patches int smallForestCount = Random.Range(5, 10); for (int i = 0; i < smallForestCount; i++) { int attempts = 0; while (attempts < 15) { attempts++; int forestX = Random.Range(5, mapData.Width - 5); int forestY = Random.Range(5, mapData.Height - 5); int forestSize = Random.Range(3, 8); if (IsAreaSuitableForForest(mapData, forestX, forestY, forestSize)) { CreateForest(mapData, forestX, forestY, forestSize); break; } } } } private void GenerateHugeForest(MapData mapData) { int attempts = 0; while (attempts < 10) { attempts++; // Try to place a huge forest away from water and edges int forestX = Random.Range(25, mapData.Width - 45); int forestY = Random.Range(25, mapData.Height - 45); int forestSize = Random.Range(25, 40); if (IsAreaSuitableForForest(mapData, forestX, forestY, forestSize)) { CreateHugeForest(mapData, forestX, forestY, forestSize); break; } } } private void CreateHugeForest(MapData mapData, int centerX, int centerY, int size) { List forestTiles = new List(); // Create multiple overlapping forest areas for irregular huge forest int subForests = Random.Range(3, 6); for (int sf = 0; sf < subForests; sf++) { // Each sub-forest has its own center near the main center int subCenterX = centerX + Random.Range(-size / 3, size / 3); int subCenterY = centerY + Random.Range(-size / 3, size / 3); int subSize = Random.Range(size / 2, size); // Generate organic forest shape for this sub-forest for (int x = subCenterX - subSize; x <= subCenterX + subSize; x++) { for (int y = subCenterY - subSize; y <= subCenterY + subSize; y++) { if (mapData.IsValidPosition(x, y)) { float distance = Vector2.Distance(new Vector2(x, y), new Vector2(subCenterX, subCenterY)); float maxDistance = subSize * Random.Range(0.7f, 1.3f); // Add some noise for irregular edges float noise = noiseGenerator.GetNoise(x, y, 0.2f); maxDistance += noise * (subSize * 0.2f); if (distance <= maxDistance) { MapTile tile = mapData.GetTile(x, y); // Allow forests on Plains and Rivers - we'll cut river paths later if (tile.terrainType == TerrainType.Plains || tile.terrainType == TerrainType.River) { forestTiles.Add(new Vector2Int(x, y)); } } } } } } // Apply forest tiles (only on Plains now, rivers are left as paths) foreach (var pos in forestTiles) { MapTile tile = mapData.GetTile(pos.x, pos.y); tile.terrainType = TerrainType.Forest; } // Fill gaps more aggressively for huge forests FillTerrainGaps(mapData, TerrainType.Forest, 2); } private bool IsAreaSuitableForForest(MapData mapData, int centerX, int centerY, int size) { int plainsCount = 0; int totalCount = 0; for (int x = centerX - size / 2; x <= centerX + size / 2; x++) { for (int y = centerY - size / 2; y <= centerY + size / 2; y++) { if (mapData.IsValidPosition(x, y)) { totalCount++; var tile = mapData.GetTile(x, y); if (tile.terrainType == TerrainType.Plains) { plainsCount++; } } } } return totalCount > 0 && (float)plainsCount / totalCount > 0.8f; } private void CreateForest(MapData mapData, int centerX, int centerY, int size) { List forestTiles = new List(); // Determine forest type - some are dense, some are sparse bool isDenseForest = Random.value < 0.4f; // 40% chance for dense forest bool hasClearing = Random.value < 0.3f && size > 6; // 30% chance for clearing in larger forests // Create organic forest shape with variation for (int x = centerX - size; x <= centerX + size; x++) { for (int y = centerY - size; y <= centerY + size; y++) { if (mapData.IsValidPosition(x, y)) { float distance = Vector2.Distance(new Vector2(x, y), new Vector2(centerX, centerY)); float maxDistance = size * Random.Range(0.6f, 1.2f); // Add noise for irregular edges float noise = noiseGenerator.GetNoise(x, y, 0.15f); float noiseInfluence = isDenseForest ? 0.1f : 0.3f; // Dense forests are more regular maxDistance += noise * (size * noiseInfluence); // Create clearings in some forests bool inClearing = false; if (hasClearing) { float clearingDistance = Vector2.Distance(new Vector2(x, y), new Vector2(centerX, centerY)); inClearing = clearingDistance < size * 0.3f && Random.value < 0.7f; } if (distance <= maxDistance && !inClearing) { MapTile tile = mapData.GetTile(x, y); // Allow forests on Plains and Rivers - we'll cut river paths later if (tile.terrainType == TerrainType.Plains || tile.terrainType == TerrainType.River) { // Dense forests fill more completely, sparse forests are more scattered float fillProbability = isDenseForest ? 0.85f : 0.65f; if (Random.value < fillProbability) { forestTiles.Add(new Vector2Int(x, y)); } } } } } } // Apply forest tiles (only on Plains now, rivers are left as paths) foreach (var pos in forestTiles) { MapTile tile = mapData.GetTile(pos.x, pos.y); tile.terrainType = TerrainType.Forest; } // Dense forests get better gap filling int gapFillLevel = isDenseForest ? 2 : 1; FillTerrainGaps(mapData, TerrainType.Forest, gapFillLevel); } private void GenerateMountains(MapData mapData) { // Generate more varied mountain ranges with different sizes int mountainCount = Random.Range(2, 5); // Slightly more mountain ranges for (int i = 0; i < mountainCount; i++) { int attempts = 0; while (attempts < 20) { attempts++; int mountainX = Random.Range(15, mapData.Width - 15); int mountainY = Random.Range(15, mapData.Height - 15); // More varied mountain sizes - some small, some large int mountainSize; float sizeRoll = Random.value; if (sizeRoll < 0.3f) mountainSize = Random.Range(5, 10); // Small mountains else if (sizeRoll < 0.7f) mountainSize = Random.Range(10, 18); // Medium mountains else mountainSize = Random.Range(18, 28); // Large mountain ranges if (IsAreaSuitableForMountains(mapData, mountainX, mountainY, mountainSize)) { CreateMountainRange(mapData, mountainX, mountainY, mountainSize); break; } } } } private bool IsAreaSuitableForMountains(MapData mapData, int centerX, int centerY, int size) { // Check if area is mostly plains (not water or already mountains) int suitableCount = 0; int totalCount = 0; for (int x = centerX - size; x <= centerX + size; x++) { for (int y = centerY - size; y <= centerY + size; y++) { if (mapData.IsValidPosition(x, y)) { totalCount++; var tile = mapData.GetTile(x, y); if (tile.terrainType == TerrainType.Plains || tile.terrainType == TerrainType.Forest) { suitableCount++; } } } } return totalCount > 0 && (float)suitableCount / totalCount > 0.7f; } private void CreateMountainRange(MapData mapData, int centerX, int centerY, int size) { List mountainTiles = new List(); // Create mountain range with multiple peaks - more varied int peaks = Random.Range(2, 6); // More variation in peak count for (int p = 0; p < peaks; p++) { // More random peak placement instead of evenly spaced float angle = Random.Range(0f, 360f) * Mathf.Deg2Rad; float peakDistance = Random.Range(0.2f, 0.5f) * size; // Varied distance from center int peakX = centerX + Mathf.RoundToInt(Mathf.Cos(angle) * peakDistance); int peakY = centerY + Mathf.RoundToInt(Mathf.Sin(angle) * peakDistance); // Each peak has its own size variation int peakSize = Random.Range(size / 3, size); for (int x = peakX - peakSize / 2; x <= peakX + peakSize / 2; x++) { for (int y = peakY - peakSize / 2; y <= peakY + peakSize / 2; y++) { if (mapData.IsValidPosition(x, y)) { float distance = Vector2.Distance(new Vector2(x, y), new Vector2(peakX, peakY)); // More random distance threshold for irregular mountain shapes float maxDistance = peakSize * Random.Range(0.3f, 0.9f); // Add noise for more natural mountain edges float noise = noiseGenerator.GetNoise(x, y, 0.15f); maxDistance += noise * (peakSize * 0.2f); if (distance <= maxDistance) { MapTile tile = mapData.GetTile(x, y); if (!tile.IsWater()) // Don't override water { mountainTiles.Add(new Vector2Int(x, y)); } } } } } } // Apply mountain tiles foreach (var pos in mountainTiles) { mapData.GetTile(pos.x, pos.y).terrainType = TerrainType.Mountain; mapData.GetTile(pos.x, pos.y).isWalkable = false; } // Fill gaps FillTerrainGaps(mapData, TerrainType.Mountain, 1); } private void FillTerrainGaps(MapData mapData, TerrainType terrainType, int maxGapSize) { for (int x = 0; x < mapData.Width; x++) { for (int y = 0; y < mapData.Height; y++) { MapTile tile = mapData.GetTile(x, y); if (tile.terrainType != terrainType) { // Check if this tile is surrounded by the target terrain type int neighborCount = 0; int targetNeighbors = 0; for (int dx = -maxGapSize; dx <= maxGapSize; dx++) { for (int dy = -maxGapSize; dy <= maxGapSize; dy++) { if (dx == 0 && dy == 0) continue; int checkX = x + dx; int checkY = y + dy; if (mapData.IsValidPosition(checkX, checkY)) { neighborCount++; if (mapData.GetTile(checkX, checkY).terrainType == terrainType) { targetNeighbors++; } } } } // If most neighbors are the target terrain, fill this gap if (neighborCount > 0 && (float)targetNeighbors / neighborCount > 0.6f) { if (terrainType == TerrainType.Ocean || terrainType == TerrainType.Lake || terrainType == TerrainType.River) { tile.isWalkable = false; } if (terrainType == TerrainType.Mountain) { tile.isWalkable = false; } tile.terrainType = terrainType; } } } } } private void GenerateShores(MapData mapData) { for (int x = 0; x < mapData.Width; x++) { for (int y = 0; y < mapData.Height; y++) { MapTile tile = mapData.GetTile(x, y); if (tile.terrainType == TerrainType.Plains) { if (IsNearWater(mapData, x, y, 2)) // Reduced range for shores { tile.terrainType = TerrainType.Shore; } } } } } private bool IsNearWater(MapData mapData, int x, int y, int range) { for (int dx = -range; dx <= range; dx++) { for (int dy = -range; dy <= range; dy++) { int checkX = x + dx; int checkY = y + dy; if (mapData.IsValidPosition(checkX, checkY)) { MapTile checkTile = mapData.GetTile(checkX, checkY); if (checkTile.IsWater()) { return true; } } } } return false; } private bool IsAreaSuitable(MapData mapData, int centerX, int centerY, int size, TerrainType requiredType) { int suitableCount = 0; int totalCount = 0; for (int x = centerX - size; x <= centerX + size; x++) { for (int y = centerY - size; y <= centerY + size; y++) { if (mapData.IsValidPosition(x, y)) { totalCount++; if (mapData.GetTile(x, y).terrainType == requiredType) { suitableCount++; } } } } return totalCount > 0 && (float)suitableCount / totalCount > 0.8f; } private void StoreRiverPositions(MapData mapData) { originalRiverPositions.Clear(); for (int x = 0; x < mapData.Width; x++) { for (int y = 0; y < mapData.Height; y++) { var tile = mapData.GetTile(x, y); if (tile.terrainType == TerrainType.River) { originalRiverPositions.Add(new Vector2Int(x, y)); } } } } private void CreateRiverPathsThroughForests(MapData mapData) { // Restore rivers that got covered by forests with more natural-looking paths foreach (var riverPos in originalRiverPositions) { var tile = mapData.GetTile(riverPos.x, riverPos.y); // If this river position got turned into forest, restore it as river if (tile.terrainType == TerrainType.Forest) { tile.terrainType = TerrainType.River; // Create narrower, more natural river paths // Only expand along the main river flow directions (not diagonally) for (int dx = -1; dx <= 1; dx++) { for (int dy = -1; dy <= 1; dy++) { // Skip center (already handled) and diagonal directions (keep forest) if ((dx == 0 && dy == 0) || (Mathf.Abs(dx) == 1 && Mathf.Abs(dy) == 1)) continue; int x = riverPos.x + dx; int y = riverPos.y + dy; if (mapData.IsValidPosition(x, y)) { var neighborTile = mapData.GetTile(x, y); // Only expand river into forests, and with lower probability for narrower channels if (neighborTile.terrainType == TerrainType.Forest && Random.value < 0.3f) { neighborTile.terrainType = TerrainType.River; } } } } } } } }