mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-25 12:54:06 -04:00
Several pathfinding optimizations (#7523)
* Slight pathfinding optimization * Cache canReach() * More optimizations * Use hashset instead of two arraylists
This commit is contained in:
parent
0df499b9fe
commit
a4424d2ab1
@ -840,6 +840,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun endTurn() {
|
fun endTurn() {
|
||||||
|
movement.clearPathfindingCache()
|
||||||
if (currentMovement > 0
|
if (currentMovement > 0
|
||||||
&& getTile().improvementInProgress != null
|
&& getTile().improvementInProgress != null
|
||||||
&& canBuildImprovement(getTile().getTileImprovementInProgress()!!)
|
&& canBuildImprovement(getTile().getTileImprovementInProgress()!!)
|
||||||
@ -886,6 +887,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startTurn() {
|
fun startTurn() {
|
||||||
|
movement.clearPathfindingCache()
|
||||||
currentMovement = getMaxMovement().toFloat()
|
currentMovement = getMaxMovement().toFloat()
|
||||||
attacksThisTurn = 0
|
attacksThisTurn = 0
|
||||||
due = true
|
due = true
|
||||||
|
@ -9,6 +9,8 @@ import com.unciv.models.ruleset.unique.UniqueType
|
|||||||
|
|
||||||
class UnitMovementAlgorithms(val unit: MapUnit) {
|
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!
|
// This function is called ALL THE TIME and should be as time-optimal as possible!
|
||||||
private fun getMovementCostBetweenAdjacentTiles(
|
private fun getMovementCostBetweenAdjacentTiles(
|
||||||
from: TileInfo,
|
from: TileInfo,
|
||||||
@ -193,9 +195,23 @@ class UnitMovementAlgorithms(val unit: MapUnit) {
|
|||||||
val damageFreePath = getShortestPath(destination, true)
|
val damageFreePath = getShortestPath(destination, true)
|
||||||
if (damageFreePath.isNotEmpty()) return damageFreePath
|
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()
|
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)
|
var tilesToCheck = listOf(currentTile)
|
||||||
val movementTreeParents = HashMap<TileInfo, TileInfo?>() // contains a map of "you can get from X to Y in that turn"
|
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 movementThisTurn = unit.currentMovement
|
||||||
var distance = 1
|
var distance = 1
|
||||||
val newTilesToCheck = ArrayList<TileInfo>()
|
val newTilesToCheck = ArrayList<TileInfo>()
|
||||||
val distanceToDestination = HashMap<TileInfo, Float>()
|
|
||||||
var considerZoneOfControl = true // only for first distance!
|
var considerZoneOfControl = true // only for first distance!
|
||||||
val visitedTiles: HashSet<TileInfo> = hashSetOf(currentTile)
|
val visitedTiles: HashSet<TileInfo> = hashSetOf(currentTile)
|
||||||
while (true) {
|
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!
|
considerZoneOfControl = false // by then units would have moved around, we don't need to consider untenable futures when it harms performance!
|
||||||
}
|
}
|
||||||
newTilesToCheck.clear()
|
newTilesToCheck.clear()
|
||||||
distanceToDestination.clear()
|
|
||||||
for (tileToCheck in tilesToCheck) {
|
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) {
|
for (reachableTile in distanceToTilesThisTurn.keys) {
|
||||||
// Avoid damaging terrain on first pass
|
// Avoid damaging terrain on first pass
|
||||||
if (avoidDamagingTerrain && unit.getDamageFromTerrain(reachableTile) > 0)
|
if (avoidDamagingTerrain && unit.getDamageFromTerrain(reachableTile) > 0)
|
||||||
continue
|
continue
|
||||||
if (reachableTile == destination) {
|
if (reachableTile == destination) {
|
||||||
distanceToDestination[tileToCheck] = distanceToTilesThisTurn[reachableTile]!!.totalDistance
|
val path = mutableListOf(destination)
|
||||||
break
|
// 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 {
|
} else {
|
||||||
if (movementTreeParents.containsKey(reachableTile)) continue // We cannot be faster than anything existing...
|
if (movementTreeParents.containsKey(reachableTile)) continue // We cannot be faster than anything existing...
|
||||||
if (!isUnknownTileWeShouldAssumeToBePassable(reachableTile) &&
|
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
|
movementTreeParents[reachableTile] = tileToCheck
|
||||||
newTilesToCheck.add(reachableTile)
|
newTilesToCheck.add(reachableTile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (distanceToDestination.isNotEmpty()) {
|
if (newTilesToCheck.isEmpty()) {
|
||||||
val path = mutableListOf(destination) // Traverse the tree upwards to get the list of tiles leading to the destination,
|
// there is NO PATH (eg blocked by enemy units)
|
||||||
// Get the tile from which the distance to the final tile in least -
|
pathfindingCache.setShortestPathCache(destination, emptyList())
|
||||||
// this is so that when we finally get there, we'll have as many movement points as possible
|
return emptyList()
|
||||||
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()) 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
|
// add newTilesToCheck to visitedTiles so we do not path over these tiles in a later iteration
|
||||||
visitedTiles.addAll(newTilesToCheck)
|
visitedTiles.addAll(newTilesToCheck)
|
||||||
// no need to check tiles that are surrounded by reachable tiles, only need to check the edgemost tiles.
|
// 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,
|
// 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
|
// 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++
|
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>> {
|
fun getAerialPathsToCities(): HashMap<TileInfo, ArrayList<TileInfo>> {
|
||||||
var tilesToCheck = ArrayList<TileInfo>()
|
var tilesToCheck = ArrayList<TileInfo>()
|
||||||
@ -747,6 +773,69 @@ class UnitMovementAlgorithms(val unit: MapUnit) {
|
|||||||
return bfs.getReachedTiles()
|
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>() {
|
class PathsToTilesWithinTurn : LinkedHashMap<TileInfo, UnitMovementAlgorithms.ParentTileAndTotalDistance>() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user