Skip to content

Commit 641b614

Browse files
feat: Support 8 directions AStar pathfinding, closes #1213
Co-authored-by: Jean-René Lavoie <>
1 parent 6c00330 commit 641b614

7 files changed

Lines changed: 310 additions & 10 deletions

File tree

fxgl-entity/src/main/java/com/almasb/fxgl/pathfinding/Pathfinder.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package com.almasb.fxgl.pathfinding;
88

99
import com.almasb.fxgl.core.collection.grid.Cell;
10+
import com.almasb.fxgl.core.collection.grid.NeighborFilteringOption;
1011

1112
import java.util.List;
1213

@@ -22,10 +23,25 @@ public interface Pathfinder<T extends Cell> {
2223
*/
2324
List<T> findPath(int sourceX, int sourceY, int targetX, int targetY);
2425

26+
/**
27+
* Empty list is returned if no path exists.
28+
*
29+
* @return a list of cells from source (excl.) to target (incl.)
30+
*/
31+
List<T> findPath(int sourceX, int sourceY, int targetX, int targetY, NeighborFilteringOption neighborFilteringOption);
32+
2533
/**
2634
* Empty list is returned if no path exists.
2735
*
2836
* @return a list of cells from source (excl.) to target (incl.) while ignoring busyCells
2937
*/
3038
List<T> findPath(int sourceX, int sourceY, int targetX, int targetY, List<T> busyCells);
39+
40+
/**
41+
* Empty list is returned if no path exists.
42+
*
43+
* @return a list of cells from source (excl.) to target (incl.) while ignoring busyCells
44+
*/
45+
List<T> findPath(int sourceX, int sourceY, int targetX, int targetY, NeighborFilteringOption neighborFilteringOption, List<T> busyCells);
46+
3147
}

fxgl-entity/src/main/java/com/almasb/fxgl/pathfinding/astar/AStarPathfinder.java

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@
66

77
package com.almasb.fxgl.pathfinding.astar;
88

9+
import com.almasb.fxgl.core.collection.grid.Cell;
10+
import com.almasb.fxgl.core.collection.grid.NeighborFilteringOption;
11+
import static com.almasb.fxgl.core.collection.grid.NeighborFilteringOption.*;
912
import com.almasb.fxgl.pathfinding.CellState;
1013
import com.almasb.fxgl.pathfinding.Pathfinder;
14+
import com.almasb.fxgl.pathfinding.heuristic.Heuristic;
15+
import com.almasb.fxgl.pathfinding.heuristic.ManhattanDistance;
16+
import com.almasb.fxgl.pathfinding.heuristic.OctileDistance;
1117

1218
import java.util.*;
1319

@@ -18,11 +24,20 @@ public final class AStarPathfinder implements Pathfinder<AStarCell> {
1824

1925
private final AStarGrid grid;
2026

27+
private final Heuristic<AStarCell> defaultHeuristic;
28+
private final Heuristic<AStarCell> diagonalHeuristic;
29+
2130
private boolean isCachingPaths = false;
2231
private Map<CacheKey, List<AStarCell>> cache = new HashMap<>();
2332

2433
public AStarPathfinder(AStarGrid grid) {
34+
this(grid, new ManhattanDistance<>(10), new OctileDistance<>());
35+
}
36+
37+
public AStarPathfinder(AStarGrid grid, Heuristic<AStarCell> defaultHeuristic, Heuristic<AStarCell> diagonalHeuristic) {
2538
this.grid = grid;
39+
this.defaultHeuristic = defaultHeuristic;
40+
this.diagonalHeuristic = diagonalHeuristic;
2641
}
2742

2843
public AStarGrid getGrid() {
@@ -46,11 +61,21 @@ public List<AStarCell> findPath(int sourceX, int sourceY, int targetX, int targe
4661
return findPath(grid.getData(), grid.get(sourceX, sourceY), grid.get(targetX, targetY));
4762
}
4863

64+
@Override
65+
public List<AStarCell> findPath(int sourceX, int sourceY, int targetX, int targetY, NeighborFilteringOption neighborFilteringOption) {
66+
return findPath(grid.getData(), grid.get(sourceX, sourceY), grid.get(targetX, targetY), neighborFilteringOption);
67+
}
68+
4969
@Override
5070
public List<AStarCell> findPath(int sourceX, int sourceY, int targetX, int targetY, List<AStarCell> busyCells) {
5171
return findPath(grid.getData(), grid.get(sourceX, sourceY), grid.get(targetX, targetY), busyCells.toArray(new AStarCell[0]));
5272
}
5373

74+
@Override
75+
public List<AStarCell> findPath(int sourceX, int sourceY, int targetX, int targetY, NeighborFilteringOption neighborFilteringOption, List<AStarCell> busyCells) {
76+
return findPath(grid.getData(), grid.get(sourceX, sourceY), grid.get(targetX, targetY), neighborFilteringOption, busyCells.toArray(new AStarCell[0]));
77+
}
78+
5479
/**
5580
* Since the equality check is based on references,
5681
* start and target must be elements of the array.
@@ -62,6 +87,20 @@ public List<AStarCell> findPath(int sourceX, int sourceY, int targetX, int targe
6287
* @return path as list of nodes from start (excl) to target (incl) or empty list if no path found
6388
*/
6489
public List<AStarCell> findPath(AStarCell[][] grid, AStarCell start, AStarCell target, AStarCell... busyNodes) {
90+
return findPath(grid, start, target, NeighborFilteringOption.FOUR_DIRECTIONS, busyNodes);
91+
}
92+
93+
/**
94+
* Since the equality check is based on references,
95+
* start and target must be elements of the array.
96+
*
97+
* @param grid the grid of nodes
98+
* @param start starting node
99+
* @param target target node
100+
* @param busyNodes busy "unwalkable" nodes
101+
* @return path as list of nodes from start (excl) to target (incl) or empty list if no path found
102+
*/
103+
public List<AStarCell> findPath(AStarCell[][] grid, AStarCell start, AStarCell target, NeighborFilteringOption neighborFilteringOption, AStarCell... busyNodes) {
65104
if (start == target || target.getState() == CellState.NOT_WALKABLE)
66105
return Collections.emptyList();
67106

@@ -75,10 +114,12 @@ public List<AStarCell> findPath(AStarCell[][] grid, AStarCell start, AStarCell t
75114
}
76115
}
77116

117+
Heuristic<AStarCell> heuristic = (neighborFilteringOption.is(FOUR_DIRECTIONS)) ? defaultHeuristic : diagonalHeuristic;
118+
78119
// reset grid cells data
79120
for (int y = 0; y < grid[0].length; y++) {
80121
for (int x = 0; x < grid.length; x++) {
81-
grid[x][y].setHCost(Math.abs(target.getX() - x) + Math.abs(target.getY() - y));
122+
grid[x][y].setHCost(heuristic.getCost(x, y, target));
82123
grid[x][y].setParent(null);
83124
grid[x][y].setGCost(0);
84125
}
@@ -92,7 +133,7 @@ public List<AStarCell> findPath(AStarCell[][] grid, AStarCell start, AStarCell t
92133
boolean found = false;
93134

94135
while (!found && !closed.contains(target)) {
95-
for (AStarCell neighbor : getValidNeighbors(current, busyNodes)) {
136+
for (AStarCell neighbor : getValidNeighbors(current, neighborFilteringOption, busyNodes)) {
96137
if (neighbor == target) {
97138
target.setParent(current);
98139
found = true;
@@ -101,16 +142,18 @@ public List<AStarCell> findPath(AStarCell[][] grid, AStarCell start, AStarCell t
101142
}
102143

103144
if (!closed.contains(neighbor)) {
145+
int gCost = isDiagonal(current, neighbor) ? diagonalHeuristic.getWeight() : defaultHeuristic.getWeight();
146+
int newGCost = current.getGCost() + gCost;
147+
104148
if (open.contains(neighbor)) {
105-
int newG = current.getGCost() + 10;
149+
if (newGCost < neighbor.getGCost()) {
106150

107-
if (newG < neighbor.getGCost()) {
108151
neighbor.setParent(current);
109-
neighbor.setGCost(newG);
152+
neighbor.setGCost(newGCost);
110153
}
111154
} else {
112155
neighbor.setParent(current);
113-
neighbor.setGCost(current.getGCost() + 10);
156+
neighbor.setGCost(newGCost);
114157
open.add(neighbor);
115158
}
116159
}
@@ -165,10 +208,15 @@ private List<AStarCell> buildPath(AStarCell start, AStarCell target) {
165208
* @param busyNodes nodes which are busy, i.e. walkable but have a temporary obstacle
166209
* @return neighbors of the node
167210
*/
168-
private List<AStarCell> getValidNeighbors(AStarCell node, AStarCell... busyNodes) {
169-
var result = grid.getNeighbors(node.getX(), node.getY());
211+
private List<AStarCell> getValidNeighbors(AStarCell node, NeighborFilteringOption neighborFilteringOption, AStarCell... busyNodes) {
212+
var result = grid.getNeighbors(node.getX(), node.getY(), neighborFilteringOption);
170213
result.removeAll(Arrays.asList(busyNodes));
171214
result.removeIf(cell -> !cell.isWalkable());
172215
return result;
173216
}
217+
218+
private boolean isDiagonal(Cell current, Cell neighbor) {
219+
return neighbor.getX() - current.getX() != 0 && neighbor.getY() - current.getY() != 0;
220+
}
221+
174222
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* FXGL - JavaFX Game Library. The MIT License (MIT).
3+
* Copyright (c) AlmasB (almaslvl@gmail.com).
4+
* See LICENSE for details.
5+
*/
6+
7+
package com.almasb.fxgl.pathfinding.heuristic;
8+
9+
import com.almasb.fxgl.core.collection.grid.Cell;
10+
11+
/**
12+
* @author Jean-René Lavoie (jeanrlavoie@gmail.com)
13+
*/
14+
public abstract class Heuristic<T extends Cell> {
15+
16+
public static final int DEFAULT_WEIGHT = 10;
17+
18+
private final int weight;
19+
20+
public Heuristic() {
21+
this(DEFAULT_WEIGHT);
22+
}
23+
24+
public Heuristic(int weight) {
25+
this.weight = weight;
26+
}
27+
28+
public abstract int getCost(int x, int y, T target);
29+
30+
public int getWeight() {
31+
return weight;
32+
}
33+
34+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* FXGL - JavaFX Game Library. The MIT License (MIT).
3+
* Copyright (c) AlmasB (almaslvl@gmail.com).
4+
* See LICENSE for details.
5+
*/
6+
7+
package com.almasb.fxgl.pathfinding.heuristic;
8+
9+
import com.almasb.fxgl.core.collection.grid.Cell;
10+
11+
/**
12+
* @author Jean-René Lavoie (jeanrlavoie@gmail.com)
13+
*/
14+
public class ManhattanDistance<T extends Cell> extends Heuristic<T> {
15+
16+
public ManhattanDistance() {
17+
super();
18+
}
19+
20+
public ManhattanDistance(int weight) {
21+
super(weight);
22+
}
23+
24+
@Override
25+
public int getCost(int x, int y, T target) {
26+
return (Math.abs(target.getX() - x) + Math.abs(target.getY() - y)) * getWeight();
27+
}
28+
29+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* FXGL - JavaFX Game Library. The MIT License (MIT).
3+
* Copyright (c) AlmasB (almaslvl@gmail.com).
4+
* See LICENSE for details.
5+
*/
6+
7+
package com.almasb.fxgl.pathfinding.heuristic;
8+
9+
import com.almasb.fxgl.core.collection.grid.Cell;
10+
11+
/**
12+
* @author Jean-René Lavoie (jeanrlavoie@gmail.com)
13+
*/
14+
public class OctileDistance<T extends Cell> extends Heuristic<T> {
15+
16+
private static final int DIAGONAL_WEIGHT = (int)(Math.sqrt(2) * 10.0);
17+
private static final int DIAGONAL_FACTOR = DIAGONAL_WEIGHT - 10;
18+
19+
public OctileDistance() {
20+
super(DIAGONAL_WEIGHT);
21+
}
22+
23+
public OctileDistance(int weight) {
24+
super(weight);
25+
}
26+
27+
@Override
28+
public int getCost(int x, int y, T target) {
29+
int dx = Math.abs(x - target.getX());
30+
int dy = Math.abs(y - target.getY());
31+
32+
if(dx == dy) {
33+
return (dx + dy) * 10;
34+
}
35+
if(dx < dy) {
36+
return DIAGONAL_FACTOR * dx + 10 * dy;
37+
}
38+
return DIAGONAL_FACTOR * dy + 10 * dx;
39+
}
40+
41+
}

fxgl-entity/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
exports com.almasb.fxgl.particle;
2424
exports com.almasb.fxgl.pathfinding;
2525
exports com.almasb.fxgl.pathfinding.astar;
26+
exports com.almasb.fxgl.pathfinding.heuristic;
2627
exports com.almasb.fxgl.pathfinding.maze;
2728
exports com.almasb.fxgl.physics;
2829
exports com.almasb.fxgl.physics.box2d.dynamics;

0 commit comments

Comments
 (0)