About Monkey 2 › Forums › Monkey 2 Code Library › AStar Pathfinding
Tagged: algorithms
This topic contains 2 replies, has 3 voices, and was last updated by
Hezkore
1 year, 7 months ago.
-
AuthorPosts
-
June 10, 2017 at 9:17 am #8574
The following example calculates paths through a 2d integer array using the AStar search algorithm.
Monkey123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362'AStar pathfinding example.#Import "<std>"#Import "<mojo>"Using std..Using mojo..Function Main()New AppInstanceNew MyWindowApp.Run()End'------------Class MyWindow Extends WindowField _grid:Int[,] '0 = No Wall, 1 = Wall.Field _start:Vec2i 'Path starting coordinate.Field _goal:Vec2i 'Path goal coordinate.Field _pathFinder:PathFinderField _path:Stack<Vec2i> 'All path's coordinates.Field _diagnals:Bool 'Include diagnals in potential path (cut corners).Field _cellWidth:Int 'Cell render width.Field _cellHeight:Int 'Cell render height.Method New(title:String = "AStar Example", width:Int = 800, height:Int = 600, flags:WindowFlags = Null)Super.New( title,width,height,flags )Local cols := 40Local rows := 20_grid = New Int[cols, rows]_cellWidth = Width / cols_cellHeight = Height / rows_diagnals = False_start = New Vec2i(1, 1)_goal = New Vec2i(1, 10)_path = New Stack<Vec2i>()_pathFinder = New PathFinder(_grid)Generate()EndMethod Generate:Void()'Randomly place some walls.For Local c :=0 Until _grid.GetSize(0) 'Columns.For Local r := 0 Until _grid.GetSize(1) 'Rows.If Floor(Rnd(0, 100)) < 33 '33% chance of wall._grid[c, r] = 1 'Wall.Else_grid[c, r] = 0 'Empty.EndifNextNextCalculatePath()EndMethod CalculatePath:Void()_pathFinder.FindPath(_start, _goal, _path, _diagnals)EndMethod OnRender( canvas:Canvas ) OverrideApp.RequestRender()For Local c := 0 Until _grid.GetSize(0)For Local r := 0 Until _grid.GetSize(1)If _grid[c, r]canvas.Color = Color.DarkGrey 'Wall.Elsecanvas.Color = Color.Black 'Empty.Endifcanvas.DrawRect(c * _cellWidth, r * _cellHeight, _cellWidth, _cellHeight)NextNext'Start.canvas.Color = Color.Greencanvas.DrawEllipse(_start.X * _cellWidth + (_cellWidth / 2), (_start.Y * _cellHeight) + (_cellHeight / 2), _cellWidth / 4, _cellHeight / 4)'Path coordinates (if any).canvas.Color = Color.YellowFor Local coord := Eachin _pathcanvas.DrawRect(coord.X * _cellWidth, coord.Y * _cellHeight, _cellWidth, _cellHeight)Next'Goal.canvas.Color = Color.Redcanvas.DrawEllipse(_goal.X * _cellWidth + (_cellWidth / 2), (_goal.Y * _cellHeight) + (_cellHeight / 2), _cellWidth / 4, _cellHeight / 4)canvas.Color = Color.Whitecanvas.DrawText("[Enter]: Generate new grid, [Left Click]: Place start, [Right Click] Place goal, [Tab] Toggle diagnals.", 0, 0)canvas.Flush()EndMethod OnKeyEvent:Void(event:KeyEvent) OverrideIf event.Type = EventType.KeyDownIf event.Key = Key.EscapeApp.Terminate()Elseif event.Key = Key.Tab'Toggle "corner cutting" on/off._diagnals = Not _diagnalsCalculatePath()Elseif event.Key = Key.Enter'Generate a new grid.Generate()EndifEndifEndMethod OnMouseEvent:Void(event:MouseEvent) OverrideIf event.Type = EventType.MouseClick'Set starting coordinate.Local c:= Floor(Mouse.Location.X / _cellWidth)Local r:= Floor(Mouse.Location.Y / _cellHeight)If _grid[c, r] = 0_start = New Vec2i(c, r)CalculatePath()EndifElseif event.Type = EventType.MouseRightClick'Set goal coordinate.Local c:= Floor(Mouse.Location.X / _cellWidth)Local r:= Floor(Mouse.Location.Y / _cellHeight)If _grid[c, r] = 0_goal = New Vec2i(c, r)CalculatePath()EndifEndifEndEnd'=========================================Class PathNodePublicField Pos:Vec2i 'PositionField H:Int 'Heuristic: movement cost to goal.Field G:Int 'Movement cost of entire path.Field F:Int 'Estimated cost for full path (G + H).Field Parent:PathNode 'Node to reach this node.Method New(pos:Vec2i)Pos = posReset()EndMethod Reset:Void()H = 0G = 0F = 0Parent = NullEndEnd'-----Class PathFinderPublicMethod New(grid:Int[,])'grid: An existing 2d array of integers with dimensions Columns x RowsGrid = grid_openStack = New Stack<PathNode>()_closedStack = New Stack<PathNode>()_pathStack = New Stack<PathNode>()_adjacentNodeStack = New Stack<PathNode>()EndProperty Grid:Int[,]()Return _gridSetter (value:Int[,])If Not value Then Return_grid = value_cols = _grid.GetSize(0)_rows = _grid.GetSize(1)'Only create a new nodeGrid if the new dimensions differ from the old dimensions.If Not _nodeGrid Or _nodeGrid.GetSize(0) <> _cols Or _nodeGrid.GetSize(1) <> _rows_nodeGrid = New PathNode[_cols, _rows]For Local c := 0 Until _colsFor Local r := 0 Until _rows_nodeGrid[c, r] = New PathNode(New Vec2i(c, r))NextNextEndifEndMethod FindPath:Void(start:Vec2i, goal:Vec2i, results:Stack<Vec2i>, includeDiagnals:Bool = False)'start: Starting coordinate: column, row.'goal: Goal coordinate: column, row.'results: An existing Stack on which to place the path coordinates.If Not results Then Returnresults.Clear()If start = goal Then Return 'No path necessary'Check for invalid start or goal coordinates.If start.X < 0 Or start.Y < 0 Or goal.X < 0 Or goal.Y < 0 Then ReturnIf start.X >= _cols Or start.Y >= _rows Or goal.X >= _cols Or goal.Y >= _rows Then ReturnCalculateHeuristicCosts(goal)Local startNode := _nodeGrid[start.X, start.Y]Local goalNode := _nodeGrid[goal.X, goal.Y]_openStack.Clear()_closedStack.Clear()_pathStack.Clear()Local currentNode := startNode_openStack.Push(currentNode)_adjacentNodeStack.Clear()While Not _openStack.Empty'Initially set the minimum full path cost high.Local minF := 4000000For Local node:= Eachin _openStackIf node.F < minFminF = node.FcurrentNode = nodeEndifNext_openStack.Remove(currentNode)_closedStack.Push(currentNode)GetAdjacentNodes(currentNode, _adjacentNodeStack, includeDiagnals)For Local node:= Eachin _adjacentNodeStackIf node = goalNodenode.Parent = currentNodeWhile node.Parent_pathStack.Push(node)node = node.ParentWend_openStack.Clear()ExitElseLocal idx:=_closedStack.FindIndex(node)If idx < 0 'Node is not in closed stack.Local idx2:=_openStack.FindIndex(node)If idx2 < 0 'node is not in open stack._openStack.Push(node)node.Parent = currentNodenode.G = currentNode.G + 10node.F = node.G + node.HElseIf (currentNode.G + 10 < node.G)'Faster.node.Parent = currentNodeEndifEndifEndifEndifNextWendFor Local node:=Eachin _pathStackresults.Push(node.Pos)NextEnd'The CalculateHeuristicCosts method is implemented based on the game type.'This example uses the Manhattan technique of calculating'the distance between two points (suitable for grid-based, rogue-like games).Method CalculateHeuristicCosts:Void(goal:Vec2i)Local Distance := Lambda:Int(vecA:Vec2i, vecB:Vec2i)'Manhattan technique of calculating the distance between two points.Local result := vecA - vecBReturn Abs(result.X) + Abs(result.Y)EndFor Local c := 0 Until _colsFor Local r := 0 Until _rows_nodeGrid[c, r].Reset()_nodeGrid[c, r].H = Distance(_nodeGrid[c, r].Pos, goal)NextNextEnd'The GetAdjacentNodes method is implemented based on the game type.'In this example the nodes North, East, South and West of the currentNode'are considered adjacent. And also NE, SE, SW, NE, if includeDiagnals is True.Method GetAdjacentNodes:Void(currentNode:PathNode, results:Stack<PathNode>, includeDiagnals:Bool)If Not results Then Returnresults.Clear()Local PushIfValid := Lambda:void(c:Int, r:Int)If c < 0 Or c >= _cols Then ReturnIf r < 0 Or r >= _rows Then ReturnIf _grid[c, r] = 0 Then results.Push(_nodeGrid[c, r]) 'Cell is empty, so it's valid to use in a potential path.EndPushIfValid(currentNode.Pos.X-1, currentNode.Pos.Y) 'West.PushIfValid(currentNode.Pos.X+1, currentNode.Pos.Y) 'East.PushIfValid(currentNode.Pos.X, currentNode.Pos.Y-1) 'North.PushIfValid(currentNode.Pos.X, currentNode.Pos.Y+1) 'South.If includeDiagnalsPushIfValid(currentNode.Pos.X-1, currentNode.Pos.Y-1) 'North West.PushIfValid(currentNode.Pos.X+1, currentNode.Pos.Y-1) 'North East.PushIfValid(currentNode.Pos.X+1, currentNode.Pos.Y+1) 'South East.PushIfValid(currentNode.Pos.X-1, currentNode.Pos.Y+1) 'South West.EndifEndPrivateField _grid:Int[,] 'Grid to calculate a path through.Field _cols:Int, _rows:Int 'Grid dimensions.'Data structures used in calculations.Field _nodeGrid:PathNode[,]Field _openStack:Stack<PathNode>Field _closedStack:Stack<PathNode>Field _pathStack:Stack<PathNode>Field _adjacentNodeStack:Stack<PathNode>EndTheory:
https://en.wikipedia.org/wiki/A*_search_algorithm
June 10, 2017 at 4:11 pm #8580Nice!
I looked through the code and noticed it does not do terrain cost. It does handle diagonal and non diagonal.
I discovered that the simple flood or seed fill is also a good and simpler way to make pathfinding. I think I remember a gdc video where this was also tipped.
August 28, 2017 at 12:28 pm #10069I’m no good at math or algorithms, but this is my modified version with tile ‘cost’.
Use the mouse wheel to change the cost of the tile under the mouse.Monkey123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379'AStar pathfinding example.#Import "<std>"#Import "<mojo>"Using std..Using mojo..Function Main()New AppInstanceNew MyWindowApp.Run()End'------------Class MyWindow Extends WindowField _grid:Float[,] '0 = No Wall, 1 = Wall.Field _start:Vec2i 'Path starting coordinate.Field _goal:Vec2i 'Path goal coordinate.Field _pathFinder:PathFinderField _path:Stack<Vec2i> 'All path's coordinates.Field _diagnals:Bool 'Include diagnals in potential path (cut corners).Field _cellWidth:Int 'Cell render width.Field _cellHeight:Int 'Cell render height.Method New(title:String = "AStar Example", width:Int = 800, height:Int = 600, flags:WindowFlags = Null)Super.New( title,width,height,flags )Local cols := 40Local rows := 20_grid = New Float[cols, rows]_cellWidth = Width / cols_cellHeight = Height / rows_diagnals = False_start = New Vec2i(38, 10)_goal = New Vec2i(1, 10)_path = New Stack<Vec2i>()_pathFinder = New PathFinder(_grid)'Multiply the cost of each tile_pathFinder.CostMulti=50 'Default is 50, just cause it seemed 'correct'?Generate()EndMethod Generate:Void()'Randomly place some walls.For Local c :=0 Until _grid.GetSize(0) 'Columns.For Local r := 0 Until _grid.GetSize(1) 'Rows._grid[c, r] = Rnd( -0.5, 1 ) 'Never a solid wallIf _grid[c, r]<0 Then _grid[c, r]=0NextNextCalculatePath()EndMethod CalculatePath:Void()_pathFinder.FindPath(_start, _goal, _path, _diagnals)EndMethod OnRender( canvas:Canvas ) OverrideApp.RequestRender()Local w:FloatFor Local c := 0 Until _grid.GetSize(0)For Local r := 0 Until _grid.GetSize(1)w=_grid[c, r]If w>=1canvas.Color = Color.Red 'Wall.Elsecanvas.Color = New Color( w*0.5, w*0.5, w*0.5 ) 'WalkableEndifcanvas.DrawRect(c * _cellWidth, r * _cellHeight, _cellWidth, _cellHeight)NextNext'Start.canvas.Color = Color.Greencanvas.DrawEllipse(_start.X * _cellWidth + (_cellWidth / 2), (_start.Y * _cellHeight) + (_cellHeight / 2), _cellWidth / 4, _cellHeight / 4)'Path coordinates (if any).canvas.Color = Color.YellowFor Local coord := Eachin _pathcanvas.DrawRect(coord.X * _cellWidth, coord.Y * _cellHeight, _cellWidth, _cellHeight)Next'Goal.canvas.Color = Color.Redcanvas.DrawEllipse(_goal.X * _cellWidth + (_cellWidth / 2), (_goal.Y * _cellHeight) + (_cellHeight / 2), _cellWidth / 4, _cellHeight / 4)canvas.Color = Color.Whitecanvas.DrawText("[Enter]: Generate new grid, [Left Click]: Place start, [Right Click] Place goal, [Tab] Toggle diagnals.", 0, 0)canvas.Flush()EndMethod OnKeyEvent:Void(event:KeyEvent) OverrideIf event.Type = EventType.KeyDownIf event.Key = Key.EscapeApp.Terminate()Elseif event.Key = Key.Tab'Toggle "corner cutting" on/off._diagnals = Not _diagnalsCalculatePath()Elseif event.Key = Key.Enter'Generate a new grid.Generate()EndifEndifEndMethod OnMouseEvent:Void(event:MouseEvent) OverrideIf event.Type = EventType.MouseClick'Set starting coordinate.Local c:= Floor(Mouse.Location.X / _cellWidth)Local r:= Floor(Mouse.Location.Y / _cellHeight)If _grid[c, r] < 1_start = New Vec2i(c, r)CalculatePath()EndifElseif event.Type = EventType.MouseRightClick'Set goal coordinate.Local c:= Floor(Mouse.Location.X / _cellWidth)Local r:= Floor(Mouse.Location.Y / _cellHeight)If _grid[c, r] < 1_goal = New Vec2i(c, r)CalculatePath()EndifElseif event.Type = EventType.MouseWheel'Change tile costLocal c:= Floor(Mouse.Location.X / _cellWidth)Local r:= Floor(Mouse.Location.Y / _cellHeight)_grid[c, r] += event.Wheel.Y*0.1If _grid[c, r]<0 Then _grid[c, r]=0If _grid[c, r]>1 Then _grid[c, r]=1CalculatePath()EndifEndEnd'=========================================Class PathNodePublicField Pos:Vec2i 'PositionField H:Int 'Heuristic: movement cost to goal.Field G:Int 'Movement cost of entire path.Field F:Int 'Estimated cost for full path (G + H).Field Parent:PathNode 'Node to reach this node.Method New(pos:Vec2i)Pos = posReset()EndMethod Reset:Void()H = 0G = 0F = 0Parent = NullEndEnd'-----Class PathFinderPublicMethod New(grid:Float[,])'grid: An existing 2d array of integers with dimensions Columns x RowsGrid = grid_openStack = New Stack<PathNode>()_closedStack = New Stack<PathNode>()_pathStack = New Stack<PathNode>()_adjacentNodeStack = New Stack<PathNode>()EndProperty Grid:Float[,]()Return _gridSetter (value:Float[,])If Not value Then Return_grid = value_cols = _grid.GetSize(0)_rows = _grid.GetSize(1)'Only create a new nodeGrid if the new dimensions differ from the old dimensions.If Not _nodeGrid Or _nodeGrid.GetSize(0) <> _cols Or _nodeGrid.GetSize(1) <> _rows_nodeGrid = New PathNode[_cols, _rows]For Local c := 0 Until _colsFor Local r := 0 Until _rows_nodeGrid[c, r] = New PathNode(New Vec2i(c, r))NextNextEndifEndProperty CostMulti:Float()Return _costMultiSetter (value:Float)_costMulti=valueEndMethod FindPath:Void(start:Vec2i, goal:Vec2i, results:Stack<Vec2i>, includeDiagnals:Bool = False)'start: Starting coordinate: column, row.'goal: Goal coordinate: column, row.'results: An existing Stack on which to place the path coordinates.If Not results Then Returnresults.Clear()If start = goal Then Return 'No path necessary'Check for invalid start or goal coordinates.If start.X < 0 Or start.Y < 0 Or goal.X < 0 Or goal.Y < 0 Then ReturnIf start.X >= _cols Or start.Y >= _rows Or goal.X >= _cols Or goal.Y >= _rows Then ReturnCalculateHeuristicCosts(goal)Local startNode := _nodeGrid[start.X, start.Y]Local goalNode := _nodeGrid[goal.X, goal.Y]_openStack.Clear()_closedStack.Clear()_pathStack.Clear()Local currentNode := startNode_openStack.Push(currentNode)_adjacentNodeStack.Clear()While Not _openStack.Empty'Initially set the minimum full path cost high.Local minF := 4000000For Local node:= Eachin _openStackIf node.F < minFminF = node.FcurrentNode = nodeEndifNext_openStack.Remove(currentNode)_closedStack.Push(currentNode)GetAdjacentNodes(currentNode, _adjacentNodeStack, includeDiagnals)For Local node:= Eachin _adjacentNodeStackIf node = goalNodenode.Parent = currentNodeWhile node.Parent_pathStack.Push(node)node = node.ParentWend_openStack.Clear()ExitElseLocal idx:=_closedStack.FindIndex(node)If idx < 0 'Node is not in closed stack.Local idx2:=_openStack.FindIndex(node)If idx2 < 0 'node is not in open stack._openStack.Push(node)node.Parent = currentNodenode.G = currentNode.G + 10node.F = node.G + node.HElseIf (currentNode.G + 10 < node.G)'Faster.node.Parent = currentNodeEndifEndifEndifEndifNextWendFor Local node:=Eachin _pathStackresults.Push(node.Pos)NextEnd'The CalculateHeuristicCosts method is implemented based on the game type.'This example uses the Manhattan technique of calculating'the distance between two points (suitable for grid-based, rogue-like games).Method CalculateHeuristicCosts:Void(goal:Vec2i)Local Distance := Lambda:Int(vecA:Vec2i, vecB:Vec2i)'Manhattan technique of calculating the distance between two points.Local result := vecA - vecBReturn Abs(result.X) + Abs(result.Y)EndFor Local c := 0 Until _colsFor Local r := 0 Until _rows_nodeGrid[c, r].Reset()_nodeGrid[c, r].H = Distance(_nodeGrid[c, r].Pos, goal)_nodeGrid[c, r].H += Clamp( _grid[c, r], 0.0, 1.0 ) * _costMultiNextNextEnd'The GetAdjacentNodes method is implemented based on the game type.'In this example the nodes North, East, South and West of the currentNode'are considered adjacent. And also NE, SE, SW, NE, if includeDiagnals is True.Method GetAdjacentNodes:Void(currentNode:PathNode, results:Stack<PathNode>, includeDiagnals:Bool)If Not results Then Returnresults.Clear()Local PushIfValid := Lambda:void(c:Int, r:Int)If c < 0 Or c >= _cols Then ReturnIf r < 0 Or r >= _rows Then ReturnIf _grid[c, r] < 1 Then results.Push(_nodeGrid[c, r]) 'Cell is empty, so it's valid to use in a potential path.EndPushIfValid(currentNode.Pos.X-1, currentNode.Pos.Y) 'West.PushIfValid(currentNode.Pos.X+1, currentNode.Pos.Y) 'East.PushIfValid(currentNode.Pos.X, currentNode.Pos.Y-1) 'North.PushIfValid(currentNode.Pos.X, currentNode.Pos.Y+1) 'South.If includeDiagnalsPushIfValid(currentNode.Pos.X-1, currentNode.Pos.Y-1) 'North West.PushIfValid(currentNode.Pos.X+1, currentNode.Pos.Y-1) 'North East.PushIfValid(currentNode.Pos.X+1, currentNode.Pos.Y+1) 'South East.PushIfValid(currentNode.Pos.X-1, currentNode.Pos.Y+1) 'South West.EndifEndPrivateField _grid:Float[,] 'Grid to calculate a path through.Field _cols:Int, _rows:Int 'Grid dimensions.Field _costMulti:Float = 50 'Cost multiplier.'Data structures used in calculations.Field _nodeGrid:PathNode[,]Field _openStack:Stack<PathNode>Field _closedStack:Stack<PathNode>Field _pathStack:Stack<PathNode>Field _adjacentNodeStack:Stack<PathNode>End -
AuthorPosts
You must be logged in to reply to this topic.