AStarGrid2D
Uma grade onde paredes bloqueiam células e as amarelas custam seis vezes mais para atravessar. O resultado é a rota mais barata, não a mais curta.
O editor acima está rodando este projeto. Mude uma linha e ele recarrega.
Classes do Godot usadas
AStarGrid2DPolygon2DLine2D
O 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)