Created
March 9, 2017 09:15
-
-
Save athalean/9a2b4e00a72ef102940668c50fdc87a1 to your computer and use it in GitHub Desktop.
Godot A* pathfinding
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
func grid_to_world(position): | |
# (0,8) is the offset to the middle of the tile | |
return map_to_world(position) + Vector2(0, 8) | |
func world_to_grid(position): | |
return world_to_map(position) | |
func get_tile_cost(pos): | |
# placeholder | |
return 1.0 | |
func get_tile_weighing_category(pos): | |
# placeholder | |
return 'normal' | |
func _heap_pop(heap, f_scores): | |
# naive O(n) implementation. Only invariant of this function is | |
# that it returns the smallest element in the data structure. | |
# Can be optimized to an actual heap with O(log n) if it becomes necessary | |
var minimum_i = 0 | |
var current_minimum = 1000000000 | |
if f_scores.has(heap[0]): | |
current_minimum = f_scores[heap[0]] | |
for i in range(1, heap.size()): | |
var this_score = 1000000000 | |
if f_scores.has(heap[i]): | |
this_score = f_scores[heap[i]] | |
if this_score < current_minimum: | |
minimum_i = i | |
current_minimum = this_score | |
var ret = heap[minimum_i] | |
heap.remove(minimum_i) | |
return ret | |
func _heap_push(heap, val, f_scores): | |
heap.append(val) | |
func get_neighbors(field): | |
var neighbors = [] | |
for move in [Vector2(-1, 0), Vector2(1, 0), Vector2(0, 1), Vector2(0, -1)]: | |
var potential_neighbor = Vector2(int(field.x + move.x), int(field.y + move.y)) | |
var world_point = grid_to_world(potential_neighbor) | |
if (world_point - get_node("WalkMap").get_closest_point(world_point)).length_squared() < 0.1: | |
neighbors.append(potential_neighbor) | |
return Vector2Array(neighbors) | |
func _heuristic_score(prev, from, to): | |
# a bit of a convoluted heuristic score that punishes taking many turns | |
return (to - from).length_squared() * (from - prev).angle_to(to - from) | |
func calculate_path(from, to, weights={}): | |
# Perform an A* to calculate a good way to the target | |
# Use WalkMap to check whether a tile is accessible | |
# Weights can be used to prefer certain categories of field costs. | |
# (I'll document this better later, triple promise!) | |
# | |
# closely following https://en.wikipedia.org/wiki/A*_search_algorithm#Pseudocode | |
# naively arrays, could be replaced with heaps and search trees in case of performance issues | |
var closed = [] | |
var open = [from] | |
var predecessors = {} | |
var g_scores = {from: 0} | |
var turns = {from: 0} | |
var f_scores = {to: (to - from).length_squared()} | |
# shackle maximum checking size | |
while open.size() > 0 and open.size() < 9999: | |
var current = _heap_pop(open, f_scores) | |
# if reached the goal, retrace steps | |
if (current - to).length_squared() < 0.1: | |
var path = [] | |
var f = to | |
while (f - from).length_squared() > 0.1: | |
path.insert(0, f) | |
f = predecessors[f] | |
return Vector2Array([from] + path) | |
open.erase(current) | |
closed.append(current) | |
for neighbor in get_neighbors(current): | |
# skip checked nodes | |
if closed.find(neighbor) > -1: | |
continue | |
var cost_modifier = 1.0 | |
var weight_category = get_tile_weighing_category(neighbor) | |
if weights.has(weight_category): | |
cost_modifier = weights[weight_category] | |
var this_turns = turns[current] | |
var turn_cost = 0 | |
if predecessors.has(current): | |
turn_cost = abs(sin((current - predecessors[current]).angle_to(neighbor - current))) | |
if turn_cost > 0.5: | |
this_turns += 1 | |
var move_cost = get_tile_cost(neighbor) | |
var tentative_g_score = g_scores[current] + move_cost * cost_modifier | |
tentative_g_score += this_turns * 1.5 | |
if open.find(neighbor) == -1: | |
_heap_push(open, neighbor, f_scores) | |
elif tentative_g_score >= g_scores[neighbor]: | |
continue # found a worse path | |
# found a better path | |
predecessors[neighbor] = current | |
turns[neighbor] = this_turns | |
g_scores[neighbor] = tentative_g_score | |
f_scores[neighbor] = tentative_g_score + (to - neighbor).length_squared() + turn_cost | |
return null |
Again Thanks for the explanation. Over the next few days I will do as you have advised and see what results I get.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The mathematics stay pretty much the same. A* is a graph algorithm for a weighted graph, and a weighted graph is nothing more than a set of points of which some can be connected and where every connection has a distance value, whether that's coordinates on a screen or cities on a map or something more abstract.
The AStar implementation in Godot does it for 3D coordinates (you can use 2D coordinates there if you always set the last number to 0) and you can treat it like a box that you tell what the world looks like and ask it questions which it then answers really fast. You can see it even models that concept of "set of points" and "connections" as you can see by the AStar class having
add_point
andconnect_points
in the documentation (https://docs.godotengine.org/en/3.1/classes/class_astar.html). That means it's flexible since you can put in basically anything but you also gotta tell it which points you're interested in. The article you linked basically describes what I'd do, with the exception that I'd useworld_to_pos
andpos_to_world
to convert between on-screen coordinates to map coordinates and back.You probably want this to sit in its own object that deals with game logic in general, but no matter how you structure it, you're most probably gonna do two things:
TileMap
→ https://docs.godotengine.org/en/3.1/classes/class_tilemap.html)get_point_path
and use the result in your game logic somehow (for example by saving it on an actor so they move there)I hope this helps - I think the specifics will depend on your game, but this is as far as Godot goes for off-the-shelf solutions. My best advise for next things to do is look into more tutorials on TileMap or try to work out from the snippet how you can build a graph of possible movements on a TileMap. Or maybe your game allows you to get away by just using a
Navigation2D
instead like in the first tutorial.