Voltar para o Blog
Quest Log

A* no Godot com AStarGrid2D: Pathfinding para Jogos em Grid

Tabuleiro em grade visto de cima com um caminho iluminado desviando de obstáculos

Aprenda pathfinding com A* no Godot usando AStarGrid2D: setup, integração com TileMap, custo de terreno e movimento em grade com código pronto.

Ferramenta:Godot

A* no Godot com AStarGrid2D: Pathfinding para Jogos em Grid

Se o seu jogo se move em células (tático por turnos, roguelike, tower defense, qualquer coisa com tabuleiro), implementar A* no Godot não exige escrever o algoritmo na mão. O Godot 4 traz a classe AStarGrid2D pronta: você diz o tamanho da grade, marca o que é parede, e ela devolve o caminho célula por célula. Sem heap, sem lista aberta, sem reinventar nada.

Esse tutorial monta o fluxo completo que eu uso: criar a grade, conectar com um TileMapLayer, dar custo diferente pra cada terreno (lama custa mais que estrada), escolher como tratar diagonais e mover um personagem pelo caminho. Todo código é GDScript do Godot 4.x e roda como está. Vale lembrar que o A* resolve só o movimento: decidir para onde e quando a unidade vai é a camada de decisão que o guia de inteligência artificial para jogos na Godot cobre.

Quando usar AStarGrid2D (e quando não)

O Godot tem três jeitos de fazer pathfinding, e escolher o errado gera retrabalho:

AStarGrid2D: pra movimento em grade. O mapa é uma matriz de células, o personagem anda de célula em célula. É o caso de jogo tático, puzzle, roguelike, tower defense. É a opção mais simples e mais rápida pra esse formato, porque a vizinhança entre células já é implícita: você não cadastra conexões, só marca o que é sólido.

AStar2D/AStar3D: pra grafos arbitrários. Você cadastra cada ponto e cada conexão na mão. Útil quando o mapa não é uma grade (waypoints espalhados, rotas de patrulha, mapa de mundo com estradas). Se o seu mapa é grade, usar AStar2D é trabalho extra pra chegar no mesmo lugar.

NavigationAgent + NavigationRegion: pra movimento livre e contínuo, sem células. Inimigo que persegue o player em qualquer direção, NPC desviando de obstáculo em tempo real. É outro problema, com outro custo.

Regra prática: o personagem ocupa uma célula e se move pra célula vizinha? AStarGrid2D. Move livre pelo espaço? Navegação. O resto é exceção.

Setup do A* no Godot: a grade mínima

O ciclo de vida da AStarGrid2D tem três passos: configurar, chamar update(), usar. O update() é obrigatório depois de mexer em region, cell_size ou offset; sem ele a grade não existe internamente e qualquer consulta falha.

var astar := AStarGrid2D.new()

func _ready() -> void:
    # A região da grade em células: começa em (0,0), 16 de largura, 9 de altura.
    astar.region = Rect2i(0, 0, 16, 9)
    # Tamanho de cada célula em pixels (usado pra converter célula -> posição).
    astar.cell_size = Vector2(64, 64)
    astar.update()

    # Marca uma parede.
    astar.set_point_solid(Vector2i(5, 3))

    # Caminho em coordenadas de célula:
    print(astar.get_id_path(Vector2i(0, 0), Vector2i(10, 5)))
    # Caminho em posições no mundo (multiplica pela cell_size):
    print(astar.get_point_path(Vector2i(0, 0), Vector2i(10, 5)))

Os dois métodos de consulta resolvem o mesmo caminho, mudando só a unidade da resposta. get_id_path() devolve um array de Vector2i com coordenadas de célula, que é o que você quer em jogo por turnos. get_point_path() devolve um PackedVector2Array com posições em pixels, prático quando o personagem vai deslizar pelo caminho.

Duas armadilhas que pegam todo mundo na primeira vez:

  • Consultar célula fora da região quebra com erro. Antes de pedir caminho pra onde o jogador clicou, valide com astar.is_in_boundsv(celula).
  • Origem ou destino sólido devolve caminho vazio. Clicou na parede? get_id_path() retorna array vazio. Trate esse caso em vez de assumir que sempre vem caminho.

Conectando a grade com um TileMapLayer

Mapa de verdade não é hardcoded: ele vem do TileMap. A ideia é gerar a grade a partir do que já está pintado no editor, usando uma camada de dados customizados do TileSet pra dizer o que é andável.

No TileSet, crie uma Custom Data Layer chamada andavel do tipo bool. Marque true nos tiles de chão e deixe false em parede, água, pedra. Com isso o level designer controla o pathfinding pintando tiles, sem tocar em código.

O script lê tudo no _ready():

@export var tile_map: TileMapLayer

var astar := AStarGrid2D.new()

func _ready() -> void:
    # A região e o tamanho de célula vêm do próprio TileMap.
    astar.region = tile_map.get_used_rect()
    astar.cell_size = Vector2(tile_map.tile_set.tile_size)
    astar.update()

    for celula in tile_map.get_used_cells():
        var dados := tile_map.get_cell_tile_data(celula)
        if dados == null or not dados.get_custom_data("andavel"):
            astar.set_point_solid(celula)

Repare que get_used_rect() pode não começar em (0,0). Se o mapa foi pintado a partir da célula (-8, -4), a região acompanha, e as consultas usam as mesmas coordenadas de célula do TileMap. É por isso que vale sempre converter posição de mundo pra célula com local_to_map() em vez de dividir por tamanho de tile na mão.

Pra fechar quadrados grandes de parede de uma vez (a borda do mapa, por exemplo), existe o atalho fill_solid_region():

astar.fill_solid_region(Rect2i(0, 0, 16, 1))  # linha inteira do topo

E quando o mapa muda em runtime (porta que abre, ponte que cai), basta atualizar o ponto:

astar.set_point_solid(celula_da_porta, false)  # abriu, virou andável

Não precisa chamar update() de novo pra isso. O update() só é necessário quando região, tamanho de célula ou offset mudam.

Curso de Godot
Quer fazer seu primeiro jogo na Godot?

No curso de Godot da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Godot
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

Custo de terreno: lama, estrada e weight scale

Aqui está o recurso que separa pathfinding básico de pathfinding de jogo tático. Nem toda célula andável custa igual: atravessar lama é mais lento que andar na estrada, e o caminho "mais curto" em células nem sempre é o melhor. O A* resolve isso com peso por célula.

# Custo padrão de cada célula é 1.0.
# Lama: custa o triplo. O A* só passa por ela se compensar muito.
astar.set_point_weight_scale(Vector2i(4, 3), 3.0)

Com peso 3.0, entrar naquela célula equivale a andar três células normais. O algoritmo continua achando o caminho de menor custo total, então ele contorna a lama quando o desvio é barato e atravessa quando o desvio sai mais caro. Você não programa essa decisão: ela emerge do custo.

O jeito limpo de alimentar isso é a mesma técnica do andavel: uma Custom Data Layer custo do tipo float no TileSet, com 1.0 no chão comum, 3.0 na lama, 0.5 na estrada. O loop de setup fica assim:

for celula in tile_map.get_used_cells():
    var dados := tile_map.get_cell_tile_data(celula)
    if dados == null or not dados.get_custom_data("andavel"):
        astar.set_point_solid(celula)
    else:
        astar.set_point_weight_scale(celula, dados.get_custom_data("custo"))

Um detalhe de design que aprendi testando: diferenças pequenas de peso (1.0 contra 1.2) quase não mudam os caminhos e o jogador não percebe. Pra terreno "sentir" diferente, use contrastes francos, tipo 1.0 contra 3.0. E estrada com custo abaixo de 1.0 é um truque ótimo pra fazer unidades preferirem rotas naturais sem nenhum script de comportamento.

Diagonais e heurísticas

Por padrão a AStarGrid2D permite movimento diagonal, e isso nem sempre é o que o jogo pede. A propriedade diagonal_mode tem quatro opções:

# Jogo tático clássico, só 4 direções:
astar.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER

# Diagonal liberada sempre (padrão):
astar.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_ALWAYS

# Diagonal só se não cortar quina de parede:
astar.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES

O modo ONLY_IF_NO_OBSTACLES merece destaque: ele impede aquele visual estranho de personagem atravessando a quina de uma parede na diagonal. Em quase todo jogo com diagonal eu uso esse em vez do ALWAYS. Existe ainda o DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE, um meio-termo que exige só um dos lados livre.

A heurística (a estimativa de distância que guia a busca) também é configurável via default_compute_heuristic e default_estimate_heuristic. A regra é casar com o movimento: HEURISTIC_MANHATTAN pra grade sem diagonal, HEURISTIC_OCTILE pra grade com diagonal (é o padrão e funciona bem), HEURISTIC_EUCLIDEAN quando o resultado vai virar movimento contínuo. Se você não tem motivo pra mexer, o padrão serve.

Última propriedade dessa família: jumping_enabled = true ativa uma otimização (Jump Point Search) que acelera bastante a busca em mapas grandes e abertos. O porém é direto: ela ignora os pesos de célula. Se o seu jogo usa custo de terreno, deixe desligada. É um ou outro.

Movendo o personagem pelo caminho

Caminho calculado é só um array. O movimento de tabuleiro clássico (anda uma célula, para, anda a próxima) fica limpo com tween:

extends Node2D

@export var tile_map: TileMapLayer

var astar := AStarGrid2D.new()
var caminho: Array[Vector2i] = []
var movendo := false

func _unhandled_input(event) -> void:
    if event is InputEventMouseButton and event.pressed \
            and event.button_index == MOUSE_BUTTON_LEFT:
        var clique := tile_map.to_local(get_global_mouse_position())
        var destino := tile_map.local_to_map(clique)
        var origem := tile_map.local_to_map(tile_map.to_local(global_position))

        if not astar.is_in_boundsv(destino) or astar.is_point_solid(destino):
            return

        caminho = astar.get_id_path(origem, destino)
        if not caminho.is_empty():
            caminho.pop_front()  # a primeira célula é onde já estamos

func _process(_delta: float) -> void:
    if movendo or caminho.is_empty():
        return
    movendo = true
    var proxima: Vector2i = caminho.pop_front()
    # map_to_local devolve o centro da célula, em coordenadas do TileMap.
    var alvo := tile_map.to_global(tile_map.map_to_local(proxima))
    var tween := create_tween()
    tween.tween_property(self, "global_position", alvo, 0.15)
    tween.finished.connect(func(): movendo = false)

Dois pontos valem atenção. Primeiro, o caminho inclui a célula de origem, por isso o pop_front() logo depois de calcular. Segundo, map_to_local() devolve o centro do tile, então o personagem para alinhado sem nenhuma conta extra de offset.

Pra movimento deslizante em vez de passo a passo, troque a fila de células por get_point_path() e siga os pontos com velocidade constante num CharacterBody2D. A estrutura é a mesma, muda só a interpolação.

Performance e recálculo

A boa notícia: pra mapa de jogo típico (algumas dezenas por algumas dezenas de células), o custo de um get_id_path() é desprezível e você pode recalcular sempre que o destino muda sem pensar duas vezes.

O que evitar é recalcular sem motivo. Recalcular caminho de vinte inimigos todo frame é desperdício clássico: o caminho só muda quando o mapa muda ou o alvo muda de célula. Guarde a célula do alvo no último cálculo e só refaça quando ela for outra. Em jogo por turnos, então, o recálculo acontece uma vez por ação e performance simplesmente não é assunto.

Pra mapa gigante e aberto sem custo de terreno, jumping_enabled resolve. Pra mapa gigante com custo de terreno, o caminho costuma ser reduzir a frequência de recálculo e espalhar os pedidos entre frames, não otimizar o A* em si.

Fechando

O fluxo inteiro cabe numa frase: região e célula vêm do TileMap, andavel e custo vêm de Custom Data do TileSet, set_point_solid() e set_point_weight_scale() montam a grade, get_id_path() devolve o caminho e um tween anda por ele. Nenhuma etapa exige entender a matemática do A*, mas todas ficam melhores quando você entende o que peso e heurística significam.

Se quiser fixar, monte um tabuleiro pequeno com três terrenos (chão, lama, estrada) e clique pra mover. Depois mude os pesos e veja o caminho se redesenhar sozinho. Ver o algoritmo trocar de rota porque a lama ficou cara é o momento em que isso deixa de ser API decorada e vira ferramenta sua. E se a grade te fisgou, o passo seguinte de lógica pura em tabuleiro é fazer um jogo de xadrez na Godot, onde a geração de movimentos usa exatamente o mesmo raciocínio de varrer direções na grade.