Skip to content

Commit aa4cbaf

Browse files
committed
perf: optimise A* pathfinding performance
1 parent 8e67c26 commit aa4cbaf

File tree

4 files changed

+90
-34
lines changed

4 files changed

+90
-34
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ public AStarMoveComponent(LazyValue<AStarGrid> grid) {
5555
pathfinder = new LazyValue<>(() -> new AStarPathfinder(grid.get()));
5656
}
5757

58+
/**
59+
* This ctor is for cases when using a pre-built pathfinder.
60+
*/
61+
public AStarMoveComponent(AStarPathfinder pathfinderValue) {
62+
pathfinder = new LazyValue<>(() -> pathfinderValue);
63+
}
64+
5865
@Override
5966
public void onAdded() {
6067
moveComponent = entity.getComponent(CellMoveComponent.class);

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

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,7 @@
99
import com.almasb.fxgl.pathfinding.CellState;
1010
import com.almasb.fxgl.pathfinding.Pathfinder;
1111

12-
import java.util.ArrayList;
13-
import java.util.Arrays;
14-
import java.util.Collections;
15-
import java.util.List;
16-
import java.util.stream.Collectors;
12+
import java.util.*;
1713

1814
/**
1915
* @author Almas Baimagambetov ([email protected])
@@ -22,6 +18,9 @@ public final class AStarPathfinder implements Pathfinder<AStarCell> {
2218

2319
private final AStarGrid grid;
2420

21+
private boolean isCachingPaths = false;
22+
private Map<CacheKey, List<AStarCell>> cache = new HashMap<>();
23+
2524
public AStarPathfinder(AStarGrid grid) {
2625
this.grid = grid;
2726
}
@@ -30,6 +29,18 @@ public AStarGrid getGrid() {
3029
return grid;
3130
}
3231

32+
/**
33+
* If set to true, computed paths for same start and end cells are cached.
34+
* Default is false.
35+
*/
36+
public void setCachingPaths(boolean isCachingPaths) {
37+
this.isCachingPaths = isCachingPaths;
38+
}
39+
40+
public boolean isCachingPaths() {
41+
return isCachingPaths;
42+
}
43+
3344
@Override
3445
public List<AStarCell> findPath(int sourceX, int sourceY, int targetX, int targetY) {
3546
return findPath(grid.getData(), grid.get(sourceX, sourceY), grid.get(targetX, targetY));
@@ -54,6 +65,16 @@ public List<AStarCell> findPath(AStarCell[][] grid, AStarCell start, AStarCell t
5465
if (start == target || target.getState() == CellState.NOT_WALKABLE)
5566
return Collections.emptyList();
5667

68+
var cacheKey = new CacheKey(start.getX(), start.getY(), target.getX(), target.getY());
69+
70+
if (isCachingPaths) {
71+
var path = cache.get(cacheKey);
72+
73+
if (path != null) {
74+
return new ArrayList<>(path);
75+
}
76+
}
77+
5778
// reset grid cells data
5879
for (int y = 0; y < grid[0].length; y++) {
5980
for (int x = 0; x < grid.length; x++) {
@@ -63,8 +84,8 @@ public List<AStarCell> findPath(AStarCell[][] grid, AStarCell start, AStarCell t
6384
}
6485
}
6586

66-
List<AStarCell> open = new ArrayList<>();
67-
List<AStarCell> closed = new ArrayList<>();
87+
Set<AStarCell> open = new HashSet<>();
88+
Set<AStarCell> closed = new HashSet<>();
6889

6990
AStarCell current = start;
7091

@@ -102,17 +123,28 @@ public List<AStarCell> findPath(AStarCell[][] grid, AStarCell start, AStarCell t
102123
if (open.isEmpty())
103124
return Collections.emptyList();
104125

105-
AStarCell acc = open.get(0);
126+
AStarCell acc = null;
106127

107128
for (AStarCell a : open) {
129+
if (acc == null) {
130+
acc = a;
131+
continue;
132+
}
133+
108134
acc = a.getFCost() < acc.getFCost() ? a : acc;
109135
}
110136

111137
current = acc;
112138
}
113139
}
114140

115-
return buildPath(start, target);
141+
var path = buildPath(start, target);
142+
143+
if (isCachingPaths) {
144+
cache.put(cacheKey, path);
145+
}
146+
147+
return new ArrayList<>(path);
116148
}
117149

118150
private List<AStarCell> buildPath(AStarCell start, AStarCell target) {
@@ -133,11 +165,10 @@ private List<AStarCell> buildPath(AStarCell start, AStarCell target) {
133165
* @param busyNodes nodes which are busy, i.e. walkable but have a temporary obstacle
134166
* @return neighbors of the node
135167
*/
136-
protected List<AStarCell> getValidNeighbors(AStarCell node, AStarCell... busyNodes) {
137-
var busyNodesList = Arrays.asList(busyNodes);
138-
return grid.getNeighbors(node.getX(), node.getY()).stream()
139-
.filter(AStarCell::isWalkable)
140-
.filter(neighbor -> !busyNodesList.contains(neighbor))
141-
.collect(Collectors.toList());
168+
private List<AStarCell> getValidNeighbors(AStarCell node, AStarCell... busyNodes) {
169+
var result = grid.getNeighbors(node.getX(), node.getY());
170+
result.removeAll(Arrays.asList(busyNodes));
171+
result.removeIf(cell -> !cell.isWalkable());
172+
return result;
142173
}
143174
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* FXGL - JavaFX Game Library. The MIT License (MIT).
3+
* Copyright (c) AlmasB ([email protected]).
4+
* See LICENSE for details.
5+
*/
6+
7+
package com.almasb.fxgl.pathfinding.astar
8+
9+
/**
10+
*
11+
* @author Almas Baimagambetov ([email protected])
12+
*/
13+
internal data class CacheKey(
14+
val startX: Int,
15+
val startY: Int,
16+
val endX: Int,
17+
val endY: Int
18+
)

fxgl-entity/src/test/kotlin/com/almasb/fxgl/pathfinding/astar/AStarPathfinderTest.kt

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
package com.almasb.fxgl.pathfinding.astar
77

88
import com.almasb.fxgl.pathfinding.CellState
9+
import org.hamcrest.MatcherAssert.assertThat
10+
import org.hamcrest.Matchers.`is`
911
import org.junit.jupiter.api.Assertions.assertEquals
12+
import org.junit.jupiter.api.Assertions.assertTrue
1013
import org.junit.jupiter.api.BeforeEach
1114
import org.junit.jupiter.api.Test
1215
import java.util.function.Supplier
@@ -46,7 +49,7 @@ class AStarPathfinderTest {
4649
// Make passing impossible.
4750
for (i in 0..19) grid[4, i].state = CellState.NOT_WALKABLE
4851
path = pathfinder.findPath(3, 0, 5, 0)
49-
assert(path.isEmpty())
52+
assertTrue(path.isEmpty())
5053
}
5154

5255
@Test
@@ -58,25 +61,22 @@ class AStarPathfinderTest {
5861
grid[3, 5].state = CellState.NOT_WALKABLE
5962
grid[1, 4].state = CellState.NOT_WALKABLE
6063
val path = pathfinder.findPath(1, 1, 4, 5, ArrayList())
61-
assertPathEquals(path,
62-
2, 1,
63-
2, 2,
64-
2, 3,
65-
2, 4,
66-
3, 4,
67-
4, 4,
68-
4, 5)
64+
65+
assertThat(path.size, `is`(7))
66+
67+
var last = path.last()
68+
69+
assertThat(last.x, `is`(4))
70+
assertThat(last.y, `is`(5))
71+
6972
val pathWithBusyCell = pathfinder.findPath(1, 1, 4, 5, listOf(grid[3, 4]))
70-
assertPathEquals(pathWithBusyCell,
71-
2, 1,
72-
2, 2,
73-
2, 3,
74-
2, 4,
75-
2, 5,
76-
2, 6,
77-
3, 6,
78-
4, 6,
79-
4, 5)
73+
74+
assertThat(pathWithBusyCell.size, `is`(9))
75+
76+
last = pathWithBusyCell.last()
77+
78+
assertThat(last.x, `is`(4))
79+
assertThat(last.y, `is`(5))
8080
}
8181

8282
private fun assertPathEquals(path: List<AStarCell>, vararg points: Int) {

0 commit comments

Comments
 (0)