Map Generation Fixes and Tweaks (#4069)

* * Renamed Continents to Two Continents
* Created Four Continents map size
It spawns a vertical and a horizontal ocean to split the map in 4 major continents. Works properly on all sizes and with world wrap

* Fixed Pangea extending to boundaries in rectangular maps
* Unified Pangea for rectangular and hexagonal maps

* Fixed Default map type bias on Rectangular maps. Also removed "pangea effect" on Default: land can spawn with the same probability in the whole map.

* New map type: 4 Corners
* Reverted name change Two Continents to Continents
* Renamed Four Continents to Four Corners (to reflect original name in Civ5)
* Added translation strings

* New map type: 4 Corners
* Reverted name change Two Continents to Continents
* Renamed Four Continents to Four Corners (to reflect original name in Civ5)
* Added translation strings
This commit is contained in:
Federico Luongo
2021-06-08 05:41:02 +02:00
committed by GitHub
parent fadd1684ac
commit 7c9b0e04b4
4 changed files with 64 additions and 51 deletions

View File

@ -212,6 +212,7 @@ Default =
Pangaea = Pangaea =
Perlin = Perlin =
Continents = Continents =
Four Corners =
Archipelago = Archipelago =
Number of City-States = Number of City-States =
One City Challenge = One City Challenge =

View File

@ -102,6 +102,7 @@ object MapShape {
object MapType { object MapType {
const val pangaea = "Pangaea" const val pangaea = "Pangaea"
const val continents = "Continents" const val continents = "Continents"
const val fourCorners = "Four Corners"
const val perlin = "Perlin" const val perlin = "Perlin"
const val archipelago = "Archipelago" const val archipelago = "Archipelago"

View File

@ -1,6 +1,5 @@
package com.unciv.logic.map.mapgenerator package com.unciv.logic.map.mapgenerator
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants import com.unciv.Constants
import com.unciv.logic.HexMath import com.unciv.logic.HexMath
import com.unciv.logic.map.* import com.unciv.logic.map.*
@ -23,6 +22,7 @@ class MapLandmassGenerator(val randomness: MapGenerationRandomness) {
when (tileMap.mapParameters.type) { when (tileMap.mapParameters.type) {
MapType.pangaea -> createPangea(tileMap) MapType.pangaea -> createPangea(tileMap)
MapType.continents -> createTwoContinents(tileMap) MapType.continents -> createTwoContinents(tileMap)
MapType.fourCorners -> createFourCorners(tileMap)
MapType.perlin -> createPerlin(tileMap) MapType.perlin -> createPerlin(tileMap)
MapType.archipelago -> createArchipelago(tileMap) MapType.archipelago -> createArchipelago(tileMap)
MapType.default -> generateLandCellularAutomata(tileMap) MapType.default -> generateLandCellularAutomata(tileMap)
@ -36,12 +36,17 @@ class MapLandmassGenerator(val randomness: MapGenerationRandomness) {
} }
} }
private fun smooth(tileMap: TileMap, randomness: MapGenerationRandomness) { /**
* Smoothen the map by clustering landmass and oceans with probability [threshold].
* [TileInfo]s with more than 3 land neighbours are converted to land.
* [TileInfo]s with more than 3 water neighbours are converted to water.
*/
private fun smoothen(tileMap: TileMap, threshold: Double = 1.0) {
for (tileInfo in tileMap.values) { for (tileInfo in tileMap.values) {
val numberOfLandNeighbors = tileInfo.neighbors.count { it.baseTerrain == Constants.grassland } if (randomness.RNG.nextFloat() > threshold)
if (randomness.RNG.nextFloat() < 0.5f)
continue continue
val numberOfLandNeighbors = tileInfo.neighbors.count { it.baseTerrain == Constants.grassland }
if (numberOfLandNeighbors > 3) if (numberOfLandNeighbors > 3)
tileInfo.baseTerrain = Constants.grassland tileInfo.baseTerrain = Constants.grassland
else if (numberOfLandNeighbors < 3) else if (numberOfLandNeighbors < 3)
@ -69,7 +74,7 @@ class MapLandmassGenerator(val randomness: MapGenerationRandomness) {
val elevationSeed = randomness.RNG.nextInt().toDouble() val elevationSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) { for (tile in tileMap.values) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed) var elevation = randomness.getPerlinNoise(tile, elevationSeed)
elevation = (elevation + getCircularNoise(tile, tileMap)) / 2.0 elevation = (elevation + getEllipticContinent(tile, tileMap)) / 2.0
spawnLandOrWater(tile, elevation, tileMap.mapParameters.waterThreshold.toDouble()) spawnLandOrWater(tile, elevation, tileMap.mapParameters.waterThreshold.toDouble())
} }
} }
@ -83,9 +88,29 @@ class MapLandmassGenerator(val randomness: MapGenerationRandomness) {
} }
} }
private fun getCircularNoise(tileInfo: TileInfo, tileMap: TileMap): Double { private fun createFourCorners(tileMap: TileMap) {
val elevationSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed)
elevation = (elevation + getFourCornersTransform(tile, tileMap)) / 2.0
spawnLandOrWater(tile, elevation, tileMap.mapParameters.waterThreshold.toDouble())
}
}
/**
* Create an elevation map that favors a central elliptic continent spanning over 85% - 95% of
* the map size.
*/
private fun getEllipticContinent(tileInfo: TileInfo, tileMap: TileMap): Double {
val randomScale = randomness.RNG.nextDouble() val randomScale = randomness.RNG.nextDouble()
val distanceFactor = percentualDistanceToCenter(tileInfo, tileMap) val ratio = 0.85 + 0.1 * randomness.RNG.nextDouble()
val a = ratio * tileMap.maxLongitude
val b = ratio * tileMap.maxLatitude
val x = tileInfo.longitude
val y = tileInfo.latitude
val distanceFactor = x * x / (a * a) + y * y / (b * b)
return min(0.3, 1.0 - (5.0 * distanceFactor * distanceFactor + randomScale) / 3.0) return min(0.3, 1.0 - (5.0 * distanceFactor * distanceFactor + randomScale) / 3.0)
} }
@ -107,19 +132,35 @@ class MapLandmassGenerator(val randomness: MapGenerationRandomness) {
return min(0.2, -1.0 + (5.0 * longitudeFactor.pow(0.6f) + randomScale) / 3.0) return min(0.2, -1.0 + (5.0 * longitudeFactor.pow(0.6f) + randomScale) / 3.0)
} }
private fun percentualDistanceToCenter(tileInfo: TileInfo, tileMap: TileMap): Double { private fun getFourCornersTransform(tileInfo: TileInfo, tileMap: TileMap): Double {
val mapRadius = tileMap.mapParameters.mapSize.radius // The idea here is to create a water area separating the two four areas.
if (tileMap.mapParameters.shape == MapShape.hexagonal) // So what we do it create a line of water in the middle - where longitude is close to 0.
return HexMath.getDistance(Vector2.Zero, tileInfo.position).toDouble() / mapRadius val randomScale = randomness.RNG.nextDouble()
else { var longitudeFactor = abs(tileInfo.longitude) / tileMap.maxLongitude
val size = HexMath.getEquivalentRectangularSize(mapRadius) var latitudeFactor = abs(tileInfo.latitude) / tileMap.maxLatitude
return HexMath.getDistance(Vector2.Zero, tileInfo.position).toDouble() / HexMath.getDistance(Vector2.Zero, Vector2(size.x / 2, size.y / 2))
// If this is a world wrap, we want it to be separated on both sides -
// so we make the actual strip of water thinner, but we put it both in the middle of the map and on the edges of the map
if (tileMap.mapParameters.worldWrap) {
longitudeFactor = min(
longitudeFactor,
(tileMap.maxLongitude - abs(tileInfo.longitude)) / tileMap.maxLongitude
) * 1.5f
latitudeFactor = min(
latitudeFactor,
(tileMap.maxLatitude - abs(tileInfo.latitude)) / tileMap.maxLatitude
) * 1.5f
} }
// there's nothing magical about this, it's just what we got from playing around with a lot of different options -
// the numbers can be changed if you find that something else creates better looking continents
val landFactor = min(longitudeFactor, latitudeFactor)
return min(0.2, -1.0 + (5.0 * landFactor.pow(0.5f) + randomScale) / 3.0)
} }
/** /**
* Generates ridged perlin noise. As for parameters see [getPerlinNoise] * Generates ridged perlin noise. As for parameters see [MapGenerationRandomness.getPerlinNoise]
*/ */
private fun getRidgedPerlinNoise(tile: TileInfo, seed: Double, private fun getRidgedPerlinNoise(tile: TileInfo, seed: Double,
nOctaves: Int = 10, nOctaves: Int = 10,
@ -132,46 +173,15 @@ class MapLandmassGenerator(val randomness: MapGenerationRandomness) {
// region Cellular automata // region Cellular automata
private fun generateLandCellularAutomata(tileMap: TileMap) { private fun generateLandCellularAutomata(tileMap: TileMap) {
val numSmooth = 4
// init
for (tile in tileMap.values) { for (tile in tileMap.values) {
val terrainType = getInitialTerrainCellularAutomata(tile, tileMap.mapParameters) tile.baseTerrain =
if (terrainType == TerrainType.Land) tile.baseTerrain = Constants.grassland if (randomness.RNG.nextDouble() < 0.55) Constants.grassland else Constants.ocean
else tile.baseTerrain = Constants.ocean
tile.setTransients() tile.setTransients()
} }
//smooth smoothen(tileMap)
val grassland = Constants.grassland
val ocean = Constants.ocean
for (loop in 0..numSmooth) {
for (tileInfo in tileMap.values) {
val numberOfLandNeighbors = tileInfo.neighbors.count { it.baseTerrain == grassland }
if (tileInfo.baseTerrain == grassland) { // land tile
if (numberOfLandNeighbors < 3)
tileInfo.baseTerrain = ocean
} else { // water tile
if (numberOfLandNeighbors > 3)
tileInfo.baseTerrain = grassland
}
}
}
} }
private fun getInitialTerrainCellularAutomata(tileInfo: TileInfo, mapParameters: MapParameters): TerrainType {
val mapRadius = mapParameters.mapSize.radius
// default
if (HexMath.getDistance(Vector2.Zero, tileInfo.position) > 0.9f * mapRadius) {
if (randomness.RNG.nextDouble() < 0.1) return TerrainType.Land else return TerrainType.Water
}
if (HexMath.getDistance(Vector2.Zero, tileInfo.position) > 0.85f * mapRadius) {
if (randomness.RNG.nextDouble() < 0.2) return TerrainType.Land else return TerrainType.Water
}
if (randomness.RNG.nextDouble() < 0.55) return TerrainType.Land else return TerrainType.Water
}
// endregion // endregion
} }

View File

@ -68,6 +68,7 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed
MapType.default, MapType.default,
MapType.pangaea, MapType.pangaea,
MapType.continents, MapType.continents,
MapType.fourCorners,
MapType.perlin, MapType.perlin,
MapType.archipelago, MapType.archipelago,
if (isEmptyMapAllowed) MapType.empty else null if (isEmptyMapAllowed) MapType.empty else null