Voltar para o Blog
Quest Log

Empurrar Caixas e Blocos no Godot 4

Personagem 2D empurrando uma caixa de madeira sobre o chão de pedra em um nível de plataforma no Godot.

Aprenda a empurrar caixa godot em jogos 2D com duas abordagens: física com RigidBody2D e empurrão em grade estilo sokoban com RayCast2D. Código real.

Empurrar caixas é uma daquelas mecânicas que parecem triviais até você sentar para implementar. Quando você vai fazer o player empurrar caixa godot percebe que existem várias decisões escondidas: a caixa desliza ou anda em passos fixos? Ela tem peso, inércia, atrito? Pode ser empurrada para qualquer lado ou só nas direções da grade? A resposta certa depende do tipo de jogo, e por isso este post mostra duas abordagens diferentes, com código real de Godot 4, para você escolher a que encaixa no seu projeto.

Empurrar Caixas e Blocos no Godot 4

As duas abordagens resolvem o mesmo problema visual, mas vêm de mundos opostos. A primeira usa o motor de física: a caixa é um corpo rígido que reage a impulsos, escorrega, bate em paredes e empilha com outras caixas. A segunda ignora a física e move a caixa em incrementos exatos de tile, garantindo que ela sempre pare alinhada à grade, como em um quebra-cabeça sokoban. Vamos ver cada uma e depois comparar onde cada uma brilha.

Abordagem 1: empurrar caixa godot com física e RigidBody2D

Aqui a caixa é um RigidBody2D e o player é um CharacterBody2D. O CharacterBody2D não empurra corpos rígidos sozinho. Quando ele usa move_and_slide, ele desliza ao redor dos obstáculos, mas não transfere força para eles. Precisamos detectar a colisão manualmente e aplicar um impulso na caixa na direção do movimento.

A montagem da cena fica assim. A caixa tem um RigidBody2D com um CollisionShape2D e nada de especial além de uma massa configurada. O player é um CharacterBody2D com seu shape e o script de movimento. O ponto central está em iterar as colisões geradas pelo move_and_slide no mesmo frame.

extends CharacterBody2D

const SPEED := 200.0
const PUSH_FORCE := 80.0

func _physics_process(delta: float) -> void:
    var direction := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
    velocity = direction * SPEED
    move_and_slide()
    _push_rigid_bodies()

func _push_rigid_bodies() -> void:
    for i in get_slide_collision_count():
        var collision := get_slide_collision(i)
        var collider := collision.get_collider()
        if collider is RigidBody2D:
            var push_dir := -collision.get_normal()
            collider.apply_central_impulse(push_dir * PUSH_FORCE)

O que está acontecendo aqui. Depois de move_and_slide, o CharacterBody2D registra cada colisão do frame. get_slide_collision_count diz quantas foram e get_slide_collision(i) devolve os dados de cada uma. Se o collider for um RigidBody2D, calculamos a direção do empurrão. A normal da colisão aponta da caixa de volta para o player, então invertemos com o sinal de menos para empurrar a caixa para longe. apply_central_impulse aplica a força no centro de massa, o que evita que a caixa gire de forma estranha ao ser empurrada por um canto.

O PUSH_FORCE é o parâmetro que você vai ajustar mais. Valor baixo deixa a caixa pesada e difícil de mover. Valor alto faz a caixa sair voando ao menor toque. Comece em torno de 80 e suba ou desça testando no jogo.

A massa da caixa também importa. Como o impulso é aplicado direto, uma caixa muito leve dispara longe e uma muito pesada nem se mexe. Para um empurrão que parece natural, mantenha a massa da caixa entre 1 e 4. Se quiser um bloco que se sinta como pedra, aumente a massa em vez de reduzir a força, assim ele resiste tanto ao player quanto a outras caixas que o empurram.

Um detalhe que evita bugs: aplicar impulso todo frame enquanto o player encosta acumula velocidade. Para um deslizar mais controlado, aumente o linear_damp da caixa no inspetor. Isso adiciona um amortecimento que freia a caixa quando o player para de empurrar, sem precisar de código extra. Se você quer entender melhor como esses corpos reagem entre si, o post sobre física de jogos no Godot cobre os tipos de corpo e quando usar cada um.

Essa abordagem dá um resultado orgânico. A caixa tem inércia, escorrega depois do empurrão, pode ser tocada de qualquer ângulo e interage com a gravidade se o jogo for top-down ou plataforma. O preço é a imprevisibilidade. A caixa pode parar a meio caminho de um tile, deslizar para um buraco ou empilhar de forma que você não planejou. Para ação e exploração isso é ótimo. Para um quebra-cabeça que exige posições exatas, é uma fonte de dor de cabeça.

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

Abordagem 2: empurrão em grade estilo sokoban

Quando o jogo é um quebra-cabeça onde cada bloco precisa parar exatamente sobre um tile, a física atrapalha mais do que ajuda. A solução é abandonar o RigidBody2D e mover a caixa manualmente em passos do tamanho do tile. A caixa vira um StaticBody2D ou um simples Node2D com colisor, e quem decide se ela pode se mover é o player, usando um RayCast2D apontado na direção do movimento.

A lógica é a clássica do sokoban. Quando o player tenta andar em uma direção, ele primeiro verifica se há uma caixa na célula à frente. Se houver, verifica se a célula logo atrás da caixa está livre. Se estiver, a caixa anda um tile e o player avança junto. Se a célula atrás estiver bloqueada por uma parede ou outra caixa, ninguém se move.

Use dois raycasts no player, um curto para detectar a caixa e a lógica de checar o espaço livre. Aqui está uma versão enxuta com o player também em grade.

extends CharacterBody2D

const TILE := 32
@onready var box_ray: RayCast2D = $BoxRay

func try_move(dir: Vector2i) -> void:
    var offset := Vector2(dir) * TILE
    box_ray.target_position = offset
    box_ray.force_raycast_update()

    if not box_ray.is_colliding():
        global_position += offset
        return

    var box := box_ray.get_collider()
    if not box.is_in_group("caixas"):
        return

    if _space_is_free(box.global_position, dir):
        box.global_position += offset
        global_position += offset

func _space_is_free(box_pos: Vector2, dir: Vector2i) -> bool:
    var target := box_pos + Vector2(dir) * TILE
    var space := get_world_2d().direct_space_state
    var params := PhysicsPointQueryParameters2D.new()
    params.position = target
    params.collide_with_bodies = true
    var hits := space.intersect_point(params)
    return hits.is_empty()

O fluxo é direto. try_move recebe uma direção em inteiros, tipo Vector2i(1, 0) para a direita. O box_ray é apontado para o tile vizinho e forçamos a atualização com force_raycast_update, porque queremos o resultado no mesmo frame, não no próximo. Se o raycast não bate em nada, o caminho está livre e o player simplesmente anda um tile.

Se bater em algo, conferimos se é uma caixa pelo grupo caixas. Em seguida, _space_is_free faz uma consulta de ponto na posição logo atrás da caixa. Se não houver nada lá, a caixa e o player avançam juntos um tile. Tudo em incrementos de TILE, então as posições ficam sempre cravadas na grade.

Se preferir checar o espaço livre com outro raycast em vez de uma consulta de ponto, dá certo também. Um segundo RayCast2D partindo da posição da caixa na direção do empurrão resolve. A consulta de ponto costuma ser mais limpa porque você não precisa posicionar e atualizar um nó de raycast a cada tentativa. Para quem está começando com sensores de distância, o post sobre RayCast2D no Godot explica os parâmetros e os erros comuns de configuração.

Para o jogo ficar bom, você não vai querer que a caixa teleporte para o novo tile. Em vez de setar global_position direto, anime essa transição com um Tween. Mantenha a lógica de grade decidindo o destino e use o tween só para o movimento visual entre o tile antigo e o novo. Assim você tem a precisão da grade com a suavidade da animação.

Comparando as duas abordagens

As duas técnicas servem a propósitos diferentes e vale ter clareza sobre o trade-off antes de codar.

A física com RigidBody2D é orgânica e barata de configurar. A caixa reage de forma crível, pode ser empurrada de qualquer ângulo, escorrega, empilha e interage com rampas e gravidade. Você ganha tudo isso quase de graça. O custo é a imprevisibilidade. Posições finais não são garantidas, a caixa pode parar entre tiles e situações de empilhamento podem gerar resultados que você não testou. É a escolha para jogos de ação, plataforma e exploração, onde a sensação importa mais que a precisão milimétrica.

O empurrão em grade é o oposto. Cada caixa para exatamente onde você manda, o estado do nível é totalmente previsível e fácil de validar, e checar se o quebra-cabeça foi resolvido vira uma simples comparação de posições. O custo é que você escreve mais lógica manual e perde a naturalidade da física. A caixa só anda nas direções da grade, em passos fixos, sem inércia. É a escolha óbvia para sokoban, puzzles de blocos e qualquer jogo onde a posição exata define a vitória.

Uma decisão de bastidores que afeta as duas abordagens é o tipo de nó que detecta a caixa. Se você está em dúvida entre usar uma área sensora ou um corpo físico para essa detecção, o post Area2D vs CollisionBody no Godot ajuda a decidir entre um sensor que só avisa e um corpo que de fato bloqueia o movimento.

Qual escolher

Se o seu jogo precisa que a caixa pare em posições exatas para resolver um desafio, vá de grade. Se a caixa é parte do cenário e você quer que ela se mexa de forma viva quando o player encosta, vá de física. Não existe abordagem certa em abstrato, existe a que combina com o seu design.

E nada impede de misturar as duas no mesmo projeto. Caixas de quebra-cabeça em salas específicas usando grade, e barris soltos pelo mundo usando física. O importante é entender o que cada técnica garante e o que ela abre mão, para não brigar com o motor depois. Comece simples, ajuste o PUSH_FORCE ou o tamanho do TILE, teste muito no jogo e deixe a sensação guiar o ajuste fino.