astar_graph_walker.bmx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. ' Class for walking an astar graph
  2. ' It's up to you to make the graph yourself.
  3. Import "astar_node.bmx"
  4. Import "priority_queue.bmx"
  5. Import "Callback.bmx"
  6. ' Customised callback class
  7. Type AStarCallback Extends Callback
  8. Field node:AStarNode;
  9. Field queue:PriorityQueue
  10. End Type
  11. Type AStarGraphWalker
  12. ' public
  13. ' Constructor
  14. Method New()
  15. _queue = New PriorityQueue
  16. _finalPath = New TList
  17. End Method
  18. ' public
  19. ' Sets the parameters for walking
  20. ' startNode - The first node in the graph where searching will start from
  21. ' endNode - The node you're trying to find the path to
  22. ' maxNodes - The maximum number of nodes in the graph you want to walk
  23. Method setParameters(startNode:AStarNode, endNode:AStarNode, maxNodes:Int )
  24. Assert startNode <> Null,"startNode Null"
  25. Assert endNode <> Null,"endNode Null"
  26. Assert maxNodes > 0,"maxNodes <= 0"
  27. _start = startNode; ' The start of the graph
  28. _end = endNode; ' The start of the graph
  29. _queue.setMaxSize(maxNodes)
  30. _parametersSet = True;
  31. End Method
  32. ' public
  33. ' Sets the callback function
  34. ' callback - A object with a callback() mtehod. Derive from Callback Type
  35. Method setCallback(callback:AStarCallback)
  36. _callback = callback
  37. End Method
  38. Method setDistanceFunction(func:Int)
  39. _distanceFunction = func
  40. End Method
  41. Method setHeuristicMultiplier(heuristic:Float)
  42. _heuristicMultiplier = heuristic
  43. End Method
  44. ' public
  45. ' Returns the final path after a path find
  46. Method getFinalPath:TList()
  47. Assert _lastWalkSucceeded, "Can't return path as last path walk failed"
  48. Return _finalPath
  49. End Method
  50. ' private
  51. ' Makes the list of successors that will be searched next
  52. ' ARGUMENTS:
  53. ' node - The node who's successors we're looking at
  54. ' endNode - The destination node we're trying to get to
  55. Method _makeSuccessors(node:AStarNode, endNode:AStarNode)
  56. Local done:Int = False
  57. For neighbour:AStarNeighbourInfo = EachIn node.getNeighbours()
  58. Local neighbourNode:AStarNode = neighbour.getNode()
  59. Assert neighbourNode,"Node is NULL"
  60. ' Only look at neighbours that aren't the start node and also aren't in
  61. ' the closed list (We'd be backtracking)
  62. If neighbourNode <> _start And Not neighbourNode.inClosed()
  63. ' Calculate total cost to get to this neighbour based on edge cost to neighbour +
  64. ' current cost to get to us
  65. Local cost:Float = neighbour.edgeCost() + node.costToGetHere()
  66. ' Estimate a distance to the goal from the neighbour
  67. Local goalDistance:Float = _distanceTo(neighbourNode, endNode)
  68. ' If heuristic was 0 then we'd have an optimal search...
  69. goalDistance = goalDistance * _heuristicMultiplier
  70. ' What we guess the total cost would be to get from start to finish through
  71. ' this neighbour
  72. Local totalCostOfNeighbourPath:Float = goalDistance + cost
  73. ' If we haven't visited this neighbour node yet at all, save it for later visiting
  74. ' This line used to be If Not neighbourNode.inClosed() And Not neighbourNode.inOpen()
  75. ' Don't need it as now we have the optimisation above that doesn't enter here if they
  76. ' neighbour is in the closed list
  77. ' If Not neighbourNode.inClosed()And Not neighbourNode.inOpen()
  78. If Not neighbourNode.inOpen()
  79. ' Assume we'll go from us to neighbour by setting us as the parent
  80. neighbourNode.setParent(node)
  81. ' Set the PQueue key as the total cost of this path to the goal
  82. ' Queue is sorted smallest first so we always look at shortest distances
  83. neighbourNode.setKey(totalCostOfNeighbourPath) ' Goes in queue based on total distance
  84. ' Save what we calculated that the cost was to get to this neighbour
  85. neighbourNode.setCostToGetHere(cost) ' What we consider it's cost is
  86. neighbourNode.setGoalDistance(goalDistance) ' What we consider it's cost is
  87. neighbourNode.setInOpen(True)
  88. _queue.insert(neighbourNode)
  89. Else
  90. ' OK, neighbour is in a list (Actually must be in Open list at this point)
  91. ' so see if we have a better path to it by seeing if the cost to get
  92. ' to this neighbour the other way is more than the cost from our node
  93. ' to this neighbour.
  94. ' If it is, then our path is better
  95. If neighbourNode.costToGetHere() > cost
  96. ' If it was in the closed list, then we're going to put it in the open list
  97. ' cause we want to now be able to look at it again as a possible path
  98. ' If neighbourNode.inClosed()
  99. ' neighbourNode.setInClosed(False)
  100. ' EndIf
  101. ' Above is removed because of optimisation
  102. neighbourNode.setParent(node)
  103. neighbourNode.setKey(totalCostOfNeighbourPath) ' Goes in queue based on total distance
  104. neighbourNode.setGoalDistance(goalDistance) ' Estimate to get to goal
  105. neighbourNode.setCostToGetHere(cost) ' What we consider it's cost is
  106. 'TODO: Optimise this. Rather than remove and add, we could shift in the queue if
  107. ' we knew it's index.
  108. ' Removed if below because optimisation allows us to know that we must be in the open list to get here
  109. ' If neighbourNode.inOpen()
  110. pos:Int = _queue.find(neighbourNode)
  111. Assert pos > 0, "Was going to remove item that wasn't in queue!"
  112. _queue.remove(pos)
  113. neighbourNode.setInOpen(False)
  114. ' EndIf
  115. _queue.insert(neighbourNode)
  116. neighbourNode.setInOpen(True)
  117. EndIf
  118. EndIf
  119. EndIf
  120. ' If _callback <> Null
  121. ' _callback.node = node
  122. ' _callback.queue = _queue
  123. ' _callback.callback()
  124. ' Flip;WaitKey
  125. ' EndIf
  126. Next
  127. End Method
  128. ' public
  129. ' Method to walk the graph, finding the shortest path
  130. '
  131. ' RETURNS:
  132. ' False - Failed to find path to the end node
  133. ' True - Found a path to the end
  134. ' PRE: Must have called setParameters first
  135. Method walk()
  136. Assert _parametersSet,"Must call setParameters() first"
  137. _lastWalkSucceeded = False
  138. Local startNode:AStarNode = _start
  139. Local endNode:AStarNode =_end
  140. ' Initialise starting node's information
  141. Local distanceToGoal:Float = _distanceTo(_start, _end)
  142. startNode.setCostToGetHere(0)
  143. startNode.setKey(distanceToGoal * _heuristicMultiplier + startNode.costToGetHere())
  144. startNode.setParent(Null)
  145. startNode.setInOpen(True)
  146. _queue.insert(startNode)
  147. While _queue.size() > 0
  148. Local node:AStarNode = AStarNode(_queue.remove())
  149. ' node.setInOpen(False)
  150. node.setInClosed(True)
  151. ' Have we found our destination???
  152. If node = endNode Then
  153. Local currentNode:AStarNode = node
  154. While currentNode <> Null
  155. _finalPath.AddFirst(currentNode)
  156. currentNode = currentNode.getParent()
  157. Wend
  158. _lastWalkSucceeded = True
  159. _queue = Null
  160. Return True
  161. EndIf
  162. _makeSuccessors(node, endNode)
  163. If _callback <> Null
  164. _callback.node = node
  165. _callback.queue = _queue
  166. _callback.callback()
  167. EndIf
  168. ' temp
  169. Wend
  170. Return False
  171. End Method
  172. ' Resets a node so that it's ready for a new path find. Call this from whatever manages the nodes
  173. ' as AStarGraphWalker, doesn't actually know how many, or what nodes you have, but it does know how
  174. ' to reset one
  175. Method resetNode(node:AStarNode)
  176. node._parentNode = Null
  177. node.setInClosed(False)
  178. node.setInOpen(False)
  179. End Method
  180. 'private
  181. ' Returns an estimated distance between two nodes
  182. Method _distanceTo:Float(startNode:AStarNode, endNode:AStarNode)
  183. Local startX = startNode._x
  184. Local startY = startNode._y
  185. Local endX = endNode._x
  186. Local endY = endNode._y
  187. Local dx = Abs(endX - startX)
  188. Local dy = Abs(endY - startY)
  189. 'TODO: I had distanceFunction without the _ below and Blitz Didn't complain
  190. Select _distanceFunction
  191. Case distanceEuclidean
  192. Return Sqr( dx * dx + dy * dy )
  193. Case distancePseudoEuclidean
  194. Return dx * dx + dy * dy
  195. Case distanceManhatten
  196. Return dx + dy
  197. Case distanceDiagonalShortcut
  198. If dx > dy
  199. Return 1.4*dy + (dx-dy)
  200. Else
  201. Return 1.4*dx + (dy-dx)
  202. End If
  203. Default
  204. Assert 0,"Bad distance function"
  205. End Select
  206. End Method
  207. ' Fields
  208. ' Possible ways to calculate distance. It's good to specify the edge costs between nodes
  209. ' relative to your distance calculations because as they are related. For instance, if you calculate edge costs
  210. ' using simple Euclidean distance, so that two adjacent blocks would be 1 away or 1.4 (if diagonal)
  211. ' multiplied by some small "difficulty factor", say 1 for normal roads, or 2 for water
  212. ' Then distanceEuclidean is a good estimator of distance and distancePseudoEuclidean
  213. ' tends to override the edgecosts and the pathfinder sort of busts through them.
  214. ' This can be a good thing as it could provide a simple way to make a unit "dumber"
  215. Const distanceEuclidean = 0
  216. Const distancePseudoEuclidean = 1
  217. Const distanceManhatten = 2
  218. Const distanceDiagonalShortcut = 3
  219. Field _heuristicMultiplier = 1 ' 0 should generate "optimal" path
  220. Field _start:AStarNode
  221. Field _end:AStarNode
  222. Field _distanceMode:Int = distanceEuclidean
  223. Field _queue:PriorityQueue
  224. Field _parametersSet = False
  225. Field _finalPath:TList
  226. Field _callback:AStarCallback = Null
  227. Field _distanceFunction = distanceEuclidean
  228. Field _lastWalkSucceeded = False
  229. EndType