Testing out JmonkeyEngine to make a game in Scala with Akka Actors within a pure FP layer
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

371 lines
11 KiB

  1. /*******************************************************************************
  2. * Copyright 2014 See AUTHORS file.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. ******************************************************************************/
  16. package wow.doge.mygame.game.subsystems.ai.gdx;
  17. import com.badlogic.gdx.ai.pfa.Connection;
  18. import com.badlogic.gdx.ai.pfa.GraphPath;
  19. import com.badlogic.gdx.ai.pfa.Heuristic;
  20. import com.badlogic.gdx.ai.pfa.PathFinder;
  21. import com.badlogic.gdx.ai.pfa.PathFinderQueue;
  22. import com.badlogic.gdx.ai.pfa.PathFinderRequest;
  23. import com.badlogic.gdx.utils.Array;
  24. import com.badlogic.gdx.utils.BinaryHeap;
  25. import com.badlogic.gdx.utils.TimeUtils;
  26. import java.util.List;
  27. import wow.doge.mygame.game.subsystems.ai.gdx.Graph;
  28. import wow.doge.mygame.game.subsystems.ai.gdx.MyIndexedGraph;
  29. import scala.collection.immutable.IndexedSeq;
  30. /**
  31. * A fully implemented {@link PathFinder} that can perform both interruptible
  32. * and non-interruptible pathfinding.
  33. * <p>
  34. * This implementation is a common variation of the A* algorithm that is faster
  35. * than the general A*.
  36. * <p>
  37. * In the general A* implementation, data are held for each node in the open or
  38. * closed lists, and these data are held as a NodeRecord instance. Records are
  39. * created when a node is first considered and then moved between the open and
  40. * closed lists, as required. There is a key step in the algorithm where the
  41. * lists are searched for a node record corresponding to a particular node. This
  42. * operation is something time-consuming.
  43. * <p>
  44. * The indexed A* algorithm improves execution speed by using an array of all
  45. * the node records for every node in the graph. Nodes must be numbered using
  46. * sequential integers (see {@link MyIndexedGraph#getIndex(Object)}), so we
  47. * don't need to search for a node in the two lists at all. We can simply use
  48. * the node index to look up its record in the array (creating it if it is
  49. * missing). This means that the close list is no longer needed. To know whether
  50. * a node is open or closed, we use the {@link NodeRecord#category category} of
  51. * the node record. This makes the search step very fast indeed (in fact, there
  52. * is no search, and we can go straight to the information we need).
  53. * Unfortunately, we can't get rid of the open list because we still need to be
  54. * able to retrieve the element with the lowest cost. However, we use a
  55. * {@link BinaryHeap} for the open list in order to keep performance as high as
  56. * possible.
  57. *
  58. * @param <N> Type of node
  59. *
  60. * @author davebaol
  61. */
  62. public class IndexedAStarPathFinder<N> implements PathFinder<N> {
  63. MyIndexedGraph<N> graph;
  64. NodeRecord<N>[] nodeRecords;
  65. BinaryHeap<NodeRecord<N>> openList;
  66. NodeRecord<N> current;
  67. public Metrics metrics;
  68. /** The unique ID for each search run. Used to mark nodes. */
  69. private int searchId;
  70. private static final int UNVISITED = 0;
  71. private static final int OPEN = 1;
  72. private static final int CLOSED = 2;
  73. public IndexedAStarPathFinder(MyIndexedGraph<N> graph) {
  74. this(graph, false);
  75. }
  76. @SuppressWarnings("unchecked")
  77. public IndexedAStarPathFinder(MyIndexedGraph<N> graph, boolean calculateMetrics) {
  78. this.graph = graph;
  79. this.nodeRecords = (NodeRecord<N>[]) new NodeRecord[graph.getNodeCount()];
  80. this.openList = new BinaryHeap<NodeRecord<N>>();
  81. if (calculateMetrics)
  82. this.metrics = new Metrics();
  83. }
  84. @Override
  85. public boolean searchConnectionPath(N startNode, N endNode, Heuristic<N> heuristic,
  86. GraphPath<Connection<N>> outPath) {
  87. // Perform AStar
  88. boolean found = search(startNode, endNode, heuristic);
  89. if (found) {
  90. // Create a path made of connections
  91. generateConnectionPath(startNode, outPath);
  92. }
  93. return found;
  94. }
  95. @Override
  96. public boolean searchNodePath(N startNode, N endNode, Heuristic<N> heuristic, GraphPath<N> outPath) {
  97. // Perform AStar
  98. boolean found = search(startNode, endNode, heuristic);
  99. if (found) {
  100. // Create a path made of nodes
  101. generateNodePath(startNode, outPath);
  102. }
  103. return found;
  104. }
  105. protected boolean search(N startNode, N endNode, Heuristic<N> heuristic) {
  106. initSearch(startNode, endNode, heuristic);
  107. // Iterate through processing each node
  108. do {
  109. // Retrieve the node with smallest estimated total cost from the open list
  110. current = openList.pop();
  111. current.category = CLOSED;
  112. // Terminate if we reached the goal node
  113. if (current.node == endNode)
  114. return true;
  115. visitChildren(endNode, heuristic);
  116. } while (openList.size > 0);
  117. // We've run out of nodes without finding the goal, so there's no solution
  118. return false;
  119. }
  120. @Override
  121. public boolean search(PathFinderRequest<N> request, long timeToRun) {
  122. long lastTime = TimeUtils.nanoTime();
  123. // We have to initialize the search if the status has just changed
  124. if (request.statusChanged) {
  125. initSearch(request.startNode, request.endNode, request.heuristic);
  126. request.statusChanged = false;
  127. }
  128. // Iterate through processing each node
  129. do {
  130. // Check the available time
  131. long currentTime = TimeUtils.nanoTime();
  132. timeToRun -= currentTime - lastTime;
  133. if (timeToRun <= PathFinderQueue.TIME_TOLERANCE)
  134. return false;
  135. // Retrieve the node with smallest estimated total cost from the open list
  136. current = openList.pop();
  137. current.category = CLOSED;
  138. // Terminate if we reached the goal node; we've found a path.
  139. if (current.node == request.endNode) {
  140. request.pathFound = true;
  141. generateNodePath(request.startNode, request.resultPath);
  142. return true;
  143. }
  144. // Visit current node's children
  145. visitChildren(request.endNode, request.heuristic);
  146. // Store the current time
  147. lastTime = currentTime;
  148. } while (openList.size > 0);
  149. // The open list is empty and we've not found a path.
  150. request.pathFound = false;
  151. return true;
  152. }
  153. protected void initSearch(N startNode, N endNode, Heuristic<N> heuristic) {
  154. if (metrics != null)
  155. metrics.reset();
  156. // Increment the search id
  157. if (++searchId < 0)
  158. searchId = 1;
  159. // Initialize the open list
  160. openList.clear();
  161. // Initialize the record for the start node and add it to the open list
  162. NodeRecord<N> startRecord = getNodeRecord(startNode);
  163. startRecord.node = startNode;
  164. startRecord.connection = null;
  165. startRecord.costSoFar = 0;
  166. addToOpenList(startRecord, heuristic.estimate(startNode, endNode));
  167. current = null;
  168. }
  169. protected void visitChildren(N endNode, Heuristic<N> heuristic) {
  170. // Get current node's outgoing connections
  171. IndexedSeq<Connection<N>> connections = graph.getConnections(current.node);
  172. // Loop through each connection in turn
  173. for (int i = 0; i < connections.size(); i++) {
  174. if (metrics != null)
  175. metrics.visitedNodes++;
  176. Connection<N> connection = connections.apply(i);
  177. // Get the cost estimate for the node
  178. N node = connection.getToNode();
  179. float nodeCost = current.costSoFar + connection.getCost();
  180. float nodeHeuristic;
  181. NodeRecord<N> nodeRecord = getNodeRecord(node);
  182. if (nodeRecord.category == CLOSED) { // The node is closed
  183. // If we didn't find a shorter route, skip
  184. if (nodeRecord.costSoFar <= nodeCost)
  185. continue;
  186. // We can use the node's old cost values to calculate its heuristic
  187. // without calling the possibly expensive heuristic function
  188. nodeHeuristic = nodeRecord.getEstimatedTotalCost() - nodeRecord.costSoFar;
  189. } else if (nodeRecord.category == OPEN) { // The node is open
  190. // If our route is no better, then skip
  191. if (nodeRecord.costSoFar <= nodeCost)
  192. continue;
  193. // Remove it from the open list (it will be re-added with the new cost)
  194. openList.remove(nodeRecord);
  195. // We can use the node's old cost values to calculate its heuristic
  196. // without calling the possibly expensive heuristic function
  197. nodeHeuristic = nodeRecord.getEstimatedTotalCost() - nodeRecord.costSoFar;
  198. } else { // the node is unvisited
  199. // We'll need to calculate the heuristic value using the function,
  200. // since we don't have a node record with a previously calculated value
  201. nodeHeuristic = heuristic.estimate(node, endNode);
  202. }
  203. // Update node record's cost and connection
  204. nodeRecord.costSoFar = nodeCost;
  205. nodeRecord.connection = connection;
  206. // Add it to the open list with the estimated total cost
  207. addToOpenList(nodeRecord, nodeCost + nodeHeuristic);
  208. }
  209. }
  210. protected void generateConnectionPath(N startNode, GraphPath<Connection<N>> outPath) {
  211. // Work back along the path, accumulating connections
  212. // outPath.clear();
  213. while (current.node != startNode) {
  214. outPath.add(current.connection);
  215. current = nodeRecords[graph.getIndex(current.connection.getFromNode())];
  216. }
  217. // Reverse the path
  218. outPath.reverse();
  219. }
  220. protected void generateNodePath(N startNode, GraphPath<N> outPath) {
  221. // Work back along the path, accumulating nodes
  222. // outPath.clear();
  223. while (current.connection != null) {
  224. outPath.add(current.node);
  225. current = nodeRecords[graph.getIndex(current.connection.getFromNode())];
  226. }
  227. outPath.add(startNode);
  228. // Reverse the path
  229. outPath.reverse();
  230. }
  231. protected void addToOpenList(NodeRecord<N> nodeRecord, float estimatedTotalCost) {
  232. openList.add(nodeRecord, estimatedTotalCost);
  233. nodeRecord.category = OPEN;
  234. if (metrics != null) {
  235. metrics.openListAdditions++;
  236. metrics.openListPeak = Math.max(metrics.openListPeak, openList.size);
  237. }
  238. }
  239. protected NodeRecord<N> getNodeRecord(N node) {
  240. int index = graph.getIndex(node);
  241. NodeRecord<N> nr = nodeRecords[index];
  242. if (nr != null) {
  243. if (nr.searchId != searchId) {
  244. nr.category = UNVISITED;
  245. nr.searchId = searchId;
  246. }
  247. return nr;
  248. }
  249. nr = nodeRecords[index] = new NodeRecord<N>();
  250. nr.node = node;
  251. nr.searchId = searchId;
  252. return nr;
  253. }
  254. /**
  255. * This nested class is used to keep track of the information we need for each
  256. * node during the search.
  257. *
  258. * @param <N> Type of node
  259. *
  260. * @author davebaol
  261. */
  262. static class NodeRecord<N> extends BinaryHeap.Node {
  263. /** The reference to the node. */
  264. N node;
  265. /** The incoming connection to the node */
  266. Connection<N> connection;
  267. /** The actual cost from the start node. */
  268. float costSoFar;
  269. /** The node category: {@link #UNVISITED}, {@link #OPEN} or {@link #CLOSED}. */
  270. int category;
  271. /** ID of the current search. */
  272. int searchId;
  273. /** Creates a {@code NodeRecord}. */
  274. public NodeRecord() {
  275. super(0);
  276. }
  277. /** Returns the estimated total cost. */
  278. public float getEstimatedTotalCost() {
  279. return getValue();
  280. }
  281. }
  282. /**
  283. * A class used by {@link IndexedAStarPathFinder} to collect search metrics.
  284. *
  285. * @author davebaol
  286. */
  287. public static class Metrics {
  288. public int visitedNodes;
  289. public int openListAdditions;
  290. public int openListPeak;
  291. public Metrics() {
  292. }
  293. public void reset() {
  294. visitedNodes = 0;
  295. openListAdditions = 0;
  296. openListPeak = 0;
  297. }
  298. }
  299. }