Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate flow field pathfinder into game simulation #1656

Merged
merged 68 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
cb5a754
gamestate: Map class to store terrain and pathfinder.
heinezen May 12, 2024
7584db4
gamestate: Verify terrain chunk size.
heinezen May 12, 2024
1ddc053
gamestate: Get path costs from nyan API.
heinezen May 12, 2024
c6e6c8b
gamestate: Generate pathfinding grids from nyan terrain defs.
heinezen May 12, 2024
3d24ca4
gamestate: Fix nyan object value pointer cast.
heinezen May 12, 2024
cdf83d8
gamestate: Add map to state.
heinezen May 12, 2024
94e1afb
gamestate: Fix access of Terrain constructor.
heinezen May 12, 2024
6d2a200
gamestate: Connect portals in pathfinder grid.
heinezen May 20, 2024
ae36387
gamestate: Record mapping of nyan path grid -> grid ID.
heinezen May 20, 2024
672efb1
gamestate: Use pathfinder in Move system.
heinezen May 20, 2024
a2ed206
path: Block diagonal flow direction if adjactent horizontal/vertical …
heinezen May 20, 2024
68443c6
path: Only use reachable start/target portal nodes for high-level path.
heinezen May 20, 2024
596aecb
path: Store whether a path has been found.
heinezen May 20, 2024
564497e
path: Prevent duplicate waypoints at beginning of path.
heinezen May 21, 2024
f239a6f
path: LOS integration with portal.
heinezen May 21, 2024
0763deb
path: Change all relative coordinates to delta types.
heinezen May 21, 2024
258c592
path: Use x,y cell position instead of coord types for most operations.
heinezen May 21, 2024
66709b4
path: Fix crash in demo 1 when path is not found.
heinezen May 21, 2024
d269150
path: Flag in integration field.
heinezen May 21, 2024
60cf82a
path: Fix time unit log (microseconds instead of picoseconds).
heinezen May 21, 2024
6c7a471
coord: Make tile_per_chunk type-compatible with tile_t.
heinezen May 25, 2024
308c913
gamestate: Fix setting recent angle position.
heinezen May 25, 2024
e7f7ed9
coord: Reorder tiles and fix missing conversions.
heinezen May 25, 2024
5828177
gamestate: Use tile center for waypoints in path.
heinezen May 25, 2024
d5d4372
path: Reset with std::fill.
heinezen May 25, 2024
d51ebba
patg: Cache fields during pathing.
heinezen May 25, 2024
e849754
path: Clear LOS next wave list before next loop iteration.
heinezen May 25, 2024
ebc0592
path: Only add cells with blocked flag to wavefront list.
heinezen May 25, 2024
805148c
path: Add flag for found LOS cells instead of lookup set.
heinezen May 26, 2024
b48c68a
path: Remove lookup set from cost integration pass.
heinezen May 26, 2024
352996f
path: Check if target cell is out of bounds for grid.
heinezen May 30, 2024
906e47a
path: Set PATHABLE and LOS flags for target cells in flow field.
heinezen May 31, 2024
4029276
path: Smooth LOS integration for target cells with cost > MIN_COST.
heinezen May 31, 2024
7471b43
path: Start LOS pass from configurable start cells.
heinezen Jun 1, 2024
52a6e86
path: Make LOS pass optional.
heinezen Jun 1, 2024
7d8025c
path: Simplify calculations for start/target coordinate deltas.
heinezen Jun 1, 2024
36eafc4
path: Skip LOS pass when checking start sector for exit portals.
heinezen Jun 1, 2024
d328738
path: Exit waypoinnt search if any LOS cell is found.
heinezen Jun 1, 2024
0da61fa
path: Calculate flow field directions for LOS cells to allow caching …
heinezen Jun 1, 2024
d651de4
path: Transfer flags for LOS/wavefront blocked from an integration fi…
heinezen Jun 1, 2024
da34efc
path: Apply LOS flags when getting fields from cache.
heinezen Jun 1, 2024
e67deef
path: Limit sectors where LOS is transferred between portals.
heinezen Jun 1, 2024
0eba33c
path: Fix time unit in demo 1.
heinezen Jun 1, 2024
7a48ef0
path: Align bit positions for shared flags of flow/integration cell v…
heinezen Jun 1, 2024
9f4355c
path: Use integration flags in flow field shader.
heinezen Jun 1, 2024
4f843d8
path: Remove FLOW_WAVEFRONT_BLOCKED flag.
heinezen Jun 1, 2024
7b71179
path: Add FLOW_TARGET flag.
heinezen Jun 2, 2024
82ce34e
path: Check ifr target cell has been reached using flow target flag.
heinezen Jun 2, 2024
c1d5637
etc: Add unused placeholders for integration flags.
heinezen Jun 2, 2024
9206916
path: Optimize performance-critical paths in the code.
heinezen Jun 2, 2024
2ac9638
path: Remopve outdated TODOs.
heinezen Jun 2, 2024
a7415ab
path: Fix existing unit tests.
heinezen Jun 2, 2024
2abbb3d
path: More helpful log messages.
heinezen Jun 2, 2024
09e22df
path: Fix waypoint finder exiting one cell too early.
heinezen Jun 2, 2024
b651e36
Fix compiler warnings.
heinezen Jun 2, 2024
3790ffb
path: Vectorize and unroll loops to maximize performance.
heinezen Jun 2, 2024
91e84a9
path: Speed up flow field access.
heinezen Jun 2, 2024
156165a
path: Optimize portal exit node search for speed.
heinezen Jun 2, 2024
7eaebae
path: Optimize access to integration cell in LOS pass.
heinezen Jun 2, 2024
3a63dc6
path: Make distinction between cardinal/diagonal checks more clear.
heinezen Jul 11, 2024
498beb3
etc: Remove pathfinding TODO from pretty printer.
heinezen Jul 13, 2024
9662186
path: Check if target cell is impassable.
heinezen Jul 27, 2024
38d05e6
renderer: Update animation frame based on keyframe insertion time.
heinezen Jul 27, 2024
7032505
gamestate: Do not spawn entities outside of map area.
heinezen Jul 27, 2024
e999fe4
doc: Document field types in flow field pathfinder.
heinezen Jul 28, 2024
3c79d9e
gamestate: Use nyan terrain definition for modpacks without terrain g…
heinezen Jul 28, 2024
347d586
presenter: Let camera look at map center on startup.
heinezen Jul 28, 2024
eb62b8c
gamestate: Create grids for all existing path types.
heinezen Sep 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
9 changes: 6 additions & 3 deletions assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl
Original file line number Diff line number Diff line change
@@ -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;
}
5 changes: 2 additions & 3 deletions doc/code/pathfinding/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- TODO: More descriptions of cost/integration/flow field calculations -->

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
Expand Down
78 changes: 78 additions & 0 deletions doc/code/pathfinding/field_types.md
Original file line number Diff line number Diff line change
@@ -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
Binary file added doc/code/pathfinding/images/cost_field.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/code/pathfinding/images/flow_field.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 9 additions & 4 deletions etc/gdb_pretty/printers.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ class PathFlowTypePrinter:
FLOW_FLAGS: dict = {
0x10: 'PATHABLE',
0x20: 'LOS',
0x40: 'WAVEFRONT_BLOCKED',
0x40: 'TARGET',
0x80: 'UNUSED',
}

Expand Down Expand Up @@ -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',
}


Expand Down Expand Up @@ -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
16 changes: 14 additions & 2 deletions libopenage/coord/chunk.cpp
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions libopenage/coord/chunk.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ namespace coord {

struct chunk_delta : CoordNeSeRelative<chunk_t, chunk, chunk_delta> {
using CoordNeSeRelative<chunk_t, chunk, chunk_delta>::CoordNeSeRelative;

tile_delta to_tile(tile_t tiles_per_chunk) const;
};

struct chunk : CoordNeSeAbsolute<chunk_t, chunk, chunk_delta> {
using CoordNeSeAbsolute<chunk_t, chunk, chunk_delta>::CoordNeSeAbsolute;

tile to_tile(tile_t tiles_per_chunk) const;
};

struct chunk3_delta : CoordNeSeUpRelative<chunk_t, chunk3, chunk3_delta> {
Expand Down
63 changes: 47 additions & 16 deletions libopenage/coord/tile.cpp
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -8,55 +8,86 @@

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{
static_cast<chunk::elem_t>(util::div(this->ne, tiles_per_chunk)),
static_cast<chunk::elem_t>(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
16 changes: 10 additions & 6 deletions libopenage/coord/tile.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ namespace coord {
struct tile_delta : CoordNeSeRelative<tile_t, tile, tile_delta> {
using CoordNeSeRelative<tile_t, tile, tile_delta>::CoordNeSeRelative;

// coordinate conversions
phys2_delta to_phys2() const;
phys3_delta to_phys3(tile_t up = 0) const;
};
Expand All @@ -34,8 +35,12 @@ struct tile : CoordNeSeAbsolute<tile_t, tile, tile_delta> {
* 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;
};
Expand All @@ -45,9 +50,7 @@ struct tile3_delta : CoordNeSeUpRelative<tile_t, tile3, tile3_delta> {

// 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;
};

Expand All @@ -56,11 +59,12 @@ struct tile3 : CoordNeSeUpAbsolute<tile_t, tile3, tile3_delta> {

// 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;
};


Expand Down
1 change: 1 addition & 0 deletions libopenage/gamestate/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ add_sources(libopenage
game_state.cpp
game.cpp
manager.cpp
map.cpp
player.cpp
simulation.cpp
terrain_chunk.cpp
Expand Down
Loading
Loading