AStarGrid2D
A grid where walls block cells and the yellow ones cost six times more to cross. The result is the cheapest route, not the shortest one.
The editor above is running this project. Change a line and it reloads.
Godot classes used
AStarGrid2DPolygon2DLine2D
The code
scripts/main.gd
extends Node
const CELL: float = 40.0
const COLS: int = 30
const ROWS: int = 16
const MUD_WEIGHT: float = 6.0
const WALKER_SPEED: float = 320.0
var origin: Vector2 = Vector2.ZERO
var astar: AStarGrid2D = AStarGrid2D.new()
var start_cell: Vector2i = Vector2i(1, 8)
var goal_cell: Vector2i = Vector2i(28, 8)
var cell_visuals: Dictionary = {}
var path_layer: Node2D
var path_line: Line2D
var goal_ring: Polygon2D
var walker: Polygon2D
var walk_path: PackedVector2Array = PackedVector2Array()
var travelled: float = 0.0
var info: Label
func _ready() -> void:
_add_background()
var view: Vector2 = get_viewport().get_visible_rect().size
origin = (view - Vector2(COLS, ROWS) * CELL) * 0.5
astar.region = Rect2i(0, 0, COLS, ROWS)
astar.cell_size = Vector2(CELL, CELL)
astar.offset = origin + Vector2(CELL, CELL) * 0.5
astar.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER
astar.default_compute_heuristic = AStarGrid2D.HEURISTIC_MANHATTAN
astar.default_estimate_heuristic = AStarGrid2D.HEURISTIC_MANHATTAN
# update() rebuilds every point from scratch, so solid flags and weights only stick after it
astar.update()
_paint_grid()
path_layer = Node2D.new()
add_child(path_layer)
path_line = Line2D.new()
path_line.width = 4.0
path_line.default_color = Color('#8eef97')
add_child(path_line)
add_child(_make_ring(start_cell, Color('#8da5f3')))
goal_ring = _make_ring(goal_cell, Color('#f3d48d'))
add_child(goal_ring)
walker = _make_circle(11.0, Color('#fc7f7f'))
add_child(walker)
info = Label.new()
info.position = Vector2(origin.x, origin.y - 42)
info.add_theme_font_size_override('font_size', 20)
info.add_theme_color_override('font_color', Color('#f3d48d'))
add_child(info)
_repath()
print('yellow cells cost %d times a plain one, click to move the goal and right click to carve a wall' % int(MUD_WEIGHT))
func _process(delta: float) -> void:
if walk_path.size() < 2:
return
travelled += WALKER_SPEED * delta
var remaining: float = travelled
var placed: bool = false
for i in range(walk_path.size() - 1):
var span: float = walk_path[i].distance_to(walk_path[i + 1])
if remaining <= span:
walker.position = walk_path[i].lerp(walk_path[i + 1], remaining / span)
placed = true
break
remaining -= span
if not placed:
travelled = 0.0
walker.position = walk_path[0]
func _unhandled_input(event: InputEvent) -> void:
if not (event is InputEventMouseButton and event.pressed):
return
var cell: Vector2i = _cell_at(event.position)
if not astar.is_in_boundsv(cell) or cell == start_cell:
return
if event.button_index == MOUSE_BUTTON_LEFT and not astar.is_point_solid(cell):
goal_cell = cell
_repath()
elif event.button_index == MOUSE_BUTTON_RIGHT and cell != goal_cell:
astar.set_point_solid(cell, not astar.is_point_solid(cell))
_repaint_cell(cell)
_repath()
func _repath() -> void:
walk_path = astar.get_point_path(start_cell, goal_cell)
path_line.points = walk_path
goal_ring.position = origin + (Vector2(goal_cell) + Vector2(0.5, 0.5)) * CELL
travelled = 0.0
for face in path_layer.get_children():
face.queue_free()
var cost: float = 0.0
for cell in astar.get_id_path(start_cell, goal_cell):
cost += astar.get_point_weight_scale(cell)
path_layer.add_child(_make_cell(cell, Color(0.56, 0.94, 0.6, 0.3)))
info.text = 'path: %d cells, cost %d' % [walk_path.size(), int(cost)]
func _paint_grid() -> void:
for y in ROWS:
for x in COLS:
var cell: Vector2i = Vector2i(x, y)
if _is_wall(cell):
astar.set_point_solid(cell, true)
elif _is_mud(cell):
astar.set_point_weight_scale(cell, MUD_WEIGHT)
_repaint_cell(cell)
func _repaint_cell(cell: Vector2i) -> void:
if cell_visuals.has(cell):
cell_visuals[cell].queue_free()
var color: Color = Color(0.55, 0.65, 0.95, 0.05)
if astar.is_point_solid(cell):
color = Color(0.55, 0.65, 0.95, 0.45)
elif astar.get_point_weight_scale(cell) > 1.0:
color = Color(0.95, 0.83, 0.55, 0.3)
var face: Polygon2D = _make_cell(cell, color)
add_child(face)
move_child(face, 1)
cell_visuals[cell] = face
func _is_wall(cell: Vector2i) -> bool:
if cell.x == 6 and cell.y < ROWS - 3:
return true
if cell.x == 23 and cell.y > 2:
return true
if cell.y == 2 and cell.x > 9 and cell.x < 20:
return true
if cell.y == ROWS - 3 and cell.x > 9 and cell.x < 20:
return true
return false
func _is_mud(cell: Vector2i) -> bool:
return cell.x > 9 and cell.x < 20 and cell.y > 2 and cell.y < ROWS - 3
func _cell_at(where: Vector2) -> Vector2i:
return Vector2i((where - origin) / CELL)
func _make_cell(cell: Vector2i, color: Color) -> Polygon2D:
var face: Polygon2D = _make_rect(Vector2(CELL - 3.0, CELL - 3.0), color)
face.position = origin + (Vector2(cell) + Vector2(0.5, 0.5)) * CELL
return face
func _make_ring(cell: Vector2i, color: Color) -> Polygon2D:
var ring: Polygon2D = _make_circle(15.0, color)
ring.position = origin + (Vector2(cell) + Vector2(0.5, 0.5)) * CELL
return ring
func _make_rect(size: Vector2, color: Color) -> Polygon2D:
var rect: Polygon2D = Polygon2D.new()
rect.polygon = PackedVector2Array([-size * 0.5, Vector2(size.x, -size.y) * 0.5, size * 0.5, Vector2(-size.x, size.y) * 0.5])
rect.color = color
return rect
func _make_circle(radius: float, color: Color) -> Polygon2D:
var points: PackedVector2Array = PackedVector2Array()
for i in 20:
points.append(Vector2(cos(TAU * i / 20.0), sin(TAU * i / 20.0)) * radius)
var circle: Polygon2D = Polygon2D.new()
circle.polygon = points
circle.color = color
return circle
func _add_background() -> void:
var background: ColorRect = ColorRect.new()
background.color = Color('#12141a')
add_child(background)
# ColorRect defaults to MOUSE_FILTER_STOP and would eat the click before _unhandled_input
background.mouse_filter = Control.MOUSE_FILTER_IGNORE
background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)