1
0
forked from 0ad/0ad

New long-range pathfinder.

Based on Philip's work located at
http://git.wildfiregames.com/gitweb/?p=0ad.git;a=shortlog;h=refs/heads/projects/philip/pathfinder
Includes code by wraitii, sanderd17 and kanetaka.

An updated version of docs/pathfinder.pdf describing the changes in
detail will be committed ASAP.

Running update-workspaces is needed after this change.

Fixes #1756.
Fixes #930, #1259, #2908, #2960, #3097
Refs #1200, #1914, #1942, #2568, #2132, #2563

This was SVN commit r16751.
This commit is contained in:
Nicolas Auvray 2015-06-12 18:58:24 +00:00
parent aa1512a662
commit 6581796103
69 changed files with 5618 additions and 2298 deletions

View File

@ -51,7 +51,7 @@
/>
<!-- Dev/cheat commands -->
<object name="devCommands" size="100%-225 50%-88 100%-8 50%+104" type="image" sprite="devCommandsBackground" z="40"
<object name="devCommands" size="100%-225 50%-88 100%-8 50%+120" type="image" sprite="devCommandsBackground" z="40"
hidden="true" hotkey="session.devcommands.toggle">
<action on="Press">toggleDeveloperOverlay();</action>
@ -145,6 +145,13 @@
<object size="100%-16 176 100% 192" type="button" style="ModernTickBox">
<action on="Press">Engine.PostNetworkCommand({"type": "promote", "entities": g_Selection.toList()});</action>
</object>
<object size="0 192 100%-18 208" type="text" style="devCommandsText">
<translatableAttribute id="caption">Hierarchical pathfinder overlay</translatableAttribute>
</object>
<object size="100%-16 192 100% 208" type="checkbox" style="ModernTickBox">
<action on="Press">Engine.GuiInterfaceCall("SetPathfinderHierDebugOverlay", this.checked);</action>
</object>
</object>
<!-- ================================ ================================ -->

Binary file not shown.

Binary file not shown.

View File

@ -117,9 +117,9 @@ BuildRestrictions.prototype.CheckPlacement = function()
break;
case "land-shore":
// 'default' is everywhere a normal unit can go
// So on passable land, and not too deep in the water
passClassName = "default";
// 'default-terrain-only' is everywhere a normal unit can go, ignoring
// obstructions (i.e. on passable land, and not too deep in the water)
passClassName = "default-terrain-only";
break;
case "land":

View File

@ -1777,6 +1777,12 @@ GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
cmpPathfinder.SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
{
var cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder);
cmpPathfinder.SetHierDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
@ -1899,6 +1905,7 @@ var exposedFunctions = {
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetPathfinderHierDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,

View File

@ -12,19 +12,8 @@ element Pathfinder {
element MaxWaterDepth { xsd:decimal }? &
element MaxTerrainSlope { xsd:decimal }? &
element MinShoreDistance { xsd:decimal }? &
element MaxShoreDistance { xsd:decimal }?
}+
} &
element MovementClasses {
element * {
attribute Speed { xsd:decimal } &
attribute Cost { xsd:decimal } &
element UnitClasses {
element * {
attribute Speed { xsd:decimal } &
attribute Cost { xsd:decimal }
}*
}?
element MaxShoreDistance { xsd:decimal }? &
element Clearance { xsd:decimal }?
}+
}
}

View File

@ -41,36 +41,9 @@
<data type="decimal"/>
</element>
</optional>
</interleave>
</element>
</oneOrMore>
</element>
<element name="MovementClasses">
<oneOrMore>
<element>
<anyName/>
<interleave>
<attribute name="Speed">
<data type="decimal"/>
</attribute>
<attribute name="Cost">
<data type="decimal"/>
</attribute>
<optional>
<element name="UnitClasses">
<zeroOrMore>
<element>
<anyName/>
<interleave>
<attribute name="Speed">
<data type="decimal"/>
</attribute>
<attribute name="Cost">
<data type="decimal"/>
</attribute>
</interleave>
</element>
</zeroOrMore>
<element name="Clearance">
<data type="decimal"/>
</element>
</optional>
</interleave>

View File

@ -6,46 +6,64 @@
<PassabilityClasses>
<!-- Unit pathfinding classes: -->
<unrestricted/>
<!-- Unit pathfinding classes: -->
<!-- 'default-terrain-only' is used for wall building placement
and by the AI pathfinder; it must be kept in sync with 'default'
-->
<default>
<MaxWaterDepth>2</MaxWaterDepth>
<MaxTerrainSlope>1.0</MaxTerrainSlope>
<Clearance>1.0</Clearance>
</default>
<default-no-clearance>
<MaxWaterDepth>2</MaxWaterDepth>
<MaxTerrainSlope>1.0</MaxTerrainSlope>
<Clearance>0.0</Clearance>
</default-no-clearance>
<default-terrain-only>
<MaxWaterDepth>2</MaxWaterDepth>
<MaxTerrainSlope>1.0</MaxTerrainSlope>
</default-terrain-only>
<siege-large>
<MaxWaterDepth>2</MaxWaterDepth>
<MaxTerrainSlope>1.0</MaxTerrainSlope>
<Clearance>4.0</Clearance>
</siege-large>
<ship>
<MinWaterDepth>1</MinWaterDepth>
<Clearance>12.0</Clearance>
</ship>
<ship-small>
<MinWaterDepth>1</MinWaterDepth>
<Clearance>4.0</Clearance>
</ship-small>
<!-- Building construction classes:
* Land is used for most buildings, which must be away
from water and not on cliffs or mountains.
* Shore is used for docks, which must be near water and
land, yet shallow enough for builders to approach.
(These should not use <Clearance>, because the foundation placement
checker already does precise obstruction-shape-based checking.)
-->
<building-land>
<MaxWaterDepth>0</MaxWaterDepth>
<MinShoreDistance>1.0</MinShoreDistance>
<MinShoreDistance>4.0</MinShoreDistance>
<MaxTerrainSlope>1.0</MaxTerrainSlope>
</building-land>
<building-shore>
<MaxShoreDistance>2.0</MaxShoreDistance>
<MaxShoreDistance>8.0</MaxShoreDistance>
<MaxTerrainSlope>1.25</MaxTerrainSlope>
</building-shore>
</PassabilityClasses>
<!-- Territory growth influences: -->
<territory>
<MaxWaterDepth>2</MaxWaterDepth>
<MaxTerrainSlope>1.0</MaxTerrainSlope>
</territory>
<!--
Warning: Movement costs are a subtle tradeoff between
pathfinding accuracy and computation cost. Be extremely
careful if you change them.
(Speeds are safer to change, but ought to be kept roughly
in sync with costs.)
-->
<MovementClasses>
<default Speed="1.0" Cost="1.08"/>
<city Speed="1.0" Cost="1.0">
<UnitClasses>
<infantry Speed="1.4" Cost="0.6"/>
</UnitClasses>
</city>
</MovementClasses>
</PassabilityClasses>
</Pathfinder>

View File

@ -33,7 +33,7 @@
<Color r="119" g="82" b="46"/>
</Minimap>
<Obstruction>
<Static width="0.8" depth="13.0"/>
<Static width="1.5" depth="13.0"/>
</Obstruction>
<StatusBars>
<HeightOffset>6.0</HeightOffset>

View File

@ -33,7 +33,7 @@
<Color r="119" g="82" b="46"/>
</Minimap>
<Obstruction>
<Static width="0.8" depth="6.5"/>
<Static width="1.5" depth="6.5"/>
</Obstruction>
<StatusBars>
<HeightOffset>6.0</HeightOffset>

View File

@ -30,7 +30,7 @@
<metal>0</metal>
</Loot>
<Obstruction>
<Static width="1.0" depth="10.5"/>
<Static width="1.5" depth="10.5"/>
</Obstruction>
<StatusBars>
<HeightOffset>6.0</HeightOffset>

View File

@ -38,8 +38,7 @@
<UnitMotion>
<FormationController>true</FormationController>
<WalkSpeed>1.0</WalkSpeed>
<PassabilityClass>default</PassabilityClass>
<CostClass>default</CostClass>
<PassabilityClass>siege-large</PassabilityClass>
</UnitMotion>
<Trader>
<GainMultiplier>1.0</GainMultiplier>

View File

@ -77,7 +77,6 @@
<LineDashColor r="158" g="11" b="15"/>
<LineStartCap>square</LineStartCap>
<LineEndCap>round</LineEndCap>
<LineCostClass>default</LineCostClass>
<LinePassabilityClass>default</LinePassabilityClass>
</RallyPointRenderer>
<Selectable>

View File

@ -39,13 +39,6 @@
<Classes datatype="tokens">Unit ConquestCritical</Classes>
<Formations datatype="tokens">
formations/null
formations/box
formations/column_closed
formations/line_closed
formations/column_open
formations/line_open
formations/flank
formations/battle_line
</Formations>
</Identity>
<Looter/>
@ -113,7 +106,6 @@
<DecayTime>0.2</DecayTime>
</Run>
<PassabilityClass>default</PassabilityClass>
<CostClass>default</CostClass>
</UnitMotion>
<Visibility>
<RetainInFog>false</RetainInFog>

View File

@ -63,6 +63,7 @@
<HeightOffset>6.5</HeightOffset>
</StatusBars>
<UnitMotion>
<PassabilityClass>siege-large</PassabilityClass>
<WalkSpeed>8.5</WalkSpeed>
<Run>
<Speed>14.0</Speed>

View File

@ -126,7 +126,6 @@
<Run>
<Speed>18.75</Speed>
</Run>
<CostClass>infantry</CostClass>
</UnitMotion>
<Vision>
<Range>80</Range>

View File

@ -55,6 +55,7 @@
<CanGuard>false</CanGuard>
</UnitAI>
<UnitMotion>
<PassabilityClass>ship-small</PassabilityClass>
<WalkSpeed>10</WalkSpeed>
</UnitMotion>
<Vision>

View File

@ -60,6 +60,7 @@
<BarHeight>0.5</BarHeight>
</StatusBars>
<UnitMotion>
<PassabilityClass>siege-large</PassabilityClass>
<WalkSpeed>8</WalkSpeed>
<Run>
<Speed>11.0</Speed>

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2013 Wildfire Games.
/* Copyright (C) 2015 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -36,6 +36,9 @@
#include "maths/FixedVector3D.h"
#include "maths/MathUtil.h"
#include "ps/CLogger.h"
#include "simulation2/helpers/Pathfinding.h"
const fixed Pathfinding::NAVCELL_SIZE = fixed::FromInt(TERRAIN_TILE_SIZE) / Pathfinding::NAVCELLS_PER_TILE;
///////////////////////////////////////////////////////////////////////////////
// CTerrain constructor
@ -339,6 +342,58 @@ fixed CTerrain::GetSlopeFixed(ssize_t i, ssize_t j) const
return fixed::FromInt(delta / TERRAIN_TILE_SIZE) / (int)HEIGHT_UNITS_PER_METRE;
}
fixed CTerrain::GetExactSlopeFixed(fixed x, fixed z) const
{
// Clamp to size-2 so we can use the tiles (xi,zi)-(xi+1,zi+1)
const ssize_t xi = clamp((ssize_t)(x / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), (ssize_t)0, m_MapSize-2);
const ssize_t zi = clamp((ssize_t)(z / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), (ssize_t)0, m_MapSize-2);
const fixed one = fixed::FromInt(1);
const fixed xf = clamp((x / (int)TERRAIN_TILE_SIZE) - fixed::FromInt(xi), fixed::Zero(), one);
const fixed zf = clamp((z / (int)TERRAIN_TILE_SIZE) - fixed::FromInt(zi), fixed::Zero(), one);
u16 h00 = m_Heightmap[zi*m_MapSize + xi];
u16 h01 = m_Heightmap[(zi+1)*m_MapSize + xi];
u16 h10 = m_Heightmap[zi*m_MapSize + (xi+1)];
u16 h11 = m_Heightmap[(zi+1)*m_MapSize + (xi+1)];
u16 delta;
if (GetTriangulationDir(xi, zi))
{
if (xf + zf <= one)
{
// Lower-left triangle (don't use h11)
delta = std::max(std::max(h00, h01), h10) -
std::min(std::min(h00, h01), h10);
}
else
{
// Upper-right triangle (don't use h00)
delta = std::max(std::max(h01, h10), h11) -
std::min(std::min(h01, h10), h11);
}
}
else
{
if (xf <= zf)
{
// Upper-left triangle (don't use h10)
delta = std::max(std::max(h00, h01), h11) -
std::min(std::min(h00, h01), h11);
}
else
{
// Lower-right triangle (don't use h01)
delta = std::max(std::max(h00, h10), h11) -
std::min(std::min(h00, h10), h11);
}
}
// Compute fractional slope (being careful to avoid intermediate overflows)
return fixed::FromInt(delta / TERRAIN_TILE_SIZE) / (int)HEIGHT_UNITS_PER_METRE;
}
float CTerrain::GetFilteredGroundLevel(float x, float z, float radius) const
{
// convert to [0,1] interval

View File

@ -87,9 +87,13 @@ public:
fixed GetExactGroundLevelFixed(fixed x, fixed z) const;
float GetFilteredGroundLevel(float x, float z, float radius) const;
// get the approximate slope (0 = horizontal, 0.5 = 30 degrees, 1.0 = 45 degrees, etc)
// get the approximate slope of a tile
// (0 = horizontal, 0.5 = 30 degrees, 1.0 = 45 degrees, etc)
fixed GetSlopeFixed(ssize_t i, ssize_t j) const;
// get the precise slope of a point, accounting for triangulation direction
fixed GetExactSlopeFixed(fixed x, fixed z) const;
// Returns true if the triangulation diagonal for tile (i, j)
// should be in the direction (1,-1); false if it should be (1,1)
bool GetTriangulationDir(ssize_t i, ssize_t j) const;

View File

@ -21,6 +21,7 @@
#include <algorithm> // for reverse
#include "graphics/Terrain.h"
#include "simulation2/helpers/Pathfinding.h"
#include "simulation2/components/ICmpTerritoryManager.h"
std::vector<STerritoryBoundary> CTerritoryBoundaryCalculator::ComputeBoundaries(const Grid<u8>* territory)
@ -123,9 +124,12 @@ std::vector<STerritoryBoundary> CTerritoryBoundaryCalculator::ComputeBoundaries(
u16 maxi = (u16)(grid.m_W-1);
u16 maxj = (u16)(grid.m_H-1);
// Size of a territory tile in metres
float territoryTileSize = (Pathfinding::NAVCELL_SIZE * ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE).ToFloat();
while (true)
{
points.push_back((CVector2D(ci, cj) + edgeOffsets[cdir]) * TERRAIN_TILE_SIZE);
points.push_back((CVector2D(ci, cj) + edgeOffsets[cdir]) * territoryTileSize);
// Given that we're on an edge on a continuous boundary and aiming anticlockwise,
// we can either carry on straight or turn left or turn right, so examine each

View File

@ -25,6 +25,7 @@
#include "ps/Shapes.h"
#include "renderer/Renderer.h"
#include "simulation2/Simulation2.h"
#include "simulation2/helpers/Pathfinding.h"
#include "simulation2/components/ICmpPlayer.h"
#include "simulation2/components/ICmpPlayerManager.h"
#include "simulation2/components/ICmpTerrain.h"
@ -92,7 +93,8 @@ void CTerritoryTexture::ConstructTexture(int unit)
if (!cmpTerrain)
return;
m_MapSize = cmpTerrain->GetVerticesPerSide() - 1;
// Convert size from terrain tiles to territory tiles
m_MapSize = cmpTerrain->GetTilesPerSide() * Pathfinding::NAVCELLS_PER_TILE / ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE;
m_TextureSize = (GLsizei)round_up_to_pow2((size_t)m_MapSize);

View File

@ -245,6 +245,19 @@ public:
return CFixed(-value);
}
CFixed operator>>(int n) const
{
ASSERT(n >= 0 && n < 32);
return CFixed(value >> n);
}
CFixed operator<<(int n) const
{
ASSERT(n >= 0 && n < 32);
// TODO: check for overflow
return CFixed(value << n);
}
/// Divide by a CFixed. Must not have n.IsZero(). Might overflow.
CFixed operator/(CFixed n) const
{
@ -262,6 +275,14 @@ public:
return CFixed(value * n);
}
/// Multiply by an integer. Avoids overflow by clamping to min/max representable value.
CFixed MultiplyClamp(int n) const
{
i64 t = (i64)value * n;
t = std::max((i64)std::numeric_limits<T>::min(), std::min((i64)std::numeric_limits<T>::max(), t));
return CFixed((i32)t);
}
/// Divide by an integer. Must not have n == 0. Cannot overflow unless n == -1.
CFixed operator/(int n) const
{

View File

@ -79,6 +79,12 @@ public:
return CFixedVector2D(X*n, Y*n);
}
/// Scalar division by an integer. Must not have n == 0.
CFixedVector2D operator/(int n) const
{
return CFixedVector2D(X/n, Y/n);
}
/**
* Multiply by a CFixed. Likely to overflow if both numbers are large,
* so we use an ugly name instead of operator* to make it obvious.

View File

@ -331,3 +331,41 @@ void TerrainTextureOverlay::RenderAfterWater(int cullGroup)
g_Renderer.GetTerrainRenderer().RenderTerrainOverlayTexture(cullGroup, matrix);
}
SColor4ub TerrainTextureOverlay::GetColor(size_t idx, u8 alpha) const
{
static u8 colors[][3] = {
{ 255, 0, 0 },
{ 0, 255, 0 },
{ 0, 0, 255 },
{ 255, 255, 0 },
{ 255, 0, 255 },
{ 0, 255, 255 },
{ 255, 255, 255 },
{ 127, 0, 0 },
{ 0, 127, 0 },
{ 0, 0, 127 },
{ 127, 127, 0 },
{ 127, 0, 127 },
{ 0, 127, 127 },
{ 127, 127, 127},
{ 255, 127, 0 },
{ 127, 255, 0 },
{ 255, 0, 127 },
{ 127, 0, 255},
{ 0, 255, 127 },
{ 0, 127, 255},
{ 255, 127, 127},
{ 127, 255, 127},
{ 127, 127, 255},
{ 127, 255, 255 },
{ 255, 127, 255 },
{ 255, 255, 127 },
};
size_t c = idx % ARRAY_SIZE(colors);
return SColor4ub(colors[c][0], colors[c][1], colors[c][2], alpha);
}

View File

@ -26,6 +26,7 @@
#include "lib/ogl.h"
struct CColor;
struct SColor4ub;
class CTerrain;
class CSimContext;
@ -186,6 +187,12 @@ protected:
*/
virtual void BuildTextureRGBA(u8* data, size_t w, size_t h) = 0;
/**
* Returns an arbitrary color, for subclasses that want to distinguish
* different integers visually.
*/
SColor4ub GetColor(size_t idx, u8 alpha) const;
private:
void RenderAfterWater(int cullGroup);

View File

@ -394,6 +394,7 @@ void TerrainRenderer::RenderTerrainOverlayTexture(int cullGroup, CMatrix3D& text
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDepthMask(0);
glDisable(GL_DEPTH_TEST);
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
@ -433,6 +434,7 @@ void TerrainRenderer::RenderTerrainOverlayTexture(int cullGroup, CMatrix3D& text
glMatrixMode(GL_MODELVIEW);
glDepthMask(1);
glEnable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2014 Wildfire Games.
/* Copyright (C) 2015 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -458,13 +458,13 @@ class CMessagePathResult : public CMessage
public:
DEFAULT_MESSAGE_IMPL(PathResult)
CMessagePathResult(u32 ticket, const ICmpPathfinder::Path& path) :
CMessagePathResult(u32 ticket, const WaypointPath& path) :
ticket(ticket), path(path)
{
}
u32 ticket;
ICmpPathfinder::Path path;
WaypointPath path;
};
/**

View File

@ -485,7 +485,10 @@ void CSimulation2Impl::UpdateComponents(CSimContext& simContext, fixed turnLengt
CmpPtr<ICmpPathfinder> cmpPathfinder(simContext, SYSTEM_ENTITY);
if (cmpPathfinder)
{
cmpPathfinder->UpdateGrid();
cmpPathfinder->FinishAsyncRequests();
}
// Push AI commands onto the queue before we use them
CmpPtr<ICmpAIManager> cmpAIManager(simContext, SYSTEM_ENTITY);

View File

@ -36,7 +36,7 @@
#include "simulation2/components/ICmpTemplateManager.h"
#include "simulation2/components/ICmpTechnologyTemplateManager.h"
#include "simulation2/components/ICmpTerritoryManager.h"
#include "simulation2/helpers/Grid.h"
#include "simulation2/helpers/LongPathfinder.h"
#include "simulation2/serialization/DebugSerializer.h"
#include "simulation2/serialization/StdDeserializer.h"
#include "simulation2/serialization/StdSerializer.h"
@ -224,6 +224,9 @@ public:
m_ScriptInterface->RegisterFunction<void, CAIWorker::DumpHeap>("DumpHeap");
m_ScriptInterface->RegisterFunction<void, CAIWorker::ForceGC>("ForceGC");
m_ScriptInterface->RegisterFunction<JS::Value, JS::HandleValue, JS::HandleValue, pass_class_t, CAIWorker::ComputePath>("ComputePath");
m_ScriptInterface->RegisterFunction<JS::Value, pass_class_t, CAIWorker::GetConnectivityGrid>("GetConnectivityGrid");
m_ScriptInterface->RegisterFunction<void, std::wstring, std::vector<u32>, u32, u32, u32, CAIWorker::DumpImage>("DumpImage");
}
@ -288,6 +291,48 @@ public:
LOGERROR("Invalid playerid in PostCommand!");
}
static JS::Value ComputePath(ScriptInterface::CxPrivate* pCxPrivate,
JS::HandleValue position, JS::HandleValue goal, pass_class_t passClass)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast<CAIWorker*> (pCxPrivate->pCBData);
JSContext* cx(self->m_ScriptInterface->GetContext());
CFixedVector2D pos, goalPos;
std::vector<CFixedVector2D> waypoints;
JS::RootedValue retVal(cx);
self->m_ScriptInterface->FromJSVal<CFixedVector2D>(cx, position, pos);
self->m_ScriptInterface->FromJSVal<CFixedVector2D>(cx, goal, goalPos);
self->ComputePath(pos, goalPos, passClass, waypoints);
self->m_ScriptInterface->ToJSVal<std::vector<CFixedVector2D> >(cx, &retVal, waypoints);
return retVal;
}
void ComputePath(const CFixedVector2D& pos, const CFixedVector2D& goal, pass_class_t passClass, std::vector<CFixedVector2D>& waypoints)
{
WaypointPath ret;
PathGoal pathGoal = { PathGoal::POINT, goal.X, goal.Y };
m_LongPathfinder.ComputePath(pos.X, pos.Y, pathGoal, passClass, ret);
for (Waypoint& wp : ret.m_Waypoints)
waypoints.emplace_back(wp.x, wp.z);
}
static JS::Value GetConnectivityGrid(ScriptInterface::CxPrivate* pCxPrivate, pass_class_t passClass)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast<CAIWorker*> (pCxPrivate->pCBData);
JSContext* cx(self->m_ScriptInterface->GetContext());
JS::RootedValue retVal(cx);
self->m_ScriptInterface->ToJSVal<Grid<u16> >(cx, &retVal, self->m_LongPathfinder.GetConnectivityGrid(passClass));
return retVal;
}
// The next two ought to be implmeneted someday but for now as it returns "null" it can't
static void DumpHeap(ScriptInterface::CxPrivate* pCxPrivate)
{
@ -465,16 +510,23 @@ public:
return true;
}
void StartComputation(const shared_ptr<ScriptInterface::StructuredClone>& gameState, const Grid<u16>& passabilityMap, const Grid<u8>& territoryMap, bool territoryMapDirty)
void StartComputation(const shared_ptr<ScriptInterface::StructuredClone>& gameState,
const Grid<u16>& passabilityMap, bool passabilityMapDirty, const Grid<u8>* dirtinessGrid, bool passabilityMapEntirelyDirty,
const Grid<u8>& territoryMap, bool territoryMapDirty,
std::map<std::string, pass_class_t> passClassMasks)
{
ENSURE(m_CommandsComputed);
m_GameState = gameState;
JSContext* cx = m_ScriptInterface->GetContext();
if (passabilityMap.m_DirtyID != m_PassabilityMap.m_DirtyID)
if (passabilityMapDirty)
{
m_PassabilityMap = passabilityMap;
if (passabilityMapEntirelyDirty)
m_LongPathfinder.Reload(passClassMasks, &m_PassabilityMap);
else
m_LongPathfinder.Update(&m_PassabilityMap, dirtinessGrid);
ScriptInterface::ToJSVal(cx, &m_PassabilityMapVal, m_PassabilityMap);
}
@ -794,6 +846,8 @@ private:
Grid<u8> m_TerritoryMap;
JS::PersistentRootedValue m_TerritoryMapVal;
LongPathfinder m_LongPathfinder;
bool m_CommandsComputed;
shared_ptr<ObjectIdCache<std::wstring> > m_SerializablePrototypes;
@ -981,6 +1035,11 @@ public:
if (cmpPathfinder)
passabilityMap = &cmpPathfinder->GetPassabilityGrid();
Grid<u8> dummyDirtinessGrid;
const Grid<u8>* dirtinessGrid = &dummyDirtinessGrid;
bool passabilityMapEntirelyDirty = false;
bool passabilityMapDirty = cmpPathfinder->GetDirtinessData(dummyDirtinessGrid, passabilityMapEntirelyDirty);
// Get the territory data
// Since getting the territory grid can trigger a recalculation, we check NeedUpdate first
bool territoryMapDirty = false;
@ -994,8 +1053,14 @@ public:
}
LoadPathfinderClasses(state);
std::map<std::string, pass_class_t> passClassMasks;
if (cmpPathfinder)
passClassMasks = cmpPathfinder->GetPassabilityClasses();
m_Worker.StartComputation(scriptInterface.WriteStructuredClone(state), *passabilityMap, *territoryMap, territoryMapDirty);
m_Worker.StartComputation(scriptInterface.WriteStructuredClone(state),
*passabilityMap, passabilityMapDirty, dirtinessGrid, passabilityMapEntirelyDirty,
*territoryMap, territoryMapDirty,
passClassMasks);
m_JustDeserialized = false;
}
@ -1082,10 +1147,10 @@ private:
JSAutoRequest rq(cx);
JS::RootedValue classesVal(cx);
scriptInterface.Eval("({ pathfinderObstruction: 1, foundationObstruction: 2 })", &classesVal);
scriptInterface.Eval("({})", &classesVal);
std::map<std::string, ICmpPathfinder::pass_class_t> classes = cmpPathfinder->GetPassabilityClasses();
for (std::map<std::string, ICmpPathfinder::pass_class_t>::iterator it = classes.begin(); it != classes.end(); ++it)
std::map<std::string, pass_class_t> classes = cmpPathfinder->GetPassabilityClasses();
for (std::map<std::string, pass_class_t>::iterator it = classes.begin(); it != classes.end(); ++it)
scriptInterface.SetProperty(classesVal, it->first.c_str(), it->second, true);
scriptInterface.SetProperty(state, "passabilityClasses", classesVal, true);

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2013 Wildfire Games.
/* Copyright (C) 2015 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -154,7 +154,7 @@ public:
if (!cmpUnitMotion)
return error;
ICmpPathfinder::pass_class_t spawnedPass = cmpUnitMotion->GetPassabilityClass();
pass_class_t spawnedPass = cmpUnitMotion->GetPassabilityClass();
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
if (!cmpPathfinder)
return error;
@ -285,7 +285,7 @@ public:
if (!cmpUnitMotion)
return error;
ICmpPathfinder::pass_class_t spawnedPass = cmpUnitMotion->GetPassabilityClass();
pass_class_t spawnedPass = cmpUnitMotion->GetPassabilityClass();
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
if (!cmpPathfinder)
return error;
@ -294,7 +294,7 @@ public:
CmpPtr<ICmpUnitMotion> cmpEntityMotion(GetEntityHandle());
if (!cmpEntityMotion)
return error;
ICmpPathfinder::pass_class_t entityPass = cmpEntityMotion->GetPassabilityClass();
pass_class_t entityPass = cmpEntityMotion->GetPassabilityClass();
CFixedVector2D initialPos = cmpPosition->GetPosition2D();
entity_angle_t initialAngle = cmpPosition->GetRotation().Y;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2013 Wildfire Games.
/* Copyright (C) 2015 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -58,6 +58,7 @@ public:
entity_pos_t m_Size0; // radius or width
entity_pos_t m_Size1; // radius or depth
flags_t m_TemplateFlags;
entity_pos_t m_Clearance;
typedef struct {
entity_pos_t dx, dz;
@ -112,10 +113,14 @@ public:
"<choice>"
"<element name='Static'>"
"<attribute name='width'>"
"<ref name='positiveDecimal'/>"
"<data type='decimal'>"
"<param name='minInclusive'>1.5</param>"
"</data>"
"</attribute>"
"<attribute name='depth'>"
"<ref name='positiveDecimal'/>"
"<data type='decimal'>"
"<param name='minInclusive'>1.5</param>"
"</data>"
"</attribute>"
"</element>"
"<element name='Unit'>"
@ -138,10 +143,14 @@ public:
"</attribute>"
"</optional>"
"<attribute name='width'>"
"<ref name='positiveDecimal'/>"
"<data type='decimal'>"
"<param name='minInclusive'>1.5</param>"
"</data>"
"</attribute>"
"<attribute name='depth'>"
"<ref name='positiveDecimal'/>"
"<data type='decimal'>"
"<param name='minInclusive'>1.5</param>"
"</data>"
"</attribute>"
"</element>"
"</zeroOrMore>"
@ -177,6 +186,9 @@ public:
virtual void Init(const CParamNode& paramNode)
{
// The minimum obstruction size is the navcell size * sqrt(2)
// This is enforced in the schema as a minimum of 1.5
fixed minObstruction = (Pathfinding::NAVCELL_SIZE.Square() * 2).Sqrt();
m_TemplateFlags = 0;
if (paramNode.GetChild("BlockMovement").ToBool())
m_TemplateFlags |= ICmpObstructionManager::FLAG_BLOCK_MOVEMENT;
@ -203,6 +215,8 @@ public:
m_Type = STATIC;
m_Size0 = paramNode.GetChild("Static").GetChild("@width").ToFixed();
m_Size1 = paramNode.GetChild("Static").GetChild("@depth").ToFixed();
ENSURE(m_Size0 > minObstruction);
ENSURE(m_Size1 > minObstruction);
}
else
{
@ -215,6 +229,8 @@ public:
Shape b;
b.size0 = it->second.GetChild("@width").ToFixed();
b.size1 = it->second.GetChild("@depth").ToFixed();
ENSURE(b.size0 > minObstruction);
ENSURE(b.size1 > minObstruction);
b.dx = it->second.GetChild("@x").ToFixed();
b.dz = it->second.GetChild("@z").ToFixed();
b.da = entity_angle_t::FromInt(0);
@ -319,7 +335,7 @@ public:
data.x, data.z, data.a, m_Size0, m_Size1, m_Flags, m_ControlGroup, m_ControlGroup2);
else if (m_Type == UNIT)
m_Tag = cmpObstructionManager->AddUnitShape(GetEntityId(),
data.x, data.z, m_Size0, (flags_t)(m_Flags | (m_Moving ? ICmpObstructionManager::FLAG_MOVING : 0)), m_ControlGroup);
data.x, data.z, m_Size0, m_Clearance, (flags_t)(m_Flags | (m_Moving ? ICmpObstructionManager::FLAG_MOVING : 0)), m_ControlGroup);
else
AddClusterShapes(data.x, data.x, data.a);
}
@ -376,7 +392,7 @@ public:
pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, m_Flags, m_ControlGroup, m_ControlGroup2);
else if (m_Type == UNIT)
m_Tag = cmpObstructionManager->AddUnitShape(GetEntityId(),
pos.X, pos.Y, m_Size0, (flags_t)(m_Flags | (m_Moving ? ICmpObstructionManager::FLAG_MOVING : 0)), m_ControlGroup);
pos.X, pos.Y, m_Size0, m_Clearance, (flags_t)(m_Flags | (m_Moving ? ICmpObstructionManager::FLAG_MOVING : 0)), m_ControlGroup);
else
AddClusterShapes(pos.X, pos.Y, cmpPosition->GetRotation().Y);
}
@ -493,6 +509,12 @@ public:
return CFixedVector2D(m_Size0 / 2, m_Size1 / 2).Length();
}
virtual void SetUnitClearance(const entity_pos_t& clearance)
{
if (m_Type == UNIT)
m_Clearance = clearance;
}
virtual bool IsControlPersistent()
{
return m_ControlPersist;
@ -526,7 +548,7 @@ public:
}
// Get passability class
ICmpPathfinder::pass_class_t passClass = cmpPathfinder->GetPassabilityClass(className);
pass_class_t passClass = cmpPathfinder->GetPassabilityClass(className);
// Ignore collisions within the same control group, or with other non-foundation-blocking shapes.
// Note that, since the control group for each entity defaults to the entity's ID, this is typically
@ -574,28 +596,15 @@ public:
virtual std::vector<entity_id_t> GetEntityCollisions(bool checkStructures, bool checkUnits)
{
// TODO: That function actually only checks for overlapping units when a new buidling is started.
// The name of the function should be changed and useless code removed.
std::vector<entity_id_t> ret;
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
if (!cmpPosition)
return ret; // error
if (!cmpPosition->IsInWorld())
return ret; // no obstruction
CFixedVector2D pos = cmpPosition->GetPosition2D();
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSystemEntity());
if (!cmpObstructionManager)
return ret; // error
// required precondition to use SkipControlGroupsRequireFlagObstructionFilter
if (m_ControlGroup == INVALID_ENTITY)
{
LOGERROR("[CmpObstruction] Cannot test for unit or structure obstructions; primary control group must be valid");
return ret;
}
flags_t flags = 0;
bool invertMatch = false;
@ -621,15 +630,13 @@ public:
// Ignore collisions within the same control group, or with other shapes that don't match the filter.
// Note that, since the control group for each entity defaults to the entity's ID, this is typically
// equivalent to only ignoring the entity's own shape and other shapes that don't match the filter.
SkipControlGroupsRequireFlagObstructionFilter filter(invertMatch,
m_ControlGroup, m_ControlGroup2, flags);
SkipControlGroupsRequireFlagObstructionFilter filter(invertMatch, m_ControlGroup, m_ControlGroup2, flags);
if (m_Type == STATIC)
cmpObstructionManager->TestStaticShape(filter, pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, &ret);
else if (m_Type == UNIT)
cmpObstructionManager->TestUnitShape(filter, pos.X, pos.Y, m_Size0, &ret);
else
cmpObstructionManager->TestStaticShape(filter, pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, &ret);
ICmpObstructionManager::ObstructionSquare square;
if (!GetObstructionSquare(square))
return ret; // error
cmpObstructionManager->GetUnitsOnObstruction(square, ret, filter);
return ret;
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2012 Wildfire Games.
/* Copyright (C) 2015 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -20,8 +20,11 @@
#include "simulation2/system/Component.h"
#include "ICmpObstructionManager.h"
#include "ICmpTerrain.h"
#include "simulation2/MessageTypes.h"
#include "simulation2/helpers/Geometry.h"
#include "simulation2/helpers/Rasterize.h"
#include "simulation2/helpers/Render.h"
#include "simulation2/helpers/Spatial.h"
#include "simulation2/serialization/SerializeTemplates.h"
@ -51,6 +54,7 @@ struct UnitShape
entity_id_t entity;
entity_pos_t x, z;
entity_pos_t r; // radius of circle, or half width of square
entity_pos_t clearance;
ICmpObstructionManager::flags_t flags;
entity_id_t group; // control group (typically the owner entity, or a formation controller entity) (units ignore collisions with others in the same group)
};
@ -81,6 +85,7 @@ struct SerializeUnitShape
serialize.NumberFixed_Unbounded("x", value.x);
serialize.NumberFixed_Unbounded("z", value.z);
serialize.NumberFixed_Unbounded("r", value.r);
serialize.NumberFixed_Unbounded("clearance", value.clearance);
serialize.NumberU8_Unbounded("flags", value.flags);
serialize.NumberU32_Unbounded("group", value.group);
}
@ -152,7 +157,9 @@ public:
m_UnitShapeNext = 1;
m_StaticShapeNext = 1;
m_DirtyID = 1; // init to 1 so default-initialised grids are considered dirty
m_Dirty = true;
m_NeedsGlobalUpdate = true;
m_DirtinessGrid = NULL;
m_PassabilityCircular = false;
@ -165,6 +172,7 @@ public:
virtual void Deinit()
{
SAFE_DELETE(m_DirtinessGrid);
}
template<typename S>
@ -225,6 +233,15 @@ public:
// Subdivision system bounds:
ENSURE(x0.IsZero() && z0.IsZero()); // don't bother implementing non-zero offsets yet
ResetSubdivisions(x1, z1);
SAFE_DELETE(m_DirtinessGrid);
CmpPtr<ICmpTerrain> cmpTerrain(GetSystemEntity());
if (!cmpTerrain)
return;
u16 tiles = cmpTerrain->GetTilesPerSide();
m_DirtinessGrid = new Grid<u8>(tiles*Pathfinding::NAVCELLS_PER_TILE, tiles*Pathfinding::NAVCELLS_PER_TILE);
}
void ResetSubdivisions(entity_pos_t x1, entity_pos_t z1)
@ -249,12 +266,12 @@ public:
}
}
virtual tag_t AddUnitShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_pos_t r, flags_t flags, entity_id_t group)
virtual tag_t AddUnitShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_pos_t r, entity_pos_t clearance, flags_t flags, entity_id_t group)
{
UnitShape shape = { ent, x, z, r, flags, group };
UnitShape shape = { ent, x, z, r, clearance, flags, group };
u32 id = m_UnitShapeNext++;
m_UnitShapes[id] = shape;
MakeDirtyUnit(flags);
MakeDirtyUnit(flags, id);
m_UnitSubdivision.Add(id, CFixedVector2D(x - r, z - r), CFixedVector2D(x + r, z + r));
@ -271,7 +288,7 @@ public:
StaticShape shape = { ent, x, z, u, v, w/2, h/2, flags, group, group2 };
u32 id = m_StaticShapeNext++;
m_StaticShapes[id] = shape;
MakeDirtyStatic(flags);
MakeDirtyStatic(flags, id);
CFixedVector2D center(x, z);
CFixedVector2D bbHalfSize = Geometry::GetHalfBoundingBox(u, v, CFixedVector2D(w/2, h/2));
@ -316,7 +333,7 @@ public:
shape.x = x;
shape.z = z;
MakeDirtyUnit(shape.flags);
MakeDirtyUnit(shape.flags, TAG_TO_INDEX(tag));
}
else
{
@ -340,7 +357,7 @@ public:
shape.u = u;
shape.v = v;
MakeDirtyStatic(shape.flags);
MakeDirtyStatic(shape.flags, TAG_TO_INDEX(tag));
}
}
@ -394,7 +411,7 @@ public:
CFixedVector2D(shape.x - shape.r, shape.z - shape.r),
CFixedVector2D(shape.x + shape.r, shape.z + shape.r));
MakeDirtyUnit(shape.flags);
MakeDirtyUnit(shape.flags, TAG_TO_INDEX(tag));
m_UnitShapes.erase(TAG_TO_INDEX(tag));
}
else
@ -405,7 +422,7 @@ public:
CFixedVector2D bbHalfSize = Geometry::GetHalfBoundingBox(shape.u, shape.v, CFixedVector2D(shape.hw, shape.hh));
m_StaticSubdivision.Remove(TAG_TO_INDEX(tag), center - bbHalfSize, center + bbHalfSize);
MakeDirtyStatic(shape.flags);
MakeDirtyStatic(shape.flags, TAG_TO_INDEX(tag));
m_StaticShapes.erase(TAG_TO_INDEX(tag));
}
}
@ -434,9 +451,9 @@ public:
virtual bool TestStaticShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h, std::vector<entity_id_t>* out);
virtual bool TestUnitShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, std::vector<entity_id_t>* out);
virtual bool Rasterise(Grid<u8>& grid);
virtual void Rasterize(Grid<u16>& grid, const entity_pos_t& expand, ICmpObstructionManager::flags_t requireMask, u16 setMask);
virtual void GetObstructionsInRange(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, std::vector<ObstructionSquare>& squares);
virtual bool FindMostImportantObstruction(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, ObstructionSquare& square);
virtual void GetUnitsOnObstruction(const ObstructionSquare& square, std::vector<entity_id_t>& out, const IObstructionTestFilter& filter);
virtual void SetPassabilityCircular(bool enabled)
{
@ -447,6 +464,11 @@ public:
GetSimContext().GetComponentManager().BroadcastMessage(msg);
}
virtual bool GetPassabilityCircular() const
{
return m_PassabilityCircular;
}
virtual void SetDebugOverlay(bool enabled)
{
m_DebugOverlayEnabled = enabled;
@ -457,20 +479,38 @@ public:
void RenderSubmit(SceneCollector& collector);
private:
// To support lazy updates of grid rasterisations of obstruction data,
// we maintain a DirtyID here and increment it whenever obstructions change;
// if a grid has a lower DirtyID then it needs to be updated.
virtual bool GetDirtinessData(Grid<u8>& dirtinessGrid, bool& globalUpdateNeeded)
{
if (!m_Dirty || !m_DirtinessGrid)
{
globalUpdateNeeded = false;
return false;
}
size_t m_DirtyID;
dirtinessGrid = *m_DirtinessGrid;
globalUpdateNeeded = m_NeedsGlobalUpdate;
m_Dirty = false;
m_NeedsGlobalUpdate = false;
m_DirtinessGrid->reset();
return true;
}
private:
// Dynamic updates for the long-range pathfinder
bool m_Dirty;
bool m_NeedsGlobalUpdate;
Grid<u8>* m_DirtinessGrid;
/**
* Mark all previous Rasterise()d grids as dirty, and the debug display.
* Mark all previous Rasterize()d grids as dirty, and the debug display.
* Call this when the world bounds have changed.
*/
void MakeDirtyAll()
{
++m_DirtyID;
m_Dirty = true;
m_NeedsGlobalUpdate = true;
m_DebugOverlayDirty = true;
}
@ -483,39 +523,56 @@ private:
m_DebugOverlayDirty = true;
}
inline void MarkDirtinessGrid(const entity_pos_t& x, const entity_pos_t& z, const entity_pos_t& r)
{
if (!m_DirtinessGrid)
return;
u16 j0, j1, i0, i1;
Pathfinding::NearestNavcell(x - r, z - r, i0, j0, m_DirtinessGrid->m_W, m_DirtinessGrid->m_H);
Pathfinding::NearestNavcell(x + r, z + r, i1, j1, m_DirtinessGrid->m_W, m_DirtinessGrid->m_H);
for (int j = j0; j < j1; ++j)
for (int i = i0; i < i1; ++i)
m_DirtinessGrid->set(i, j, 1);
}
/**
* Mark all previous Rasterise()d grids as dirty, if they depend on this shape.
* Mark all previous Rasterize()d grids as dirty, if they depend on this shape.
* Call this when a static shape has changed.
*/
void MakeDirtyStatic(flags_t flags)
void MakeDirtyStatic(flags_t flags, u32 index)
{
if (flags & (FLAG_BLOCK_PATHFINDING|FLAG_BLOCK_FOUNDATION))
++m_DirtyID;
if (flags & (FLAG_BLOCK_PATHFINDING | FLAG_BLOCK_FOUNDATION))
{
m_Dirty = true;
auto it = m_StaticShapes.find(index);
if (it != m_StaticShapes.end())
MarkDirtinessGrid(it->second.x, it->second.z, std::max(it->second.hw, it->second.hh));
}
m_DebugOverlayDirty = true;
}
/**
* Mark all previous Rasterise()d grids as dirty, if they depend on this shape.
* Mark all previous Rasterize()d grids as dirty, if they depend on this shape.
* Call this when a unit shape has changed.
*/
void MakeDirtyUnit(flags_t flags)
void MakeDirtyUnit(flags_t flags, u32 index)
{
if (flags & (FLAG_BLOCK_PATHFINDING|FLAG_BLOCK_FOUNDATION))
++m_DirtyID;
if (flags & (FLAG_BLOCK_PATHFINDING | FLAG_BLOCK_FOUNDATION))
{
m_Dirty = true;
auto it = m_UnitShapes.find(index);
if (it != m_UnitShapes.end())
MarkDirtinessGrid(it->second.x, it->second.z, it->second.r);
}
m_DebugOverlayDirty = true;
}
/**
* Test whether a Rasterise()d grid is dirty and needs updating
*/
template<typename T>
bool IsDirty(const Grid<T>& grid)
{
return grid.m_DirtyID < m_DirtyID;
}
/**
* Return whether the given point is within the world bounds by at least r
*/
@ -706,176 +763,61 @@ bool CCmpObstructionManager::TestUnitShape(const IObstructionTestFilter& filter,
return false; // didn't collide, if we got this far
}
/**
* Compute the tile indexes on the grid nearest to a given point
*/
static void NearestTile(entity_pos_t x, entity_pos_t z, u16& i, u16& j, u16 w, u16 h)
void CCmpObstructionManager::Rasterize(Grid<u16>& grid, const entity_pos_t& expand, ICmpObstructionManager::flags_t requireMask, u16 setMask)
{
i = (u16)clamp((x / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), 0, w-1);
j = (u16)clamp((z / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), 0, h-1);
}
PROFILE3("Rasterize");
/**
* Returns the position of the center of the given tile
*/
static void TileCenter(u16 i, u16 j, entity_pos_t& x, entity_pos_t& z)
{
x = entity_pos_t::FromInt(i*(int)TERRAIN_TILE_SIZE + (int)TERRAIN_TILE_SIZE/2);
z = entity_pos_t::FromInt(j*(int)TERRAIN_TILE_SIZE + (int)TERRAIN_TILE_SIZE/2);
}
// Since m_DirtyID is only updated for pathfinding/foundation blocking shapes,
// NeedUpdate+Rasterize will only be accurate for that subset of shapes.
// (If we ever want to support rasterizing more shapes, we need to improve
// the dirty-detection system too.)
ENSURE(!(requireMask & ~(FLAG_BLOCK_PATHFINDING|FLAG_BLOCK_FOUNDATION)));
bool CCmpObstructionManager::Rasterise(Grid<u8>& grid)
{
if (!IsDirty(grid))
return false;
// Cells are only marked as blocked if the whole cell is strictly inside the shape.
// (That ensures the shape's geometric border is always reachable.)
PROFILE("Rasterise");
// TODO: it might be nice to rasterize with rounded corners for large 'expand' values.
grid.m_DirtyID = m_DirtyID;
// (This could be implemented much more efficiently.)
// TODO: this is all hopelessly inefficient
// What we should perhaps do is have some kind of quadtree storing Shapes so it's
// quick to invalidate and update small numbers of tiles
grid.reset();
// For tile-based pathfinding:
// Since we only count tiles whose centers are inside the square,
// we maybe want to expand the square a bit so we're less likely to think there's
// free space between buildings when there isn't. But this is just a random guess
// and needs to be tweaked until everything works nicely.
//entity_pos_t expandPathfinding = entity_pos_t::FromInt(TERRAIN_TILE_SIZE / 2);
// Actually that's bad because units get stuck when the A* pathfinder thinks they're
// blocked on all sides, so it's better to underestimate
entity_pos_t expandPathfinding = entity_pos_t::FromInt(0);
// For AI building foundation planning, we want to definitely block all
// potentially-obstructed tiles (so we don't blindly build on top of an obstruction),
// so we need to expand by at least 1/sqrt(2) of a tile
entity_pos_t expandFoundation = (entity_pos_t::FromInt(TERRAIN_TILE_SIZE) * 3) / 4;
for (std::map<u32, StaticShape>::iterator it = m_StaticShapes.begin(); it != m_StaticShapes.end(); ++it)
for (auto& pair : m_StaticShapes)
{
CFixedVector2D center(it->second.x, it->second.z);
const StaticShape& shape = pair.second;
if (!(shape.flags & requireMask))
continue;
if (it->second.flags & FLAG_BLOCK_PATHFINDING)
ObstructionSquare square = { shape.x, shape.z, shape.u, shape.v, shape.hw, shape.hh };
SimRasterize::Spans spans;
SimRasterize::RasterizeRectWithClearance(spans, square, expand, Pathfinding::NAVCELL_SIZE);
for (SimRasterize::Span& span : spans)
{
CFixedVector2D halfSize(it->second.hw + expandPathfinding, it->second.hh + expandPathfinding);
CFixedVector2D halfBound = Geometry::GetHalfBoundingBox(it->second.u, it->second.v, halfSize);
u16 i0, j0, i1, j1;
NearestTile(center.X - halfBound.X, center.Y - halfBound.Y, i0, j0, grid.m_W, grid.m_H);
NearestTile(center.X + halfBound.X, center.Y + halfBound.Y, i1, j1, grid.m_W, grid.m_H);
for (u16 j = j0; j <= j1; ++j)
i16 j = span.j;
if (j >= 0 && j <= grid.m_H)
{
for (u16 i = i0; i <= i1; ++i)
{
entity_pos_t x, z;
TileCenter(i, j, x, z);
if (Geometry::PointIsInSquare(CFixedVector2D(x, z) - center, it->second.u, it->second.v, halfSize))
grid.set(i, j, grid.get(i, j) | TILE_OBSTRUCTED_PATHFINDING);
}
}
}
if (it->second.flags & FLAG_BLOCK_FOUNDATION)
{
CFixedVector2D halfSize(it->second.hw + expandFoundation, it->second.hh + expandFoundation);
CFixedVector2D halfBound = Geometry::GetHalfBoundingBox(it->second.u, it->second.v, halfSize);
u16 i0, j0, i1, j1;
NearestTile(center.X - halfBound.X, center.Y - halfBound.Y, i0, j0, grid.m_W, grid.m_H);
NearestTile(center.X + halfBound.X, center.Y + halfBound.Y, i1, j1, grid.m_W, grid.m_H);
for (u16 j = j0; j <= j1; ++j)
{
for (u16 i = i0; i <= i1; ++i)
{
entity_pos_t x, z;
TileCenter(i, j, x, z);
if (Geometry::PointIsInSquare(CFixedVector2D(x, z) - center, it->second.u, it->second.v, halfSize))
grid.set(i, j, grid.get(i, j) | TILE_OBSTRUCTED_FOUNDATION);
}
i16 i0 = std::max(span.i0, (i16)0);
i16 i1 = std::min(span.i1, (i16)grid.m_W);
for (i16 i = i0; i < i1; ++i)
grid.set(i, j, grid.get(i, j) | setMask);
}
}
}
for (std::map<u32, UnitShape>::iterator it = m_UnitShapes.begin(); it != m_UnitShapes.end(); ++it)
for (auto& pair : m_UnitShapes)
{
CFixedVector2D center(it->second.x, it->second.z);
CFixedVector2D center(pair.second.x, pair.second.z);
if (it->second.flags & FLAG_BLOCK_PATHFINDING)
{
entity_pos_t r = it->second.r + expandPathfinding;
if (!(pair.second.flags & requireMask))
continue;
u16 i0, j0, i1, j1;
NearestTile(center.X - r, center.Y - r, i0, j0, grid.m_W, grid.m_H);
NearestTile(center.X + r, center.Y + r, i1, j1, grid.m_W, grid.m_H);
for (u16 j = j0; j <= j1; ++j)
for (u16 i = i0; i <= i1; ++i)
grid.set(i, j, grid.get(i, j) | TILE_OBSTRUCTED_PATHFINDING);
}
entity_pos_t r = pair.second.r + expand;
if (it->second.flags & FLAG_BLOCK_FOUNDATION)
{
entity_pos_t r = it->second.r + expandFoundation;
u16 i0, j0, i1, j1;
NearestTile(center.X - r, center.Y - r, i0, j0, grid.m_W, grid.m_H);
NearestTile(center.X + r, center.Y + r, i1, j1, grid.m_W, grid.m_H);
for (u16 j = j0; j <= j1; ++j)
for (u16 i = i0; i <= i1; ++i)
grid.set(i, j, grid.get(i, j) | TILE_OBSTRUCTED_FOUNDATION);
}
}
// Any tiles outside or very near the edge of the map are impassable
// WARNING: CCmpRangeManager::LosIsOffWorld needs to be kept in sync with this
const u16 edgeSize = 3; // number of tiles around the edge that will be off-world
u8 edgeFlags = TILE_OBSTRUCTED_PATHFINDING | TILE_OBSTRUCTED_FOUNDATION | TILE_OUTOFBOUNDS;
if (m_PassabilityCircular)
{
for (u16 j = 0; j < grid.m_H; ++j)
{
for (u16 i = 0; i < grid.m_W; ++i)
{
// Based on CCmpRangeManager::LosIsOffWorld
// but tweaked since it's tile-based instead.
// (We double all the values so we can handle half-tile coordinates.)
// This needs to be slightly tighter than the LOS circle,
// else units might get themselves lost in the SoD around the edge.
ssize_t dist2 = (i*2 + 1 - grid.m_W)*(i*2 + 1 - grid.m_W)
+ (j*2 + 1 - grid.m_H)*(j*2 + 1 - grid.m_H);
if (dist2 >= (grid.m_W - 2*edgeSize) * (grid.m_H - 2*edgeSize))
grid.set(i, j, edgeFlags);
}
}
}
else
{
u16 i0, j0, i1, j1;
NearestTile(m_WorldX0, m_WorldZ0, i0, j0, grid.m_W, grid.m_H);
NearestTile(m_WorldX1, m_WorldZ1, i1, j1, grid.m_W, grid.m_H);
for (u16 j = 0; j < grid.m_H; ++j)
for (u16 i = 0; i < i0+edgeSize; ++i)
grid.set(i, j, edgeFlags);
for (u16 j = 0; j < grid.m_H; ++j)
for (u16 i = (u16)(i1-edgeSize+1); i < grid.m_W; ++i)
grid.set(i, j, edgeFlags);
for (u16 j = 0; j < j0+edgeSize; ++j)
for (u16 i = (u16)(i0+edgeSize); i < i1-edgeSize+1; ++i)
grid.set(i, j, edgeFlags);
for (u16 j = (u16)(j1-edgeSize+1); j < grid.m_H; ++j)
for (u16 i = (u16)(i0+edgeSize); i < i1-edgeSize+1; ++i)
grid.set(i, j, edgeFlags);
Pathfinding::NearestNavcell(center.X - r, center.Y - r, i0, j0, grid.m_W, grid.m_H);
Pathfinding::NearestNavcell(center.X + r, center.Y + r, i1, j1, grid.m_W, grid.m_H);
for (u16 j = j0+1; j < j1; ++j)
for (u16 i = i0+1; i < i1; ++i)
grid.set(i, j, grid.get(i, j) | setMask);
}
return true;
}
void CCmpObstructionManager::GetObstructionsInRange(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, std::vector<ObstructionSquare>& squares)
@ -886,9 +828,9 @@ void CCmpObstructionManager::GetObstructionsInRange(const IObstructionTestFilter
std::vector<entity_id_t> unitShapes;
m_UnitSubdivision.GetInRange(unitShapes, CFixedVector2D(x0, z0), CFixedVector2D(x1, z1));
for (size_t i = 0; i < unitShapes.size(); ++i)
for (entity_id_t& unitShape : unitShapes)
{
std::map<u32, UnitShape>::iterator it = m_UnitShapes.find(unitShapes[i]);
auto it = m_UnitShapes.find(unitShape);
ENSURE(it != m_UnitShapes.end());
if (!filter.TestShape(UNIT_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, INVALID_ENTITY))
@ -902,15 +844,14 @@ void CCmpObstructionManager::GetObstructionsInRange(const IObstructionTestFilter
CFixedVector2D u(entity_pos_t::FromInt(1), entity_pos_t::Zero());
CFixedVector2D v(entity_pos_t::Zero(), entity_pos_t::FromInt(1));
ObstructionSquare s = { it->second.x, it->second.z, u, v, r, r };
squares.push_back(s);
squares.emplace_back(ObstructionSquare{ it->second.x, it->second.z, u, v, r, r });
}
std::vector<entity_id_t> staticShapes;
m_StaticSubdivision.GetInRange(staticShapes, CFixedVector2D(x0, z0), CFixedVector2D(x1, z1));
for (size_t i = 0; i < staticShapes.size(); ++i)
for (entity_id_t& staticShape : staticShapes)
{
std::map<u32, StaticShape>::iterator it = m_StaticShapes.find(staticShapes[i]);
auto it = m_StaticShapes.find(staticShape);
ENSURE(it != m_StaticShapes.end());
if (!filter.TestShape(STATIC_INDEX_TO_TAG(it->first), it->second.flags, it->second.group, it->second.group2))
@ -924,46 +865,65 @@ void CCmpObstructionManager::GetObstructionsInRange(const IObstructionTestFilter
// TODO: maybe we should use Geometry::GetHalfBoundingBox to be more precise?
ObstructionSquare s = { it->second.x, it->second.z, it->second.u, it->second.v, it->second.hw, it->second.hh };
squares.push_back(s);
squares.emplace_back(ObstructionSquare{ it->second.x, it->second.z, it->second.u, it->second.v, it->second.hw, it->second.hh });
}
}
bool CCmpObstructionManager::FindMostImportantObstruction(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, ObstructionSquare& square)
void CCmpObstructionManager::GetUnitsOnObstruction(const ObstructionSquare& square, std::vector<entity_id_t>& out, const IObstructionTestFilter& filter)
{
std::vector<ObstructionSquare> squares;
PROFILE3("GetUnitsOnObstruction");
CFixedVector2D center(x, z);
// In order to avoid getting units on impassable cells, we want to find all
// units s.t. the RasterizeRectWithClearance of the building's shape with the
// unit's clearance covers the navcell the unit is on.
// First look for obstructions that are covering the exact target point
GetObstructionsInRange(filter, x, z, x, z, squares);
// Building squares are more important but returned last, so check backwards
for (std::vector<ObstructionSquare>::reverse_iterator it = squares.rbegin(); it != squares.rend(); ++it)
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
if (!cmpPathfinder)
return;
entity_pos_t maxClearance = cmpPathfinder->GetMaximumClearance();
std::vector<entity_id_t> unitShapes;
CFixedVector2D center(square.x, square.z);
CFixedVector2D expandedBox =
Geometry::GetHalfBoundingBox(square.u, square.v, CFixedVector2D(square.hw, square.hh)) +
CFixedVector2D(maxClearance, maxClearance);
m_UnitSubdivision.GetInRange(unitShapes, center - expandedBox, center + expandedBox);
std::map<entity_pos_t, SimRasterize::Spans> rasterizedRects;
for (entity_id_t& unitShape : unitShapes)
{
CFixedVector2D halfSize(it->hw, it->hh);
if (Geometry::PointIsInSquare(CFixedVector2D(it->x, it->z) - center, it->u, it->v, halfSize))
auto it = m_UnitShapes.find(unitShape);
ENSURE(it != m_UnitShapes.end());
UnitShape& shape = it->second;
if (!filter.TestShape(UNIT_INDEX_TO_TAG(unitShape), shape.flags, shape.group, INVALID_ENTITY))
continue;
if (rasterizedRects.find(shape.clearance) == rasterizedRects.end())
{
square = *it;
return true;
SimRasterize::Spans& newSpans = rasterizedRects[shape.clearance];
SimRasterize::RasterizeRectWithClearance(newSpans, square, shape.clearance, Pathfinding::NAVCELL_SIZE);
}
SimRasterize::Spans& spans = rasterizedRects[shape.clearance];
// Check whether the unit's center is on a navcell that's in
// any of the spans
u16 i = (shape.x / Pathfinding::NAVCELL_SIZE).ToInt_RoundToNegInfinity();
u16 j = (shape.z / Pathfinding::NAVCELL_SIZE).ToInt_RoundToNegInfinity();
for (size_t k = 0; k < spans.size(); ++k)
{
if (j == spans[k].j && spans[k].i0 <= i && i < spans[k].i1)
out.push_back(shape.entity);
}
}
// Then look for obstructions that cover the target point when expanded by r
// (i.e. if the target is not inside an object but closer than we can get to it)
GetObstructionsInRange(filter, x-r, z-r, x+r, z+r, squares);
// Building squares are more important but returned last, so check backwards
for (std::vector<ObstructionSquare>::reverse_iterator it = squares.rbegin(); it != squares.rend(); ++it)
{
CFixedVector2D halfSize(it->hw + r, it->hh + r);
if (Geometry::PointIsInSquare(CFixedVector2D(it->x, it->z) - center, it->u, it->v, halfSize))
{
square = *it;
return true;
}
}
return false;
// TODO : Should we expand by r here?
}
void CCmpObstructionManager::RenderSubmit(SceneCollector& collector)

View File

@ -34,8 +34,10 @@
#include "simulation2/components/ICmpObstructionManager.h"
#include "simulation2/components/ICmpTerrain.h"
#include "simulation2/components/ICmpWaterManager.h"
#include "simulation2/helpers/Rasterize.h"
#include "simulation2/serialization/SerializeTemplates.h"
// Default cost to move a single tile is a fairly arbitrary number, which should be big
// enough to be precise when multiplied/divided and small enough to never overflow when
// summing the cost of a whole path.
@ -47,13 +49,14 @@ void CCmpPathfinder::Init(const CParamNode& UNUSED(paramNode))
{
m_MapSize = 0;
m_Grid = NULL;
m_ObstructionGrid = NULL;
m_BaseGrid = NULL;
m_ObstructionsDirty = true;
m_TerrainDirty = true;
m_NextAsyncTicket = 1;
m_DebugOverlay = NULL;
m_DebugGrid = NULL;
m_DebugPath = NULL;
m_DebugOverlay = false;
m_SameTurnMovesCount = 0;
@ -90,74 +93,18 @@ void CCmpPathfinder::Init(const CParamNode& UNUSED(paramNode))
{
std::string name = it->first;
ENSURE((int)m_PassClasses.size() <= PASS_CLASS_BITS);
pass_class_t mask = (pass_class_t)(1u << (m_PassClasses.size() + 2));
pass_class_t mask = PASS_CLASS_MASK_FROM_INDEX(m_PassClasses.size());
m_PassClasses.push_back(PathfinderPassability(mask, it->second));
m_PassClassMasks[name] = mask;
}
const CParamNode::ChildrenMap& moveClasses = externalParamNode.GetChild("Pathfinder").GetChild("MovementClasses").GetChildren();
// First find the set of unit classes used by any terrain classes,
// and assign unique tags to terrain classes
std::set<std::string> unitClassNames;
unitClassNames.insert("default"); // must always have costs for default
{
size_t i = 0;
for (CParamNode::ChildrenMap::const_iterator it = moveClasses.begin(); it != moveClasses.end(); ++it)
{
std::string terrainClassName = it->first;
m_TerrainCostClassTags[terrainClassName] = (cost_class_t)i;
++i;
const CParamNode::ChildrenMap& unitClasses = it->second.GetChild("UnitClasses").GetChildren();
for (CParamNode::ChildrenMap::const_iterator uit = unitClasses.begin(); uit != unitClasses.end(); ++uit)
unitClassNames.insert(uit->first);
}
}
// For each terrain class, set the costs for every unit class,
// and assign unique tags to unit classes
{
size_t i = 0;
for (std::set<std::string>::const_iterator nit = unitClassNames.begin(); nit != unitClassNames.end(); ++nit)
{
m_UnitCostClassTags[*nit] = (cost_class_t)i;
++i;
std::vector<u32> costs;
std::vector<fixed> speeds;
for (CParamNode::ChildrenMap::const_iterator it = moveClasses.begin(); it != moveClasses.end(); ++it)
{
// Default to the general costs for this terrain class
fixed cost = it->second.GetChild("@Cost").ToFixed();
fixed speed = it->second.GetChild("@Speed").ToFixed();
// Check for specific cost overrides for this unit class
const CParamNode& unitClass = it->second.GetChild("UnitClasses").GetChild(nit->c_str());
if (unitClass.IsOk())
{
cost = unitClass.GetChild("@Cost").ToFixed();
speed = unitClass.GetChild("@Speed").ToFixed();
}
costs.push_back((cost * DEFAULT_MOVE_COST).ToInt_RoundToZero());
speeds.push_back(speed);
}
m_MoveCosts.push_back(costs);
m_MoveSpeeds.push_back(speeds);
}
}
}
void CCmpPathfinder::Deinit()
{
SetDebugOverlay(false); // cleans up memory
ResetDebugPath();
delete m_Grid;
delete m_ObstructionGrid;
SAFE_DELETE(m_Grid);
SAFE_DELETE(m_BaseGrid);
}
struct SerializeLongRequest
@ -170,7 +117,6 @@ struct SerializeLongRequest
serialize.NumberFixed_Unbounded("z0", value.z0);
SerializeGoal()(serialize, "goal", value.goal);
serialize.NumberU16_Unbounded("pass class", value.passClass);
serialize.NumberU8_Unbounded("cost class", value.costClass);
serialize.NumberU32_Unbounded("notify", value.notify);
}
};
@ -225,7 +171,7 @@ void CCmpPathfinder::HandleMessage(const CMessage& msg, bool UNUSED(global))
case MT_WaterChanged:
case MT_ObstructionMapShapeChanged:
{
// TODO: we ought to only bother updating the dirtied region
// TODO PATHFINDER: we ought to only bother updating the dirtied region
m_TerrainDirty = true;
break;
}
@ -241,20 +187,12 @@ void CCmpPathfinder::RenderSubmit(SceneCollector& collector)
{
for (size_t i = 0; i < m_DebugOverlayShortPathLines.size(); ++i)
collector.Submit(&m_DebugOverlayShortPathLines[i]);
m_LongPathfinder.HierarchicalRenderSubmit(collector);
}
fixed CCmpPathfinder::GetMovementSpeed(entity_pos_t x0, entity_pos_t z0, u8 costClass)
{
UpdateGrid();
u16 i, j;
NearestTile(x0, z0, i, j);
TerrainTile tileTag = m_Grid->get(i, j);
return m_MoveSpeeds.at(costClass).at(GET_COST_CLASS(tileTag));
}
ICmpPathfinder::pass_class_t CCmpPathfinder::GetPassabilityClass(const std::string& name)
pass_class_t CCmpPathfinder::GetPassabilityClass(const std::string& name)
{
if (m_PassClassMasks.find(name) == m_PassClassMasks.end())
{
@ -265,43 +203,20 @@ ICmpPathfinder::pass_class_t CCmpPathfinder::GetPassabilityClass(const std::stri
return m_PassClassMasks[name];
}
std::map<std::string, ICmpPathfinder::pass_class_t> CCmpPathfinder::GetPassabilityClasses()
std::map<std::string, pass_class_t> CCmpPathfinder::GetPassabilityClasses()
{
return m_PassClassMasks;
}
ICmpPathfinder::cost_class_t CCmpPathfinder::GetCostClass(const std::string& name)
const PathfinderPassability* CCmpPathfinder::GetPassabilityFromMask(pass_class_t passClass) const
{
if (m_UnitCostClassTags.find(name) == m_UnitCostClassTags.end())
for (const PathfinderPassability& passability : m_PassClasses)
{
LOGERROR("Invalid unit cost class name '%s'", name.c_str());
return m_UnitCostClassTags["default"];
if (passability.m_Mask == passClass)
return &passability;
}
return m_UnitCostClassTags[name];
}
fixed CCmpPathfinder::DistanceToGoal(CFixedVector2D pos, const CCmpPathfinder::Goal& goal)
{
switch (goal.type)
{
case CCmpPathfinder::Goal::POINT:
return (pos - CFixedVector2D(goal.x, goal.z)).Length();
case CCmpPathfinder::Goal::CIRCLE:
return ((pos - CFixedVector2D(goal.x, goal.z)).Length() - goal.hw).Absolute();
case CCmpPathfinder::Goal::SQUARE:
{
CFixedVector2D halfSize(goal.hw, goal.hh);
CFixedVector2D d(pos.X - goal.x, pos.Y - goal.z);
return Geometry::DistanceToSquare(d, goal.u, goal.v, halfSize);
}
default:
debug_warn(L"invalid type");
return fixed::Zero();
}
return NULL;
}
const Grid<u16>& CCmpPathfinder::GetPassabilityGrid()
@ -310,6 +225,79 @@ const Grid<u16>& CCmpPathfinder::GetPassabilityGrid()
return *m_Grid;
}
/**
* Given a grid of passable/impassable navcells (based on some passability mask),
* computes a new grid where a navcell is impassable (per that mask) if
* it is <=clearance navcells away from an impassable navcell in the original grid.
* The results are ORed onto the original grid.
*
* This is used for adding clearance onto terrain-based navcell passability.
*
* TODO PATHFINDER: might be nicer to get rounded corners by measuring clearances as
* Euclidean distances; currently it effectively does dist=max(dx,dy) instead.
* This would only really be a problem for big clearances.
*/
static void ExpandImpassableCells(Grid<u16>& grid, u16 clearance, pass_class_t mask)
{
PROFILE3("ExpandImpassableCells");
u16 w = grid.m_W;
u16 h = grid.m_H;
// First expand impassable cells horizontally into a temporary 1-bit grid
Grid<u8> tempGrid(w, h);
for (u16 j = 0; j < h; ++j)
{
// New cell (i,j) is blocked if (i',j) blocked for any i-clearance <= i' <= i+clearance
// Count the number of blocked cells around i=0
u16 numBlocked = 0;
for (u16 i = 0; i <= clearance && i < w; ++i)
if (!IS_PASSABLE(grid.get(i, j), mask))
++numBlocked;
for (u16 i = 0; i < w; ++i)
{
// Store a flag if blocked by at least one nearby cell
if (numBlocked)
tempGrid.set(i, j, 1);
// Slide the numBlocked window along:
// remove the old i-clearance value, add the new (i+1)+clearance
// (avoiding overflowing the grid)
if (i >= clearance && !IS_PASSABLE(grid.get(i-clearance, j), mask))
--numBlocked;
if (i+1+clearance < w && !IS_PASSABLE(grid.get(i+1+clearance, j), mask))
++numBlocked;
}
}
for (u16 i = 0; i < w; ++i)
{
// New cell (i,j) is blocked if (i,j') blocked for any j-clearance <= j' <= j+clearance
// Count the number of blocked cells around j=0
u16 numBlocked = 0;
for (u16 j = 0; j <= clearance && j < h; ++j)
if (tempGrid.get(i, j))
++numBlocked;
for (u16 j = 0; j < h; ++j)
{
// Add the mask if blocked by at least one nearby cell
if (numBlocked)
grid.set(i, j, grid.get(i, j) | mask);
// Slide the numBlocked window along:
// remove the old j-clearance value, add the new (j+1)+clearance
// (avoiding overflowing the grid)
if (j >= clearance && tempGrid.get(i, j-clearance))
--numBlocked;
if (j+1+clearance < h && tempGrid.get(i, j+1+clearance))
++numBlocked;
}
}
}
Grid<u16> CCmpPathfinder::ComputeShoreGrid(bool expandOnWater)
{
PROFILE3("ComputeShoreGrid");
@ -323,16 +311,16 @@ Grid<u16> CCmpPathfinder::ComputeShoreGrid(bool expandOnWater)
const u16 shoreMax = 32767;
// First pass - find underwater tiles
Grid<bool> waterGrid(m_MapSize, m_MapSize);
Grid<u8> waterGrid(m_MapSize, m_MapSize);
for (u16 j = 0; j < m_MapSize; ++j)
{
for (u16 i = 0; i < m_MapSize; ++i)
{
fixed x, z;
TileCenter(i, j, x, z);
Pathfinding::TileCenter(i, j, x, z);
bool underWater = cmpWaterManager && (cmpWaterManager->GetWaterLevel(x, z) > terrain.GetExactGroundLevelFixed(x, z));
waterGrid.set(i, j, underWater);
waterGrid.set(i, j, underWater ? 1 : 0);
}
}
@ -360,7 +348,7 @@ Grid<u16> CCmpPathfinder::ComputeShoreGrid(bool expandOnWater)
}
}
// Expand influences to find shore distance
// Expand influences on land to find shore distance
for (u16 y = 0; y < m_MapSize; ++y)
{
u16 min = shoreMax;
@ -381,9 +369,9 @@ Grid<u16> CCmpPathfinder::ComputeShoreGrid(bool expandOnWater)
{
if (!waterGrid.get(x-1, y) || expandOnWater)
{
u16 g = shoreGrid.get(x - 1, y);
u16 g = shoreGrid.get(x-1, y);
if (g > min)
shoreGrid.set(x - 1, y, min);
shoreGrid.set(x-1, y, min);
else if (g < min)
min = g;
@ -411,9 +399,9 @@ Grid<u16> CCmpPathfinder::ComputeShoreGrid(bool expandOnWater)
{
if (!waterGrid.get(x, y-1) || expandOnWater)
{
u16 g = shoreGrid.get(x, y - 1);
u16 g = shoreGrid.get(x, y-1);
if (g > min)
shoreGrid.set(x, y - 1, min);
shoreGrid.set(x, y-1, min);
else if (g < min)
min = g;
@ -425,17 +413,73 @@ Grid<u16> CCmpPathfinder::ComputeShoreGrid(bool expandOnWater)
return shoreGrid;
}
void CCmpPathfinder::ComputeTerrainPassabilityGrid(const Grid<u16>& shoreGrid)
{
PROFILE3("terrain passability");
CmpPtr<ICmpWaterManager> cmpWaterManager(GetSimContext(), SYSTEM_ENTITY);
CTerrain& terrain = GetSimContext().GetTerrain();
// Compute initial terrain-dependent passability
for (int j = 0; j < m_MapSize * Pathfinding::NAVCELLS_PER_TILE; ++j)
{
for (int i = 0; i < m_MapSize * Pathfinding::NAVCELLS_PER_TILE; ++i)
{
// World-space coordinates for this navcell
fixed x, z;
Pathfinding::NavcellCenter(i, j, x, z);
// Terrain-tile coordinates for this navcell
int itile = i / Pathfinding::NAVCELLS_PER_TILE;
int jtile = j / Pathfinding::NAVCELLS_PER_TILE;
// Gather all the data potentially needed to determine passability:
fixed height = terrain.GetExactGroundLevelFixed(x, z);
fixed water;
if (cmpWaterManager)
water = cmpWaterManager->GetWaterLevel(x, z);
fixed depth = water - height;
//fixed slope = terrain.GetExactSlopeFixed(x, z);
// Exact slopes give kind of weird output, so just use rough tile-based slopes
fixed slope = terrain.GetSlopeFixed(itile, jtile);
// Get world-space coordinates from shoreGrid (which uses terrain tiles)
fixed shoredist = fixed::FromInt(shoreGrid.get(itile, jtile)).MultiplyClamp(TERRAIN_TILE_SIZE);
// Compute the passability for every class for this cell:
NavcellData t = 0;
for (PathfinderPassability& passability : m_PassClasses)
{
if (!passability.IsPassable(depth, slope, shoredist))
t |= passability.m_Mask;
}
m_Grid->set(i, j, t);
}
}
}
void CCmpPathfinder::UpdateGrid()
{
CmpPtr<ICmpTerrain> cmpTerrain(GetSystemEntity());
PROFILE3("UpdateGrid");
CmpPtr<ICmpTerrain> cmpTerrain(GetSimContext(), SYSTEM_ENTITY);
if (!cmpTerrain)
return; // error
bool forceGlobalUpdate = false;
// If the terrain was resized then delete the old grid data
if (m_Grid && m_MapSize != cmpTerrain->GetTilesPerSide())
{
SAFE_DELETE(m_Grid);
SAFE_DELETE(m_ObstructionGrid);
SAFE_DELETE(m_BaseGrid);
m_TerrainDirty = true;
}
@ -443,127 +487,142 @@ void CCmpPathfinder::UpdateGrid()
if (!m_Grid)
{
m_MapSize = cmpTerrain->GetTilesPerSide();
m_Grid = new Grid<TerrainTile>(m_MapSize, m_MapSize);
m_ObstructionGrid = new Grid<u8>(m_MapSize, m_MapSize);
m_Grid = new Grid<NavcellData>(m_MapSize * Pathfinding::NAVCELLS_PER_TILE, m_MapSize * Pathfinding::NAVCELLS_PER_TILE);
m_BaseGrid = new Grid<NavcellData>(m_MapSize * Pathfinding::NAVCELLS_PER_TILE, m_MapSize * Pathfinding::NAVCELLS_PER_TILE);
forceGlobalUpdate = true;
m_TerrainDirty = true;
}
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSystemEntity());
bool obstructionsDirty = cmpObstructionManager->Rasterise(*m_ObstructionGrid);
if (obstructionsDirty && !m_TerrainDirty)
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
if (forceGlobalUpdate)
{
PROFILE("UpdateGrid obstructions");
// Obstructions changed - we need to recompute passability
// Since terrain hasn't changed we only need to update the obstruction bits
// and can skip the rest of the data
for (u16 j = 0; j < m_MapSize; ++j)
{
for (u16 i = 0; i < m_MapSize; ++i)
{
TerrainTile& t = m_Grid->get(i, j);
u8 obstruct = m_ObstructionGrid->get(i, j);
if (obstruct & ICmpObstructionManager::TILE_OBSTRUCTED_PATHFINDING)
t |= 1;
else
t &= (TerrainTile)~1;
if (obstruct & ICmpObstructionManager::TILE_OBSTRUCTED_FOUNDATION)
t |= 2;
else
t &= (TerrainTile)~2;
}
}
++m_Grid->m_DirtyID;
m_ObstructionsDirty = true;
m_ObstructionsGlobalUpdate = true;
}
else if (obstructionsDirty || m_TerrainDirty)
else
m_ObstructionsDirty = cmpObstructionManager->GetDirtinessData(m_DirtinessGrid, m_ObstructionsGlobalUpdate);
if (!m_ObstructionsDirty && !m_TerrainDirty)
return;
// If the terrain has changed, recompute entirely m_Grid
// Else, use data from m_BaseGrid and add obstructions
if (m_TerrainDirty)
{
PROFILE("UpdateGrid full");
// Obstructions or terrain changed - we need to recompute passability
// TODO: only bother recomputing the region that has actually changed
Grid<u16> shoreGrid = ComputeShoreGrid();
CmpPtr<ICmpWaterManager> cmpWaterManager(GetSystemEntity());
CTerrain& terrain = GetSimContext().GetTerrain();
ComputeTerrainPassabilityGrid(shoreGrid);
// Apply passability classes to terrain
for (u16 j = 0; j < m_MapSize; ++j)
// Compute off-world passability
// WARNING: CCmpRangeManager::LosIsOffWorld needs to be kept in sync with this
const int edgeSize = 3 * Pathfinding::NAVCELLS_PER_TILE; // number of tiles around the edge that will be off-world
NavcellData edgeMask = 0;
for (PathfinderPassability& passability : m_PassClasses)
edgeMask |= passability.m_Mask;
int w = m_Grid->m_W;
int h = m_Grid->m_H;
if (cmpObstructionManager->GetPassabilityCircular())
{
for (u16 i = 0; i < m_MapSize; ++i)
for (int j = 0; j < h; ++j)
{
fixed x, z;
TileCenter(i, j, x, z);
TerrainTile t = 0;
u8 obstruct = m_ObstructionGrid->get(i, j);
fixed height = terrain.GetExactGroundLevelFixed(x, z);
fixed water;
if (cmpWaterManager)
water = cmpWaterManager->GetWaterLevel(x, z);
fixed depth = water - height;
fixed slope = terrain.GetSlopeFixed(i, j);
fixed shoredist = fixed::FromInt(shoreGrid.get(i, j));
if (obstruct & ICmpObstructionManager::TILE_OBSTRUCTED_PATHFINDING)
t |= 1;
if (obstruct & ICmpObstructionManager::TILE_OBSTRUCTED_FOUNDATION)
t |= 2;
if (obstruct & ICmpObstructionManager::TILE_OUTOFBOUNDS)
for (int i = 0; i < w; ++i)
{
// If out of bounds, nobody is allowed to pass
for (size_t n = 0; n < m_PassClasses.size(); ++n)
t |= m_PassClasses[n].m_Mask;
}
else
{
for (size_t n = 0; n < m_PassClasses.size(); ++n)
{
if (!m_PassClasses[n].IsPassable(depth, slope, shoredist))
t |= m_PassClasses[n].m_Mask;
}
}
// Based on CCmpRangeManager::LosIsOffWorld
// but tweaked since it's tile-based instead.
// (We double all the values so we can handle half-tile coordinates.)
// This needs to be slightly tighter than the LOS circle,
// else units might get themselves lost in the SoD around the edge.
std::string moveClass = terrain.GetMovementClass(i, j);
if (m_TerrainCostClassTags.find(moveClass) != m_TerrainCostClassTags.end())
t |= COST_CLASS_MASK(m_TerrainCostClassTags[moveClass]);
int dist2 = (i*2 + 1 - w)*(i*2 + 1 - w)
+ (j*2 + 1 - h)*(j*2 + 1 - h);
m_Grid->set(i, j, t);
if (dist2 >= (w - 2*edgeSize) * (h - 2*edgeSize))
m_Grid->set(i, j, m_Grid->get(i, j) | edgeMask);
}
}
}
else
{
for (u16 j = 0; j < h; ++j)
for (u16 i = 0; i < edgeSize; ++i)
m_Grid->set(i, j, m_Grid->get(i, j) | edgeMask);
for (u16 j = 0; j < h; ++j)
for (u16 i = w-edgeSize+1; i < w; ++i)
m_Grid->set(i, j, m_Grid->get(i, j) | edgeMask);
for (u16 j = 0; j < edgeSize; ++j)
for (u16 i = edgeSize; i < w-edgeSize+1; ++i)
m_Grid->set(i, j, m_Grid->get(i, j) | edgeMask);
for (u16 j = h-edgeSize+1; j < h; ++j)
for (u16 i = edgeSize; i < w-edgeSize+1; ++i)
m_Grid->set(i, j, m_Grid->get(i, j) | edgeMask);
}
// Expand the impassability grid, for any class with non-zero clearance,
// so that we can stop units getting too close to impassable navcells
for (PathfinderPassability& passability : m_PassClasses)
{
if (!passability.m_HasClearance)
continue;
// TODO: if multiple classes have the same clearance, we should
// only bother doing this once for them all
int clearance = (passability.m_Clearance / Pathfinding::NAVCELL_SIZE).ToInt_RoundToInfinity();
if (clearance > 0)
ExpandImpassableCells(*m_Grid, clearance, passability.m_Mask);
}
// Store the updated terrain-only grid
*m_BaseGrid = *m_Grid;
m_TerrainDirty = false;
++m_Grid->m_DirtyID;
}
else
{
ENSURE(m_Grid->m_W == m_BaseGrid->m_W && m_Grid->m_H == m_BaseGrid->m_H);
memcpy(m_Grid->m_Data, m_BaseGrid->m_Data, (m_Grid->m_W)*(m_Grid->m_H)*sizeof(NavcellData));
}
// TODO: tweak rasterizing conditions
// Add obstructions onto the grid, for any class with (possibly zero) clearance
for (PathfinderPassability& passability : m_PassClasses)
{
// TODO: if multiple classes have the same clearance, we should
// only bother running Rasterize once for them all
if (passability.m_HasClearance)
cmpObstructionManager->Rasterize(*m_Grid, passability.m_Clearance, ICmpObstructionManager::FLAG_BLOCK_PATHFINDING, passability.m_Mask);
}
if (m_ObstructionsGlobalUpdate)
m_LongPathfinder.Reload(m_PassClassMasks, m_Grid);
else
m_LongPathfinder.Update(m_Grid, &m_DirtinessGrid);
}
bool CCmpPathfinder::GetDirtinessData(Grid<u8>& dirtinessGrid, bool& globalUpdateNeeded)
{
if (!m_ObstructionsDirty)
return false;
dirtinessGrid = m_DirtinessGrid;
globalUpdateNeeded = m_ObstructionsGlobalUpdate;
return true;
}
//////////////////////////////////////////////////////////
// Async path requests:
u32 CCmpPathfinder::ComputePathAsync(entity_pos_t x0, entity_pos_t z0, const Goal& goal, pass_class_t passClass, cost_class_t costClass, entity_id_t notify)
u32 CCmpPathfinder::ComputePathAsync(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass, entity_id_t notify)
{
AsyncLongPathRequest req = { m_NextAsyncTicket++, x0, z0, goal, passClass, costClass, notify };
AsyncLongPathRequest req = { m_NextAsyncTicket++, x0, z0, goal, passClass, notify };
m_AsyncLongPathRequests.push_back(req);
return req.ticket;
}
u32 CCmpPathfinder::ComputeShortPathAsync(entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const Goal& goal, pass_class_t passClass, bool avoidMovingUnits, entity_id_t group, entity_id_t notify)
u32 CCmpPathfinder::ComputeShortPathAsync(entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const PathGoal& goal, pass_class_t passClass, bool avoidMovingUnits, entity_id_t group, entity_id_t notify)
{
AsyncShortPathRequest req = { m_NextAsyncTicket++, x0, z0, r, range, goal, passClass, avoidMovingUnits, group, notify };
m_AsyncShortPathRequests.push_back(req);
@ -593,8 +652,8 @@ void CCmpPathfinder::ProcessLongRequests(const std::vector<AsyncLongPathRequest>
for (size_t i = 0; i < longRequests.size(); ++i)
{
const AsyncLongPathRequest& req = longRequests[i];
Path path;
ComputePath(req.x0, req.z0, req.goal, req.passClass, req.costClass, path);
WaypointPath path;
ComputePath(req.x0, req.z0, req.goal, req.passClass, path);
CMessagePathResult msg(req.ticket, path);
GetSimContext().GetComponentManager().PostMessage(req.notify, msg);
}
@ -605,7 +664,7 @@ void CCmpPathfinder::ProcessShortRequests(const std::vector<AsyncShortPathReques
for (size_t i = 0; i < shortRequests.size(); ++i)
{
const AsyncShortPathRequest& req = shortRequests[i];
Path path;
WaypointPath path;
ControlGroupMovementObstructionFilter filter(req.avoidMovingUnits, req.group);
ComputeShortPath(filter, req.x0, req.z0, req.r, req.range, req.goal, req.passClass, path);
CMessagePathResult msg(req.ticket, path);
@ -670,14 +729,10 @@ void CCmpPathfinder::ProcessSameTurnMoves()
}
}
ICmpObstruction::EFoundationCheck CCmpPathfinder::CheckUnitPlacement(const IObstructionTestFilter& filter,
entity_pos_t x, entity_pos_t z, entity_pos_t r, pass_class_t passClass)
{
return CCmpPathfinder::CheckUnitPlacement(filter, x, z, r, passClass, false);
}
//////////////////////////////////////////////////////////
ICmpObstruction::EFoundationCheck CCmpPathfinder::CheckUnitPlacement(const IObstructionTestFilter& filter,
entity_pos_t x, entity_pos_t z, entity_pos_t r, pass_class_t passClass, bool onlyCenterPoint)
entity_pos_t x, entity_pos_t z, entity_pos_t r, pass_class_t passClass, bool UNUSED(onlyCenterPoint))
{
// Check unit obstruction
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSystemEntity());
@ -687,34 +742,17 @@ ICmpObstruction::EFoundationCheck CCmpPathfinder::CheckUnitPlacement(const IObst
if (cmpObstructionManager->TestUnitShape(filter, x, z, r, NULL))
return ICmpObstruction::FOUNDATION_CHECK_FAIL_OBSTRUCTS_FOUNDATION;
// Test against terrain:
UpdateGrid();
if (onlyCenterPoint)
{
u16 i, j;
NearestTile(x , z, i, j);
if (IS_TERRAIN_PASSABLE(m_Grid->get(i,j), passClass))
return ICmpObstruction::FOUNDATION_CHECK_SUCCESS;
// Test against terrain and static obstructions:
u16 i, j;
Pathfinding::NearestNavcell(x, z, i, j, m_MapSize*Pathfinding::NAVCELLS_PER_TILE, m_MapSize*Pathfinding::NAVCELLS_PER_TILE);
if (!IS_PASSABLE(m_Grid->get(i, j), passClass))
return ICmpObstruction::FOUNDATION_CHECK_FAIL_TERRAIN_CLASS;
}
u16 i0, j0, i1, j1;
NearestTile(x - r, z - r, i0, j0);
NearestTile(x + r, z + r, i1, j1);
for (u16 j = j0; j <= j1; ++j)
{
for (u16 i = i0; i <= i1; ++i)
{
if (!IS_TERRAIN_PASSABLE(m_Grid->get(i,j), passClass))
{
return ICmpObstruction::FOUNDATION_CHECK_FAIL_TERRAIN_CLASS;
}
}
}
// (Static obstructions will be redundantly tested against in both the
// obstruction-shape test and navcell-passability test, which is slightly
// inefficient but shouldn't affect behaviour)
return ICmpObstruction::FOUNDATION_CHECK_SUCCESS;
}
@ -728,7 +766,7 @@ ICmpObstruction::EFoundationCheck CCmpPathfinder::CheckBuildingPlacement(const I
ICmpObstruction::EFoundationCheck CCmpPathfinder::CheckBuildingPlacement(const IObstructionTestFilter& filter,
entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w,
entity_pos_t h, entity_id_t id, pass_class_t passClass, bool onlyCenterPoint)
entity_pos_t h, entity_id_t id, pass_class_t passClass, bool UNUSED(onlyCenterPoint))
{
// Check unit obstruction
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSystemEntity());
@ -747,38 +785,29 @@ ICmpObstruction::EFoundationCheck CCmpPathfinder::CheckBuildingPlacement(const I
if (!cmpObstruction || !cmpObstruction->GetObstructionSquare(square))
return ICmpObstruction::FOUNDATION_CHECK_FAIL_NO_OBSTRUCTION;
if (onlyCenterPoint)
entity_pos_t expand;
const PathfinderPassability* passability = GetPassabilityFromMask(passClass);
if (passability && passability->m_HasClearance)
expand = passability->m_Clearance;
SimRasterize::Spans spans;
SimRasterize::RasterizeRectWithClearance(spans, square, expand, Pathfinding::NAVCELL_SIZE);
for (SimRasterize::Span& span : spans)
{
u16 i, j;
NearestTile(x, z, i, j);
i16 i0 = span.i0;
i16 i1 = span.i1;
i16 j = span.j;
if (IS_TERRAIN_PASSABLE(m_Grid->get(i,j), passClass))
return ICmpObstruction::FOUNDATION_CHECK_SUCCESS;
// Fail if any span extends outside the grid
if (i0 < 0 || i1 > m_Grid->m_W || j < 0 || j > m_Grid->m_H)
return ICmpObstruction::FOUNDATION_CHECK_FAIL_TERRAIN_CLASS;
return ICmpObstruction::FOUNDATION_CHECK_FAIL_TERRAIN_CLASS;
}
// Expand bounds by 1/sqrt(2) tile (multiply by TERRAIN_TILE_SIZE since we want world coordinates)
entity_pos_t expand = entity_pos_t::FromInt(2).Sqrt().Multiply(entity_pos_t::FromInt(TERRAIN_TILE_SIZE / 2));
CFixedVector2D halfSize(square.hw + expand, square.hh + expand);
CFixedVector2D halfBound = Geometry::GetHalfBoundingBox(square.u, square.v, halfSize);
u16 i0, j0, i1, j1;
NearestTile(square.x - halfBound.X, square.z - halfBound.Y, i0, j0);
NearestTile(square.x + halfBound.X, square.z + halfBound.Y, i1, j1);
for (u16 j = j0; j <= j1; ++j)
{
for (u16 i = i0; i <= i1; ++i)
{
entity_pos_t x, z;
TileCenter(i, j, x, z);
if (Geometry::PointIsInSquare(CFixedVector2D(x - square.x, z - square.z), square.u, square.v, halfSize)
&& !IS_TERRAIN_PASSABLE(m_Grid->get(i,j), passClass))
{
// Fail if any span includes an impassable tile
for (i16 i = i0; i < i1; ++i)
if (!IS_PASSABLE(m_Grid->get(i, j), passClass))
return ICmpObstruction::FOUNDATION_CHECK_FAIL_TERRAIN_CLASS;
}
}
}
return ICmpObstruction::FOUNDATION_CHECK_SUCCESS;
}

View File

@ -20,11 +20,11 @@
/**
* @file
* Declares CCmpPathfinder, whose implementation is split into multiple source files,
* and provides common code needed for more than one of those files.
* CCmpPathfinder includes two pathfinding algorithms (one tile-based, one vertex-based)
* with some shared state and functionality, so the code is split into
* CCmpPathfinder_Vertex.cpp, CCmpPathfinder_Tile.cpp and CCmpPathfinder.cpp
* Declares CCmpPathfinder. Its implementation is mainly done in CCmpPathfinder.cpp,
* but the short-range (vertex) pathfinding is done in CCmpPathfinder_Vertex.cpp.
* This file provides common code needed for both files.
*
* The long-range pathfinding is done by a LongPathfinder object.
*/
#include "simulation2/system/Component.h"
@ -34,12 +34,12 @@
#include "graphics/Overlay.h"
#include "graphics/Terrain.h"
#include "maths/MathUtil.h"
#include "ps/CLogger.h"
#include "simulation2/components/ICmpObstructionManager.h"
#include "simulation2/helpers/Geometry.h"
#include "simulation2/helpers/Grid.h"
#include "simulation2/helpers/LongPathfinder.h"
class PathfinderOverlay;
class SceneCollector;
struct PathfindTile;
#ifdef NDEBUG
#define PATHFIND_DEBUG 0
@ -47,105 +47,14 @@ struct PathfindTile;
#define PATHFIND_DEBUG 1
#endif
/*
* For efficient pathfinding we want to try hard to minimise the per-tile search cost,
* so we precompute the tile passability flags and movement costs for the various different
* types of unit.
* We also want to minimise memory usage (there can easily be 100K tiles so we don't want
* to store many bytes for each).
*
* To handle passability efficiently, we have a small number of passability classes
* (e.g. "infantry", "ship"). Each unit belongs to a single passability class, and
* uses that for all its pathfinding.
* Passability is determined by water depth, terrain slope, forestness, buildingness.
* We need at least one bit per class per tile to represent passability.
*
* We use a separate bit to indicate building obstructions (instead of folding it into
* the class passabilities) so that it can be ignored when doing the accurate short paths.
* We use another bit to indicate tiles near obstructions that block construction,
* for the AI to plan safe building spots.
*
* To handle movement costs, we have an arbitrary number of unit cost classes (e.g. "infantry", "camel"),
* and a small number of terrain cost classes (e.g. "grass", "steep grass", "road", "sand"),
* and a cost mapping table between the classes (e.g. camels are fast on sand).
* We need log2(|terrain cost classes|) bits per tile to represent costs.
*
* We could have one passability bitmap per class, and another array for cost classes,
* but instead (for no particular reason) we'll pack them all into a single u16 array.
*
* We handle dynamic updates currently by recomputing the entire array, which is stupid;
* it should only bother updating the region that has changed.
*/
class PathfinderPassability
{
public:
PathfinderPassability(ICmpPathfinder::pass_class_t mask, const CParamNode& node) :
m_Mask(mask)
{
if (node.GetChild("MinWaterDepth").IsOk())
m_MinDepth = node.GetChild("MinWaterDepth").ToFixed();
else
m_MinDepth = std::numeric_limits<fixed>::min();
if (node.GetChild("MaxWaterDepth").IsOk())
m_MaxDepth = node.GetChild("MaxWaterDepth").ToFixed();
else
m_MaxDepth = std::numeric_limits<fixed>::max();
if (node.GetChild("MaxTerrainSlope").IsOk())
m_MaxSlope = node.GetChild("MaxTerrainSlope").ToFixed();
else
m_MaxSlope = std::numeric_limits<fixed>::max();
if (node.GetChild("MinShoreDistance").IsOk())
m_MinShore = node.GetChild("MinShoreDistance").ToFixed();
else
m_MinShore = std::numeric_limits<fixed>::min();
if (node.GetChild("MaxShoreDistance").IsOk())
m_MaxShore = node.GetChild("MaxShoreDistance").ToFixed();
else
m_MaxShore = std::numeric_limits<fixed>::max();
}
bool IsPassable(fixed waterdepth, fixed steepness, fixed shoredist)
{
return ((m_MinDepth <= waterdepth && waterdepth <= m_MaxDepth) && (steepness < m_MaxSlope) && (m_MinShore <= shoredist && shoredist <= m_MaxShore));
}
ICmpPathfinder::pass_class_t m_Mask;
private:
fixed m_MinDepth;
fixed m_MaxDepth;
fixed m_MaxSlope;
fixed m_MinShore;
fixed m_MaxShore;
};
typedef u16 TerrainTile;
// 1 bit for pathfinding obstructions,
// 1 bit for construction obstructions (used by AI),
// PASS_CLASS_BITS for terrain passability (allowing PASS_CLASS_BITS classes),
// COST_CLASS_BITS for movement costs (allowing 2^COST_CLASS_BITS classes)
const int PASS_CLASS_BITS = 10;
const int COST_CLASS_BITS = 16 - (PASS_CLASS_BITS + 2);
#define IS_TERRAIN_PASSABLE(item, classmask) (((item) & (classmask)) == 0)
#define IS_PASSABLE(item, classmask) (((item) & ((classmask) | 1)) == 0)
#define GET_COST_CLASS(item) ((item) >> (PASS_CLASS_BITS + 2))
#define COST_CLASS_MASK(id) ( (TerrainTile) ((id) << (PASS_CLASS_BITS + 2)) )
typedef SparseGrid<PathfindTile> PathfindTileGrid;
struct AsyncLongPathRequest
{
u32 ticket;
entity_pos_t x0;
entity_pos_t z0;
ICmpPathfinder::Goal goal;
ICmpPathfinder::pass_class_t passClass;
ICmpPathfinder::cost_class_t costClass;
PathGoal goal;
pass_class_t passClass;
entity_id_t notify;
};
@ -156,8 +65,8 @@ struct AsyncShortPathRequest
entity_pos_t z0;
entity_pos_t r;
entity_pos_t range;
ICmpPathfinder::Goal goal;
ICmpPathfinder::pass_class_t passClass;
PathGoal goal;
pass_class_t passClass;
bool avoidMovingUnits;
entity_id_t group;
entity_id_t notify;
@ -186,11 +95,6 @@ public:
std::map<std::string, pass_class_t> m_PassClassMasks;
std::vector<PathfinderPassability> m_PassClasses;
std::map<std::string, cost_class_t> m_TerrainCostClassTags;
std::map<std::string, cost_class_t> m_UnitCostClassTags;
std::vector<std::vector<u32> > m_MoveCosts; // costs[unitClass][terrainClass]
std::vector<std::vector<fixed> > m_MoveSpeeds; // speeds[unitClass][terrainClass]
// Dynamic state:
std::vector<AsyncLongPathRequest> m_AsyncLongPathRequests;
@ -201,22 +105,23 @@ public:
// Lazily-constructed dynamic state (not serialized):
u16 m_MapSize; // tiles per side
Grid<TerrainTile>* m_Grid; // terrain/passability information
Grid<u8>* m_ObstructionGrid; // cached obstruction information (TODO: we shouldn't bother storing this, it's redundant with LSBs of m_Grid)
Grid<NavcellData>* m_Grid; // terrain/passability information
Grid<NavcellData>* m_BaseGrid; // same as m_Grid, but only with terrain, to avoid some recomputations
bool m_TerrainDirty; // indicates if m_Grid has been updated since terrain changed
// Update data, stored for the AI manager
bool m_ObstructionsDirty;
bool m_ObstructionsGlobalUpdate;
Grid<u8> m_DirtinessGrid;
// Interface to the long-range pathfinder.
LongPathfinder m_LongPathfinder;
// For responsiveness we will process some moves in the same turn they were generated in
u16 m_MaxSameTurnMoves; // max number of moves that can be created and processed in the same turn
// Debugging - output from last pathfind operation:
PathfindTileGrid* m_DebugGrid;
u32 m_DebugSteps;
Path* m_DebugPath;
PathfinderOverlay* m_DebugOverlay;
pass_class_t m_DebugPassClass;
bool m_DebugOverlay;
std::vector<SOverlayLine> m_DebugOverlayShortPathLines;
static std::string GetSchema()
@ -238,35 +143,72 @@ public:
virtual std::map<std::string, pass_class_t> GetPassabilityClasses();
virtual cost_class_t GetCostClass(const std::string& name);
const PathfinderPassability* GetPassabilityFromMask(pass_class_t passClass) const;
virtual entity_pos_t GetClearance(pass_class_t passClass) const
{
const PathfinderPassability* passability = GetPassabilityFromMask(passClass);
if (!passability->m_HasClearance)
return fixed::Zero();
return passability->m_Clearance;
}
virtual entity_pos_t GetMaximumClearance() const
{
entity_pos_t max = fixed::Zero();
for (const PathfinderPassability& passability : m_PassClasses)
{
if (passability.m_HasClearance && passability.m_Clearance > max)
max = passability.m_Clearance;
}
return max;
}
virtual const Grid<u16>& GetPassabilityGrid();
virtual bool GetDirtinessData(Grid<u8>& dirtinessGrid, bool& globalUpdateNeeded);
virtual Grid<u16> ComputeShoreGrid(bool expandOnWater = false);
virtual void ComputePath(entity_pos_t x0, entity_pos_t z0, const Goal& goal, pass_class_t passClass, cost_class_t costClass, Path& ret);
virtual void ComputePath(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass, WaypointPath& ret)
{
m_LongPathfinder.ComputePath(x0, z0, goal, passClass, ret);
}
virtual u32 ComputePathAsync(entity_pos_t x0, entity_pos_t z0, const Goal& goal, pass_class_t passClass, cost_class_t costClass, entity_id_t notify);
virtual u32 ComputePathAsync(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass, entity_id_t notify);
virtual void ComputeShortPath(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const Goal& goal, pass_class_t passClass, Path& ret);
virtual void ComputeShortPath(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const PathGoal& goal, pass_class_t passClass, WaypointPath& ret);
virtual u32 ComputeShortPathAsync(entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const Goal& goal, pass_class_t passClass, bool avoidMovingUnits, entity_id_t controller, entity_id_t notify);
virtual u32 ComputeShortPathAsync(entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const PathGoal& goal, pass_class_t passClass, bool avoidMovingUnits, entity_id_t controller, entity_id_t notify);
virtual void SetDebugPath(entity_pos_t x0, entity_pos_t z0, const Goal& goal, pass_class_t passClass, cost_class_t costClass);
virtual void SetDebugPath(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass)
{
m_LongPathfinder.SetDebugPath(x0, z0, goal, passClass);
}
virtual void ResetDebugPath();
virtual void SetDebugOverlay(bool enabled)
{
m_DebugOverlay = enabled;
m_LongPathfinder.SetDebugOverlay(enabled);
}
virtual void SetDebugOverlay(bool enabled);
virtual void SetHierDebugOverlay(bool enabled)
{
m_LongPathfinder.SetHierDebugOverlay(enabled, &GetSimContext());
}
virtual fixed GetMovementSpeed(entity_pos_t x0, entity_pos_t z0, cost_class_t costClass);
virtual CFixedVector2D GetNearestPointOnGoal(CFixedVector2D pos, const Goal& goal);
virtual void GetDebugData(u32& steps, double& time, Grid<u8>& grid)
{
m_LongPathfinder.GetDebugData(steps, time, grid);
}
virtual bool CheckMovement(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, pass_class_t passClass);
virtual ICmpObstruction::EFoundationCheck CheckUnitPlacement(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, pass_class_t passClass);
virtual ICmpObstruction::EFoundationCheck CheckUnitPlacement(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, pass_class_t passClass, bool onlyCenterPoint);
virtual ICmpObstruction::EFoundationCheck CheckUnitPlacement(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, pass_class_t passClass, bool onlyCenterPoint);
virtual ICmpObstruction::EFoundationCheck CheckBuildingPlacement(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h, entity_id_t id, pass_class_t passClass);
@ -280,30 +222,12 @@ virtual ICmpObstruction::EFoundationCheck CheckUnitPlacement(const IObstructionT
virtual void ProcessSameTurnMoves();
/**
* Returns the tile containing the given position
*/
void NearestTile(entity_pos_t x, entity_pos_t z, u16& i, u16& j)
{
i = (u16)clamp((x / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), 0, m_MapSize-1);
j = (u16)clamp((z / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), 0, m_MapSize-1);
}
/**
* Returns the position of the center of the given tile
*/
static void TileCenter(u16 i, u16 j, entity_pos_t& x, entity_pos_t& z)
{
x = entity_pos_t::FromInt(i*(int)TERRAIN_TILE_SIZE + (int)TERRAIN_TILE_SIZE/2);
z = entity_pos_t::FromInt(j*(int)TERRAIN_TILE_SIZE + (int)TERRAIN_TILE_SIZE/2);
}
static fixed DistanceToGoal(CFixedVector2D pos, const CCmpPathfinder::Goal& goal);
/**
* Regenerates the grid based on the current obstruction list, if necessary
*/
void UpdateGrid();
virtual void UpdateGrid();
void ComputeTerrainPassabilityGrid(const Grid<u16>& shoreGrid);
void RenderSubmit(SceneCollector& collector);
};

View File

@ -1,502 +0,0 @@
/* Copyright (C) 2010 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/>.
*/
/**
* @file
* Tile-based algorithm for CCmpPathfinder.
* This is a fairly naive algorithm and could probably be improved substantially
* (hopefully without needing to change the interface much).
*/
#include "precompiled.h"
#include "CCmpPathfinder_Common.h"
#include "ps/Profile.h"
#include "renderer/TerrainOverlay.h"
#include "simulation2/helpers/PriorityQueue.h"
typedef PriorityQueueHeap<std::pair<u16, u16>, u32> PriorityQueue;
#define PATHFIND_STATS 0
#define USE_DIAGONAL_MOVEMENT 1
// Heuristic cost to move between adjacent tiles.
// This should be similar to DEFAULT_MOVE_COST.
const u32 g_CostPerTile = 256;
/**
* Tile data for A* computation.
* (We store an array of one of these per terrain tile, so it ought to be optimised for size)
*/
struct PathfindTile
{
public:
enum {
STATUS_UNEXPLORED = 0,
STATUS_OPEN = 1,
STATUS_CLOSED = 2
};
bool IsUnexplored() { return status == STATUS_UNEXPLORED; }
bool IsOpen() { return status == STATUS_OPEN; }
bool IsClosed() { return status == STATUS_CLOSED; }
void SetStatusOpen() { status = STATUS_OPEN; }
void SetStatusClosed() { status = STATUS_CLOSED; }
// Get pi,pj coords of predecessor to this tile on best path, given i,j coords of this tile
u16 GetPredI(u16 i) { return (u16)(i + dpi); }
u16 GetPredJ(u16 j) { return (u16)(j + dpj); }
// Set the pi,pj coords of predecessor, given i,j coords of this tile
void SetPred(u16 pi_, u16 pj_, u16 i, u16 j)
{
dpi = (i8)((int)pi_ - (int)i);
dpj = (i8)((int)pj_ - (int)j);
#if PATHFIND_DEBUG
// predecessor must be adjacent
ENSURE(pi_-i == -1 || pi_-i == 0 || pi_-i == 1);
ENSURE(pj_-j == -1 || pj_-j == 0 || pj_-j == 1);
#endif
}
private:
u8 status; // this only needs 2 bits
i8 dpi, dpj; // these only really need 2 bits in total
public:
u32 cost; // g (cost to this tile)
u32 h; // h (heuristic cost to goal) (TODO: is it really better for performance to store this instead of recomputing?)
#if PATHFIND_DEBUG
u32 GetStep() { return step; }
void SetStep(u32 s) { step = s; }
private:
u32 step; // step at which this tile was last processed (for debug rendering)
#else
u32 GetStep() { return 0; }
void SetStep(u32) { }
#endif
};
/**
* Terrain overlay for pathfinder debugging.
* Renders a representation of the most recent pathfinding operation.
*/
class PathfinderOverlay : public TerrainOverlay
{
NONCOPYABLE(PathfinderOverlay);
public:
CCmpPathfinder& m_Pathfinder;
PathfinderOverlay(CCmpPathfinder& pathfinder)
: TerrainOverlay(pathfinder.GetSimContext()), m_Pathfinder(pathfinder)
{
}
virtual void StartRender()
{
m_Pathfinder.UpdateGrid();
}
virtual void ProcessTile(ssize_t i, ssize_t j)
{
if (m_Pathfinder.m_Grid && !IS_PASSABLE(m_Pathfinder.m_Grid->get((int)i, (int)j), m_Pathfinder.m_DebugPassClass))
RenderTile(CColor(1, 0, 0, 0.6f), false);
if (m_Pathfinder.m_DebugGrid)
{
PathfindTile& n = m_Pathfinder.m_DebugGrid->get((int)i, (int)j);
float c = clamp((float)n.GetStep() / (float)m_Pathfinder.m_DebugSteps, 0.f, 1.f);
if (n.IsOpen())
RenderTile(CColor(1, 1, c, 0.6f), false);
else if (n.IsClosed())
RenderTile(CColor(0, 1, c, 0.6f), false);
}
}
virtual void EndRender()
{
if (m_Pathfinder.m_DebugPath)
{
std::vector<ICmpPathfinder::Waypoint>& wp = m_Pathfinder.m_DebugPath->m_Waypoints;
for (size_t n = 0; n < wp.size(); ++n)
{
u16 i, j;
m_Pathfinder.NearestTile(wp[n].x, wp[n].z, i, j);
RenderTileOutline(CColor(1, 1, 1, 1), 2, false, i, j);
}
}
}
};
void CCmpPathfinder::SetDebugOverlay(bool enabled)
{
if (enabled && !m_DebugOverlay)
{
m_DebugOverlay = new PathfinderOverlay(*this);
}
else if (!enabled && m_DebugOverlay)
{
delete m_DebugOverlay;
m_DebugOverlay = NULL;
}
}
void CCmpPathfinder::SetDebugPath(entity_pos_t x0, entity_pos_t z0, const Goal& goal, pass_class_t passClass, cost_class_t costClass)
{
if (!m_DebugOverlay)
return;
delete m_DebugGrid;
m_DebugGrid = NULL;
delete m_DebugPath;
m_DebugPath = new Path();
ComputePath(x0, z0, goal, passClass, costClass, *m_DebugPath);
m_DebugPassClass = passClass;
}
void CCmpPathfinder::ResetDebugPath()
{
delete m_DebugGrid;
m_DebugGrid = NULL;
delete m_DebugPath;
m_DebugPath = NULL;
}
//////////////////////////////////////////////////////////
struct PathfinderState
{
u32 steps; // number of algorithm iterations
u16 iGoal, jGoal; // goal tile
u16 rGoal; // radius of goal (around tile center)
ICmpPathfinder::pass_class_t passClass;
std::vector<u32> moveCosts;
PriorityQueue open;
// (there's no explicit closed list; it's encoded in PathfindTile)
PathfindTileGrid* tiles;
Grid<TerrainTile>* terrain;
bool ignoreImpassable; // allows us to escape if stuck in patches of impassability
u32 hBest; // heuristic of closest discovered tile to goal
u16 iBest, jBest; // closest tile
#if PATHFIND_STATS
// Performance debug counters
size_t numProcessed;
size_t numImproveOpen;
size_t numImproveClosed;
size_t numAddToOpen;
size_t sumOpenSize;
#endif
};
static bool AtGoal(u16 i, u16 j, const ICmpPathfinder::Goal& goal)
{
// Allow tiles slightly more than sqrt(2) from the actual goal,
// i.e. adjacent diagonally to the target tile
fixed tolerance = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3/2);
entity_pos_t x, z;
CCmpPathfinder::TileCenter(i, j, x, z);
fixed dist = CCmpPathfinder::DistanceToGoal(CFixedVector2D(x, z), goal);
return (dist < tolerance);
}
// Calculate heuristic cost from tile i,j to destination
// (This ought to be an underestimate for correctness)
static u32 CalculateHeuristic(u16 i, u16 j, u16 iGoal, u16 jGoal, u16 rGoal)
{
#if USE_DIAGONAL_MOVEMENT
CFixedVector2D pos (fixed::FromInt(i), fixed::FromInt(j));
CFixedVector2D goal (fixed::FromInt(iGoal), fixed::FromInt(jGoal));
fixed dist = (pos - goal).Length();
// TODO: the heuristic could match the costs better - it's not really Euclidean movement
fixed rdist = dist - fixed::FromInt(rGoal);
rdist = rdist.Absolute();
// To avoid overflows on large distances we have to convert to int before multiplying
// by the full tile cost, which means we lose some accuracy over short distances,
// so do a partial multiplication here.
// (This will overflow if sqrt(2)*tilesPerSide*premul >= 32768, so
// premul=32 means max tilesPerSide=724)
const int premul = 32;
cassert(g_CostPerTile % premul == 0);
return (rdist * premul).ToInt_RoundToZero() * (g_CostPerTile / premul);
#else
return (abs((int)i - (int)iGoal) + abs((int)j - (int)jGoal)) * g_CostPerTile;
#endif
}
// Calculate movement cost from predecessor tile pi,pj to tile i,j
static u32 CalculateCostDelta(u16 pi, u16 pj, u16 i, u16 j, PathfindTileGrid* tempGrid, u32 tileCost)
{
u32 dg = tileCost;
#if USE_DIAGONAL_MOVEMENT
// XXX: Probably a terrible hack:
// For simplicity, we only consider horizontally/vertically adjacent neighbours, but
// units can move along arbitrary lines. That results in ugly square paths, so we want
// to prefer diagonal paths.
// Instead of solving this nicely, I'll just special-case 45-degree and 30-degree lines
// by checking the three predecessor tiles (which'll be in the closed set and therefore
// likely to be reasonably stable) and reducing the cost, and use a Euclidean heuristic.
// At least this makes paths look a bit nicer for now...
PathfindTile& p = tempGrid->get(pi, pj);
u16 ppi = p.GetPredI(pi);
u16 ppj = p.GetPredJ(pj);
if (ppi != i && ppj != j)
dg = (dg << 16) / 92682; // dg*sqrt(2)/2
else
{
PathfindTile& pp = tempGrid->get(ppi, ppj);
int di = abs(i - pp.GetPredI(ppi));
int dj = abs(j - pp.GetPredJ(ppj));
if ((di == 1 && dj == 2) || (di == 2 && dj == 1))
dg = (dg << 16) / 79742; // dg*(sqrt(5)-sqrt(2))
}
#endif
return dg;
}
// Do the A* processing for a neighbour tile i,j.
static void ProcessNeighbour(u16 pi, u16 pj, u16 i, u16 j, u32 pg, PathfinderState& state)
{
#if PATHFIND_STATS
state.numProcessed++;
#endif
// Reject impassable tiles
TerrainTile tileTag = state.terrain->get(i, j);
if (!IS_PASSABLE(tileTag, state.passClass) && !state.ignoreImpassable)
return;
u32 dg = CalculateCostDelta(pi, pj, i, j, state.tiles, state.moveCosts.at(GET_COST_CLASS(tileTag)));
u32 g = pg + dg; // cost to this tile = cost to predecessor + delta from predecessor
PathfindTile& n = state.tiles->get(i, j);
// If this is a new tile, compute the heuristic distance
if (n.IsUnexplored())
{
n.h = CalculateHeuristic(i, j, state.iGoal, state.jGoal, state.rGoal);
// Remember the best tile we've seen so far, in case we never actually reach the target
if (n.h < state.hBest)
{
state.hBest = n.h;
state.iBest = i;
state.jBest = j;
}
}
else
{
// If we've already seen this tile, and the new path to this tile does not have a
// better cost, then stop now
if (g >= n.cost)
return;
// Otherwise, we have a better path.
// If we've already added this tile to the open list:
if (n.IsOpen())
{
// This is a better path, so replace the old one with the new cost/parent
n.cost = g;
n.SetPred(pi, pj, i, j);
n.SetStep(state.steps);
state.open.promote(std::make_pair(i, j), g + n.h);
#if PATHFIND_STATS
state.numImproveOpen++;
#endif
return;
}
// If we've already found the 'best' path to this tile:
if (n.IsClosed())
{
// This is a better path (possible when we use inadmissible heuristics), so reopen it
#if PATHFIND_STATS
state.numImproveClosed++;
#endif
// (fall through)
}
}
// Add it to the open list:
n.SetStatusOpen();
n.cost = g;
n.SetPred(pi, pj, i, j);
n.SetStep(state.steps);
PriorityQueue::Item t = { std::make_pair(i, j), g + n.h };
state.open.push(t);
#if PATHFIND_STATS
state.numAddToOpen++;
#endif
}
void CCmpPathfinder::ComputePath(entity_pos_t x0, entity_pos_t z0, const Goal& goal, pass_class_t passClass, cost_class_t costClass, Path& path)
{
UpdateGrid();
PROFILE3("ComputePath");
PathfinderState state = { 0 };
// Convert the start/end coordinates to tile indexes
u16 i0, j0;
NearestTile(x0, z0, i0, j0);
NearestTile(goal.x, goal.z, state.iGoal, state.jGoal);
// If we're already at the goal tile, then move directly to the exact goal coordinates
if (AtGoal(i0, j0, goal))
{
Waypoint w = { goal.x, goal.z };
path.m_Waypoints.push_back(w);
return;
}
// If the target is a circle, we want to aim for the edge of it (so e.g. if we're inside
// a large circle then the heuristics will aim us directly outwards);
// otherwise just aim at the center point. (We'll never try moving outwards to a square shape.)
if (goal.type == Goal::CIRCLE)
state.rGoal = (u16)(goal.hw / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero();
else
state.rGoal = 0;
state.passClass = passClass;
state.moveCosts = m_MoveCosts.at(costClass);
state.steps = 0;
state.tiles = new PathfindTileGrid(m_MapSize, m_MapSize);
state.terrain = m_Grid;
state.iBest = i0;
state.jBest = j0;
state.hBest = CalculateHeuristic(i0, j0, state.iGoal, state.jGoal, state.rGoal);
PriorityQueue::Item start = { std::make_pair(i0, j0), 0 };
state.open.push(start);
state.tiles->get(i0, j0).SetStatusOpen();
state.tiles->get(i0, j0).SetPred(i0, j0, i0, j0);
state.tiles->get(i0, j0).cost = 0;
// To prevent units getting very stuck, if they start on an impassable tile
// surrounded entirely by impassable tiles, we ignore the impassability
state.ignoreImpassable = !IS_PASSABLE(state.terrain->get(i0, j0), state.passClass);
while (1)
{
++state.steps;
// Hack to avoid spending ages computing giant paths, particularly when
// the destination is unreachable
if (state.steps > 40000)
break;
// If we ran out of tiles to examine, give up
if (state.open.empty())
break;
#if PATHFIND_STATS
state.sumOpenSize += state.open.size();
#endif
// Move best tile from open to closed
PriorityQueue::Item curr = state.open.pop();
u16 i = curr.id.first;
u16 j = curr.id.second;
state.tiles->get(i, j).SetStatusClosed();
// If we've reached the destination, stop
if (AtGoal(i, j, goal))
{
state.iBest = i;
state.jBest = j;
state.hBest = 0;
break;
}
// As soon as we find an escape route from the impassable terrain,
// take it and forbid any further use of impassable tiles
if (state.ignoreImpassable)
{
if (i > 0 && IS_PASSABLE(state.terrain->get(i-1, j), state.passClass))
state.ignoreImpassable = false;
else if (i < m_MapSize-1 && IS_PASSABLE(state.terrain->get(i+1, j), state.passClass))
state.ignoreImpassable = false;
else if (j > 0 && IS_PASSABLE(state.terrain->get(i, j-1), state.passClass))
state.ignoreImpassable = false;
else if (j < m_MapSize-1 && IS_PASSABLE(state.terrain->get(i, j+1), state.passClass))
state.ignoreImpassable = false;
}
u32 g = state.tiles->get(i, j).cost;
if (i > 0)
ProcessNeighbour(i, j, (u16)(i-1), j, g, state);
if (i < m_MapSize-1)
ProcessNeighbour(i, j, (u16)(i+1), j, g, state);
if (j > 0)
ProcessNeighbour(i, j, i, (u16)(j-1), g, state);
if (j < m_MapSize-1)
ProcessNeighbour(i, j, i, (u16)(j+1), g, state);
}
// Reconstruct the path (in reverse)
u16 ip = state.iBest, jp = state.jBest;
while (ip != i0 || jp != j0)
{
PathfindTile& n = state.tiles->get(ip, jp);
entity_pos_t x, z;
TileCenter(ip, jp, x, z);
Waypoint w = { x, z };
path.m_Waypoints.push_back(w);
// Follow the predecessor link
ip = n.GetPredI(ip);
jp = n.GetPredJ(jp);
}
// Save this grid for debug display
delete m_DebugGrid;
m_DebugGrid = state.tiles;
m_DebugSteps = state.steps;
PROFILE2_ATTR("from: (%d, %d)", i0, j0);
PROFILE2_ATTR("to: (%d, %d)", state.iGoal, state.jGoal);
PROFILE2_ATTR("reached: (%d, %d)", state.iBest, state.jBest);
PROFILE2_ATTR("steps: %u", state.steps);
#if PATHFIND_STATS
printf("PATHFINDER: steps=%d avgo=%d proc=%d impc=%d impo=%d addo=%d\n", state.steps, state.sumOpenSize/state.steps, state.numProcessed, state.numImproveClosed, state.numImproveOpen, state.numAddToOpen);
#endif
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2012 Wildfire Games.
/* Copyright (C) 2015 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -101,14 +101,20 @@ struct Vertex
};
// Obstruction edges (paths will not cross any of these).
// When used in the 'edges' list, defines the two points of the edge.
// When used in the 'edgesAA' list, defines the opposing corners of an axis-aligned square
// (from which four individual edges can be trivially computed), requiring p0 <= p1
// Defines the two points of the edge.
struct Edge
{
CFixedVector2D p0, p1;
};
// Axis-aligned obstruction squares (paths will not cross any of these).
// Defines the opposing corners of an axis-aligned square
// (from which four individual edges can be trivially computed), requiring p0 <= p1
struct Square
{
CFixedVector2D p0, p1;
};
// Axis-aligned obstruction edges.
// p0 defines one end; c1 is either the X or Y coordinate of the other end,
// depending on the context in which this is used.
@ -283,209 +289,266 @@ inline static bool CheckVisibilityTop(CFixedVector2D a, CFixedVector2D b, const
return true;
}
typedef PriorityQueueHeap<u16, fixed, fixed> VertexPriorityQueue;
static CFixedVector2D NearestPointOnGoal(CFixedVector2D pos, const CCmpPathfinder::Goal& goal)
{
CFixedVector2D g(goal.x, goal.z);
switch (goal.type)
{
case CCmpPathfinder::Goal::POINT:
{
return g;
}
case CCmpPathfinder::Goal::CIRCLE:
{
CFixedVector2D d = pos - g;
if (d.IsZero())
d = CFixedVector2D(fixed::FromInt(1), fixed::Zero()); // some arbitrary direction
d.Normalize(goal.hw);
return g + d;
}
case CCmpPathfinder::Goal::SQUARE:
{
CFixedVector2D halfSize(goal.hw, goal.hh);
CFixedVector2D d = pos - g;
return g + Geometry::NearestPointOnSquare(d, goal.u, goal.v, halfSize);
}
default:
debug_warn(L"invalid type");
return CFixedVector2D();
}
}
CFixedVector2D CCmpPathfinder::GetNearestPointOnGoal(CFixedVector2D pos, const CCmpPathfinder::Goal& goal)
{
return NearestPointOnGoal(pos, goal);
// (It's intentional that we don't put the implementation inside this
// function, to avoid the (admittedly unmeasured and probably trivial)
// cost of a virtual call inside ComputeShortPath)
}
typedef PriorityQueueHeap<u16, fixed> PriorityQueue;
struct TileEdge
{
u16 i, j;
enum { TOP, BOTTOM, LEFT, RIGHT } dir;
};
static void AddTerrainEdges(std::vector<Edge>& edgesAA, std::vector<Vertex>& vertexes,
u16 i0, u16 j0, u16 i1, u16 j1, fixed r,
ICmpPathfinder::pass_class_t passClass, const Grid<TerrainTile>& terrain)
/**
* Add edges and vertexes to represent the boundaries between passable and impassable
* navcells (for impassable terrain and for static obstruction shapes).
* Navcells i0 <= i <= i1, j0 <= j <= j1 will be considered.
*/
static void AddTerrainEdges(std::vector<Edge>& edges, std::vector<Vertex>& vertexes,
int i0, int j0, int i1, int j1,
pass_class_t passClass, const Grid<NavcellData>& grid)
{
PROFILE("AddTerrainEdges");
std::vector<TileEdge> tileEdges;
// Clamp the coordinates so we won't attempt to sample outside of the grid.
// (This assumes the outermost ring of navcells (which are always impassable)
// won't have a boundary with any passable navcells. TODO: is that definitely
// safe enough?)
// Find all edges between tiles of differently passability statuses
for (u16 j = j0; j <= j1; ++j)
i0 = clamp(i0, 1, grid.m_W-2);
j0 = clamp(j0, 1, grid.m_H-2);
i1 = clamp(i1, 1, grid.m_W-2);
j1 = clamp(j1, 1, grid.m_H-2);
for (int j = j0; j <= j1; ++j)
{
for (u16 i = i0; i <= i1; ++i)
for (int i = i0; i <= i1; ++i)
{
if (!IS_TERRAIN_PASSABLE(terrain.get(i, j), passClass))
if (IS_PASSABLE(grid.get(i, j), passClass))
continue;
if (IS_PASSABLE(grid.get(i+1, j), passClass) && IS_PASSABLE(grid.get(i, j+1), passClass) && IS_PASSABLE(grid.get(i+1, j+1), passClass))
{
bool any = false; // whether we're adding any edges of this tile
Vertex vert;
vert.status = Vertex::UNEXPLORED;
vert.quadOutward = QUADRANT_ALL;
vert.quadInward = QUADRANT_BL;
vert.p = CFixedVector2D(fixed::FromInt(i+1)+EDGE_EXPAND_DELTA, fixed::FromInt(j+1)+EDGE_EXPAND_DELTA).Multiply(Pathfinding::NAVCELL_SIZE);
vertexes.push_back(vert);
}
if (j > 0 && IS_TERRAIN_PASSABLE(terrain.get(i, j-1), passClass))
if (IS_PASSABLE(grid.get(i-1, j), passClass) && IS_PASSABLE(grid.get(i, j+1), passClass) && IS_PASSABLE(grid.get(i-1, j+1), passClass))
{
Vertex vert;
vert.status = Vertex::UNEXPLORED;
vert.quadOutward = QUADRANT_ALL;
vert.quadInward = QUADRANT_BR;
vert.p = CFixedVector2D(fixed::FromInt(i)-EDGE_EXPAND_DELTA, fixed::FromInt(j+1)+EDGE_EXPAND_DELTA).Multiply(Pathfinding::NAVCELL_SIZE);
vertexes.push_back(vert);
}
if (IS_PASSABLE(grid.get(i+1, j), passClass) && IS_PASSABLE(grid.get(i, j-1), passClass) && IS_PASSABLE(grid.get(i+1, j-1), passClass))
{
Vertex vert;
vert.status = Vertex::UNEXPLORED;
vert.quadOutward = QUADRANT_ALL;
vert.quadInward = QUADRANT_TL;
vert.p = CFixedVector2D(fixed::FromInt(i+1)+EDGE_EXPAND_DELTA, fixed::FromInt(j)-EDGE_EXPAND_DELTA).Multiply(Pathfinding::NAVCELL_SIZE);
vertexes.push_back(vert);
}
if (IS_PASSABLE(grid.get(i-1, j), passClass) && IS_PASSABLE(grid.get(i, j-1), passClass) && IS_PASSABLE(grid.get(i-1, j-1), passClass))
{
Vertex vert;
vert.status = Vertex::UNEXPLORED;
vert.quadOutward = QUADRANT_ALL;
vert.quadInward = QUADRANT_TR;
vert.p = CFixedVector2D(fixed::FromInt(i)-EDGE_EXPAND_DELTA, fixed::FromInt(j)-EDGE_EXPAND_DELTA).Multiply(Pathfinding::NAVCELL_SIZE);
vertexes.push_back(vert);
}
}
}
// XXX rewrite this stuff
for (int j = j0; j < j1; ++j)
{
std::vector<u16> segmentsR;
std::vector<u16> segmentsL;
for (int i = i0; i <= i1; ++i)
{
bool a = IS_PASSABLE(grid.get(i, j+1), passClass);
bool b = IS_PASSABLE(grid.get(i, j), passClass);
if (a && !b)
segmentsL.push_back(i);
if (b && !a)
segmentsR.push_back(i);
}
if (!segmentsR.empty())
{
segmentsR.push_back(0); // sentinel value to simplify the loop
u16 ia = segmentsR[0];
u16 ib = ia + 1;
for (size_t n = 1; n < segmentsR.size(); ++n)
{
if (segmentsR[n] == ib)
++ib;
else
{
TileEdge e = { i, j, TileEdge::BOTTOM };
tileEdges.push_back(e);
any = true;
CFixedVector2D v0 = CFixedVector2D(fixed::FromInt(ia), fixed::FromInt(j+1)).Multiply(Pathfinding::NAVCELL_SIZE);
CFixedVector2D v1 = CFixedVector2D(fixed::FromInt(ib), fixed::FromInt(j+1)).Multiply(Pathfinding::NAVCELL_SIZE);
edges.emplace_back(Edge{ v0, v1 });
ia = segmentsR[n];
ib = ia + 1;
}
}
}
if (j < terrain.m_H-1 && IS_TERRAIN_PASSABLE(terrain.get(i, j+1), passClass))
if (!segmentsL.empty())
{
segmentsL.push_back(0); // sentinel value to simplify the loop
u16 ia = segmentsL[0];
u16 ib = ia + 1;
for (size_t n = 1; n < segmentsL.size(); ++n)
{
if (segmentsL[n] == ib)
++ib;
else
{
TileEdge e = { i, j, TileEdge::TOP };
tileEdges.push_back(e);
any = true;
}
CFixedVector2D v0 = CFixedVector2D(fixed::FromInt(ib), fixed::FromInt(j+1)).Multiply(Pathfinding::NAVCELL_SIZE);
CFixedVector2D v1 = CFixedVector2D(fixed::FromInt(ia), fixed::FromInt(j+1)).Multiply(Pathfinding::NAVCELL_SIZE);
edges.emplace_back(Edge{ v0, v1 });
if (i > 0 && IS_TERRAIN_PASSABLE(terrain.get(i-1, j), passClass))
{
TileEdge e = { i, j, TileEdge::LEFT };
tileEdges.push_back(e);
any = true;
}
if (i < terrain.m_W-1 && IS_TERRAIN_PASSABLE(terrain.get(i+1, j), passClass))
{
TileEdge e = { i, j, TileEdge::RIGHT };
tileEdges.push_back(e);
any = true;
}
// If we want to add any edge, then add the whole square to the axis-aligned-edges list.
// (The inner edges are redundant but it's easier than trying to split the squares apart.)
if (any)
{
CFixedVector2D v0 = CFixedVector2D(fixed::FromInt(i * (int)TERRAIN_TILE_SIZE) - r, fixed::FromInt(j * (int)TERRAIN_TILE_SIZE) - r);
CFixedVector2D v1 = CFixedVector2D(fixed::FromInt((i+1) * (int)TERRAIN_TILE_SIZE) + r, fixed::FromInt((j+1) * (int)TERRAIN_TILE_SIZE) + r);
Edge e = { v0, v1 };
edgesAA.push_back(e);
ia = segmentsL[n];
ib = ia + 1;
}
}
}
}
// TODO: maybe we should precompute these terrain edges since they'll rarely change?
// TODO: for efficiency (minimising the A* search space), we should coalesce adjoining edges
// Add all the tile outer edges to the search vertex lists
for (size_t n = 0; n < tileEdges.size(); ++n)
for (int i = i0; i < i1; ++i)
{
u16 i = tileEdges[n].i;
u16 j = tileEdges[n].j;
CFixedVector2D v0, v1;
Vertex vert;
vert.status = Vertex::UNEXPLORED;
vert.quadOutward = QUADRANT_ALL;
std::vector<u16> segmentsU;
std::vector<u16> segmentsD;
switch (tileEdges[n].dir)
for (int j = j0; j <= j1; ++j)
{
case TileEdge::BOTTOM:
{
v0 = CFixedVector2D(fixed::FromInt(i * (int)TERRAIN_TILE_SIZE) - r, fixed::FromInt(j * (int)TERRAIN_TILE_SIZE) - r);
v1 = CFixedVector2D(fixed::FromInt((i+1) * (int)TERRAIN_TILE_SIZE) + r, fixed::FromInt(j * (int)TERRAIN_TILE_SIZE) - r);
vert.p.X = v0.X - EDGE_EXPAND_DELTA; vert.p.Y = v0.Y - EDGE_EXPAND_DELTA; vert.quadInward = QUADRANT_TR; vertexes.push_back(vert);
vert.p.X = v1.X + EDGE_EXPAND_DELTA; vert.p.Y = v1.Y - EDGE_EXPAND_DELTA; vert.quadInward = QUADRANT_TL; vertexes.push_back(vert);
break;
bool a = IS_PASSABLE(grid.get(i+1, j), passClass);
bool b = IS_PASSABLE(grid.get(i, j), passClass);
if (a && !b)
segmentsU.push_back(j);
if (b && !a)
segmentsD.push_back(j);
}
case TileEdge::TOP:
if (!segmentsU.empty())
{
v0 = CFixedVector2D(fixed::FromInt((i+1) * (int)TERRAIN_TILE_SIZE) + r, fixed::FromInt((j+1) * (int)TERRAIN_TILE_SIZE) + r);
v1 = CFixedVector2D(fixed::FromInt(i * (int)TERRAIN_TILE_SIZE) - r, fixed::FromInt((j+1) * (int)TERRAIN_TILE_SIZE) + r);
vert.p.X = v0.X + EDGE_EXPAND_DELTA; vert.p.Y = v0.Y + EDGE_EXPAND_DELTA; vert.quadInward = QUADRANT_BL; vertexes.push_back(vert);
vert.p.X = v1.X - EDGE_EXPAND_DELTA; vert.p.Y = v1.Y + EDGE_EXPAND_DELTA; vert.quadInward = QUADRANT_BR; vertexes.push_back(vert);
break;
segmentsU.push_back(0); // sentinel value to simplify the loop
u16 ja = segmentsU[0];
u16 jb = ja + 1;
for (size_t n = 1; n < segmentsU.size(); ++n)
{
if (segmentsU[n] == jb)
++jb;
else
{
CFixedVector2D v0 = CFixedVector2D(fixed::FromInt(i+1), fixed::FromInt(ja)).Multiply(Pathfinding::NAVCELL_SIZE);
CFixedVector2D v1 = CFixedVector2D(fixed::FromInt(i+1), fixed::FromInt(jb)).Multiply(Pathfinding::NAVCELL_SIZE);
edges.emplace_back(Edge{ v0, v1 });
ja = segmentsU[n];
jb = ja + 1;
}
}
}
case TileEdge::LEFT:
if (!segmentsD.empty())
{
v0 = CFixedVector2D(fixed::FromInt(i * (int)TERRAIN_TILE_SIZE) - r, fixed::FromInt((j+1) * (int)TERRAIN_TILE_SIZE) + r);
v1 = CFixedVector2D(fixed::FromInt(i * (int)TERRAIN_TILE_SIZE) - r, fixed::FromInt(j * (int)TERRAIN_TILE_SIZE) - r);
vert.p.X = v0.X - EDGE_EXPAND_DELTA; vert.p.Y = v0.Y + EDGE_EXPAND_DELTA; vert.quadInward = QUADRANT_BR; vertexes.push_back(vert);
vert.p.X = v1.X - EDGE_EXPAND_DELTA; vert.p.Y = v1.Y - EDGE_EXPAND_DELTA; vert.quadInward = QUADRANT_TR; vertexes.push_back(vert);
break;
}
case TileEdge::RIGHT:
{
v0 = CFixedVector2D(fixed::FromInt((i+1) * (int)TERRAIN_TILE_SIZE) + r, fixed::FromInt(j * (int)TERRAIN_TILE_SIZE) - r);
v1 = CFixedVector2D(fixed::FromInt((i+1) * (int)TERRAIN_TILE_SIZE) + r, fixed::FromInt((j+1) * (int)TERRAIN_TILE_SIZE) + r);
vert.p.X = v0.X + EDGE_EXPAND_DELTA; vert.p.Y = v0.Y - EDGE_EXPAND_DELTA; vert.quadInward = QUADRANT_TL; vertexes.push_back(vert);
vert.p.X = v1.X + EDGE_EXPAND_DELTA; vert.p.Y = v1.Y + EDGE_EXPAND_DELTA; vert.quadInward = QUADRANT_BL; vertexes.push_back(vert);
break;
}
segmentsD.push_back(0); // sentinel value to simplify the loop
u16 ja = segmentsD[0];
u16 jb = ja + 1;
for (size_t n = 1; n < segmentsD.size(); ++n)
{
if (segmentsD[n] == jb)
++jb;
else
{
CFixedVector2D v0 = CFixedVector2D(fixed::FromInt(i+1), fixed::FromInt(jb)).Multiply(Pathfinding::NAVCELL_SIZE);
CFixedVector2D v1 = CFixedVector2D(fixed::FromInt(i+1), fixed::FromInt(ja)).Multiply(Pathfinding::NAVCELL_SIZE);
edges.emplace_back(Edge{ v0, v1 });
ja = segmentsD[n];
jb = ja + 1;
}
}
}
}
}
static void SplitAAEdges(CFixedVector2D a,
const std::vector<Edge>& edgesAA,
const std::vector<Edge>& edges,
const std::vector<Square>& squares,
std::vector<Edge>& edgesUnaligned,
std::vector<EdgeAA>& edgesLeft, std::vector<EdgeAA>& edgesRight,
std::vector<EdgeAA>& edgesBottom, std::vector<EdgeAA>& edgesTop)
{
edgesLeft.reserve(edgesAA.size());
edgesRight.reserve(edgesAA.size());
edgesBottom.reserve(edgesAA.size());
edgesTop.reserve(edgesAA.size());
edgesLeft.reserve(squares.size());
edgesRight.reserve(squares.size());
edgesBottom.reserve(squares.size());
edgesTop.reserve(squares.size());
for (size_t i = 0; i < edgesAA.size(); ++i)
for (const Square& square : squares)
{
if (a.X <= edgesAA[i].p0.X)
if (a.X <= square.p0.X)
edgesLeft.emplace_back(EdgeAA{ square.p0, square.p1.Y });
if (a.X >= square.p1.X)
edgesRight.emplace_back(EdgeAA{ square.p1, square.p0.Y });
if (a.Y <= square.p0.Y)
edgesBottom.emplace_back(EdgeAA{ square.p0, square.p1.X });
if (a.Y >= square.p1.Y)
edgesTop.emplace_back(EdgeAA{ square.p1, square.p0.X });
}
for (const Edge& edge : edges)
{
if (edge.p0.X == edge.p1.X)
{
EdgeAA e = { edgesAA[i].p0, edgesAA[i].p1.Y };
edgesLeft.push_back(e);
if (edge.p1.Y < edge.p0.Y)
{
if (!(a.X <= edge.p0.X))
continue;
edgesLeft.emplace_back(EdgeAA{ edge.p1, edge.p0.Y });
}
else
{
if (!(a.X >= edge.p0.X))
continue;
edgesRight.emplace_back(EdgeAA{ edge.p1, edge.p0.Y });
}
}
if (a.X >= edgesAA[i].p1.X)
else if (edge.p0.Y == edge.p1.Y)
{
EdgeAA e = { edgesAA[i].p1, edgesAA[i].p0.Y };
edgesRight.push_back(e);
}
if (a.Y <= edgesAA[i].p0.Y)
{
EdgeAA e = { edgesAA[i].p0, edgesAA[i].p1.X };
edgesBottom.push_back(e);
}
if (a.Y >= edgesAA[i].p1.Y)
{
EdgeAA e = { edgesAA[i].p1, edgesAA[i].p0.X };
edgesTop.push_back(e);
if (edge.p0.X < edge.p1.X)
{
if (!(a.Y <= edge.p0.Y))
continue;
edgesBottom.emplace_back(EdgeAA{ edge.p0, edge.p1.X });
}
else
{
if (!(a.Y >= edge.p0.Y))
continue;
edgesTop.emplace_back(EdgeAA{ edge.p0, edge.p1.X });
}
}
else
edgesUnaligned.push_back(edge);
}
}
/**
* Functor for sorting edges by approximate proximity to a fixed point.
* Functor for sorting edge-squares by approximate proximity to a fixed point.
*/
struct EdgeSort
struct SquareSort
{
CFixedVector2D src;
EdgeSort(CFixedVector2D src) : src(src) { }
bool operator()(const Edge& a, const Edge& b)
SquareSort(CFixedVector2D src) : src(src) { }
bool operator()(const Square& a, const Square& b)
{
if ((a.p0 - src).CompareLength(b.p0 - src) < 0)
return true;
@ -495,12 +558,9 @@ struct EdgeSort
void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
entity_pos_t x0, entity_pos_t z0, entity_pos_t r,
entity_pos_t range, const Goal& goal, pass_class_t passClass, Path& path)
entity_pos_t range, const PathGoal& goal, pass_class_t passClass, WaypointPath& path)
{
UpdateGrid(); // TODO: only need to bother updating if the terrain changed
PROFILE3("ComputeShortPath");
// ScopeTimer UID__(L"ComputeShortPath");
m_DebugOverlayShortPathLines.clear();
@ -511,17 +571,19 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
m_DebugOverlayShortPathLines.back().m_Color = CColor(1, 0, 0, 1);
switch (goal.type)
{
case CCmpPathfinder::Goal::POINT:
case PathGoal::POINT:
{
SimRender::ConstructCircleOnGround(GetSimContext(), goal.x.ToFloat(), goal.z.ToFloat(), 0.2f, m_DebugOverlayShortPathLines.back(), true);
break;
}
case CCmpPathfinder::Goal::CIRCLE:
case PathGoal::CIRCLE:
case PathGoal::INVERTED_CIRCLE:
{
SimRender::ConstructCircleOnGround(GetSimContext(), goal.x.ToFloat(), goal.z.ToFloat(), goal.hw.ToFloat(), m_DebugOverlayShortPathLines.back(), true);
break;
}
case CCmpPathfinder::Goal::SQUARE:
case PathGoal::SQUARE:
case PathGoal::INVERTED_SQUARE:
{
float a = atan2f(goal.v.X.ToFloat(), goal.v.Y.ToFloat());
SimRender::ConstructSquareOnGround(GetSimContext(), goal.x.ToFloat(), goal.z.ToFloat(), goal.hw.ToFloat()*2, goal.hh.ToFloat()*2, a, m_DebugOverlayShortPathLines.back(), true);
@ -533,7 +595,7 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
// List of collision edges - paths must never cross these.
// (Edges are one-sided so intersections are fine in one direction, but not the other direction.)
std::vector<Edge> edges;
std::vector<Edge> edgesAA; // axis-aligned squares
std::vector<Square> edgeSquares; // axis-aligned squares; equivalent to 4 edges
// Create impassable edges at the max-range boundary, so we can't escape the region
// where we're meant to be searching
@ -541,17 +603,13 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
fixed rangeXMax = x0 + range;
fixed rangeZMin = z0 - range;
fixed rangeZMax = z0 + range;
{
// (The edges are the opposite direction to usual, so it's an inside-out square)
Edge e0 = { CFixedVector2D(rangeXMin, rangeZMin), CFixedVector2D(rangeXMin, rangeZMax) };
Edge e1 = { CFixedVector2D(rangeXMin, rangeZMax), CFixedVector2D(rangeXMax, rangeZMax) };
Edge e2 = { CFixedVector2D(rangeXMax, rangeZMax), CFixedVector2D(rangeXMax, rangeZMin) };
Edge e3 = { CFixedVector2D(rangeXMax, rangeZMin), CFixedVector2D(rangeXMin, rangeZMin) };
edges.push_back(e0);
edges.push_back(e1);
edges.push_back(e2);
edges.push_back(e3);
}
// (The edges are the opposite direction to usual, so it's an inside-out square)
edges.emplace_back(Edge{ CFixedVector2D(rangeXMin, rangeZMin), CFixedVector2D(rangeXMin, rangeZMax) });
edges.emplace_back(Edge{ CFixedVector2D(rangeXMin, rangeZMax), CFixedVector2D(rangeXMax, rangeZMax) });
edges.emplace_back(Edge{ CFixedVector2D(rangeXMax, rangeZMax), CFixedVector2D(rangeXMax, rangeZMin) });
edges.emplace_back(Edge{ CFixedVector2D(rangeXMax, rangeZMin), CFixedVector2D(rangeXMin, rangeZMin) });
// List of obstruction vertexes (plus start/end points); we'll try to find paths through
// the graph defined by these vertexes
@ -559,7 +617,7 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
// Add the start point to the graph
CFixedVector2D posStart(x0, z0);
fixed hStart = (posStart - NearestPointOnGoal(posStart, goal)).Length();
fixed hStart = (posStart - goal.NearestPointOnGoal(posStart)).Length();
Vertex start = { posStart, fixed::Zero(), hStart, 0, Vertex::OPEN, QUADRANT_NONE, QUADRANT_ALL };
vertexes.push_back(start);
const size_t START_VERTEX_ID = 0;
@ -574,9 +632,9 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
// Add terrain obstructions
{
u16 i0, j0, i1, j1;
NearestTile(rangeXMin, rangeZMin, i0, j0);
NearestTile(rangeXMax, rangeZMax, i1, j1);
AddTerrainEdges(edgesAA, vertexes, i0, j0, i1, j1, r, passClass, *m_Grid);
Pathfinding::NearestNavcell(rangeXMin, rangeZMin, i0, j0, m_MapSize*Pathfinding::NAVCELLS_PER_TILE, m_MapSize*Pathfinding::NAVCELLS_PER_TILE);
Pathfinding::NearestNavcell(rangeXMax, rangeZMax, i1, j1, m_MapSize*Pathfinding::NAVCELLS_PER_TILE, m_MapSize*Pathfinding::NAVCELLS_PER_TILE);
AddTerrainEdges(edges, vertexes, i0, j0, i1, j1, passClass, *m_Grid);
}
// Find all the obstruction squares that might affect us
@ -586,7 +644,7 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
// Resize arrays to reduce reallocations
vertexes.reserve(vertexes.size() + squares.size()*4);
edgesAA.reserve(edgesAA.size() + squares.size()); // (assume most squares are AA)
edgeSquares.reserve(edgeSquares.size() + squares.size()); // (assume most squares are AA)
// Convert each obstruction square into collision edges and search graph vertexes
for (size_t i = 0; i < squares.size(); ++i)
@ -615,7 +673,7 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
// Add the edges:
CFixedVector2D h0(squares[i].hw + r, squares[i].hh + r);
CFixedVector2D h0(squares[i].hw + r, squares[i].hh + r);
CFixedVector2D h1(squares[i].hw + r, -(squares[i].hh + r));
CFixedVector2D ev0(center.X - h0.Dot(u), center.Y + h0.Dot(v));
@ -623,20 +681,13 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
CFixedVector2D ev2(center.X + h0.Dot(u), center.Y - h0.Dot(v));
CFixedVector2D ev3(center.X + h1.Dot(u), center.Y - h1.Dot(v));
if (aa)
{
Edge e = { ev1, ev3 };
edgesAA.push_back(e);
}
edgeSquares.emplace_back(Square{ ev1, ev3 });
else
{
Edge e0 = { ev0, ev1 };
Edge e1 = { ev1, ev2 };
Edge e2 = { ev2, ev3 };
Edge e3 = { ev3, ev0 };
edges.push_back(e0);
edges.push_back(e1);
edges.push_back(e2);
edges.push_back(e3);
edges.emplace_back(Edge{ ev0, ev1 });
edges.emplace_back(Edge{ ev1, ev2 });
edges.emplace_back(Edge{ ev2, ev3 });
edges.emplace_back(Edge{ ev3, ev0 });
}
// TODO: should clip out vertexes and edges that are outside the range,
@ -645,36 +696,75 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
ENSURE(vertexes.size() < 65536); // we store array indexes as u16
// Render the debug overlay
if (m_DebugOverlay)
{
// Render the obstruction edges
for (size_t i = 0; i < edges.size(); ++i)
#define PUSH_POINT(p) STMT(xz.push_back(p.X.ToFloat()); xz.push_back(p.Y.ToFloat()))
// Render the vertexes as little Pac-Man shapes to indicate quadrant direction
for (size_t i = 0; i < vertexes.size(); ++i)
{
m_DebugOverlayShortPathLines.push_back(SOverlayLine());
m_DebugOverlayShortPathLines.back().m_Color = CColor(0, 1, 1, 1);
std::vector<float> xz;
xz.push_back(edges[i].p0.X.ToFloat());
xz.push_back(edges[i].p0.Y.ToFloat());
xz.push_back(edges[i].p1.X.ToFloat());
xz.push_back(edges[i].p1.Y.ToFloat());
SimRender::ConstructLineOnGround(GetSimContext(), xz, m_DebugOverlayShortPathLines.back(), true);
m_DebugOverlayShortPathLines.emplace_back();
m_DebugOverlayShortPathLines.back().m_Color = CColor(1, 1, 0, 1);
float x = vertexes[i].p.X.ToFloat();
float z = vertexes[i].p.Y.ToFloat();
float a0 = 0, a1 = 0;
// Get arc start/end angles depending on quadrant (if any)
if (vertexes[i].quadInward == QUADRANT_BL) { a0 = -0.25f; a1 = 0.50f; }
else if (vertexes[i].quadInward == QUADRANT_TR) { a0 = 0.25f; a1 = 1.00f; }
else if (vertexes[i].quadInward == QUADRANT_TL) { a0 = -0.50f; a1 = 0.25f; }
else if (vertexes[i].quadInward == QUADRANT_BR) { a0 = 0.00f; a1 = 0.75f; }
if (a0 == a1)
SimRender::ConstructCircleOnGround(GetSimContext(), x, z, 0.5f,
m_DebugOverlayShortPathLines.back(), true);
else
SimRender::ConstructClosedArcOnGround(GetSimContext(), x, z, 0.5f,
a0 * ((float)M_PI*2.0f), a1 * ((float)M_PI*2.0f),
m_DebugOverlayShortPathLines.back(), true);
}
for (size_t i = 0; i < edgesAA.size(); ++i)
// Render the edges
for (size_t i = 0; i < edges.size(); ++i)
{
m_DebugOverlayShortPathLines.emplace_back();
m_DebugOverlayShortPathLines.back().m_Color = CColor(0, 1, 1, 1);
std::vector<float> xz;
PUSH_POINT(edges[i].p0);
PUSH_POINT(edges[i].p1);
// Add an arrowhead to indicate the direction
CFixedVector2D d = edges[i].p1 - edges[i].p0;
d.Normalize(fixed::FromInt(1)/8);
CFixedVector2D p2 = edges[i].p1 - d*2;
CFixedVector2D p3 = p2 + d.Perpendicular();
CFixedVector2D p4 = p2 - d.Perpendicular();
PUSH_POINT(p3);
PUSH_POINT(p4);
PUSH_POINT(edges[i].p1);
SimRender::ConstructLineOnGround(GetSimContext(), xz, m_DebugOverlayShortPathLines.back(), true);
}
#undef PUSH_POINT
// Render the axis-aligned squares
for (size_t i = 0; i < edgeSquares.size(); ++i)
{
m_DebugOverlayShortPathLines.push_back(SOverlayLine());
m_DebugOverlayShortPathLines.back().m_Color = CColor(0, 1, 1, 1);
std::vector<float> xz;
xz.push_back(edgesAA[i].p0.X.ToFloat());
xz.push_back(edgesAA[i].p0.Y.ToFloat());
xz.push_back(edgesAA[i].p0.X.ToFloat());
xz.push_back(edgesAA[i].p1.Y.ToFloat());
xz.push_back(edgesAA[i].p1.X.ToFloat());
xz.push_back(edgesAA[i].p1.Y.ToFloat());
xz.push_back(edgesAA[i].p1.X.ToFloat());
xz.push_back(edgesAA[i].p0.Y.ToFloat());
xz.push_back(edgesAA[i].p0.X.ToFloat());
xz.push_back(edgesAA[i].p0.Y.ToFloat());
Square s = edgeSquares[i];
xz.push_back(s.p0.X.ToFloat());
xz.push_back(s.p0.Y.ToFloat());
xz.push_back(s.p0.X.ToFloat());
xz.push_back(s.p1.Y.ToFloat());
xz.push_back(s.p1.X.ToFloat());
xz.push_back(s.p1.Y.ToFloat());
xz.push_back(s.p1.X.ToFloat());
xz.push_back(s.p0.Y.ToFloat());
xz.push_back(s.p0.X.ToFloat());
xz.push_back(s.p0.Y.ToFloat());
SimRender::ConstructLineOnGround(GetSimContext(), xz, m_DebugOverlayShortPathLines.back(), true);
}
}
@ -690,10 +780,10 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
// we can't reach. Since the algorithm can only reach a vertex once (and then it'll be marked
// as closed), we won't be doing any redundant visibility computations.
PROFILE_START("A*");
PROFILE_START("Short pathfinding - A*");
PriorityQueue open;
PriorityQueue::Item qiStart = { START_VERTEX_ID, start.h };
VertexPriorityQueue open;
VertexPriorityQueue::Item qiStart = { START_VERTEX_ID, start.h, start.h };
open.push(qiStart);
u16 idBest = START_VERTEX_ID;
@ -702,7 +792,7 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
while (!open.empty())
{
// Move best tile from open to closed
PriorityQueue::Item curr = open.pop();
VertexPriorityQueue::Item curr = open.pop();
vertexes[curr.id].status = Vertex::CLOSED;
// If we've reached the destination, stop
@ -714,13 +804,14 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
// Sort the edges so ones nearer this vertex are checked first by CheckVisibility,
// since they're more likely to block the rays
std::sort(edgesAA.begin(), edgesAA.end(), EdgeSort(vertexes[curr.id].p));
std::sort(edgeSquares.begin(), edgeSquares.end(), SquareSort(vertexes[curr.id].p));
std::vector<Edge> edgesUnaligned;
std::vector<EdgeAA> edgesLeft;
std::vector<EdgeAA> edgesRight;
std::vector<EdgeAA> edgesBottom;
std::vector<EdgeAA> edgesTop;
SplitAAEdges(vertexes[curr.id].p, edgesAA, edgesLeft, edgesRight, edgesBottom, edgesTop);
SplitAAEdges(vertexes[curr.id].p, edges, edgeSquares, edgesUnaligned, edgesLeft, edgesRight, edgesBottom, edgesTop);
// Check the lines to every other vertex
for (size_t n = 0; n < vertexes.size(); ++n)
@ -732,7 +823,7 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
CFixedVector2D npos;
if (n == GOAL_VERTEX_ID)
{
npos = NearestPointOnGoal(vertexes[curr.id].p, goal);
npos = goal.NearestPointOnGoal(vertexes[curr.id].p);
// To prevent integer overflows later on, we need to ensure all vertexes are
// 'close' to the source. The goal might be far away (not a good idea but
@ -768,7 +859,7 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
CheckVisibilityRight(vertexes[curr.id].p, npos, edgesRight) &&
CheckVisibilityBottom(vertexes[curr.id].p, npos, edgesBottom) &&
CheckVisibilityTop(vertexes[curr.id].p, npos, edgesTop) &&
CheckVisibility(vertexes[curr.id].p, npos, edges);
CheckVisibility(vertexes[curr.id].p, npos, edgesUnaligned);
/*
// Render the edges that we examine
@ -792,7 +883,7 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
// Add it to the open list:
vertexes[n].status = Vertex::OPEN;
vertexes[n].g = g;
vertexes[n].h = DistanceToGoal(npos, goal);
vertexes[n].h = goal.DistanceToPoint(npos);
vertexes[n].pred = curr.id;
// If this is an axis-aligned shape, the path must continue in the same quadrant
@ -805,7 +896,7 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
if (n == GOAL_VERTEX_ID)
vertexes[n].p = npos; // remember the new best goal position
PriorityQueue::Item t = { (u16)n, g + vertexes[n].h };
VertexPriorityQueue::Item t = { (u16)n, g + vertexes[n].h, vertexes[n].h };
open.push(t);
// Remember the heuristically best vertex we've seen so far, in case we never actually reach the target
@ -823,6 +914,7 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
continue;
// Otherwise, we have a better path, so replace the old one with the new cost/parent
fixed gprev = vertexes[n].g;
vertexes[n].g = g;
vertexes[n].pred = curr.id;
@ -834,7 +926,7 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
if (n == GOAL_VERTEX_ID)
vertexes[n].p = npos; // remember the new best goal position
open.promote((u16)n, g + vertexes[n].h);
open.promote((u16)n, gprev + vertexes[n].h, g + vertexes[n].h, vertexes[n].h);
}
}
}
@ -842,18 +934,17 @@ void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter,
// Reconstruct the path (in reverse)
for (u16 id = idBest; id != START_VERTEX_ID; id = vertexes[id].pred)
{
Waypoint w = { vertexes[id].p.X, vertexes[id].p.Y };
path.m_Waypoints.push_back(w);
}
path.m_Waypoints.emplace_back(Waypoint{ vertexes[id].p.X, vertexes[id].p.Y });
PROFILE_END("A*");
PROFILE_END("Short pathfinding - A*");
}
bool CCmpPathfinder::CheckMovement(const IObstructionTestFilter& filter,
entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r,
pass_class_t passClass)
{
// Test against dynamic obstructions first
CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSystemEntity());
if (!cmpObstructionManager)
return false;
@ -861,34 +952,7 @@ bool CCmpPathfinder::CheckMovement(const IObstructionTestFilter& filter,
if (cmpObstructionManager->TestLine(filter, x0, z0, x1, z1, r))
return false;
// Test against terrain:
// (TODO: this could probably be a tiny bit faster by not reusing all the vertex computation code)
UpdateGrid();
std::vector<Edge> edgesAA;
std::vector<Vertex> vertexes;
u16 i0, j0, i1, j1;
NearestTile(std::min(x0, x1) - r, std::min(z0, z1) - r, i0, j0);
NearestTile(std::max(x0, x1) + r, std::max(z0, z1) + r, i1, j1);
AddTerrainEdges(edgesAA, vertexes, i0, j0, i1, j1, r, passClass, *m_Grid);
CFixedVector2D a(x0, z0);
CFixedVector2D b(x1, z1);
std::vector<EdgeAA> edgesLeft;
std::vector<EdgeAA> edgesRight;
std::vector<EdgeAA> edgesBottom;
std::vector<EdgeAA> edgesTop;
SplitAAEdges(a, edgesAA, edgesLeft, edgesRight, edgesBottom, edgesTop);
bool visible =
CheckVisibilityLeft(a, b, edgesLeft) &&
CheckVisibilityRight(a, b, edgesRight) &&
CheckVisibilityBottom(a, b, edgesBottom) &&
CheckVisibilityTop(a, b, edgesTop);
return visible;
// Test against the passability grid.
// This ignores r.
return m_LongPathfinder.CheckLineMovement(x0, z0, x1, z1, passClass);
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2014 Wildfire Games.
/* Copyright (C) 2015 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -68,9 +68,8 @@ struct SVisibilitySegment
class CCmpRallyPointRenderer : public ICmpRallyPointRenderer
{
// import some types for less verbosity
typedef ICmpPathfinder::Path Path;
typedef ICmpPathfinder::Goal Goal;
typedef ICmpPathfinder::Waypoint Waypoint;
typedef WaypointPath Path;
typedef PathGoal Goal;
typedef ICmpRangeManager::CLosQuerier CLosQuerier;
typedef SOverlayTexturedLine::LineCapType LineCapType;
@ -114,7 +113,6 @@ protected:
std::wstring m_LineTexturePath;
std::wstring m_LineTextureMaskPath;
std::string m_LinePassabilityClass; ///< Pathfinder passability class to use for computing the (long-range) marker line path.
std::string m_LineCostClass; ///< Pathfinder cost class to use for computing the (long-range) marker line path.
CTexturePtr m_Texture;
CTexturePtr m_TextureMask;
@ -140,7 +138,6 @@ public:
"<LineEndCap>square</LineEndCap>"
"<LineColor r='20' g='128' b='240'></LineColor>"
"<LineDashColor r='158' g='11' b='15'></LineDashColor>"
"<LineCostClass>default</LineCostClass>"
"<LinePassabilityClass>default</LinePassabilityClass>"
"</a:example>"
"<element name='MarkerTemplate' a:help='Template name for the rally point marker entity (typically a waypoint flag actor)'>"
@ -195,9 +192,6 @@ public:
"</element>"
"<element name='LinePassabilityClass' a:help='The pathfinder passability class to use for computing the rally point marker line path'>"
"<text />"
"</element>"
"<element name='LineCostClass' a:help='The pathfinder cost class to use for computing the rally point marker line path'>"
"<text />"
"</element>";
}
@ -412,6 +406,11 @@ private:
*/
void FixFootprintWaypoints(std::vector<CVector2D>& coords, CmpPtr<ICmpPosition> cmpPosition, CmpPtr<ICmpFootprint> cmpFootprint);
/**
* Get the point on the footprint edge that's as close from "start" as possible.
*/
void GetClosestsEdgePointFrom(CFixedVector2D& result, CFixedVector2D& start, CmpPtr<ICmpPosition> cmpPosition, CmpPtr<ICmpFootprint> cmpFootprint);
/**
* Returns a list of indices of waypoints in the current path (m_Path[index]) where the LOS visibility changes, ordered from
* building/previous rally point to rally point. Used to construct the overlay line segments and track changes to the SoD.
@ -477,7 +476,6 @@ void CCmpRallyPointRenderer::Init(const CParamNode& paramNode)
m_LineTextureMaskPath = paramNode.GetChild("LineTextureMask").ToString();
m_LineStartCapType = SOverlayTexturedLine::StrToLineCapType(paramNode.GetChild("LineStartCap").ToString());
m_LineEndCapType = SOverlayTexturedLine::StrToLineCapType(paramNode.GetChild("LineEndCap").ToString());
m_LineCostClass = paramNode.GetChild("LineCostClass").ToUTF8();
m_LinePassabilityClass = paramNode.GetChild("LinePassabilityClass").ToUTF8();
// ---------------------------------------------------------------------------------------------
@ -621,66 +619,61 @@ void CCmpRallyPointRenderer::RecomputeRallyPointPath(size_t index, CmpPtr<ICmpPo
}
m_VisibilitySegments[index].clear();
entity_pos_t pathStartX;
entity_pos_t pathStartY;
if (index == 0)
{
pathStartX = cmpPosition->GetPosition2D().X;
pathStartY = cmpPosition->GetPosition2D().Y;
}
else
{
pathStartX = m_RallyPoints[index-1].X;
pathStartY = m_RallyPoints[index-1].Y;
}
// Find a long path to the goal point -- this uses the tile-based pathfinder, which will return a
// list of waypoints (i.e. a Path) from the building/previous rally point to the goal, where each
// list of waypoints (i.e. a Path) from the goal to the foundation/previous rally point, where each
// waypoint is centered at a tile. We'll have to do some post-processing on the path to get it smooth.
Path path;
std::vector<Waypoint>& waypoints = path.m_Waypoints;
CFixedVector2D start(cmpPosition->GetPosition2D());
Goal goal = { Goal::POINT, m_RallyPoints[index].X, m_RallyPoints[index].Y };
cmpPathfinder->ComputePath(
pathStartX,
pathStartY,
goal,
cmpPathfinder->GetPassabilityClass(m_LinePassabilityClass),
cmpPathfinder->GetCostClass(m_LineCostClass),
path
);
if (index == 0)
GetClosestsEdgePointFrom(start,m_RallyPoints[index], cmpPosition, cmpFootprint);
else
{
start.X = m_RallyPoints[index-1].X;
start.Y = m_RallyPoints[index-1].Y;
}
cmpPathfinder->ComputePath(start.X, start.Y, goal, cmpPathfinder->GetPassabilityClass(m_LinePassabilityClass), path);
// Check if we got a path back; if not we probably have two markers less than one tile apart.
if (path.m_Waypoints.size() < 2)
{
m_Path[index].push_back(CVector2D(goal.x.ToFloat(), goal.z.ToFloat()));
m_Path[index].push_back(CVector2D(pathStartX.ToFloat(), pathStartY.ToFloat()));
m_Path[index].emplace_back(start.X.ToFloat(), start.Y.ToFloat());
m_Path[index].emplace_back(m_RallyPoints[index].X.ToFloat(), m_RallyPoints[index].Y.ToFloat());
return;
}
else if (index == 0)
{
// sometimes this ends up not being optimal if you asked for a long path, so improve.
CFixedVector2D newend(waypoints[waypoints.size()-2].x,waypoints[waypoints.size()-2].z);
GetClosestsEdgePointFrom(newend,newend, cmpPosition, cmpFootprint);
waypoints.back().x = newend.X;
waypoints.back().z = newend.Y;
}
else
{
// make sure we actually start at the rallypoint because the pathfinder moves us to a usable tile.
waypoints.back().x = m_RallyPoints[index-1].X;
waypoints.back().z = m_RallyPoints[index-1].Y;
}
// pathfinder makes us go to the nearest passable cell which isn't always what we want
waypoints[0].x = m_RallyPoints[index].X;
waypoints[0].z = m_RallyPoints[index].Y;
// From here on, we choose to represent the waypoints as CVector2D floats to avoid to have to convert back and forth
// between fixed-point Waypoint/CFixedVector2D and various other float-based formats used by interpolation and whatnot.
// Since we'll only be further using these points for rendering purposes, using floats should be fine.
// Make sure to add the actual goal point as the last point (the long pathfinder only finds paths to the tile closest to the
// goal, so we need to complete the last bit from the closest tile to the rally point itself)
// NOTE: the points are returned in reverse order (from the goal to the start point), so we actually need to insert it at the
// front of the coordinate list. Hence, we'll do this first before appending the rest of the fixed waypoints as CVector2Ds.
Waypoint& lastWaypoint = waypoints.back();
if (lastWaypoint.x != goal.x || lastWaypoint.z != goal.z)
m_Path[index].push_back(CVector2D(goal.x.ToFloat(), goal.z.ToFloat()));
// add the rest of the waypoints
for (size_t i = 0; i < waypoints.size(); ++i)
m_Path[index].push_back(CVector2D(waypoints[i].x.ToFloat(), waypoints[i].z.ToFloat()));
for (Waypoint& waypoint : waypoints)
m_Path[index].emplace_back(waypoint.x.ToFloat(), waypoint.z.ToFloat());
// add the start position
m_Path[index].push_back(CVector2D(pathStartX.ToFloat(), pathStartY.ToFloat()));
// m_Path[index].emplace_back(m_RallyPoints[index].X.ToFloat(), m_RallyPoints[index].Y.ToFloat());
// -------------------------------------------------------------------------------------------
// post-processing
// Post-processing
// Linearize the path;
// Pass through the waypoints, averaging each waypoint with its next one except the last one. Because the path
@ -691,45 +684,40 @@ void CCmpRallyPointRenderer::RecomputeRallyPointPath(size_t index, CmpPtr<ICmpPo
m_Path[index][i] = (m_Path[index][i] + m_Path[index][i-1]) / 2.0f;
// if there's a footprint and this path starts from this entity, remove any points returned by the pathfinder that may be on obstructed footprint tiles
if (index == 0 && cmpFootprint)
FixFootprintWaypoints(m_Path[index], cmpPosition, cmpFootprint);
//if (index == 0 && cmpFootprint)
// FixFootprintWaypoints(m_Path[index], cmpPosition, cmpFootprint);
// Eliminate some consecutive waypoints that are visible from eachother. Reduce across a maximum distance of approx. 6 tiles
// (prevents segments that are too long to properly stick to the terrain)
ReduceSegmentsByVisibility(m_Path[index], 6);
//// <DEBUG> ///////////////////////////////////////////////
// Debug overlays
if (m_EnableDebugNodeOverlay)
{
while (index >= m_DebugNodeOverlays.size())
{
std::vector<SOverlayLine> tmp;
m_DebugNodeOverlays.push_back(tmp);
}
m_DebugNodeOverlays.emplace_back();
m_DebugNodeOverlays[index].clear();
}
if (m_EnableDebugNodeOverlay && m_SmoothPath)
{
// Create separate control point overlays so we can differentiate when using smoothing (offset them a little higher from the
// terrain so we can still see them after the interpolated points are added)
for (size_t j = 0; j < m_Path[index].size(); ++j)
for (CVector2D& point : m_Path[index])
{
SOverlayLine overlayLine;
overlayLine.m_Color = CColor(1.0f, 0.0f, 0.0f, 1.0f);
overlayLine.m_Thickness = 2;
SimRender::ConstructSquareOnGround(GetSimContext(), m_Path[index][j].X, m_Path[index][j].Y, 0.2f, 0.2f, 1.0f, overlayLine, true);
SimRender::ConstructSquareOnGround(GetSimContext(), point.X, point.Y, 0.2f, 0.2f, 1.0f, overlayLine, true);
m_DebugNodeOverlays[index].push_back(overlayLine);
}
}
//// </DEBUG> //////////////////////////////////////////////
if (m_SmoothPath)
// The number of points to interpolate goes hand in hand with the maximum amount of node links allowed to be joined together
// by the visibility reduction. The more node links that can be joined together, the more interpolated points you need to
// generate to be able to deal with local terrain height changes.
SimRender::InterpolatePointsRNS(m_Path[index], false, 0, 8); // no offset, keep line at its exact path
// -------------------------------------------------------------------------------------------
SimRender::InterpolatePointsRNS(m_Path[index], false, 0, 4); // no offset, keep line at its exact path
// find which point is the last visible point before going into the SoD, so we have a point to compare to on the next turn
GetVisibilitySegments(m_VisibilitySegments[index], index);
@ -932,6 +920,53 @@ void CCmpRallyPointRenderer::UpdateOverlayLines()
}
}
void CCmpRallyPointRenderer::GetClosestsEdgePointFrom(CFixedVector2D& result, CFixedVector2D& start, CmpPtr<ICmpPosition> cmpPosition, CmpPtr<ICmpFootprint> cmpFootprint)
{
ENSURE(cmpPosition);
ENSURE(cmpFootprint);
// grab the shape and dimensions of the footprint
entity_pos_t footprintSize0, footprintSize1, footprintHeight;
ICmpFootprint::EShape footprintShape;
cmpFootprint->GetShape(footprintShape, footprintSize0, footprintSize1, footprintHeight);
// grab the center of the footprint
CFixedVector2D center = cmpPosition->GetPosition2D();
switch (footprintShape)
{
case ICmpFootprint::SQUARE:
{
// in this case, footprintSize0 and 1 indicate the size along the X and Z axes, respectively.
// the building's footprint could be rotated any which way, so let's get the rotation around the Y axis
// and the rotated unit vectors in the X/Z plane of the shape's footprint
// (the Footprint itself holds only the outline, the Position holds the orientation)
fixed s, c; // sine and cosine of the Y axis rotation angle (aka the yaw)
fixed a = cmpPosition->GetRotation().Y;
sincos_approx(a, s, c);
CFixedVector2D u(c, -s); // unit vector along the rotated X axis
CFixedVector2D v(s, c); // unit vector along the rotated Z axis
CFixedVector2D halfSize(footprintSize0/2, footprintSize1/2);
CFixedVector2D footprintEdgePoint = Geometry::NearestPointOnSquare(start - center, u, v, halfSize);
result = center + footprintEdgePoint;
break;
}
case ICmpFootprint::CIRCLE:
{
// in this case, both footprintSize0 and 1 indicate the circle's radius
// Transform target to the point nearest on the edge.
CFixedVector2D centerVec2D(center.X, center.Y);
CFixedVector2D centerToLast(start - centerVec2D);
centerToLast.Normalize();
result = centerVec2D + (centerToLast.Multiply(footprintSize0));
break;
}
}
}
void CCmpRallyPointRenderer::FixFootprintWaypoints(std::vector<CVector2D>& coords, CmpPtr<ICmpPosition> cmpPosition, CmpPtr<ICmpFootprint> cmpFootprint)
{
ENSURE(cmpPosition);
@ -1034,7 +1069,7 @@ void CCmpRallyPointRenderer::ReduceSegmentsByVisibility(std::vector<CVector2D>&
std::vector<CVector2D> newCoords;
StationaryOnlyObstructionFilter obstructionFilter;
entity_pos_t lineRadius = fixed::FromFloat(m_LineThickness);
ICmpPathfinder::pass_class_t passabilityClass = cmpPathFinder->GetPassabilityClass(m_LinePassabilityClass);
pass_class_t passabilityClass = cmpPathFinder->GetPassabilityClass(m_LinePassabilityClass);
newCoords.push_back(coords[0]); // save the first base node

View File

@ -27,6 +27,7 @@
#include "simulation2/components/ICmpMirage.h"
#include "simulation2/components/ICmpOwnership.h"
#include "simulation2/components/ICmpPosition.h"
#include "simulation2/components/ICmpObstructionManager.h"
#include "simulation2/components/ICmpTerritoryManager.h"
#include "simulation2/components/ICmpVisibility.h"
#include "simulation2/components/ICmpVision.h"
@ -1734,13 +1735,25 @@ public:
virtual void ExploreTerritories()
{
PROFILE3("ExploreTerritories");
CmpPtr<ICmpTerritoryManager> cmpTerritoryManager(GetSystemEntity());
const Grid<u8>& grid = cmpTerritoryManager->GetTerritoryGrid();
ENSURE(grid.m_W == m_TerrainVerticesPerSide-1 && grid.m_H == m_TerrainVerticesPerSide-1);
// For each tile, if it is owned by a valid player then update the LOS
// for every vertex around that tile, to mark them as explored
// Territory data is stored per territory-tile (typically a multiple of terrain-tiles).
// LOS data is stored per terrain-tile vertex.
// For each territory-tile, if it is owned by a valid player then update the LOS
// for every vertex inside/around that tile, to mark them as explored.
// Currently this code doesn't support territory-tiles smaller than terrain-tiles
// (it will get scale==0 and break), or a non-integer multiple, so check that first
cassert(ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE >= Pathfinding::NAVCELLS_PER_TILE);
cassert(ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE % Pathfinding::NAVCELLS_PER_TILE == 0);
int scale = ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE / Pathfinding::NAVCELLS_PER_TILE;
ENSURE(grid.m_W*scale == m_TerrainVerticesPerSide-1 && grid.m_H*scale == m_TerrainVerticesPerSide-1);
for (u16 j = 0; j < grid.m_H; ++j)
{
@ -1749,15 +1762,19 @@ public:
u8 p = grid.get(i, j) & ICmpTerritoryManager::TERRITORY_PLAYER_MASK;
if (p > 0 && p <= MAX_LOS_PLAYER_ID)
{
u32 &explored = m_ExploredVertices.at(p);
explored += !(m_LosState[i + j*m_TerrainVerticesPerSide] & (LOS_EXPLORED << (2*(p-1))));
m_LosState[i + j*m_TerrainVerticesPerSide] |= (LOS_EXPLORED << (2*(p-1)));
explored += !(m_LosState[i+1 + j*m_TerrainVerticesPerSide] & (LOS_EXPLORED << (2*(p-1))));
m_LosState[i+1 + j*m_TerrainVerticesPerSide] |= (LOS_EXPLORED << (2*(p-1)));
explored += !(m_LosState[i + (j+1)*m_TerrainVerticesPerSide] & (LOS_EXPLORED << (2*(p-1))));
m_LosState[i + (j+1)*m_TerrainVerticesPerSide] |= (LOS_EXPLORED << (2*(p-1)));
explored += !(m_LosState[i+1 + (j+1)*m_TerrainVerticesPerSide] & (LOS_EXPLORED << (2*(p-1))));
m_LosState[i+1 + (j+1)*m_TerrainVerticesPerSide] |= (LOS_EXPLORED << (2*(p-1)));
u32& explored = m_ExploredVertices.at(p);
for (int dj = 0; dj <= scale; ++dj)
{
for (int di = 0; di <= scale; ++di)
{
u32& losState = m_LosState[(i*scale+di) + (j*scale+dj)*m_TerrainVerticesPerSide];
if (!(losState & (LOS_EXPLORED << (2*(p-1)))))
{
explored++;
losState |= (LOS_EXPLORED << (2*(p-1)));
}
}
}
}
}
}
@ -1846,7 +1863,7 @@ public:
*/
inline bool LosIsOffWorld(ssize_t i, ssize_t j)
{
// WARNING: CCmpObstructionManager::Rasterise needs to be kept in sync with this
// WARNING: CCmpPathfinder::UpdateGrid needs to be kept in sync with this
const ssize_t edgeSize = 3; // number of vertexes around the edge that will be off-world
if (m_LosCircular)

View File

@ -35,7 +35,6 @@
#include "simulation2/components/ICmpPlayer.h"
#include "simulation2/components/ICmpPlayerManager.h"
#include "simulation2/components/ICmpPosition.h"
#include "simulation2/components/ICmpTerrain.h"
#include "simulation2/components/ICmpTerritoryInfluence.h"
#include "simulation2/helpers/Geometry.h"
#include "simulation2/helpers/Grid.h"
@ -45,15 +44,14 @@
class CCmpTerritoryManager;
class TerritoryOverlay : public TerrainOverlay
class TerritoryOverlay : public TerrainTextureOverlay
{
NONCOPYABLE(TerritoryOverlay);
public:
CCmpTerritoryManager& m_TerritoryManager;
TerritoryOverlay(CCmpTerritoryManager& manager);
virtual void StartRender();
virtual void ProcessTile(ssize_t i, ssize_t j);
virtual void BuildTextureRGBA(u8* data, size_t w, size_t h);
};
class CCmpTerritoryManager : public ICmpTerritoryManager
@ -88,6 +86,9 @@ public:
// processed flag in bit 7 (TERRITORY_PROCESSED_MASK)
Grid<u8>* m_Territories;
// Saves the cost per tile (to stop territory on impassable tiles)
Grid<u8>* m_CostGrid;
// Set to true when territories change; will send a TerritoriesChanged message
// during the Update phase
bool m_TriggerEvent;
@ -112,6 +113,7 @@ public:
virtual void Init(const CParamNode& UNUSED(paramNode))
{
m_Territories = NULL;
m_CostGrid = NULL;
m_DebugOverlay = NULL;
// m_DebugOverlay = new TerritoryOverlay(*this);
m_BoundaryLinesDirty = true;
@ -137,6 +139,7 @@ public:
virtual void Deinit()
{
SAFE_DELETE(m_Territories);
SAFE_DELETE(m_CostGrid);
SAFE_DELETE(m_DebugOverlay);
}
@ -177,6 +180,8 @@ public:
case MT_TerrainChanged:
case MT_WaterChanged:
{
// also recalculate the cost grid to support atlas changes
SAFE_DELETE(m_CostGrid);
MakeDirty();
break;
}
@ -251,6 +256,8 @@ public:
return false;
}
void CalculateCostGrid();
void CalculateTerritories();
std::vector<STerritoryBoundary> ComputeBoundaries();
@ -305,10 +312,50 @@ struct Tile
/**
* Compute the tile indexes on the grid nearest to a given point
*/
static void NearestTile(entity_pos_t x, entity_pos_t z, u16& i, u16& j, u16 w, u16 h)
static void NearestTerritoryTile(entity_pos_t x, entity_pos_t z, u16& i, u16& j, u16 w, u16 h)
{
i = (u16)clamp((x / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), 0, w-1);
j = (u16)clamp((z / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), 0, h-1);
entity_pos_t scale = Pathfinding::NAVCELL_SIZE * ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE;
i = clamp((x / scale).ToInt_RoundToNegInfinity(), 0, w - 1);
j = clamp((z / scale).ToInt_RoundToNegInfinity(), 0, h - 1);
}
void CCmpTerritoryManager::CalculateCostGrid()
{
if (m_CostGrid)
return;
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
if (!cmpPathfinder)
return;
pass_class_t passClassTerritory = cmpPathfinder->GetPassabilityClass("territory");
pass_class_t passClassUnrestricted = cmpPathfinder->GetPassabilityClass("unrestricted");
const Grid<u16>& passGrid = cmpPathfinder->GetPassabilityGrid();
int tilesW = passGrid.m_W / NAVCELLS_PER_TERRITORY_TILE;
int tilesH = passGrid.m_H / NAVCELLS_PER_TERRITORY_TILE;
m_CostGrid = new Grid<u8>(tilesW, tilesH);
for (int i = 0; i < tilesW; ++i)
{
for (int j = 0; j < tilesH; ++j)
{
u16 c = 0;
for (u16 di = 0; di < NAVCELLS_PER_TERRITORY_TILE; ++di)
for (u16 dj = 0; dj < NAVCELLS_PER_TERRITORY_TILE; ++dj)
c |= passGrid.get(
i * NAVCELLS_PER_TERRITORY_TILE + di,
j * NAVCELLS_PER_TERRITORY_TILE + dj);
if (c & passClassTerritory)
m_CostGrid->set(i, j, m_ImpassableCost);
else if (c & passClassUnrestricted)
m_CostGrid->set(i, j, 255); // off the world; use maximum cost
else
m_CostGrid->set(i, j, 1);
}
}
}
void CCmpTerritoryManager::CalculateTerritories()
@ -318,15 +365,14 @@ void CCmpTerritoryManager::CalculateTerritories()
PROFILE("CalculateTerritories");
CmpPtr<ICmpTerrain> cmpTerrain(GetSystemEntity());
// If the terrain hasn't been loaded (e.g. this is called during map initialisation),
// If the pathfinder hasn't been loaded (e.g. this is called during map initialisation),
// abort the computation (and assume callers can cope with m_Territories == NULL)
if (!cmpTerrain->IsLoaded())
CalculateCostGrid();
if (!m_CostGrid)
return;
const u16 tilesW = cmpTerrain->GetTilesPerSide();
const u16 tilesH = tilesW;
const u16 tilesW = m_CostGrid->m_W;
const u16 tilesH = m_CostGrid->m_H;
m_Territories = new Grid<u8>(tilesW, tilesH);
@ -356,15 +402,6 @@ void CCmpTerritoryManager::CalculateTerritories()
// store the root influences to mark territory as connected
std::vector<entity_id_t> rootInfluenceEntities;
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
if (!cmpPathfinder)
return;
ICmpPathfinder::pass_class_t passClassDefault = cmpPathfinder->GetPassabilityClass("default");
ICmpPathfinder::pass_class_t passClassUnrestricted = cmpPathfinder->GetPassabilityClass("unrestricted");
const Grid<u16>& passibilityGrid = cmpPathfinder->GetPassabilityGrid();
for (const std::pair<player_id_t, std::vector<entity_id_t> >& pair : influenceEntities)
{
// entityGrid stores the weight for a single entity, and is reset per entity
@ -385,14 +422,14 @@ void CCmpTerritoryManager::CalculateTerritories()
CmpPtr<ICmpTerritoryInfluence> cmpTerritoryInfluence(GetSimContext(), ent);
u32 weight = cmpTerritoryInfluence->GetWeight();
u32 radius = cmpTerritoryInfluence->GetRadius() / TERRAIN_TILE_SIZE;
u32 radius = cmpTerritoryInfluence->GetRadius() / (Pathfinding::NAVCELL_SIZE * NAVCELLS_PER_TERRITORY_TILE).ToInt_RoundToNegInfinity();
if (weight == 0 || radius == 0)
continue;
u32 falloff = weight / radius;
CFixedVector2D pos = cmpPosition->GetPosition2D();
u16 i, j;
NearestTile(pos.X, pos.Y, i, j, tilesW, tilesH);
NearestTerritoryTile(pos.X, pos.Y, i, j, tilesW, tilesH);
if (cmpTerritoryInfluence->IsRoot())
rootInfluenceEntities.push_back(ent);
@ -407,13 +444,7 @@ void CCmpTerritoryManager::CalculateTerritories()
// Expand influences outwards
FLOODFILL(i, j,
u32 dg = falloff;
// enlarge the falloff for unpassable tiles
u16 g = passibilityGrid.get(nx, nz);
if (g & passClassUnrestricted)
dg *= 255; // off the world; use maximum cost
else if (g & passClassDefault)
dg *= m_ImpassableCost;
u32 dg = falloff * m_CostGrid->get(nx, nz);
// diagonal neighbour -> multiply with approx sqrt(2)
if (nx != x && nz != z)
@ -451,7 +482,7 @@ void CCmpTerritoryManager::CalculateTerritories()
CFixedVector2D pos = cmpPosition->GetPosition2D();
u16 i, j;
NearestTile(pos.X, pos.Y, i, j, tilesW, tilesH);
NearestTerritoryTile(pos.X, pos.Y, i, j, tilesW, tilesH);
u8 owner = (u8)cmpOwnership->GetOwner();
@ -592,7 +623,7 @@ player_id_t CCmpTerritoryManager::GetOwner(entity_pos_t x, entity_pos_t z)
if (!m_Territories)
return 0;
NearestTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H);
NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H);
return m_Territories->get(i, j) & TERRITORY_PLAYER_MASK;
}
@ -608,7 +639,7 @@ std::vector<u32> CCmpTerritoryManager::GetNeighbours(entity_pos_t x, entity_pos_
return ret;
u16 i, j;
NearestTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H);
NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H);
// calculate the neighbours
player_id_t thisOwner = m_Territories->get(i, j) & TERRITORY_PLAYER_MASK;
@ -644,7 +675,7 @@ bool CCmpTerritoryManager::IsConnected(entity_pos_t x, entity_pos_t z)
if (!m_Territories)
return false;
NearestTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H);
NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H);
return (m_Territories->get(i, j) & TERRITORY_CONNECTED_MASK) != 0;
}
@ -655,7 +686,7 @@ void CCmpTerritoryManager::SetTerritoryBlinking(entity_pos_t x, entity_pos_t z)
return;
u16 i, j;
NearestTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H);
NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H);
u16 tilesW = m_Territories->m_W;
u16 tilesH = m_Territories->m_H;
@ -671,33 +702,25 @@ void CCmpTerritoryManager::SetTerritoryBlinking(entity_pos_t x, entity_pos_t z)
m_BoundaryLinesDirty = true;
}
TerritoryOverlay::TerritoryOverlay(CCmpTerritoryManager& manager)
: TerrainOverlay(manager.GetSimContext()), m_TerritoryManager(manager)
TerritoryOverlay::TerritoryOverlay(CCmpTerritoryManager& manager) :
TerrainTextureOverlay((float)Pathfinding::NAVCELLS_PER_TILE / ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE),
m_TerritoryManager(manager)
{ }
void TerritoryOverlay::StartRender()
void TerritoryOverlay::BuildTextureRGBA(u8* data, size_t w, size_t h)
{
m_TerritoryManager.CalculateTerritories();
}
void TerritoryOverlay::ProcessTile(ssize_t i, ssize_t j)
{
if (!m_TerritoryManager.m_Territories)
return;
u8 id = (m_TerritoryManager.m_Territories->get((int) i, (int) j) & ICmpTerritoryManager::TERRITORY_PLAYER_MASK);
float a = 0.2f;
switch (id)
for (size_t j = 0; j < h; ++j)
{
case 0: break;
case 1: RenderTile(CColor(1, 0, 0, a), false); break;
case 2: RenderTile(CColor(0, 1, 0, a), false); break;
case 3: RenderTile(CColor(0, 0, 1, a), false); break;
case 4: RenderTile(CColor(1, 1, 0, a), false); break;
case 5: RenderTile(CColor(0, 1, 1, a), false); break;
case 6: RenderTile(CColor(1, 0, 1, a), false); break;
default: RenderTile(CColor(1, 1, 1, a), false); break;
for (size_t i = 0; i < w; ++i)
{
SColor4ub color;
u8 id = (m_TerritoryManager.m_Territories->get((int)i, (int)j) & ICmpTerritoryManager::TERRITORY_PLAYER_MASK);
color = GetColor(id, 64);
*data++ = color.R;
*data++ = color.G;
*data++ = color.B;
*data++ = color.A;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2013 Wildfire Games.
/* Copyright (C) 2015 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -56,6 +56,8 @@ public:
virtual entity_pos_t GetUnitRadius() = 0;
virtual void SetUnitClearance(const entity_pos_t& clearance) = 0;
virtual bool IsControlPersistent() = 0;
/**

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2012 Wildfire Games.
/* Copyright (C) 2015 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -48,7 +48,7 @@ class IObstructionTestFilter;
* Units can be marked as either moving or stationary, which simply determines whether
* certain filters include or exclude them.
*
* The @c Rasterise function approximates the current set of shapes onto a 2D grid,
* The @c Rasterize function approximates the current set of shapes onto a 2D grid,
* for use with tile-based pathfinding.
*/
class ICmpObstructionManager : public IComponent
@ -114,14 +114,15 @@ public:
* @param ent entity ID associated with this shape (or INVALID_ENTITY if none)
* @param x,z coordinates of center, in world space
* @param r radius of circle or half the unit's width/height
* @param clearance pathfinding clearance of the unit
* @param flags a set of EFlags values
* @param group control group (typically the owner entity, or a formation controller entity
* - units ignore collisions with others in the same group)
* @return a valid tag for manipulating the shape
* @see UnitShape
*/
virtual tag_t AddUnitShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_angle_t r, flags_t flags,
entity_id_t group) = 0;
virtual tag_t AddUnitShape(entity_id_t ent, entity_pos_t x, entity_pos_t z, entity_angle_t r, entity_pos_t clearance,
flags_t flags, entity_id_t group) = 0;
/**
* Adjust the position and angle of an existing shape.
@ -205,7 +206,7 @@ public:
std::vector<entity_id_t>* out) = 0;
/**
* Bit-flags for Rasterise.
* Bit-flags for Rasterize.
*/
enum TileObstruction
{
@ -215,16 +216,20 @@ public:
};
/**
* Convert the current set of shapes onto a grid.
* Tiles that are intersected by a pathfind-blocking shape will have TILE_OBSTRUCTED_PATHFINDING set;
* tiles that are intersected by a foundation-blocking shape will also have TILE_OBSTRUCTED_FOUNDATION;
* tiles that are outside the world bounds will also have TILE_OUTOFBOUNDS;
* others will be set to 0.
* This is very cheap if the grid has been rasterised before and the set of shapes has not changed.
* @param grid the grid to be updated
* @return true if any changes were made to the grid, false if it was already up-to-date
* Convert the current set of shapes onto a navcell grid.
* Shapes are expanded by the clearance radius @p expand.
* Only shapes with at least one of the flags from @p requireMask will be considered.
* @p setMask will be ORed onto the @p grid value for all navcells
* that are wholly enclosed by an expanded shape.
*/
virtual bool Rasterise(Grid<u8>& grid) = 0;
virtual void Rasterize(Grid<u16>& grid, const entity_pos_t& expand, ICmpObstructionManager::flags_t requireMask, u16 setMask) = 0;
/**
* Gets dirtiness information and resets it afterwards. Then it's the role of CCmpPathfinder
* to pass the information to other components if needed. (AIs, etc.)
* The return value is false if an update is unnecessary.
*/
virtual bool GetDirtinessData(Grid<u8>& dirtinessGrid, bool& globalUpdateNeeded) = 0;
/**
* Standard representation for all types of shapes, for use with geometry processing code.
@ -248,12 +253,10 @@ public:
virtual void GetObstructionsInRange(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, std::vector<ObstructionSquare>& squares) = 0;
/**
* Find a single obstruction that blocks a unit at the given point with the given radius.
* Static obstructions (buildings) are more important than unit obstructions, and
* obstructions that cover the given point are more important than those that only cover
* the point expanded by the radius.
* Returns the entity IDs of all unit shapes that intersect the given
* obstruction square, filtering out using the given filter.
*/
virtual bool FindMostImportantObstruction(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, ObstructionSquare& square) = 0;
virtual void GetUnitsOnObstruction(const ObstructionSquare& square, std::vector<entity_id_t>& out, const IObstructionTestFilter& filter) = 0;
/**
* Get the obstruction square representing the given shape.
@ -270,6 +273,8 @@ public:
*/
virtual void SetPassabilityCircular(bool enabled) = 0;
virtual bool GetPassabilityCircular() const = 0;
/**
* Toggle the rendering of debug info.
*/
@ -344,10 +349,19 @@ public:
{
if (group == m_Group || (group2 != INVALID_ENTITY && group2 == m_Group))
return false;
// If an obstruction already blocks tile-based pathfinding,
// it will be handled as part of the terrain passability handling
// and doesn't need to be matched by this filter
if (flags & ICmpObstructionManager::FLAG_BLOCK_PATHFINDING)
return false;
if (!(flags & ICmpObstructionManager::FLAG_BLOCK_MOVEMENT))
return false;
if ((flags & ICmpObstructionManager::FLAG_MOVING) && !m_AvoidMoving)
return false;
return true;
}
};

View File

@ -23,4 +23,5 @@
BEGIN_INTERFACE_WRAPPER(Pathfinder)
DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpPathfinder, SetDebugOverlay, bool)
DEFINE_INTERFACE_METHOD_1("SetHierDebugOverlay", void, ICmpPathfinder, SetHierDebugOverlay, bool)
END_INTERFACE_WRAPPER(Pathfinder)

View File

@ -21,7 +21,8 @@
#include "simulation2/system/Interface.h"
#include "simulation2/components/ICmpObstruction.h"
#include "simulation2/helpers/Position.h"
#include "simulation2/helpers/PathGoal.h"
#include "simulation2/helpers/Pathfinding.h"
#include "maths/FixedVector2D.h"
@ -39,7 +40,7 @@ template<typename T> class Grid;
* accounts for terrain costs but ignore units, and a 'short' vertex-based pathfinder that
* provides precise paths and avoids other units.
*
* Both use the same concept of a Goal: either a point, circle or square.
* Both use the same concept of a PathGoal: either a point, circle or square.
* (If the starting point is inside the goal shape then the path will move outwards
* to reach the shape's outline.)
*
@ -48,34 +49,6 @@ template<typename T> class Grid;
class ICmpPathfinder : public IComponent
{
public:
typedef u16 pass_class_t;
typedef u8 cost_class_t;
struct Goal
{
enum Type {
POINT,
CIRCLE,
SQUARE
} type;
entity_pos_t x, z; // position of center
CFixedVector2D u, v; // if SQUARE, then orthogonal unit axes
entity_pos_t hw, hh; // if SQUARE, then half width & height; if CIRCLE, then hw is radius
};
struct Waypoint
{
entity_pos_t x, z;
};
/**
* Returned path.
* Waypoints are in *reverse* order (the earliest is at the back of the list)
*/
struct Path
{
std::vector<Waypoint> m_Waypoints;
};
/**
* Get the list of all known passability classes.
@ -88,14 +61,21 @@ public:
*/
virtual pass_class_t GetPassabilityClass(const std::string& name) = 0;
virtual entity_pos_t GetClearance(pass_class_t passClass) const = 0;
/**
* Get the tag for a given movement cost class name.
* Logs an error and returns something acceptable if the name is unrecognised.
* Get the larger clearance in all passability classes.
*/
virtual cost_class_t GetCostClass(const std::string& name) = 0;
virtual entity_pos_t GetMaximumClearance() const = 0;
virtual const Grid<u16>& GetPassabilityGrid() = 0;
/**
* Passes the lazily-stored dirtiness data collected from
* the obstruction manager during the previous grid update.
*/
virtual bool GetDirtinessData(Grid<u8>& dirtinessGrid, bool& globalUpdateNeeded) = 0;
/**
* Get a grid representing the distance to the shore of the terrain tile.
*/
@ -106,7 +86,7 @@ public:
* The waypoints correspond to the centers of horizontally/vertically adjacent tiles
* along the path.
*/
virtual void ComputePath(entity_pos_t x0, entity_pos_t z0, const Goal& goal, pass_class_t passClass, cost_class_t costClass, Path& ret) = 0;
virtual void ComputePath(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass, WaypointPath& ret) = 0;
/**
* Asynchronous version of ComputePath.
@ -114,12 +94,12 @@ public:
* Returns a unique non-zero number, which will match the 'ticket' in the result,
* so callers can recognise each individual request they make.
*/
virtual u32 ComputePathAsync(entity_pos_t x0, entity_pos_t z0, const Goal& goal, pass_class_t passClass, cost_class_t costClass, entity_id_t notify) = 0;
virtual u32 ComputePathAsync(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass, entity_id_t notify) = 0;
/**
* If the debug overlay is enabled, render the path that will computed by ComputePath.
*/
virtual void SetDebugPath(entity_pos_t x0, entity_pos_t z0, const Goal& goal, pass_class_t passClass, cost_class_t costClass) = 0;
virtual void SetDebugPath(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass) = 0;
/**
* Compute a precise path from the given point to the goal, and return the set of waypoints.
@ -127,7 +107,7 @@ public:
* a unit of radius 'r' will be able to follow the path with no collisions.
* The path is restricted to a box of radius 'range' from the starting point.
*/
virtual void ComputeShortPath(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const Goal& goal, pass_class_t passClass, Path& ret) = 0;
virtual void ComputeShortPath(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const PathGoal& goal, pass_class_t passClass, WaypointPath& ret) = 0;
/**
* Asynchronous version of ComputeShortPath (using ControlGroupObstructionFilter).
@ -135,18 +115,7 @@ public:
* Returns a unique non-zero number, which will match the 'ticket' in the result,
* so callers can recognise each individual request they make.
*/
virtual u32 ComputeShortPathAsync(entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const Goal& goal, pass_class_t passClass, bool avoidMovingUnits, entity_id_t group, entity_id_t notify) = 0;
/**
* Find the speed factor (typically around 1.0) for a unit of the given cost class
* at the given position.
*/
virtual fixed GetMovementSpeed(entity_pos_t x0, entity_pos_t z0, cost_class_t costClass) = 0;
/**
* Returns the coordinates of the point on the goal that is closest to pos in a straight line.
*/
virtual CFixedVector2D GetNearestPointOnGoal(CFixedVector2D pos, const Goal& goal) = 0;
virtual u32 ComputeShortPathAsync(entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const PathGoal& goal, pass_class_t passClass, bool avoidMovingUnits, entity_id_t group, entity_id_t notify) = 0;
/**
* Check whether the given movement line is valid and doesn't hit any obstructions
@ -155,14 +124,6 @@ public:
*/
virtual bool CheckMovement(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, pass_class_t passClass) = 0;
/**
* Check whether a unit placed here is valid and doesn't hit any obstructions
* or impassable terrain.
* @return ICmpObstruction::FOUNDATION_CHECK_SUCCESS if the placement is okay, else
* a value describing the type of failure.
*/
virtual ICmpObstruction::EFoundationCheck CheckUnitPlacement(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, pass_class_t passClass) = 0;
/**
* Check whether a unit placed here is valid and doesn't hit any obstructions
* or impassable terrain.
@ -170,7 +131,7 @@ public:
* @return ICmpObstruction::FOUNDATION_CHECK_SUCCESS if the placement is okay, else
* a value describing the type of failure.
*/
virtual ICmpObstruction::EFoundationCheck CheckUnitPlacement(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, pass_class_t passClass, bool onlyCenterPoint) = 0;
virtual ICmpObstruction::EFoundationCheck CheckUnitPlacement(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, pass_class_t passClass, bool onlyCenterPoint = false) = 0;
/**
* Check whether a building placed here is valid and doesn't hit any obstructions
@ -195,6 +156,11 @@ public:
*/
virtual void SetDebugOverlay(bool enabled) = 0;
/**
* Toggle the storage and rendering of debug info for the hierarchical pathfinder.
*/
virtual void SetHierDebugOverlay(bool enabled) = 0;
/**
* Finish computing asynchronous path requests and send the CMessagePathResult messages.
*/
@ -205,6 +171,16 @@ public:
*/
virtual void ProcessSameTurnMoves() = 0;
/**
* Regenerates the grid based on the current obstruction list, if necessary
*/
virtual void UpdateGrid() = 0;
/**
* Returns some stats about the last ComputePath.
*/
virtual void GetDebugData(u32& steps, double& time, Grid<u8>& grid) = 0;
DECLARE_INTERFACE_TYPE(Pathfinder)
};

View File

@ -29,6 +29,14 @@ class ICmpTerritoryManager : public IComponent
public:
virtual bool NeedUpdate(size_t* dirtyID) = 0;
/**
* Number of pathfinder navcells per territory tile.
* Passability data is stored per navcell, but we probably don't need that much
* resolution, and a lower resolution can make the boundary lines look prettier
* and will take less memory, so we downsample the passability data.
*/
static const int NAVCELLS_PER_TERRITORY_TILE = 8;
static const int TERRITORY_PLAYER_MASK = 0x1F;
static const int TERRITORY_CONNECTED_MASK = 0x20;
static const int TERRITORY_BLINKING_MASK = 0x40;

View File

@ -112,9 +112,9 @@ public:
m_Script.CallVoid("SetFacePointAfterMove", facePointAfterMove);
}
virtual ICmpPathfinder::pass_class_t GetPassabilityClass()
virtual pass_class_t GetPassabilityClass()
{
return m_Script.Call<ICmpPathfinder::pass_class_t>("GetPassabilityClass");
return m_Script.Call<pass_class_t>("GetPassabilityClass");
}
virtual std::string GetPassabilityClassName()

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2011 Wildfire Games.
/* Copyright (C) 2012 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -27,8 +27,7 @@
* Motion interface for entities with complex movement capabilities.
* (Simpler motion is handled by ICmpMotion instead.)
*
* Currently this is limited to telling the entity to walk to a point.
* Eventually it should support different movement speeds, moving to areas
* It should eventually support different movement speeds, moving to areas
* instead of points, moving as part of a group, moving as part of a formation,
* etc.
*/
@ -38,6 +37,7 @@ public:
/**
* Attempt to walk into range of a to a given point, or as close as possible.
* The range is measured from the center of the unit.
* If the unit is already in range, or cannot move anywhere at all, or if there is
* some other error, then returns false.
* Otherwise, returns true and sends a MotionChanged message after starting to move,
@ -60,6 +60,8 @@ public:
/**
* Attempt to walk into range of a given target entity, or as close as possible.
* The range is measured between approximately the edges of the unit and the target, so that
* maxRange=0 is not unreachably close to the target.
* If the unit is already in range, or cannot move anywhere at all, or if there is
* some other error, then returns false.
* Otherwise, returns true and sends a MotionChanged message after starting to move,
@ -117,7 +119,7 @@ public:
/**
* Get the unit's passability class.
*/
virtual ICmpPathfinder::pass_class_t GetPassabilityClass() = 0;
virtual pass_class_t GetPassabilityClass() = 0;
/**
* Get the passability class name (as defined in pathfinder.xml)

View File

@ -26,10 +26,10 @@ class TestCmpObstructionManager : public CxxTest::TestSuite
// some variables for setting up a scene with 3 shapes
entity_id_t ent1, ent2, ent3; // entity IDs
entity_angle_t ent1a, ent2r, ent3r; // angles/radiuses
entity_angle_t ent1a; // angles
entity_pos_t ent1x, ent1z, ent1w, ent1h, // positions/dimensions
ent2x, ent2z,
ent3x, ent3z;
ent2x, ent2z, ent2r, ent2c,
ent3x, ent3z, ent3r, ent3c;
entity_id_t ent1g1, ent1g2, ent2g, ent3g; // control groups
tag_t shape1, shape2, shape3;
@ -58,12 +58,14 @@ public:
ent2 = 2;
ent2r = fixed::FromFloat(1);
ent2c = fixed::Zero();
ent2x = ent1x;
ent2z = ent1z;
ent2g = ent1g1;
ent3 = 3;
ent3r = fixed::FromFloat(3);
ent3c = fixed::Zero();
ent3x = ent2x;
ent3z = ent2z + ent2r + ent3r; // ensure it just touches the border of ent2
ent3g = ent3;
@ -77,11 +79,11 @@ public:
ICmpObstructionManager::FLAG_BLOCK_MOVEMENT |
ICmpObstructionManager::FLAG_MOVING, ent1g1, ent1g2);
shape2 = cmp->AddUnitShape(ent2, ent2x, ent2z, ent2r,
shape2 = cmp->AddUnitShape(ent2, ent2x, ent2z, ent2r, ent2c,
ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION |
ICmpObstructionManager::FLAG_BLOCK_FOUNDATION, ent2g);
shape3 = cmp->AddUnitShape(ent3, ent3x, ent3z, ent3r,
shape3 = cmp->AddUnitShape(ent3, ent3x, ent3z, ent3r, ent3c,
ICmpObstructionManager::FLAG_BLOCK_MOVEMENT |
ICmpObstructionManager::FLAG_BLOCK_FOUNDATION, ent3g);
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2013 Wildfire Games.
/* Copyright (C) 2015 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -26,6 +26,7 @@
#include "lib/timer.h"
#include "lib/tex/tex.h"
#include "ps/Loader.h"
#include "ps/Pyrogenesis.h"
#include "simulation2/Simulation2.h"
class TestCmpPathfinder : public CxxTest::TestSuite
@ -33,11 +34,12 @@ class TestCmpPathfinder : public CxxTest::TestSuite
public:
void setUp()
{
CXeromyces::Startup();
g_VFS = CreateVfs(20 * MiB);
TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST));
TS_ASSERT_OK(g_VFS->Mount(L"cache/", DataDir()/"cache"));
TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "mod", VFS_MOUNT_MUST_EXIST));
TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "public", VFS_MOUNT_MUST_EXIST, 1));
TS_ASSERT_OK(g_VFS->Mount(L"cache/", DataDir() / "cache"));
CXeromyces::Startup();
// Need some stuff for terrain movement costs:
// (TODO: this ought to be independent of any graphics code)
@ -54,7 +56,6 @@ public:
CXeromyces::Terminate();
}
// disabled by default; run tests with the "-test TestCmpPathfinder" flag to enable
void test_performance_DISABLED()
{
CTerrain terrain;
@ -84,8 +85,8 @@ public:
entity_pos_t z1 = entity_pos_t::FromInt(495);
ICmpPathfinder::Goal goal = { ICmpPathfinder::Goal::POINT, x1, z1 };
ICmpPathfinder::Path path;
cmp->ComputePath(x0, z0, goal, cmp->GetPassabilityClass("default"), cmp->GetCostClass("default"), path);
WaypointPath path;
cmp->ComputePath(x0, z0, goal, cmp->GetPassabilityClass("default"), path);
for (size_t i = 0; i < path.m_Waypoints.size(); ++i)
printf("%d: %f %f\n", (int)i, path.m_Waypoints[i].x.ToDouble(), path.m_Waypoints[i].z.ToDouble());
#endif
@ -99,10 +100,10 @@ public:
entity_pos_t z0 = entity_pos_t::FromInt(rand() % 512);
entity_pos_t x1 = x0 + entity_pos_t::FromInt(rand() % 64);
entity_pos_t z1 = z0 + entity_pos_t::FromInt(rand() % 64);
ICmpPathfinder::Goal goal = { ICmpPathfinder::Goal::POINT, x1, z1 };
PathGoal goal = { PathGoal::POINT, x1, z1 };
ICmpPathfinder::Path path;
cmp->ComputePath(x0, z0, goal, cmp->GetPassabilityClass("default"), cmp->GetCostClass("default"), path);
WaypointPath path;
cmp->ComputePath(x0, z0, goal, cmp->GetPassabilityClass("default"), path);
}
t = timer_Time() - t;
@ -129,14 +130,216 @@ public:
fixed x = fixed::FromFloat(1.5f*range.ToFloat() * rand()/(float)RAND_MAX);
fixed z = fixed::FromFloat(1.5f*range.ToFloat() * rand()/(float)RAND_MAX);
// printf("# %f %f\n", x.ToFloat(), z.ToFloat());
cmpObstructionMan->AddUnitShape(INVALID_ENTITY, x, z, fixed::FromInt(2), 0, INVALID_ENTITY);
cmpObstructionMan->AddUnitShape(INVALID_ENTITY, x, z, fixed::FromInt(2), fixed::Zero(), 0, INVALID_ENTITY);
}
NullObstructionFilter filter;
ICmpPathfinder::Goal goal = { ICmpPathfinder::Goal::POINT, range, range };
ICmpPathfinder::Path path;
PathGoal goal = { PathGoal::POINT, range, range };
WaypointPath path;
cmpPathfinder->ComputeShortPath(filter, range/3, range/3, fixed::FromInt(2), range, goal, 0, path);
for (size_t i = 0; i < path.m_Waypoints.size(); ++i)
printf("# %d: %f %f\n", (int)i, path.m_Waypoints[i].x.ToFloat(), path.m_Waypoints[i].z.ToFloat());
}
template<typename T>
void DumpGrid(std::ostream& stream, const Grid<T>& grid, int mask)
{
for (u16 j = 0; j < grid.m_H; ++j)
{
for (u16 i = 0; i < grid.m_W; )
{
if (!(grid.get(i, j) & mask))
{
i++;
continue;
}
u16 i0 = i;
for (i = i0+1; ; ++i)
{
if (i >= grid.m_W || !(grid.get(i, j) & mask))
{
stream << " <rect x='" << i0 << "' y='" << j << "' width='" << (i-i0) << "' height='1'/>\n";
break;
}
}
}
}
}
void test_perf2_DISABLED()
{
CTerrain terrain;
CSimulation2 sim2(NULL, g_ScriptRuntime, &terrain);
sim2.LoadDefaultScripts();
sim2.ResetState();
CMapReader* mapReader = new CMapReader(); // it'll call "delete this" itself
LDR_BeginRegistering();
mapReader->LoadMap(L"maps/scenarios/Peloponnese.pmp",
sim2.GetScriptInterface().GetJSRuntime(), JS::UndefinedHandleValue,
&terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
&sim2, &sim2.GetSimContext(), -1, false);
LDR_EndRegistering();
TS_ASSERT_OK(LDR_NonprogressiveLoad());
sim2.Update(0);
std::ofstream stream(OsString("perf2.html").c_str(), std::ofstream::out | std::ofstream::trunc);
CmpPtr<ICmpObstructionManager> cmpObstructionManager(sim2, SYSTEM_ENTITY);
CmpPtr<ICmpPathfinder> cmpPathfinder(sim2, SYSTEM_ENTITY);
pass_class_t obstructionsMask = cmpPathfinder->GetPassabilityClass("default") | ICmpObstructionManager::TILE_OBSTRUCTED_PATHFINDING;
const Grid<u16>& obstructions = cmpPathfinder->GetPassabilityGrid();
int scale = 1;
stream << "<!DOCTYPE html>\n";
stream << "<style>\n";
stream << ".passability rect { fill: black; }\n";
stream << ".astar-open rect { fill: rgba(255,255,0,0.75); }\n";
stream << ".astar-closed rect { fill: rgba(255,0,0,0.75); }\n";
// stream << ".astar-closed rect { fill: rgba(0,255,0,0.75); }\n";
stream << ".path { stroke: rgba(0,0,255,0.75); stroke-width: 1; fill: none; }\n";
stream << "</style>\n";
stream << "<svg width='" << obstructions.m_W*scale << "' height='" << obstructions.m_H*scale << "'>\n";
stream << "<g transform='translate(0 " << obstructions.m_H*scale << ") scale(" << scale << " -" << scale << ")'>\n";
stream << " <g class='passability'>\n";
DumpGrid(stream, obstructions, obstructionsMask);
stream << " </g>\n";
DumpPath(stream, 128*4, 256*4, 128*4, 384*4, cmpPathfinder);
// RepeatPath(500, 128*4, 256*4, 128*4, 384*4, cmpPathfinder);
//
// DumpPath(stream, 128*4, 204*4, 192*4, 204*4, cmpPathfinder);
//
// DumpPath(stream, 128*4, 230*4, 32*4, 230*4, cmpPathfinder);
stream << "</g>\n";
stream << "</svg>\n";
}
void test_perf3_DISABLED()
{
CTerrain terrain;
CSimulation2 sim2(NULL, g_ScriptRuntime, &terrain);
sim2.LoadDefaultScripts();
sim2.ResetState();
CMapReader* mapReader = new CMapReader(); // it'll call "delete this" itself
LDR_BeginRegistering();
mapReader->LoadMap(L"maps/scenarios/Peloponnese.pmp",
sim2.GetScriptInterface().GetJSRuntime(), JS::UndefinedHandleValue,
&terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
&sim2, &sim2.GetSimContext(), -1, false);
LDR_EndRegistering();
TS_ASSERT_OK(LDR_NonprogressiveLoad());
sim2.Update(0);
std::ofstream stream(OsString("perf3.html").c_str(), std::ofstream::out | std::ofstream::trunc);
CmpPtr<ICmpObstructionManager> cmpObstructionManager(sim2, SYSTEM_ENTITY);
CmpPtr<ICmpPathfinder> cmpPathfinder(sim2, SYSTEM_ENTITY);
pass_class_t obstructionsMask = cmpPathfinder->GetPassabilityClass("default") | ICmpObstructionManager::TILE_OBSTRUCTED_PATHFINDING;
const Grid<u16>& obstructions = cmpPathfinder->GetPassabilityGrid();
int scale = 31;
stream << "<!DOCTYPE html>\n";
stream << "<style>\n";
stream << ".passability rect { fill: black; }\n";
stream << ".astar-open rect { fill: rgba(255,255,0,0.75); }\n";
stream << ".astar-closed rect { fill: rgba(255,0,0,0.75); }\n";
stream << ".path { stroke: rgba(0,0,255,0.75); stroke-width: " << (1.0f / scale) << "; fill: none; }\n";
stream << "</style>\n";
stream << "<svg width='" << obstructions.m_W*scale << "' height='" << obstructions.m_H*scale << "'>\n";
stream << "<defs><pattern id='grid' width='1' height='1' patternUnits='userSpaceOnUse'>\n";
stream << "<rect fill='none' stroke='#888' stroke-width='" << (1.0f / scale) << "' width='" << scale << "' height='" << scale << "'/>\n";
stream << "</pattern></defs>\n";
stream << "<g transform='translate(0 " << obstructions.m_H*scale << ") scale(" << scale << " -" << scale << ")'>\n";
stream << " <g class='passability'>\n";
DumpGrid(stream, obstructions, obstructionsMask);
stream << " </g>\n";
for (int j = 160; j < 190; ++j)
for (int i = 220; i < 290; ++i)
DumpPath(stream, 230, 178, i, j, cmpPathfinder);
stream << "<rect fill='url(#grid)' width='100%' height='100%'/>\n";
stream << "</g>\n";
stream << "</svg>\n";
}
void DumpPath(std::ostream& stream, int i0, int j0, int i1, int j1, CmpPtr<ICmpPathfinder>& cmpPathfinder)
{
entity_pos_t x0 = entity_pos_t::FromInt(i0);
entity_pos_t z0 = entity_pos_t::FromInt(j0);
entity_pos_t x1 = entity_pos_t::FromInt(i1);
entity_pos_t z1 = entity_pos_t::FromInt(j1);
PathGoal goal = { PathGoal::POINT, x1, z1 };
WaypointPath path;
cmpPathfinder->ComputePath(x0, z0, goal, cmpPathfinder->GetPassabilityClass("default"), path);
u32 debugSteps;
double debugTime;
Grid<u8> debugGrid;
cmpPathfinder->GetDebugData(debugSteps, debugTime, debugGrid);
// stream << " <g style='visibility:hidden'>\n";
stream << " <g>\n";
// stream << " <g class='astar-open'>\n";
// DumpGrid(stream, debugGrid, 1);
// stream << " </g>\n";
// stream << " <g class='astar-closed'>\n";
// DumpGrid(stream, debugGrid, 2);
// stream << " </g>\n";
// stream << " <g class='astar-closed'>\n";
// DumpGrid(stream, debugGrid, 3);
// stream << " </g>\n";
stream << " </g>\n";
stream << " <polyline";
stream << " onmouseover='this.previousElementSibling.style.visibility=\"visible\"' onmouseout='this.previousElementSibling.style.visibility=\"hidden\"'";
double length = 0;
for (ssize_t i = 0; i < (ssize_t)path.m_Waypoints.size()-1; ++i)
{
double dx = (path.m_Waypoints[i+1].x - path.m_Waypoints[i].x).ToDouble();
double dz = (path.m_Waypoints[i+1].z - path.m_Waypoints[i].z).ToDouble();
length += sqrt(dx*dx + dz*dz);
}
stream << " title='length: " << length << "; tiles explored: " << debugSteps << "; time: " << debugTime*1000 << " msec'";
stream << " class='path' points='";
for (size_t i = 0; i < path.m_Waypoints.size(); ++i)
stream << path.m_Waypoints[i].x.ToDouble()*Pathfinding::NAVCELLS_PER_TILE/TERRAIN_TILE_SIZE << "," << path.m_Waypoints[i].z.ToDouble()*Pathfinding::NAVCELLS_PER_TILE/TERRAIN_TILE_SIZE << " ";
stream << "'/>\n";
}
void RepeatPath(int n, int i0, int j0, int i1, int j1, CmpPtr<ICmpPathfinder>& cmpPathfinder)
{
entity_pos_t x0 = entity_pos_t::FromInt(i0);
entity_pos_t z0 = entity_pos_t::FromInt(j0);
entity_pos_t x1 = entity_pos_t::FromInt(i1);
entity_pos_t z1 = entity_pos_t::FromInt(j1);
PathGoal goal = { PathGoal::POINT, x1, z1 };
double t = timer_Time();
for (int i = 0; i < n; ++i)
{
WaypointPath path;
cmpPathfinder->ComputePath(x0, z0, goal, cmpPathfinder->GetPassabilityClass("default"), path);
}
t = timer_Time() - t;
debug_printf("### RepeatPath %fms each (%fs total)\n", 1000*t / n, t);
}
};

View File

@ -49,11 +49,11 @@ public:
TS_ASSERT_EQUALS((player_id_t)7, boundaries[0].owner);
TS_ASSERT_EQUALS(false, boundaries[0].blinking); // high bits aren't set by GetGrid
// assumes CELL_SIZE is 4; dealt with in TestBoundaryPointsEqual
int expectedPoints[][2] = {{ 2, 4}, { 6, 4}, {10, 4}, {14, 4}, {18, 4}, {22, 4},
{24, 6}, {24,10}, {24,14},
{22,16}, {18,16}, {14,16}, {10,16}, { 6,16}, { 2,16},
{ 0,14}, { 0,10}, { 0, 6}};
// assumes CELL_SIZE is 2; dealt with in TestBoundaryPointsEqual
int expectedPoints[][2] = {{ 4, 8}, {12, 8}, {20, 8}, {28, 8}, {36, 8}, {44, 8},
{48,12}, {48,20}, {48,28},
{44,32}, {36,32}, {28,32}, {20,32}, {12,32}, { 4,32},
{ 0,28}, { 0,20}, { 0,12}};
TestBoundaryPointsEqual(boundaries[0].points, expectedPoints);
}
@ -97,8 +97,8 @@ public:
}
else
{
TS_ASSERT_EQUALS(onesInnerNumExpectedPoints, boundary.points.size()); // all inner boundaries are of size 4
if (boundary.points[0].X < 14.f)
TS_ASSERT_EQUALS(onesInnerNumExpectedPoints, boundary.points.size()); // all inner boundaries are of size 2
if (boundary.points[0].X < 24.f)
{
// leftmost inner boundary, i.e. onesInner0
TSM_ASSERT_EQUALS("Found multiple leftmost inner boundaries for territory owned by player 1", onesInner0, (STerritoryBoundary*) NULL);
@ -139,14 +139,14 @@ public:
TS_ASSERT_EQUALS(twosOuter->points.size(), 4U);
TS_ASSERT_EQUALS(threesOuter->points.size(), 4U);
int onesOuterExpectedPoints[][2] = {{6,4}, {10,4}, {14,4}, {18,4}, {22,4}, {26,4},
{28,6}, {26,8}, {24,10}, {26,12}, {28,14},
{26,16}, {22,16}, {18,16}, {14,16}, {10,16}, {6,16},
{4,14}, {4,10}, {4,6}};
int onesInner0ExpectedPoints[][2] = {{10,12}, {12,10}, {10,8}, {8,10}};
int onesInner2ExpectedPoints[][2] = {{18,12}, {20,10}, {18,8}, {16,10}};
int twosOuterExpectedPoints[][2] = {{18,8}, {20,10}, {18,12}, {16,10}};
int threesOuterExpectedPoints[][2] = {{26,8}, {28,10}, {26,12}, {24,10}};
int onesOuterExpectedPoints[][2] = {{12, 8}, {20, 8}, {28, 8}, {36, 8}, {44, 8}, {52, 8},
{56,12}, {52,16}, {48,20}, {52,24}, {56,28},
{52,32}, {44,32}, {36,32}, {28,32}, {20,32}, {12,32},
{ 8,28}, { 8,20}, { 8,12}};
int onesInner0ExpectedPoints[][2] = {{20,24}, {24,20}, {20,16}, {16,20}};
int onesInner2ExpectedPoints[][2] = {{36,24}, {40,20}, {36,16}, {32,20}};
int twosOuterExpectedPoints[][2] = {{36,16}, {40,20}, {36,24}, {32,20}};
int threesOuterExpectedPoints[][2] = {{52,16}, {56,20}, {52,24}, {48,20}};
TestBoundaryPointsEqual(onesOuter->points, onesOuterExpectedPoints);
TestBoundaryPointsEqual(onesInner0->points, onesInner0ExpectedPoints);
@ -230,15 +230,15 @@ public:
twosInner = tmp;
}
int onesOuterExpectedPoints[][2] = {{14, 8}, {18, 8}, {20,10}, {20,14}, {18,16}, {14,16}, {12,14}, {12,10}};
int twosOuterExpectedPoints[][2] = {{ 6, 0}, {10, 0}, {14, 0}, {16, 2}, {18, 4}, {22, 4},
{24, 6}, {24,10}, {24,14}, {24,18}, {24,22},
{22,24}, {18,24}, {14,24}, {10,24}, { 6,24},
{4, 22}, {4, 18}, {4, 14}, {4, 10}, { 4, 6}, { 4, 2}};
int twosInnerExpectedPoints[][2] = {{10,20}, {14,20}, {18,20}, {20,18}, {20,14}, {20,10}, {18, 8},
{14, 8}, {12, 6}, {10, 4}, { 8, 6}, { 8,10}, { 8,14}, { 8,18}};
int threesOuterExpectedPoints[][2] = {{18, 0}, {22, 0}, {26, 0}, {28, 2}, {28, 6}, {28,10}, {28,14}, {26,16},
{24,14}, {24,10}, {24, 6}, {22, 4}, {18, 4}, {16, 2}};
int onesOuterExpectedPoints[][2] = {{28,16}, {36,16}, {40,20}, {40,28}, {36,32}, {28,32}, {24,28}, {24,20}};
int twosOuterExpectedPoints[][2] = {{12, 0}, {20, 0}, {28, 0}, {32, 4}, {36, 8}, {44, 8},
{48,12}, {48,20}, {48,28}, {48,36}, {48,44},
{44,48}, {36,48}, {28,48}, {20,48}, {12,48},
{ 8,44}, { 8,36}, { 8,28}, { 8,20}, { 8,12}, { 8, 4}};
int twosInnerExpectedPoints[][2] = {{20,40}, {28,40}, {36,40}, {40,36}, {40,28}, {40,20}, {36,16},
{28,16}, {24,12}, {20, 8}, {16,12}, {16,20}, {16,28}, {16,36}};
int threesOuterExpectedPoints[][2] = {{36, 0}, {44, 0}, {52, 0}, {56, 4}, {56,12}, {56,20}, {56,28}, {52,32},
{48,28}, {48,20}, {48,12}, {44, 8}, {36, 8}, {32, 4}};
TestBoundaryPointsEqual(onesOuter->points, onesOuterExpectedPoints);
TestBoundaryPointsEqual(twosOuter->points, twosOuterExpectedPoints);
@ -281,7 +281,7 @@ private:
// version of 'points', so that the starting position doesn't need to match exactly.
for (size_t i = 0; i < points.size(); i++)
{
// the input numbers in expectedPoints are defined under the assumption that CELL_SIZE is 4, so let's include
// the input numbers in expectedPoints are defined under the assumption that CELL_SIZE is 2, so let's include
// a scaling factor to protect against that should CELL_SIZE ever change
TS_ASSERT_DELTA(points[i].X, float(expectedPoints[i][0]) * 4.f / TERRAIN_TILE_SIZE, 1e-7);
TS_ASSERT_DELTA(points[i].Y, float(expectedPoints[i][1]) * 4.f / TERRAIN_TILE_SIZE, 1e-7);

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2012 Wildfire Games.
/* Copyright (C) 2015 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -50,7 +50,7 @@ float Geometry::ChordToCentralAngle(const float chordLength, const float radius)
return acosf(1.f - SQR(chordLength)/(2.f*SQR(radius))); // cfr. law of cosines
}
fixed Geometry::DistanceToSquare(CFixedVector2D point, CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize)
fixed Geometry::DistanceToSquare(CFixedVector2D point, CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize, bool countInsideAsZero)
{
/*
* Relative to its own coordinate system, we have a square like:
@ -92,7 +92,7 @@ fixed Geometry::DistanceToSquare(CFixedVector2D point, CFixedVector2D u, CFixedV
fixed closest = (dv.Absolute() - hh).Absolute(); // horizontal edges
if (-hh < dv && dv < hh) // region I
closest = std::min(closest, (du.Absolute() - hw).Absolute()); // vertical edges
closest = countInsideAsZero ? fixed::Zero() : std::min(closest, (du.Absolute() - hw).Absolute()); // vertical edges
return closest;
}

View File

@ -32,51 +32,67 @@ namespace Geometry
{
/**
* Checks if a point is inside the given rotated square or rectangle.
* Checks if a point is inside the given rotated rectangle.
* Points precisely on an edge are considered to be inside.
*
* @note Currently assumes the @p u and @p v vectors are perpendicular.
* @param point point vector of the point that is to be tested relative to the origin (center) of the shape.
* @param u rotated X axis unit vector relative to the absolute XZ plane. Indicates the orientation of the rectangle. If not rotated,
* this value is the absolute X axis unit vector (1,0). If rotated by angle theta, this should be (cos theta, -sin theta), as
* the absolute Z axis points down in the unit circle.
* @param v rotated Z axis unit vector relative to the absolute XZ plane. Indicates the orientation of the rectangle. If not rotated,
* this value is the absolute Z axis unit vector (0,1). If rotated by angle theta, this should be (sin theta, cos theta), as
* the absolute Z axis points down in the unit circle.
* @param halfSize Holds half the dimensions of the shape along the u and v vectors, respectively.
* The rectangle is defined by the four vertexes
* (+/-u*halfSize.X +/-v*halfSize.Y).
*
* @return true if @p point is inside the square with rotated X axis unit vector @p u and rotated Z axis unit vector @p v,
* and half dimensions specified by @p halfSizes.
* The @p u and @p v vectors must be perpendicular.
*/
bool PointIsInSquare(CFixedVector2D point, CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize);
CFixedVector2D GetHalfBoundingBox(CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize);
fixed DistanceToSquare(CFixedVector2D point, CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize);
bool PointIsInSquare(CFixedVector2D point,
CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize);
/**
* Given a circle of radius @p radius, and a chord of length @p chordLength on this circle, computes the central angle formed by
* Returns a vector (bx,by) such that every point inside
* the given rotated rectangle has coordinates
* (x,y) with -bx <= x <= bx, -by <= y < by.
*
* The rectangle is defined by the four vertexes
* (+/-u*halfSize.X +/-v*halfSize.Y).
*/
CFixedVector2D GetHalfBoundingBox(CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize);
/**
* Returns the minimum Euclidean distance from the given point to
* any point on the boundary of the given rotated rectangle.
*
* If @p countInsideAsZero is true, and the point is inside the rectangle,
* it will return 0.
* If @p countInsideAsZero is false, the (positive) distance to the boundary
* will be returned regardless of where the point is.
*
* The rectangle is defined by the four vertexes
* (+/-u*halfSize.X +/-v*halfSize.Y).
*
* The @p u and @p v vectors must be perpendicular and unit length.
*/
fixed DistanceToSquare(CFixedVector2D point,
CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize,
bool countInsideAsZero = false);
/**
* Returns a point on the boundary of the given rotated rectangle
* that is closest (or equally closest) to the given point
* in Euclidean distance.
*
* The rectangle is defined by the four vertexes
* (+/-u*halfSize.X +/-v*halfSize.Y).
*
* The @p u and @p v vectors must be perpendicular and unit length.
*/
CFixedVector2D NearestPointOnSquare(CFixedVector2D point,
CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize);
/**
* Given a circle of radius @p radius, and a chord of length @p chordLength
* on this circle, computes the central angle formed by
* connecting the chord's endpoints to the center of the circle.
*
* @param radius Radius of the circle; must be strictly positive.
*/
float ChordToCentralAngle(const float chordLength, const float radius);
/**
* Find point closest to the given point on the edge of the given square or rectangle.
*
* @note Currently assumes the @p u and @p v vectors are perpendicular.
* @param point point vector of the point we want to get the nearest edge point for, relative to the origin (center) of the shape.
* @param u rotated X axis unit vector, relative to the absolute XZ plane. Indicates the orientation of the shape. If not rotated,
* this value is the absolute X axis unit vector (1,0). If rotated by angle theta, this should be (cos theta, -sin theta).
* @param v rotated Z axis unit vector, relative to the absolute XZ plane. Indicates the orientation of the shape. If not rotated,
* this value is the absolute Z axis unit vector (0,1). If rotated by angle theta, this should be (sin theta, cos theta).
* @param halfSize Holds half the dimensions of the shape along the u and v vectors, respectively.
*
* @return point that is closest to @p point on the edge of the square specified by orientation unit vectors @p u and @p v and half
* dimensions @p halfSize, relative to the center of the square
*/
CFixedVector2D NearestPointOnSquare(CFixedVector2D point, CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize);
bool TestRaySquare(CFixedVector2D a, CFixedVector2D b, CFixedVector2D u, CFixedVector2D v, CFixedVector2D halfSize);
bool TestRayAASquare(CFixedVector2D a, CFixedVector2D b, CFixedVector2D halfSize);

View File

@ -0,0 +1,622 @@
/* Copyright (C) 2015 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/Overlay.h"
#include "ps/Profile.h"
// Find the root ID of a region, used by InitRegions
inline u16 RootID(u16 x, const std::vector<u16>& v)
{
// Just add a basic check for infinite loops
int checkLoop = 0;
while (v[x] < x)
{
++checkLoop;
ENSURE(checkLoop < 1000 && "Long loop (probably infinite), unable to find an invariant point");
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;
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)
{
u16 id0 = RootID(DownID, connect);
u16 id1 = RootID(LeftID, connect);
if (id0 < id1)
connect[id1] = id0;
else if (id0 > id1)
connect[id0] = id1;
}
}
else if (DownID > 0)
*pCurrentID = DownID;
else
{
// New ID
*pCurrentID = ++regionID;
connect.push_back(regionID);
}
}
}
// Directly point the root ID
m_NumRegions = 0;
for (u16 i = regionID; i > 0; --i)
{
if (connect[i] == i)
++m_NumRegions;
else
connect[i] = RootID(i,connect);
}
// Replace IDs by the root ID
for (int i = 0; i < CHUNK_SIZE; ++i)
for (int j = 0; j < CHUNK_SIZE; ++j)
m_Regions[i][j] = connect[m_Regions[i][j]];
}
/**
* 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 whether any navcell in the given region is inside the goal.
*/
bool HierarchicalPathfinder::Chunk::RegionContainsGoal(u16 r, const PathGoal& goal) const
{
// Inefficiently check every single navcell:
for (u16 j = 0; j < CHUNK_SIZE; ++j)
{
for (u16 i = 0; i < CHUNK_SIZE; ++i)
{
if (m_Regions[j][i] == r)
{
if (goal.NavcellContainsGoal(m_ChunkI * CHUNK_SIZE + i, m_ChunkJ * CHUNK_SIZE + j))
return true;
}
}
}
return false;
}
/**
* 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)
{
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;
}
}
}
}
}
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::Recompute(const std::map<std::string, pass_class_t>& passClassMasks, Grid<NavcellData>* grid)
{
PROFILE3("Hierarchical Recompute");
m_PassClassMasks = passClassMasks;
m_W = grid->m_W;
m_H = grid->m_H;
// 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;
ENSURE(m_ChunksW < 256 && m_ChunksH < 256); // else the u8 Chunk::m_ChunkI will overflow
m_Chunks.clear();
m_Edges.clear();
for (auto& passClassMask : passClassMasks)
{
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];
for (int cj = 0; cj < m_ChunksH; ++cj)
{
for (int ci = 0; ci < m_ChunksW; ++ci)
{
FindEdges(ci, cj, passClass, edges);
}
}
}
if (m_DebugOverlay)
{
PROFILE("debug overlay");
m_DebugOverlayLines.clear();
AddDebugEdges(GetPassabilityClass("default"));
}
}
void HierarchicalPathfinder::Update(Grid<NavcellData>* grid, const Grid<u8>* dirtinessGrid)
{
PROFILE3("Hierarchical Update");
std::vector<std::pair<int, int> > processedChunks;
for (int j = 0; j < dirtinessGrid->m_H; ++j)
{
for (int i = 0; i < dirtinessGrid->m_W; ++i)
{
if (!dirtinessGrid->get(i, j))
continue;
std::pair<int, int> chunkID(i / CHUNK_SIZE, j / CHUNK_SIZE);
for (auto& passClassMask : m_PassClassMasks)
{
pass_class_t passClass = passClassMask.second;
Chunk& a = m_Chunks[passClass].at(chunkID.second*m_ChunksW + chunkID.first);
if (std::find(processedChunks.begin(), processedChunks.end(), chunkID) == processedChunks.end())
{
processedChunks.push_back(chunkID);
a.InitRegions(chunkID.first, chunkID.second, grid, passClass);
}
}
}
}
// TODO: Also be clever with edges
m_Edges.clear();
for (auto& passClassMask : m_PassClassMasks)
{
pass_class_t passClass = passClassMask.second;
EdgesMap& edges = m_Edges[passClass];
for (int cj = 0; cj < m_ChunksH; ++cj)
{
for (int ci = 0; ci < m_ChunksW; ++ci)
{
FindEdges(ci, cj, passClass, edges);
}
}
}
if (m_DebugOverlay)
{
PROFILE("debug overlay");
m_DebugOverlayLines.clear();
AddDebugEdges(GetPassabilityClass("default"));
}
}
/**
* Find edges between regions in this chunk and the adjacent below/left chunks.
*/
void HierarchicalPathfinder::FindEdges(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);
// 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.)
if (ci > 0)
{
Chunk& b = chunks.at(cj*m_ChunksW + (ci-1));
for (int j = 0; j < CHUNK_SIZE; ++j)
{
RegionID ra = a.Get(0, j);
RegionID rb = b.Get(CHUNK_SIZE-1, j);
if (ra.r && rb.r)
{
edges[ra].insert(rb);
edges[rb].insert(ra);
}
}
}
if (cj > 0)
{
Chunk& b = chunks.at((cj-1)*m_ChunksW + ci);
for (int i = 0; i < CHUNK_SIZE; ++i)
{
RegionID ra = a.Get(i, 0);
RegionID rb = b.Get(i, CHUNK_SIZE-1);
if (ra.r && rb.r)
{
edges[ra].insert(rb);
edges[rb].insert(ra);
}
}
}
}
/**
* 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);
}
}
}
HierarchicalPathfinder::RegionID HierarchicalPathfinder::Get(u16 i, u16 j, pass_class_t passClass)
{
int ci = i / CHUNK_SIZE;
int cj = j / CHUNK_SIZE;
ENSURE(ci < m_ChunksW && cj < m_ChunksH);
return m_Chunks[passClass][cj*m_ChunksW + ci].Get(i % CHUNK_SIZE, j % CHUNK_SIZE);
}
bool HierarchicalPathfinder::MakeGoalReachable(u16 i0, u16 j0, PathGoal& goal, pass_class_t passClass)
{
RegionID source = Get(i0, j0, passClass);
// Find everywhere that's reachable
std::set<RegionID> reachableRegions;
FindReachableRegions(source, reachableRegions, passClass);
// Check whether any reachable region contains the goal
for (const RegionID& region : reachableRegions)
{
// Skip region if its chunk doesn't contain the goal area
entity_pos_t x0 = Pathfinding::NAVCELL_SIZE * (region.ci * CHUNK_SIZE);
entity_pos_t z0 = Pathfinding::NAVCELL_SIZE * (region.cj * CHUNK_SIZE);
entity_pos_t x1 = x0 + Pathfinding::NAVCELL_SIZE * CHUNK_SIZE;
entity_pos_t z1 = z0 + Pathfinding::NAVCELL_SIZE * CHUNK_SIZE;
if (!goal.RectContainsGoal(x0, z0, x1, z1))
continue;
// If the region contains the goal area, the goal is reachable
// and we don't need to move it
if (GetChunk(region.ci, region.cj, passClass).RegionContainsGoal(region.r, goal))
return false;
}
// The goal area wasn't reachable,
// so find the navcell that's nearest to the goal's center
u16 iGoal, jGoal;
Pathfinding::NearestNavcell(goal.x, goal.z, iGoal, jGoal, m_W, m_H);
FindNearestNavcellInRegions(reachableRegions, iGoal, jGoal, passClass);
// Construct a new point goal at the nearest reachable navcell
PathGoal newGoal;
newGoal.type = PathGoal::POINT;
Pathfinding::NavcellCenter(iGoal, jGoal, newGoal.x, newGoal.z);
goal = newGoal;
return true;
}
void HierarchicalPathfinder::FindNearestPassableNavcell(u16& i, u16& j, pass_class_t passClass)
{
std::set<RegionID> regions;
FindPassableRegions(regions, passClass);
FindNearestNavcellInRegions(regions, i, j, passClass);
}
void HierarchicalPathfinder::FindNearestNavcellInRegions(const std::set<RegionID>& regions, u16& iGoal, u16& jGoal, pass_class_t passClass)
{
// Find the navcell in the given regions that's nearest to the goal navcell:
// * For each region, record the (squared) minimal distance to the goal point
// * Sort regions by that underestimated distance
// * For each region, find the actual nearest navcell
// * Stop when the underestimated distances are worse than the best real distance
std::vector<std::pair<u32, RegionID> > regionDistEsts; // pair of (distance^2, region)
for (const RegionID& region : regions)
{
int i0 = region.ci * CHUNK_SIZE;
int j0 = region.cj * CHUNK_SIZE;
int i1 = i0 + CHUNK_SIZE - 1;
int j1 = j0 + CHUNK_SIZE - 1;
// Pick the point in the chunk nearest the goal
int iNear = Clamp((int)iGoal, i0, i1);
int jNear = Clamp((int)jGoal, j0, j1);
int dist2 = (iNear - iGoal)*(iNear - iGoal)
+ (jNear - jGoal)*(jNear - jGoal);
regionDistEsts.emplace_back(dist2, region);
}
// Sort by increasing distance (tie-break on RegionID)
std::sort(regionDistEsts.begin(), regionDistEsts.end());
int iBest = iGoal;
int jBest = jGoal;
u32 dist2Best = std::numeric_limits<u32>::max();
for (auto& pair : regionDistEsts)
{
if (pair.first >= dist2Best)
break;
RegionID region = pair.second;
int i, j;
u32 dist2;
GetChunk(region.ci, region.cj, passClass).RegionNavcellNearest(region.r, iGoal, jGoal, i, j, dist2);
if (dist2 < dist2Best)
{
iBest = i;
jBest = j;
dist2Best = dist2;
}
}
iGoal = iBest;
jGoal = jBest;
}
void HierarchicalPathfinder::FindReachableRegions(RegionID from, std::set<RegionID>& reachable, pass_class_t passClass)
{
// Flood-fill the region graph, starting at 'from',
// collecting all the regions that are reachable via edges
std::vector<RegionID> open;
open.push_back(from);
reachable.insert(from);
while (!open.empty())
{
RegionID curr = open.back();
open.pop_back();
for (const RegionID& region : m_Edges[passClass][curr])
// Add to the reachable set; if this is the first time we added
// it then also add it to the open list
if (reachable.insert(region).second)
open.push_back(region);
}
}
void HierarchicalPathfinder::FindPassableRegions(std::set<RegionID>& regions, pass_class_t passClass)
{
// Construct a set of all regions of all chunks for this pass class
for (const Chunk& chunk : m_Chunks[passClass])
{
// region 0 is impassable tiles
for (int r = 1; r <= chunk.m_NumRegions; ++r)
regions.insert(RegionID(chunk.m_ChunkI, chunk.m_ChunkJ, r));
}
}
Grid<u16> HierarchicalPathfinder::GetConnectivityGrid(pass_class_t passClass)
{
Grid<u16> connectivityGrid(m_W, m_H);
connectivityGrid.reset();
u16 idx = 1;
for (size_t j1 = 0; j1 < m_H; ++j1)
{
for (size_t i1 = 0; i1 < m_W; ++i1)
{
if (connectivityGrid.get(i1, j1) != 0)
continue;
RegionID region = Get(i1, j1, passClass);
if (region.r == 0)
continue;
std::set<RegionID> reachable;
FindReachableRegions(region, reachable, passClass);
for (size_t j2 = 0; j2 < m_H; ++j2)
{
for (size_t i2 = 0; i2 < m_W; ++i2)
{
if (std::find(reachable.begin(), reachable.end(), Get(i2, j2, passClass)) != reachable.end())
connectivityGrid.set(i1, j1, idx);
}
}
++idx;
}
}
return connectivityGrid;
}

View File

@ -0,0 +1,215 @@
/* Copyright (C) 2015 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/>.
*/
#ifndef INCLUDED_HIERPATHFINDER
#define INCLUDED_HIERPATHFINDER
#include "Pathfinding.h"
#include "renderer/TerrainOverlay.h"
#include "Render.h"
#include "graphics/SColor.h"
/**
* Hierarchical pathfinder.
*
* It doesn't find shortest paths, but deals with connectivity.
*
* The navcell-grid representation of the map is split into fixed-size chunks.
* Within a chunk, each maximal set of adjacently-connected passable navcells
* is defined as a region.
* Each region is a vertex in the hierarchical pathfinder's graph.
* When two regions in adjacent chunks are connected by passable navcells,
* the graph contains an edge between the corresponding two vertexes.
* (There will never be an edge between two regions in the same chunk.)
*
* Since regions are typically fairly large, it is possible to determine
* connectivity between any two navcells by mapping them onto their appropriate
* region and then doing a relatively small graph search.
*/
class HierarchicalOverlay;
class HierarchicalPathfinder
{
public:
struct RegionID
{
u8 ci, cj; // chunk ID
u16 r; // unique-per-chunk local region ID
RegionID(u8 ci, u8 cj, u16 r) : ci(ci), cj(cj), r(r) { }
bool operator<(RegionID b) const
{
// Sort by chunk ID, then by per-chunk region ID
if (ci < b.ci)
return true;
if (b.ci < ci)
return false;
if (cj < b.cj)
return true;
if (b.cj < cj)
return false;
return r < b.r;
}
bool operator==(RegionID b) const
{
return ((ci == b.ci) && (cj == b.cj) && (r == b.r));
}
};
HierarchicalPathfinder();
~HierarchicalPathfinder();
void SetDebugOverlay(bool enabled, const CSimContext* simContext);
void Recompute(const std::map<std::string, pass_class_t>& passClassMasks, Grid<NavcellData>* passabilityGrid);
void Update(Grid<NavcellData>* grid, const Grid<u8>* dirtinessGrid);
RegionID Get(u16 i, u16 j, pass_class_t passClass);
/**
* Updates @p goal so that it's guaranteed to be reachable from the navcell
* @p i0, @p j0 (which is assumed to be on a passable navcell).
* If any part of the goal area is already reachable then
* nothing is changed; otherwise the goal is replaced with a point goal
* at the nearest reachable navcell to the original goal's center.
* Returns true if the goal was replaced.
*/
bool MakeGoalReachable(u16 i0, u16 j0, PathGoal& goal, pass_class_t passClass);
/**
* Updates @p i, @p j (which is assumed to be an impassable navcell)
* to the nearest passable navcell.
*/
void FindNearestPassableNavcell(u16& i, u16& j, pass_class_t passClass);
/**
* Generates the connectivity grid associated with the given pass_class
*/
Grid<u16> GetConnectivityGrid(pass_class_t passClass);
pass_class_t GetPassabilityClass(const std::string& name) const
{
auto it = m_PassClassMasks.find(name);
if (it != m_PassClassMasks.end())
return it->second;
LOGERROR("Invalid passability class name '%s'", name.c_str());
return 0;
}
private:
static const u8 CHUNK_SIZE = 96; // number of navcells per side
// TODO PATHFINDER: figure out best number. Probably 64 < n < 128
struct Chunk
{
u8 m_ChunkI, m_ChunkJ; // chunk ID
u16 m_NumRegions; // number of local region IDs (starting from 1)
u16 m_Regions[CHUNK_SIZE][CHUNK_SIZE]; // local region ID per navcell
cassert(CHUNK_SIZE*CHUNK_SIZE/2 < 65536); // otherwise we could overflow m_NumRegions with a checkerboard pattern
void InitRegions(int ci, int cj, Grid<NavcellData>* grid, pass_class_t passClass);
RegionID Get(int i, int j) const;
void RegionCenter(u16 r, int& i, int& j) const;
bool RegionContainsGoal(u16 r, const PathGoal& goal) const;
void RegionNavcellNearest(u16 r, int iGoal, int jGoal, int& iBest, int& jBest, u32& dist2Best) const;
};
typedef std::map<RegionID, std::set<RegionID> > EdgesMap;
void FindEdges(u8 ci, u8 cj, pass_class_t passClass, EdgesMap& edges);
void FindReachableRegions(RegionID from, std::set<RegionID>& reachable, pass_class_t passClass);
void FindPassableRegions(std::set<RegionID>& regions, pass_class_t passClass);
/**
* Updates @p iGoal and @p jGoal to the navcell that is the nearest to the
* initial goal coordinates, in one of the given @p regions.
* (Assumes @p regions is non-empty.)
*/
void FindNearestNavcellInRegions(const std::set<RegionID>& regions, u16& iGoal, u16& jGoal, pass_class_t passClass);
Chunk& GetChunk(u8 ci, u8 cj, pass_class_t passClass)
{
return m_Chunks[passClass].at(cj * m_ChunksW + ci);
}
u16 m_W, m_H;
u16 m_ChunksW, m_ChunksH;
std::map<pass_class_t, std::vector<Chunk> > m_Chunks;
std::map<pass_class_t, EdgesMap> m_Edges;
std::map<std::string, pass_class_t> m_PassClassMasks;
void AddDebugEdges(pass_class_t passClass);
HierarchicalOverlay* m_DebugOverlay;
const CSimContext* m_SimContext; // Used for drawing the debug lines
public:
std::vector<SOverlayLine> m_DebugOverlayLines;
};
class HierarchicalOverlay : public TerrainTextureOverlay
{
public:
HierarchicalPathfinder& m_PathfinderHier;
HierarchicalOverlay(HierarchicalPathfinder& pathfinderHier) :
TerrainTextureOverlay(Pathfinding::NAVCELLS_PER_TILE), m_PathfinderHier(pathfinderHier)
{
}
virtual void BuildTextureRGBA(u8* data, size_t w, size_t h)
{
pass_class_t passClass = m_PathfinderHier.GetPassabilityClass("default");
for (size_t j = 0; j < h; ++j)
{
for (size_t i = 0; i < w; ++i)
{
SColor4ub color;
HierarchicalPathfinder::RegionID rid = m_PathfinderHier.Get(i, j, passClass);
if (rid.r == 0)
color = SColor4ub(0, 0, 0, 0);
else if (rid.r == 0xFFFF)
color = SColor4ub(255, 0, 255, 255);
else
color = GetColor(rid.r + rid.ci*5 + rid.cj*7, 127);
*data++ = color.R;
*data++ = color.G;
*data++ = color.B;
*data++ = color.A;
}
}
}
};
#endif // INCLUDED_HIERPATHFINDER

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,424 @@
/* Copyright (C) 2015 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/>.
*/
#ifndef INCLUDED_LONGPATHFINDER
#define INCLUDED_LONGPATHFINDER
#include "Pathfinding.h"
#include "HierarchicalPathfinder.h"
#include "PriorityQueue.h"
#include "graphics/Overlay.h"
#include "renderer/Scene.h"
#define ACCEPT_DIAGONAL_GAPS 0
/**
* Represents the 2D coordinates of a tile.
* The i/j components are packed into a single u32, since we usually use these
* objects for equality comparisons and the VC2010 optimizer doesn't seem to automatically
* compare two u16s in a single operation.
* TODO: maybe VC2012 will?
*/
struct TileID
{
TileID() { }
TileID(u16 i, u16 j) : data((i << 16) | j) { }
bool operator==(const TileID& b) const
{
return data == b.data;
}
/// Returns lexicographic order over (i,j)
bool operator<(const TileID& b) const
{
return data < b.data;
}
u16 i() const { return data >> 16; }
u16 j() const { return data & 0xFFFF; }
private:
u32 data;
};
/**
* Tile data for A* computation.
* (We store an array of one of these per terrain tile, so it ought to be optimised for size)
*/
struct PathfindTile
{
public:
enum {
STATUS_UNEXPLORED = 0,
STATUS_OPEN = 1,
STATUS_CLOSED = 2
};
bool IsUnexplored() { return GetStatus() == STATUS_UNEXPLORED; }
bool IsOpen() { return GetStatus() == STATUS_OPEN; }
bool IsClosed() { return GetStatus() == STATUS_CLOSED; }
void SetStatusOpen() { SetStatus(STATUS_OPEN); }
void SetStatusClosed() { SetStatus(STATUS_CLOSED); }
// Get pi,pj coords of predecessor to this tile on best path, given i,j coords of this tile
inline int GetPredI(int i) { return i + GetPredDI(); }
inline int GetPredJ(int j) { return j + GetPredDJ(); }
inline PathCost GetCost() const { return g; }
inline void SetCost(PathCost cost) { g = cost; }
private:
PathCost g; // cost to reach this tile
u32 data; // 2-bit status; 15-bit PredI; 15-bit PredJ; packed for storage efficiency
public:
inline u8 GetStatus() const
{
return data & 3;
}
inline void SetStatus(u8 s)
{
ASSERT(s < 4);
data &= ~3;
data |= (s & 3);
}
int GetPredDI() const
{
return (i32)data >> 17;
}
int GetPredDJ() const
{
return ((i32)data << 15) >> 17;
}
// Set the pi,pj coords of predecessor, given i,j coords of this tile
inline void SetPred(int pi, int pj, int i, int j)
{
int di = pi - i;
int dj = pj - j;
ASSERT(-16384 <= di && di < 16384);
ASSERT(-16384 <= dj && dj < 16384);
data &= 3;
data |= (((u32)di & 0x7FFF) << 17) | (((u32)dj & 0x7FFF) << 2);
}
};
struct CircularRegion
{
CircularRegion(entity_pos_t x, entity_pos_t z, entity_pos_t r) : x(x), z(z), r(r) {}
entity_pos_t x, z, r;
};
typedef PriorityQueueHeap<TileID, PathCost, PathCost> PriorityQueue;
typedef SparseGrid<PathfindTile> PathfindTileGrid;
class JumpPointCache;
struct PathfinderState
{
u32 steps; // number of algorithm iterations
PathGoal goal;
u16 iGoal, jGoal; // goal tile
pass_class_t passClass;
PriorityQueue open;
// (there's no explicit closed list; it's encoded in PathfindTile)
PathfindTileGrid* tiles;
Grid<NavcellData>* terrain;
PathCost hBest; // heuristic of closest discovered tile to goal
u16 iBest, jBest; // closest tile
JumpPointCache* jpc;
};
class LongOverlay;
class LongPathfinder
{
public:
LongPathfinder()
{
m_UseJPSCache = false;
m_Grid = NULL;
m_GridSize = 0;
m_DebugOverlay = NULL;
m_DebugGrid = NULL;
m_DebugPath = NULL;
}
~LongPathfinder()
{
SAFE_DELETE(m_DebugOverlay);
SAFE_DELETE(m_DebugGrid);
SAFE_DELETE(m_DebugPath);
}
void SetDebugOverlay(bool enabled);
void SetHierDebugOverlay(bool enabled, const CSimContext *simContext)
{
m_PathfinderHier.SetDebugOverlay(enabled, simContext);
}
void SetDebugPath(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass)
{
if (!m_DebugOverlay)
return;
SAFE_DELETE(m_DebugGrid);
delete m_DebugPath;
m_DebugPath = new WaypointPath();
ComputePath(x0, z0, goal, passClass, *m_DebugPath);
m_DebugPassClass = passClass;
}
void Reload(std::map<std::string, pass_class_t> passClassMasks, Grid<NavcellData>* passabilityGrid)
{
m_Grid = passabilityGrid;
ASSERT(passabilityGrid->m_H == passabilityGrid->m_W);
m_GridSize = passabilityGrid->m_W;
m_JumpPointCache.clear();
m_PathfinderHier.Recompute(passClassMasks, passabilityGrid);
}
void Update(Grid<NavcellData>* passabilityGrid, const Grid<u8>* dirtinessGrid)
{
m_Grid = passabilityGrid;
ASSERT(passabilityGrid->m_H == passabilityGrid->m_W);
ASSERT(m_GridSize == passabilityGrid->m_H);
m_JumpPointCache.clear();
m_PathfinderHier.Update(passabilityGrid, dirtinessGrid);
}
void HierarchicalRenderSubmit(SceneCollector& collector)
{
for (size_t i = 0; i < m_PathfinderHier.m_DebugOverlayLines.size(); ++i)
collector.Submit(&m_PathfinderHier.m_DebugOverlayLines[i]);
}
/**
* Compute a tile-based path from the given point to the goal, and return the set of waypoints.
* The waypoints correspond to the centers of horizontally/vertically adjacent tiles
* along the path.
*/
void ComputePath(entity_pos_t x0, entity_pos_t z0, const PathGoal& origGoal,
pass_class_t passClass, WaypointPath& path)
{
if (!m_Grid)
{
LOGERROR("The pathfinder grid hasn't been setup yet, aborting ComputePath");
return;
}
ComputeJPSPath(x0, z0, origGoal, passClass, path);
}
/**
* Compute a tile-based path from the given point to the goal, excluding the regions
* specified in excludedRegions (which are treated as impassable) and return the set of waypoints.
* The waypoints correspond to the centers of horizontally/vertically adjacent tiles
* along the path.
*/
void ComputePath(entity_pos_t x0, entity_pos_t z0, const PathGoal& origGoal,
pass_class_t passClass, std::vector<CircularRegion> excludedRegions, WaypointPath& path);
Grid<u16> GetConnectivityGrid(pass_class_t passClass)
{
return m_PathfinderHier.GetConnectivityGrid(passClass);
}
void GetDebugData(u32& steps, double& time, Grid<u8>& grid)
{
GetDebugDataJPS(steps, time, grid);
}
/*
* Checks that the line (x0,z0)-(x1,z1) does not intersect any impassable navcells.
*/
bool CheckLineMovement(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, pass_class_t passClass);
Grid<NavcellData>* m_Grid;
u16 m_GridSize;
// Debugging - output from last pathfind operation:
LongOverlay* m_DebugOverlay;
PathfindTileGrid* m_DebugGrid;
u32 m_DebugSteps;
double m_DebugTime;
PathGoal m_DebugGoal;
WaypointPath* m_DebugPath;
pass_class_t m_DebugPassClass;
private:
PathCost CalculateHeuristic(int i, int j, int iGoal, int jGoal);
void ProcessNeighbour(int pi, int pj, int i, int j, PathCost pg, PathfinderState& state);
/**
* JPS algorithm helper functions
* @param detectGoal is not used if m_UseJPSCache is true
*/
void AddJumpedHoriz(int i, int j, int di, PathCost g, PathfinderState& state, bool detectGoal);
int HasJumpedHoriz(int i, int j, int di, PathfinderState& state, bool detectGoal);
void AddJumpedVert(int i, int j, int dj, PathCost g, PathfinderState& state, bool detectGoal);
int HasJumpedVert(int i, int j, int dj, PathfinderState& state, bool detectGoal);
void AddJumpedDiag(int i, int j, int di, int dj, PathCost g, PathfinderState& state);
/**
* See LongPathfinder.cpp for implementation details
* TODO: cleanup documentation
*/
void ComputeJPSPath(entity_pos_t x0, entity_pos_t z0, const PathGoal& origGoal, pass_class_t passClass, WaypointPath& path);
void GetDebugDataJPS(u32& steps, double& time, Grid<u8>& grid);
// Helper functions for ComputePath
/**
* Same kind of interface as ComputePath, but works when the unit is starting
* on an impassable navcell.
* Returns a path heading directly to the nearest passable navcell, then the goal.
*/
void ComputePathOffImpassable(entity_pos_t x0, entity_pos_t z0, const PathGoal& origGoal, pass_class_t passClass, WaypointPath& path);
/**
* Given a path with an arbitrary collection of waypoints, updates the
* waypoints to be nicer. Calls "Testline" between waypoints
* so that bended paths can become straight if there's nothing in between
* (this happens because A* is 8-direction, and the map isn't actually a grid).
* If @param maxDist is non-zero, path waypoints will be espaced by at most @param maxDist.
* In that case the distance between (x0, z0) and the first waypoint will also be made less than maxDist.
*/
void ImprovePathWaypoints(WaypointPath& path, pass_class_t passClass, entity_pos_t maxDist, entity_pos_t x0, entity_pos_t z0);
/**
* Generate a passability map, stored in the 16th bit of navcells, based on passClass,
* but with a set of impassable circular regions.
*/
void GenerateSpecialMap(pass_class_t passClass, std::vector<CircularRegion> excludedRegions);
bool m_UseJPSCache;
std::map<pass_class_t, shared_ptr<JumpPointCache> > m_JumpPointCache;
HierarchicalPathfinder m_PathfinderHier;
};
/**
* Terrain overlay for pathfinder debugging.
* Renders a representation of the most recent pathfinding operation.
*/
class LongOverlay : public TerrainTextureOverlay
{
public:
LongPathfinder& m_Pathfinder;
LongOverlay(LongPathfinder& pathfinder) :
TerrainTextureOverlay(Pathfinding::NAVCELLS_PER_TILE), m_Pathfinder(pathfinder)
{
}
virtual void BuildTextureRGBA(u8* data, size_t w, size_t h)
{
// Grab the debug data for the most recently generated path
u32 steps;
double time;
Grid<u8> debugGrid;
m_Pathfinder.GetDebugData(steps, time, debugGrid);
// Render navcell passability
u8* p = data;
for (size_t j = 0; j < h; ++j)
{
for (size_t i = 0; i < w; ++i)
{
SColor4ub color(0, 0, 0, 0);
if (!IS_PASSABLE(m_Pathfinder.m_Grid->get((int)i, (int)j), m_Pathfinder.m_DebugPassClass))
color = SColor4ub(255, 0, 0, 127);
if (debugGrid.m_W && debugGrid.m_H)
{
u8 n = debugGrid.get((int)i, (int)j);
if (n == 1)
color = SColor4ub(255, 255, 0, 127);
else if (n == 2)
color = SColor4ub(0, 255, 0, 127);
if (m_Pathfinder.m_DebugGoal.NavcellContainsGoal(i, j))
color = SColor4ub(0, 0, 255, 127);
}
*p++ = color.R;
*p++ = color.G;
*p++ = color.B;
*p++ = color.A;
}
}
// Render the most recently generated path
if (m_Pathfinder.m_DebugPath && !m_Pathfinder.m_DebugPath->m_Waypoints.empty())
{
std::vector<Waypoint>& waypoints = m_Pathfinder.m_DebugPath->m_Waypoints;
u16 ip = 0, jp = 0;
for (size_t k = 0; k < waypoints.size(); ++k)
{
u16 i, j;
Pathfinding::NearestNavcell(waypoints[k].x, waypoints[k].z, i, j, m_Pathfinder.m_GridSize, m_Pathfinder.m_GridSize);
if (k == 0)
{
ip = i;
jp = j;
}
else
{
bool firstCell = true;
do
{
if (data[(jp*w + ip)*4+3] == 0)
{
data[(jp*w + ip)*4+0] = 0xFF;
data[(jp*w + ip)*4+1] = 0xFF;
data[(jp*w + ip)*4+2] = 0xFF;
data[(jp*w + ip)*4+3] = firstCell ? 0xA0 : 0x60;
}
ip = ip < i ? ip+1 : ip > i ? ip-1 : ip;
jp = jp < j ? jp+1 : jp > j ? jp-1 : jp;
firstCell = false;
}
while (ip != i || jp != j);
}
}
}
}
};
#endif // INCLUDED_LONGPATHFINDER

View File

@ -0,0 +1,302 @@
/* Copyright (C) 2015 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 "PathGoal.h"
#include "graphics/Terrain.h"
#include "Pathfinding.h"
static bool NavcellContainsCircle(int i, int j, fixed x, fixed z, fixed r, bool inside)
{
// Accept any navcell (i,j) that contains a point which is inside[/outside]
// (or on the edge of) the circle
// Get world-space bounds of navcell
entity_pos_t x0 = entity_pos_t::FromInt(i).Multiply(Pathfinding::NAVCELL_SIZE);
entity_pos_t z0 = entity_pos_t::FromInt(j).Multiply(Pathfinding::NAVCELL_SIZE);
entity_pos_t x1 = x0 + Pathfinding::NAVCELL_SIZE;
entity_pos_t z1 = z0 + Pathfinding::NAVCELL_SIZE;
if (inside)
{
// Get the point inside the navcell closest to (x,z)
entity_pos_t nx = Clamp(x, x0, x1);
entity_pos_t nz = Clamp(z, z0, z1);
// Check if that point is inside the circle
return (CFixedVector2D(nx, nz) - CFixedVector2D(x, z)).CompareLength(r) <= 0;
}
else
{
// If any corner of the navcell is outside the circle, return true.
// Otherwise, since the circle is convex, there cannot be any other point
// in the navcell that is outside the circle.
return (
(CFixedVector2D(x0, z0) - CFixedVector2D(x, z)).CompareLength(r) >= 0
|| (CFixedVector2D(x1, z0) - CFixedVector2D(x, z)).CompareLength(r) >= 0
|| (CFixedVector2D(x0, z1) - CFixedVector2D(x, z)).CompareLength(r) >= 0
|| (CFixedVector2D(x1, z1) - CFixedVector2D(x, z)).CompareLength(r) >= 0
);
}
}
static bool NavcellContainsSquare(int i, int j,
fixed x, fixed z, CFixedVector2D u, CFixedVector2D v, fixed hw, fixed hh,
bool inside)
{
// Accept any navcell (i,j) that contains a point which is inside[/outside]
// (or on the edge of) the square
// Get world-space bounds of navcell
entity_pos_t x0 = entity_pos_t::FromInt(i).Multiply(Pathfinding::NAVCELL_SIZE);
entity_pos_t z0 = entity_pos_t::FromInt(j).Multiply(Pathfinding::NAVCELL_SIZE);
entity_pos_t x1 = x0 + Pathfinding::NAVCELL_SIZE;
entity_pos_t z1 = z0 + Pathfinding::NAVCELL_SIZE;
if (inside)
{
// Get the point inside the navcell closest to (x,z)
entity_pos_t nx = Clamp(x, x0, x1);
entity_pos_t nz = Clamp(z, z0, z1);
// Check if that point is inside the circle
return Geometry::PointIsInSquare(CFixedVector2D(nx - x, nz - z), u, v, CFixedVector2D(hw, hh));
}
else
{
// If any corner of the navcell is outside the square, return true.
// Otherwise, since the square is convex, there cannot be any other point
// in the navcell that is outside the square.
return (
Geometry::PointIsInSquare(CFixedVector2D(x0 - x, z0 - z), u, v, CFixedVector2D(hw, hh))
|| Geometry::PointIsInSquare(CFixedVector2D(x1 - x, z0 - z), u, v, CFixedVector2D(hw, hh))
|| Geometry::PointIsInSquare(CFixedVector2D(x0 - x, z1 - z), u, v, CFixedVector2D(hw, hh))
|| Geometry::PointIsInSquare(CFixedVector2D(x1 - x, z1 - z), u, v, CFixedVector2D(hw, hh))
);
}
}
bool PathGoal::NavcellContainsGoal(int i, int j) const
{
switch (type)
{
case POINT:
{
// Only accept a single navcell
int gi = (x >> Pathfinding::NAVCELL_SIZE_LOG2).ToInt_RoundToNegInfinity();
int gj = (z >> Pathfinding::NAVCELL_SIZE_LOG2).ToInt_RoundToNegInfinity();
return gi == i && gj == j;
}
case CIRCLE:
return NavcellContainsCircle(i, j, x, z, hw, true);
case INVERTED_CIRCLE:
return NavcellContainsCircle(i, j, x, z, hw, false);
case SQUARE:
return NavcellContainsSquare(i, j, x, z, u, v, hw, hh, true);
case INVERTED_SQUARE:
return NavcellContainsSquare(i, j, x, z, u, v, hw, hh, false);
NODEFAULT;
}
}
bool PathGoal::NavcellRectContainsGoal(int i0, int j0, int i1, int j1, int* gi, int* gj) const
{
// Get min/max to simplify range checks
int imin = std::min(i0, i1);
int imax = std::max(i0, i1);
int jmin = std::min(j0, j1);
int jmax = std::max(j0, j1);
// Direction to iterate from ij0 towards ij1
int di = i1 < i0 ? -1 : +1;
int dj = j1 < j0 ? -1 : +1;
switch (type)
{
case POINT:
{
// Calculate the navcell that contains the point goal
int i = (x >> Pathfinding::NAVCELL_SIZE_LOG2).ToInt_RoundToNegInfinity();
int j = (z >> Pathfinding::NAVCELL_SIZE_LOG2).ToInt_RoundToNegInfinity();
// If that goal navcell is in the given range, return it
if (imin <= i && i <= imax && jmin <= j && j <= jmax)
{
if (gi)
*gi = i;
if (gj)
*gj = j;
return true;
}
return false;
}
case CIRCLE:
{
// Loop over all navcells in the given range (starting at (i0,j0) since
// this function is meant to find the goal navcell nearest to there
// assuming jmin==jmax || imin==imax),
// and check whether any point in each navcell is within the goal circle.
// (TODO: this is pretty inefficient.)
for (int j = j0; jmin <= j && j <= jmax; j += dj)
{
for (int i = i0; imin <= i && i <= imax; i += di)
{
entity_pos_t x0 = entity_pos_t::FromInt(i).Multiply(Pathfinding::NAVCELL_SIZE);
entity_pos_t z0 = entity_pos_t::FromInt(j).Multiply(Pathfinding::NAVCELL_SIZE);
entity_pos_t x1 = x0 + Pathfinding::NAVCELL_SIZE;
entity_pos_t z1 = z0 + Pathfinding::NAVCELL_SIZE;
entity_pos_t nx = Clamp(x, x0, x1);
entity_pos_t nz = Clamp(z, z0, z1);
if ((CFixedVector2D(nx, nz) - CFixedVector2D(x, z)).CompareLength(hw) <= 0)
{
if (gi)
*gi = i;
if (gj)
*gj = j;
return true;
}
}
}
return false;
}
case SQUARE:
{
// Loop over all navcells in the given range (starting at (i0,j0) since
// this function is meant to find the goal navcell nearest to there
// assuming jmin==jmax || imin==imax),
// and check whether any point in each navcell is within the goal square.
// (TODO: this is pretty inefficient.)
for (int j = j0; jmin <= j && j <= jmax; j += dj)
{
for (int i = i0; imin <= i && i <= imax; i += di)
{
entity_pos_t x0 = entity_pos_t::FromInt(i).Multiply(Pathfinding::NAVCELL_SIZE);
entity_pos_t z0 = entity_pos_t::FromInt(j).Multiply(Pathfinding::NAVCELL_SIZE);
entity_pos_t x1 = x0 + Pathfinding::NAVCELL_SIZE;
entity_pos_t z1 = z0 + Pathfinding::NAVCELL_SIZE;
entity_pos_t nx = Clamp(x, x0, x1);
entity_pos_t nz = Clamp(z, z0, z1);
if (Geometry::PointIsInSquare(CFixedVector2D(nx - x, nz - z), u, v, CFixedVector2D(hw, hh)))
{
if (gi)
*gi = i;
if (gj)
*gj = j;
return true;
}
}
}
return false;
}
case INVERTED_CIRCLE:
case INVERTED_SQUARE:
// Haven't bothered implementing these, since they're not needed by the
// current pathfinder design
debug_warn(L"PathGoal::NavcellRectContainsGoal doesn't support inverted shapes");
return false;
NODEFAULT;
}
}
bool PathGoal::RectContainsGoal(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1) const
{
switch (type)
{
case POINT:
return x0 <= x && x <= x1 && z0 <= z && z <= z1;
case CIRCLE:
{
entity_pos_t nx = Clamp(x, x0, x1);
entity_pos_t nz = Clamp(z, z0, z1);
return (CFixedVector2D(nx, nz) - CFixedVector2D(x, z)).CompareLength(hw) <= 0;
}
case SQUARE:
{
entity_pos_t nx = Clamp(x, x0, x1);
entity_pos_t nz = Clamp(z, z0, z1);
return Geometry::PointIsInSquare(CFixedVector2D(nx - x, nz - z), u, v, CFixedVector2D(hw, hh));
}
case INVERTED_CIRCLE:
case INVERTED_SQUARE:
// Haven't bothered implementing these, since they're not needed by the
// current pathfinder design
debug_warn(L"PathGoal::RectContainsGoal doesn't support inverted shapes");
return false;
NODEFAULT;
}
}
fixed PathGoal::DistanceToPoint(CFixedVector2D pos) const
{
switch (type)
{
case PathGoal::POINT:
return (pos - CFixedVector2D(x, z)).Length();
case PathGoal::CIRCLE:
case PathGoal::INVERTED_CIRCLE:
return ((pos - CFixedVector2D(x, z)).Length() - hw).Absolute();
case PathGoal::SQUARE:
case PathGoal::INVERTED_SQUARE:
{
CFixedVector2D halfSize(hw, hh);
CFixedVector2D d(pos.X - x, pos.Y - z);
return Geometry::DistanceToSquare(d, u, v, halfSize);
}
NODEFAULT;
}
}
CFixedVector2D PathGoal::NearestPointOnGoal(CFixedVector2D pos) const
{
CFixedVector2D g(x, z);
switch (type)
{
case PathGoal::POINT:
return g;
case PathGoal::CIRCLE:
case PathGoal::INVERTED_CIRCLE:
{
CFixedVector2D d = pos - g;
if (d.IsZero())
d = CFixedVector2D(fixed::FromInt(1), fixed::Zero()); // some arbitrary direction
d.Normalize(hw);
return g + d;
}
case PathGoal::SQUARE:
case PathGoal::INVERTED_SQUARE:
{
CFixedVector2D halfSize(hw, hh);
CFixedVector2D d = pos - g;
return g + Geometry::NearestPointOnSquare(d, u, v, halfSize);
}
NODEFAULT;
}
}

View File

@ -0,0 +1,84 @@
/* Copyright (C) 2015 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/>.
*/
#ifndef INCLUDED_PATHGOAL
#define INCLUDED_PATHGOAL
#include "maths/FixedVector2D.h"
#include "simulation2/helpers/Position.h"
/**
* Pathfinder goal.
* The goal can be either a point, a circle, or a square (rectangle).
* For circles/squares, any point inside the shape is considered to be
* part of the goal.
* Also, it can be an 'inverted' circle/square, where any point outside
* the shape is part of the goal.
*/
class PathGoal
{
public:
enum Type {
POINT, // single point
CIRCLE, // the area inside a circle
INVERTED_CIRCLE, // the area outside a circle
SQUARE, // the area inside a square
INVERTED_SQUARE // the area outside a square
} type;
entity_pos_t x, z; // position of center
CFixedVector2D u, v; // if [INVERTED_]SQUARE, then orthogonal unit axes
entity_pos_t hw, hh; // if [INVERTED_]SQUARE, then half width & height; if [INVERTED_]CIRCLE, then hw is radius
entity_pos_t maxdist; // maximum distance wanted between two path waypoints
/**
* Returns true if the given navcell contains a part of the goal area.
*/
bool NavcellContainsGoal(int i, int j) const;
/**
* Returns true if any navcell (i, j) where
* min(i0,i1) <= i <= max(i0,i1)
* min(j0,j1) <= j <= max(j0,j1),
* contains a part of the goal area.
* If so, arguments i and j (if not NULL) are set to the goal navcell nearest
* to (i0, j0), assuming the rect has either width or height = 1.
*/
bool NavcellRectContainsGoal(int i0, int j0, int i1, int j1, int* i, int* j) const;
/**
* Returns true if the rectangle defined by (x0,z0)-(x1,z1) (inclusive)
* contains a part of the goal area.
*/
bool RectContainsGoal(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1) const;
/**
* Returns the minimum distance from the point with the given @p pos
* to any point on the outline of the goal shape.
*/
fixed DistanceToPoint(CFixedVector2D pos) const;
/**
* Returns the coordinates of the point on the goal that is closest to pos in a straight line.
*/
CFixedVector2D NearestPointOnGoal(CFixedVector2D pos) const;
};
#endif // INCLUDED_PATHGOAL

View File

@ -0,0 +1,241 @@
/* Copyright (C) 2015 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/>.
*/
#ifndef INCLUDED_PATHFINDING
#define INCLUDED_PATHFINDING
#include "ps/CLogger.h"
#include "simulation2/system/ParamNode.h"
#include "graphics/Terrain.h"
#include "Geometry.h"
#include "Grid.h"
#include "PathGoal.h"
typedef u16 pass_class_t;
struct Waypoint
{
entity_pos_t x, z;
};
/**
* Returned path.
* Waypoints are in *reverse* order (the earliest is at the back of the list)
*/
struct WaypointPath
{
std::vector<Waypoint> m_Waypoints;
};
/**
* Represents the cost of a path consisting of horizontal/vertical and
* diagonal movements over a uniform-cost grid.
* Maximum path length before overflow is about 45K steps.
*/
struct PathCost
{
PathCost() : data(0) { }
/// Construct from a number of horizontal/vertical and diagonal steps
PathCost(u16 hv, u16 d)
: data(hv * 65536 + d * 92682) // 2^16 * sqrt(2) == 92681.9
{
}
/// Construct for horizontal/vertical movement of given number of steps
static PathCost horizvert(u16 n)
{
return PathCost(n, 0);
}
/// Construct for diagonal movement of given number of steps
static PathCost diag(u16 n)
{
return PathCost(0, n);
}
PathCost operator+(const PathCost& a) const
{
PathCost c;
c.data = data + a.data;
return c;
}
PathCost& operator+=(const PathCost& a)
{
data += a.data;
return *this;
}
bool operator<=(const PathCost& b) const { return data <= b.data; }
bool operator< (const PathCost& b) const { return data < b.data; }
bool operator>=(const PathCost& b) const { return data >= b.data; }
bool operator>(const PathCost& b) const { return data > b.data; }
u32 ToInt()
{
return data;
}
private:
u32 data;
};
typedef u16 NavcellData; // 1 bit per passability class (up to PASS_CLASS_BITS)
static const int PASS_CLASS_BITS = 16;
#define IS_PASSABLE(item, classmask) (((item) & (classmask)) == 0)
#define PASS_CLASS_MASK_FROM_INDEX(id) ((pass_class_t)(1u << id))
#define SPECIAL_PASS_CLASS PASS_CLASS_MASK_FROM_INDEX(PASS_CLASS_BITS) // 16th bit, used for special in-place computations
namespace Pathfinding
{
/**
* The pathfinders operate primarily over a navigation grid (a uniform-cost
* 2D passability grid, with horizontal/vertical (not diagonal) connectivity).
* This is based on the terrain tile passability, plus the rasterized shapes of
* obstructions, all expanded outwards by the radius of the units.
* Since units are much smaller than terrain tiles, the nav grid should be
* higher resolution than the tiles.
* We therefore split each terrain tile into NxN "nav cells" (for some integer N,
* preferably a power of two).
*/
const int NAVCELLS_PER_TILE = 4;
/**
* Size of a navcell in metres ( = TERRAIN_TILE_SIZE / NAVCELLS_PER_TILE)
*/
extern const fixed NAVCELL_SIZE;
const int NAVCELL_SIZE_INT = 1;
const int NAVCELL_SIZE_LOG2 = 0;
/**
* Compute the navcell indexes on the grid nearest to a given point
* w, h are the grid dimensions, i.e. the number of navcells per side
*/
inline void NearestNavcell(entity_pos_t x, entity_pos_t z, u16& i, u16& j, u16 w, u16 h)
{
i = (u16)clamp((x / NAVCELL_SIZE).ToInt_RoundToNegInfinity(), 0, w - 1);
j = (u16)clamp((z / NAVCELL_SIZE).ToInt_RoundToNegInfinity(), 0, h - 1);
}
/**
* Returns the position of the center of the given tile
*/
inline void TileCenter(u16 i, u16 j, entity_pos_t& x, entity_pos_t& z)
{
cassert(TERRAIN_TILE_SIZE % 2 == 0);
x = entity_pos_t::FromInt(i*(int)TERRAIN_TILE_SIZE + (int)TERRAIN_TILE_SIZE / 2);
z = entity_pos_t::FromInt(j*(int)TERRAIN_TILE_SIZE + (int)TERRAIN_TILE_SIZE / 2);
}
inline void NavcellCenter(u16 i, u16 j, entity_pos_t& x, entity_pos_t& z)
{
x = entity_pos_t::FromInt(i * 2 + 1).Multiply(NAVCELL_SIZE / 2);
z = entity_pos_t::FromInt(j * 2 + 1).Multiply(NAVCELL_SIZE / 2);
}
}
/*
* For efficient pathfinding we want to try hard to minimise the per-tile search cost,
* so we precompute the tile passability flags and movement costs for the various different
* types of unit.
* We also want to minimise memory usage (there can easily be 100K tiles so we don't want
* to store many bytes for each).
*
* To handle passability efficiently, we have a small number of passability classes
* (e.g. "infantry", "ship"). Each unit belongs to a single passability class, and
* uses that for all its pathfinding.
* Passability is determined by water depth, terrain slope, forestness, buildingness.
* We need at least one bit per class per tile to represent passability.
*
* We use a separate bit to indicate building obstructions (instead of folding it into
* the class passabilities) so that it can be ignored when doing the accurate short paths.
* We use another bit to indicate tiles near obstructions that block construction,
* for the AI to plan safe building spots.
*/
class PathfinderPassability
{
public:
PathfinderPassability(pass_class_t mask, const CParamNode& node) :
m_Mask(mask)
{
if (node.GetChild("MinWaterDepth").IsOk())
m_MinDepth = node.GetChild("MinWaterDepth").ToFixed();
else
m_MinDepth = std::numeric_limits<fixed>::min();
if (node.GetChild("MaxWaterDepth").IsOk())
m_MaxDepth = node.GetChild("MaxWaterDepth").ToFixed();
else
m_MaxDepth = std::numeric_limits<fixed>::max();
if (node.GetChild("MaxTerrainSlope").IsOk())
m_MaxSlope = node.GetChild("MaxTerrainSlope").ToFixed();
else
m_MaxSlope = std::numeric_limits<fixed>::max();
if (node.GetChild("MinShoreDistance").IsOk())
m_MinShore = node.GetChild("MinShoreDistance").ToFixed();
else
m_MinShore = std::numeric_limits<fixed>::min();
if (node.GetChild("MaxShoreDistance").IsOk())
m_MaxShore = node.GetChild("MaxShoreDistance").ToFixed();
else
m_MaxShore = std::numeric_limits<fixed>::max();
if (node.GetChild("Clearance").IsOk())
{
m_HasClearance = true;
m_Clearance = node.GetChild("Clearance").ToFixed();
if (!(m_Clearance % Pathfinding::NAVCELL_SIZE).IsZero())
{
// If clearance isn't an integer number of navcells then we'll
// probably get weird behaviour when expanding the navcell grid
// by clearance, vs expanding static obstructions by clearance
LOGWARNING("Pathfinder passability class has clearance %f, should be multiple of %f",
m_Clearance.ToFloat(), Pathfinding::NAVCELL_SIZE.ToFloat());
}
}
else
{
m_HasClearance = false;
m_Clearance = fixed::Zero();
}
}
bool IsPassable(fixed waterdepth, fixed steepness, fixed shoredist)
{
return ((m_MinDepth <= waterdepth && waterdepth <= m_MaxDepth) && (steepness < m_MaxSlope) && (m_MinShore <= shoredist && shoredist <= m_MaxShore));
}
pass_class_t m_Mask;
bool m_HasClearance; // whether static obstructions are impassable
fixed m_Clearance; // min distance from static obstructions
private:
fixed m_MinDepth;
fixed m_MaxDepth;
fixed m_MaxSlope;
fixed m_MinShore;
fixed m_MaxShore;
};
#endif // INCLUDED_PATHFINDING

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2010 Wildfire Games.
/* Copyright (C) 2015 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -32,14 +32,17 @@
template <typename Item, typename CMP>
struct QueueItemPriority
{
bool operator()(const Item& a, const Item& b)
bool operator()(const Item& a, const Item& b) const
{
if (CMP()(b.rank, a.rank)) // higher costs are lower priority
return true;
if (CMP()(a.rank, b.rank))
return false;
// Need to tie-break to get a consistent ordering
// TODO: Should probably tie-break on g or h or something, but don't bother for now
if (CMP()(b.h, a.h)) // higher heuristic costs are lower priority
return true;
if (CMP()(a.h, b.h))
return false;
if (a.id < b.id)
return true;
if (b.id < a.id)
@ -57,7 +60,7 @@ struct QueueItemPriority
* This is quite dreadfully slow in MSVC's debug STL implementation,
* so we shouldn't use it unless we reimplement the heap functions more efficiently.
*/
template <typename ID, typename R, typename CMP = std::less<R> >
template <typename ID, typename R, typename H, typename CMP = std::less<R> >
class PriorityQueueHeap
{
public:
@ -65,6 +68,7 @@ public:
{
ID id;
R rank; // f = g+h (estimated total cost of path through here)
H h; // heuristic cost
};
void push(const Item& item)
@ -73,19 +77,10 @@ public:
push_heap(m_Heap.begin(), m_Heap.end(), QueueItemPriority<Item, CMP>());
}
Item* find(ID id)
void promote(ID id, R UNUSED(oldrank), R newrank, H newh)
{
for (size_t n = 0; n < m_Heap.size(); ++n)
{
if (m_Heap[n].id == id)
return &m_Heap[n];
}
return NULL;
}
void promote(ID id, R newrank)
{
for (size_t n = 0; n < m_Heap.size(); ++n)
// Loop backwards since it seems a little faster in practice
for (ssize_t n = m_Heap.size() - 1; n >= 0; --n)
{
if (m_Heap[n].id == id)
{
@ -93,6 +88,7 @@ public:
ENSURE(m_Heap[n].rank > newrank);
#endif
m_Heap[n].rank = newrank;
m_Heap[n].h = newh;
push_heap(m_Heap.begin(), m_Heap.begin()+n+1, QueueItemPriority<Item, CMP>());
return;
}
@ -104,7 +100,7 @@ public:
#if PRIORITYQUEUE_DEBUG
ENSURE(m_Heap.size());
#endif
Item r = m_Heap.front();
Item r = m_Heap[0];
pop_heap(m_Heap.begin(), m_Heap.end(), QueueItemPriority<Item, CMP>());
m_Heap.pop_back();
return r;
@ -120,6 +116,11 @@ public:
return m_Heap.size();
}
void clear()
{
m_Heap.clear();
}
std::vector<Item> m_Heap;
};
@ -130,7 +131,7 @@ public:
* It seems fractionally slower than a binary heap in optimised builds, but is
* much simpler and less susceptible to MSVC's painfully slow debug STL.
*/
template <typename ID, typename R, typename CMP = std::less<R> >
template <typename ID, typename R, typename H, typename CMP = std::less<R> >
class PriorityQueueList
{
public:
@ -138,6 +139,7 @@ public:
{
ID id;
R rank; // f = g+h (estimated total cost of path through here)
H h; // heuristic cost
};
void push(const Item& item)
@ -155,9 +157,10 @@ public:
return NULL;
}
void promote(ID id, R newrank)
void promote(ID id, R UNUSED(oldrank), R newrank, H newh)
{
find(id)->rank = newrank;
find(id)->h = newh;
}
Item pop()
@ -193,6 +196,12 @@ public:
return m_List.size();
}
void clear()
{
m_List.clear();
}
std::vector<Item> m_List;
};

View File

@ -0,0 +1,107 @@
/* Copyright (C) 2015 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 "Rasterize.h"
#include "simulation2/helpers/Geometry.h"
void SimRasterize::RasterizeRectWithClearance(Spans& spans,
const ICmpObstructionManager::ObstructionSquare& shape,
entity_pos_t clearance, entity_pos_t cellSize)
{
// Get the bounds of cells that might possibly be within the shape
// (We'll then test each of those cells more precisely)
CFixedVector2D halfSize(shape.hw, shape.hh);
CFixedVector2D halfBound = Geometry::GetHalfBoundingBox(shape.u, shape.v, halfSize);
// add 1 to at least have 1 tile out of reach
i16 iMax = (i16)( (halfBound.X + clearance) / cellSize).ToInt_RoundToInfinity() + 1;
i16 jMax = (i16)( (halfBound.Y + clearance) / cellSize).ToInt_RoundToInfinity() + 1;
i16 offsetX = (i16)(shape.x / cellSize).ToInt_RoundToNearest();
i16 offsetZ = (i16)(shape.z / cellSize).ToInt_RoundToNearest();
i16 i0 = -iMax;
i16 i1 = iMax;
if (jMax <= 0)
return; // empty bounds - this shouldn't happen
spans.reserve(jMax * 2);
// TODO: Compare the squared distance to avoid sqrting
#define IS_IN_SQUARE(i, j) (Geometry::DistanceToSquare(CFixedVector2D(cellSize*i, cellSize*j), shape.u, shape.v, halfSize, true) <= clearance)
// The rasterization is finished when for one row, all columns are visited and
// no tile in-range is found.
bool finished = false;
// Loop over half of the rows
// Other rows can be added easily due to rectangle symmetry
// For each row, search the outer bounds, using the bounds of the previous row
// as an estimation
for (i16 j = 0; j <= jMax; ++j)
{
bool foundI0 = false;
// check if the estimation is in or out the square, and move accordingly
bool isI0InSquare = IS_IN_SQUARE(i0, j);
while (!foundI0 && !finished)
{
if (isI0InSquare && !IS_IN_SQUARE(--i0, j))
{
foundI0 = true;
++i0; // add one to bring i0 back in the square
}
else if (!isI0InSquare && IS_IN_SQUARE(++i0, j))
foundI0 = true;
// when this row has no obstructions, we're done
if (i0 > iMax)
finished = true;
ENSURE(i0 >= -iMax);
}
if (finished)
break;
bool foundI1 = false;
// check if the estimation is in or out the square, and move accordingly
bool isI1InSquare = IS_IN_SQUARE(i1, j);
while (!foundI1)
{
if (isI1InSquare && !IS_IN_SQUARE(++i1, j))
{
foundI1 = true;
--i1; // subtract 1 to bring i1 back in the square
}
else if (!isI1InSquare && IS_IN_SQUARE(--i1, j))
foundI1 = true;
// this row will have obstructions, or we will have stopped earlier
ENSURE(i1 >= i0 && i1 <= iMax);
}
spans.emplace_back(Span{ (i16)(offsetX + i0), (i16)(offsetX + i1), (i16)(offsetZ + j) });
// add symmetrical row from j == 1 onwards
if (j > 0)
spans.emplace_back(Span{ (i16)(offsetX - i1), (i16)(offsetX - i0), (i16)(offsetZ - j) });
}
// ensure that the entire bound was found
ENSURE(finished);
#undef IS_IN_SQUARE
}

View File

@ -0,0 +1,54 @@
/* Copyright (C) 2012 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/>.
*/
#ifndef INCLUDED_HELPER_RASTERIZE
#define INCLUDED_HELPER_RASTERIZE
/**
* @file
* Helper functions related to rasterizing geometric shapes to grids.
*/
#include "simulation2/components/ICmpObstructionManager.h"
namespace SimRasterize
{
/**
* Represents the set of cells (i,j) where i0 <= i < i1
*/
struct Span
{
i16 i0;
i16 i1;
i16 j;
};
typedef std::vector<Span> Spans;
/**
* Converts an ObstructionSquare @p shape (a rotated rectangle),
* expanded by the given @p clearance,
* into a list of spans of cells that are strictly inside the shape.
*/
void RasterizeRectWithClearance(Spans& spans,
const ICmpObstructionManager::ObstructionSquare& shape,
entity_pos_t clearance, entity_pos_t cellSize);
}
#endif // INCLUDED_HELPER_RASTERIZE

View File

@ -67,8 +67,11 @@ void SimRender::ConstructLineOnGround(const CSimContext& context, const std::vec
}
}
void SimRender::ConstructCircleOnGround(const CSimContext& context, float x, float z, float radius,
SOverlayLine& overlay, bool floating, float heightOffset)
static void ConstructCircleOrClosedArc(
const CSimContext& context, float x, float z, float radius,
bool isCircle,
float start, float end,
SOverlayLine& overlay, bool floating, float heightOffset)
{
overlay.m_Coords.clear();
@ -85,20 +88,56 @@ void SimRender::ConstructCircleOnGround(const CSimContext& context, float x, flo
}
// Adapt the circle resolution to look reasonable for small and largeish radiuses
size_t numPoints = clamp((size_t)(radius*4.0f), (size_t)12, (size_t)48);
size_t numPoints = clamp((size_t)(radius*(end-start)), (size_t)12, (size_t)48);
overlay.m_Coords.reserve((numPoints + 1) * 3);
if (isCircle)
overlay.m_Coords.reserve((numPoints + 1 + 2) * 3);
else
overlay.m_Coords.reserve((numPoints + 1) * 3);
float cy;
if (!isCircle)
{
// Start at the center point
cy = std::max(water, cmpTerrain->GetExactGroundLevel(x, z)) + heightOffset;
overlay.m_Coords.push_back(x);
overlay.m_Coords.push_back(cy);
overlay.m_Coords.push_back(z);
}
for (size_t i = 0; i <= numPoints; ++i) // use '<=' so it's a closed loop
{
float a = (float)i * 2 * (float)M_PI / (float)numPoints;
float px = x + radius * sinf(a);
float pz = z + radius * cosf(a);
float a = start + (float)i * (end - start) / (float)numPoints;
float px = x + radius * cosf(a);
float pz = z + radius * sinf(a);
float py = std::max(water, cmpTerrain->GetExactGroundLevel(px, pz)) + heightOffset;
overlay.m_Coords.push_back(px);
overlay.m_Coords.push_back(py);
overlay.m_Coords.push_back(pz);
}
if (!isCircle)
{
// Return to the center point
overlay.m_Coords.push_back(x);
overlay.m_Coords.push_back(cy);
overlay.m_Coords.push_back(z);
}
}
void SimRender::ConstructCircleOnGround(
const CSimContext& context, float x, float z, float radius,
SOverlayLine& overlay, bool floating, float heightOffset)
{
ConstructCircleOrClosedArc(context, x, z, radius, true, 0.0f, 2.0f*(float)M_PI, overlay, floating, heightOffset);
}
void SimRender::ConstructClosedArcOnGround(
const CSimContext& context, float x, float z, float radius,
float start, float end,
SOverlayLine& overlay, bool floating, float heightOffset)
{
ConstructCircleOrClosedArc(context, x, z, radius, false, start, end, overlay, floating, heightOffset);
}
// This method splits up a straight line into a number of line segments each having a length ~= TERRAIN_TILE_SIZE

View File

@ -64,7 +64,8 @@ namespace SimRender
* @param[in] floating If true, the line conforms to water as well.
* @param[in] heightOffset Height above terrain to offset the line.
*/
void ConstructLineOnGround(const CSimContext& context, const std::vector<float>& xz,
void ConstructLineOnGround(
const CSimContext& context, const std::vector<float>& xz,
SOverlayLine& overlay,
bool floating, float heightOffset = 0.25f);
@ -78,7 +79,18 @@ void ConstructLineOnGround(const CSimContext& context, const std::vector<float>&
* @param[in] heightOffset Height above terrain to offset the circle.
* @param heightOffset The vertical offset to apply to points, to raise the line off the terrain a bit.
*/
void ConstructCircleOnGround(const CSimContext& context, float x, float z, float radius,
void ConstructCircleOnGround(
const CSimContext& context, float x, float z, float radius,
SOverlayLine& overlay,
bool floating, float heightOffset = 0.25f);
/**
* Constructs overlay line as an outlined circle sector (an arc with straight lines between the
* endpoints and the circle's center), conforming to terrain.
*/
void ConstructClosedArcOnGround(
const CSimContext& context, float x, float z, float radius,
float start, float end,
SOverlayLine& overlay,
bool floating, float heightOffset = 0.25f);
@ -92,7 +104,8 @@ void ConstructCircleOnGround(const CSimContext& context, float x, float z, float
* @param[in] floating If true, the rectangle conforms to water as well.
* @param[in] heightOffset Height above terrain to offset the rectangle.
*/
void ConstructSquareOnGround(const CSimContext& context, float x, float z, float w, float h, float a,
void ConstructSquareOnGround(
const CSimContext& context, float x, float z, float w, float h, float a,
SOverlayLine& overlay,
bool floating, float heightOffset = 0.25f);

View File

@ -290,3 +290,22 @@ template<> void ScriptInterface::ToJSVal<Grid<u16> >(JSContext* cx, JS::MutableH
ret.setObject(*obj);
}
// TODO: This is copy-pasted from scriptinterface/ScriptConversions.h (#define VECTOR stuff), would be nice to remove the duplication
template<> void ScriptInterface::ToJSVal<std::vector<CFixedVector2D> >(JSContext* cx, JS::MutableHandleValue ret, const std::vector<CFixedVector2D>& val)
{
JSAutoRequest rq(cx);
JS::RootedObject obj(cx, JS_NewArrayObject(cx, 0));
if (!obj)
{
ret.setUndefined();
return;
}
for (size_t i = 0; i < val.size(); ++i)
{
JS::RootedValue el(cx);
ScriptInterface::ToJSVal<CFixedVector2D>(cx, &el, val[i]);
JS_SetElement(cx, obj, i, el);
}
ret.setObject(*obj);
}

View File

@ -205,13 +205,13 @@ struct SerializeBool
struct SerializeWaypoint
{
void operator()(ISerializer& serialize, const char* UNUSED(name), const ICmpPathfinder::Waypoint& value)
void operator()(ISerializer& serialize, const char* UNUSED(name), const Waypoint& value)
{
serialize.NumberFixed_Unbounded("waypoint x", value.x);
serialize.NumberFixed_Unbounded("waypoint z", value.z);
}
void operator()(IDeserializer& deserialize, const char* UNUSED(name), ICmpPathfinder::Waypoint& value)
void operator()(IDeserializer& deserialize, const char* UNUSED(name), Waypoint& value)
{
deserialize.NumberFixed_Unbounded("waypoint x", value.x);
deserialize.NumberFixed_Unbounded("waypoint z", value.z);
@ -221,9 +221,9 @@ struct SerializeWaypoint
struct SerializeGoal
{
template<typename S>
void operator()(S& serialize, const char* UNUSED(name), ICmpPathfinder::Goal& value)
void operator()(S& serialize, const char* UNUSED(name), PathGoal& value)
{
SerializeU8_Enum<ICmpPathfinder::Goal::Type, ICmpPathfinder::Goal::SQUARE>()(serialize, "type", value.type);
SerializeU8_Enum<PathGoal::Type, PathGoal::SQUARE>()(serialize, "type", value.type);
serialize.NumberFixed_Unbounded("goal x", value.x);
serialize.NumberFixed_Unbounded("goal z", value.z);
serialize.NumberFixed_Unbounded("goal u x", value.u.X);
@ -232,6 +232,7 @@ struct SerializeGoal
serialize.NumberFixed_Unbounded("goal v z", value.v.Y);
serialize.NumberFixed_Unbounded("goal hw", value.hw);
serialize.NumberFixed_Unbounded("goal hh", value.hh);
serialize.NumberFixed_Unbounded("maxdist", value.maxdist);
}
};

View File

@ -137,10 +137,10 @@ QUERYHANDLER(GetTerrainPassabilityClasses)
CmpPtr<ICmpPathfinder> cmpPathfinder(*AtlasView::GetView_Game()->GetSimulation2(), SYSTEM_ENTITY);
if (cmpPathfinder)
{
std::map<std::string, ICmpPathfinder::pass_class_t> classes = cmpPathfinder->GetPassabilityClasses();
std::map<std::string, pass_class_t> classes = cmpPathfinder->GetPassabilityClasses();
std::vector<std::wstring> classNames;
for (std::map<std::string, ICmpPathfinder::pass_class_t>::iterator it = classes.begin(); it != classes.end(); ++it)
for (std::map<std::string, pass_class_t>::iterator it = classes.begin(); it != classes.end(); ++it)
classNames.push_back(CStr(it->first).FromUTF8());
msg->classNames = classNames;
}

View File

@ -250,10 +250,9 @@ void AtlasViewGame::Render()
{
cmpPathfinder->SetDebugOverlay(true);
// Kind of a hack to make it update the terrain grid
ICmpPathfinder::Goal goal = { ICmpPathfinder::Goal::POINT, fixed::Zero(), fixed::Zero() };
ICmpPathfinder::pass_class_t passClass = cmpPathfinder->GetPassabilityClass(m_DisplayPassability);
ICmpPathfinder::cost_class_t costClass = cmpPathfinder->GetCostClass("default");
cmpPathfinder->SetDebugPath(fixed::Zero(), fixed::Zero(), goal, passClass, costClass);
PathGoal goal = { PathGoal::POINT, fixed::Zero(), fixed::Zero() };
pass_class_t passClass = cmpPathfinder->GetPassabilityClass(m_DisplayPassability);
cmpPathfinder->SetDebugPath(fixed::Zero(), fixed::Zero(), goal, passClass);
}
}