Fixed the visual gaps in territory borders (#5446)

* Implemented left/right-concave border segments

* Fixed ConvexConcave border image

It was flipped horizontally.

* Implemented border left/right-concave detection

* Moved border images into their own directory

They're not really icons, after all.

* Cleaned up code a bit and added some more comments

* Applied requested change and consistified some function names

* Removed the old border images

I was sure I already did this, but apparently not.
This commit is contained in:
Arthur van der Staaij
2021-10-11 08:23:28 +02:00
committed by GitHub
parent f0545130e9
commit df39dfd2a8
14 changed files with 784 additions and 670 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 B

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -184,9 +184,24 @@ object HexMath {
(abs(relativeX) + abs(relativeY)).toInt() (abs(relativeX) + abs(relativeY)).toInt()
} }
private val clockPositionToHexVectorMap: Map<Int, Vector2> = mapOf(
0 to Vector2(1f, 1f), // This alias of 12 makes clock modulo logic easier
12 to Vector2(1f, 1f),
2 to Vector2(0f, 1f),
4 to Vector2(-1f, 0f),
6 to Vector2(-1f, -1f),
8 to Vector2(0f, -1f),
10 to Vector2(1f, 0f)
)
/** Returns the hex-space distance corresponding to [clockPosition], or a zero vector if [clockPosition] is invalid */
fun getClockPositionToHexVector(clockPosition: Int): Vector2 {
return clockPositionToHexVectorMap[clockPosition]?: Vector2.Zero
}
// Statically allocate the Vectors (in World coordinates) // Statically allocate the Vectors (in World coordinates)
// of the 6 clock directions for border and road drawing in TileGroup // of the 6 clock directions for border and road drawing in TileGroup
private val clockToWorldVectors: Map<Int,Vector2> = mapOf( private val clockPositionToWorldVectorMap: Map<Int,Vector2> = mapOf(
2 to hex2WorldCoords(Vector2(0f, -1f)), 2 to hex2WorldCoords(Vector2(0f, -1f)),
4 to hex2WorldCoords(Vector2(1f, 0f)), 4 to hex2WorldCoords(Vector2(1f, 0f)),
6 to hex2WorldCoords(Vector2(1f, 1f)), 6 to hex2WorldCoords(Vector2(1f, 1f)),
@ -194,8 +209,9 @@ object HexMath {
10 to hex2WorldCoords(Vector2(-1f, 0f)), 10 to hex2WorldCoords(Vector2(-1f, 0f)),
12 to hex2WorldCoords(Vector2(-1f, -1f)) ) 12 to hex2WorldCoords(Vector2(-1f, -1f)) )
fun getClockDirectionToWorldVector(clockDirection: Int): Vector2 = /** Returns the world/screen-space distance corresponding to [clockPosition], or a zero vector if [clockPosition] is invalid */
clockToWorldVectors[clockDirection] ?: Vector2.Zero fun getClockPositionToWorldVector(clockPosition: Int): Vector2 =
clockPositionToWorldVectorMap[clockPosition] ?: Vector2.Zero
fun getDistanceFromEdge(vector: Vector2, mapParameters: MapParameters): Int { fun getDistanceFromEdge(vector: Vector2, mapParameters: MapParameters): Int {
val x = vector.x.toInt() val x = vector.x.toInt()

View File

@ -157,6 +157,16 @@ open class TileInfo {
// We have to .toList() so that the values are stored together once for caching, // We have to .toList() so that the values are stored together once for caching,
// and the toSequence so that aggregations (like neighbors.flatMap{it.units} don't take up their own space // and the toSequence so that aggregations (like neighbors.flatMap{it.units} don't take up their own space
/** Returns the left shared neighbor of [this] and [neighbor] (relative to the view direction [this]->[neighbor]), or null if there is no such tile. */
fun getLeftSharedNeighbor(neighbor: TileInfo): TileInfo? {
return tileMap.getClockPositionNeighborTile(this,(tileMap.getNeighborTileClockPosition(this, neighbor) - 2) % 12)
}
/** Returns the right shared neighbor [this] and [neighbor] (relative to the view direction [this]->[neighbor]), or null if there is no such tile. */
fun getRightSharedNeighbor(neighbor: TileInfo): TileInfo? {
return tileMap.getClockPositionNeighborTile(this,(tileMap.getNeighborTileClockPosition(this, neighbor) + 2) % 12)
}
@delegate:Transient @delegate:Transient
val height : Int by lazy { val height : Int by lazy {
getAllTerrains().flatMap { it.uniqueObjects } getAllTerrains().flatMap { it.uniqueObjects }

View File

@ -223,7 +223,7 @@ class TileMap {
} }
/** /**
* Returns the clockPosition of [otherTile] seen from [tile]'s position * Returns the clock position of [otherTile] seen from [tile]'s position
* Returns -1 if not neighbors * Returns -1 if not neighbors
*/ */
fun getNeighborTileClockPosition(tile: TileInfo, otherTile: TileInfo): Int { fun getNeighborTileClockPosition(tile: TileInfo, otherTile: TileInfo): Int {
@ -249,12 +249,24 @@ class TileMap {
} }
} }
/**
* Returns the neighbor tile of [tile] at [clockPosition], if it exists.
* Takes world wrap into account
* Returns null if there is no such neighbor tile or if [clockPosition] is not a valid clock position
*/
fun getClockPositionNeighborTile(tile: TileInfo, clockPosition: Int): TileInfo? {
val difference = HexMath.getClockPositionToHexVector(clockPosition)
if (difference == Vector2.Zero) return null
val possibleNeighborPosition = tile.position.cpy().add(difference)
return getIfTileExistsOrNull(possibleNeighborPosition.x.toInt(), possibleNeighborPosition.y.toInt())
}
/** Convert relative direction of [otherTile] seen from [tile]'s position into a vector /** Convert relative direction of [otherTile] seen from [tile]'s position into a vector
* in world coordinates of length sqrt(3), so that it can be used to go from tile center to * in world coordinates of length sqrt(3), so that it can be used to go from tile center to
* the edge of the hex in that direction (meaning the center of the border between the hexes) * the edge of the hex in that direction (meaning the center of the border between the hexes)
*/ */
fun getNeighborTilePositionAsWorldCoords(tile: TileInfo, otherTile: TileInfo): Vector2 = fun getNeighborTilePositionAsWorldCoords(tile: TileInfo, otherTile: TileInfo): Vector2 =
HexMath.getClockDirectionToWorldVector(getNeighborTileClockPosition(tile, otherTile)) HexMath.getClockPositionToWorldVector(getNeighborTileClockPosition(tile, otherTile))
/** /**
* Returns the closest position to (0, 0) outside the map which can be wrapped * Returns the closest position to (0, 0) outside the map which can be wrapped

View File

@ -81,8 +81,19 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
var resourceImage: Actor? = null var resourceImage: Actor? = null
var resource: String? = null var resource: String? = null
class RoadImage {
var roadStatus: RoadStatus = RoadStatus.None
var image: Image? = null
}
data class BorderSegment(
var images: List<Image>,
var isLeftConcave: Boolean = false,
var isRightConcave: Boolean = false,
)
private val roadImages = HashMap<TileInfo, RoadImage>() private val roadImages = HashMap<TileInfo, RoadImage>()
private val borderImages = HashMap<TileInfo, List<Image>>() // map of neighboring tile to border images private val borderSegments = HashMap<TileInfo, BorderSegment>() // map of neighboring tile to border segments
@Suppress("LeakingThis") // we trust TileGroupIcons not to use our `this` in its constructor except storing it for later @Suppress("LeakingThis") // we trust TileGroupIcons not to use our `this` in its constructor except storing it for later
val icons = TileGroupIcons(this) val icons = TileGroupIcons(this)
@ -110,11 +121,6 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
var showEntireMap = UncivGame.Current.viewEntireMapForDebug var showEntireMap = UncivGame.Current.viewEntireMapForDebug
var forMapEditorIcon = false var forMapEditorIcon = false
class RoadImage {
var roadStatus: RoadStatus = RoadStatus.None
var image: Image? = null
}
init { init {
this.setSize(groupSize, groupSize) this.setSize(groupSize, groupSize)
this.addActor(baseLayerGroup) this.addActor(baseLayerGroup)
@ -290,7 +296,7 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
updatePixelMilitaryUnit(false) updatePixelMilitaryUnit(false)
updatePixelCivilianUnit(false) updatePixelCivilianUnit(false)
if (borderImages.isNotEmpty()) clearBorders() if (borderSegments.isNotEmpty()) clearBorders()
icons.update(false,false ,false, false, null) icons.update(false,false ,false, false, null)
@ -402,11 +408,11 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
} }
private fun clearBorders() { private fun clearBorders() {
for (images in borderImages.values) for (borderSegment in borderSegments.values)
for (image in images) for (image in borderSegment.images)
image.remove() image.remove()
borderImages.clear() borderSegments.clear()
} }
private var previousTileOwner: CivilizationInfo? = null private var previousTileOwner: CivilizationInfo? = null
@ -416,7 +422,6 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
// removing all the border images and putting them back again! // removing all the border images and putting them back again!
val tileOwner = tileInfo.getOwner() val tileOwner = tileInfo.getOwner()
if (previousTileOwner != tileOwner) clearBorders() if (previousTileOwner != tileOwner) clearBorders()
previousTileOwner = tileOwner previousTileOwner = tileOwner
@ -425,25 +430,65 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
val civOuterColor = tileInfo.getOwner()!!.nation.getOuterColor() val civOuterColor = tileInfo.getOwner()!!.nation.getOuterColor()
val civInnerColor = tileInfo.getOwner()!!.nation.getInnerColor() val civInnerColor = tileInfo.getOwner()!!.nation.getInnerColor()
for (neighbor in tileInfo.neighbors) { for (neighbor in tileInfo.neighbors) {
var shouldRemoveBorderSegment = false
var shouldAddBorderSegment = false
var borderSegmentShouldBeLeftConcave = false
var borderSegmentShouldBeRightConcave = false
val neighborOwner = neighbor.getOwner() val neighborOwner = neighbor.getOwner()
if (neighborOwner == tileOwner && borderImages.containsKey(neighbor)) // the neighbor used to not belong to us, but now it's ours if (neighborOwner == tileOwner && borderSegments.containsKey(neighbor)) { // the neighbor used to not belong to us, but now it's ours
{ shouldRemoveBorderSegment = true
for (image in borderImages[neighbor]!!)
image.remove()
borderImages.remove(neighbor)
} }
if (neighborOwner != tileOwner && !borderImages.containsKey(neighbor)) { // there should be a border here but there isn't else if (neighborOwner != tileOwner) {
val leftSharedNeighbor = tileInfo.getLeftSharedNeighbor(neighbor)
val rightSharedNeighbor = tileInfo.getRightSharedNeighbor(neighbor)
// If a shared neighbor doesn't exist (because it's past a map edge), we act as if it's our tile for border concave/convex-ity purposes.
// This is because we do not draw borders against non-existing tiles either.
borderSegmentShouldBeLeftConcave = leftSharedNeighbor == null || leftSharedNeighbor.getOwner() == tileOwner
borderSegmentShouldBeRightConcave = rightSharedNeighbor == null || rightSharedNeighbor.getOwner() == tileOwner
if (!borderSegments.containsKey(neighbor)) { // there should be a border here but there isn't
shouldAddBorderSegment = true
}
else if (
borderSegmentShouldBeLeftConcave != borderSegments[neighbor]!!.isLeftConcave ||
borderSegmentShouldBeRightConcave != borderSegments[neighbor]!!.isRightConcave
) { // the concave/convex-ity of the border here is wrong
shouldRemoveBorderSegment = true
shouldAddBorderSegment = true
}
}
if (shouldRemoveBorderSegment) {
for (image in borderSegments[neighbor]!!.images)
image.remove()
borderSegments.remove(neighbor)
}
if (shouldAddBorderSegment) {
val images = mutableListOf<Image>()
val borderSegment = BorderSegment(images, borderSegmentShouldBeLeftConcave, borderSegmentShouldBeRightConcave)
borderSegments[neighbor] = borderSegment
val borderShapeString = when {
borderSegment.isLeftConcave && borderSegment.isRightConcave -> "Concave"
!borderSegment.isLeftConcave && !borderSegment.isRightConcave -> "Convex"
else -> "ConvexConcave"
}
val isConcaveConvex = borderSegment.isLeftConcave && !borderSegment.isRightConcave
val relativeWorldPosition = tileInfo.tileMap.getNeighborTilePositionAsWorldCoords(tileInfo, neighbor) val relativeWorldPosition = tileInfo.tileMap.getNeighborTilePositionAsWorldCoords(tileInfo, neighbor)
// This is some crazy voodoo magic so I'll explain. // This is some crazy voodoo magic so I'll explain.
val images = mutableListOf<Image>()
borderImages[neighbor] = images
val sign = if (relativeWorldPosition.x < 0) -1 else 1 val sign = if (relativeWorldPosition.x < 0) -1 else 1
val angle = sign * (atan(sign * relativeWorldPosition.y / relativeWorldPosition.x) * 180 / PI - 90.0).toFloat() val angle = sign * (atan(sign * relativeWorldPosition.y / relativeWorldPosition.x) * 180 / PI - 90.0).toFloat()
val innerBorderImage = ImageGetter.getImage("OtherIcons/Border-inner") val innerBorderImage = ImageGetter.getImage("BorderImages/${borderShapeString}Inner")
innerBorderImage.width = 38f if (isConcaveConvex) {
innerBorderImage.scaleX = -innerBorderImage.scaleX
}
innerBorderImage.width = hexagonImageWidth
innerBorderImage.setOrigin(Align.center) // THEN the origin is correct, innerBorderImage.setOrigin(Align.center) // THEN the origin is correct,
innerBorderImage.rotateBy(angle) // and the rotation works. innerBorderImage.rotateBy(angle) // and the rotation works.
innerBorderImage.center(this) // move to center of tile innerBorderImage.center(this) // move to center of tile
@ -452,8 +497,11 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
miscLayerGroup.addActor(innerBorderImage) miscLayerGroup.addActor(innerBorderImage)
images.add(innerBorderImage) images.add(innerBorderImage)
val outerBorderImage = ImageGetter.getImage("OtherIcons/Border-outer") val outerBorderImage = ImageGetter.getImage("BorderImages/${borderShapeString}Outer")
outerBorderImage.width = 38f if (isConcaveConvex) {
outerBorderImage.scaleX = -outerBorderImage.scaleX
}
outerBorderImage.width = hexagonImageWidth
outerBorderImage.setOrigin(Align.center) // THEN the origin is correct, outerBorderImage.setOrigin(Align.center) // THEN the origin is correct,
outerBorderImage.rotateBy(angle) // and the rotation works. outerBorderImage.rotateBy(angle) // and the rotation works.
outerBorderImage.center(this) // move to center of tile outerBorderImage.center(this) // move to center of tile