AStarGrid2D

Una rejilla donde los muros bloquean celdas y las amarillas cuestan seis veces más de cruzar. El resultado es la ruta más barata, no la más corta.

El editor de arriba está ejecutando este proyecto. Cambia una línea y se recarga.

Clases de Godot usadas

AStarGrid2DPolygon2DLine2D

El código

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)

Más ejemplos de Navegación