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)

More Navigation examples