mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 13:23:56 -07:00
Make include-what-you-use happy with some files in source/simulation2 and fix what needs to be fixed. Ref: #8086 Signed-off-by: Ralph Sennhauser <ralph.sennhauser@gmail.com>
865 lines
27 KiB
C++
865 lines
27 KiB
C++
/* Copyright (C) 2025 Wildfire Games.
|
|
* This file is part of 0 A.D.
|
|
*
|
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* 0 A.D. is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "precompiled.h"
|
|
|
|
#include "HierarchicalPathfinder.h"
|
|
|
|
#include "graphics/Color.h"
|
|
#include "graphics/Overlay.h"
|
|
#include "lib/code_generation.h"
|
|
#include "maths/Fixed.h"
|
|
#include "maths/FixedVector2D.h"
|
|
#include "ps/Profile.h"
|
|
#include "ps/Profiler2.h"
|
|
#include "renderer/Scene.h"
|
|
#include "simulation2/helpers/Grid.h"
|
|
#include "simulation2/helpers/PathGoal.h"
|
|
#include "simulation2/helpers/Pathfinding.h"
|
|
#include "simulation2/helpers/Position.h"
|
|
#include "simulation2/helpers/Render.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
|
|
class CSimContext;
|
|
|
|
// Find the root ID of a region, used by InitRegions
|
|
inline u16 RootID(u16 x, const std::vector<u16>& v)
|
|
{
|
|
while (v[x] < x)
|
|
x = v[x];
|
|
|
|
return x;
|
|
}
|
|
|
|
void HierarchicalPathfinder::Chunk::InitRegions(int ci, int cj, Grid<NavcellData>* grid, pass_class_t passClass)
|
|
{
|
|
ENSURE(ci < 256 && cj < 256); // avoid overflows
|
|
m_ChunkI = ci;
|
|
m_ChunkJ = cj;
|
|
|
|
memset(m_Regions, 0, sizeof(m_Regions));
|
|
|
|
int i0 = ci * CHUNK_SIZE;
|
|
int j0 = cj * CHUNK_SIZE;
|
|
int i1 = std::min(i0 + CHUNK_SIZE, (int)grid->m_W);
|
|
int j1 = std::min(j0 + CHUNK_SIZE, (int)grid->m_H);
|
|
|
|
// Efficiently flood-fill the m_Regions grid
|
|
|
|
int regionID = 0;
|
|
std::vector<u16> connect;
|
|
|
|
u16* pCurrentID = NULL;
|
|
u16 LeftID = 0;
|
|
u16 DownID = 0;
|
|
bool Checked = false; // prevent some unneccessary RootID calls
|
|
|
|
connect.reserve(32); // TODO: What's a sensible number?
|
|
connect.push_back(0); // connect[0] = 0
|
|
|
|
// Start by filling the grid with 0 for blocked,
|
|
// and regionID for unblocked
|
|
for (int j = j0; j < j1; ++j)
|
|
{
|
|
for (int i = i0; i < i1; ++i)
|
|
{
|
|
pCurrentID = &m_Regions[j-j0][i-i0];
|
|
if (!IS_PASSABLE(grid->get(i, j), passClass))
|
|
{
|
|
*pCurrentID = 0;
|
|
continue;
|
|
}
|
|
|
|
if (j > j0)
|
|
DownID = m_Regions[j-1-j0][i-i0];
|
|
|
|
if (i == i0)
|
|
LeftID = 0;
|
|
else
|
|
LeftID = m_Regions[j-j0][i-1-i0];
|
|
|
|
if (LeftID > 0)
|
|
{
|
|
*pCurrentID = LeftID;
|
|
if (*pCurrentID != DownID && DownID > 0 && !Checked)
|
|
{
|
|
u16 id0 = RootID(DownID, connect);
|
|
u16 id1 = RootID(LeftID, connect);
|
|
Checked = true; // this avoids repeatedly connecting the same IDs
|
|
|
|
if (id0 < id1)
|
|
connect[id1] = id0;
|
|
else if (id0 > id1)
|
|
connect[id0] = id1;
|
|
}
|
|
else if (DownID == 0)
|
|
Checked = false;
|
|
}
|
|
else if (DownID > 0)
|
|
{
|
|
*pCurrentID = DownID;
|
|
Checked = false;
|
|
}
|
|
else
|
|
{
|
|
// New ID
|
|
*pCurrentID = ++regionID;
|
|
connect.push_back(regionID);
|
|
Checked = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mark connected regions as being the same ID (i.e. the lowest)
|
|
m_RegionsID.clear();
|
|
for (u16 i = 1; i < regionID+1; ++i)
|
|
{
|
|
if (connect[i] != i)
|
|
connect[i] = RootID(i, connect);
|
|
else
|
|
m_RegionsID.push_back(connect[i]);
|
|
}
|
|
|
|
// Replace IDs by the root ID
|
|
for (int j = 0; j < CHUNK_SIZE; ++j)
|
|
for (int i = 0; i < CHUNK_SIZE; ++i)
|
|
m_Regions[j][i] = connect[m_Regions[j][i]];
|
|
}
|
|
|
|
/**
|
|
* Returns a RegionID for the given global navcell coords
|
|
* (which must be inside this chunk);
|
|
*/
|
|
HierarchicalPathfinder::RegionID HierarchicalPathfinder::Chunk::Get(int i, int j) const
|
|
{
|
|
ENSURE(i < CHUNK_SIZE && j < CHUNK_SIZE);
|
|
return RegionID(m_ChunkI, m_ChunkJ, m_Regions[j][i]);
|
|
}
|
|
|
|
/**
|
|
* Return the global navcell coords that correspond roughly to the
|
|
* center of the given region in this chunk.
|
|
* (This is not guaranteed to be actually inside the region.)
|
|
*/
|
|
void HierarchicalPathfinder::Chunk::RegionCenter(u16 r, int& i_out, int& j_out) const
|
|
{
|
|
// Find the mean of i,j coords of navcells in this region:
|
|
|
|
u32 si = 0, sj = 0; // sum of i,j coords
|
|
u32 n = 0; // number of navcells in region
|
|
|
|
cassert(CHUNK_SIZE < 256); // conservative limit to ensure si and sj don't overflow
|
|
|
|
for (int j = 0; j < CHUNK_SIZE; ++j)
|
|
{
|
|
for (int i = 0; i < CHUNK_SIZE; ++i)
|
|
{
|
|
if (m_Regions[j][i] == r)
|
|
{
|
|
si += i;
|
|
sj += j;
|
|
n += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Avoid divide-by-zero
|
|
if (n == 0)
|
|
n = 1;
|
|
|
|
i_out = m_ChunkI * CHUNK_SIZE + si / n;
|
|
j_out = m_ChunkJ * CHUNK_SIZE + sj / n;
|
|
}
|
|
|
|
/**
|
|
* Returns the global navcell coords, and the squared distance to the goal
|
|
* navcell, of whichever navcell inside the given region is closest to
|
|
* that goal.
|
|
*/
|
|
void HierarchicalPathfinder::Chunk::RegionNavcellNearest(u16 r, int iGoal, int jGoal, int& iBest, int& jBest, u32& dist2Best) const
|
|
{
|
|
iBest = 0;
|
|
jBest = 0;
|
|
dist2Best = std::numeric_limits<u32>::max();
|
|
|
|
for (int j = 0; j < CHUNK_SIZE; ++j)
|
|
{
|
|
for (int i = 0; i < CHUNK_SIZE; ++i)
|
|
{
|
|
if (m_Regions[j][i] != r)
|
|
continue;
|
|
|
|
u32 dist2 = (i + m_ChunkI*CHUNK_SIZE - iGoal)*(i + m_ChunkI*CHUNK_SIZE - iGoal)
|
|
+ (j + m_ChunkJ*CHUNK_SIZE - jGoal)*(j + m_ChunkJ*CHUNK_SIZE - jGoal);
|
|
|
|
if (dist2 < dist2Best)
|
|
{
|
|
iBest = i + m_ChunkI*CHUNK_SIZE;
|
|
jBest = j + m_ChunkJ*CHUNK_SIZE;
|
|
dist2Best = dist2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gives the global navcell coords, and the squared distance to the (i0,j0)
|
|
* navcell, of whichever navcell inside the given region and inside the given goal
|
|
* is closest to (i0,j0)
|
|
* Returns true if the goal is inside the region, false otherwise.
|
|
*/
|
|
bool HierarchicalPathfinder::Chunk::RegionNearestNavcellInGoal(u16 r, u16 i0, u16 j0, const PathGoal& goal, u16& iOut, u16& jOut, u32& dist2Best) const
|
|
{
|
|
// TODO: this should be optimized further.
|
|
// Most used cases empirically seem to be SQUARE, INVERTED_CIRCLE and then POINT and CIRCLE somehwat equally
|
|
iOut = 0;
|
|
jOut = 0;
|
|
dist2Best = std::numeric_limits<u32>::max();
|
|
|
|
// Calculate the navcell that contains the center of the goal.
|
|
int gi = (goal.x >> Pathfinding::NAVCELL_SIZE_LOG2).ToInt_RoundToNegInfinity();
|
|
int gj = (goal.z >> Pathfinding::NAVCELL_SIZE_LOG2).ToInt_RoundToNegInfinity();
|
|
|
|
switch(goal.type)
|
|
{
|
|
case PathGoal::POINT:
|
|
{
|
|
// i and j can be equal to CHUNK_SIZE on the top and right borders of the map,
|
|
// specially when mapSize is a multiple of CHUNK_SIZE
|
|
int i = std::min((int)CHUNK_SIZE - 1, gi - m_ChunkI * CHUNK_SIZE);
|
|
int j = std::min((int)CHUNK_SIZE - 1, gj - m_ChunkJ * CHUNK_SIZE);
|
|
if (m_Regions[j][i] == r)
|
|
{
|
|
iOut = gi;
|
|
jOut = gj;
|
|
dist2Best = (gi - i0)*(gi - i0)
|
|
+ (gj - j0)*(gj - j0);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
case PathGoal::CIRCLE:
|
|
case PathGoal::SQUARE:
|
|
{
|
|
// restrict ourselves to a square surrounding the goal.
|
|
int radius = (std::max(goal.hw*3/2,goal.hh*3/2) >> Pathfinding::NAVCELL_SIZE_LOG2).ToInt_RoundToInfinity();
|
|
int imin = std::max(0, gi-m_ChunkI*CHUNK_SIZE-radius);
|
|
int imax = std::min((int)CHUNK_SIZE, gi-m_ChunkI*CHUNK_SIZE+radius+1);
|
|
int jmin = std::max(0, gj-m_ChunkJ*CHUNK_SIZE-radius);
|
|
int jmax = std::min((int)CHUNK_SIZE, gj-m_ChunkJ*CHUNK_SIZE+radius+1);
|
|
bool found = false;
|
|
u32 dist2 = std::numeric_limits<u32>::max();
|
|
for (u16 j = jmin; j < jmax; ++j)
|
|
{
|
|
for (u16 i = imin; i < imax; ++i)
|
|
{
|
|
if (m_Regions[j][i] != r)
|
|
continue;
|
|
|
|
if (found)
|
|
{
|
|
dist2 = (i + m_ChunkI*CHUNK_SIZE - i0)*(i + m_ChunkI*CHUNK_SIZE - i0)
|
|
+ (j + m_ChunkJ*CHUNK_SIZE - j0)*(j + m_ChunkJ*CHUNK_SIZE - j0);
|
|
if (dist2 >= dist2Best)
|
|
continue;
|
|
}
|
|
|
|
if (goal.NavcellContainsGoal(m_ChunkI * CHUNK_SIZE + i, m_ChunkJ * CHUNK_SIZE + j))
|
|
{
|
|
if (!found)
|
|
{
|
|
found = true;
|
|
dist2 = (i + m_ChunkI*CHUNK_SIZE - i0)*(i + m_ChunkI*CHUNK_SIZE - i0)
|
|
+ (j + m_ChunkJ*CHUNK_SIZE - j0)*(j + m_ChunkJ*CHUNK_SIZE - j0);
|
|
}
|
|
iOut = i + m_ChunkI*CHUNK_SIZE;
|
|
jOut = j + m_ChunkJ*CHUNK_SIZE;
|
|
dist2Best = dist2;
|
|
}
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
case PathGoal::INVERTED_CIRCLE:
|
|
case PathGoal::INVERTED_SQUARE:
|
|
{
|
|
bool found = false;
|
|
u32 dist2 = std::numeric_limits<u32>::max();
|
|
// loop over all navcells.
|
|
for (u16 j = 0; j < CHUNK_SIZE; ++j)
|
|
{
|
|
for (u16 i = 0; i < CHUNK_SIZE; ++i)
|
|
{
|
|
if (m_Regions[j][i] != r)
|
|
continue;
|
|
|
|
if (found)
|
|
{
|
|
dist2 = (i + m_ChunkI*CHUNK_SIZE - i0)*(i + m_ChunkI*CHUNK_SIZE - i0)
|
|
+ (j + m_ChunkJ*CHUNK_SIZE - j0)*(j + m_ChunkJ*CHUNK_SIZE - j0);
|
|
if (dist2 >= dist2Best)
|
|
continue;
|
|
}
|
|
|
|
if (goal.NavcellContainsGoal(m_ChunkI * CHUNK_SIZE + i, m_ChunkJ * CHUNK_SIZE + j))
|
|
{
|
|
if (!found)
|
|
{
|
|
found = true;
|
|
dist2 = (i + m_ChunkI*CHUNK_SIZE - i0)*(i + m_ChunkI*CHUNK_SIZE - i0)
|
|
+ (j + m_ChunkJ*CHUNK_SIZE - j0)*(j + m_ChunkJ*CHUNK_SIZE - j0);
|
|
}
|
|
iOut = i + m_ChunkI*CHUNK_SIZE;
|
|
jOut = j + m_ChunkJ*CHUNK_SIZE;
|
|
dist2Best = dist2;
|
|
}
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
HierarchicalPathfinder::HierarchicalPathfinder() : m_DebugOverlay(NULL)
|
|
{
|
|
}
|
|
|
|
HierarchicalPathfinder::~HierarchicalPathfinder()
|
|
{
|
|
SAFE_DELETE(m_DebugOverlay);
|
|
}
|
|
|
|
void HierarchicalPathfinder::SetDebugOverlay(bool enabled, const CSimContext* simContext)
|
|
{
|
|
if (enabled && !m_DebugOverlay)
|
|
{
|
|
m_DebugOverlay = new HierarchicalOverlay(*this);
|
|
m_DebugOverlayLines.clear();
|
|
m_SimContext = simContext;
|
|
AddDebugEdges(GetPassabilityClass("default"));
|
|
}
|
|
else if (!enabled && m_DebugOverlay)
|
|
{
|
|
SAFE_DELETE(m_DebugOverlay);
|
|
m_DebugOverlayLines.clear();
|
|
m_SimContext = NULL;
|
|
}
|
|
}
|
|
|
|
void HierarchicalPathfinder::RenderSubmit(SceneCollector& collector)
|
|
{
|
|
if (!m_DebugOverlay)
|
|
return;
|
|
|
|
for (size_t i = 0; i < m_DebugOverlayLines.size(); ++i)
|
|
collector.Submit(&m_DebugOverlayLines[i]);
|
|
}
|
|
|
|
void HierarchicalPathfinder::Recompute(Grid<NavcellData>* grid,
|
|
const std::map<std::string, pass_class_t>& nonPathfindingPassClassMasks,
|
|
const std::map<std::string, pass_class_t>& pathfindingPassClassMasks)
|
|
{
|
|
PROFILE2("Hierarchical Recompute");
|
|
|
|
m_PassClassMasks = pathfindingPassClassMasks;
|
|
|
|
std::map<std::string, pass_class_t> allPassClasses = m_PassClassMasks;
|
|
allPassClasses.insert(nonPathfindingPassClassMasks.begin(), nonPathfindingPassClassMasks.end());
|
|
|
|
m_W = grid->m_W;
|
|
m_H = grid->m_H;
|
|
|
|
ENSURE((grid->m_W + CHUNK_SIZE - 1) / CHUNK_SIZE < 256 && (grid->m_H + CHUNK_SIZE - 1) / CHUNK_SIZE < 256); // else the u8 Chunk::m_ChunkI will overflow
|
|
|
|
// Divide grid into chunks with round-to-positive-infinity
|
|
m_ChunksW = (grid->m_W + CHUNK_SIZE - 1) / CHUNK_SIZE;
|
|
m_ChunksH = (grid->m_H + CHUNK_SIZE - 1) / CHUNK_SIZE;
|
|
|
|
m_Chunks.clear();
|
|
m_Edges.clear();
|
|
|
|
// Reset global regions.
|
|
m_NextGlobalRegionID = 1;
|
|
|
|
for (auto& passClassMask : allPassClasses)
|
|
{
|
|
pass_class_t passClass = passClassMask.second;
|
|
|
|
// Compute the regions within each chunk
|
|
m_Chunks[passClass].resize(m_ChunksW*m_ChunksH);
|
|
for (int cj = 0; cj < m_ChunksH; ++cj)
|
|
{
|
|
for (int ci = 0; ci < m_ChunksW; ++ci)
|
|
{
|
|
m_Chunks[passClass].at(cj*m_ChunksW + ci).InitRegions(ci, cj, grid, passClass);
|
|
}
|
|
}
|
|
|
|
// Construct the search graph over the regions.
|
|
EdgesMap& edges = m_Edges[passClass];
|
|
RecomputeAllEdges(passClass, edges);
|
|
|
|
// Spread global regions.
|
|
std::map<RegionID, GlobalRegionID>& globalRegion = m_GlobalRegions[passClass];
|
|
globalRegion.clear();
|
|
for (u8 cj = 0; cj < m_ChunksH; ++cj)
|
|
for (u8 ci = 0; ci < m_ChunksW; ++ci)
|
|
for (u16 rid : GetChunk(ci, cj, passClass).m_RegionsID)
|
|
{
|
|
RegionID reg{ci,cj,rid};
|
|
if (globalRegion.find(reg) == globalRegion.end())
|
|
{
|
|
GlobalRegionID ID = m_NextGlobalRegionID++;
|
|
|
|
globalRegion.insert({ reg, ID });
|
|
// Avoid creating an empty link if possible, FindReachableRegions uses [] which calls the default constructor.
|
|
if (edges.find(reg) != edges.end())
|
|
{
|
|
std::set<RegionID> reachable;
|
|
FindReachableRegions(reg, reachable, passClass);
|
|
for (const RegionID& region : reachable)
|
|
globalRegion.insert({ region, ID });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_DebugOverlay)
|
|
{
|
|
m_DebugOverlayLines.clear();
|
|
AddDebugEdges(GetPassabilityClass("default"));
|
|
}
|
|
}
|
|
|
|
void HierarchicalPathfinder::Update(Grid<NavcellData>* grid, const Grid<u8>& dirtinessGrid)
|
|
{
|
|
PROFILE3("Hierarchical Update");
|
|
|
|
ASSERT(m_NextGlobalRegionID < std::numeric_limits<GlobalRegionID>::max());
|
|
|
|
std::map<pass_class_t, std::vector<RegionID> > needNewGlobalRegionMap;
|
|
|
|
// Algorithm for the partial update:
|
|
// 1. Loop over chunks.
|
|
// 2. For any dirty chunk:
|
|
// - remove all regions from the global region map
|
|
// - remove all edges, by removing the neighbor connection with them and then deleting us
|
|
// - recreate regions inside the chunk
|
|
// - reconnect the regions. We may do too much work if we reconnect with a dirty chunk, but that's fine.
|
|
// 3. Recreate global regions.
|
|
// This means that if any chunk changes, we may need to flood (at most once) the whole map.
|
|
// That's quite annoying, but I can't think of an easy way around it.
|
|
// If we could be sure that a region's topology hasn't changed, we could skip removing its global region
|
|
// but that's non trivial as we have no easy way to determine said topology (regions could "switch" IDs on update for now).
|
|
for (u8 cj = 0; cj < m_ChunksH; ++cj)
|
|
{
|
|
int j0 = cj * CHUNK_SIZE;
|
|
int j1 = std::min(j0 + CHUNK_SIZE, (int)dirtinessGrid.m_H);
|
|
for (u8 ci = 0; ci < m_ChunksW; ++ci)
|
|
{
|
|
// Skip chunks where no navcells are dirty.
|
|
int i0 = ci * CHUNK_SIZE;
|
|
int i1 = std::min(i0 + CHUNK_SIZE, (int)dirtinessGrid.m_W);
|
|
if (!dirtinessGrid.any_set_in_square(i0, j0, i1, j1))
|
|
continue;
|
|
|
|
for (const std::pair<const std::string, pass_class_t>& passClassMask : m_PassClassMasks)
|
|
{
|
|
pass_class_t passClass = passClassMask.second;
|
|
Chunk& a = m_Chunks[passClass].at(ci + cj*m_ChunksW);
|
|
|
|
// Clean up edges and global region ID
|
|
EdgesMap& edgeMap = m_Edges[passClass];
|
|
for (u16 i : a.m_RegionsID)
|
|
{
|
|
RegionID reg{ci, cj, i};
|
|
m_GlobalRegions[passClass].erase(reg);
|
|
for (const RegionID& neighbor : edgeMap[reg])
|
|
{
|
|
edgeMap[neighbor].erase(reg);
|
|
if (edgeMap[neighbor].empty())
|
|
edgeMap.erase(neighbor);
|
|
}
|
|
edgeMap.erase(reg);
|
|
}
|
|
|
|
// Recompute regions inside this chunk.
|
|
a.InitRegions(ci, cj, grid, passClass);
|
|
|
|
for (u16 i : a.m_RegionsID)
|
|
needNewGlobalRegionMap[passClass].push_back(RegionID{ci, cj, i});
|
|
|
|
UpdateEdges(ci, cj, passClass, edgeMap);
|
|
}
|
|
}
|
|
}
|
|
|
|
UpdateGlobalRegions(needNewGlobalRegionMap);
|
|
|
|
if (m_DebugOverlay)
|
|
{
|
|
m_DebugOverlayLines.clear();
|
|
AddDebugEdges(GetPassabilityClass("default"));
|
|
}
|
|
}
|
|
|
|
void HierarchicalPathfinder::ComputeNeighbors(EdgesMap& edges, Chunk& a, Chunk& b, bool transpose, bool opposite) const
|
|
{
|
|
// For each edge between chunks, we loop over every adjacent pair of
|
|
// navcells in the two chunks. If they are both in valid regions
|
|
// (i.e. are passable navcells) then add a graph edge between those regions.
|
|
// (We don't need to test for duplicates since EdgesMap already uses a
|
|
// std::set which will drop duplicate entries.)
|
|
// But as set.insert can be quite slow on large collection, and that we usually
|
|
// try to insert the same values, we cache the previous one for a fast test.
|
|
RegionID raPrev(0,0,0);
|
|
RegionID rbPrev(0,0,0);
|
|
for (int k = 0; k < CHUNK_SIZE; ++k)
|
|
{
|
|
u8 aSide = opposite ? CHUNK_SIZE - 1 : 0;
|
|
u8 bSide = CHUNK_SIZE - 1 - aSide;
|
|
RegionID ra = transpose ? a.Get(k, aSide) : a.Get(aSide, k);
|
|
RegionID rb = transpose ? b.Get(k, bSide) : b.Get(bSide, k);
|
|
if (ra.r && rb.r)
|
|
{
|
|
if (ra == raPrev && rb == rbPrev)
|
|
continue;
|
|
edges[ra].insert(rb);
|
|
edges[rb].insert(ra);
|
|
raPrev = ra;
|
|
rbPrev = rb;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Connect a chunk's regions to their neighbors. Not optimised for global recomputing.
|
|
*/
|
|
void HierarchicalPathfinder::UpdateEdges(u8 ci, u8 cj, pass_class_t passClass, EdgesMap& edges)
|
|
{
|
|
std::vector<Chunk>& chunks = m_Chunks[passClass];
|
|
|
|
Chunk& a = chunks.at(cj*m_ChunksW + ci);
|
|
|
|
if (ci > 0)
|
|
ComputeNeighbors(edges, a, chunks.at(cj*m_ChunksW + (ci-1)), false, false);
|
|
|
|
if (ci < m_ChunksW-1)
|
|
ComputeNeighbors(edges, a, chunks.at(cj*m_ChunksW + (ci+1)), false, true);
|
|
|
|
if (cj > 0)
|
|
ComputeNeighbors(edges, a, chunks.at((cj-1)*m_ChunksW + ci), true, false);
|
|
|
|
if (cj < m_ChunksH - 1)
|
|
ComputeNeighbors(edges, a, chunks.at((cj+1)*m_ChunksW + ci), true, true);
|
|
}
|
|
|
|
/**
|
|
* Find edges between regions in all chunks, in an optimised manner (only look at top/left)
|
|
*/
|
|
void HierarchicalPathfinder::RecomputeAllEdges(pass_class_t passClass, EdgesMap& edges)
|
|
{
|
|
std::vector<Chunk>& chunks = m_Chunks[passClass];
|
|
|
|
edges.clear();
|
|
|
|
for (int cj = 0; cj < m_ChunksH; ++cj)
|
|
{
|
|
for (int ci = 0; ci < m_ChunksW; ++ci)
|
|
{
|
|
Chunk& a = chunks.at(cj*m_ChunksW + ci);
|
|
|
|
if (ci > 0)
|
|
ComputeNeighbors(edges, a, chunks.at(cj*m_ChunksW + (ci-1)), false, false);
|
|
|
|
if (cj > 0)
|
|
ComputeNeighbors(edges, a, chunks.at((cj-1)*m_ChunksW + ci), true, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Debug visualisation of graph edges between regions.
|
|
*/
|
|
void HierarchicalPathfinder::AddDebugEdges(pass_class_t passClass)
|
|
{
|
|
const EdgesMap& edges = m_Edges[passClass];
|
|
const std::vector<Chunk>& chunks = m_Chunks[passClass];
|
|
|
|
for (auto& edge : edges)
|
|
{
|
|
for (const RegionID& region: edge.second)
|
|
{
|
|
// Draw a line between the two regions' centers
|
|
|
|
int i0, j0, i1, j1;
|
|
chunks[edge.first.cj * m_ChunksW + edge.first.ci].RegionCenter(edge.first.r, i0, j0);
|
|
chunks[region.cj * m_ChunksW + region.ci].RegionCenter(region.r, i1, j1);
|
|
|
|
CFixedVector2D a, b;
|
|
Pathfinding::NavcellCenter(i0, j0, a.X, a.Y);
|
|
Pathfinding::NavcellCenter(i1, j1, b.X, b.Y);
|
|
|
|
// Push the endpoints inwards a little to avoid overlaps
|
|
CFixedVector2D d = b - a;
|
|
d.Normalize(entity_pos_t::FromInt(1));
|
|
a += d;
|
|
b -= d;
|
|
|
|
std::vector<float> xz;
|
|
xz.push_back(a.X.ToFloat());
|
|
xz.push_back(a.Y.ToFloat());
|
|
xz.push_back(b.X.ToFloat());
|
|
xz.push_back(b.Y.ToFloat());
|
|
|
|
m_DebugOverlayLines.emplace_back();
|
|
m_DebugOverlayLines.back().m_Color = CColor(1.0, 1.0, 1.0, 1.0);
|
|
SimRender::ConstructLineOnGround(*m_SimContext, xz, m_DebugOverlayLines.back(), true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void HierarchicalPathfinder::UpdateGlobalRegions(const std::map<pass_class_t, std::vector<RegionID> >& needNewGlobalRegionMap)
|
|
{
|
|
// Use FindReachableRegions because we cannot be sure, even if we find a non-dirty chunk nearby,
|
|
// that we weren't the only bridge connecting that chunk to the rest of the global region.
|
|
for (const std::pair<const pass_class_t, std::vector<RegionID>>& regionsInNeed : needNewGlobalRegionMap)
|
|
{
|
|
for (const RegionID& reg : regionsInNeed.second)
|
|
{
|
|
std::map<RegionID, GlobalRegionID>& globalRegions = m_GlobalRegions[regionsInNeed.first];
|
|
// If we have already been given a region, skip us.
|
|
if (globalRegions.find(reg) != globalRegions.end())
|
|
continue;
|
|
|
|
std::set<RegionID> reachable;
|
|
FindReachableRegions(reg, reachable, regionsInNeed.first);
|
|
|
|
GlobalRegionID ID = m_NextGlobalRegionID++;
|
|
|
|
for (const RegionID& regionId : reachable)
|
|
globalRegions[regionId] = ID;
|
|
}
|
|
}
|
|
}
|
|
|
|
HierarchicalPathfinder::RegionID HierarchicalPathfinder::Get(u16 i, u16 j, pass_class_t passClass) const
|
|
{
|
|
int ci = i / CHUNK_SIZE;
|
|
int cj = j / CHUNK_SIZE;
|
|
ENSURE(ci < m_ChunksW && cj < m_ChunksH);
|
|
return m_Chunks.at(passClass)[cj*m_ChunksW + ci].Get(i % CHUNK_SIZE, j % CHUNK_SIZE);
|
|
}
|
|
|
|
HierarchicalPathfinder::GlobalRegionID HierarchicalPathfinder::GetGlobalRegion(u16 i, u16 j, pass_class_t passClass) const
|
|
{
|
|
return GetGlobalRegion(Get(i, j, passClass), passClass);
|
|
}
|
|
|
|
HierarchicalPathfinder::GlobalRegionID HierarchicalPathfinder::GetGlobalRegion(RegionID region, pass_class_t passClass) const
|
|
{
|
|
return region.r == 0 ? GlobalRegionID(0) : m_GlobalRegions.at(passClass).at(region);
|
|
}
|
|
|
|
void CreatePointGoalAt(u16 i, u16 j, PathGoal& goal)
|
|
{
|
|
PathGoal newGoal;
|
|
newGoal.type = PathGoal::POINT;
|
|
Pathfinding::NavcellCenter(i, j, newGoal.x, newGoal.z);
|
|
goal = newGoal;
|
|
}
|
|
|
|
bool HierarchicalPathfinder::MakeGoalReachable(u16 i0, u16 j0, PathGoal& goal, pass_class_t passClass) const
|
|
{
|
|
PROFILE2("MakeGoalReachable");
|
|
|
|
u16 iGoal, jGoal;
|
|
Pathfinding::NearestNavcell(goal.x, goal.z, iGoal, jGoal, m_W, m_H);
|
|
|
|
std::set<InterestingRegion, SortByBestToPoint> goalRegions(SortByBestToPoint(i0, j0));
|
|
// This returns goal regions ordered by distance from the best navcell in each region.
|
|
FindGoalRegionsAndBestNavcells(i0, j0, iGoal, jGoal, goal, goalRegions, passClass);
|
|
|
|
// Because of the sorting above, we can stop as soon as the first reachable goal region is found.
|
|
for (const InterestingRegion& region : goalRegions)
|
|
if (GetGlobalRegion(region.region, passClass) == GetGlobalRegion(i0, j0, passClass))
|
|
{
|
|
iGoal = region.bestI;
|
|
jGoal = region.bestJ;
|
|
|
|
// No need to move reachable point goals.
|
|
if (goal.type != PathGoal::POINT)
|
|
CreatePointGoalAt(iGoal, jGoal, goal);
|
|
return true;
|
|
}
|
|
|
|
// Goal wasn't reachable - get the closest navcell in the nearest reachable region.
|
|
std::set<RegionID, SortByCenterToPoint> reachableRegions(SortByCenterToPoint(iGoal, jGoal));
|
|
FindReachableRegions(Get(i0, j0, passClass), reachableRegions, passClass);
|
|
|
|
FindNearestNavcellInRegions(reachableRegions, iGoal, jGoal, passClass);
|
|
CreatePointGoalAt(iGoal, jGoal, goal);
|
|
return false;
|
|
}
|
|
|
|
|
|
bool HierarchicalPathfinder::IsGoalReachable(u16 i0, u16 j0, const PathGoal& goal, pass_class_t passClass) const
|
|
{
|
|
PROFILE2("IsGoalReachable");
|
|
|
|
u16 iGoal, jGoal;
|
|
Pathfinding::NearestNavcell(goal.x, goal.z, iGoal, jGoal, m_W, m_H);
|
|
|
|
std::set<InterestingRegion, SortByBestToPoint> goalRegions(SortByBestToPoint(i0, j0));
|
|
// This returns goal regions ordered by distance from the best navcell in each region.
|
|
FindGoalRegionsAndBestNavcells(i0, j0, iGoal, jGoal, goal, goalRegions, passClass);
|
|
|
|
// Because of the sorting above, we can stop as soon as the first reachable goal region is found.
|
|
for (const InterestingRegion& region : goalRegions)
|
|
if (GetGlobalRegion(region.region, passClass) == GetGlobalRegion(i0, j0, passClass))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
void HierarchicalPathfinder::FindNearestPassableNavcell(u16& i, u16& j, pass_class_t passClass) const
|
|
{
|
|
std::set<RegionID, SortByCenterToPoint> regions(SortByCenterToPoint(i, j));
|
|
|
|
// Construct a set of all regions of all chunks for this pass class
|
|
for (const Chunk& chunk : m_Chunks.at(passClass))
|
|
for (int r : chunk.m_RegionsID)
|
|
regions.insert(RegionID(chunk.m_ChunkI, chunk.m_ChunkJ, r));
|
|
|
|
FindNearestNavcellInRegions(regions, i, j, passClass);
|
|
}
|
|
|
|
void HierarchicalPathfinder::FindNearestNavcellInRegions(const std::set<RegionID, SortByCenterToPoint>& regions, u16& iGoal, u16& jGoal, pass_class_t passClass) const
|
|
{
|
|
u16 bestI = iGoal, bestJ = jGoal; // Somewhat sensible default-values should regions() be passed empty.
|
|
u32 bestDist = std::numeric_limits<u32>::max();
|
|
|
|
// Because regions are sorted by increasing distance, we can ignore regions that are obviously farther than the current best point.
|
|
// Since regions are squares, that happens when the center of a region is at least sqrt(2) * CHUNK_SIZE farther than the current best point.
|
|
// Add one to avoid cases where the center navcell is actually slightly off-center (= CHUNK_SIZE is even)
|
|
u32 maxDistFromBest = (fixed::FromInt(3) / 2 * CHUNK_SIZE).ToInt_RoundToInfinity() + 1;
|
|
// TODO: update to static_assert with constexpr
|
|
ENSURE(maxDistFromBest < std::numeric_limits<u16>::max());
|
|
maxDistFromBest *= maxDistFromBest;
|
|
|
|
for (const RegionID& region : regions)
|
|
{
|
|
u32 chunkDist = region.DistanceTo(iGoal, jGoal);
|
|
// This might overflow, but only if we are already close to the maximal possible distance, so the condition would probably be false anyways.
|
|
// It's also a bit pessimistic, so we'll still consider a few too many regions.
|
|
if (bestDist < std::numeric_limits<u32>::max() && chunkDist > maxDistFromBest + bestDist)
|
|
break; // Break, the set is ordered by increased distance so a closer region will not be found.
|
|
|
|
int ri, rj;
|
|
u32 dist;
|
|
GetChunk(region.ci, region.cj, passClass).RegionNavcellNearest(region.r, iGoal, jGoal, ri, rj, dist);
|
|
if (dist < bestDist)
|
|
{
|
|
bestI = ri;
|
|
bestJ = rj;
|
|
bestDist = dist;
|
|
}
|
|
}
|
|
iGoal = bestI;
|
|
jGoal = bestJ;
|
|
}
|
|
|
|
void HierarchicalPathfinder::FindGoalRegionsAndBestNavcells(u16 i0, u16 j0, u16 gi, u16 gj, const PathGoal& goal, std::set<InterestingRegion, SortByBestToPoint>& regions, pass_class_t passClass) const
|
|
{
|
|
if (goal.type == PathGoal::POINT)
|
|
{
|
|
RegionID region = Get(gi, gj, passClass);
|
|
if (region.r > 0)
|
|
regions.insert({region, gi, gj});
|
|
return;
|
|
}
|
|
|
|
// For non-point cases, we'll test each region inside the bounds of the goal.
|
|
// we might occasionally test a few too many for circles but it's not too bad.
|
|
// Note that this also works in the Inverse-circle / Inverse-square case
|
|
// Since our ranges are inclusive, we will necessarily test at least the perimeter/outer bound of the goal.
|
|
// If we find a navcell, great, if not, well then we'll be surrounded by an impassable barrier.
|
|
// Since in the Inverse-XX case we're supposed to start inside, then we can't ever reach the goal so it's good enough.
|
|
// It's not worth it to skip the "inner" regions since we'd need ranges above CHUNK_SIZE for that to start mattering
|
|
// (and even then not always) and that just doesn't happen for Inverse-XX goals
|
|
int size = (std::max(goal.hh, goal.hw) * 3 / 2).ToInt_RoundToInfinity();
|
|
|
|
u16 bestI, bestJ;
|
|
u32 c; // Unused.
|
|
|
|
for (u8 sz = std::max(0,(gj - size) / CHUNK_SIZE); sz <= std::min(m_ChunksH-1, (gj + size + 1) / CHUNK_SIZE); ++sz)
|
|
for (u8 sx = std::max(0,(gi - size) / CHUNK_SIZE); sx <= std::min(m_ChunksW-1, (gi + size + 1) / CHUNK_SIZE); ++sx)
|
|
{
|
|
const Chunk& chunk = GetChunk(sx, sz, passClass);
|
|
for (u16 i : chunk.m_RegionsID)
|
|
if (chunk.RegionNearestNavcellInGoal(i, i0, j0, goal, bestI, bestJ, c))
|
|
regions.insert({RegionID{sx, sz, i}, bestI, bestJ});
|
|
}
|
|
}
|
|
|
|
void HierarchicalPathfinder::FillRegionOnGrid(const RegionID& region, pass_class_t passClass, u16 value, Grid<u16>& grid) const
|
|
{
|
|
ENSURE(grid.m_W == m_W && grid.m_H == m_H);
|
|
|
|
int i0 = region.ci * CHUNK_SIZE;
|
|
int j0 = region.cj * CHUNK_SIZE;
|
|
|
|
const Chunk& c = m_Chunks.at(passClass)[region.cj * m_ChunksW + region.ci];
|
|
|
|
for (int j = 0; j < CHUNK_SIZE; ++j)
|
|
for (int i = 0; i < CHUNK_SIZE; ++i)
|
|
if (c.m_Regions[j][i] == region.r)
|
|
grid.set(i0 + i, j0 + j, value);
|
|
}
|
|
|
|
Grid<u16> HierarchicalPathfinder::GetConnectivityGrid(pass_class_t passClass) const
|
|
{
|
|
Grid<u16> connectivityGrid(m_W, m_H);
|
|
connectivityGrid.reset();
|
|
|
|
u16 idx = 1;
|
|
|
|
for (u16 i = 0; i < m_W; ++i)
|
|
{
|
|
for (u16 j = 0; j < m_H; ++j)
|
|
{
|
|
if (connectivityGrid.get(i, j) != 0)
|
|
continue;
|
|
|
|
RegionID from = Get(i, j, passClass);
|
|
if (from.r == 0)
|
|
continue;
|
|
|
|
std::set<RegionID> reachable;
|
|
FindReachableRegions(from, reachable, passClass);
|
|
|
|
for (const RegionID& region : reachable)
|
|
FillRegionOnGrid(region, passClass, idx, connectivityGrid);
|
|
|
|
++idx;
|
|
}
|
|
}
|
|
|
|
return connectivityGrid;
|
|
}
|