-
Notifications
You must be signed in to change notification settings - Fork 172
Description
Let's say that if we have many units requesting a path at the same time, all paths are counted as one frame. Even if each path is short, 50 paths at a time cause lag. Before A*, a prepass (beam to the target) is launched. On long paths, this is expensive and useless, as the beam almost always hits a wall.
I suggest adding MAX_PATHFINDS_PER_FRAME = 3 and an additional condition to the processPathfindQueue() loop.
Then it turns out that unprocessed requests will remain in the queue and will be executed in the following frames. With a large cluster of units, some of them will start moving with a delay of several frames.
And PREPASS_MAX_MANHATTAN = 32
In theory it will only be called when the distance from the current cell to the target does not exceed the threshold.
Add new constants:
constexpr const Int MAX_PATHFINDS_PER_FRAME = 3;
constexpr const Int PREPASS_MAX_MANHATTAN = 32;
constexpr const Int GROUND_PATH_CELL_LIMIT = 2500;File GameEngine/Source/GameLogic/AI/AIPathfind.cpp
Pathfinder::processPathfindQueue()
Add MAX_PATHFINDS_PER_FRAME condition to the loop. Unprocessed requests remain in the queue and will be executed in the following frames.
m_cumulativeCellsAllocated = 0;
#ifdef DEBUG_QPF
Int pathsFound = 0;
#endif
Int pathsThisFrame = 0;
while (m_cumulativeCellsAllocated < PATHFIND_CELLS_PER_FRAME &&
m_queuePRTail!=m_queuePRHead &&
pathsThisFrame < MAX_PATHFINDS_PER_FRAME) {
Object *obj = TheGameLogic->findObjectByID(m_queuedPathfindRequests[m_queuePRHead]);
m_queuedPathfindRequests[m_queuePRHead] = INVALID_ID;
if (obj) {
AIUpdateInterface *ai = obj->getAIUpdateInterface();
if (ai) {
ai->doPathfind(this);
pathsThisFrame++;
#ifdef DEBUG_QPF
pathsFound++;
#endif
}
}
m_queuePRHead = m_queuePRHead+1;
if (m_queuePRHead >= PATHFIND_QUEUE_LEN) {
m_queuePRHead = 0;
}
}processPathfindQueue() full click to open
void Pathfinder::processPathfindQueue()
{
//USE_PERF_TIMER(processPathfindQueue)
if (!m_isMapReady) {
return;
}
#ifdef DEBUG_QPF
#ifdef DEBUG_LOGGING
Int startTimeMS = ::GetTickCount();
__int64 startTime64;
double timeToUpdate=0.0f;
__int64 endTime64,freq64;
QueryPerformanceFrequency((LARGE_INTEGER *)&freq64);
QueryPerformanceCounter((LARGE_INTEGER *)&startTime64);
#endif
#endif
if (m_zoneManager.needToCalculateZones()) {
m_zoneManager.calculateZones(m_map, m_layers, m_extent);
return;
}
// Get the current logical extent.
Region3D terrainExtent;
TheTerrainLogic->getExtent( &terrainExtent );
IRegion2D bounds;
bounds.lo.x = REAL_TO_INT_FLOOR(terrainExtent.lo.x / PATHFIND_CELL_SIZE_F);
bounds.hi.x = REAL_TO_INT_FLOOR(terrainExtent.hi.x / PATHFIND_CELL_SIZE_F);
bounds.lo.y = REAL_TO_INT_FLOOR(terrainExtent.lo.y / PATHFIND_CELL_SIZE_F);
bounds.hi.y = REAL_TO_INT_FLOOR(terrainExtent.hi.y / PATHFIND_CELL_SIZE_F);
bounds.hi.x--;
bounds.hi.y--;
m_logicalExtent = bounds;
m_cumulativeCellsAllocated = 0; // Number of pathfind cells examined.
#ifdef DEBUG_QPF
Int pathsFound = 0;
#endif
Int pathsThisFrame = 0;
while (m_cumulativeCellsAllocated < PATHFIND_CELLS_PER_FRAME &&
m_queuePRTail!=m_queuePRHead &&
pathsThisFrame < MAX_PATHFINDS_PER_FRAME) {
Object *obj = TheGameLogic->findObjectByID(m_queuedPathfindRequests[m_queuePRHead]);
m_queuedPathfindRequests[m_queuePRHead] = INVALID_ID;
if (obj) {
AIUpdateInterface *ai = obj->getAIUpdateInterface();
if (ai) {
ai->doPathfind(this);
pathsThisFrame++;
#ifdef DEBUG_QPF
pathsFound++;
#endif
}
}
m_queuePRHead = m_queuePRHead+1;
if (m_queuePRHead >= PATHFIND_QUEUE_LEN) {
m_queuePRHead = 0;
}
}
#ifdef DEBUG_QPF
if (pathsFound>0) {
#ifdef DEBUG_LOGGING
QueryPerformanceCounter((LARGE_INTEGER *)&endTime64);
timeToUpdate = ((double)(endTime64-startTime64) / (double)(freq64));
if (timeToUpdate>0.01f)
{
DEBUG_LOG(("%d Pathfind queue: %d paths, %d cells --", TheGameLogic->getFrame(), pathsFound, m_cumulativeCellsAllocated));
DEBUG_LOG(("time %f (%f)", timeToUpdate, (::GetTickCount()-startTimeMS)/1000.0f));
}
#endif
}
#endif
#if defined(RTS_DEBUG)
doDebugIcons();
#endif
}
Pathfinder::examineNeighboringCells()
Add Manhattan distance check before prepass. Prepass will only be called when the distance from the current cell to the target does not exceed the threshold.
if (attackDistance==NO_ATTACK && !m_isTunneling && !locomotorSet.isDownhillOnly() && goalCell) {
ExamineCellsStruct info;
info.thePathfinder = this;
info.theLoco = &locomotorSet;
info.centerInCell = centerInCell;
info.radius = radius;
info.obj = obj;
info.isHuman = isHuman;
info.goalCell = goalCell;
ICoord2D start, end;
start.x = parentCell->getXIndex();
start.y = parentCell->getYIndex();
end.x = goalCell->getXIndex();
end.y = goalCell->getYIndex();
Int manhattan = IABS(start.x - end.x) + IABS(start.y - end.y);
if (manhattan <= PREPASS_MAX_MANHATTAN) {
iterateCellsAlongLine(start, end, parentCell->getLayer(), examineCellsCallback, &info);
}
}examineNeighboringCells() full click to open
Int Pathfinder::examineNeighboringCells(PathfindCell *parentCell, PathfindCell *goalCell, const LocomotorSet& locomotorSet,
Bool isHuman, Bool centerInCell, Int radius, const ICoord2D &startCellNdx,
const Object *obj, Int attackDistance)
{
Bool canPathThroughUnits = false;
if (obj && obj->getAIUpdateInterface()) {
canPathThroughUnits = obj->getAIUpdateInterface()->canPathThroughUnits();
}
Bool isCrusher = obj ? obj->getCrusherLevel() > 0 : false;
if (attackDistance==NO_ATTACK && !m_isTunneling && !locomotorSet.isDownhillOnly() && goalCell) {
ExamineCellsStruct info;
info.thePathfinder = this;
info.theLoco = &locomotorSet;
info.centerInCell = centerInCell;
info.radius = radius;
info.obj = obj;
info.isHuman = isHuman;
info.goalCell = goalCell;
ICoord2D start, end;
start.x = parentCell->getXIndex();
start.y = parentCell->getYIndex();
end.x = goalCell->getXIndex();
end.y = goalCell->getYIndex();
Int manhattan = IABS(start.x - end.x) + IABS(start.y - end.y);
if (manhattan <= PREPASS_MAX_MANHATTAN) {
iterateCellsAlongLine(start, end, parentCell->getLayer(), examineCellsCallback, &info);
}
}
Int cellCount = 0;
// expand search to neighboring orthogonal cells
static ICoord2D delta[] =
{
{ 1, 0 }, { 0, 1 }, { -1, 0 }, { 0, -1 },
{ 1, 1 }, { -1, 1 }, { -1, -1 }, { 1, -1 }
};
const Int numNeighbors = 8;
const Int firstDiagonal = 4;
ICoord2D newCellCoord;
PathfindCell *newCell;
const Int adjacent[5] = {0, 1, 2, 3, 0};
Bool neighborFlags[8] = { 0 };
UnsignedInt newCostSoFar = 0;
for( int i=0; i<numNeighbors; i++ )
{
neighborFlags[i] = false;
// determine neighbor cell to try
newCellCoord.x = parentCell->getXIndex() + delta[i].x;
newCellCoord.y = parentCell->getYIndex() + delta[i].y;
// get the neighboring cell
newCell = getCell(parentCell->getLayer(), newCellCoord.x, newCellCoord.y );
// check if cell is on the map
if (!newCell)
continue;
Bool notZonePassable = false;
if ((newCell->getLayer()==LAYER_GROUND) && !m_zoneManager.isPassable(newCellCoord.x, newCellCoord.y)) {
notZonePassable = true;
}
// check if new cell is in logical map. (computer can move off logical map)
if (isHuman && checkCellOutsideExtents(newCellCoord))
continue;
// check if this neighbor cell is already on the open (waiting to be tried)
// or closed (already tried) lists
if ( newCell->hasInfo() && (newCell->getOpen() || newCell->getClosed()) )
continue;
if (i>=firstDiagonal) {
// make sure one of the adjacent sides is open.
if (!neighborFlags[adjacent[i-4]] && !neighborFlags[adjacent[i-3]]) {
continue;
}
}
// do the gravity check here
if ( locomotorSet.isDownhillOnly() )
{
Coord3D fromPos;
fromPos.x = parentCell->getXIndex() * PATHFIND_CELL_SIZE_F ;
fromPos.y = parentCell->getYIndex() * PATHFIND_CELL_SIZE_F ;
fromPos.z = TheTerrainLogic->getGroundHeight(fromPos.x , fromPos.y);
Coord3D toPos;
toPos.x = newCellCoord.x * PATHFIND_CELL_SIZE_F ;
toPos.y = newCellCoord.y * PATHFIND_CELL_SIZE_F ;
toPos.z = TheTerrainLogic->getGroundHeight(toPos.x , toPos.y);
if ( fromPos.z < toPos.z )
continue;
}
Bool movementValid = validMovementPosition(isCrusher, locomotorSet.getValidSurfaces(), newCell, parentCell);
Bool dozerHack = false;
if (!movementValid && obj->isKindOf(KINDOF_DOZER) && newCell->getType() == PathfindCell::CELL_OBSTACLE) {
Object* obstacle = TheGameLogic->findObjectByID(newCell->getObstacleID());
if (obstacle && !(obj->getRelationship(obstacle) == ENEMIES)) {
movementValid = true;
dozerHack = true;
}
}
if (!movementValid && !m_isTunneling) {
continue;
}
if (!dozerHack)
neighborFlags[i] = true;
TCheckMovementInfo info;
info.cell = newCellCoord;
info.layer = parentCell->getLayer();
info.centerInCell = centerInCell;
info.radius = radius;
info.considerTransient = false;
info.acceptableSurfaces = locomotorSet.getValidSurfaces();
Int dx = newCellCoord.x-startCellNdx.x;
Int dy = newCellCoord.y-startCellNdx.y;
if (dx<0) dx = -dx;
if (dy<0) dy = -dy;
if (dx>1+radius) info.considerTransient = false;
if (dy>1+radius) info.considerTransient = false;
if (!checkForMovement(obj, info) || info.enemyFixed) {
if (!m_isTunneling) {
continue;
}
movementValid = false;
}
if (movementValid && !newCell->getPinched()) {
//Note to self - only turn off tunneling after check for movement.jba.
m_isTunneling = false;
}
if (!newCell->hasInfo()) {
if (!newCell->allocateInfo(newCellCoord)) {
// Out of cells for pathing...
return cellCount;
}
cellCount++;
}
newCostSoFar = newCell->costSoFar( parentCell );
if (info.allyMoving && dx<10 && dy<10) {
newCostSoFar += 3*COST_DIAGONAL;
}
if (newCell->getType() == PathfindCell::CELL_CLIFF && !newCell->getPinched() ) {
Coord3D fromPos;
fromPos.x = parentCell->getXIndex() * PATHFIND_CELL_SIZE_F ;
fromPos.y = parentCell->getYIndex() * PATHFIND_CELL_SIZE_F ;
fromPos.z = TheTerrainLogic->getGroundHeight(fromPos.x , fromPos.y);
Coord3D toPos;
toPos.x = newCellCoord.x * PATHFIND_CELL_SIZE_F ;
toPos.y = newCellCoord.y * PATHFIND_CELL_SIZE_F ;
toPos.z = TheTerrainLogic->getGroundHeight(toPos.x , toPos.y);
if ( fabs(fromPos.z - toPos.z)<PATHFIND_CELL_SIZE_F) {
newCostSoFar += 7*COST_DIAGONAL;
}
} else if (newCell->getPinched()) {
newCostSoFar += COST_ORTHOGONAL;
}
newCell->setBlockedByAlly(false);
if (info.allyFixedCount>0) {
#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING
newCostSoFar += 3*COST_DIAGONAL*info.allyFixedCount;
#else
newCostSoFar += 3*COST_DIAGONAL;
#endif
if (!canPathThroughUnits)
newCell->setBlockedByAlly(true);
}
Int costRemaining = 0;
if (goalCell) {
if (attackDistance == NO_ATTACK) {
costRemaining = newCell->costToGoal( goalCell );
} else {
dx = newCellCoord.x - goalCell->getXIndex();
dy = newCellCoord.y - goalCell->getYIndex();
costRemaining = COST_ORTHOGONAL*sqrt(dx*dx + dy*dy);
costRemaining -= attackDistance/2;
if (costRemaining<0)
costRemaining=0;
if (info.allyGoal) {
if (obj->isKindOf(KINDOF_VEHICLE)) {
newCostSoFar += 3*COST_ORTHOGONAL;
} else {
// Infantry can pass through infantry.
newCostSoFar += COST_ORTHOGONAL;
}
}
}
}
if (notZonePassable) {
newCostSoFar += 100*COST_ORTHOGONAL;
}
if (newCell->getType()==PathfindCell::CELL_OBSTACLE) {
newCostSoFar += 100*COST_ORTHOGONAL;
}
if (m_isTunneling) {
if (!validMovementPosition( isCrusher, locomotorSet.getValidSurfaces(), newCell, parentCell )) {
newCostSoFar += 10*COST_ORTHOGONAL;
}
}
newCell->setCostSoFar(newCostSoFar);
// keep track of path we're building - point back to cell we moved here from
newCell->setParentCell(parentCell) ;
if (m_isTunneling) {
costRemaining = 0; // find the closest valid cell.
}
newCell->setTotalCost(newCell->getCostSoFar() + costRemaining) ;
// if newCell was on closed list, remove it from the list
if (newCell->getClosed())
newCell->removeFromClosedList( m_closedList );
// if the newCell was already on the open list, remove it so it can be re-inserted in order
if (newCell->getOpen())
newCell->removeFromOpenList( m_openList );
// insert newCell in open list such that open list is sorted, smallest total path cost first
newCell->putOnSortedOpenList( m_openList );
}
return cellCount;
}
Pathfinder::findGroundPath()
Two changes inside the main A* loop:
Cell limit placed after goalCell check so that a ready path is never discarded
// take head cell off of open list - it has lowest estimated total path cost
parentCell = m_openList.getHead();
parentCell->removeFromOpenList(m_openList);
if (parentCell == goalCell)
{
// success - found a path to the goal
// ... (unchanged) ...
return path;
}
// limin number of cells explored to prevent frame spikes
if (cellCount >= GROUND_PATH_CELL_LIMIT) {
parentCell->releaseInfo();
break;
}
// put parent cell onto closed list - its evaluation is finished
parentCell->putOnClosedList( m_closedList );Manhattan check on prepass same logic as in examineNeighboringCells
ICoord2D start, end;
start.x = parentCell->getXIndex();
start.y = parentCell->getYIndex();
end.x = goalCell->getXIndex();
end.y = goalCell->getYIndex();
// Only run prepass when goal is nearby
Int manhattan = IABS(start.x - end.x) + IABS(start.y - end.y);
if (manhattan <= PREPASS_MAX_MANHATTAN) {
iterateCellsAlongLine(start, end, parentCell->getLayer(), groundCellsCallback, &info);
}findGroundPath() full click to open
Path *Pathfinder::findGroundPath( const Coord3D *from,
const Coord3D *rawTo, Int pathDiameter, Bool crusher)
{
//CRCDEBUG_LOG(("Pathfinder::findGroundPath()"));
#ifdef DEBUG_LOGGING
Int startTimeMS = ::GetTickCount();
#endif
#ifdef INTENSE_DEBUG
DEBUG_LOG(("Find ground path..."));
#endif
Bool centerInCell = false;
m_zoneManager.clearPassableFlags();
Bool isHuman = true;
Path *hPat = internal_findHierarchicalPath(isHuman, LOCOMOTORSURFACE_GROUND, from, rawTo, false, false);
if (hPat) {
deleteInstance(hPat);
} else {
m_zoneManager.setAllPassable();
}
if (rawTo->x == 0.0f && rawTo->y == 0.0f) {
DEBUG_LOG(("Attempting pathfind to 0,0, generally a bug."));
return nullptr;
}
DEBUG_ASSERTCRASH(m_openList.empty() && m_closedList.empty(), ("Dangling lists."));
if (m_isMapReady == false) {
return nullptr;
}
Coord3D adjustTo = *rawTo;
Coord3D *to = &adjustTo;
Coord3D clipFrom = *from;
clip(&clipFrom, &adjustTo);
m_isTunneling = false;
PathfindLayerEnum destinationLayer = TheTerrainLogic->getLayerForDestination(to);
ICoord2D cell;
worldToCell( to, &cell );
if (pathDiameter!=clearCellForDiameter(crusher, cell.x, cell.y, destinationLayer, pathDiameter)) {
Int offset=1;
ICoord2D newCell;
const Int MAX_OFFSET = 8;
while (offset<MAX_OFFSET) {
newCell = cell;
cell.x += offset;
if (clearCellForDiameter(crusher, cell.x, cell.y, destinationLayer, pathDiameter)==pathDiameter) break;
cell.y += offset;
if (clearCellForDiameter(crusher, cell.x, cell.y, destinationLayer, pathDiameter)==pathDiameter) break;
cell.x -= offset;
if (clearCellForDiameter(crusher, cell.x, cell.y, destinationLayer, pathDiameter)==pathDiameter) break;
cell.x -= offset;
if (clearCellForDiameter(crusher, cell.x, cell.y, destinationLayer, pathDiameter)==pathDiameter) break;
cell.y -= offset;
if (clearCellForDiameter(crusher, cell.x, cell.y, destinationLayer, pathDiameter)==pathDiameter) break;
cell.y -= offset;
if (clearCellForDiameter(crusher, cell.x, cell.y, destinationLayer, pathDiameter)==pathDiameter) break;
cell.x += offset;
if (clearCellForDiameter(crusher, cell.x, cell.y, destinationLayer, pathDiameter)==pathDiameter) break;
cell.x += offset;
if (clearCellForDiameter(crusher, cell.x, cell.y, destinationLayer, pathDiameter)==pathDiameter) break;
offset++;
cell = newCell;
}
if (offset >= MAX_OFFSET) {
return nullptr;
}
}
// determine goal cell
PathfindCell *goalCell = getCell( destinationLayer, cell.x, cell.y );
if (goalCell == nullptr) {
return nullptr;
}
if (!goalCell->allocateInfo(cell)) {
return nullptr;
}
// determine start cell
ICoord2D startCellNdx;
PathfindLayerEnum layer = TheTerrainLogic->getLayerForDestination(from);
PathfindCell *parentCell = getClippedCell( layer,&clipFrom );
if (parentCell == nullptr) {
#if RETAIL_COMPATIBLE_PATHFINDING
if (s_useFixedPathfinding)
#endif
{
goalCell->releaseInfo();
}
return nullptr;
}
if (parentCell!=goalCell) {
worldToCell(&clipFrom, &startCellNdx);
if (!parentCell->allocateInfo(startCellNdx)) {
goalCell->releaseInfo();
return nullptr;
}
}
Int zone1, zone2;
// m_isCrusher = false;
zone1 = m_zoneManager.getEffectiveZone(LOCOMOTORSURFACE_GROUND, false, parentCell->getZone());
zone2 = m_zoneManager.getEffectiveZone(LOCOMOTORSURFACE_GROUND, false, goalCell->getZone());
//DEBUG_LOG(("Zones %d to %d", zone1, zone2));
if ( zone1 != zone2) {
goalCell->releaseInfo();
parentCell->releaseInfo();
return nullptr;
}
parentCell->startPathfind(goalCell);
// initialize "open" list to contain start cell
#if RETAIL_COMPATIBLE_PATHFINDING
if (!s_useFixedPathfinding) {
m_openList.reset(parentCell);
}
else
#endif
{
m_openList.reset();
parentCell->putOnSortedOpenList(m_openList);
}
// "closed" list is initially empty
m_closedList.reset();
// TheSuperHackers @fix helmutbuhler This was originally uninitialized and in the loop below.
#if RETAIL_COMPATIBLE_CRC
UnsignedInt newCostSoFar = 0;
#endif
//
// Continue search until "open" list is empty, or
// until goal is found.
//
Int cellCount = 0;
while( !m_openList.empty() )
{
// take head cell off of open list - it has lowest estimated total path cost
parentCell = m_openList.getHead();
parentCell->removeFromOpenList(m_openList);
if (parentCell == goalCell)
{
// success - found a path to the goal
#ifdef INTENSE_DEBUG
DEBUG_LOG((" time %d msec %d cells", (::GetTickCount()-startTimeMS), cellCount));
DEBUG_LOG((" SUCCESS"));
#endif
#if defined(RTS_DEBUG)
Bool show = TheGlobalData->m_debugAI==AI_DEBUG_GROUND_PATHS;
if (show)
debugShowSearch(true);
#endif
m_isTunneling = false;
// construct and return path
Path *path = buildGroundPath(crusher, from, goalCell, centerInCell, pathDiameter );
#if RETAIL_COMPATIBLE_PATHFINDING
if (!s_useFixedPathfinding)
{
parentCell->releaseInfo();
cleanOpenAndClosedLists();
}
else
#endif
{
cleanOpenAndClosedLists();
parentCell->releaseInfo();
}
return path;
}
if (cellCount >= GROUND_PATH_CELL_LIMIT) {
parentCell->releaseInfo();
break;
}
// put parent cell onto closed list - its evaluation is finished
parentCell->putOnClosedList( m_closedList );
// Check to see if we can change layers in this cell.
checkChangeLayers(parentCell);
GroundCellsStruct info;
info.thePathfinder = this;
info.centerInCell = centerInCell;
info.pathDiameter = pathDiameter;
info.goalCell = goalCell;
info.crusher = crusher;
ICoord2D start, end;
start.x = parentCell->getXIndex();
start.y = parentCell->getYIndex();
end.x = goalCell->getXIndex();
end.y = goalCell->getYIndex();
Int manhattan = IABS(start.x - end.x) + IABS(start.y - end.y);
if (manhattan <= PREPASS_MAX_MANHATTAN) {
iterateCellsAlongLine(start, end, parentCell->getLayer(), groundCellsCallback, &info);
}
// expand search to neighboring orthogonal cells
static ICoord2D delta[] =
{
{ 1, 0 }, { 0, 1 }, { -1, 0 }, { 0, -1 },
{ 1, 1 }, { -1, 1 }, { -1, -1 }, { 1, -1 }
};
const Int numNeighbors = 8;
const Int firstDiagonal = 4;
ICoord2D newCellCoord;
PathfindCell *newCell;
const Int adjacent[5] = {0, 1, 2, 3, 0};
Bool neighborFlags[8] = { 0 };
// TheSuperHackers @fix Mauller 23/05/2025 Fixes uninitialized variable.
#if RETAIL_COMPATIBLE_CRC
// newCostSoFar defined in outer block.
#else
UnsignedInt newCostSoFar = 0;
#endif
for( int i=0; i<numNeighbors; i++ )
{
neighborFlags[i] = false;
// determine neighbor cell to try
newCellCoord.x = parentCell->getXIndex() + delta[i].x;
newCellCoord.y = parentCell->getYIndex() + delta[i].y;
// get the neighboring cell
newCell = getCell(parentCell->getLayer(), newCellCoord.x, newCellCoord.y );
// check if cell is on the map
if (newCell == nullptr)
continue;
if ((newCell->getLayer()==LAYER_GROUND) && !m_zoneManager.isPassable(newCellCoord.x, newCellCoord.y)) {
// check if we are within 3.
Bool passable = false;
if (m_zoneManager.clipIsPassable(newCellCoord.x+3, newCellCoord.y+3)) passable = true;
if (m_zoneManager.clipIsPassable(newCellCoord.x-3, newCellCoord.y+3)) passable = true;
if (m_zoneManager.clipIsPassable(newCellCoord.x+3, newCellCoord.y-3)) passable = true;
if (m_zoneManager.clipIsPassable(newCellCoord.x-3, newCellCoord.y-3)) passable = true;
if (!passable) continue;
}
// check if this neighbor cell is already on the open (waiting to be tried)
// or closed (already tried) lists
Bool onList = false;
if (newCell->hasInfo()) {
if (newCell->getOpen() || newCell->getClosed())
{
// already on one of the lists
onList = true;
}
}
Int clearDiameter = 0;
if (newCell!=goalCell) {
if (i>=firstDiagonal) {
// make sure one of the adjacent sides is open.
if (!neighborFlags[adjacent[i-4]] && !neighborFlags[adjacent[i-3]]) {
continue;
}
}
// See how wide the cell is.
clearDiameter = clearCellForDiameter(crusher, newCellCoord.x, newCellCoord.y, newCell->getLayer(), pathDiameter);
if (newCell->getType() != PathfindCell::CELL_CLEAR) {
continue;
}
if (newCell->getPinched()) {
continue;
}
neighborFlags[i] = true;
if (!newCell->allocateInfo(newCellCoord)) {
// Out of cells for pathing...
continue;
}
cellCount++;
#if RETAIL_COMPATIBLE_CRC
// TheSuperHackers @fix helmutbuhler 11/06/2025 The indentation was wrong on retail here.
newCostSoFar = newCell->costSoFar( parentCell );
if (clearDiameter<pathDiameter) {
int delta = pathDiameter-clearDiameter;
newCostSoFar += 0.6f*(delta*COST_ORTHOGONAL);
}
newCell->setBlockedByAlly(false);
}
#else
}
newCostSoFar = newCell->costSoFar( parentCell );
if (clearDiameter<pathDiameter) {
int delta = pathDiameter-clearDiameter;
newCostSoFar += 0.6f*(delta*COST_ORTHOGONAL);
}
newCell->setBlockedByAlly(false);
#endif
Int costRemaining = 0;
costRemaining = newCell->costToGoal( goalCell );
// check if this neighbor cell is already on the open (waiting to be tried)
// or closed (already tried) lists
if (onList)
{
// already on one of the lists - if existing costSoFar is less,
// the new cell is on a longer path, so skip it
if (newCell->getCostSoFar() <= newCostSoFar)
continue;
}
newCell->setCostSoFar(newCostSoFar);
// keep track of path we're building - point back to cell we moved here from
newCell->setParentCell(parentCell) ;
newCell->setTotalCost(newCell->getCostSoFar() + costRemaining) ;
// if newCell was on closed list, remove it from the list
if (newCell->getClosed())
newCell->removeFromClosedList( m_closedList );
// if the newCell was already on the open list, remove it so it can be re-inserted in order
if (newCell->getOpen())
newCell->removeFromOpenList( m_openList );
// insert newCell in open list such that open list is sorted, smallest total path cost first
newCell->putOnSortedOpenList( m_openList );
}
}
// failure - goal cannot be reached
#ifdef INTENSE_DEBUG
DEBUG_LOG((" FAILURE"));
#endif
#if defined(RTS_DEBUG)
if (TheGlobalData->m_debugAI)
{
extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color);
RGBColor color;
color.blue = 0;
color.red = color.green = 1;
addIcon(nullptr, 0, 0, color);
debugShowSearch(false);
Coord3D pos;
pos = *from;
pos.z = TheTerrainLogic->getGroundHeight( pos.x, pos.y ) + 0.5f;
addIcon(&pos, 3*PATHFIND_CELL_SIZE_F, 600, color);
pos = *to;
pos.z = TheTerrainLogic->getGroundHeight( pos.x, pos.y ) + 0.5f;
addIcon(&pos, 3*PATHFIND_CELL_SIZE_F, 600, color);
Real dx, dy;
dx = from->x - to->x;
dy = from->y - to->y;
Int count = sqrt(dx*dx+dy*dy)/(PATHFIND_CELL_SIZE_F/2);
if (count<2) count = 2;
Int i;
color.green = 0;
for (i=1; i<count; i++) {
pos.x = from->x + (to->x-from->x)*i/count;
pos.y = from->y + (to->y-from->y)*i/count;
pos.z = TheTerrainLogic->getGroundHeight( pos.x, pos.y ) + 0.5f;
addIcon(&pos, PATHFIND_CELL_SIZE_F/2, 60, color);
}
}
#endif
DEBUG_LOG(("%d FindGroundPath failed from (%f,%f) to (%f,%f) --", TheGameLogic->getFrame(), from->x, from->y, to->x, to->y));
DEBUG_LOG(("time %f", (::GetTickCount()-startTimeMS)/1000.0f));
#ifdef DUMP_PERF_STATS
TheGameLogic->incrementOverallFailedPathfinds();
#endif
m_isTunneling = false;
#if RETAIL_COMPATIBLE_PATHFINDING
if (!s_useFixedPathfinding)
{
goalCell->releaseInfo();
cleanOpenAndClosedLists();
}
else
#endif
{
cleanOpenAndClosedLists();
parentCell->releaseInfo();
goalCell->releaseInfo();
}
return nullptr;
}
MAX_PATHFINDS_PER_FRAME requests which is unprocess will be stay in queue and execute next frame. Units start moving with a delay of several frames at most.
PREPASS_MAX_MANHATTAN is only a hint for A*. Without it finds the same path and just explores more cells.
GROUND_PATH_CELL_LIMIT is always checked before the limit so a found path is never discarded. This cell that triggered break is cleaned up with releaseInfo()