Limit camera movement within explored region (#8661)

* Load game resets scroll position

* Limit camera within explored region
This commit is contained in:
Gualdimar 2023-02-23 22:41:21 +02:00 committed by GitHub
parent b5ce086860
commit 713f116400
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 233 additions and 13 deletions

View File

@ -252,7 +252,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
*
* Sets the returned `WorldScreen` as the only active screen.
*/
suspend fun loadGame(newGameInfo: GameInfo): WorldScreen = withThreadPoolContext toplevel@{
suspend fun loadGame(newGameInfo: GameInfo, callFromLoadScreen: Boolean = false): WorldScreen = withThreadPoolContext toplevel@{
val prevGameInfo = gameInfo
gameInfo = newGameInfo
@ -266,7 +266,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
initializeResources(prevGameInfo, newGameInfo)
val isLoadingSameGame = worldScreen != null && prevGameInfo != null && prevGameInfo.gameId == newGameInfo.gameId
val worldScreenRestoreState = if (isLoadingSameGame) worldScreen!!.getRestoreState() else null
val worldScreenRestoreState = if (!callFromLoadScreen && isLoadingSameGame) worldScreen!!.getRestoreState() else null
lateinit var loadingScreen: LoadingScreen

View File

@ -542,6 +542,8 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
}) {
for (unit in civInfo.units.getCivUnits())
unit.updateVisibleTiles(false) // this needs to be done after all the units are assigned to their civs and all other transients are set
if(civInfo.playerType == PlayerType.Human)
civInfo.exploredRegion.setMapParameters(tileMap.mapParameters.worldWrap, tileMap.mapParameters.mapSize.radius) // Required for the correct calculation of the explored region on world wrap maps
civInfo.cache.updateSightAndResources() // only run ONCE and not for each unit - this is a huge performance saver!
// Since this depends on the cities of ALL civilizations,

View File

@ -188,6 +188,9 @@ class Civilization : IsPartOfGameInfoSerialization {
var citiesCreated = 0
var exploredTiles = HashSet<Vector2>()
// Limit camera within explored region
var exploredRegion = ExploredRegion()
fun hasExplored(tile: Tile) = tile.isExplored(this)
var lastSeenImprovement = HashMapVector2<String>()
@ -271,6 +274,7 @@ class Civilization : IsPartOfGameInfoSerialization {
// Cloning it by-pointer is a horrific move, since the serialization would go over it ANYWAY and still lead to concurrency problems.
// Cloning it by iterating on the tilemap values may seem ridiculous, but it's a perfectly thread-safe way to go about it, unlike the other solutions.
toReturn.exploredTiles.addAll(gameInfo.tileMap.values.asSequence().map { it.position }.filter { it in exploredTiles })
toReturn.exploredRegion = exploredRegion.clone()
toReturn.lastSeenImprovement.putAll(lastSeenImprovement)
toReturn.notifications.addAll(notifications)
toReturn.notificationsLog.addAll(notificationsLog)

View File

@ -0,0 +1,165 @@
package com.unciv.logic.civilization
import com.badlogic.gdx.math.Vector2
import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.logic.map.HexMath.getLatitude
import com.unciv.logic.map.HexMath.getLongitude
import com.unciv.logic.map.HexMath.worldFromLatLong
import com.unciv.ui.components.tilegroups.TileGroupMap
import kotlin.math.abs
class ExploredRegion () : IsPartOfGameInfoSerialization {
@Transient
private var isWorldWrap = false
@Transient
private var mapRadius = 0f
@Transient
private val tileRadius = (TileGroupMap.groupSize + 4) * 0.75f
@Transient
private var shouldRecalculateCoords = true
@Transient
private var shouldRestrictX = false
// Top left point of the explored region in stage (x;y) starting from the top left corner
@Transient
private var topLeftStage = Vector2()
// Bottom right point of the explored region in stage (x;y) starting from the top left corner
@Transient
private var bottomRightStage = Vector2()
// Top left point of the explored region in hex (long;lat) from the center of the map
private var topLeft = Vector2()
// Bottom right point of the explored region in hex (long;lat) from the center of the map
private var bottomRight = Vector2()
// Getters
fun shouldRecalculateCoords(): Boolean = shouldRecalculateCoords
fun shouldRestrictX(): Boolean = shouldRestrictX
fun getLeftX(): Float = topLeftStage.x
fun getRightX():Float = bottomRightStage.x
fun getTopY(): Float = topLeftStage.y
fun getBottomY():Float = bottomRightStage.y
fun clone(): ExploredRegion {
val toReturn = ExploredRegion()
toReturn.topLeft = topLeft
toReturn.bottomRight = bottomRight
return toReturn
}
fun setMapParameters(worldWrap: Boolean, radius: Int)
{
isWorldWrap = worldWrap
mapRadius = radius.toFloat()
}
// Check if tilePosition is beyond explored region
fun checkTilePosition(tilePosition: Vector2, explorerPosition: Vector2?) {
var longitude = getLongitude(tilePosition)
val latitude = getLatitude(tilePosition)
// First time call
if (topLeft == Vector2.Zero && bottomRight == Vector2.Zero) {
topLeft = Vector2(longitude, latitude)
bottomRight = Vector2(longitude, latitude)
return
}
// Check X coord
if (topLeft.x >= bottomRight.x) {
if (longitude > topLeft.x) {
// For world wrap maps when the maximumX is reached, we move to a minimumX - 1f
if (isWorldWrap && longitude == mapRadius) longitude = mapRadius * -1f
topLeft.x = longitude
shouldRecalculateCoords = true
} else if (longitude < bottomRight.x) {
// For world wrap maps when the minimumX is reached, we move to a maximumX + 1f
if (isWorldWrap && longitude == (mapRadius * -1f + 1f)) longitude = mapRadius + 1f
bottomRight.x = longitude
shouldRecalculateCoords = true
}
} else {
// When we cross the map edge with world wrap, the vectors are swapped along the x-axis
if (longitude < bottomRight.x && longitude > topLeft.x) {
val rightSideDistance: Float
val leftSideDistance: Float
// If we have explorerPosition, get distance to explorer
// This solves situations when a newly explored cell is in the middle of an unexplored area
if(explorerPosition != null) {
val explorerLongitude = getLongitude(explorerPosition)
rightSideDistance = if(explorerLongitude < 0 && bottomRight.x > 0)
// The explorer is still on the right edge of the map, but has explored over the edge
mapRadius * 2f + explorerLongitude - bottomRight.x
else
abs(explorerLongitude - bottomRight.x)
leftSideDistance = if(explorerLongitude > 0 && topLeft.x < 0)
// The explorer is still on the left edge of the map, but has explored over the edge
mapRadius * 2f - explorerLongitude + topLeft.x
else
abs(topLeft.x - explorerLongitude)
} else {
// If we don't have explorerPosition, we calculate the distance to the edges of the explored region
// e.g. when capitals are revealed
rightSideDistance = bottomRight.x - longitude
leftSideDistance = longitude - topLeft.x
}
// Expand region from the nearest edge
if (rightSideDistance > leftSideDistance) {
topLeft.x = longitude
shouldRecalculateCoords = true
} else {
bottomRight.x = longitude
shouldRecalculateCoords = true
}
}
}
// Check Y coord
if (latitude > topLeft.y) {
topLeft.y = latitude
shouldRecalculateCoords = true
} else if (latitude < bottomRight.y) {
bottomRight.y = latitude
shouldRecalculateCoords = true
}
}
fun calculateStageCoords(mapMaxX: Float, mapMaxY: Float) {
shouldRecalculateCoords = false
// Check if we explored the whole world wrap map horizontally
shouldRestrictX = bottomRight.x - topLeft.x != 1f
// Get world (x;y)
val topLeftWorld = worldFromLatLong(topLeft, tileRadius)
val bottomRightWorld = worldFromLatLong(bottomRight, tileRadius)
// Convert X to the stage coords
val mapCenterX = mapMaxX * 0.5f + tileRadius
var left = mapCenterX + topLeftWorld.x
var right = mapCenterX + bottomRightWorld.x
// World wrap over edge check
if (left > mapMaxX) left = 10f
if (right < 0f) right = mapMaxX - 10f
// Convert Y to the stage coords
val mapCenterY = mapMaxY * 0.5f
val top = mapCenterY-topLeftWorld.y
val bottom = mapCenterY-bottomRightWorld.y
topLeftStage = Vector2(left, top)
bottomRightStage = Vector2(right, bottom)
}
}

View File

@ -1,5 +1,6 @@
package com.unciv.logic.civilization.transients
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.city.City
@ -74,7 +75,7 @@ class CivInfoTransientCache(val civInfo: Civilization) {
}
// This is a big performance
fun updateViewableTiles() {
fun updateViewableTiles(explorerPosition: Vector2? = null) {
setNewViewableTiles()
updateViewableInvisibleTiles()
@ -87,7 +88,7 @@ class CivInfoTransientCache(val civInfo: Civilization) {
// and we never actually iterate on the explored tiles (only check contains()),
// so there's no fear of concurrency problems.
civInfo.viewableTiles.asSequence().forEach { tile ->
tile.setExplored(civInfo, true)
tile.setExplored(civInfo, true, explorerPosition)
}

View File

@ -48,6 +48,15 @@ object HexMath {
return Vector2(x, y)
}
/**
* Convert hex latitude and longitude into world coordinates.
*/
fun worldFromLatLong(vector: Vector2, tileRadius: Float): Vector2 {
val x = vector.x * tileRadius * 1.5f * -1f
val y = vector.y * tileRadius * sqrt(3f) * 0.5f
return Vector2(x, y)
}
/** returns a vector containing width and height a rectangular map should have to have
* approximately the same number of tiles as an hexagonal map given a height/width ratio */
fun getEquivalentRectangularSize(size: Int, ratio: Float = 0.65f): Vector2 {

View File

@ -290,7 +290,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
/**
* Update this unit's cache of viewable tiles and its civ's as well.
*/
fun updateVisibleTiles(updateCivViewableTiles:Boolean = true) {
fun updateVisibleTiles(updateCivViewableTiles:Boolean = true, explorerPosition: Vector2? = null) {
val oldViewableTiles = viewableTiles
viewableTiles = when {
@ -302,7 +302,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
// Set equality automatically determines if anything changed - https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-abstract-set/equals.html
if (updateCivViewableTiles && oldViewableTiles != viewableTiles)
civ.cache.updateViewableTiles()
civ.cache.updateViewableTiles(explorerPosition)
}
fun isActionUntilHealed() = action?.endsWith("until healed") == true
@ -607,7 +607,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
promotions.addPromotion(promotion, true)
}
updateVisibleTiles()
updateVisibleTiles(true, currentTile.position)
}
fun putInTile(tile: Tile) {

View File

@ -245,10 +245,12 @@ open class Tile : IsPartOfGameInfoSerialization {
return exploredBy.contains(player.civName) || player.exploredTiles.contains(position)
}
fun setExplored(player: Civilization, isExplored: Boolean) {
fun setExplored(player: Civilization, isExplored: Boolean, explorerPosition: Vector2? = null) {
if (isExplored) {
exploredBy.add(player.civName)
player.exploredTiles.add(position)
if(player.playerType == PlayerType.Human)
player.exploredRegion.checkTilePosition(position, explorerPosition)
} else {
exploredBy.remove(player.civName)
player.exploredTiles.remove(position)

View File

@ -290,8 +290,8 @@ open class ZoomableScrollPane(
onPanStartListener?.invoke()
}
setScrollbarsVisible(true)
scrollX -= deltaX
scrollY += deltaY
scrollX = restrictX(deltaX)
scrollY = restrictY(deltaY)
when {
continuousScrollingX && scrollPercentX >= 1 && deltaX < 0 -> {
@ -316,6 +316,9 @@ open class ZoomableScrollPane(
}
}
open fun restrictX(deltaX: Float): Float = scrollX - deltaX
open fun restrictY(deltaY:Float): Float = scrollY + deltaY
override fun getFlickScrollListener(): ActorGestureListener {
return FlickScrollListener()
}

View File

@ -123,7 +123,7 @@ class LoadGameScreen : LoadOrSaveScreen() {
try {
// This is what can lead to ANRs - reading the file and setting the transients, that's why this is in another thread
val loadedGame = game.files.loadGameByName(selectedSave)
game.loadGame(loadedGame)
game.loadGame(loadedGame, true)
} catch (notAPlayer: UncivShowableException) {
launchOnGLThread {
val (message) = getLoadExceptionMessage(notAPlayer)
@ -146,7 +146,7 @@ class LoadGameScreen : LoadOrSaveScreen() {
try {
val clipboardContentsString = Gdx.app.clipboard.contents.trim()
val loadedGame = UncivFiles.gameInfoFromString(clipboardContentsString)
game.loadGame(loadedGame)
game.loadGame(loadedGame, true)
} catch (ex: Exception) {
launchOnGLThread { handleLoadGameException(ex, "Could not load game from clipboard!") }
}
@ -171,7 +171,7 @@ class LoadGameScreen : LoadOrSaveScreen() {
handleLoadGameException(result.exception!!, "Could not load game from custom location!")
} else if (result.isSuccessful()) {
Concurrency.run {
game.loadGame(result.gameData!!)
game.loadGame(result.gameData!!, true)
}
}
}

View File

@ -777,6 +777,40 @@ class WorldMapHolder(
super.reloadMaxZoom()
}
override fun restrictX(deltaX: Float): Float {
val exploredRegion = worldScreen.viewingCiv.exploredRegion
var result = scrollX - deltaX
if (exploredRegion.shouldRecalculateCoords()) exploredRegion.calculateStageCoords(maxX, maxY)
if (!exploredRegion.shouldRestrictX()) return result
val leftX = exploredRegion.getLeftX()
val rightX = exploredRegion.getRightX()
if (deltaX < 0 && scrollX <= rightX && result > rightX)
result = rightX
else if (deltaX > 0 && scrollX >= leftX && result < leftX)
result = leftX
return result
}
override fun restrictY(deltaY: Float): Float {
val exploredRegion = worldScreen.viewingCiv.exploredRegion
var result = scrollY + deltaY
if (exploredRegion.shouldRecalculateCoords()) exploredRegion.calculateStageCoords(maxX, maxY)
val topY = exploredRegion.getTopY()
val bottomY = exploredRegion.getBottomY()
if (result < topY) result = topY
else if (result > bottomY) result = bottomY
return result
}
// For debugging purposes
override fun draw(batch: Batch?, parentAlpha: Float) = super.draw(batch, parentAlpha)