Browse Source

Make path finding index-based

Reduces the size of the `PathNodes` array from 28 KiB to just 8 KiB.

Also reduces the size of `pnode_tblptr` from `300 * sizeof(void *)` to
`300 * 2` bytes.
pull/4762/head
Gleb Mazovetskiy 4 years ago
parent
commit
01ad1814ea
  1. 2
      Source/controls/plrctrls.cpp
  2. 2
      Source/monster.cpp
  3. 287
      Source/path.cpp
  4. 16
      Source/path.h
  5. 3
      Source/platform/locale.cpp
  6. 6
      Source/player.cpp
  7. 2
      Source/player.h
  8. 2
      appveyor.yml
  9. 2
      test/path_test.cpp
  10. 2
      test/player_test.cpp
  11. 2
      test/writehero_test.cpp

2
Source/controls/plrctrls.cpp

@ -120,7 +120,7 @@ int GetDistance(Point destination, int maxDistance)
return 0;
}
int8_t walkpath[MAX_PATH_LENGTH];
int8_t walkpath[MaxPathLength];
Player &myPlayer = *MyPlayer;
int steps = FindPath([&myPlayer](Point position) { return PosOkPlayer(myPlayer, position); }, myPlayer.position.future, destination, walkpath);
if (steps > maxDistance)

2
Source/monster.cpp

@ -1922,7 +1922,7 @@ bool IsTileAccessible(const Monster &monster, Point position)
bool AiPlanWalk(int i)
{
int8_t path[MAX_PATH_LENGTH];
int8_t path[MaxPathLength];
/** Maps from walking path step to facing direction. */
const Direction plr2monst[9] = { Direction::South, Direction::NorthEast, Direction::NorthWest, Direction::SouthEast, Direction::SouthWest, Direction::North, Direction::East, Direction::South, Direction::West };

287
Source/path.cpp

@ -11,108 +11,137 @@
#include "objects.h"
namespace devilution {
namespace {
constexpr size_t MaxPathNodes = 300;
struct PathNode {
static constexpr uint16_t InvalidIndex = std::numeric_limits<uint16_t>::max();
static constexpr size_t MaxChildren = 8;
int16_t x = 0;
int16_t y = 0;
uint16_t parentIndex = InvalidIndex;
uint16_t childIndices[MaxChildren] = { InvalidIndex, InvalidIndex, InvalidIndex, InvalidIndex, InvalidIndex, InvalidIndex, InvalidIndex, InvalidIndex };
uint16_t nextNodeIndex = InvalidIndex;
uint8_t f = 0;
uint8_t h = 0;
uint8_t g = 0;
[[nodiscard]] Point position() const
{
return Point { x, y };
}
void addChild(uint16_t childIndex)
{
size_t index = 0;
for (; index < MaxChildren; ++index) {
if (childIndices[index] == InvalidIndex)
break;
}
assert(index < MaxChildren);
childIndices[index] = childIndex;
}
};
PathNode PathNodes[MaxPathNodes];
/** A linked list of the A* frontier, sorted by distance */
PATHNODE *path_2_nodes;
PathNode *Path2Nodes;
/**
* @brief return a node for a position on the frontier, or NULL if not found
*/
PATHNODE *GetNode1(Point targetPosition)
uint16_t GetNode1(Point targetPosition)
{
PATHNODE *result = path_2_nodes->NextNode;
while (result != nullptr) {
if (result->position == targetPosition)
uint16_t result = Path2Nodes->nextNodeIndex;
while (result != PathNode::InvalidIndex) {
if (PathNodes[result].position() == targetPosition)
return result;
result = result->NextNode;
result = PathNodes[result].nextNodeIndex;
}
return nullptr;
return PathNode::InvalidIndex;
}
/**
* @brief insert pPath into the frontier (keeping the frontier sorted by total distance)
* @brief insert `front` node into the frontier (keeping the frontier sorted by total distance)
*/
void NextNode(PATHNODE *pPath)
void NextNode(uint16_t front)
{
if (path_2_nodes->NextNode == nullptr) {
path_2_nodes->NextNode = pPath;
if (Path2Nodes->nextNodeIndex == PathNode::InvalidIndex) {
Path2Nodes->nextNodeIndex = front;
return;
}
PATHNODE *current = path_2_nodes;
PATHNODE *next = path_2_nodes->NextNode;
int f = pPath->f;
while (next != nullptr && next->f < f) {
current = next;
next = next->NextNode;
PathNode *current = Path2Nodes;
uint16_t nextIndex = Path2Nodes->nextNodeIndex;
const uint8_t maxF = PathNodes[front].f;
while (nextIndex != PathNode::InvalidIndex && PathNodes[nextIndex].f < maxF) {
current = &PathNodes[nextIndex];
nextIndex = current->nextNodeIndex;
}
pPath->NextNode = next;
current->NextNode = pPath;
PathNodes[front].nextNodeIndex = nextIndex;
current->nextNodeIndex = front;
}
/** A linked list of all visited nodes */
PATHNODE *pnode_ptr;
PathNode *VisitedNodes;
/**
* @brief return a node for this position if it was visited, or NULL if not found
*/
PATHNODE *GetNode2(Point targetPosition)
uint16_t GetNode2(Point targetPosition)
{
PATHNODE *result = pnode_ptr->NextNode;
while (result != nullptr) {
if (result->position == targetPosition)
uint16_t result = VisitedNodes->nextNodeIndex;
while (result != PathNode::InvalidIndex) {
if (PathNodes[result].position() == targetPosition)
return result;
result = result->NextNode;
result = PathNodes[result].nextNodeIndex;
}
return nullptr;
return result;
}
/**
* @brief get the next node on the A* frontier to explore (estimated to be closest to the goal), mark it as visited, and return it
*/
PATHNODE *GetNextPath()
uint16_t GetNextPath()
{
PATHNODE *result = path_2_nodes->NextNode;
if (result == nullptr) {
uint16_t result = Path2Nodes->nextNodeIndex;
if (result == PathNode::InvalidIndex) {
return result;
}
path_2_nodes->NextNode = result->NextNode;
result->NextNode = pnode_ptr->NextNode;
pnode_ptr->NextNode = result;
Path2Nodes->nextNodeIndex = PathNodes[result].nextNodeIndex;
PathNodes[result].nextNodeIndex = VisitedNodes->nextNodeIndex;
VisitedNodes->nextNodeIndex = result;
return result;
}
constexpr size_t MAXPATHNODES = 300;
/** Notes visisted by the path finding algorithm. */
PATHNODE path_nodes[MAXPATHNODES];
/** the number of in-use nodes in path_nodes */
uint32_t gdwCurNodes;
/**
* @brief zero one of the preallocated nodes and return a pointer to it, or NULL if none are available
*/
PATHNODE *NewStep()
uint16_t NewStep()
{
if (gdwCurNodes >= MAXPATHNODES)
return nullptr;
if (gdwCurNodes >= MaxPathNodes)
return PathNode::InvalidIndex;
PATHNODE *newNode = &path_nodes[gdwCurNodes];
gdwCurNodes++;
memset(newNode, 0, sizeof(PATHNODE));
return newNode;
PathNodes[gdwCurNodes] = {};
return gdwCurNodes++;
}
/** A stack for recursively searching nodes */
PATHNODE *pnode_tblptr[MAXPATHNODES];
uint16_t pnode_tblptr[MaxPathNodes];
/** size of the pnode_tblptr stack */
uint32_t gdwCurPathStep;
/**
* @brief push pPath onto the pnode_tblptr stack
*/
void PushActiveStep(PATHNODE *pPath)
void PushActiveStep(uint16_t pPath)
{
assert(gdwCurPathStep < MAXPATHNODES);
assert(gdwCurPathStep < MaxPathNodes);
pnode_tblptr[gdwCurPathStep] = pPath;
gdwCurPathStep++;
}
@ -120,7 +149,7 @@ void PushActiveStep(PATHNODE *pPath)
/**
* @brief pop and return a node from the pnode_tblptr stack
*/
PATHNODE *PopActiveStep()
uint16_t PopActiveStep()
{
gdwCurPathStep--;
return pnode_tblptr[gdwCurPathStep];
@ -144,22 +173,24 @@ int CheckEqual(Point startPosition, Point destinationPosition)
/**
* @brief update all path costs using depth-first search starting at pPath
*/
void SetCoords(PATHNODE *pPath)
void SetCoords(uint16_t pPath)
{
PushActiveStep(pPath);
// while there are path nodes to check
while (gdwCurPathStep > 0) {
PATHNODE *pathOld = PopActiveStep();
for (auto *pathAct : pathOld->Child) {
if (pathAct == nullptr)
uint16_t pathOldIndex = PopActiveStep();
const PathNode &pathOld = PathNodes[pathOldIndex];
for (uint16_t childIndex : pathOld.childIndices) {
if (childIndex == PathNode::InvalidIndex)
break;
if (pathOld->g + CheckEqual(pathOld->position, pathAct->position) < pathAct->g) {
if (path_solid_pieces(pathOld->position, pathAct->position)) {
pathAct->Parent = pathOld;
pathAct->g = pathOld->g + CheckEqual(pathOld->position, pathAct->position);
pathAct->f = pathAct->g + pathAct->h;
PushActiveStep(pathAct);
PathNode &pathAct = PathNodes[childIndex];
if (pathOld.g + CheckEqual(pathOld.position(), pathAct.position()) < pathAct.g) {
if (path_solid_pieces(pathOld.position(), pathAct.position())) {
pathAct.parentIndex = pathOldIndex;
pathAct.g = pathOld.g + CheckEqual(pathOld.position(), pathAct.position());
pathAct.f = pathAct.g + pathAct.h;
PushActiveStep(childIndex);
}
}
}
@ -195,70 +226,59 @@ int GetHeuristicCost(Point startPosition, Point destinationPosition)
/**
* @brief add a step from pPath to destination, return 1 if successful, and update the frontier/visited nodes accordingly
*
* @param pPath pointer to the current path node
* @param pathIndex index of the current path node
* @param candidatePosition expected to be a neighbour of the current path node position
* @param destinationPosition where we hope to end up
* @return true if step successfully added, false if we ran out of nodes to use
*/
bool ParentPath(PATHNODE *pPath, Point candidatePosition, Point destinationPosition)
bool ParentPath(uint16_t pathIndex, Point candidatePosition, Point destinationPosition)
{
int nextG = pPath->g + CheckEqual(pPath->position, candidatePosition);
PathNode &path = PathNodes[pathIndex];
int nextG = path.g + CheckEqual(path.position(), candidatePosition);
// 3 cases to consider
// case 1: (dx,dy) is already on the frontier
PATHNODE *dxdy = GetNode1(candidatePosition);
if (dxdy != nullptr) {
int i;
for (i = 0; i < 8; i++) {
if (pPath->Child[i] == nullptr)
break;
}
pPath->Child[i] = dxdy;
if (nextG < dxdy->g) {
if (path_solid_pieces(pPath->position, candidatePosition)) {
uint16_t dxdyIndex = GetNode1(candidatePosition);
if (dxdyIndex != PathNode::InvalidIndex) {
path.addChild(dxdyIndex);
PathNode &dxdy = PathNodes[dxdyIndex];
if (nextG < dxdy.g) {
if (path_solid_pieces(path.position(), candidatePosition)) {
// we'll explore it later, just update
dxdy->Parent = pPath;
dxdy->g = nextG;
dxdy->f = nextG + dxdy->h;
dxdy.parentIndex = pathIndex;
dxdy.g = nextG;
dxdy.f = nextG + dxdy.h;
}
}
} else {
// case 2: (dx,dy) was already visited
dxdy = GetNode2(candidatePosition);
if (dxdy != nullptr) {
int i;
for (i = 0; i < 8; i++) {
if (pPath->Child[i] == nullptr)
break;
}
pPath->Child[i] = dxdy;
if (nextG < dxdy->g && path_solid_pieces(pPath->position, candidatePosition)) {
dxdyIndex = GetNode2(candidatePosition);
if (dxdyIndex != PathNode::InvalidIndex) {
path.addChild(dxdyIndex);
PathNode &dxdy = PathNodes[dxdyIndex];
if (nextG < dxdy.g && path_solid_pieces(path.position(), candidatePosition)) {
// update the node
dxdy->Parent = pPath;
dxdy->g = nextG;
dxdy->f = nextG + dxdy->h;
dxdy.parentIndex = pathIndex;
dxdy.g = nextG;
dxdy.f = nextG + dxdy.h;
// already explored, so re-update others starting from that node
SetCoords(dxdy);
SetCoords(dxdyIndex);
}
} else {
// case 3: (dx,dy) is totally new
dxdy = NewStep();
if (dxdy == nullptr)
dxdyIndex = NewStep();
if (dxdyIndex == PathNode::InvalidIndex)
return false;
dxdy->Parent = pPath;
dxdy->g = nextG;
dxdy->h = GetHeuristicCost(candidatePosition, destinationPosition);
dxdy->f = nextG + dxdy->h;
dxdy->position = candidatePosition;
PathNode &dxdy = PathNodes[dxdyIndex];
dxdy.parentIndex = pathIndex;
dxdy.g = nextG;
dxdy.h = GetHeuristicCost(candidatePosition, destinationPosition);
dxdy.f = nextG + dxdy.h;
dxdy.x = static_cast<int16_t>(candidatePosition.x);
dxdy.y = static_cast<int16_t>(candidatePosition.y);
// add it to the frontier
NextNode(dxdy);
int i;
for (i = 0; i < 8; i++) {
if (pPath->Child[i] == nullptr)
break;
}
pPath->Child[i] = dxdy;
NextNode(dxdyIndex);
path.addChild(dxdyIndex);
}
}
return true;
@ -269,13 +289,14 @@ bool ParentPath(PATHNODE *pPath, Point candidatePosition, Point destinationPosit
*
* @return false if we ran out of preallocated nodes to use, else true
*/
bool GetPath(const std::function<bool(Point)> &posOk, PATHNODE *pPath, Point destination)
bool GetPath(const std::function<bool(Point)> &posOk, uint16_t pathIndex, Point destination)
{
for (auto dir : PathDirs) {
Point tile = pPath->position + dir;
bool ok = posOk(tile);
if ((ok && path_solid_pieces(pPath->position, tile)) || (!ok && tile == destination)) {
if (!ParentPath(pPath, tile, destination))
for (Displacement dir : PathDirs) {
const PathNode &path = PathNodes[pathIndex];
const Point tile = path.position() + dir;
const bool ok = posOk(tile);
if ((ok && path_solid_pieces(path.position(), tile)) || (!ok && tile == destination)) {
if (!ParentPath(pathIndex, tile, destination))
return false;
}
}
@ -340,48 +361,50 @@ bool IsTileOccupied(Point position)
return false;
}
int FindPath(const std::function<bool(Point)> &posOk, Point startPosition, Point destinationPosition, int8_t path[MAX_PATH_LENGTH])
int FindPath(const std::function<bool(Point)> &posOk, Point startPosition, Point destinationPosition, int8_t path[MaxPathLength])
{
/**
* for reconstructing the path after the A* search is done. The longest
* possible path is actually 24 steps, even though we can fit 25
*/
static int8_t pnodeVals[MAX_PATH_LENGTH];
static int8_t pnodeVals[MaxPathLength];
// clear all nodes, create root nodes for the visited/frontier linked lists
gdwCurNodes = 0;
path_2_nodes = NewStep();
pnode_ptr = NewStep();
Path2Nodes = &PathNodes[NewStep()];
VisitedNodes = &PathNodes[NewStep()];
gdwCurPathStep = 0;
PATHNODE *pathStart = NewStep();
pathStart->g = 0;
pathStart->h = GetHeuristicCost(startPosition, destinationPosition);
pathStart->f = pathStart->h + pathStart->g;
pathStart->position = startPosition;
path_2_nodes->NextNode = pathStart;
const uint16_t pathStartIndex = NewStep();
PathNode &pathStart = PathNodes[pathStartIndex];
pathStart.x = static_cast<int16_t>(startPosition.x);
pathStart.y = static_cast<int16_t>(startPosition.y);
pathStart.f = pathStart.h + pathStart.g;
pathStart.h = GetHeuristicCost(startPosition, destinationPosition);
pathStart.g = 0;
Path2Nodes->nextNodeIndex = pathStartIndex;
// A* search until we find (dx,dy) or fail
PATHNODE *nextNode;
while ((nextNode = GetNextPath()) != nullptr) {
uint16_t nextNodeIndex;
while ((nextNodeIndex = GetNextPath()) != PathNode::InvalidIndex) {
// reached the end, success!
if (nextNode->position == destinationPosition) {
PATHNODE *current = nextNode;
int pathLength = 0;
while (current->Parent != nullptr) {
if (pathLength >= MAX_PATH_LENGTH)
if (PathNodes[nextNodeIndex].position() == destinationPosition) {
const PathNode *current = &PathNodes[nextNodeIndex];
size_t pathLength = 0;
while (current->parentIndex != PathNode::InvalidIndex) {
if (pathLength >= MaxPathLength)
break;
pnodeVals[pathLength++] = GetPathDirection(current->Parent->position, current->position);
current = current->Parent;
pnodeVals[pathLength++] = GetPathDirection(PathNodes[current->parentIndex].position(), current->position());
current = &PathNodes[current->parentIndex];
}
if (pathLength != MAX_PATH_LENGTH) {
int i;
if (pathLength != MaxPathLength) {
size_t i;
for (i = 0; i < pathLength; i++)
path[i] = pnodeVals[pathLength - i - 1];
return i;
return static_cast<int>(i);
}
return 0;
}
// ran out of nodes, abort!
if (!GetPath(posOk, nextNode, destinationPosition))
if (!GetPath(posOk, nextNodeIndex, destinationPosition))
return 0;
}
// frontier is empty, no path!

16
Source/path.h

@ -5,7 +5,9 @@
*/
#pragma once
#include <cstdint>
#include <functional>
#include <limits>
#include <SDL.h>
@ -15,17 +17,7 @@
namespace devilution {
#define MAX_PATH_LENGTH 25
struct PATHNODE {
uint8_t f;
uint8_t h;
uint8_t g;
Point position;
struct PATHNODE *Parent;
struct PATHNODE *Child[8];
struct PATHNODE *NextNode;
};
constexpr size_t MaxPathLength = 25;
bool IsTileNotSolid(Point position);
bool IsTileSolid(Point position);
@ -44,7 +36,7 @@ bool IsTileOccupied(Point position);
* @brief Find the shortest path from startPosition to destinationPosition, using PosOk(Point) to check that each step is a valid position.
* Store the step directions (corresponds to an index in PathDirs) in path, which must have room for 24 steps
*/
int FindPath(const std::function<bool(Point)> &posOk, Point startPosition, Point destinationPosition, int8_t path[MAX_PATH_LENGTH]);
int FindPath(const std::function<bool(Point)> &posOk, Point startPosition, Point destinationPosition, int8_t path[MaxPathLength]);
/**
* @brief check if stepping from a given position to a neighbouring tile cuts a corner.

3
Source/platform/locale.cpp

@ -11,6 +11,9 @@
#elif defined(__3DS__)
#include "platform/ctr/locale.hpp"
#elif defined(_WIN32)
// Suppress definitions of `min` and `max` macros by <windows.h>:
#define NOMINMAX 1
#define WIN32_LEAN_AND_MEAN
// clang-format off
// Suppress definitions of `min` and `max` macros by <windows.h>:
#define NOMINMAX 1

6
Source/player.cpp

@ -1561,11 +1561,11 @@ void CheckNewPath(int pnum, bool pmWillBeCalled)
break;
}
for (int j = 1; j < MAX_PATH_LENGTH; j++) {
for (size_t j = 1; j < MaxPathLength; j++) {
player.walkpath[j - 1] = player.walkpath[j];
}
player.walkpath[MAX_PATH_LENGTH - 1] = WALK_NONE;
player.walkpath[MaxPathLength - 1] = WALK_NONE;
if (player._pmode == PM_STAND) {
StartStand(pnum, player._pdir);
@ -2158,7 +2158,7 @@ void Player::UpdatePreviewCelSprite(_cmd_id cmdId, Point point, uint16_t wParam1
}
if (minimalWalkDistance >= 0 && position.future != point) {
int8_t testWalkPath[MAX_PATH_LENGTH];
int8_t testWalkPath[MaxPathLength];
int steps = FindPath([this](Point position) { return PosOkPlayer(*this, position); }, position.future, point, testWalkPath);
if (steps == 0) {
// Can't walk to desired location => stand still

2
Source/player.h

@ -215,7 +215,7 @@ struct Player {
Player &operator=(Player &&) noexcept = default;
PLR_MODE _pmode;
int8_t walkpath[MAX_PATH_LENGTH];
int8_t walkpath[MaxPathLength];
bool plractive;
action_id destAction;
int destParam1;

2
appveyor.yml

@ -1,4 +1,4 @@
version: 1.0.{build}
version: 1.1.{build}
pull_requests:
do_not_increment_build_number: true

2
test/path_test.cpp

@ -103,7 +103,7 @@ TEST(PathTest, SolidPieces)
void CheckPath(Point startPosition, Point destinationPosition, std::vector<int8_t> expectedSteps)
{
static int8_t pathSteps[MAX_PATH_LENGTH];
static int8_t pathSteps[MaxPathLength];
auto pathLength = FindPath([](Point) { return true; }, startPosition, destinationPosition, pathSteps);
EXPECT_EQ(pathLength, expectedSteps.size()) << "Wrong path length for a path from " << startPosition << " to " << destinationPosition;

2
test/player_test.cpp

@ -136,7 +136,7 @@ static void AssertPlayer(Player &player)
ASSERT_EQ(player.pDamAcFlags, ItemSpecialEffectHf::None);
ASSERT_EQ(player._pmode, 0);
ASSERT_EQ(Count8(player.walkpath, MAX_PATH_LENGTH), 0);
ASSERT_EQ(Count8(player.walkpath, MaxPathLength), 0);
ASSERT_EQ(player._pSpell, 0);
ASSERT_EQ(player._pSplType, 0);
ASSERT_EQ(player._pSplFrom, 0);

2
test/writehero_test.cpp

@ -262,7 +262,7 @@ static void AssertPlayer(Player &player)
ASSERT_EQ(player.pDamAcFlags, ItemSpecialEffectHf::None);
ASSERT_EQ(player._pmode, 0);
ASSERT_EQ(Count8(player.walkpath, MAX_PATH_LENGTH), 25);
ASSERT_EQ(Count8(player.walkpath, MaxPathLength), 25);
ASSERT_EQ(player._pgfxnum, 36);
ASSERT_EQ(player.AnimInfo.TicksPerFrame, 4);
ASSERT_EQ(player.AnimInfo.TickCounterOfCurrentFrame, 1);

Loading…
Cancel
Save