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

Precomputing of jump point check for all directions #12

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ before component generation, which is done in example [simple_4](examples/simple
See [examples](examples/) for finding paths with multiple goals and generating waypoints instead of full paths.

### Benchmarks
The system can be benchmarked using scenarios from the [Moving AI 2D pathfinding benchmarks](https://movingai.com/benchmarks/grids.html). The [grid_pathfinding_benchmark](grid_pathfinding_benchmark) utility crate provides general support for loading these files. The default benchmark executed using `cargo bench` runs three scenario sets from the [Dragon Age: Origins](https://movingai.com/benchmarks/dao/index.html): `dao/arena`, `dao/den312` and `dao/arena2` (or `dao/den009d` when using the rectilinear algorithm). Running these requires the corresponding map and scenario files to be saved in folders called `maps/dao` and `scenarios/dao`.
The system can be benchmarked using scenarios from the [Moving AI 2D pathfinding benchmarks](https://movingai.com/benchmarks/grids.html). The [grid_pathfinding_benchmark](grid_pathfinding_benchmark) utility crate provides general support for loading these files. The default benchmark executed using `cargo bench` runs three scenario sets from the [Dragon Age: Origins](https://movingai.com/benchmarks/dao/index.html): `dao/arena`, `dao/den312` and `dao/arena2`. Running these requires the corresponding map and scenario files to be saved in folders called `maps/dao` and `scenarios/dao`.

A baseline can be set using
```bash
Expand All @@ -62,11 +62,10 @@ cargo bench -- --baseline main
```

### Performance
Using an i5-6600 quad-core running at 3.3 GHz, running the `dao/arena2` set of 910 scenarios on a 281x209 grid takes 123 ms using JPS allowing diagonals and with improved pruning disabled. Using default neighbor generation as in normal A* (enabled by setting `GRAPH_PRUNING = false`) makes this take 1.26 s, a factor 10 difference. As a rule, the relative difference increases as maps get larger, with the `dao/arena` set of 130 scenarios on a 49x49 grid taking 721 us and 1.01 ms respectively with and without pruning.
Using an i5-6600 quad-core running at 3.3 GHz, running the `dao/arena2` set of 910 scenarios on a 281x209 grid takes 114 ms using JPS allowing diagonals and with improved pruning disabled. Using default neighbor generation as in normal A* (enabled by setting `GRAPH_PRUNING = false`) makes this take 1.21 s, a factor 10 difference. As a rule, the relative difference increases as maps get larger, with the `dao/arena` set of 130 scenarios on a 49x49 grid taking 673 us and 952 us respectively with and without pruning.


An existing C++ [JPS implementation](https://github.com/nathansttt/hog2) runs the same scenarios in about 60 ms. The fastest solver known to the author is the [l1-path-finder](https://mikolalysenko.github.io/l1-path-finder/www/) (implemented in Javascript) which can do this in 38 ms using A* with landmarks (for a 4-neighborhood).


### Goal of crate
The long-term goal of this crate is to provide a fast off-the-shelf pathfinding implementation for grids.
The long-term goal of this crate is to provide a fast off-the-shelf pathfinding implementation for uniform-cost grids.
2 changes: 1 addition & 1 deletion benches/comparison_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ fn dao_bench(c: &mut Criterion) {
pathing_grid.grid = bool_grid.clone();
pathing_grid.allow_diagonal_move = allow_diag;
pathing_grid.improved_pruning = pruning;
pathing_grid.update_all_neighbours();
pathing_grid.initialize();
pathing_grid.generate_components();
let diag_str = if allow_diag { "8-grid" } else { "4-grid" };
let improved_str = if pruning { " (improved pruning)" } else { "" };
Expand Down
2 changes: 1 addition & 1 deletion benches/single_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ fn dao_bench_single(c: &mut Criterion) {
pathing_grid.grid = bool_grid.clone();
pathing_grid.allow_diagonal_move = allow_diag;
pathing_grid.improved_pruning = pruning;
pathing_grid.update_all_neighbours();
pathing_grid.initialize();
pathing_grid.generate_components();
let diag_str = if allow_diag { "8-grid" } else { "4-grid" };
let improved_str = if pruning { " (improved pruning)" } else { "" };
Expand Down
2 changes: 1 addition & 1 deletion examples/benchmark_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ fn main() {
pathing_grid.grid = bool_grid.clone();
pathing_grid.allow_diagonal_move = allow_diag;
pathing_grid.improved_pruning = pruning;
pathing_grid.update_all_neighbours();
pathing_grid.initialize();
pathing_grid.generate_components();
let number_of_scenarios = scenarios.len() as u32;
let before = Instant::now();
Expand Down
82 changes: 66 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ pub fn waypoints_to_path(waypoints: Vec<Point>) -> Vec<Point> {
#[derive(Clone, Debug)]
pub struct PathingGrid {
pub grid: BoolGrid,
pub jump_point: SimpleGrid<u8>,
pub neighbours: SimpleGrid<u8>,
pub components: UnionFind<usize>,
pub components_dirty: bool,
Expand All @@ -75,16 +76,19 @@ pub struct PathingGrid {

impl Default for PathingGrid {
fn default() -> PathingGrid {
PathingGrid {
let mut grid = PathingGrid {
grid: BoolGrid::default(),
jump_point: SimpleGrid::default(),
neighbours: SimpleGrid::default(),
components: UnionFind::new(0),
components_dirty: false,
improved_pruning: true,
heuristic_factor: 1.0,
allow_diagonal_move: true,
context: Arc::new(Mutex::new(AstarContext::new())),
}
};
grid.initialize();
grid
}
}
impl PathingGrid {
Expand Down Expand Up @@ -131,11 +135,27 @@ impl PathingGrid {
}
fn is_forced(&self, dir: Direction, node: &Point) -> bool {
let dir_num = dir.num();
if dir.diagonal() {
!self.indexed_neighbor(node, 3 + dir_num) || !self.indexed_neighbor(node, 5 + dir_num)
} else {
!self.indexed_neighbor(node, 2 + dir_num) || !self.indexed_neighbor(node, 6 + dir_num)
self.jump_point.get_point(*node) & (1 << dir_num) != 0
}

fn forced_mask(&self, node: &Point) -> u8 {
let mut forced_mask: u8 = 0;
for dir_num in 0..8 {
if dir_num % 2 == 1 {
if !self.indexed_neighbor(node, 3 + dir_num)
|| !self.indexed_neighbor(node, 5 + dir_num)
{
forced_mask |= 1 << dir_num;
}
} else {
if !self.indexed_neighbor(node, 2 + dir_num)
|| !self.indexed_neighbor(node, 6 + dir_num)
{
forced_mask |= 1 << dir_num;
}
};
}
forced_mask
}

fn pruned_neighborhood<'a>(
Expand Down Expand Up @@ -482,6 +502,43 @@ impl PathingGrid {
}
}
}
pub fn set_jumppoints(&mut self, point: Point) {
let value = self.forced_mask(&point);
self.jump_point.set_point(point, value);
}
pub fn fix_jumppoints(&mut self, point: Point) {
self.set_jumppoints(point);
for p in self.neighborhood_points(&point) {
if self.point_in_bounds(p) {
self.set_jumppoints(p);
}
}
}

/// Performs the full jump point precomputation
pub fn set_all_jumppoints(&mut self) {
for x in 0..self.width() {
for y in 0..self.height() {
self.set_jumppoints(Point::new(x as i32, y as i32));
}
}
}

pub fn initialize(&mut self) {
// Emulates 'placing' of blocked tile around map border to correctly initialize neighbours
// and make behaviour of a map bordered by tiles the same as a borderless map.
for i in -1..=(self.width() as i32) {
self.update_neighbours(i, -1, true);
self.update_neighbours(i, self.height() as i32, true);
}
for j in -1..=(self.height() as i32) {
self.update_neighbours(-1, j, true);
self.update_neighbours(self.width() as i32, j, true);
}
self.update_all_neighbours();
self.set_all_jumppoints();
}

/// Generates a new [UnionFind] structure and links up grid neighbours to the same components.
pub fn generate_components(&mut self) {
let w = self.grid.width;
Expand Down Expand Up @@ -545,6 +602,7 @@ impl Grid<bool> for PathingGrid {
fn new(width: usize, height: usize, default_value: bool) -> Self {
let mut base_grid = PathingGrid {
grid: BoolGrid::new(width, height, default_value),
jump_point: SimpleGrid::new(width, height, 0b00000000),
neighbours: SimpleGrid::new(width, height, 0b11111111),
components: UnionFind::new(width * height),
components_dirty: false,
Expand All @@ -553,16 +611,7 @@ impl Grid<bool> for PathingGrid {
allow_diagonal_move: true,
context: Arc::new(Mutex::new(AstarContext::new())),
};
// Emulates 'placing' of blocked tile around map border to correctly initialize neighbours
// and make behaviour of a map bordered by tiles the same as a borderless map.
for i in -1..=(width as i32) {
base_grid.update_neighbours(i, -1, true);
base_grid.update_neighbours(i, height as i32, true);
}
for j in -1..=(height as i32) {
base_grid.update_neighbours(-1, j, true);
base_grid.update_neighbours(width as i32, j, true);
}
base_grid.initialize();
base_grid
}
fn get(&self, x: usize, y: usize) -> bool {
Expand All @@ -585,6 +634,7 @@ impl Grid<bool> for PathingGrid {
}
self.update_neighbours(p.x, p.y, blocked);
self.grid.set(x, y, blocked);
self.fix_jumppoints(p);
}
fn width(&self) -> usize {
self.grid.width()
Expand Down
Loading