mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-21 21:30:20 +07:00
Several pathfinding optimizations (#7523)
* Slight pathfinding optimization * Cache canReach() * More optimizations * Use hashset instead of two arraylists
This commit is contained in:

committed by
GitHub

parent
0df499b9fe
commit
a4424d2ab1
@ -840,6 +840,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
}
|
||||
|
||||
fun endTurn() {
|
||||
movement.clearPathfindingCache()
|
||||
if (currentMovement > 0
|
||||
&& getTile().improvementInProgress != null
|
||||
&& canBuildImprovement(getTile().getTileImprovementInProgress()!!)
|
||||
@ -886,6 +887,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
}
|
||||
|
||||
fun startTurn() {
|
||||
movement.clearPathfindingCache()
|
||||
currentMovement = getMaxMovement().toFloat()
|
||||
attacksThisTurn = 0
|
||||
due = true
|
||||
|
@ -9,6 +9,8 @@ import com.unciv.models.ruleset.unique.UniqueType
|
||||
|
||||
class UnitMovementAlgorithms(val unit: MapUnit) {
|
||||
|
||||
private val pathfindingCache = PathfindingCache(unit)
|
||||
|
||||
// This function is called ALL THE TIME and should be as time-optimal as possible!
|
||||
private fun getMovementCostBetweenAdjacentTiles(
|
||||
from: TileInfo,
|
||||
@ -193,9 +195,23 @@ class UnitMovementAlgorithms(val unit: MapUnit) {
|
||||
val damageFreePath = getShortestPath(destination, true)
|
||||
if (damageFreePath.isNotEmpty()) return damageFreePath
|
||||
}
|
||||
if (unit.baseUnit.isWaterUnit()
|
||||
&& destination.neighbors.none { isUnknownTileWeShouldAssumeToBePassable(it) || it.isWater }) {
|
||||
// edge case where this unit is a boat and all of the tiles around the destination are
|
||||
// explored and known to be land so we know a priori that no path exists
|
||||
pathfindingCache.setShortestPathCache(destination, listOf())
|
||||
return listOf()
|
||||
}
|
||||
val cachedPath = pathfindingCache.getShortestPathCache(destination)
|
||||
if (cachedPath.isNotEmpty())
|
||||
return cachedPath
|
||||
|
||||
val currentTile = unit.getTile()
|
||||
if (currentTile.position == destination) return listOf(currentTile) // edge case that's needed, so that workers will know that they can reach their own tile. *sigh*
|
||||
if (currentTile.position == destination) {
|
||||
// edge case that's needed, so that workers will know that they can reach their own tile. *sigh*
|
||||
pathfindingCache.setShortestPathCache(destination, listOf(currentTile))
|
||||
return listOf(currentTile)
|
||||
}
|
||||
|
||||
var tilesToCheck = listOf(currentTile)
|
||||
val movementTreeParents = HashMap<TileInfo, TileInfo?>() // contains a map of "you can get from X to Y in that turn"
|
||||
@ -204,7 +220,6 @@ class UnitMovementAlgorithms(val unit: MapUnit) {
|
||||
var movementThisTurn = unit.currentMovement
|
||||
var distance = 1
|
||||
val newTilesToCheck = ArrayList<TileInfo>()
|
||||
val distanceToDestination = HashMap<TileInfo, Float>()
|
||||
var considerZoneOfControl = true // only for first distance!
|
||||
val visitedTiles: HashSet<TileInfo> = hashSetOf(currentTile)
|
||||
while (true) {
|
||||
@ -213,47 +228,50 @@ class UnitMovementAlgorithms(val unit: MapUnit) {
|
||||
considerZoneOfControl = false // by then units would have moved around, we don't need to consider untenable futures when it harms performance!
|
||||
}
|
||||
newTilesToCheck.clear()
|
||||
distanceToDestination.clear()
|
||||
for (tileToCheck in tilesToCheck) {
|
||||
val distanceToTilesThisTurn = getDistanceToTilesWithinTurn(tileToCheck.position, movementThisTurn, considerZoneOfControl, visitedTiles)
|
||||
val distanceToTilesThisTurn = if (distance == 1) {
|
||||
getDistanceToTiles(considerZoneOfControl) // check cache
|
||||
}
|
||||
else {
|
||||
getDistanceToTilesWithinTurn(tileToCheck.position, movementThisTurn, considerZoneOfControl, visitedTiles)
|
||||
}
|
||||
for (reachableTile in distanceToTilesThisTurn.keys) {
|
||||
// Avoid damaging terrain on first pass
|
||||
if (avoidDamagingTerrain && unit.getDamageFromTerrain(reachableTile) > 0)
|
||||
continue
|
||||
if (reachableTile == destination) {
|
||||
distanceToDestination[tileToCheck] = distanceToTilesThisTurn[reachableTile]!!.totalDistance
|
||||
break
|
||||
val path = mutableListOf(destination)
|
||||
// Traverse the tree upwards to get the list of tiles leading to the destination
|
||||
var intermediateTile = tileToCheck
|
||||
while (intermediateTile != currentTile) {
|
||||
path.add(intermediateTile)
|
||||
intermediateTile = movementTreeParents[intermediateTile]!!
|
||||
}
|
||||
path.reverse() // and reverse in order to get the list in chronological order
|
||||
pathfindingCache.setShortestPathCache(destination, path)
|
||||
return path
|
||||
} else {
|
||||
if (movementTreeParents.containsKey(reachableTile)) continue // We cannot be faster than anything existing...
|
||||
if (!isUnknownTileWeShouldAssumeToBePassable(reachableTile) &&
|
||||
!canMoveTo(reachableTile)) continue // This is a tile that we can't actually enter - either an intermediary tile containing our unit, or an enemy unit/city
|
||||
!canMoveTo(reachableTile)) continue // This is a tile that we can't actually enter - either an intermediary tile containing our unit, or an enemy unit/city
|
||||
movementTreeParents[reachableTile] = tileToCheck
|
||||
newTilesToCheck.add(reachableTile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (distanceToDestination.isNotEmpty()) {
|
||||
val path = mutableListOf(destination) // Traverse the tree upwards to get the list of tiles leading to the destination,
|
||||
// Get the tile from which the distance to the final tile in least -
|
||||
// this is so that when we finally get there, we'll have as many movement points as possible
|
||||
var intermediateTile = distanceToDestination.minByOrNull { it.value }!!.key
|
||||
while (intermediateTile != currentTile) {
|
||||
path.add(intermediateTile)
|
||||
intermediateTile = movementTreeParents[intermediateTile]!!
|
||||
}
|
||||
path.reverse() // and reverse in order to get the list in chronological order
|
||||
return path
|
||||
if (newTilesToCheck.isEmpty()) {
|
||||
// there is NO PATH (eg blocked by enemy units)
|
||||
pathfindingCache.setShortestPathCache(destination, emptyList())
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
if (newTilesToCheck.isEmpty()) return emptyList() // there is NO PATH (eg blocked by enemy units)
|
||||
|
||||
// add newTilesToCheck to visitedTiles so we do not path over these tiles in a later iteration
|
||||
visitedTiles.addAll(newTilesToCheck)
|
||||
// no need to check tiles that are surrounded by reachable tiles, only need to check the edgemost tiles.
|
||||
// Because anything we can reach from intermediate tiles, can be more easily reached by the edgemost tiles,
|
||||
// since we'll have to pass through an edgemost tile in order to reach the destination anyway
|
||||
tilesToCheck = newTilesToCheck.filterNot { tile -> tile.neighbors.all { it in newTilesToCheck || it in tilesToCheck } }
|
||||
tilesToCheck = newTilesToCheck.filterNot { tile -> tile.neighbors.all { it in visitedTiles } }
|
||||
|
||||
distance++
|
||||
}
|
||||
@ -690,7 +708,15 @@ class UnitMovementAlgorithms(val unit: MapUnit) {
|
||||
}
|
||||
|
||||
|
||||
fun getDistanceToTiles(considerZoneOfControl: Boolean = true): PathsToTilesWithinTurn = getDistanceToTilesWithinTurn(unit.currentTile.position, unit.currentMovement, considerZoneOfControl)
|
||||
fun getDistanceToTiles(considerZoneOfControl: Boolean = true): PathsToTilesWithinTurn {
|
||||
val cacheResults = pathfindingCache.getDistanceToTiles(considerZoneOfControl)
|
||||
if (cacheResults != null) {
|
||||
return cacheResults
|
||||
}
|
||||
val distanceToTiles = getDistanceToTilesWithinTurn(unit.currentTile.position, unit.currentMovement, considerZoneOfControl)
|
||||
pathfindingCache.setDistanceToTiles(considerZoneOfControl, distanceToTiles)
|
||||
return distanceToTiles
|
||||
}
|
||||
|
||||
fun getAerialPathsToCities(): HashMap<TileInfo, ArrayList<TileInfo>> {
|
||||
var tilesToCheck = ArrayList<TileInfo>()
|
||||
@ -747,6 +773,69 @@ class UnitMovementAlgorithms(val unit: MapUnit) {
|
||||
return bfs.getReachedTiles()
|
||||
}
|
||||
|
||||
fun clearPathfindingCache() = pathfindingCache.clear()
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache for the results of [UnitMovementAlgorithms.getDistanceToTiles] accounting for zone of control.
|
||||
* [UnitMovementAlgorithms.getDistanceToTiles] is called in numerous places for AI pathfinding so
|
||||
* being able to skip redundant calculations helps out over a long game (especially with high level
|
||||
* AI or a big map). Same thing with [UnitMovementAlgorithms.getShortestPath] which is called in
|
||||
* [UnitMovementAlgorithms.canReach] and in [UnitMovementAlgorithms.headTowards]. Often, the AI will
|
||||
* see if it can reach a tile using canReach then if it can, it will headTowards it. We can cache
|
||||
* the result since otherwise this is a redundant calculation that will find the same path.
|
||||
*/
|
||||
class PathfindingCache(private val unit: MapUnit) {
|
||||
private var shortestPathCache = listOf<TileInfo>()
|
||||
private var destination: TileInfo? = null
|
||||
private val distanceToTilesCache = mutableMapOf<Boolean, PathsToTilesWithinTurn>()
|
||||
private var movement = -1f
|
||||
private var currentTile: TileInfo? = null
|
||||
|
||||
/** Check if the caches are valid (only checking if the unit has moved or consumed movement points;
|
||||
* the isPlayerCivilization check is performed in the functions because we want isValid() == false
|
||||
* to have a specific behavior) */
|
||||
private fun isValid(): Boolean = (movement == unit.currentMovement) && (unit.getTile() == currentTile)
|
||||
|
||||
fun getShortestPathCache(destination: TileInfo): List<TileInfo> {
|
||||
if (unit.civInfo.isPlayerCivilization()) return listOf()
|
||||
if (isValid() && this.destination == destination) {
|
||||
return shortestPathCache
|
||||
}
|
||||
return listOf()
|
||||
}
|
||||
|
||||
fun setShortestPathCache(destination: TileInfo, newShortestPath: List<TileInfo>) {
|
||||
if (unit.civInfo.isPlayerCivilization()) return
|
||||
if (isValid()) {
|
||||
shortestPathCache = newShortestPath
|
||||
this.destination = destination
|
||||
}
|
||||
}
|
||||
|
||||
fun getDistanceToTiles(zoneOfControl: Boolean): PathsToTilesWithinTurn? {
|
||||
if (unit.civInfo.isPlayerCivilization()) return null
|
||||
if (isValid())
|
||||
return distanceToTilesCache[zoneOfControl]
|
||||
return null
|
||||
}
|
||||
|
||||
fun setDistanceToTiles(zoneOfControl: Boolean, paths: PathsToTilesWithinTurn) {
|
||||
if (unit.civInfo.isPlayerCivilization()) return
|
||||
if (!isValid()) {
|
||||
clear() // we want to reset the entire cache at this point
|
||||
}
|
||||
distanceToTilesCache[zoneOfControl] = paths
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
distanceToTilesCache.clear()
|
||||
movement = unit.currentMovement
|
||||
currentTile = unit.getTile()
|
||||
destination = null
|
||||
shortestPathCache = listOf()
|
||||
}
|
||||
}
|
||||
|
||||
class PathsToTilesWithinTurn : LinkedHashMap<TileInfo, UnitMovementAlgorithms.ParentTileAndTotalDistance>() {
|
||||
|
Reference in New Issue
Block a user