Skip to content

Commit

Permalink
Started refactoring generation/.
Browse files Browse the repository at this point in the history
  • Loading branch information
JackAshwell11 committed Oct 24, 2023
1 parent bb504f7 commit b90bf90
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 63 deletions.
1 change: 1 addition & 0 deletions src/hades_extensions/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,4 @@ add_subdirectory(test)

# TODO: Try and see if include formatting can be improved
# TODO: Try and have clang-tidy and cppcheck in CI
# TODO: Could have yaml/toml/json templates for game objects. Could have it specify tile size, image, attributes, components, etc and they're loaded on startup. Could maybe have this even as public api
2 changes: 1 addition & 1 deletion src/hades_extensions/include/generation/astar.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
/// @param end - The end position for the algorithm.
/// @throws std::length_error - Grid size must be bigger than 0.
/// @return A vector of positions mapping out the shortest path from start to end.
std::vector<Position> calculate_astar_path(Grid &grid, Position start, Position end);
std::vector<Position> calculate_astar_path(const Grid &grid, const Position &start, const Position &end);
11 changes: 2 additions & 9 deletions src/hades_extensions/include/generation/primitives.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
#include <cmath>
#include <memory>
#include <stdexcept>
#include <vector>

// Custom includes
#include "hash_combine.hpp"
Expand Down Expand Up @@ -51,17 +50,14 @@ struct Position {
/// Represents a 2D grid with a set width and height through a 1D vector.
struct Grid {
/// The width of the 2D grid.
int width{};
int width;

/// The height of the 2D grid.
int height{};
int height;

/// The vector which represents the 2D grid.
std::unique_ptr<std::vector<TileType>> grid;

/// The default constructor.
Grid() = default;

/// Initialise the object.
///
/// @param width_val - The width of the 2D grid.
Expand Down Expand Up @@ -123,9 +119,6 @@ struct Rect {
/// The height of the rect.
int height;

/// The default constructor.
Rect() = default;

/// Initialise the object.
///
/// @param top_left_val - The top left position of the rect.
Expand Down
83 changes: 38 additions & 45 deletions src/hades_extensions/src/generation/astar.cpp
Original file line number Diff line number Diff line change
@@ -1,103 +1,96 @@
// Std includes
#include <array>
#include <queue>
#include <stdexcept>
#include <unordered_map>

// Custom includes
#include "generation/astar.hpp"

// ----- STRUCTURES ------------------------------
/// Represents a grid position and its costs from the start position
///
/// @param cost - The cost to traverse to this neighbour.
/// @param destination - The destination position in the grid.
/// Represents a grid position and its distance from the start position.
struct Neighbour {
// std::priority_queue uses a max heap, but we want a min heap, so the operator needs to be reversed
inline bool operator<(const Neighbour &neighbour) const { return cost > neighbour.cost; }

/// The cost to traverse to this neighbour.
int cost;

/// The destination position in the grid.
Position destination;

inline bool operator<(const Neighbour nghbr) const {
// The priority_queue data structure gets the maximum priority, so we need
// to override that functionality to get the minimum priority
return cost > nghbr.cost;
}
/// Initialise the object.
Neighbour() = default;

/// Initialise the object.
///
/// @param cost - The cost to traverse to this neighbour.
/// @param destination - The destination position in the grid.
Neighbour(int cost, Position destination) : cost(cost), destination(destination) {}
};

// ----- CONSTANTS ------------------------------
// Represents the north, south, east, west, north-east, north-west, south-east
// and south-west directions on a compass
const std::array<Position, 8> INTERCARDINAL_OFFSETS = {
Position{-1, -1}, Position{0, -1}, Position{1, -1}, Position{-1, 0},
Position{1, 0}, Position{-1, 1}, Position{0, 1}, Position{1, 1},
};
// Represents the north, south, east, west, north-east, north-west, south-east and south-west directions on a compass
const std::array<Position, 8> INTERCARDINAL_OFFSETS = {Position{-1, -1}, Position{0, -1}, Position{1, -1},
Position{-1, 0}, Position{1, 0}, Position{-1, 1},
Position{0, 1}, Position{1, 1}};

// ----- FUNCTIONS ------------------------------
std::vector<Position> calculate_astar_path(Grid &grid, const Position start, const Position end) {
// Check if the grid size is not zero, if not, set up a few variables needed
// for the pathfinding
std::vector<Position> calculate_astar_path(const Grid &grid, const Position &start, const Position &end) {
// Check if the grid size is not zero
if (!grid.width) {
throw std::length_error("Grid size must be bigger than 0.");
}

// Initialise the result vector, priority queue and neighbours map which will be used during the algorithm
std::vector<Position> result;
std::priority_queue<Neighbour> queue;
std::unordered_map<Position, Neighbour> neighbours{{start, {0, start}}};
queue.push({0, start});
queue.emplace(0, start);

// Loop until the priority queue is empty
// Loop until we have explored every neighbour or until we've reached the end
while (!queue.empty()) {
// Get the lowest cost pair from the priority queue
Position current = queue.top().destination;
queue.pop();

// Check if we've reached our target
// Check if we've reached the end. If so, backtrack through the neighbours to get the resultant path
if (current == end) {
// Backtrack through neighbours to get the path
while (!(neighbours[current].destination == current)) {
// Add the current pair to the result list
while (!(neighbours.at(current).destination == current)) {
result.push_back(current);

// Get the next pair in the path
current = neighbours[current].destination;
current = neighbours.at(current).destination;
}

// Add the start position and exit out of the loop
// Add the start position to the result and break out of the loop
result.push_back(start);
break;
}

// Add all the neighbours to the heap with their cost being f = g + h:
// f - The total cost of traversing the neighbour.
// g - The distance between the start pair and the neighbour pair.
// h - The estimated distance from the neighbour pair to the end pair.
// We're using the Chebyshev distance for this.
for (Position offset : INTERCARDINAL_OFFSETS) {
// Calculate the neighbour's position and check if its valid excluding
// the boundaries as that produces weird paths
// h - The estimated distance from the neighbour pair to the end pair (this uses the Chebyshev distance).
for (const Position &offset : INTERCARDINAL_OFFSETS) {
// Calculate the neighbour's position and check if its valid excluding the boundaries
Position neighbour = current + offset;
if (neighbour.x < 1 || neighbour.x >= grid.width - 1 || neighbour.y < 1 || neighbour.y >= grid.height - 1) {
continue;
}

// Test if the neighbour is an obstacle or not. If so, skip to the next
// neighbour as we want to move around it
// Move around the neighbour if it is an obstacle as they have an infinite cost
if (grid.get_value(neighbour) == TileType::Obstacle) {
continue;
}

// Calculate the distance from the start
int distance = neighbours[current].cost + 1;

// Check if we need to add a new neighbour to the heap
if ((!neighbours.contains(neighbour)) || distance < neighbours[neighbour].cost) {
// Check if we've found a more efficient path to the neighbour and if so, add all of its neighbours to the queue
int distance = neighbours.at(current).cost + 1;
if (!neighbours.contains(neighbour) || distance < neighbours.at(neighbour).cost) {
neighbours[neighbour] = {distance, current};

// Add the neighbour to the priority queue
Position diff = end - neighbour;
queue.emplace(distance + std::max(diff.x, diff.y), neighbour);
queue.emplace(distance + std::max(abs(end.x - neighbour.x), abs(end.y - neighbour.y)), neighbour);
}
}
}

// Return result
// Return the most efficient path
return result;
}
11 changes: 3 additions & 8 deletions src/hades_extensions/src/generation/primitives.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,19 @@
// Custom includes
#include "generation/primitives.hpp"

// ----- CONSTANTS ------------------------------
// Defines constants for hallway and entity generation
const std::unordered_set<TileType> REPLACEABLE_TILES = {TileType::Empty, TileType::Obstacle};

// ----- FUNCTIONS ------------------------------
void Rect::place_rect(Grid &grid) const {
// Place the walls
for (int y = std::max(top_left.y, 0); y < std::min(bottom_right.y + 1, grid.height); y++) {
for (int x = std::max(top_left.x, 0); x < std::min(bottom_right.x + 1, grid.width); x++) {
if (REPLACEABLE_TILES.contains(grid.get_value({x, y}))) {
if (grid.get_value({x, y}) == TileType::Empty || grid.get_value({x, y}) == TileType::Obstacle) {
grid.set_value({x, y}, TileType::Wall);
}
}
}

// Place the floors. The ranges must be -1 in all directions since we don't
// want to overwrite the walls keeping the player in, but we still want to
// overwrite walls that block the path for hallways
// Place the floors making sure the ranges are -1 in all directions since we don't want to overwrite the walls keeping
// the player in, but we still want to overwrite walls that block the path for hallways
for (int y = std::max(top_left.y + 1, 1); y < std::min(bottom_right.y, grid.height - 1); y++) {
for (int x = std::max(top_left.x + 1, 1); x < std::min(bottom_right.x, grid.width - 1); x++) {
grid.set_value({x, y}, TileType::Floor);
Expand Down

0 comments on commit b90bf90

Please sign in to comment.