diff --git a/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl index b5ff4042a3..b003945619 100644 --- a/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl +++ b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl @@ -1,24 +1,33 @@ #version 330 -in float v_cost; +/// Flow field value +in float flow_val; + +/// Integration field flags +in float int_val; out vec4 outcol; +int WAVEFRONT_BLOCKED = 0x04; +int LINE_OF_SIGHT = 0x20; +int PATHABLE = 0x10; + void main() { - int cost = int(v_cost); - if (bool(cost & 0x40)) { + int flow_flags = int(flow_val) & 0xF0; + int int_flags = int(int_val); + if (bool(int_flags & WAVEFRONT_BLOCKED)) { // wavefront blocked outcol = vec4(0.9, 0.9, 0.9, 1.0); return; } - if (bool(cost & 0x20)) { + if (bool(int_flags & LINE_OF_SIGHT)) { // line of sight outcol = vec4(1.0, 1.0, 1.0, 1.0); return; } - if (bool(cost & 0x10)) { + if (bool(flow_flags & PATHABLE)) { // pathable outcol = vec4(0.7, 0.7, 0.7, 1.0); return; diff --git a/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl b/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl index 2c2190e57c..03900a8daf 100644 --- a/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl +++ b/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl @@ -1,15 +1,18 @@ #version 330 layout(location=0) in vec3 position; -layout(location=1) in float cost; +layout(location=1) in float flow_cell; +layout(location=2) in float int_cell; uniform mat4 model; uniform mat4 view; uniform mat4 proj; -out float v_cost; +out float flow_val; +out float int_val; void main() { gl_Position = proj * view * model * vec4(position, 1.0); - v_cost = cost; + flow_val = flow_cell; + int_val = int_cell; } diff --git a/doc/code/pathfinding/README.md b/doc/code/pathfinding/README.md index a7ec70d1bd..19d1dc8d19 100644 --- a/doc/code/pathfinding/README.md +++ b/doc/code/pathfinding/README.md @@ -109,13 +109,12 @@ a path request is made, the main influence on performance is the A\* algorithm. a limited number of portals, the A\* search should overall be very cheap. The resulting list of sectors and portals is subsequently used in the low-level flow -field calculations. As a first step, the pathfinder uses its integrator to generate +field calculations. More details can be found in the [field types](field_types.md) document. +As a first step, the pathfinder uses its integrator to generate a flow field for each identified sector. Generation starts with the target sector and ends with the start sector. Flow field results are passed through at the cells of the identified portals to make the flow between sectors seamless. - - In a second step, the pathfinder follows the movement vectors in the flow fields from the start cell to the target cell. Waypoints are created for every direction change, so that game entities can travel in straight lines between them. The list of waypoints diff --git a/doc/code/pathfinding/field_types.md b/doc/code/pathfinding/field_types.md new file mode 100644 index 0000000000..d7251da686 --- /dev/null +++ b/doc/code/pathfinding/field_types.md @@ -0,0 +1,78 @@ +# Field Types + +This document describes the field types used in the flow field pathfinding system. + +Most of the descriptions are based on the [*Crowd Pathfinding and Steering Using Flow Field Tiles*](http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf) article by Elijah Emerson. + +## Cost Field + +A cost field is a square grid of cells that record the cost of movement on the location +of each cell. Higher cost values indicate that it is less desirable to move through that cell. +The field is usually initialized at the start of the game and persists for the lifetime of +the entire pathfinding grid. During gameplay, individual cell costs may be altered to reflect +changes in the environment. + +Cost values are represented as `uint8_t` (0-255) values. The range of usable cost values +is `1` to `254`. `255` is a special value that represents an impassable cell. `0` is reserved +for initialization and should not be used for flow field calculations. + +![Cost Field](images/cost_field.png) + +- **green**: minimum cost +- **red**: maximum cost +- **black**: impassable cell + +## Integration Field + +The integration field is created from a cost field when a path is requested. For a specific +target cell, the integration field stores the accumulated cost of reaching that cell from +every other cell in the field. + +Integration values are calculated using a wavefront algorithm. The algorithm starts at the +target cell(s) and propagates outward, updating the integration value of each cell it visits. +The integration value is calculated by adding the cost value of the current cell to the lowest +integration value of the 4 cardinal neighbors. The integration value of the target cell(s) is `0`. + +Integration values are represented as `uint16_t` (0-65535) values. The range of usable integration +values is `1` to `65534`. `65535` is a special value that represents an unreachable cell. During +initialization, all cells are set to `65535`. + +An additional refinement step in the form of line-of-sight testing may be performed before the +integration values are calculated. This step flags every cell that is in line of sight of the +target cell. This allows for smoother pathing, as game entities can move in a straight line to +the target cell. The algorithm for this step is described in more detail in section 23.6.2 +of the [*Crowd Pathfinding and Steering Using Flow Field Tiles*](http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf) article. + +In addition to the integration values, the integration field also stores flags for each cell: + +- `FOUND`: cell has been visited +- `TARGET`: cell is a target cell +- `LOS`: cell is in line of sight of target cell +- `WAVEFRONT_BLOCKED`: cell is blocking line of sight to target cell + +![Integration Field](images/integration_field.png) + +- **green**: lower integration values +- **purple**: higher integration values +- **black**: unreachable cell + +## Flow Field + +Creating the flow field is the final step in the flow field calculation. The field +is created from the integration field. Cells in the flow field store the direction to +the neighbor cell with the lowest *integrated* cost. Thus, directions create a "flow" +towards the target cell. Following the directions from anywhere on the field will lead +to the shortest path to the target cell. + +Flow field values are represented as `uint8_t` values. The 4 least significant bits are used +to store the direction to the neighbor cell with the lowest integrated cost. Therefore, 8 +directions can be represented. The 4 most significant bits are used for flags: +- `PATHABLE`: cell is passable +- `LOS`: cell is in line of sight of target cell +- `TARGET`: cell is a target cell + +![Flow Field](images/flow_field.png) + +- **white**: line of sight +- **bright/dark grey**: passable cells (not in line of sight) +- **black**: impassable cell diff --git a/doc/code/pathfinding/images/cost_field.png b/doc/code/pathfinding/images/cost_field.png new file mode 100644 index 0000000000..8d820b93fa Binary files /dev/null and b/doc/code/pathfinding/images/cost_field.png differ diff --git a/doc/code/pathfinding/images/flow_field.png b/doc/code/pathfinding/images/flow_field.png new file mode 100644 index 0000000000..ec2985ece2 Binary files /dev/null and b/doc/code/pathfinding/images/flow_field.png differ diff --git a/doc/code/pathfinding/images/integration_field.png b/doc/code/pathfinding/images/integration_field.png new file mode 100644 index 0000000000..56df3abf49 Binary files /dev/null and b/doc/code/pathfinding/images/integration_field.png differ diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index 1a7803619f..fe6d43e397 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -291,7 +291,7 @@ class PathFlowTypePrinter: FLOW_FLAGS: dict = { 0x10: 'PATHABLE', 0x20: 'LOS', - 0x40: 'WAVEFRONT_BLOCKED', + 0x40: 'TARGET', 0x80: 'UNUSED', } @@ -333,8 +333,14 @@ def children(self): # Integrated flags INTEGRATED_FLAGS: dict = { - 0x01: 'LOS', - 0x02: 'WAVEFRONT_BLOCKED', + 0x01: 'UNUSED', + 0x02: 'FOUND', + 0x04: 'WAVEFRONT_BLOCKED', + 0x08: 'UNUSED', + 0x10: 'UNUSED', + 0x20: 'LOS', + 0x40: 'TARGET', + 0x80: 'UNUSED', } @@ -410,6 +416,5 @@ def children(self): # TODO: curve types -# TODO: pathfinding types # TODO: input event codes # TODO: eigen types https://github.com/dmillard/eigengdb diff --git a/libopenage/coord/chunk.cpp b/libopenage/coord/chunk.cpp index fd061586b7..62ab65be32 100644 --- a/libopenage/coord/chunk.cpp +++ b/libopenage/coord/chunk.cpp @@ -1,8 +1,20 @@ -// Copyright 2016-2018 the openage authors. See copying.md for legal info. +// Copyright 2016-2024 the openage authors. See copying.md for legal info. #include "chunk.h" +#include "coord/tile.h" + + namespace openage { namespace coord { -}} // namespace openage::coord +tile_delta chunk_delta::to_tile(tile_t tiles_per_chunk) const { + return tile_delta{this->ne * tiles_per_chunk, this->se * tiles_per_chunk}; +} + +tile chunk::to_tile(tile_t tiles_per_chunk) const { + return tile{this->ne * tiles_per_chunk, this->se * tiles_per_chunk}; +} + +} // namespace coord +} // namespace openage diff --git a/libopenage/coord/chunk.h b/libopenage/coord/chunk.h index a894cc56c1..158888c9a1 100644 --- a/libopenage/coord/chunk.h +++ b/libopenage/coord/chunk.h @@ -12,10 +12,14 @@ namespace coord { struct chunk_delta : CoordNeSeRelative { using CoordNeSeRelative::CoordNeSeRelative; + + tile_delta to_tile(tile_t tiles_per_chunk) const; }; struct chunk : CoordNeSeAbsolute { using CoordNeSeAbsolute::CoordNeSeAbsolute; + + tile to_tile(tile_t tiles_per_chunk) const; }; struct chunk3_delta : CoordNeSeUpRelative { diff --git a/libopenage/coord/tile.cpp b/libopenage/coord/tile.cpp index e20868ff98..e447819799 100644 --- a/libopenage/coord/tile.cpp +++ b/libopenage/coord/tile.cpp @@ -1,4 +1,4 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. +// Copyright 2016-2024 the openage authors. See copying.md for legal info. #include "tile.h" @@ -8,21 +8,40 @@ namespace openage::coord { +phys2_delta tile_delta::to_phys2() const { + return phys2_delta{phys2::elem_t::from_int(this->ne), + phys2::elem_t::from_int(this->se)}; +} -tile3 tile::to_tile3(tile_t up) const { - return tile3(this->ne, this->se, up); +phys3_delta tile_delta::to_phys3(tile_t up) const { + return phys3_delta{phys3::elem_t::from_int(this->ne), + phys3::elem_t::from_int(this->se), + phys3::elem_t::from_int(up)}; } +tile3 tile::to_tile3(tile_t up) const { + return tile3{this->ne, this->se, up}; +} phys2 tile::to_phys2() const { - return phys2{phys3::elem_t::from_int(this->ne), phys3::elem_t::from_int(this->se)}; + return phys2{phys3::elem_t::from_int(this->ne), + phys3::elem_t::from_int(this->se)}; } - phys3 tile::to_phys3(tile_t up) const { return this->to_tile3(up).to_phys3(); } +phys2 tile::to_phys2_center() const { + return phys2{phys3::elem_t::from_int(this->ne) + 0.5, + phys3::elem_t::from_int(this->se) + 0.5}; +} + +phys3 tile::to_phys3_center(tile_t up) const { + return phys3{phys3::elem_t::from_int(this->ne) + 0.5, + phys3::elem_t::from_int(this->se) + 0.5, + phys3::elem_t::from_int(up)}; +} chunk tile::to_chunk() const { return chunk{ @@ -30,33 +49,45 @@ chunk tile::to_chunk() const { static_cast(util::div(this->se, tiles_per_chunk))}; } - tile_delta tile::get_pos_on_chunk() const { return tile_delta{ util::mod(this->ne, tiles_per_chunk), util::mod(this->se, tiles_per_chunk)}; } +tile_delta tile3_delta::to_tile() const { + return tile_delta{this->ne, this->se}; +} + +phys3_delta tile3_delta::to_phys3() const { + return phys3_delta{phys3::elem_t::from_int(this->ne), + phys3::elem_t::from_int(this->se), + phys3::elem_t::from_int(up)}; +} + +tile tile3::to_tile() const { + return tile{this->ne, this->se}; +} phys2 tile3::to_phys2() const { return this->to_tile().to_phys2(); } - phys3 tile3::to_phys3() const { - return phys3{ - phys3::elem_t::from_int(this->ne), - phys3::elem_t::from_int(this->se), - phys3::elem_t::from_int(this->up)}; + return phys3{phys3::elem_t::from_int(this->ne), + phys3::elem_t::from_int(this->se), + phys3::elem_t::from_int(this->up)}; } - -phys2_delta tile_delta::to_phys2() const { - return phys2_delta(this->ne, this->se); +phys2 tile3::to_phys2_center() const { + return phys2{phys3::elem_t::from_int(this->ne) + 0.5, + phys3::elem_t::from_int(this->se) + 0.5}; } -phys3_delta tile_delta::to_phys3(tile_t up) const { - return phys3_delta(this->ne, this->se, up); +phys3 tile3::to_phys3_center() const { + return phys3{phys3::elem_t::from_int(this->ne) + 0.5, + phys3::elem_t::from_int(this->se) + 0.5, + phys3::elem_t::from_int(this->up)}; } } // namespace openage::coord diff --git a/libopenage/coord/tile.h b/libopenage/coord/tile.h index e8e61a09c2..ec685b8002 100644 --- a/libopenage/coord/tile.h +++ b/libopenage/coord/tile.h @@ -20,6 +20,7 @@ namespace coord { struct tile_delta : CoordNeSeRelative { using CoordNeSeRelative::CoordNeSeRelative; + // coordinate conversions phys2_delta to_phys2() const; phys3_delta to_phys3(tile_t up = 0) const; }; @@ -34,8 +35,12 @@ struct tile : CoordNeSeAbsolute { * elevation. */ tile3 to_tile3(tile_t up = 0) const; + phys2 to_phys2() const; phys3 to_phys3(tile_t up = 0) const; + phys2 to_phys2_center() const; + phys3 to_phys3_center(tile_t up = 0) const; + chunk to_chunk() const; tile_delta get_pos_on_chunk() const; }; @@ -45,9 +50,7 @@ struct tile3_delta : CoordNeSeUpRelative { // coordinate conversions // simply discards the UP component of the coordinate delta. - constexpr tile_delta to_tile() const { - return tile_delta{this->ne, this->se}; - } + tile_delta to_tile() const; phys3_delta to_phys3() const; }; @@ -56,11 +59,12 @@ struct tile3 : CoordNeSeUpAbsolute { // coordinate conversions // simply discards the UP component of the coordinate. - constexpr tile to_tile() const { - return tile{this->ne, this->se}; - } + tile to_tile() const; + phys2 to_phys2() const; phys3 to_phys3() const; + phys2 to_phys2_center() const; + phys3 to_phys3_center() const; }; diff --git a/libopenage/gamestate/CMakeLists.txt b/libopenage/gamestate/CMakeLists.txt index a94cd93514..3f0b57bf6b 100644 --- a/libopenage/gamestate/CMakeLists.txt +++ b/libopenage/gamestate/CMakeLists.txt @@ -5,6 +5,7 @@ add_sources(libopenage game_state.cpp game.cpp manager.cpp + map.cpp player.cpp simulation.cpp terrain_chunk.cpp diff --git a/libopenage/gamestate/api/terrain.cpp b/libopenage/gamestate/api/terrain.cpp index e69edee5fc..ad5e300b47 100644 --- a/libopenage/gamestate/api/terrain.cpp +++ b/libopenage/gamestate/api/terrain.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #include "terrain.h" @@ -21,4 +21,18 @@ const std::string APITerrain::get_terrain_path(const nyan::Object &terrain) { return resolve_file_path(terrain, terrain_path); } +const std::unordered_map APITerrain::get_path_costs(const nyan::Object &terrain) { + std::unordered_map result; + + nyan::dict_t path_costs = terrain.get_dict("Terrain.path_costs"); + for (const auto &pair : path_costs) { + auto key = std::dynamic_pointer_cast(pair.first.get_ptr()); + auto value = std::dynamic_pointer_cast(pair.second.get_ptr()); + + result.emplace(key->get_name(), value->get()); + } + + return result; +} + } // namespace openage::gamestate::api diff --git a/libopenage/gamestate/api/terrain.h b/libopenage/gamestate/api/terrain.h index 0046a47bdc..d049bce279 100644 --- a/libopenage/gamestate/api/terrain.h +++ b/libopenage/gamestate/api/terrain.h @@ -30,6 +30,15 @@ class APITerrain { * @return Relative path to the terrain file. */ static const std::string get_terrain_path(const nyan::Object &terrain); + + /** + * Get the path costs of a terrain. + * + * @param terrain \p Terrain nyan object (type == \p engine.util.terrain.Terrain). + * + * @return Path costs for the cost fields of the pathfinder. + */ + static const std::unordered_map get_path_costs(const nyan::Object &terrain); }; } // namespace openage::gamestate::api diff --git a/libopenage/gamestate/component/internal/position.cpp b/libopenage/gamestate/component/internal/position.cpp index 61611382cf..7c4b7bbb83 100644 --- a/libopenage/gamestate/component/internal/position.cpp +++ b/libopenage/gamestate/component/internal/position.cpp @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the openage authors. See copying.md for legal info. +// Copyright 2021-2024 the openage authors. See copying.md for legal info. #include "position.h" @@ -60,7 +60,7 @@ const curve::Segmented &Position::get_angles() const { void Position::set_angle(const time::time_t &time, const coord::phys_angle_t &angle) { auto old_angle = this->angle.get(time); - this->angle.set_insert_jump(time, old_angle, angle); + this->angle.set_last_jump(time, old_angle, angle); } } // namespace openage::gamestate::component diff --git a/libopenage/gamestate/event/spawn_entity.cpp b/libopenage/gamestate/event/spawn_entity.cpp index 9a728a71ef..66f6df9be7 100644 --- a/libopenage/gamestate/event/spawn_entity.cpp +++ b/libopenage/gamestate/event/spawn_entity.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #include "spawn_entity.h" @@ -19,6 +19,7 @@ #include "gamestate/game_entity.h" #include "gamestate/game_state.h" #include "gamestate/manager.h" +#include "gamestate/map.h" #include "gamestate/types.h" // TODO: Testing @@ -155,6 +156,21 @@ void SpawnEntityHandler::invoke(openage::event::EventLoop & /* loop */, const param_map ¶ms) { auto gstate = std::dynamic_pointer_cast(state); + // Check if spawn position is on the map + auto pos = params.get("position", gamestate::WORLD_ORIGIN); + auto map_size = gstate->get_map()->get_size(); + if (not(pos.ne >= 0 + and pos.ne < map_size[0] + and pos.se >= 0 + and pos.se < map_size[1])) { + // Do nothing if the spawn position is not on the map + log::log(DBG << "Entity spawn failed: " + << "Spawn position " << pos + << " is not inside the map area " + << "(map size: " << map_size << ")"); + return; + } + auto nyan_db = gstate->get_db_view(); auto game_entities = nyan_db->get_obj_children_all("engine.util.game_entity.GameEntity"); @@ -183,7 +199,6 @@ void SpawnEntityHandler::invoke(openage::event::EventLoop & /* loop */, auto entity_pos = std::dynamic_pointer_cast( entity->get_component(component::component_t::POSITION)); - auto pos = params.get("position", gamestate::WORLD_ORIGIN); entity_pos->set_position(time, pos); entity_pos->set_angle(time, coord::phys_angle_t::from_int(315)); diff --git a/libopenage/gamestate/game.cpp b/libopenage/gamestate/game.cpp index 8e3497bfbc..e63499d769 100644 --- a/libopenage/gamestate/game.cpp +++ b/libopenage/gamestate/game.cpp @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. +// Copyright 2018-2024 the openage authors. See copying.md for legal info. #include "game.h" @@ -13,6 +13,7 @@ #include "assets/modpack.h" #include "gamestate/entity_factory.h" #include "gamestate/game_state.h" +#include "gamestate/map.h" #include "gamestate/terrain.h" #include "gamestate/terrain_factory.h" #include "gamestate/universe.h" @@ -53,7 +54,7 @@ const std::shared_ptr &Game::get_state() const { void Game::attach_renderer(const std::shared_ptr &render_factory) { this->universe->attach_renderer(render_factory); - this->state->get_terrain()->attach_renderer(render_factory); + this->state->get_map()->get_terrain()->attach_renderer(render_factory); } void Game::load_data(const std::shared_ptr &mod_manager) { @@ -127,8 +128,6 @@ void Game::load_path(const util::Path &base_dir, } void Game::generate_terrain(const std::shared_ptr &terrain_factory) { - auto terrain = terrain_factory->add_terrain(); - auto chunk0 = terrain_factory->add_chunk(this->state, util::Vector2s{10, 10}, coord::tile_delta{0, 0}); @@ -141,12 +140,11 @@ void Game::generate_terrain(const std::shared_ptr &terrain_facto auto chunk3 = terrain_factory->add_chunk(this->state, util::Vector2s{10, 10}, coord::tile_delta{10, 10}); - terrain->add_chunk(chunk0); - terrain->add_chunk(chunk1); - terrain->add_chunk(chunk2); - terrain->add_chunk(chunk3); - this->state->set_terrain(terrain); + auto terrain = terrain_factory->add_terrain({20, 20}, {chunk0, chunk1, chunk2, chunk3}); + + auto map = std::make_shared(this->state, terrain); + this->state->set_map(map); } } // namespace openage::gamestate diff --git a/libopenage/gamestate/game_state.cpp b/libopenage/gamestate/game_state.cpp index 9de7c69bff..4bf0aaa348 100644 --- a/libopenage/gamestate/game_state.cpp +++ b/libopenage/gamestate/game_state.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #include "game_state.h" @@ -37,8 +37,8 @@ void GameState::add_player(const std::shared_ptr &player) { this->players[player->get_id()] = player; } -void GameState::set_terrain(const std::shared_ptr &terrain) { - this->terrain = terrain; +void GameState::set_map(const std::shared_ptr &map) { + this->map = map; } const std::shared_ptr &GameState::get_game_entity(entity_id_t id) const { @@ -59,8 +59,8 @@ const std::shared_ptr &GameState::get_player(player_id_t id) const { return this->players.at(id); } -const std::shared_ptr &GameState::get_terrain() const { - return this->terrain; +const std::shared_ptr &GameState::get_map() const { + return this->map; } const std::shared_ptr &GameState::get_mod_manager() const { diff --git a/libopenage/gamestate/game_state.h b/libopenage/gamestate/game_state.h index bf1531bd55..98cdbc187a 100644 --- a/libopenage/gamestate/game_state.h +++ b/libopenage/gamestate/game_state.h @@ -26,8 +26,8 @@ class EventLoop; namespace gamestate { class GameEntity; +class Map; class Player; -class Terrain; /** * State of the game. @@ -70,11 +70,11 @@ class GameState : public openage::event::State { void add_player(const std::shared_ptr &player); /** - * Set the terrain of the current game. + * Set the map of the current game. * - * @param terrain Terrain object. + * @param terrain Map object. */ - void set_terrain(const std::shared_ptr &terrain); + void set_map(const std::shared_ptr &map); /** * Get a game entity by its ID. @@ -102,11 +102,11 @@ class GameState : public openage::event::State { const std::shared_ptr &get_player(player_id_t id) const; /** - * Get the terrain of the current game. + * Get the map of the current game. * - * @return Terrain object. + * @return Map object. */ - const std::shared_ptr &get_terrain() const; + const std::shared_ptr &get_map() const; /** * TODO: Only for testing. @@ -131,9 +131,9 @@ class GameState : public openage::event::State { std::unordered_map> players; /** - * Terrain of the current game. + * Map of the current game. */ - std::shared_ptr terrain; + std::shared_ptr map; /** * TODO: Only for testing diff --git a/libopenage/gamestate/map.cpp b/libopenage/gamestate/map.cpp new file mode 100644 index 0000000000..1de5b0fe27 --- /dev/null +++ b/libopenage/gamestate/map.cpp @@ -0,0 +1,80 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "map.h" + +#include + +#include "gamestate/api/terrain.h" +#include "gamestate/game_state.h" +#include "gamestate/terrain.h" +#include "gamestate/terrain_chunk.h" +#include "pathfinding/cost_field.h" +#include "pathfinding/grid.h" +#include "pathfinding/pathfinder.h" +#include "pathfinding/sector.h" + + +namespace openage::gamestate { +Map::Map(const std::shared_ptr &state, + const std::shared_ptr &terrain) : + terrain{terrain}, + pathfinder{std::make_shared()}, + grid_lookup{} { + // Create a grid for each path type + // TODO: This is non-deterministic because of the unordered set. Is this a problem? + auto nyan_db = state->get_db_view(); + std::unordered_set path_types = nyan_db->get_obj_children_all("engine.util.path_type.PathType"); + size_t grid_idx = 0; + auto chunk_size = this->terrain->get_chunk(0)->get_size(); + auto side_length = std::max(chunk_size[0], chunk_size[1]); + auto grid_size = this->terrain->get_chunks_size(); + for (const auto &path_type : path_types) { + auto grid = std::make_shared(grid_idx, grid_size, side_length); + this->pathfinder->add_grid(grid); + + this->grid_lookup.emplace(path_type, grid_idx); + grid_idx += 1; + } + + // Set path costs + for (size_t chunk_idx = 0; chunk_idx < this->terrain->get_chunks().size(); ++chunk_idx) { + auto chunk_terrain = this->terrain->get_chunk(chunk_idx); + for (size_t tile_idx = 0; tile_idx < chunk_terrain->get_tiles().size(); ++tile_idx) { + auto tile = chunk_terrain->get_tile(tile_idx); + auto path_costs = api::APITerrain::get_path_costs(tile.terrain); + + for (const auto &path_cost : path_costs) { + auto grid_id = this->grid_lookup.at(path_cost.first); + auto grid = this->pathfinder->get_grid(grid_id); + + auto sector = grid->get_sector(chunk_idx); + auto cost_field = sector->get_cost_field(); + cost_field->set_cost(tile_idx, path_cost.second); + } + } + } + + // Connect sectors with portals + for (const auto &path_type : this->grid_lookup) { + auto grid = this->pathfinder->get_grid(path_type.second); + grid->init_portals(); + } +} + +const util::Vector2s &Map::get_size() const { + return this->terrain->get_size(); +} + +const std::shared_ptr &Map::get_terrain() const { + return this->terrain; +} + +const std::shared_ptr &Map::get_pathfinder() const { + return this->pathfinder; +} + +path::grid_id_t Map::get_grid_id(const nyan::fqon_t &path_grid) const { + return this->grid_lookup.at(path_grid); +} + +} // namespace openage::gamestate diff --git a/libopenage/gamestate/map.h b/libopenage/gamestate/map.h new file mode 100644 index 0000000000..2817f74457 --- /dev/null +++ b/libopenage/gamestate/map.h @@ -0,0 +1,86 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include + +#include "pathfinding/types.h" +#include "util/vector.h" + + +namespace openage { +namespace path { +class Pathfinder; +} // namespace path + +namespace gamestate { +class GameState; +class Terrain; + +class Map { +public: + /** + * Create a new map from existing terrain. + * + * Initializes the pathfinder with the terrain path costs. + * + * @param state Game state. + * @param terrain Terrain object. + */ + Map(const std::shared_ptr &state, + const std::shared_ptr &terrain); + + ~Map() = default; + + /** + * Get the size of the map. + * + * @return Map size (width x height). + */ + const util::Vector2s &get_size() const; + + /** + * Get the terrain of the map. + * + * @return Terrain. + */ + const std::shared_ptr &get_terrain() const; + + /** + * Get the pathfinder for the map. + * + * @return Pathfinder. + */ + const std::shared_ptr &get_pathfinder() const; + + /** + * Get the grid ID associated with a nyan path grid object. + * + * @param path_grid Path grid object fqon. + * + * @return Grid ID. + */ + path::grid_id_t get_grid_id(const nyan::fqon_t &path_grid) const; + +private: + /** + * Terrain. + */ + std::shared_ptr terrain; + + /** + * Pathfinder. + */ + std::shared_ptr pathfinder; + + /** + * Lookup table for mapping path grid objects in nyan to grid indices. + */ + std::unordered_map grid_lookup; +}; + +} // namespace gamestate +} // namespace openage diff --git a/libopenage/gamestate/system/activity.cpp b/libopenage/gamestate/system/activity.cpp index 49371fba30..7ef1d574b4 100644 --- a/libopenage/gamestate/system/activity.cpp +++ b/libopenage/gamestate/system/activity.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #include "activity.h" @@ -79,7 +79,7 @@ void Activity::advance(const time::time_t &start_time, case activity::node_t::TASK_SYSTEM: { auto node = std::static_pointer_cast(current_node); auto task = node->get_system_id(); - event_wait_time = Activity::handle_subsystem(entity, start_time, task); + event_wait_time = Activity::handle_subsystem(start_time, entity, state, task); auto next_id = node->get_next(); current_node = node->next(next_id); } break; @@ -120,18 +120,20 @@ void Activity::advance(const time::time_t &start_time, activity_component->set_node(start_time, current_node); } -const time::time_t Activity::handle_subsystem(const std::shared_ptr &entity, - const time::time_t &start_time, +const time::time_t Activity::handle_subsystem(const time::time_t &start_time, + const std::shared_ptr &entity, + const std::shared_ptr &state, system_id_t system_id) { switch (system_id) { case system_id_t::IDLE: return Idle::idle(entity, start_time); break; case system_id_t::MOVE_COMMAND: - return Move::move_command(entity, start_time); + return Move::move_command(entity, state, start_time); break; case system_id_t::MOVE_DEFAULT: - return Move::move_default(entity, {1, 1, 1}, start_time); + // TODO: replace destination value with a parameter + return Move::move_default(entity, state, {1, 1, 1}, start_time); break; default: throw Error{ERR << "Unhandled subsystem " << static_cast(system_id)}; diff --git a/libopenage/gamestate/system/activity.h b/libopenage/gamestate/system/activity.h index a5c38e6f49..11dcce68ca 100644 --- a/libopenage/gamestate/system/activity.h +++ b/libopenage/gamestate/system/activity.h @@ -40,14 +40,16 @@ class Activity { /** * Run a built-in engine subsystem. * - * @param entity Game entity. * @param start_time Start time of change. + * @param entity Game entity. + * @param state Game state. * @param system_id ID of the subsystem to run. * * @return Runtime of the change in simulation time. */ - static const time::time_t handle_subsystem(const std::shared_ptr &entity, - const time::time_t &start_time, + static const time::time_t handle_subsystem(const time::time_t &start_time, + const std::shared_ptr &entity, + const std::shared_ptr &state, system_id_t system_id); }; diff --git a/libopenage/gamestate/system/move.cpp b/libopenage/gamestate/system/move.cpp index 3ebd02085b..3da45ceab3 100644 --- a/libopenage/gamestate/system/move.cpp +++ b/libopenage/gamestate/system/move.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #include "move.h" @@ -24,11 +24,57 @@ #include "gamestate/component/internal/position.h" #include "gamestate/component/types.h" #include "gamestate/game_entity.h" +#include "gamestate/game_state.h" +#include "gamestate/map.h" +#include "pathfinding/path.h" +#include "pathfinding/pathfinder.h" #include "util/fixed_point.h" namespace openage::gamestate::system { + + +std::vector find_path(const std::shared_ptr &pathfinder, + path::grid_id_t grid_id, + const coord::phys3 &start, + const coord::phys3 &end) { + auto start_tile = start.to_tile(); + auto end_tile = end.to_tile(); + + // Search for a path between the start and end tiles + path::PathRequest request{ + grid_id, + start_tile, + end_tile, + }; + auto tile_path = pathfinder->get_path(request); + + if (tile_path.status != path::PathResult::FOUND) { + // No path found + return {}; + } + + // Get the waypoints of the path + std::vector path; + path.reserve(tile_path.waypoints.size()); + + // Start poition is first waypoint + path.push_back(start); + + // Pathfinder waypoints contain start and end tile; we can ignore them + for (size_t i = 1; i < tile_path.waypoints.size() - 1; ++i) { + auto tile = tile_path.waypoints.at(i); + path.push_back(tile.to_phys3_center()); + } + + // End position is last waypoint + path.push_back(end); + + return path; +} + const time::time_t Move::move_command(const std::shared_ptr &entity, + const std::shared_ptr &state, const time::time_t &start_time) { auto command_queue = std::dynamic_pointer_cast( entity->get_component(component::component_t::COMMANDQUEUE)); @@ -40,11 +86,12 @@ const time::time_t Move::move_command(const std::shared_ptrget_target(), start_time); + return Move::move_default(entity, state, command->get_target(), start_time); } const time::time_t Move::move_default(const std::shared_ptr &entity, + const std::shared_ptr &state, const coord::phys3 &destination, const time::time_t &start_time) { if (not entity->has_component(component::component_t::MOVE)) [[unlikely]] { @@ -61,6 +108,7 @@ const time::time_t Move::move_default(const std::shared_ptrget_component(component::component_t::MOVE)); auto move_ability = move_component->get_ability(); auto move_speed = move_ability.get("Move.speed"); + auto move_path_grid = move_ability.get("Move.path_type"); auto pos_component = std::dynamic_pointer_cast( entity->get_component(component::component_t::POSITION)); @@ -70,38 +118,58 @@ const time::time_t Move::move_default(const std::shared_ptris_infinite_positive()) { - auto angle_diff = new_angle - current_angle; - if (angle_diff < 0) { - // get the positive difference - angle_diff = angle_diff * -1; - } - if (angle_diff > 180) { - // always use the smaller angle - angle_diff = angle_diff - 360; - angle_diff = angle_diff * -1; + // Find path + auto map = state->get_map(); + auto pathfinder = map->get_pathfinder(); + auto grid_id = map->get_grid_id(move_path_grid->get_name()); + auto waypoints = find_path(pathfinder, grid_id, current_pos, destination); + + // use waypoints for movement + double total_time = 0; + pos_component->set_position(start_time, current_pos); + for (size_t i = 1; i < waypoints.size(); ++i) { + auto prev_waypoint = waypoints[i - 1]; + auto cur_waypoint = waypoints[i]; + + auto path_vector = cur_waypoint - prev_waypoint; + auto path_angle = path_vector.to_angle(); + + // rotation + if (not turn_speed->is_infinite_positive()) { + auto angle_diff = path_angle - current_angle; + if (angle_diff < 0) { + // get the positive difference + angle_diff = angle_diff * -1; + } + if (angle_diff > 180) { + // always use the smaller angle + angle_diff = angle_diff - 360; + angle_diff = angle_diff * -1; + } + + // Set an intermediate position keyframe to halt the game entity + // until the rotation is done + double turn_time = angle_diff.to_double() / turn_speed->get(); + total_time += turn_time; + pos_component->set_position(start_time + total_time, prev_waypoint); + + // update current angle for next waypoint + current_angle = path_angle; } + pos_component->set_angle(start_time + total_time, path_angle); - turn_time = angle_diff.to_double() / turn_speed->get(); - } - pos_component->set_angle(start_time + turn_time, new_angle); + // movement + double move_time = 0; + if (not move_speed->is_infinite_positive()) { + auto distance = path_vector.length(); + move_time = distance / move_speed->get(); + } + total_time += move_time; - // movement - double move_time = 0; - if (not move_speed->is_infinite_positive()) { - auto distance = path.length(); - move_time = distance / move_speed->get(); + pos_component->set_position(start_time + total_time, cur_waypoint); } - pos_component->set_position(start_time, current_pos); - pos_component->set_position(start_time + turn_time + move_time, destination); - + // properties auto ability = move_component->get_ability(); if (api::APIAbility::check_property(ability, api::ability_property_t::ANIMATED)) { auto property = api::APIAbility::get_property(ability, api::ability_property_t::ANIMATED); @@ -113,7 +181,7 @@ const time::time_t Move::move_default(const std::shared_ptr &entity, + const std::shared_ptr &state, const time::time_t &start_time); /** * Move a game entity to a destination. * * @param entity Game entity. + * @param state Game state. * @param destination Destination coordinates. * @param start_time Start time of change. * * @return Runtime of the change in simulation time. */ static const time::time_t move_default(const std::shared_ptr &entity, + const std::shared_ptr &state, const coord::phys3 &destination, const time::time_t &start_time); }; diff --git a/libopenage/gamestate/terrain.cpp b/libopenage/gamestate/terrain.cpp index 9e40230ffe..97c5ef5b51 100644 --- a/libopenage/gamestate/terrain.cpp +++ b/libopenage/gamestate/terrain.cpp @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the openage authors. See copying.md for legal info. +// Copyright 2018-2024 the openage authors. See copying.md for legal info. #include "terrain.h" @@ -6,6 +6,8 @@ #include #include +#include "error/error.h" + #include "gamestate/terrain_chunk.h" #include "renderer/render_factory.h" @@ -18,6 +20,69 @@ Terrain::Terrain() : // TODO: Get actual size of terrain. } +Terrain::Terrain(const util::Vector2s &size, + std::vector> &&chunks) : + size{size}, + chunks{std::move(chunks)} { + // Check if chunk is correct + coord::tile_delta current_offset{0, 0}; + util::Vector2s all_chunks_size{0, 0}; + if (this->chunks.size() > 0) { + all_chunks_size = this->chunks[0]->get_size(); + } + + for (const auto &chunk : this->chunks) { + auto chunk_size = chunk->get_size(); + + // Check chunk delta + auto chunk_offset = chunk->get_offset(); + if (current_offset != chunk_offset) [[unlikely]] { + throw error::Error{ERR << "Chunk offset of chunk " << chunk->get_offset() + << " does not match position in vector: " << chunk_offset + << " (expected: " << current_offset << ")"}; + } + + current_offset.ne += chunk_size[0]; + + // Wrap around to the next row + if (current_offset.ne == static_cast(size[0])) { + current_offset.ne = 0; + current_offset.se += chunk_size[1]; + } + // Check if chunk size is correct + else if (chunk_size != chunk_size) [[unlikely]] { + throw error::Error{ERR << "Chunk size of chunk " << chunk->get_offset() + << " is not equal to the first chunk size: " << chunk_size + << " (expected: " << all_chunks_size << ")"}; + } + else if (chunk_size[0] != chunk_size[1]) { + throw error::Error{ERR << "Chunk size of chunk " << chunk->get_offset() + << " is not square: " << chunk_size}; + } + + // Check terrain boundaries + if (current_offset.ne > static_cast(size[0])) { + throw error::Error{ERR << "Width of chunk " << chunk->get_offset() + << " exceeds terrain width: " << chunk_size[0] + << " (max width: " << size[0] << ")"}; + } + else if (current_offset.se > static_cast(size[1])) { + throw error::Error{ERR << "Height of chunk " << chunk->get_offset() + << " exceeds terrain height: " << chunk_size[1] + << " (max height: " << size[1] << ")"}; + } + } +} + +const util::Vector2s &Terrain::get_size() const { + return this->size; +} + +const util::Vector2s Terrain::get_chunks_size() const { + auto chunk_size = this->chunks[0]->get_size(); + return {this->size[0] / chunk_size[0], this->size[1] / chunk_size[1]}; +} + void Terrain::add_chunk(const std::shared_ptr &chunk) { this->chunks.push_back(chunk); } @@ -26,6 +91,16 @@ const std::vector> &Terrain::get_chunks() const { return this->chunks; } +const std::shared_ptr &Terrain::get_chunk(size_t idx) const { + return this->chunks.at(idx); +} + +const std::shared_ptr &Terrain::get_chunk(const coord::chunk &pos) const { + size_t index = pos.ne + pos.se * this->size[0]; + + return this->get_chunk(index); +} + void Terrain::attach_renderer(const std::shared_ptr &render_factory) { for (auto &chunk : this->get_chunks()) { auto render_entity = render_factory->add_terrain_render_entity(chunk->get_size(), diff --git a/libopenage/gamestate/terrain.h b/libopenage/gamestate/terrain.h index 97ffcbdffd..d77eaf939c 100644 --- a/libopenage/gamestate/terrain.h +++ b/libopenage/gamestate/terrain.h @@ -6,8 +6,10 @@ #include #include +#include "coord/chunk.h" #include "util/vector.h" + namespace openage { namespace renderer { class RenderFactory; @@ -27,6 +29,35 @@ class Terrain { Terrain(); ~Terrain() = default; + /** + * Create a new terrain. + * + * Chunks must conform to these constraints: + * - All chunks that are not last in a row OR columns must have the same size (width x height). + * - All chunks that are not last in a row OR columns must be square (width == height). + * - The last chunk in a row may have a different width. + * - The last chunk in a column may have a different height. + * + * @param size Total size of the terrain in tiles (width x height). + * @param chunks Terrain chunks. + */ + Terrain(const util::Vector2s &size, + std::vector> &&chunks); + + /** + * Get the size of the terrain (in tiles). + * + * @return Terrain tile size (width x height). + */ + const util::Vector2s &get_size() const; + + /** + * Get the size of the terrain (in chunks). + * + * @return Terrain chunk size (width x height). + */ + const util::Vector2s get_chunks_size() const; + /** * Add a chunk to the terrain. * @@ -41,6 +72,24 @@ class Terrain { */ const std::vector> &get_chunks() const; + /** + * Get a specific chunk of the terrain. + * + * @param idx Index of the chunk. + * + * @return Terrain chunk. + */ + const std::shared_ptr &get_chunk(size_t idx) const; + + /** + * Get a specific chunk of the terrain. + * + * @param pos Position of the chunk in the terrain. + * + * @return Terrain chunk. + */ + const std::shared_ptr &get_chunk(const coord::chunk &pos) const; + /** * Attach a renderer which enables graphical display. * @@ -56,14 +105,16 @@ class Terrain { private: /** - * Total size of the map - * origin is the left corner - * x = top left edge; y = top right edge + * Total size of the terrain in tiles (width x height). + * + * Origin is the top left corner (left corner with camera projection). */ util::Vector2s size; /** * Subdivision of the main terrain entity. + * + * Ordered in rows from left to right, top to bottom. */ std::vector> chunks; }; diff --git a/libopenage/gamestate/terrain_chunk.cpp b/libopenage/gamestate/terrain_chunk.cpp index e988260333..d863db9f67 100644 --- a/libopenage/gamestate/terrain_chunk.cpp +++ b/libopenage/gamestate/terrain_chunk.cpp @@ -30,6 +30,18 @@ const coord::tile_delta &TerrainChunk::get_offset() const { return this->offset; } +const std::vector &TerrainChunk::get_tiles() const { + return this->tiles; +} + +const TerrainTile &TerrainChunk::get_tile(size_t idx) const { + return this->tiles.at(idx); +} + +const TerrainTile &TerrainChunk::get_tile(const coord::tile &pos) const { + return this->tiles.at(pos.ne + pos.se * this->size[0]); +} + void TerrainChunk::render_update(const time::time_t &time) { if (this->render_entity != nullptr) { // TODO: Update individual tiles instead of the whole chunk diff --git a/libopenage/gamestate/terrain_chunk.h b/libopenage/gamestate/terrain_chunk.h index 684d3af357..574e63da4d 100644 --- a/libopenage/gamestate/terrain_chunk.h +++ b/libopenage/gamestate/terrain_chunk.h @@ -48,6 +48,31 @@ class TerrainChunk { */ const coord::tile_delta &get_offset() const; + /** + * Get the tiles of this terrain chunk. + * + * @return Terrain tiles. + */ + const std::vector &get_tiles() const; + + /** + * Get the tile at the given index. + * + * @param idx Index of the tile. + * + * @return Terrain tile. + */ + const TerrainTile &get_tile(size_t idx) const; + + /** + * Get the tile at the given position. + * + * @param pos Position of the tile. + * + * @return Terrain tile. + */ + const TerrainTile &get_tile(const coord::tile &pos) const; + /** * Update the render entity. * diff --git a/libopenage/gamestate/terrain_factory.cpp b/libopenage/gamestate/terrain_factory.cpp index 17b5d452cb..3f16a6acf1 100644 --- a/libopenage/gamestate/terrain_factory.cpp +++ b/libopenage/gamestate/terrain_factory.cpp @@ -26,15 +26,30 @@ static const std::vector test_terrain_paths = { "../test/textures/test_terrain2.terrain", }; -static const std::vector aoe1_test_terrain = {}; -static const std::vector de1_test_terrain = {}; +static const std::vector aoe1_test_terrain = { + "aoe1_base.data.terrain.forest.forest.Forest", + "aoe1_base.data.terrain.grass.grass.Grass", + "aoe1_base.data.terrain.dirt.dirt.Dirt", + "aoe1_base.data.terrain.water.water.Water", +}; +static const std::vector de1_test_terrain = { + "aoe1_base.data.terrain.desert.desert.Desert", + "aoe1_base.data.terrain.grass.grass.Grass", + "aoe1_base.data.terrain.dirt.dirt.Dirt", + "aoe1_base.data.terrain.water.water.Water", +}; static const std::vector aoe2_test_terrain = { "aoe2_base.data.terrain.foundation.foundation.Foundation", "aoe2_base.data.terrain.grass.grass.Grass", "aoe2_base.data.terrain.dirt.dirt.Dirt", "aoe2_base.data.terrain.water.water.Water", }; -static const std::vector de2_test_terrain = {}; +static const std::vector de2_test_terrain = { + "de2_base.data.terrain.foundation.foundation.Foundation", + "de2_base.data.terrain.grass.grass.Grass", + "de2_base.data.terrain.dirt.dirt.Dirt", + "de2_base.data.terrain.water.water.Water", +}; static const std::vector hd_test_terrain = { "hd_base.data.terrain.foundation.foundation.Foundation", "hd_base.data.terrain.grass.grass.Grass", @@ -47,10 +62,16 @@ static const std::vector swgb_test_terrain = { "swgb_base.data.terrain.desert0.desert0.Desert0", "swgb_base.data.terrain.water1.water1.Water1", }; -static const std::vector trial_test_terrain = {}; +static const std::vector trial_test_terrain = { + "trial_base.data.terrain.foundation.foundation.Foundation", + "trial_base.data.terrain.grass.grass.Grass", + "trial_base.data.terrain.dirt.dirt.Dirt", + "trial_base.data.terrain.water.water.Water", +}; // TODO: Remove hardcoded test texture references static std::vector test_terrains; // declare static so we only have to do this once +static bool has_graphics = false; void build_test_terrains(const std::shared_ptr &gstate) { auto modpack_ids = gstate->get_mod_manager()->get_load_order(); @@ -59,36 +80,43 @@ void build_test_terrains(const std::shared_ptr &gstate) { test_terrains.insert(test_terrains.end(), aoe1_test_terrain.begin(), aoe1_test_terrain.end()); + has_graphics = false; } else if (modpack_id == "de1_base") { test_terrains.insert(test_terrains.end(), de1_test_terrain.begin(), de1_test_terrain.end()); + has_graphics = false; } else if (modpack_id == "aoe2_base") { test_terrains.insert(test_terrains.end(), aoe2_test_terrain.begin(), aoe2_test_terrain.end()); + has_graphics = true; } else if (modpack_id == "de2_base") { test_terrains.insert(test_terrains.end(), de2_test_terrain.begin(), de2_test_terrain.end()); + has_graphics = false; } else if (modpack_id == "hd_base") { test_terrains.insert(test_terrains.end(), hd_test_terrain.begin(), hd_test_terrain.end()); + has_graphics = true; } else if (modpack_id == "swgb_base") { test_terrains.insert(test_terrains.end(), swgb_test_terrain.begin(), swgb_test_terrain.end()); + has_graphics = true; } else if (modpack_id == "trial_base") { test_terrains.insert(test_terrains.end(), trial_test_terrain.begin(), trial_test_terrain.end()); + has_graphics = true; } } } @@ -170,9 +198,10 @@ static const std::vector> layout_chunks{ }; -std::shared_ptr TerrainFactory::add_terrain() { +std::shared_ptr TerrainFactory::add_terrain(const util::Vector2s &size, + std::vector> &&chunks) { // TODO: Replace this with a proper terrain generator. - auto terrain = std::make_shared(); + auto terrain = std::make_shared(size, std::move(chunks)); return terrain; } @@ -184,7 +213,7 @@ std::shared_ptr TerrainFactory::add_chunk(const std::shared_ptr terrain_obj; + nyan::Object terrain_obj; if (test_terrains.empty()) { build_test_terrains(gstate); } @@ -203,20 +232,14 @@ std::shared_ptr TerrainFactory::add_chunk(const std::shared_ptrget_db_view()->get_object(test_terrains.at(terrain_index)); - terrain_info_path = api::APITerrain::get_terrain_path(terrain_obj.value()); - tiles.push_back({terrain_obj, terrain_info_path, terrain_elevation_t::zero()}); - } - test_chunk_index += 1; - } - else { - // use a test texture - if (test_chunk_index >= test_terrain_paths.size()) { - test_chunk_index = 0; - } - terrain_info_path = test_terrain_paths.at(test_chunk_index); + if (has_graphics) { + terrain_info_path = api::APITerrain::get_terrain_path(terrain_obj); + } + else { + terrain_info_path = test_terrain_paths.at(test_chunk_index % test_terrain_paths.size()); + } - for (size_t i = 0; i < size[0] * size[1]; ++i) { tiles.push_back({terrain_obj, terrain_info_path, terrain_elevation_t::zero()}); } diff --git a/libopenage/gamestate/terrain_factory.h b/libopenage/gamestate/terrain_factory.h index ab419acc30..f5ce40b283 100644 --- a/libopenage/gamestate/terrain_factory.h +++ b/libopenage/gamestate/terrain_factory.h @@ -36,7 +36,8 @@ class TerrainFactory { * * @return New terrain object. */ - std::shared_ptr add_terrain(); + std::shared_ptr add_terrain(const util::Vector2s &size, + std::vector> &&chunks); /** * Create a new empty terrain chunk. diff --git a/libopenage/gamestate/terrain_tile.h b/libopenage/gamestate/terrain_tile.h index 36ae11ddf3..6d6dd99b55 100644 --- a/libopenage/gamestate/terrain_tile.h +++ b/libopenage/gamestate/terrain_tile.h @@ -20,10 +20,8 @@ using terrain_elevation_t = util::FixedPoint; struct TerrainTile { /** * Terrain definition used by this tile. - * - * TODO: Make this non-optional once all modpacks support terrain graphics. */ - std::optional terrain; + nyan::Object terrain; /** * Path to the terrain asset used by this tile. diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp index 248a260d8e..3299084548 100644 --- a/libopenage/pathfinding/cost_field.cpp +++ b/libopenage/pathfinding/cost_field.cpp @@ -21,18 +21,26 @@ size_t CostField::get_size() const { return this->size; } -cost_t CostField::get_cost(const coord::tile &pos) const { +cost_t CostField::get_cost(const coord::tile_delta &pos) const { return this->cells.at(pos.ne + pos.se * this->size); } +cost_t CostField::get_cost(size_t x, size_t y) const { + return this->cells.at(x + y * this->size); +} + cost_t CostField::get_cost(size_t idx) const { return this->cells.at(idx); } -void CostField::set_cost(const coord::tile &pos, cost_t cost) { +void CostField::set_cost(const coord::tile_delta &pos, cost_t cost) { this->cells[pos.ne + pos.se * this->size] = cost; } +void CostField::set_cost(size_t x, size_t y, cost_t cost) { + this->cells[x + y * this->size] = cost; +} + void CostField::set_cost(size_t idx, cost_t cost) { this->cells[idx] = cost; } diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index ffb27bdec8..a1fc87b046 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -10,7 +10,7 @@ namespace openage { namespace coord { -struct tile; +struct tile_delta; } // namespace coord namespace path { @@ -37,10 +37,19 @@ class CostField { /** * Get the cost at a specified position. * - * @param pos Coordinates of the cell. + * @param pos Coordinates of the cell (relative to field origin). * @return Cost at the specified position. */ - cost_t get_cost(const coord::tile &pos) const; + cost_t get_cost(const coord::tile_delta &pos) const; + + /** + * Get the cost at a specified position. + * + * @param x X-coordinate of the cell. + * @param y Y-coordinate of the cell. + * @return Cost at the specified position. + */ + cost_t get_cost(size_t x, size_t y) const; /** * Get the cost at a specified position. @@ -53,10 +62,19 @@ class CostField { /** * Set the cost at a specified position. * - * @param pos Coordinates of the cell. + * @param pos Coordinates of the cell (relative to field origin). + * @param cost Cost to set. + */ + void set_cost(const coord::tile_delta &pos, cost_t cost); + + /** + * Set the cost at a specified position. + * + * @param x X-coordinate of the cell. + * @param y Y-coordinate of the cell. * @param cost Cost to set. */ - void set_cost(const coord::tile &pos, cost_t cost); + void set_cost(size_t x, size_t y, cost_t cost); /** * Set the cost at a specified position. diff --git a/libopenage/pathfinding/definitions.h b/libopenage/pathfinding/definitions.h index d61c5fe43c..2ace3c79ab 100644 --- a/libopenage/pathfinding/definitions.h +++ b/libopenage/pathfinding/definitions.h @@ -35,7 +35,7 @@ constexpr cost_t COST_IMPASSABLE = 255; /** * Start value for goal cells. */ -const integrated_cost_t INTEGRATED_COST_START = 0; +constexpr integrated_cost_t INTEGRATED_COST_START = 0; /** * Unreachable value for a cells in the integration grid. @@ -45,12 +45,22 @@ constexpr integrated_cost_t INTEGRATED_COST_UNREACHABLE = std::numeric_limits(field_length); - cost_field->set_cost(coord::tile{0, 0}, COST_IMPASSABLE); - cost_field->set_cost(coord::tile{1, 0}, 254); - cost_field->set_cost(coord::tile{4, 3}, 128); - cost_field->set_cost(coord::tile{5, 3}, 128); - cost_field->set_cost(coord::tile{6, 3}, 128); - cost_field->set_cost(coord::tile{4, 4}, 128); - cost_field->set_cost(coord::tile{5, 4}, 128); - cost_field->set_cost(coord::tile{6, 4}, 128); - cost_field->set_cost(coord::tile{1, 7}, COST_IMPASSABLE); + cost_field->set_cost(coord::tile_delta{0, 0}, COST_IMPASSABLE); + cost_field->set_cost(coord::tile_delta{1, 0}, 254); + cost_field->set_cost(coord::tile_delta{4, 3}, 128); + cost_field->set_cost(coord::tile_delta{5, 3}, 128); + cost_field->set_cost(coord::tile_delta{6, 3}, 128); + cost_field->set_cost(coord::tile_delta{4, 4}, 128); + cost_field->set_cost(coord::tile_delta{5, 4}, 128); + cost_field->set_cost(coord::tile_delta{6, 4}, 128); + cost_field->set_cost(coord::tile_delta{1, 7}, COST_IMPASSABLE); log::log(INFO << "Created cost field"); // Create an integration field from the cost field @@ -49,7 +49,7 @@ void path_demo_0(const util::Path &path) { log::log(INFO << "Created integration field"); // Set cell (7, 7) to be the initial target cell - auto wavefront_blocked = integration_field->integrate_los(cost_field, coord::tile{7, 7}); + auto wavefront_blocked = integration_field->integrate_los(cost_field, coord::tile_delta{7, 7}); integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); log::log(INFO << "Calculated integration field for target cell (7, 7)"); @@ -92,7 +92,8 @@ void path_demo_0(const util::Path &path) { // Recalculate the integration field and the flow field integration_field->reset(); - auto wavefront_blocked = integration_field->integrate_los(cost_field, coord::tile{grid_x, grid_y}); + auto wavefront_blocked = integration_field->integrate_los(cost_field, + coord::tile_delta{grid_x, grid_y}); integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); log::log(INFO << "Calculated integration field for target cell (" << grid_x << ", " << grid_y << ")"); @@ -110,7 +111,7 @@ void path_demo_0(const util::Path &path) { render_manager->show_integration_field(integration_field); break; case RenderManager0::field_t::FLOW: - render_manager->show_flow_field(flow_field); + render_manager->show_flow_field(flow_field, integration_field); break; } @@ -136,7 +137,7 @@ void path_demo_0(const util::Path &path) { log::log(INFO << "Showing integration field"); } else if (ev.key() == Qt::Key_F3) { // Show flow field - render_manager->show_flow_field(flow_field); + render_manager->show_flow_field(flow_field, integration_field); current_field = RenderManager0::field_t::FLOW; log::log(INFO << "Showing flow field"); } @@ -240,7 +241,8 @@ void RenderManager0::show_integration_field(const std::shared_ptrfield_pass->set_renderables({renderable}); } -void RenderManager0::show_flow_field(const std::shared_ptr &field) { +void RenderManager0::show_flow_field(const std::shared_ptr &flow_field, + const std::shared_ptr &int_field) { Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); auto unifs = this->flow_shader->new_uniform_input( "model", @@ -249,7 +251,7 @@ void RenderManager0::show_flow_field(const std::shared_ptr &fie this->camera->get_view_matrix(), "proj", this->camera->get_projection_matrix()); - auto mesh = get_flow_field_mesh(field, 4); + auto mesh = get_flow_field_mesh(flow_field, int_field, 4); auto geometry = this->renderer->add_mesh_geometry(mesh); renderer::Renderable renderable{ unifs, @@ -275,7 +277,7 @@ void RenderManager0::show_vectors(const std::shared_ptr &field) this->vector_pass->clear_renderables(); for (size_t y = 0; y < field->get_size(); ++y) { for (size_t x = 0; x < field->get_size(); ++x) { - auto cell = field->get_cell(coord::tile(x, y)); + auto cell = field->get_cell(coord::tile_delta(x, y)); if (cell & FLOW_PATHABLE_MASK and not(cell & FLOW_LOS_MASK)) { Eigen::Affine3f arrow_model = Eigen::Affine3f::Identity(); arrow_model.translate(Eigen::Vector3f(y + 0.5, 0, -1.0f * x - 0.5)); @@ -566,19 +568,19 @@ renderer::resources::MeshData RenderManager0::get_cost_field_mesh(const std::sha // for each vertex, compare the surrounding tiles std::vector surround{}; if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cost(coord::tile((i - 1) / resolution, (j - 1) / resolution)); + auto cost = field->get_cost((i - 1) / resolution, (j - 1) / resolution); surround.push_back(cost); } if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cost(coord::tile((i - 1) / resolution, j / resolution)); + auto cost = field->get_cost((i - 1) / resolution, j / resolution); surround.push_back(cost); } if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cost(coord::tile(i / resolution, j / resolution)); + auto cost = field->get_cost(i / resolution, j / resolution); surround.push_back(cost); } if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cost(coord::tile(i / resolution, (j - 1) / resolution)); + auto cost = field->get_cost(i / resolution, (j - 1) / resolution); surround.push_back(cost); } // use the cost of the most expensive surrounding tile @@ -652,19 +654,19 @@ renderer::resources::MeshData RenderManager0::get_integration_field_mesh(const s // for each vertex, compare the surrounding tiles std::vector surround{}; if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cell(coord::tile((i - 1) / resolution, (j - 1) / resolution)).cost; + auto cost = field->get_cell((i - 1) / resolution, (j - 1) / resolution).cost; surround.push_back(cost); } if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cell(coord::tile((i - 1) / resolution, j / resolution)).cost; + auto cost = field->get_cell((i - 1) / resolution, j / resolution).cost; surround.push_back(cost); } if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cell(coord::tile(i / resolution, j / resolution)).cost; + auto cost = field->get_cell(i / resolution, j / resolution).cost; surround.push_back(cost); } if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cell(coord::tile(i / resolution, (j - 1) / resolution)).cost; + auto cost = field->get_cell(i / resolution, (j - 1) / resolution).cost; surround.push_back(cost); } // use the cost of the most expensive surrounding tile @@ -719,42 +721,63 @@ renderer::resources::MeshData RenderManager0::get_integration_field_mesh(const s return {std::move(vert_data), std::move(idx_data), info}; } -renderer::resources::MeshData RenderManager0::get_flow_field_mesh(const std::shared_ptr &field, +renderer::resources::MeshData RenderManager0::get_flow_field_mesh(const std::shared_ptr &flow_field, + const std::shared_ptr &int_field, size_t resolution) { // increase by 1 in every dimension because to get the vertex length // of each dimension util::Vector2s size{ - field->get_size() * resolution + 1, - field->get_size() * resolution + 1, + flow_field->get_size() * resolution + 1, + flow_field->get_size() * resolution + 1, }; auto vert_distance = 1.0f / resolution; // add vertices for the cells of the grid std::vector verts{}; auto vert_count = size[0] * size[1]; - verts.reserve(vert_count * 4); + verts.reserve(vert_count * 5); for (int i = 0; i < static_cast(size[0]); ++i) { for (int j = 0; j < static_cast(size[1]); ++j) { // for each vertex, compare the surrounding tiles - std::vector surround{}; + std::vector ff_surround{}; + std::vector int_surround{}; if (j - 1 >= 0 and i - 1 >= 0) { - auto cost = field->get_cell(coord::tile((i - 1) / resolution, (j - 1) / resolution)); - surround.push_back(cost); + auto ff_cost = flow_field->get_cell((i - 1) / resolution, (j - 1) / resolution); + ff_surround.push_back(ff_cost); + + auto int_flags = int_field->get_cell((i - 1) / resolution, (j - 1) / resolution).flags; + int_surround.push_back(int_flags); } - if (j < static_cast(field->get_size()) and i - 1 >= 0) { - auto cost = field->get_cell(coord::tile((i - 1) / resolution, j / resolution)); - surround.push_back(cost); + if (j < static_cast(flow_field->get_size()) and i - 1 >= 0) { + auto ff_cost = flow_field->get_cell((i - 1) / resolution, j / resolution); + ff_surround.push_back(ff_cost); + + auto int_flags = int_field->get_cell((i - 1) / resolution, j / resolution).flags; + int_surround.push_back(int_flags); } - if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { - auto cost = field->get_cell(coord::tile(i / resolution, j / resolution)); - surround.push_back(cost); + if (j < static_cast(flow_field->get_size()) and i < static_cast(flow_field->get_size())) { + auto ff_cost = flow_field->get_cell(i / resolution, j / resolution); + ff_surround.push_back(ff_cost); + + auto int_flags = int_field->get_cell(i / resolution, j / resolution).flags; + int_surround.push_back(int_flags); } - if (j - 1 >= 0 and i < static_cast(field->get_size())) { - auto cost = field->get_cell(coord::tile(i / resolution, (j - 1) / resolution)); - surround.push_back(cost); + if (j - 1 >= 0 and i < static_cast(flow_field->get_size())) { + auto ff_cost = flow_field->get_cell(i / resolution, (j - 1) / resolution); + ff_surround.push_back(ff_cost); + + auto int_flags = int_field->get_cell(i / resolution, (j - 1) / resolution).flags; + int_surround.push_back(int_flags); + } + // combine the flags of the sorrounding tiles + auto ff_max_flags = 0; + for (auto &val : ff_surround) { + ff_max_flags |= val & 0xF0; + } + auto int_max_flags = 0; + for (auto &val : int_surround) { + int_max_flags |= val; } - // use the cost of the most expensive surrounding tile - auto max_cost = *std::max_element(surround.begin(), surround.end()); coord::scene3 v{ static_cast(i * vert_distance), @@ -765,7 +788,8 @@ renderer::resources::MeshData RenderManager0::get_flow_field_mesh(const std::sha verts.push_back(world_v[0]); verts.push_back(world_v[1]); verts.push_back(world_v[2]); - verts.push_back(max_cost); + verts.push_back(ff_max_flags); + verts.push_back(int_max_flags); } } @@ -789,7 +813,9 @@ renderer::resources::MeshData RenderManager0::get_flow_field_mesh(const std::sha } renderer::resources::VertexInputInfo info{ - {renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32}, + {renderer::resources::vertex_input_t::V3F32, + renderer::resources::vertex_input_t::F32, + renderer::resources::vertex_input_t::F32}, renderer::resources::vertex_layout_t::AOS, renderer::resources::vertex_primitive_t::TRIANGLES, renderer::resources::index_t::U16}; diff --git a/libopenage/pathfinding/demo/demo_0.h b/libopenage/pathfinding/demo/demo_0.h index 7a07ebfd7e..adf5651971 100644 --- a/libopenage/pathfinding/demo/demo_0.h +++ b/libopenage/pathfinding/demo/demo_0.h @@ -92,9 +92,11 @@ class RenderManager0 { /** * Draw a flow field to the screen. * - * @param field Flow field. + * @param flow_field Flow field. + * @param int_field Integration field. */ - void show_flow_field(const std::shared_ptr &field); + void show_flow_field(const std::shared_ptr &flow_field, + const std::shared_ptr &int_field); /** * Draw the steering vectors of a flow field to the screen. @@ -160,9 +162,11 @@ class RenderManager0 { /** * Create a mesh for the flow field. * - * @param field Flow field to visualize. + * @param flow_field Flow field to visualize. + * @param int_field Integration field. */ - static renderer::resources::MeshData get_flow_field_mesh(const std::shared_ptr &field, + static renderer::resources::MeshData get_flow_field_mesh(const std::shared_ptr &flow_field, + const std::shared_ptr &int_field, size_t resolution = 2); /** diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 21aaa2c9d8..cd67e7b79f 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -96,7 +96,7 @@ void path_demo_1(const util::Path &path) { Path path_result = pathfinder->get_path(path_request); timer.stop(); - log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " ps"); + log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " us"); // Create a renderer to display the grid and path auto qtapp = std::make_shared(); @@ -133,10 +133,12 @@ void path_demo_1(const util::Path &path) { path_result = pathfinder->get_path(new_path_request); timer.stop(); - log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " ps"); + log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " us"); - // Create renderables for the waypoints of the path - render_manager->create_waypoint_tiles(path_result); + if (path_result.status == PathResult::FOUND) { + // Create renderables for the waypoints of the path + render_manager->create_waypoint_tiles(path_result); + } } else if (ev.button() == Qt::LeftButton) { // Set start cell start = coord::tile{grid_x, grid_y}; @@ -151,10 +153,12 @@ void path_demo_1(const util::Path &path) { path_result = pathfinder->get_path(new_path_request); timer.stop(); - log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " ps"); + log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " us"); - // Create renderables for the waypoints of the path - render_manager->create_waypoint_tiles(path_result); + if (path_result.status == PathResult::FOUND) { + // Create renderables for the waypoints of the path + render_manager->create_waypoint_tiles(path_result); + } } } }); @@ -163,7 +167,6 @@ void path_demo_1(const util::Path &path) { render_manager->create_waypoint_tiles(path_result); // Run the renderer pss to draw the grid and path into a window - // TODO: Make this a while (not window.should_close()) loop render_manager->run(); } @@ -449,7 +452,7 @@ void RenderManager1::create_impassible_tiles(const std::shared_ptr & auto cost_field = sector->get_cost_field(); for (size_t y = 0; y < sector_size; y++) { for (size_t x = 0; x < sector_size; x++) { - auto cost = cost_field->get_cost(coord::tile(x, y)); + auto cost = cost_field->get_cost(x, y); if (cost == COST_IMPASSABLE) { std::array tile_data{ -1.0f + x * tile_offset_width + sector_size * sector_x * tile_offset_width, diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp index 0aa8daf84b..e582345b48 100644 --- a/libopenage/pathfinding/flow_field.cpp +++ b/libopenage/pathfinding/flow_field.cpp @@ -2,6 +2,8 @@ #include "flow_field.h" +#include + #include "error/error.h" #include "log/log.h" @@ -28,16 +30,31 @@ size_t FlowField::get_size() const { return this->size; } -flow_t FlowField::get_cell(const coord::tile &pos) const { +flow_t FlowField::get_cell(const coord::tile_delta &pos) const { return this->cells.at(pos.ne + pos.se * this->size); } -flow_dir_t FlowField::get_dir(const coord::tile &pos) const { +flow_t FlowField::get_cell(size_t x, size_t y) const { + return this->cells.at(x + y * this->size); +} + +flow_t FlowField::get_cell(size_t idx) const { + return this->cells.at(idx); +} + +flow_dir_t FlowField::get_dir(const coord::tile_delta &pos) const { return static_cast(this->get_cell(pos) & FLOW_DIR_MASK); } -void FlowField::build(const std::shared_ptr &integration_field, - const std::unordered_set &target_cells) { +flow_dir_t FlowField::get_dir(size_t x, size_t y) const { + return static_cast(this->get_cell(x, y) & FLOW_DIR_MASK); +} + +flow_dir_t FlowField::get_dir(size_t idx) const { + return static_cast(this->get_cell(idx) & FLOW_DIR_MASK); +} + +void FlowField::build(const std::shared_ptr &integration_field) { ENSURE(integration_field->get_size() == this->get_size(), "integration field size " << integration_field->get_size() << "x" << integration_field->get_size() @@ -51,84 +68,102 @@ void FlowField::build(const std::shared_ptr &integration_field for (size_t x = 0; x < this->size; ++x) { size_t idx = y * this->size + x; - if (target_cells.contains(idx)) { - // Ignore target cells - continue; - } + const auto &integrate_cell = integrate_cells[idx]; + auto &flow_cell = flow_cells[idx]; - if (integrate_cells[idx].cost == INTEGRATED_COST_UNREACHABLE) { + if (integrate_cell.cost == INTEGRATED_COST_UNREACHABLE) { // Cell cannot be used as path continue; } - if (integrate_cells[idx].flags & INTEGRATE_LOS_MASK) { - // Cell is in line of sight - this->cells[idx] = this->cells[idx] | FLOW_LOS_MASK; + flow_t transfer_flags = integrate_cell.flags & FLOW_FLAGS_MASK; + flow_cell |= transfer_flags; - // we can skip calculating the flow direction as we can - // move straight to the target from this cell - this->cells[idx] = this->cells[idx] | FLOW_PATHABLE_MASK; + if (flow_cell & FLOW_TARGET_MASK) { + // target cells are pathable + flow_cell |= FLOW_PATHABLE_MASK; + + // they also have a preset flow direction so we can skip here continue; } - if (integrate_cells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { - // Cell is blocked by a line-of-sight corner - this->cells[idx] = this->cells[idx] | FLOW_WAVEFRONT_BLOCKED_MASK; - } + // Store which of the non-diagonal directions are unreachable. + // north == 0x01, east == 0x02, south == 0x04, west == 0x08 + uint8_t directions_unreachable = 0x00; // Find the neighbor with the smallest cost. - flow_dir_t direction = static_cast(this->cells[idx] & FLOW_DIR_MASK); + flow_dir_t direction = static_cast(flow_cell & FLOW_DIR_MASK); auto smallest_cost = INTEGRATED_COST_UNREACHABLE; + + // Cardinal directions if (y > 0) { auto cost = integrate_cells[idx - this->size].cost; - if (cost < smallest_cost) { - smallest_cost = cost; - direction = flow_dir_t::NORTH; + if (cost == INTEGRATED_COST_UNREACHABLE) { + directions_unreachable |= 0x01; } - } - if (x < this->size - 1 && y > 0) { - auto cost = integrate_cells[idx - this->size + 1].cost; - if (cost < smallest_cost) { + else if (cost < smallest_cost) { smallest_cost = cost; - direction = flow_dir_t::NORTH_EAST; + direction = flow_dir_t::NORTH; } } if (x < this->size - 1) { auto cost = integrate_cells[idx + 1].cost; - if (cost < smallest_cost) { + if (cost == INTEGRATED_COST_UNREACHABLE) { + directions_unreachable |= 0x02; + } + else if (cost < smallest_cost) { smallest_cost = cost; direction = flow_dir_t::EAST; } } - if (x < this->size - 1 && y < this->size - 1) { - auto cost = integrate_cells[idx + this->size + 1].cost; - if (cost < smallest_cost) { + if (y < this->size - 1) { + auto cost = integrate_cells[idx + this->size].cost; + if (cost == INTEGRATED_COST_UNREACHABLE) { + directions_unreachable |= 0x04; + } + else if (cost < smallest_cost) { smallest_cost = cost; - direction = flow_dir_t::SOUTH_EAST; + direction = flow_dir_t::SOUTH; } } - if (y < this->size - 1) { - auto cost = integrate_cells[idx + this->size].cost; + if (x > 0) { + auto cost = integrate_cells[idx - 1].cost; + if (cost == INTEGRATED_COST_UNREACHABLE) { + directions_unreachable |= 0x08; + } + else if (cost < smallest_cost) { + smallest_cost = cost; + direction = flow_dir_t::WEST; + } + } + + // Diagonal directions + if (x < this->size - 1 and y > 0 + and not(directions_unreachable & 0x01 and directions_unreachable & 0x02)) { + auto cost = integrate_cells[idx - this->size + 1].cost; if (cost < smallest_cost) { smallest_cost = cost; - direction = flow_dir_t::SOUTH; + direction = flow_dir_t::NORTH_EAST; } } - if (x > 0 && y < this->size - 1) { - auto cost = integrate_cells[idx + this->size - 1].cost; + if (x < this->size - 1 and y < this->size - 1 + and not(directions_unreachable & 0x02 and directions_unreachable & 0x04)) { + auto cost = integrate_cells[idx + this->size + 1].cost; if (cost < smallest_cost) { smallest_cost = cost; - direction = flow_dir_t::SOUTH_WEST; + direction = flow_dir_t::SOUTH_EAST; } } - if (x > 0) { - auto cost = integrate_cells[idx - 1].cost; + if (x > 0 and y < this->size - 1 + and not(directions_unreachable & 0x04 and directions_unreachable & 0x08)) { + auto cost = integrate_cells[idx + this->size - 1].cost; if (cost < smallest_cost) { smallest_cost = cost; - direction = flow_dir_t::WEST; + direction = flow_dir_t::SOUTH_WEST; } } - if (x > 0 && y > 0) { + if (x > 0 and y > 0 + and not(directions_unreachable & 0x01 and directions_unreachable & 0x08)) { auto cost = integrate_cells[idx - this->size - 1].cost; if (cost < smallest_cost) { smallest_cost = cost; @@ -137,10 +172,10 @@ void FlowField::build(const std::shared_ptr &integration_field } // Set the flow field cell to pathable. - flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; + flow_cell |= FLOW_PATHABLE_MASK; // Set the flow field cell to the direction of the smallest cost. - flow_cells[idx] = flow_cells[idx] | static_cast(direction); + flow_cell |= static_cast(direction); } } } @@ -166,9 +201,6 @@ void FlowField::build(const std::shared_ptr &integration_field // TODO: Compare integration values from other side of portal // auto &integrate_cells = integration_field->get_cells(); - // cells that are part of the portal - std::unordered_set portal_cells; - // set the direction for the flow field cells that are part of the portal if (direction == PortalDirection::NORTH_SOUTH) { bool other_is_north = entry_start.se > exit_start.se; @@ -178,7 +210,6 @@ void FlowField::build(const std::shared_ptr &integration_field auto idx = y * this->size + x; flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::NORTH); - portal_cells.insert(idx); } } else { @@ -187,7 +218,6 @@ void FlowField::build(const std::shared_ptr &integration_field auto idx = y * this->size + x; flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::SOUTH); - portal_cells.insert(idx); } } } @@ -199,7 +229,6 @@ void FlowField::build(const std::shared_ptr &integration_field auto idx = y * this->size + x; flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::EAST); - portal_cells.insert(idx); } } else { @@ -208,7 +237,6 @@ void FlowField::build(const std::shared_ptr &integration_field auto idx = y * this->size + x; flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::WEST); - portal_cells.insert(idx); } } } @@ -216,7 +244,7 @@ void FlowField::build(const std::shared_ptr &integration_field throw Error(ERR << "Invalid portal direction: " << static_cast(direction)); } - this->build(integration_field, portal_cells); + this->build(integration_field); } const std::vector &FlowField::get_cells() const { @@ -224,11 +252,32 @@ const std::vector &FlowField::get_cells() const { } void FlowField::reset() { - for (auto &cell : this->cells) { - cell = FLOW_INIT; - } + std::fill(this->cells.begin(), this->cells.end(), FLOW_INIT); log::log(DBG << "Flow field has been reset"); } +void FlowField::reset_dynamic_flags() { + flow_t mask = 0xFF & ~(FLOW_LOS_MASK); + for (flow_t &cell : this->cells) { + cell = cell & mask; + } + + log::log(DBG << "Flow field dynamic flags have been reset"); +} + +void FlowField::transfer_dynamic_flags(const std::shared_ptr &integration_field) { + auto &integrate_cells = integration_field->get_cells(); + auto &flow_cells = this->cells; + + for (size_t idx = 0; idx < integrate_cells.size(); ++idx) { + if (integrate_cells[idx].flags & INTEGRATE_LOS_MASK) { + // Cell is in line of sight + flow_cells[idx] |= FLOW_LOS_MASK; + } + } + + log::log(DBG << "Flow field dynamic flags have been transferred"); +} + } // namespace openage::path diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h index 1c1adcfe33..5dc91fed96 100644 --- a/libopenage/pathfinding/flow_field.h +++ b/libopenage/pathfinding/flow_field.h @@ -13,7 +13,7 @@ namespace openage { namespace coord { -struct tile; +struct tile_delta; } // namespace coord namespace path { @@ -46,30 +46,65 @@ class FlowField { /** * Get the flow field value at a specified position. * - * @param pos Coordinates of the cell. + * @param pos Coordinates of the cell (relative to field origin). * * @return Flowfield value at the specified position. */ - flow_t get_cell(const coord::tile &pos) const; + flow_t get_cell(const coord::tile_delta &pos) const; + + /** + * Get the flow field value at a specified position. + * + * @param x X-coordinate of the cell. + * @param y Y-coordinate of the cell. + * + * @return Flowfield value at the specified position. + */ + flow_t get_cell(size_t x, size_t y) const; + + /** + * Get the flow field direction at a specified position. + * + * @param idx Index of the cell. + * + * @return Flowfield value at the specified position. + */ + flow_t get_cell(size_t idx) const; + + /** + * Get the flow field direction at a specified position. + * + * @param pos Coordinates of the cell (relative to field origin). + * + * @return Flowfield direction at the specified position. + */ + flow_dir_t get_dir(const coord::tile_delta &pos) const; /** * Get the flow field direction at a specified position. * - * @param pos Coordinates of the cell. + * @param x X-coordinate of the cell. + * @param y Y-coordinate of the cell. * * @return Flowfield direction at the specified position. */ - flow_dir_t get_dir(const coord::tile &pos) const; + flow_dir_t get_dir(size_t x, size_t y) const; + + /** + * Get the flow field direction at a specified position. + * + * @param idx Index of the cell. + * + * @return Flowfield direction at the specified position. + */ + flow_dir_t get_dir(size_t idx) const; /** * Build the flow field. * * @param integration_field Integration field. - * @param target_cells Target cells of the flow field. These cells are ignored - * when building the field. */ - void build(const std::shared_ptr &integration_field, - const std::unordered_set &target_cells = {}); + void build(const std::shared_ptr &integration_field); /** * Build the flow field for a portal. @@ -96,6 +131,32 @@ class FlowField { */ void reset(); + /** + * Reset all flags that are dependent on the path target location. + * + * These flags should be removed when the field is cached and reused for + * other targets. + * + * Relevant flags are: + * - FLOW_LOS_MASK + * - FLOW_WAVEFRONT_BLOCKED_MASK + */ + void reset_dynamic_flags(); + + /** + * Transfer dynamic flags from an integration field. + * + * These flags should be transferred when the field is copied from cache. + * Flow field directions are not altered. + * + * Relevant flags are: + * - FLOW_LOS_MASK + * - FLOW_WAVEFRONT_BLOCKED_MASK + * + * @param integration_field Integration field. + */ + void transfer_dynamic_flags(const std::shared_ptr &integration_field); + private: /** * Side length of the field. diff --git a/libopenage/pathfinding/grid.h b/libopenage/pathfinding/grid.h index 83b4c2b721..8f24743433 100644 --- a/libopenage/pathfinding/grid.h +++ b/libopenage/pathfinding/grid.h @@ -23,7 +23,7 @@ class Grid { * Create a new empty grid of width x height sectors with a specified size. * * @param id ID of the grid. - * @param size Size of the grid (width x height). + * @param size Size of the grid in sectors (width x height). * @param sector_size Side length of each sector. */ Grid(grid_id_t id, @@ -34,7 +34,7 @@ class Grid { * Create a grid of width x height sectors from a list of existing sectors. * * @param id ID of the grid. - * @param size Size of the grid (width x height). + * @param size Size of the grid in sectors (width x height). * @param sectors Existing sectors. */ Grid(grid_id_t id, @@ -49,16 +49,16 @@ class Grid { grid_id_t get_id() const; /** - * Get the size of the grid. + * Get the size of the grid (in number of sectors). * - * @return Size of the grid (width x height). + * @return Size of the grid (in number of sectors) (width x height). */ const util::Vector2s &get_size() const; /** - * Get the side length of the sectors on the grid. + * Get the side length of the sectors on the grid (in number of cells). * - * @return Sector side length. + * @return Sector side length (in number of cells). */ size_t get_sector_size() const; diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp index bc2745879f..26134506b8 100644 --- a/libopenage/pathfinding/integration_field.cpp +++ b/libopenage/pathfinding/integration_field.cpp @@ -25,68 +25,247 @@ size_t IntegrationField::get_size() const { return this->size; } -const integrated_t &IntegrationField::get_cell(const coord::tile &pos) const { +const integrated_t &IntegrationField::get_cell(const coord::tile_delta &pos) const { return this->cells.at(pos.ne + pos.se * this->size); } +const integrated_t &IntegrationField::get_cell(size_t x, size_t y) const { + return this->cells.at(x + y * this->size); +} + const integrated_t &IntegrationField::get_cell(size_t idx) const { return this->cells.at(idx); } std::vector IntegrationField::integrate_los(const std::shared_ptr &cost_field, - const coord::tile &target) { + const coord::tile_delta &target) { ENSURE(cost_field->get_size() == this->get_size(), "cost field size " << cost_field->get_size() << "x" << cost_field->get_size() << " does not match integration field size " << this->get_size() << "x" << this->get_size()); - // Store the wavefront_blocked cells - std::vector wavefront_blocked; + ENSURE(target.ne >= 0 + and target.se >= 0 + and target.ne < static_cast(this->size) + and target.se < static_cast(this->size), + "target cell (" << target.ne << ", " << target.se << ") " + << "is out of bounds for integration field of size " + << this->size << "x" << this->size); + + std::vector start_cells; + integrated_cost_t start_cost = INTEGRATED_COST_START; // Target cell index auto target_idx = target.ne + target.se * this->size; - // Lookup table for cells that have been found - std::unordered_set found; - found.reserve(this->size * this->size); + this->cells[target_idx].cost = start_cost; + this->cells[target_idx].flags |= INTEGRATE_TARGET_MASK; + + if (cost_field->get_cost(target_idx) > COST_MIN) { + // Do a preliminary LOS integration wave for targets that have cost > min cost + // This avoids the bresenham's line algorithm calculations + // (which wouldn't return accurate results for blocker == target) + // and makes sure that sorrounding cells that are min cost are considered + // in line-of-sight. + + this->cells[target_idx].flags |= INTEGRATE_FOUND_MASK; + this->cells[target_idx].flags |= INTEGRATE_LOS_MASK; + + // Add neighbors to current wave + if (target.se > 0) { + start_cells.push_back(target_idx - this->size); + } + if (target.ne > 0) { + start_cells.push_back(target_idx - 1); + } + if (target.se < static_cast(this->size - 1)) { + start_cells.push_back(target_idx + this->size); + } + if (target.ne < static_cast(this->size - 1)) { + start_cells.push_back(target_idx + 1); + } + + // Increment wave cost as we technically handled the first wave in this block + start_cost += 1; + } + else { + // Move outwards from the target cell, updating the integration field + start_cells.push_back(target_idx); + } + + return this->integrate_los(cost_field, target, start_cost, std::move(start_cells)); +} + +std::vector IntegrationField::integrate_los(const std::shared_ptr &cost_field, + const std::shared_ptr &other, + sector_id_t other_sector_id, + const std::shared_ptr &portal, + const coord::tile_delta &target) { + ENSURE(cost_field->get_size() == this->get_size(), + "cost field size " + << cost_field->get_size() << "x" << cost_field->get_size() + << " does not match integration field size " + << this->get_size() << "x" << this->get_size()); + + std::vector wavefront_blocked_portal; + + std::vector start_cells; + + auto exit_start = portal->get_exit_start(other_sector_id); + auto exit_end = portal->get_exit_end(other_sector_id); + auto entry_start = portal->get_entry_start(other_sector_id); + + auto x_diff = exit_start.ne - entry_start.ne; + auto y_diff = exit_start.se - entry_start.se; + + auto &cost_cells = cost_field->get_costs(); + auto &other_cells = other->get_cells(); + + // transfer masks for flags from the other side of the portal + // only LOS and wavefront blocked flags are relevant + integrated_flags_t transfer_mask = INTEGRATE_LOS_MASK | INTEGRATE_WAVEFRONT_BLOCKED_MASK; + + // every portal cell is a target cell + for (auto y = exit_start.se; y <= exit_end.se; ++y) { + for (auto x = exit_start.ne; x <= exit_end.ne; ++x) { + // cell index on the exit side of the portal + auto target_idx = x + y * this->size; + + // cell index on the entry side of the portal + auto entry_idx = x - x_diff + (y - y_diff) * this->size; + + // Set the cost of all target cells to the start value + this->cells[target_idx].cost = INTEGRATED_COST_START; + this->cells[target_idx].flags = other_cells[entry_idx].flags & transfer_mask; + + this->cells[target_idx].flags |= INTEGRATE_TARGET_MASK; + + if (not(this->cells[target_idx].flags & transfer_mask)) { + // If neither LOS nor wavefront blocked flags are set for the portal entry, + // the portal exit cell doesn't affect the LOS and we can skip further checks + continue; + } + + // Get the cost of the current cell + auto cell_cost = cost_cells[target_idx]; + + if (cell_cost > COST_MIN or this->cells[target_idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { + // cell blocks line of sight + + // set the blocked flag for the cell if it wasn't set already + this->cells[target_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; + wavefront_blocked_portal.push_back(target_idx); + + // set the found flag for the cell, so that the start costs + // are not changed in the main LOS integration + this->cells[target_idx].flags |= INTEGRATE_FOUND_MASK; + + // check each neighbor for a corner + auto corners = this->get_los_corners(cost_field, target, coord::tile_delta(x, y)); + for (auto &corner : corners) { + // draw a line from the corner to the edge of the field + // to get the cells blocked by the corner + auto blocked_cells = this->bresenhams_line(target, corner.first, corner.second); + for (auto blocked_idx : blocked_cells) { + if (cost_cells[blocked_idx] > COST_MIN) { + // stop if blocked_idx is not min cost + // because this idx may create a new corner + break; + } + // set the blocked flag for the cell + this->cells[blocked_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; + + // clear los flag if it was set + this->cells[blocked_idx].flags &= ~INTEGRATE_LOS_MASK; + + wavefront_blocked_portal.push_back(blocked_idx); + } + } + continue; + } + + // the cell has the LOS flag and is added to the start cells + start_cells.push_back(target_idx); + } + } + + if (start_cells.empty()) { + // Main LOS integration will not enter its loop + // so we can take a shortcut and just return the + // wavefront blocked cells we already found + return wavefront_blocked_portal; + } + + // Call main LOS integration function + auto wavefront_blocked_main = this->integrate_los(cost_field, + target, + INTEGRATED_COST_START, + std::move(start_cells)); + wavefront_blocked_main.insert(wavefront_blocked_main.end(), + wavefront_blocked_portal.begin(), + wavefront_blocked_portal.end()); + return wavefront_blocked_main; +} + +std::vector IntegrationField::integrate_los(const std::shared_ptr &cost_field, + const coord::tile_delta &target, + integrated_cost_t start_cost, + std::vector &&start_wave) { + ENSURE(cost_field->get_size() == this->get_size(), + "cost field size " + << cost_field->get_size() << "x" << cost_field->get_size() + << " does not match integration field size " + << this->get_size() << "x" << this->get_size()); + + // Store the wavefront_blocked cells + std::vector wavefront_blocked; // Cells that still have to be visited by the current wave - std::deque current_wave; + std::vector current_wave = std::move(start_wave); // Cells that have to be visited in the next wave - std::deque next_wave; + std::vector next_wave; + + // Preallocate ~30% of the field size for the wavefront + // This reduces the number of reallocations on push_back operations + // TODO: Find "optimal" value for reserve + current_wave.reserve(this->size * 3); + next_wave.reserve(this->size * 3); // Cost of the current wave - integrated_cost_t cost = INTEGRATED_COST_START; + integrated_cost_t wave_cost = start_cost; + + // Get the cost field values + auto &cost_cells = cost_field->get_costs(); + auto &integrate_cells = this->cells; - // Move outwards from the target cell, updating the integration field - current_wave.push_back(target_idx); do { - while (!current_wave.empty()) { - auto idx = current_wave.front(); - current_wave.pop_front(); + for (size_t i = 0; i < current_wave.size(); ++i) { + // inner loop: handle a wave + auto idx = current_wave[i]; + auto &cell = integrate_cells[idx]; - if (found.contains(idx)) { - // Skip cells that have already been found + if (cell.flags & INTEGRATE_FOUND_MASK) { + // Skip cells that are already in the line of sight continue; } - else if (this->cells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { + else if (cell.flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { // Stop at cells that are blocked by a LOS corner - this->cells[idx].cost = cost - 1 + cost_field->get_cost(idx); - found.insert(idx); + cell.cost = wave_cost - 1 + cost_cells[idx]; + cell.flags |= INTEGRATE_FOUND_MASK; continue; } // Add the current cell to the found cells - found.insert(idx); + cell.flags |= INTEGRATE_FOUND_MASK; // Get the x and y coordinates of the current cell auto x = idx % this->size; auto y = idx / this->size; // Get the cost of the current cell - auto cell_cost = cost_field->get_cost(idx); + auto cell_cost = cost_cells[idx]; if (cell_cost > COST_MIN) { // cell blocks line of sight @@ -94,62 +273,69 @@ std::vector IntegrationField::integrate_los(const std::shared_ptrcells[idx].cost = cost - 1 + cost_field->get_cost(idx); + cell.cost = wave_cost - 1 + cell_cost; + cell.flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; } // check each neighbor for a corner - auto corners = this->get_los_corners(cost_field, target, coord::tile(x, y)); + auto corners = this->get_los_corners(cost_field, target, coord::tile_delta(x, y)); for (auto &corner : corners) { // draw a line from the corner to the edge of the field // to get the cells blocked by the corner auto blocked_cells = this->bresenhams_line(target, corner.first, corner.second); - for (auto &blocked_idx : blocked_cells) { - if (cost_field->get_cost(blocked_idx) > COST_MIN) { + for (auto blocked_idx : blocked_cells) { + if (cost_cells[blocked_idx] > COST_MIN) { // stop if blocked_idx is impassable break; } // set the blocked flag for the cell - this->cells[blocked_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; + integrate_cells[blocked_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; // clear los flag if it was set - this->cells[blocked_idx].flags &= ~INTEGRATE_LOS_MASK; + integrate_cells[blocked_idx].flags &= ~INTEGRATE_LOS_MASK; + + wavefront_blocked.push_back(blocked_idx); } - wavefront_blocked.insert(wavefront_blocked.end(), blocked_cells.begin(), blocked_cells.end()); } continue; } // The cell is in the line of sight at min cost // Set the LOS flag and cost - this->cells[idx].cost = cost; - this->cells[idx].flags |= INTEGRATE_LOS_MASK; + cell.cost = wave_cost; + cell.flags |= INTEGRATE_LOS_MASK; // Search the neighbors of the current cell if (y > 0) { - next_wave.push_back(idx - this->size); + auto neighbor_idx = idx - this->size; + next_wave.push_back(neighbor_idx); } if (x > 0) { - next_wave.push_back(idx - 1); + auto neighbor_idx = idx - 1; + next_wave.push_back(neighbor_idx); } if (y < this->size - 1) { - next_wave.push_back(idx + this->size); + auto neighbor_idx = idx + this->size; + next_wave.push_back(neighbor_idx); } if (x < this->size - 1) { - next_wave.push_back(idx + 1); + auto neighbor_idx = idx + 1; + next_wave.push_back(neighbor_idx); } } // increment the cost and advance the wavefront outwards - cost += 1; + wave_cost += 1; current_wave.swap(next_wave); + next_wave.clear(); } - while (!current_wave.empty()); + while (not current_wave.empty()); return wavefront_blocked; } void IntegrationField::integrate_cost(const std::shared_ptr &cost_field, - const coord::tile &target) { + const coord::tile_delta &target) { ENSURE(cost_field->get_size() == this->get_size(), "cost field size " << cost_field->get_size() << "x" << cost_field->get_size() @@ -161,6 +347,7 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie // Move outwards from the target cell, updating the integration field this->cells[target_idx].cost = INTEGRATED_COST_START; + this->cells[target_idx].flags |= INTEGRATE_TARGET_MASK; this->integrate_cost(cost_field, {target_idx}); } @@ -184,6 +371,7 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie // Set the cost of all target cells to the start value this->cells[target_idx].cost = INTEGRATED_COST_START; + this->cells[target_idx].flags |= INTEGRATE_TARGET_MASK; start_cells.push_back(target_idx); // TODO: Transfer flags and cost from the other integration field @@ -196,58 +384,65 @@ void IntegrationField::integrate_cost(const std::shared_ptr &cost_fie void IntegrationField::integrate_cost(const std::shared_ptr &cost_field, std::vector &&start_cells) { - // Lookup table for cells that are in the open list - std::unordered_set in_list; - in_list.reserve(this->size * this->size); + // Cells that still have to be visited by the current wave + std::vector current_wave = std::move(start_cells); + + // Cells that have to be visited in the next wave + std::vector next_wave; - // Cells that still have to be visited - // they may be visited multiple times - std::deque open_list; + // Preallocate ~30% of the field size for the wavefront + // This reduces the number of reallocations on push_back operations + // TODO: Find "optimal" value for reserve + current_wave.reserve(this->size * 3); + next_wave.reserve(this->size * 3); - // Stores neighbors of the current cell - std::vector neighbors; - neighbors.reserve(4); + // Get the cost field values + auto &cost_cells = cost_field->get_costs(); // Move outwards from the wavefront, updating the integration field - open_list.insert(open_list.end(), start_cells.begin(), start_cells.end()); - while (!open_list.empty()) { - auto idx = open_list.front(); - open_list.pop_front(); - - // Get the x and y coordinates of the current cell - auto x = idx % this->size; - auto y = idx / this->size; + while (not current_wave.empty()) { + for (size_t i = 0; i < current_wave.size(); ++i) { + auto idx = current_wave[i]; - auto integrated_current = this->cells.at(idx).cost; + // Get the x and y coordinates of the current cell + auto x = idx % this->size; + auto y = idx / this->size; - // Get the neighbors of the current cell - if (y > 0) { - neighbors.push_back(idx - this->size); - } - if (x > 0) { - neighbors.push_back(idx - 1); - } - if (y < this->size - 1) { - neighbors.push_back(idx + this->size); - } - if (x < this->size - 1) { - neighbors.push_back(idx + 1); - } + auto integrated_current = this->cells[idx].cost; - // Update the integration field of the neighboring cells - for (auto &neighbor_idx : neighbors) { - this->update_neighbor(neighbor_idx, - cost_field->get_cost(neighbor_idx), - integrated_current, - open_list, - in_list); + // Get the neighbors of the current cell + if (y > 0) { + auto neighbor_idx = idx - this->size; + this->update_neighbor(neighbor_idx, + cost_cells[neighbor_idx], + integrated_current, + next_wave); + } + if (x > 0) { + auto neighbor_idx = idx - 1; + this->update_neighbor(neighbor_idx, + cost_cells[neighbor_idx], + integrated_current, + next_wave); + } + if (y < this->size - 1) { + auto neighbor_idx = idx + this->size; + this->update_neighbor(neighbor_idx, + cost_cells[neighbor_idx], + integrated_current, + next_wave); + } + if (x < this->size - 1) { + auto neighbor_idx = idx + 1; + this->update_neighbor(neighbor_idx, + cost_cells[neighbor_idx], + integrated_current, + next_wave); + } } - // Clear the neighbors vector - neighbors.clear(); - - // Remove the current cell from the open list lookup table - in_list.erase(idx); + current_wave.swap(next_wave); + next_wave.clear(); } } @@ -256,17 +451,24 @@ const std::vector &IntegrationField::get_cells() const { } void IntegrationField::reset() { - for (auto &cell : this->cells) { - cell = INTEGRATE_INIT; - } + std::fill(this->cells.begin(), this->cells.end(), INTEGRATE_INIT); + log::log(DBG << "Integration field has been reset"); } +void IntegrationField::reset_dynamic_flags() { + integrated_flags_t mask = 0xFF & ~(INTEGRATE_LOS_MASK | INTEGRATE_WAVEFRONT_BLOCKED_MASK | INTEGRATE_FOUND_MASK); + for (integrated_t &cell : this->cells) { + cell.flags = cell.flags & mask; + } + + log::log(DBG << "Integration field dynamic flags have been reset"); +} + void IntegrationField::update_neighbor(size_t idx, cost_t cell_cost, integrated_cost_t integrated_cost, - std::deque &open_list, - std::unordered_set &in_list) { + std::vector &wave) { ENSURE(cell_cost > COST_INIT, "cost field cell value must be non-zero"); // Check if the cell is impassable @@ -276,21 +478,18 @@ void IntegrationField::update_neighbor(size_t idx, } auto cost = integrated_cost + cell_cost; - if (cost < this->cells.at(idx).cost) { + if (cost < this->cells[idx].cost) { // If the new integration value is smaller than the current one, // update the cell and add it to the open list this->cells[idx].cost = cost; - if (not in_list.contains(idx)) { - in_list.insert(idx); - open_list.push_back(idx); - } + wave.push_back(idx); } } std::vector> IntegrationField::get_los_corners(const std::shared_ptr &cost_field, - const coord::tile &target, - const coord::tile &blocker) { + const coord::tile_delta &target, + const coord::tile_delta &blocker) { std::vector> corners; // Get the cost of the blocking cell's neighbors @@ -308,16 +507,16 @@ std::vector> IntegrationField::get_los_corners(const std::sh // Get neighbor costs (if they exist) if (blocker.se > 0) { - top_cost = cost_field->get_cost(coord::tile{blocker.ne, blocker.se - 1}); + top_cost = cost_field->get_cost(blocker.ne, blocker.se - 1); } if (blocker.ne > 0) { - left_cost = cost_field->get_cost(coord::tile{blocker.ne - 1, blocker.se}); + left_cost = cost_field->get_cost(blocker.ne - 1, blocker.se); } if (static_cast(blocker.se) < this->size - 1) { - bottom_cost = cost_field->get_cost(coord::tile{blocker.ne, blocker.se + 1}); + bottom_cost = cost_field->get_cost(blocker.ne, blocker.se + 1); } if (static_cast(blocker.ne) < this->size - 1) { - right_cost = cost_field->get_cost(coord::tile{blocker.ne + 1, blocker.se}); + right_cost = cost_field->get_cost(blocker.ne + 1, blocker.se); } // Check which corners are blocking LOS @@ -423,7 +622,7 @@ std::vector> IntegrationField::get_los_corners(const std::sh return corners; } -std::vector IntegrationField::bresenhams_line(const coord::tile &target, +std::vector IntegrationField::bresenhams_line(const coord::tile_delta &target, int corner_x, int corner_y) { std::vector cells; diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h index 183fb9cff6..c9149cc82f 100644 --- a/libopenage/pathfinding/integration_field.h +++ b/libopenage/pathfinding/integration_field.h @@ -13,7 +13,7 @@ namespace openage { namespace coord { -struct tile; +struct tile_delta; } // namespace coord namespace path { @@ -42,10 +42,19 @@ class IntegrationField { /** * Get the integration value at a specified position. * - * @param pos Coordinates of the cell. + * @param pos Coordinates of the cell (relative to field origin). * @return Integration value at the specified position. */ - const integrated_t &get_cell(const coord::tile &pos) const; + const integrated_t &get_cell(const coord::tile_delta &pos) const; + + /** + * Get the integration value at a specified position. + * + * @param x X-coordinate of the cell. + * @param y Y-coordinate of the cell. + * @return Integration value at the specified position. + */ + const integrated_t &get_cell(size_t x, size_t y) const; /** * Get the integration value at a specified position. @@ -58,16 +67,58 @@ class IntegrationField { /** * Calculate the line-of-sight integration flags for a target cell. * + * The target cell coordinates must lie within the field. + * * Returns a list of cells that are flagged as "wavefront blocked". These cells * can be used as a starting point for the cost integration. * * @param cost_field Cost field to integrate. - * @param target Coordinates of the target cell. + * @param target Coordinates of the target cell (relative to field origin). * * @return Cells flagged as "wavefront blocked". */ std::vector integrate_los(const std::shared_ptr &cost_field, - const coord::tile &target); + const coord::tile_delta &target); + + /** + * Calculate the line-of-sight integration flags starting from a portal to another + * integration field. + * + * Returns a list of cells that are flagged as "wavefront blocked". These cells + * can be used as a starting point for the cost integration. + * + * @param cost_field Cost field to integrate. + * @param other Integration field of the other sector. + * @param other_sector_id Sector ID of the other integration field. + * @param portal Portal connecting the two fields. + * @param target Coordinates of the target cell (relative to field origin). + * + * @return Cells flagged as "wavefront blocked". + */ + std::vector integrate_los(const std::shared_ptr &cost_field, + const std::shared_ptr &other, + sector_id_t other_sector_id, + const std::shared_ptr &portal, + const coord::tile_delta &target); + + /** + * Calculate the line-of-sight integration flags for a target cell. + * + * Returns a list of cells that are flagged as "wavefront blocked". These cells + * can be used as a starting point for the cost integration. + * + * @param cost_field Cost field to integrate. + * @param target Coordinates of the target cell (relative to field origin). + * @param start_cost Integration cost for the start wave. + * @param start_wave Cells used for the first LOS integration wave. The wavefront + * expands outwards from these cells. + * + * @return Cells flagged as "wavefront blocked". + */ + std::vector integrate_los(const std::shared_ptr &cost_field, + const coord::tile_delta &target, + integrated_cost_t start_cost, + std::vector &&start_wave); /** * Calculate the cost integration field starting from a target cell. @@ -76,7 +127,7 @@ class IntegrationField { * @param target Coordinates of the target cell. */ void integrate_cost(const std::shared_ptr &cost_field, - const coord::tile &target); + const coord::tile_delta &target); /** * Calculate the cost integration field starting from a portal to another @@ -111,6 +162,18 @@ class IntegrationField { */ void reset(); + /** + * Reset all flags that are dependent on the path target location. These + * flags should be removed when the field is cached and reused for + * other targets. + * + * Relevant flags are: + * - INTEGRATE_LOS_MASK + * - INTEGRATE_WAVEFRONT_BLOCKED_MASK + * - INTEGRATE_FOUND_MASK + */ + void reset_dynamic_flags(); + private: /** * Update a neigbor cell during the cost integration process. @@ -118,29 +181,27 @@ class IntegrationField { * @param idx Index of the neighbor cell that is updated. * @param cell_cost Cost of the neighbor cell from the cost field. * @param integrated_cost Current integrated cost of the updating cell in the integration field. - * @param open_list List of cells to be updated. - * @param in_list Set of cells that have been updated. + * @param wave List of cells that are part of the next wavefront. * * @return New integration value of the cell. */ void update_neighbor(size_t idx, cost_t cell_cost, integrated_cost_t integrated_cost, - std::deque &open_list, - std::unordered_set &in_list); + std::vector &wave); /** * Get the LOS corners around a cell. * * @param cost_field Cost field to integrate. - * @param target Cell coordinates of the target. - * @param blocker Cell coordinates of the cell blocking LOS. + * @param target Cell coordinates of the target (relative to field origin). + * @param blocker Cell coordinates of the cell blocking LOS (relative to field origin). * * @return Field coordinates of the LOS corners. */ std::vector> get_los_corners(const std::shared_ptr &cost_field, - const coord::tile &target, - const coord::tile &blocker); + const coord::tile_delta &target, + const coord::tile_delta &blocker); /** * Get the cells in a bresenham's line between the corner cell and the field edge. @@ -150,13 +211,13 @@ class IntegrationField { * the cells between two arbitrary points. We do this because the intersection * point with the field edge is unknown. * - * @param target Cell coordinates of the target. + * @param target Cell coordinates of the target (relative to field origin). * @param corner_x X field coordinate edge of the LOS corner. * @param corner_y Y field coordinate edge of the LOS corner. * * @return Cell indices of the LOS line. */ - std::vector bresenhams_line(const coord::tile &target, + std::vector bresenhams_line(const coord::tile_delta &target, int corner_x, int corner_y); diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index 28f8a829ca..265c5285cc 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -2,34 +2,92 @@ #include "integrator.h" +#include "log/log.h" + #include "pathfinding/cost_field.h" #include "pathfinding/flow_field.h" #include "pathfinding/integration_field.h" +#include "pathfinding/portal.h" namespace openage::path { std::shared_ptr Integrator::integrate(const std::shared_ptr &cost_field, - const coord::tile &target) { + const coord::tile_delta &target, + bool with_los) { auto integration_field = std::make_shared(cost_field->get_size()); - auto wavefront_blocked = integration_field->integrate_los(cost_field, target); - integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); + log::log(DBG << "Integrating cost field for target coord " << target); + if (with_los) { + log::log(SPAM << "Performing LOS pass"); + auto wavefront_blocked = integration_field->integrate_los(cost_field, target); + integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); + } + else { + log::log(SPAM << "Skipping LOS pass"); + integration_field->integrate_cost(cost_field, target); + } return integration_field; } std::shared_ptr Integrator::integrate(const std::shared_ptr &cost_field, + const std::shared_ptr &other, sector_id_t other_sector_id, - const std::shared_ptr &portal) { + const std::shared_ptr &portal, + const coord::tile_delta &target, + bool with_los) { + auto cache_key = std::make_pair(portal->get_id(), other_sector_id); + auto cached = this->field_cache.find(cache_key); + if (cached != this->field_cache.end()) { + log::log(DBG << "Using cached integration field for portal " << portal->get_id() + << " from sector " << other_sector_id); + if (with_los) { + log::log(SPAM << "Performing LOS pass on cached field"); + + // Make a copy of the cached field to avoid modifying the cached field + auto integration_field = std::make_shared(*cached->second.first); + + // Only integrate LOS; leave the rest of the field as is + integration_field->integrate_los(cost_field, other, other_sector_id, portal, target); + + return integration_field; + } + return cached->second.first; + } + + log::log(DBG << "Integrating cost field for portal " << portal->get_id() + << " from sector " << other_sector_id); + + // Create a new integration field auto integration_field = std::make_shared(cost_field->get_size()); - integration_field->integrate_cost(cost_field, other_sector_id, portal); + + // LOS pass + std::vector wavefront_blocked; + if (with_los) { + log::log(SPAM << "Performing LOS pass"); + wavefront_blocked = integration_field->integrate_los(cost_field, other, other_sector_id, portal, target); + } + + // Cost integration + if (wavefront_blocked.empty()) { + // No LOS pass or no blocked cells + // use the portal as the target + integration_field->integrate_cost(cost_field, other_sector_id, portal); + } + else { + // LOS pass was performed and some cells were blocked + // use the blocked cells as the start wave + integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); + } return integration_field; } std::shared_ptr Integrator::build(const std::shared_ptr &integration_field) { auto flow_field = std::make_shared(integration_field->get_size()); + + log::log(DBG << "Building flow field from integration field"); flow_field->build(integration_field); return flow_field; @@ -38,27 +96,93 @@ std::shared_ptr Integrator::build(const std::shared_ptr Integrator::build(const std::shared_ptr &integration_field, const std::shared_ptr &other, sector_id_t other_sector_id, - const std::shared_ptr &portal) { + const std::shared_ptr &portal, + bool with_los) { + auto cache_key = std::make_pair(portal->get_id(), other_sector_id); + auto cached = this->field_cache.find(cache_key); + if (cached != this->field_cache.end()) { + log::log(DBG << "Using cached flow field for portal " << portal->get_id() + << " from sector " << other_sector_id); + if (with_los) { + log::log(SPAM << "Transferring LOS flags to cached flow field"); + + // Make a copy of the cached flow field + auto flow_field = std::make_shared(*cached->second.second); + + // Transfer the LOS flags to the flow field + flow_field->transfer_dynamic_flags(integration_field); + + return flow_field; + } + + return cached->second.second; + } + + log::log(DBG << "Building flow field for portal " << portal->get_id() + << " from sector " << other_sector_id); + auto flow_field = std::make_shared(integration_field->get_size()); flow_field->build(integration_field, other, other_sector_id, portal); return flow_field; } -Integrator::build_return_t Integrator::build(const std::shared_ptr &cost_field, - const coord::tile &target) { +Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_field, + const coord::tile_delta &target) { auto integration_field = this->integrate(cost_field, target); auto flow_field = this->build(integration_field); return std::make_pair(integration_field, flow_field); } -Integrator::build_return_t Integrator::build(const std::shared_ptr &cost_field, - const std::shared_ptr &other_integration_field, - sector_id_t other_sector_id, - const std::shared_ptr &portal) { - auto integration_field = this->integrate(cost_field, other_sector_id, portal); - auto flow_field = this->build(integration_field, other_integration_field, other_sector_id, portal); +Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_field, + const std::shared_ptr &other, + sector_id_t other_sector_id, + const std::shared_ptr &portal, + const coord::tile_delta &target, + bool with_los) { + auto cache_key = std::make_pair(portal->get_id(), other_sector_id); + auto cached = this->field_cache.find(cache_key); + if (cached != this->field_cache.end()) { + log::log(DBG << "Using cached integration and flow fields for portal " << portal->get_id() + << " from sector " << other_sector_id); + if (with_los) { + log::log(SPAM << "Performing LOS pass on cached field"); + + // Make a copy of the cached integration field + auto integration_field = std::make_shared(*cached->second.first); + + // Only integrate LOS; leave the rest of the field as is + integration_field->integrate_los(cost_field, other, other_sector_id, portal, target); + + log::log(SPAM << "Transferring LOS flags to cached flow field"); + + // Make a copy of the cached flow field + auto flow_field = std::make_shared(*cached->second.second); + + // Transfer the LOS flags to the flow field + flow_field->transfer_dynamic_flags(integration_field); + + return std::make_pair(integration_field, flow_field); + } + + return cached->second; + } + + auto integration_field = this->integrate(cost_field, other, other_sector_id, portal, target, with_los); + auto flow_field = this->build(integration_field, other, other_sector_id, portal); + + log::log(DBG << "Caching integration and flow fields for portal ID: " << portal->get_id() + << ", sector ID: " << other_sector_id); + + // Copy the fields to the cache. + std::shared_ptr cached_integration_field = std::make_shared(*integration_field); + cached_integration_field->reset_dynamic_flags(); + + std::shared_ptr cached_flow_field = std::make_shared(*flow_field); + cached_flow_field->reset_dynamic_flags(); + + this->field_cache[cache_key] = std::make_pair(cached_integration_field, cached_flow_field); return std::make_pair(integration_field, flow_field); } diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index 027f627274..c27d3c1eb1 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -4,13 +4,15 @@ #include #include +#include #include "pathfinding/types.h" +#include "util/hash.h" namespace openage { namespace coord { -struct tile; +struct tile_delta; } // namespace coord namespace path { @@ -30,26 +32,40 @@ class Integrator { /** * Integrate the cost field for a target. * + * This should be used for the field containing the target cell. + * The target coordinates must lie within the boundaries of the cost field. + * * @param cost_field Cost field. * @param target Coordinates of the target cell. + * @param with_los If true an LOS pass is performed before cost integration. * * @return Integration field. */ std::shared_ptr integrate(const std::shared_ptr &cost_field, - const coord::tile &target); + const coord::tile_delta &target, + bool with_los = true); /** * Integrate the cost field from a portal. * + * This should be used for the fields on the portal path that are not the target field. + * The target coordinates must be relative to the origin of the sector the cost field belongs to. + * * @param cost_field Cost field. + * @param other Integration field of the other side of the portal. * @param other_sector_id Sector ID of the other side of the portal. * @param portal Portal. + * @param target Coordinates of the target cell, relative to the integration field origin. + * @param with_los If true an LOS pass is performed before cost integration. * * @return Integration field. */ std::shared_ptr integrate(const std::shared_ptr &cost_field, + const std::shared_ptr &other, sector_id_t other_sector_id, - const std::shared_ptr &portal); + const std::shared_ptr &portal, + const coord::tile_delta &target, + bool with_los = true); /** * Build the flow field from an integration field. @@ -67,42 +83,70 @@ class Integrator { * @param other Integration field of the other side of the portal. * @param other_sector_id Sector ID of the other side of the portal. * @param portal Portal. + * @param with_los If true LOS flags are calculated if the flow field is in cache. * * @return Flow field. */ std::shared_ptr build(const std::shared_ptr &integration_field, const std::shared_ptr &other, sector_id_t other_sector_id, - const std::shared_ptr &portal); + const std::shared_ptr &portal, + bool with_los = true); - using build_return_t = std::pair, std::shared_ptr>; + using get_return_t = std::pair, std::shared_ptr>; /** - * Build the integration field and flow field for a target. + * Get the integration field and flow field for a target. * * @param cost_field Cost field. * @param target Coordinates of the target cell. * - * @return Flow field. + * @return Integration field and flow field. */ - build_return_t build(const std::shared_ptr &cost_field, - const coord::tile &target); + get_return_t get(const std::shared_ptr &cost_field, + const coord::tile_delta &target); /** - * Build the integration field and flow field from a portal. + * Get the integration field and flow field from a portal. * * @param cost_field Cost field. - * @param other_integration_field Integration field of the other side of the portal. + * @param other Integration field of the other side of the portal. * @param other_sector_id Sector ID of the other side of the portal. * @param portal Portal. + * @param target Coordinates of the target cell, relative to the integration field origin. + * @param with_los If true an LOS pass is performed before cost integration. + * + * @return Integration field and flow field. */ - build_return_t build(const std::shared_ptr &cost_field, - const std::shared_ptr &other_integration_field, - sector_id_t other_sector_id, - const std::shared_ptr &portal); + get_return_t get(const std::shared_ptr &cost_field, + const std::shared_ptr &other, + sector_id_t other_sector_id, + const std::shared_ptr &portal, + const coord::tile_delta &target, + bool with_los = true); private: - // TODO: Cache created flow fields. + /** + * Hash function for the field cache. + */ + struct pair_hash { + template + std::size_t operator()(const std::pair &pair) const { + return util::hash_combine(std::hash{}(pair.first), std::hash{}(pair.second)); + } + }; + + /** + * Cache for already computed fields. + * + * Key is the portal ID and the sector ID from which the field was entered. Fields that are cached are + * cleared of dynamic flags, i.e. wavefront or LOS flags. These have to be recalculated + * when the field is reused. + */ + std::unordered_map, + get_return_t, + pair_hash> + field_cache; }; } // namespace path diff --git a/libopenage/pathfinding/path.h b/libopenage/pathfinding/path.h index 8d64f5914f..d1c61ac325 100644 --- a/libopenage/pathfinding/path.h +++ b/libopenage/pathfinding/path.h @@ -5,6 +5,7 @@ #include #include "coord/tile.h" +#include "pathfinding/types.h" namespace openage::path { @@ -13,11 +14,11 @@ namespace openage::path { * Path request for the pathfinder. */ struct PathRequest { - // ID of the grid to use for pathfinding. + /// ID of the grid to use for pathfinding. size_t grid_id; - // Start position of the path. + /// Start position of the path. coord::tile start; - // Target position of the path. + /// Target position of the path. coord::tile target; }; @@ -25,11 +26,13 @@ struct PathRequest { * Path found by the pathfinder. */ struct Path { - // ID of the grid to used for pathfinding. + /// ID of the grid to used for pathfinding. size_t grid_id; - // Waypoints of the path. - // First waypoint is the start position of the path request. - // Last waypoint is the target position of the path request. + /// Status + PathResult status; + /// Waypoints of the path. + /// First waypoint is the start position of the path request. + /// Last waypoint is the target position of the path request. std::vector waypoints; }; diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 7e09278f7c..288f7875df 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -2,9 +2,12 @@ #include "pathfinder.h" +#include "coord/chunk.h" #include "coord/phys.h" +#include "pathfinding/cost_field.h" #include "pathfinding/flow_field.h" #include "pathfinding/grid.h" +#include "pathfinding/integration_field.h" #include "pathfinding/integrator.h" #include "pathfinding/portal.h" #include "pathfinding/sector.h" @@ -18,55 +21,170 @@ Pathfinder::Pathfinder() : } const Path Pathfinder::get_path(const PathRequest &request) { - // High-level pathfinding - // Find the portals to use to get from the start to the target - auto portal_path = this->portal_a_star(request); - - // Low-level pathfinding - // Find the path within the sectors auto grid = this->grids.at(request.grid_id); auto sector_size = grid->get_sector_size(); + // Check if the target is within the grid + auto grid_size = grid->get_size(); + auto grid_width = grid_size[0] * sector_size; + auto grid_height = grid_size[1] * sector_size; + if (request.target.ne < 0 + or request.target.se < 0 + or request.target.ne >= static_cast(grid_width) + or request.target.se >= static_cast(grid_height)) { + log::log(DBG << "Path not found (start = " + << request.start << "; target = " + << request.target << "): " + << "Target is out of bounds."); + return Path{request.grid_id, PathResult::OUT_OF_BOUNDS, {}}; + } + + auto start_sector_x = request.start.ne / sector_size; + auto start_sector_y = request.start.se / sector_size; + auto start_sector = grid->get_sector(start_sector_x, start_sector_y); + auto target_sector_x = request.target.ne / sector_size; auto target_sector_y = request.target.se / sector_size; auto target_sector = grid->get_sector(target_sector_x, target_sector_y); - coord::tile_t target_x = request.target.ne % sector_size; - coord::tile_t target_y = request.target.se % sector_size; - auto target = coord::tile{target_x, target_y}; + auto target = request.target - target_sector->get_position().to_tile(sector_size); + if (target_sector->get_cost_field()->get_cost(target) == COST_IMPASSABLE) { + // TODO: This may be okay if the target is a building or unit + log::log(DBG << "Path not found (start = " + << request.start << "; target = " + << request.target << "): " + << "Target is impassable."); + return Path{request.grid_id, PathResult::NOT_FOUND, {}}; + } + + // Integrate the target field + coord::tile_delta target_delta = request.target - target_sector->get_position().to_tile(sector_size); + auto target_integration_field = this->integrator->integrate(target_sector->get_cost_field(), + target_delta); + + if (target_sector == start_sector) { + auto start = request.start - start_sector->get_position().to_tile(sector_size); + + if (target_integration_field->get_cell(start.ne, start.se).cost != INTEGRATED_COST_UNREACHABLE) { + // Exit early if the start and target are in the same sector + // and are reachable from within the same sector + auto flow_field = this->integrator->build(target_integration_field); + auto flow_field_waypoints = this->get_waypoints({std::make_pair(target_sector->get_id(), flow_field)}, request); + + std::vector waypoints{}; + if (flow_field_waypoints.at(0) != request.start) { + waypoints.push_back(request.start); + } + waypoints.insert(waypoints.end(), flow_field_waypoints.begin(), flow_field_waypoints.end()); + + log::log(DBG << "Path found (start = " + << request.start << "; target = " + << request.target << "): " + << "Path is within the same sector."); + return Path{request.grid_id, PathResult::FOUND, waypoints}; + } + } + + // Check which portals are reachable from the target field + std::unordered_set target_portal_ids; + for (auto &portal : target_sector->get_portals()) { + auto center_cell = portal->get_entry_center(target_sector->get_id()); + + if (target_integration_field->get_cell(center_cell).cost != INTEGRATED_COST_UNREACHABLE) { + target_portal_ids.insert(portal->get_id()); + } + } + + // Check which portals are reachable from the start field + coord::tile_delta start = request.start - start_sector->get_position().to_tile(sector_size); + auto start_integration_field = this->integrator->integrate(start_sector->get_cost_field(), + start, + false); + + std::unordered_set start_portal_ids; + for (auto &portal : start_sector->get_portals()) { + auto center_cell = portal->get_entry_center(start_sector->get_id()); + + if (start_integration_field->get_cell(center_cell).cost != INTEGRATED_COST_UNREACHABLE) { + start_portal_ids.insert(portal->get_id()); + } + } + + if (target_portal_ids.empty() or start_portal_ids.empty()) { + // Exit early if no portals are reachable from the start or target + log::log(DBG << "Path not found (start = " + << request.start << "; target = " + << request.target << "): " + << "No portals are reachable from the start or target."); + return Path{request.grid_id, PathResult::NOT_FOUND, {}}; + } + + // High-level pathfinding + // Find the portals to use to get from the start to the target + auto portal_result = this->portal_a_star(request, target_portal_ids, start_portal_ids); + auto portal_status = portal_result.first; + auto portal_path = portal_result.second; + + // Low-level pathfinding + // Find the path within the sectors - auto sector_fields = this->integrator->build(target_sector->get_cost_field(), target); - auto prev_integration_field = sector_fields.first; + // Build flow field for the target sector + auto prev_integration_field = target_integration_field; + auto prev_flow_field = this->integrator->build(prev_integration_field); auto prev_sector_id = target_sector->get_id(); + Integrator::get_return_t sector_fields{prev_integration_field, prev_flow_field}; + std::vector>> flow_fields; flow_fields.reserve(portal_path.size() + 1); - flow_fields.push_back(std::make_pair(target_sector->get_id(), sector_fields.second)); + + int los_depth = 1; + for (auto &portal : portal_path) { auto prev_sector = grid->get_sector(prev_sector_id); auto next_sector_id = portal->get_exit_sector(prev_sector_id); auto next_sector = grid->get_sector(next_sector_id); - sector_fields = this->integrator->build(next_sector->get_cost_field(), - prev_integration_field, - prev_sector_id, - portal); + target_delta = request.target - next_sector->get_position().to_tile(sector_size); + bool with_los = los_depth > 0; + + sector_fields = this->integrator->get(next_sector->get_cost_field(), + prev_integration_field, + prev_sector_id, + portal, + target_delta, + with_los); flow_fields.push_back(std::make_pair(next_sector_id, sector_fields.second)); prev_integration_field = sector_fields.first; prev_sector_id = next_sector_id; + los_depth -= 1; } // reverse the flow fields so they are ordered from start to target std::reverse(flow_fields.begin(), flow_fields.end()); // traverse the flow fields to get the waypoints - std::vector waypoints{request.start}; auto flow_field_waypoints = this->get_waypoints(flow_fields, request); + std::vector waypoints{}; + if (flow_field_waypoints.at(0) != request.start) { + waypoints.push_back(request.start); + } waypoints.insert(waypoints.end(), flow_field_waypoints.begin(), flow_field_waypoints.end()); - return Path{request.grid_id, waypoints}; + if (portal_status == PathResult::NOT_FOUND) { + log::log(DBG << "Path not found (start = " + << request.start << "; target = " + << request.target << ")"); + } + else { + log::log(DBG << "Path found (start = " + << request.start << "; target = " + << request.target << ")"); + } + + return Path{request.grid_id, portal_status, waypoints}; } const std::shared_ptr &Pathfinder::get_grid(grid_id_t id) const { @@ -77,7 +195,9 @@ void Pathfinder::add_grid(const std::shared_ptr &grid) { this->grids[grid->get_id()] = grid; } -const std::vector> Pathfinder::portal_a_star(const PathRequest &request) const { +const Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &request, + const std::unordered_set &target_portal_ids, + const std::unordered_set &start_portal_ids) const { std::vector> result; auto grid = this->grids.at(request.grid_id); @@ -87,32 +207,27 @@ const std::vector> Pathfinder::portal_a_star(const PathR auto start_sector_y = request.start.se / sector_size; auto start_sector = grid->get_sector(start_sector_x, start_sector_y); - auto target_sector_x = request.target.ne / sector_size; - auto target_sector_y = request.target.se / sector_size; - auto target_sector = grid->get_sector(target_sector_x, target_sector_y); - - if (start_sector == target_sector) { - // exit early if the start and target are in the same sector - return result; - } - // path node storage, always provides cheapest next node. heap_t node_candidates; // list of known portals and corresponding node. nodemap_t visited_portals; - // Cost to travel from one portal to another - // TODO: Determine this cost for each portal + // TODO: Compute cost to travel from one portal to another when creating portals // const int distance_cost = 1; - // start nodes: all portals in the start sector + // create start nodes for (auto &portal : start_sector->get_portals()) { + if (not start_portal_ids.contains(portal->get_id())) { + // only consider portals that are reachable from the start cell + continue; + } + auto portal_node = std::make_shared(portal, start_sector->get_id(), nullptr); - auto sector_pos = grid->get_sector(portal->get_exit_sector(start_sector->get_id()))->get_position(); + auto sector_pos = grid->get_sector(portal->get_exit_sector(start_sector->get_id()))->get_position().to_tile(sector_size); auto portal_pos = portal->get_exit_center(start_sector->get_id()); - auto portal_abs_pos = portal_pos + coord::tile_delta(sector_pos.ne * sector_size, sector_pos.se * sector_size); + auto portal_abs_pos = sector_pos + portal_pos; auto heuristic_cost = Pathfinder::heuristic_cost(portal_abs_pos, request.target); portal_node->current_cost = 0; @@ -133,13 +248,16 @@ const std::vector> Pathfinder::portal_a_star(const PathR current_node->was_best = true; - // check if the current node is the target - if (current_node->portal->get_exit_sector(current_node->entry_sector) == target_sector->get_id()) { + // check if the current node is a portal in the target sector that can + // be reached from the target cell + auto exit_portal_id = current_node->portal->get_id(); + if (target_portal_ids.contains(exit_portal_id)) { auto backtrace = current_node->generate_backtrace(); for (auto &node : backtrace) { result.push_back(node->portal); } - return result; + log::log(DBG << "Portal path found with " << result.size() << " portal traversals."); + return std::make_pair(PathResult::FOUND, result); } // check if the current node is the closest to the target @@ -156,7 +274,7 @@ const std::vector> Pathfinder::portal_a_star(const PathR continue; } - // Get distance cost from current node to exit + // Get distance cost (from current node to exit node) auto distance_cost = Pathfinder::distance_cost( current_node->portal->get_exit_center(current_node->entry_sector), exit->portal->get_entry_center(exit->entry_sector)); @@ -166,9 +284,12 @@ const std::vector> Pathfinder::portal_a_star(const PathR if (not_visited or tentative_cost < exit->current_cost) { if (not_visited) { - // calculate the heuristic cost + // Get heuristic cost (from exit node to target cell) + auto exit_sector = grid->get_sector(exit->portal->get_exit_sector(exit->entry_sector)); + auto exit_sector_pos = exit_sector->get_position().to_tile(sector_size); + auto exit_portal_pos = exit->portal->get_exit_center(exit->entry_sector); exit->heuristic_cost = Pathfinder::heuristic_cost( - exit->portal->get_exit_center(exit->entry_sector), + exit_sector_pos + exit_portal_pos, request.target); } @@ -194,7 +315,9 @@ const std::vector> Pathfinder::portal_a_star(const PathR result.push_back(node->portal); } - return result; + log::log(DBG << "Portal path not found."); + log::log(DBG << "Closest portal: " << closest_node->portal->get_id()); + return std::make_pair(PathResult::NOT_FOUND, result); } const std::vector Pathfinder::get_waypoints(const std::vector>> &flow_fields, @@ -207,40 +330,35 @@ const std::vector Pathfinder::get_waypoints(const std::vectorget_sector_size(); coord::tile_t start_x = request.start.ne % sector_size; coord::tile_t start_y = request.start.se % sector_size; - coord::tile_t target_x = request.target.ne % sector_size; - coord::tile_t target_y = request.target.se % sector_size; + + bool los_reached = false; coord::tile_t current_x = start_x; coord::tile_t current_y = start_y; - flow_dir_t current_direction = flow_fields.at(0).second->get_dir(coord::tile{current_x, current_y}); + flow_dir_t current_direction = flow_fields.at(0).second->get_dir(current_x, current_y); for (size_t i = 0; i < flow_fields.size(); ++i) { - auto sector = grid->get_sector(flow_fields.at(i).first); - auto flow_field = flow_fields.at(i).second; + auto §or = grid->get_sector(flow_fields[i].first); + auto sector_pos = sector->get_position().to_tile(sector_size); + auto &flow_field = flow_fields[i].second; // navigate the flow field vectors until we reach its edge (or the target) - while (current_x < static_cast(sector_size) - and current_y < static_cast(sector_size) - and current_x >= 0 - and current_y >= 0) { - auto cell = flow_field->get_cell(coord::tile{current_x, current_y}); + flow_t cell; + do { + cell = flow_field->get_cell(current_x, current_y); + if (cell & FLOW_LOS_MASK) { // check if we reached an LOS cell - auto sector_pos = sector->get_position(); - auto cell_pos = coord::tile(sector_pos.ne * sector_size, - sector_pos.se * sector_size) - + coord::tile_delta(current_x, current_y); + auto cell_pos = sector_pos + coord::tile_delta(current_x, current_y); waypoints.push_back(cell_pos); + los_reached = true; break; } // check if we need to change direction - auto cell_direction = flow_field->get_dir(coord::tile(current_x, current_y)); + auto cell_direction = static_cast(cell & FLOW_DIR_MASK); if (cell_direction != current_direction) { // add the current cell as a waypoint - auto sector_pos = sector->get_position(); - auto cell_pos = coord::tile(sector_pos.ne * sector_size, - sector_pos.se * sector_size) - + coord::tile_delta(current_x, current_y); + auto cell_pos = sector_pos + coord::tile_delta(current_x, current_y); waypoints.push_back(cell_pos); current_direction = cell_direction; } @@ -279,6 +397,13 @@ const std::vector Pathfinder::get_waypoints(const std::vector(current_direction)}; } } + while (not(cell & FLOW_TARGET_MASK)); + + if (los_reached or i == flow_fields.size() - 1) { + // exit the loop if we found an LOS cell or reached + // the target cell in the last flow field + break; + } // reset the current position for the next flow field switch (current_direction) { @@ -316,10 +441,7 @@ const std::vector Pathfinder::get_waypoints(const std::vectorget_sector(flow_fields.back().first)->get_position(); - auto target_pos = coord::tile(sector_pos.ne * sector_size, sector_pos.se * sector_size) - + coord::tile_delta{target_x, target_y}; - waypoints.push_back(target_pos); + waypoints.push_back(request.target); return waypoints; } @@ -333,8 +455,8 @@ int Pathfinder::heuristic_cost(const coord::tile &portal_pos, return delta.length(); } -int Pathfinder::distance_cost(const coord::tile &portal1_pos, - const coord::tile &portal2_pos) { +int Pathfinder::distance_cost(const coord::tile_delta &portal1_pos, + const coord::tile_delta &portal2_pos) { auto delta = portal2_pos.to_phys2() - portal1_pos.to_phys2(); return delta.length(); @@ -391,22 +513,25 @@ std::vector PortalNode::generate_backtrace() { std::vector PortalNode::get_exits(const nodemap_t &nodes, sector_id_t entry_sector) { - std::vector exits; + auto &exits = this->portal->get_exits(entry_sector); + std::vector exit_nodes; + exit_nodes.reserve(exits.size()); auto exit_sector = this->portal->get_exit_sector(entry_sector); - for (auto &exit : this->portal->get_exits(entry_sector)) { + for (auto &exit : exits) { auto exit_id = exit->get_id(); - if (nodes.contains(exit_id)) { - exits.push_back(nodes.at(exit_id)); + auto exit_node = nodes.find(exit_id); + if (exit_node != nodes.end()) { + exit_nodes.push_back(exit_node->second); } else { - exits.push_back(std::make_shared(exit, - exit_sector, - this->shared_from_this())); + exit_nodes.push_back(std::make_shared(exit, + exit_sector, + this->shared_from_this())); } } - return exits; + return exit_nodes; } diff --git a/libopenage/pathfinding/pathfinder.h b/libopenage/pathfinding/pathfinder.h index a4df0350d8..1809b7524d 100644 --- a/libopenage/pathfinding/pathfinder.h +++ b/libopenage/pathfinding/pathfinder.h @@ -4,6 +4,7 @@ #include #include +#include #include "coord/tile.h" #include "datastructure/pairing_heap.h" @@ -62,14 +63,20 @@ class Pathfinder { const Path get_path(const PathRequest &request); private: + using portal_star_t = std::pair>>; + /** * High-level pathfinder. Uses A* to find the path through the portals of sectors. * * @param request Pathfinding request. + * @param target_portal_ids IDs of portals that can be reached from the target cell. + * @param start_portal_ids IDs of portals that can be reached from the start cell. * * @return Portals to traverse in order to reach the target. */ - const std::vector> portal_a_star(const PathRequest &request) const; + const portal_star_t portal_a_star(const PathRequest &request, + const std::unordered_set &target_portal_ids, + const std::unordered_set &start_portal_ids) const; /** * Low-level pathfinder. Uses flow fields to find the path through the sectors. @@ -85,8 +92,9 @@ class Pathfinder { /** * Calculate the heuristic cost between a portal and a target cell. * - * @param portal_pos Position of the portal. This should be the center of the portal exit. - * @param target_pos Position of the target cell. + * @param portal_pos Position of the portal (absolute on the grid). + * This should be the center of the portal exit. + * @param target_pos Position of the target cell (absolute on the grid). * * @return Heuristic cost between the cells. */ @@ -95,12 +103,12 @@ class Pathfinder { /** * Calculate the distance cost between two portals. * - * @param portal1_pos Center of the first portal. - * @param portal2_pos Center of the second portal. + * @param portal1_pos Center of the first portal (relative to sector origin). + * @param portal2_pos Center of the second portal (relative to sector origin). * * @return Distance cost between the portal centers. */ - static int distance_cost(const coord::tile &portal1_pos, const coord::tile &portal2_pos); + static int distance_cost(const coord::tile_delta &portal1_pos, const coord::tile_delta &portal2_pos); /** * Grids managed by this pathfinder. diff --git a/libopenage/pathfinding/portal.cpp b/libopenage/pathfinding/portal.cpp index da3581047f..9b8f64cd51 100644 --- a/libopenage/pathfinding/portal.cpp +++ b/libopenage/pathfinding/portal.cpp @@ -11,8 +11,8 @@ Portal::Portal(portal_id_t id, sector_id_t sector0, sector_id_t sector1, PortalDirection direction, - const coord::tile &cell_start, - const coord::tile &cell_end) : + const coord::tile_delta &cell_start, + const coord::tile_delta &cell_end) : id{id}, sector0{sector0}, sector1{sector1}, @@ -65,7 +65,7 @@ sector_id_t Portal::get_exit_sector(sector_id_t entry_sector) const { return this->sector0; } -const coord::tile Portal::get_entry_start(sector_id_t entry_sector) const { +const coord::tile_delta Portal::get_entry_start(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -75,7 +75,7 @@ const coord::tile Portal::get_entry_start(sector_id_t entry_sector) const { return this->get_sector1_start(); } -const coord::tile Portal::get_entry_center(sector_id_t entry_sector) const { +const coord::tile_delta Portal::get_entry_center(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -89,7 +89,7 @@ const coord::tile Portal::get_entry_center(sector_id_t entry_sector) const { return {(start.ne + end.ne) / 2, (start.se + end.se) / 2}; } -const coord::tile Portal::get_entry_end(sector_id_t entry_sector) const { +const coord::tile_delta Portal::get_entry_end(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -99,7 +99,7 @@ const coord::tile Portal::get_entry_end(sector_id_t entry_sector) const { return this->get_sector1_end(); } -const coord::tile Portal::get_exit_start(sector_id_t entry_sector) const { +const coord::tile_delta Portal::get_exit_start(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -109,7 +109,7 @@ const coord::tile Portal::get_exit_start(sector_id_t entry_sector) const { return this->get_sector0_start(); } -const coord::tile Portal::get_exit_center(sector_id_t entry_sector) const { +const coord::tile_delta Portal::get_exit_center(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -123,7 +123,7 @@ const coord::tile Portal::get_exit_center(sector_id_t entry_sector) const { return {(start.ne + end.ne) / 2, (start.se + end.se) / 2}; } -const coord::tile Portal::get_exit_end(sector_id_t entry_sector) const { +const coord::tile_delta Portal::get_exit_end(sector_id_t entry_sector) const { ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); if (entry_sector == this->sector0) { @@ -137,22 +137,22 @@ PortalDirection Portal::get_direction() const { return this->direction; } -const coord::tile &Portal::get_sector0_start() const { +const coord::tile_delta &Portal::get_sector0_start() const { return this->cell_start; } -const coord::tile &Portal::get_sector0_end() const { +const coord::tile_delta &Portal::get_sector0_end() const { return this->cell_end; } -const coord::tile Portal::get_sector1_start() const { +const coord::tile_delta Portal::get_sector1_start() const { if (this->direction == PortalDirection::NORTH_SOUTH) { return {this->cell_start.ne, 0}; } return {0, this->cell_start.se}; } -const coord::tile Portal::get_sector1_end() const { +const coord::tile_delta Portal::get_sector1_end() const { if (this->direction == PortalDirection::NORTH_SOUTH) { return {this->cell_end.ne, 0}; } diff --git a/libopenage/pathfinding/portal.h b/libopenage/pathfinding/portal.h index 2487e449c7..6593eea852 100644 --- a/libopenage/pathfinding/portal.h +++ b/libopenage/pathfinding/portal.h @@ -49,15 +49,15 @@ class Portal { * @param sector1 Second sector connected by the portal. * Must be south or west on the grid in relation to sector 0. * @param direction Direction of the portal from sector 0 to sector 1. - * @param cell_start Start cell coordinate in sector 0. - * @param cell_end End cell coordinate in sector 0. + * @param cell_start Start cell coordinate in sector 0 (relative to sector origin). + * @param cell_end End cell coordinate in sector 0 (relative to sector origin). */ Portal(portal_id_t id, sector_id_t sector0, sector_id_t sector1, PortalDirection direction, - const coord::tile &cell_start, - const coord::tile &cell_end); + const coord::tile_delta &cell_start, + const coord::tile_delta &cell_end); ~Portal() = default; @@ -110,54 +110,54 @@ class Portal { * * @param entry_sector Sector from which the portal is entered. * - * @return Cell coordinates of the start of the portal in the entry sector. + * @return Cell coordinates of the start of the portal in the entry sector (relative to sector origin). */ - const coord::tile get_entry_start(sector_id_t entry_sector) const; + const coord::tile_delta get_entry_start(sector_id_t entry_sector) const; /** * Get the cell coordinates of the center of the portal in the entry sector. * * @param entry_sector Sector from which the portal is entered. * - * @return Cell coordinates of the center of the portal in the entry sector. + * @return Cell coordinates of the center of the portal in the entry sector (relative to sector origin). */ - const coord::tile get_entry_center(sector_id_t entry_sector) const; + const coord::tile_delta get_entry_center(sector_id_t entry_sector) const; /** * Get the cell coordinates of the start of the portal in the entry sector. * * @param entry_sector Sector from which the portal is entered. * - * @return Cell coordinates of the start of the portal in the entry sector. + * @return Cell coordinates of the start of the portal in the entry sector (relative to sector origin). */ - const coord::tile get_entry_end(sector_id_t entry_sector) const; + const coord::tile_delta get_entry_end(sector_id_t entry_sector) const; /** * Get the cell coordinates of the start of the portal in the exit sector. * * @param entry_sector Sector from which the portal is entered. * - * @return Cell coordinates of the start of the portal in the exit sector. + * @return Cell coordinates of the start of the portal in the exit sector (relative to sector origin). */ - const coord::tile get_exit_start(sector_id_t entry_sector) const; + const coord::tile_delta get_exit_start(sector_id_t entry_sector) const; /** * Get the cell coordinates of the center of the portal in the exit sector. * * @param entry_sector Sector from which the portal is entered. * - * @return Cell coordinates of the center of the portal in the exit sector. + * @return Cell coordinates of the center of the portal in the exit sector (relative to sector origin). */ - const coord::tile get_exit_center(sector_id_t entry_sector) const; + const coord::tile_delta get_exit_center(sector_id_t entry_sector) const; /** * Get the cell coordinates of the end of the portal in the exit sector. * * @param entry_sector Sector from which the portal is entered. * - * @return Cell coordinates of the end of the portal in the exit sector. + * @return Cell coordinates of the end of the portal in the exit sector (relative to sector origin). */ - const coord::tile get_exit_end(sector_id_t entry_sector) const; + const coord::tile_delta get_exit_end(sector_id_t entry_sector) const; /** * Get the direction of the portal from sector 0 to sector 1. @@ -170,30 +170,30 @@ class Portal { /** * Get the start cell coordinates of the portal. * - * @return Start cell coordinates of the portal. + * @return Start cell coordinates of the portal (relative to sector origin). */ - const coord::tile &get_sector0_start() const; + const coord::tile_delta &get_sector0_start() const; /** * Get the end cell coordinates of the portal. * - * @return End cell coordinates of the portal. + * @return End cell coordinates of the portal (relative to sector origin). */ - const coord::tile &get_sector0_end() const; + const coord::tile_delta &get_sector0_end() const; /** * Get the start cell coordinates of the portal. * - * @return Start cell coordinates of the portal. + * @return Start cell coordinates of the portal (relative to sector origin). */ - const coord::tile get_sector1_start() const; + const coord::tile_delta get_sector1_start() const; /** * Get the end cell coordinates of the portal. * - * @return End cell coordinates of the portal. + * @return End cell coordinates of the portal (relative to sector origin). */ - const coord::tile get_sector1_end() const; + const coord::tile_delta get_sector1_end() const; /** * ID of the portal. @@ -230,13 +230,17 @@ class Portal { PortalDirection direction; /** - * Start cell coordinate. + * Start cell coordinate in sector 0 (relative to sector origin). + * + * Coordinates for sector 1 are calculated on-the-fly using the direction. */ - coord::tile cell_start; + coord::tile_delta cell_start; /** - * End cell coordinate. + * End cell coordinate in sector 0 (relative to sector origin). + * + * Coordinates for sector 1 are calculated on-the-fly using the direction. */ - coord::tile cell_end; + coord::tile_delta cell_end; }; } // namespace openage::path diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index cf6ffaa755..f50dc209bf 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -61,17 +61,17 @@ std::vector> Sector::find_portals(const std::shared_ptr< size_t end = 0; bool passable_edge = false; for (size_t i = 0; i < this->cost_field->get_size(); ++i) { - auto coord_this = coord::tile{0, 0}; - auto coord_other = coord::tile{0, 0}; + auto coord_this = coord::tile_delta{0, 0}; + auto coord_other = coord::tile_delta{0, 0}; if (direction == PortalDirection::NORTH_SOUTH) { // right edge; top to bottom - coord_this = coord::tile(i, this->cost_field->get_size() - 1); - coord_other = coord::tile(i, 0); + coord_this = coord::tile_delta(i, this->cost_field->get_size() - 1); + coord_other = coord::tile_delta(i, 0); } else if (direction == PortalDirection::EAST_WEST) { // bottom edge; east to west - coord_this = coord::tile(this->cost_field->get_size() - 1, i); - coord_other = coord::tile(0, i); + coord_this = coord::tile_delta(this->cost_field->get_size() - 1, i); + coord_other = coord::tile_delta(0, i); } if (this->cost_field->get_cost(coord_this) != COST_IMPASSABLE @@ -94,17 +94,17 @@ std::vector> Sector::find_portals(const std::shared_ptr< if (passable_edge) { // create a new portal - auto coord_start = coord::tile{0, 0}; - auto coord_end = coord::tile{0, 0}; + auto coord_start = coord::tile_delta{0, 0}; + auto coord_end = coord::tile_delta{0, 0}; if (direction == PortalDirection::NORTH_SOUTH) { // right edge; top to bottom - coord_start = coord::tile(start, this->cost_field->get_size() - 1); - coord_end = coord::tile(end, this->cost_field->get_size() - 1); + coord_start = coord::tile_delta(start, this->cost_field->get_size() - 1); + coord_end = coord::tile_delta(end, this->cost_field->get_size() - 1); } else if (direction == PortalDirection::EAST_WEST) { // bottom edge; east to west - coord_start = coord::tile(this->cost_field->get_size() - 1, start); - coord_end = coord::tile(this->cost_field->get_size() - 1, end); + coord_start = coord::tile_delta(this->cost_field->get_size() - 1, start); + coord_end = coord::tile_delta(this->cost_field->get_size() - 1, end); } result.push_back( diff --git a/libopenage/pathfinding/sector.h b/libopenage/pathfinding/sector.h index f39f1904a9..a4ba94fa52 100644 --- a/libopenage/pathfinding/sector.h +++ b/libopenage/pathfinding/sector.h @@ -57,7 +57,7 @@ class Sector { /** * Get the position of this sector in the grid. * - * @return Position of the sector. + * @return Position of the sector (absolute on the grid). */ const coord::chunk &get_position() const; @@ -110,7 +110,7 @@ class Sector { sector_id_t id; /** - * Position of the sector in the grid. + * Position of the sector (absolute on the grid). */ coord::chunk position; diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index 2c9775ca36..5f92496ff8 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -28,8 +28,8 @@ void flow_field() { // Test the different field types { auto integration_field = std::make_shared(3); - integration_field->integrate_cost(cost_field, coord::tile{2, 2}); - auto int_cells = integration_field->get_cells(); + integration_field->integrate_cost(cost_field, coord::tile_delta{2, 2}); + auto &int_cells = integration_field->get_cells(); // The integration field should look like: // | 4 | 3 | 2 | @@ -60,7 +60,7 @@ void flow_field() { FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH), FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), - FLOW_PATHABLE_MASK | static_cast(flow_dir_t::NORTH), + FLOW_TARGET_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::NORTH), }; // Compare the integration field cells with the expected values @@ -85,8 +85,8 @@ void flow_field() { auto integrator = std::make_shared(); // Build the flow field - auto flow_field = integrator->build(cost_field, coord::tile{2, 2}).second; - auto ff_cells = flow_field->get_cells(); + auto flow_field = integrator->get(cost_field, coord::tile_delta{2, 2}).second; + auto &ff_cells = flow_field->get_cells(); // The flow field for targeting (2, 2) hould look like this: // | E | SE | S | @@ -94,14 +94,14 @@ void flow_field() { // | E | E | N | auto ff_expected = std::vector{ FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), - FLOW_WAVEFRONT_BLOCKED_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), - FLOW_LOS_MASK | FLOW_PATHABLE_MASK, - FLOW_WAVEFRONT_BLOCKED_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), + FLOW_LOS_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), 0, - FLOW_LOS_MASK | FLOW_PATHABLE_MASK, - FLOW_LOS_MASK | FLOW_PATHABLE_MASK, - FLOW_LOS_MASK | FLOW_PATHABLE_MASK, - FLOW_LOS_MASK | FLOW_PATHABLE_MASK, + FLOW_LOS_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH), + FLOW_LOS_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), + FLOW_LOS_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), + FLOW_LOS_MASK | FLOW_PATHABLE_MASK | FLOW_TARGET_MASK, }; // Compare the flow field cells with the expected values diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h index 11190f6551..1b48a49654 100644 --- a/libopenage/pathfinding/types.h +++ b/libopenage/pathfinding/types.h @@ -8,6 +8,18 @@ namespace openage::path { +/** + * Path result type. + */ +enum class PathResult { + /// Path was found. + FOUND, + /// Path was not found. + NOT_FOUND, + /// Target is not on grid. + OUT_OF_BOUNDS, +}; + /** * Movement cost in the cost field. * @@ -41,8 +53,16 @@ struct integrated_t { /** * Flags. * - * Bit 6: Wave front blocked flag. - * Bit 7: Line of sight flag. + * Bit 0-3: Shared flags with the flow field. + * - 0: Unused. + * - 1: Target flag. + * - 2: Line of sight flag. + * - 3: Unused. + * Bit 4-7: Integration field specific flags. + * - 4: Unused. + * - 5: Wave front blocked flag. + * - 6: LOS found flag. + * - 7: Unused. */ integrated_flags_t flags; }; @@ -67,7 +87,7 @@ enum class flow_dir_t : uint8_t { * Flow field cell value. * * Bit 0: Unused. - * Bit 1: Wave front blocked flag. + * Bit 1: Target flag. * Bit 2: Line of sight flag. * Bit 3: Pathable flag. * Bits 4-7: flow direction. diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index 62435074d4..33c798bcef 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -116,6 +116,7 @@ void Presenter::init_graphics(bool debug) { // Camera this->camera = std::make_shared(this->renderer, this->window->get_size()); + this->camera->look_at_coord(coord::scene3{10.0, 10.0, 0}); // Center camera on the map this->window->add_resize_callback([this](size_t w, size_t h, double /*scale*/) { this->camera->resize(w, h); }); diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index d4629f866b..0a707807ca 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -98,7 +98,7 @@ void WorldObject::update_uniforms(const time::time_t &time) { auto angle_degrees = this->angle.get(time).to_float(); // Animation information - auto animation_info = this->animation_info.get(time); + auto [last_update, animation_info] = this->animation_info.frame(time); for (size_t layer_idx = 0; layer_idx < this->layer_uniforms.size(); ++layer_idx) { auto &layer_unifs = this->layer_uniforms.at(layer_idx); @@ -123,7 +123,7 @@ void WorldObject::update_uniforms(const time::time_t &time) { case renderer::resources::display_mode::LOOP: { // ONCE and LOOP are animated based on time auto &timing = layer.get_frame_timing(); - frame_idx = timing->get_frame(time, this->render_entity->get_update_time()); + frame_idx = timing->get_frame(time, last_update); } break; case renderer::resources::display_mode::OFF: default: