Scurry
An A-star 2D polygon map search implementation and set of polygon, geometry and vector utility functions for Elixir.
Quickstart
See the quickstart or hex.pm docs.
Wx Demo
Start a wxwidgets demo of a-star by running Scurry.Wx.start();
$ mix deps.get
$ mix compile
$ iex -S mix
Erlang/OTP 25 [erts-13.1.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns] [dtrace]
Interactive Elixir (1.14.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Scurry.Wx.start()- The start point is a green crosshair.
- The cursor position is a red crosshair if inside the main polution, gray if outside.
-
Moving the mouse will show a line from start to the cursor.
- It’ll be green if the there’s a line of sight.
- It’ll be gray if not, and there’ll be a small red crosshair at the first blockage, and small gray crosshair all subsequent blocks.
- Holding down left mouse button will show full search graph in bright orange and a thick green path for the found path.
-
Releasing the left mouse button resets the start to there.
- You can place the start outside the main polygon.
How to use
Adding scurry to your list of dependencies in mix.exs:
def deps do
[
{:scurry, "~> 2.0"},
]
endThen, update your dependencies:
$ mix deps.getInternals
There’s better API documentation than this see the hexdocs, especially see the quickstart.
mix docs
open doc/index.htmlVectors
In lib/vector.ex you’ll find the basic vector operations (dot,
cross, length, add/sub) needed. A vector is a tuple of numbers, {x, y}.
Lines
A line is a tuple of vectors, {{x1, y1}, {x2, y2}}.
Polygon
In lib/plygon.ex you’ll find the various polygon related functions,
such as line of sight, nearest point, convex etc.
A polygon is a list of vertices (nodes) that are {x, y} tuples that
represent screen coordinates.
In elixir, it looks like
polygon = [{x, y}, {x, y}, ...]
The screen coordinate 0, 0 is upper left the x-axis goes
left-to-right and y-axis top-to-bottom. Polygons must be defined
clockwise and not-closed. This is important for convex/concave vertex
checks.
Polygon map
The wxwidgets demo map is loaded from a json file, and looks like
{
"polygons": [
"main": [
[x, y], [x, y], [x,y], ...
],
"hole1": [
[x, y], [x, y], [x,y], ...
],
"hole2": [
[x, y], [x, y], [x,y], ...
]
]
}
The main polygon is the primary walking area - as complex as it
needs to be.
Subsequent polygons (not named main) are holes within it.
Polygons don’t need to be closed (last [x, y] equals the first),
this will be handled internally.
The polygon name isn’t used internally, only to decide which polygon is the primary boundary and which are holes.
Graph
The A-star graph doesn’t need to be a polygon map. It just needs to be
map from node to a list of {node, cost} edges. Node just has to be
a term that elixir can use as a map key.
For the 2D map search, it is a map from vertice to a list of
{vertice, cost}. This is computed from the polygon map using a set
of vertices. This set is composed of;
- the main polygon’s concave (pointing into the world)
- the holes’ convex (point out of the hole, into the world)
and PolygonMap.get_vertices/2 creates this.
vertices = [
{x1, y1}=vertice1,
{x2, y2}=vertice2,
{x3, y3}=vertice3,
...
]
This is transformed to a graph (PolygonMap.create_graph/4) given the
polygon, holes, vertices (from above) and cost function.
Assuming a cost_fun that has type vertice, vertice :: cost, the graph looks like;
graph = %{
vertice1 => [
vertice2, cost_fun(vertice1, vertice2),
vertice3, cost_fun(vertice1, vertice3),
vertice4, cost_fun(vertice1, vertice4),
],
# When expressed as "vertice = {x, y}"
{x1, x2} => [
{{x2, y2}, cost_fun({x1, y2}, {x2, y2})},
{{x3, y3}, cost_fun({x1, y2}, {x3, y3})},
...
],
...
}
Note: create_graph uses PolygonMap.is_line_of_sight?/3 to
determine if two vertices should have an edge. This is currently not
configurable or passed as a function.
The default cost_fun and heur_fun is the euclidean distance been
the two points. The difference between the two is, cost_fun is used
while computing the graph and heur_fun while computing the
path. Typically they will be the same but that is dependent on use
case.
cost_fun = fn a, b -> Vector.distance(a, b) endA-star
In the context of A-star, we use the terminology node instead of
vertice since we’re describing graphs - not strictly polygons. In
the example, each node is a polygon vertice (ie. {x, y}).
A node is opaque to the algorithm, it just uses them as
keys for it’s internal state maps and arguments to heur_fun.
indexes.
The A-star algorithm main call is Astar.search/4 and takes.
graphto search. The graph should be constructed as
graph = %{
node1 => [
node2, cost_fun(node1, node2),
node3, cost_fun(node1, node3),
node4, cost_fun(node1, node4),
],
# When expressed as "node = {x, y}"
{x1, x2} => [
{{x2, y2}, cost_fun({x1, y2}, {x2, y2})},
{{x3, y3}, cost_fun({x1, y2}, {x3, y3})},
...
],
...
}startandstop, the nodes to find a path between.heur_funfunctionnode, node :: costcomputes the heuristic cost. The default is the euclidian distance.
fn a, b -> Vector.distance(a, b) endThe state it maintains and returns
shortest_path_tree, a map of edges,node_a => node_b, wherenode_bis the “previous” node fromnode_athat is the shortest path.queuepriority queue / list[node, node, ...]sorted on the cost (seef_costbelow) of the path fromstartto node tostop.frontiermap ofnode => node (prev)that have been reached and edges yet to try and have been added to thequeue. It’s a map, so when we visit a node, we can add how we reached it toshortest_path_tree.g_cost, mapnode => costwith the minimal current cost from thestarttonode. Each iteration compare the current node’sg_costagainst the value in the map. If it’s less, we’ve found a shorter path to this node and update theg_costmap.f_cost, mapnode => costwith the “total cost” of path fromstart, via node, tostop. This means the computed minimal cost fromstartto node (g_cost) plus the heuristic cost viaheur_fun. This is used to reorderqueue.
shortest_path_tree is the most relevant field, and can be converted
to a path using Astar.path/1.
Within astar.ex, there’s two steps; search & getting the
path. search returns the full state, and path could be
extended to return the cost along the path if needed. It can fetch
this from g_cost.