Voltar para o Blog
Quest Log

Movimento em Grade (Grid) no Godot 4

Personagem 2D pixel art se deslocando de uma célula para outra sobre um tabuleiro quadriculado no Godot.

Como fazer movimento em grade godot estilo Pokémon ou roguelike: snap à grade, checagem de colisão por célula e deslizamento suave com Tween.

Jogos como Pokémon, roguelikes clássicos e a maioria dos puzzles de tabuleiro não deixam o personagem andar livremente pelo mapa. O personagem pula de uma célula para a célula vizinha, sempre alinhado a uma grade invisível. Esse padrão é o movimento em grade godot, e ele resolve dois problemas de uma vez: o posicionamento fica previsível (ótimo para colisão, salvamento e lógica de turnos) e a movimentação ganha aquela sensação de jogo de tabuleiro. Neste tutorial você vai montar um sistema de movimento em grade no Godot 4 do zero, com snap inicial, checagem de célula livre e deslizamento suave entre as casas.

Movimento em Grade (Grid) no Godot 4

A ideia central é simples. Em vez de somar velocidade à posição a cada quadro, você decide para qual célula o personagem quer ir, confere se ela está livre e, se estiver, anima o deslocamento até lá. Enquanto o personagem desliza, nenhum input novo é aceito. Quando ele chega, o sistema volta a aceitar comandos. Tudo gira em torno de um número: o tamanho do tile.

Definindo o tamanho do tile

O tamanho do tile é a distância em pixels entre o centro de uma célula e o centro da vizinha. Em pixel art é comum usar 16 ou 32 pixels. Esse valor precisa bater com o tamanho dos tiles do seu mapa, senão o personagem vai parar entre as casas.

Vamos começar com um CharacterBody2D chamado Player. Defina o tamanho como constante para nunca digitar o número solto pelo código.

extends CharacterBody2D

const TILE_SIZE: int = 16

var is_moving: bool = false

A flag is_moving é o coração do sistema. Quando ela é true, o personagem está no meio de uma transição e deve ignorar qualquer novo comando. Sem essa trava o jogador acumularia movimentos e o personagem escorregaria vários tiles de uma vez.

Snap inicial à grade

Antes de tudo, garanta que o personagem comece exatamente em cima de uma célula. Se você posicionou ele no editor em uma coordenada qualquer, ele pode estar levemente fora da grade, e todo o resto vai herdar esse desalinhamento. O snap arredonda a posição para o múltiplo de TILE_SIZE mais próximo.

func _ready() -> void:
    global_position = global_position.snapped(Vector2(TILE_SIZE, TILE_SIZE))
    global_position += Vector2(TILE_SIZE, TILE_SIZE) * 0.5

A primeira linha alinha o canto da célula. A segunda soma metade do tile para centralizar o personagem na casa, caso seu sprite tenha o pivô no centro. Se o pivô do seu sprite estiver no canto superior esquerdo, remova a segunda linha. Teste no seu projeto e ajuste conforme o ponto de origem do sprite.

Lendo o input de uma direção por vez

No movimento em grade você quer uma direção por passo, nunca diagonais acidentais nem duas direções somadas. A maneira mais limpa é checar as ações na ordem e parar na primeira que estiver pressionada.

func _process(_delta: float) -> void:
    if is_moving:
        return

    var direction := Vector2.ZERO

    if Input.is_action_pressed("ui_right"):
        direction = Vector2.RIGHT
    elif Input.is_action_pressed("ui_left"):
        direction = Vector2.LEFT
    elif Input.is_action_pressed("ui_down"):
        direction = Vector2.DOWN
    elif Input.is_action_pressed("ui_up"):
        direction = Vector2.UP

    if direction != Vector2.ZERO:
        try_move(direction)

Repare no if is_moving: return logo no começo. Enquanto o Tween estiver rodando, a função sai imediatamente e nenhum comando é lido. As ações ui_right, ui_left, ui_down e ui_up já existem por padrão no mapeamento de entrada do Godot, então você pode testar com as setas sem configurar nada. Para um jogo de verdade, crie suas próprias ações no menu Project Settings, na aba Input Map.

Checando se a célula alvo está livre

Saber para onde ir é metade do trabalho. A outra metade é confirmar que a célula de destino não está bloqueada por uma parede, um inimigo ou o limite do mapa. Existem duas abordagens comuns. A mais direta no Godot 4 usa um RayCast2D apontando na direção do movimento.

Adicione um nó RayCast2D como filho do Player. Deixe a propriedade target_position zerada por enquanto, pois vamos defini-la por código a cada checagem.

@onready var ray: RayCast2D = $RayCast2D

func try_move(direction: Vector2) -> void:
    ray.target_position = direction * TILE_SIZE
    ray.force_raycast_update()

    if ray.is_colliding():
        return

    move_to(global_position + direction * TILE_SIZE)

O raio é apontado exatamente para o centro da célula vizinha (direction * TILE_SIZE). O force_raycast_update() é obrigatório: ele recalcula a colisão no mesmo quadro, em vez de esperar o próximo ciclo de física. Se houver algo na frente, a função retorna e o personagem fica parado. Caso contrário, chamamos move_to com a posição de destino.

Para que isso funcione, suas paredes e obstáculos precisam ter um corpo de colisão (um StaticBody2D com CollisionShape2D, por exemplo) em uma camada que o RayCast2D enxergue. Configure as máscaras de colisão do raio para incluir apenas as camadas que devem bloquear o movimento.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

Deslizando suave entre células com Tween

Teletransportar o personagem para a próxima casa funciona, mas fica robótico. O charme do movimento em grade está no deslizamento suave de uma célula para a outra. No Godot 4 isso é feito com um Tween criado em tempo de execução, interpolando o global_position.

func move_to(target: Vector2) -> void:
    is_moving = true

    var tween := create_tween()
    tween.tween_property(self, "global_position", target, 0.2)
    tween.set_trans(Tween.TRANS_SINE)
    tween.set_ease(Tween.EASE_IN_OUT)

    await tween.finished
    is_moving = false

A duração de 0.2 segundos define o ritmo do passo. Valores menores deixam o jogo ágil, valores maiores dão peso e cadência de roguelike por turnos. O await tween.finished pausa a função até o deslocamento terminar e só então libera a flag is_moving. Como _process retorna cedo enquanto is_moving é true, o jogador pode até manter a tecla pressionada que o personagem só anda um tile por vez, esperando cada passo concluir antes do próximo.

O set_trans e o set_ease controlam a curva da animação. Com TRANS_SINE e EASE_IN_OUT o passo acelera no começo e desacelera no fim, parecendo mais natural. Se você quiser o movimento de velocidade constante, troque por TRANS_LINEAR. Experimente as combinações para achar o feeling do seu jogo.

Juntando tudo

Aqui está o script completo, pronto para colar em um CharacterBody2D com um filho RayCast2D.

extends CharacterBody2D

const TILE_SIZE: int = 16

var is_moving: bool = false

@onready var ray: RayCast2D = $RayCast2D

func _ready() -> void:
    global_position = global_position.snapped(Vector2(TILE_SIZE, TILE_SIZE))
    global_position += Vector2(TILE_SIZE, TILE_SIZE) * 0.5

func _process(_delta: float) -> void:
    if is_moving:
        return

    var direction := Vector2.ZERO

    if Input.is_action_pressed("ui_right"):
        direction = Vector2.RIGHT
    elif Input.is_action_pressed("ui_left"):
        direction = Vector2.LEFT
    elif Input.is_action_pressed("ui_down"):
        direction = Vector2.DOWN
    elif Input.is_action_pressed("ui_up"):
        direction = Vector2.UP

    if direction != Vector2.ZERO:
        try_move(direction)

func try_move(direction: Vector2) -> void:
    ray.target_position = direction * TILE_SIZE
    ray.force_raycast_update()

    if ray.is_colliding():
        return

    move_to(global_position + direction * TILE_SIZE)

func move_to(target: Vector2) -> void:
    is_moving = true

    var tween := create_tween()
    tween.tween_property(self, "global_position", target, 0.2)
    tween.set_trans(Tween.TRANS_SINE)
    tween.set_ease(Tween.EASE_IN_OUT)

    await tween.finished
    is_moving = false

Com isso você já tem um personagem que anda casa por casa, respeita paredes e desliza com suavidade. Rode a cena, posicione alguns obstáculos com colisão e veja o sistema funcionar.

Alinhando à grade do TileMapLayer

Se o seu mapa usa um TileMapLayer (o nó de mapa de tiles do Godot 4), dá para amarrar o movimento à grade real do mapa em vez de confiar só na constante TILE_SIZE. O TileMapLayer oferece dois métodos que convertem entre coordenadas de pixel e coordenadas de célula: local_to_map e map_to_local.

local_to_map recebe uma posição em pixels e devolve a coordenada da célula (um Vector2i). map_to_local faz o caminho inverso, devolvendo o centro da célula em pixels. Isso é útil para garantir o snap perfeito e para consultar qual tile existe em uma célula.

@onready var tilemap: TileMapLayer = $"../TileMapLayer"

func snap_to_map() -> void:
    var cell := tilemap.local_to_map(tilemap.to_local(global_position))
    global_position = tilemap.to_global(tilemap.map_to_local(cell))

func is_cell_free(direction: Vector2) -> bool:
    var current_cell := tilemap.local_to_map(tilemap.to_local(global_position))
    var target_cell := current_cell + Vector2i(direction)
    var tile_data := tilemap.get_cell_tile_data(target_cell)
    return tile_data == null

Aqui is_cell_free checa colisão consultando os dados do tile de destino. Se você marcou certos tiles como sólidos usando uma custom data layer ou um campo no TileSet, dá para ler essa informação em tile_data e decidir se a célula bloqueia. Essa abordagem dispensa o RayCast2D quando todo o cenário é feito de tiles, e costuma ser mais barata. Para projetos que misturam tiles e corpos físicos, vale combinar as duas checagens. Se você ainda está montando o cenário, vale conferir como funciona o nó de tiles no guia sobre TileMapLayer no Godot 4.

Para onde ir a partir daqui

O sistema que montamos cobre o caso mais comum, mas ele é a base de coisas maiores. Para inimigos que perseguem o jogador célula por célula, você vai querer pathfinding. O Godot tem ferramentas prontas para isso, e vale estudar como usar a classe AStarGrid2D para encontrar caminhos sobre a mesma grade que você acabou de definir. Com o caminho calculado em coordenadas de célula, basta reaproveitar a mesma função move_to para mover a unidade casa por casa.

Outras melhorias diretas: adicionar animações de andar por direção, suportar segurar a tecla para passos contínuos, ou impedir entrada em casas de água com uma camada de dados no TileSet. A estrutura com is_moving e Tween aguenta tudo isso sem reescrita. Comece pequeno, deixe um personagem andando na grade com solidez, e cresça a partir daí.