Voltar para o Blog
Quest Log

Física de Jogos no Godot: Tutorial Completo de CharacterBody e RigidBody

Diagrama explicativo de física e colisões no Godot Engine

Entenda física de jogos no Godot 4 na prática: CharacterBody2D/3D, RigidBody, colisões, layers e masks, raycasting e otimização de performance, com código.

Física de Jogos no Godot: Tutorial Completo de CharacterBody e RigidBody

Física é o que faz um jogo "sentir" bem. O pulo que responde na hora, a colisão que para onde tem que parar, o personagem que desliza na parede em vez de grudar. Quando está certo, ninguém percebe. Quando está errado, todo mundo larga o jogo na primeira fase.

O Godot tem dois sistemas de física, um pra 2D e outro pra 3D, e quatro tipos principais de corpo físico. A maioria dos travamentos de iniciante vem de escolher o corpo errado pro trabalho: usar RigidBody quando queria controle total, ou empurrar um personagem com _process() em vez de _physics_process() e não entender por que ele treme.

Esse tutorial cobre o caminho que eu uso na prática: CharacterBody pra personagem, RigidBody pra objeto que reage a força, colisão por layers, raycasting e otimização. Todo código aqui é GDScript do Godot 4.x e roda como está.

Fundamentos de Física no Godot

Antes de qualquer código, dois conceitos resolvem metade dos problemas: qual engine roda por baixo e em qual loop você escreve.

O sistema de physics engine

O Godot usa engines de física separadas pra 2D e 3D.

2D (GodotPhysics2D):

  • Engine própria, feita pra jogos 2D
  • Trabalha só nos eixos X e Y
  • Gravidade padrão: (0, 980) pixels/s² (Y positivo aponta pra baixo no 2D)

3D (GodotPhysics3D ou Jolt):

  • O Godot 4.5 trouxe a Jolt Physics como opção, com performance e estabilidade melhores em cenas pesadas
  • A GodotPhysics3D continua disponível por compatibilidade
  • Gravidade padrão: (0, -9.8, 0) metros/s² (Y negativo aponta pra baixo no 3D)

_process vs _physics_process

Essa é a regra que mais economiza dor de cabeça: todo código de movimento e física vai em _physics_process, nunca em _process.

func _process(delta: float) -> void:
    # Roda uma vez por frame renderizado. A taxa varia com o FPS.
    # Use pra coisa visual: animação por código, atualizar HUD, etc.
    pass

func _physics_process(delta: float) -> void:
    # Roda em passo fixo (padrão: 60x por segundo), independente do FPS.
    # Use pra movimento, gravidade e qualquer coisa que toque física.
    pass

O passo fixo é o que garante comportamento igual em todo PC. Se você move o personagem no _process(), a velocidade dele passa a depender do FPS da máquina, e o corpo físico fica fora de sincronia com a engine. Resultado: tremor (jitter) e colisão que falha de vez em quando. Em _physics_process() isso não acontece.

Tipos de physics bodies

O Godot tem quatro tipos de corpo, mais a Area pra detecção. Escolher certo aqui evita 90% dos problemas:

StaticBody2D/3D: não se move. Parede, chão, obstáculo fixo. Colide com tudo mas nunca sai do lugar. É o mais barato pra engine.

CharacterBody2D/3D: movimento controlado por script. É o que você usa pra player e inimigo controlado por código. Tem o método move_and_slide(), que move o corpo e resolve colisão deslizando na superfície. Use pra qualquer personagem que você controla diretamente.

RigidBody2D/3D: simulação física completa. Caixa, barril, ragdoll, qualquer coisa que tomba, rola e quica sozinha. Você não seta a posição: aplica força e impulso, e a engine calcula o resto.

AnimatableBody2D/3D: um corpo que você move por script ou animação mas que não reage a forças. Plataforma móvel, elevador, porta. A diferença pro StaticBody é que ele empurra outros corpos quando se move.

Area2D/3D: não colide fisicamente. Só detecta quando algo entra e sai. Zona de gatilho, item pra coletar, área de dano. É mais leve que colisão de verdade.

Shapes de colisão

Todo corpo precisa de uma CollisionShape pra saber o formato da colisão. As principais:

  • RectangleShape2D / BoxShape3D: retângulo e caixa. O caso mais comum.
  • CircleShape2D / SphereShape3D: círculo e esfera. Bom pra projétil e bola.
  • CapsuleShape2D / 3D: cápsula. É a melhor escolha pra personagem, porque sobe degrau e desliza em quina sem travar.
  • ConvexPolygonShape / ConcavePolygonShape: formas customizadas. Mais caras.

Shape simples é muito mais barata que polígono complexo. Quando precisar de um formato esquisito, prefira juntar duas ou três shapes simples em vez de desenhar um côncavo cheio de vértices. A engine agradece.

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

CharacterBody2D: movimento de personagem

CharacterBody é o corpo que você mais vai usar. Vou montar um controlador de plataforma do zero e ir acrescentando o que faz a diferença no game feel.

Setup básico

A estrutura de nodes é simples: o corpo, uma shape e um sprite.

Player (CharacterBody2D)
├── CollisionShape2D
└── Sprite2D

O script base de plataforma:

extends CharacterBody2D

const SPEED: float = 300.0
const JUMP_VELOCITY: float = -400.0

# Pega a gravidade das configurações do projeto em vez de chumbar o valor.
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")

func _physics_process(delta: float) -> void:
    # Gravidade enquanto está no ar.
    if not is_on_floor():
        velocity.y += gravity * delta

    # Pulo só quando está no chão.
    if Input.is_action_just_pressed("ui_accept") and is_on_floor():
        velocity.y = JUMP_VELOCITY

    # Direção horizontal: -1, 0 ou 1.
    var direction = Input.get_axis("ui_left", "ui_right")
    if direction:
        velocity.x = direction * SPEED
    else:
        velocity.x = move_toward(velocity.x, 0, SPEED)

    move_and_slide()

Repare que JUMP_VELOCITY é negativo. No 2D do Godot, Y cresce pra baixo, então pular é dar velocidade negativa em Y.

O que o move_and_slide() faz

Essa única chamada faz o trabalho pesado:

  • Move o corpo de acordo com velocity
  • Detecta colisão e resolve sozinho
  • Desliza ao longo da superfície em vez de grudar nela
  • Atualiza o estado de contato (chão, parede, teto)

Depois de chamar, você consulta o estado:

move_and_slide()

is_on_floor()   # encostou no chão?
is_on_ceiling() # bateu a cabeça no teto?
is_on_wall()    # está tocando parede?

# Detalhes de cada colisão que aconteceu neste frame:
for i in get_slide_collision_count():
    var collision = get_slide_collision(i)
    print(collision.get_collider().name, collision.get_normal())

Aceleração e fricção

Movimento que liga e desliga na hora parece robótico. Em vez de setar velocity.x direto pra SPEED, você acelera até lá e desacelera com fricção. O move_toward() faz exatamente isso: caminha um valor por frame na direção do alvo.

extends CharacterBody2D

const SPEED: float = 300.0
const ACCELERATION: float = 2000.0  # pixels/s² ao acelerar
const FRICTION: float = 1500.0      # pixels/s² ao frear

func _physics_process(delta: float) -> void:
    var direction = Input.get_axis("ui_left", "ui_right")
    if direction:
        velocity.x = move_toward(velocity.x, direction * SPEED, ACCELERATION * delta)
    else:
        velocity.x = move_toward(velocity.x, 0, FRICTION * delta)

    move_and_slide()

Quanto maior ACCELERATION, mais "duro" e responsivo o controle fica; quanto menor, mais o personagem patina. Não existe valor certo, é tuning. Roda o jogo, sente, ajusta. Um plataforma preciso pede aceleração alta; um jogo de gelo pede fricção baixa de propósito.

Pulo de altura variável

Em jogo de plataforma bom, segurar o botão pula mais alto e tocar de leve dá um pulinho. O truque é cortar a velocidade do pulo quando o jogador solta o botão antes do topo:

const JUMP_VELOCITY: float = -400.0
const JUMP_CUT: float = 0.5  # fração da velocidade que sobra ao soltar cedo

func _physics_process(delta: float) -> void:
    if not is_on_floor():
        velocity.y += gravity * delta

    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y = JUMP_VELOCITY

    # Soltou o botão ainda subindo? Corta o impulso.
    if Input.is_action_just_released("jump") and velocity.y < 0:
        velocity.y *= JUMP_CUT

    move_and_slide()

Coyote time e jump buffer

Essas duas perdoam o erro de timing do jogador e fazem o pulo parecer muito mais confiável:

  • Coyote time: se o jogador apertar pular logo depois de sair da borda da plataforma, ainda conta como pulo válido. O nome vem do coiote do desenho que fica suspenso no ar.
  • Jump buffer: se o jogador apertar pular um instante antes de tocar o chão, o jogo segura a intenção e dispara assim que pousa.

Os dois são só temporizadores em segundos:

const COYOTE_TIME: float = 0.1
const JUMP_BUFFER_TIME: float = 0.1

var coyote_timer: float = 0.0
var jump_buffer_timer: float = 0.0

func _physics_process(delta: float) -> void:
    if not is_on_floor():
        velocity.y += gravity * delta

    # Recarrega o coyote no chão, descarrega no ar.
    if is_on_floor():
        coyote_timer = COYOTE_TIME
    else:
        coyote_timer -= delta

    # Recarrega o buffer ao apertar, descarrega com o tempo.
    if Input.is_action_just_pressed("jump"):
        jump_buffer_timer = JUMP_BUFFER_TIME
    else:
        jump_buffer_timer -= delta

    # Pula se tem intenção recente (buffer) e direito de pular (coyote).
    if jump_buffer_timer > 0 and coyote_timer > 0:
        velocity.y = JUMP_VELOCITY
        jump_buffer_timer = 0
        coyote_timer = 0

    move_and_slide()

São poucas linhas, mas a diferença no controle é grande. Vale testar com e sem pra sentir.

CharacterBody3D: controlador first-person

No 3D entra o eixo Z e a rotação da câmera pelo mouse. A lógica de movimento é a mesma, só que em três eixos. Esse é praticamente o controlador padrão que o próprio Godot gera no template de FPS, e se quiser aprofundar o movimento de personagem em primeira pessoa 3D o passo a passo completo está num guia dedicado:

extends CharacterBody3D

const SPEED: float = 5.0
const JUMP_VELOCITY: float = 4.5
const SENSITIVITY: float = 0.003

@onready var camera: Camera3D = $Camera3D

var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")

func _ready() -> void:
    Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

func _input(event) -> void:
    if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
        # Gira o corpo no eixo Y (olhar pros lados).
        rotate_y(-event.relative.x * SENSITIVITY)
        # Gira só a câmera no eixo X (olhar pra cima/baixo).
        camera.rotate_x(-event.relative.y * SENSITIVITY)
        # Trava pra não dar cambalhota olhando pra cima/baixo.
        camera.rotation.x = clamp(camera.rotation.x, -PI/2, PI/2)

func _physics_process(delta: float) -> void:
    if not is_on_floor():
        velocity.y -= gravity * delta

    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y = JUMP_VELOCITY

    # input_dir é um Vector2; transformamos pela base do corpo
    # pra mover relativo a pra onde o personagem está olhando.
    var input_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_back")
    var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()

    if direction:
        velocity.x = direction.x * SPEED
        velocity.z = direction.z * SPEED
    else:
        velocity.x = move_toward(velocity.x, 0, SPEED)
        velocity.z = move_toward(velocity.z, 0, SPEED)

    move_and_slide()

Detalhe importante: no 3D a gravidade é subtraída (velocity.y -= ...) porque Y aponta pra cima. No 2D era somada. É a fonte de bug mais boba que existe quando você migra código de um pro outro.

RigidBody: quando a engine controla o movimento

RigidBody é pro oposto do CharacterBody. Aqui você não diz onde o corpo vai: você aplica força e impulso, e a engine simula a queda, o tombo, o quique. Caixa que cai, barril que rola, dado que joga.

extends RigidBody2D

func _ready() -> void:
    mass = 10.0
    gravity_scale = 1.0   # 1.0 = gravidade normal; 0 = flutua; 2 = pesado
    linear_damp = 0.1     # resistência ao movimento (tipo arrasto do ar)
    angular_damp = 0.5    # resistência à rotação

func explodir() -> void:
    # Impulso = empurrão instantâneo.
    apply_central_impulse(Vector2(500, -500))

Impulso vs força

A confusão mais comum com RigidBody é misturar os dois:

  • Impulso (apply_central_impulse): muda a velocidade de uma vez. Use pra explosão, pulo, coice de colisão. Chama uma vez.
  • Força (apply_central_force): empurra continuamente enquanto você aplica. Use pra motor, vento, ímã. Tem que chamar todo frame em _physics_process.
# Impulso: uma chamada, efeito imediato.
apply_central_impulse(Vector2(500, 0))

# Força: contínua, dentro do loop de física.
func _physics_process(delta: float) -> void:
    apply_central_force(Vector2(100, 0))

Travar rotação

Às vezes você quer a física de queda mas sem o corpo girando (uma moeda que cai reta, por exemplo):

func _ready() -> void:
    lock_rotation = true

Sleeping e colisão contínua

Dois detalhes que valem conhecer:

Sleeping. Quando um RigidBody para de se mexer, a engine coloca ele "pra dormir" e deixa de simular até algo encostar. É otimização gratuita e fica ligada por padrão. Você só mexe nisso em caso específico:

sleeping = false   # força acordar agora
can_sleep = false  # nunca dorme (cuidado: pesa na performance)

Colisão contínua (CCD). Corpo muito rápido pode atravessar uma parede fina entre um frame e outro (tunneling), porque num frame está de um lado e no próximo já passou. A colisão contínua resolve, ao custo de um pouco mais de processamento. Ligue só nos corpos rápidos de verdade, como projétil:

continuous_cd = CCD_MODE_CAST_RAY

Colisões: layers e masks

Esse é o sistema que diz o que colide com o quê, e entender ele evita aquele bug clássico de inimigo travando em inimigo ou bala batendo na bala. São duas coisas separadas:

  • Collision layer: "em quais camadas eu estou"
  • Collision mask: "com quais camadas eu colido"

Dois corpos só interagem se a layer de um estiver na mask do outro. Pensar nas duas perguntas separadas resolve a maioria das dúvidas:

Camadas: 1 = Player, 2 = Inimigos, 3 = Paredes, 4 = Itens

Player → layer 1, mask [2, 3, 4]   (bate em inimigo, parede e item)
Inimigo → layer 2, mask [1, 3]     (bate em player e parede, ignora outro inimigo)

No editor você marca isso por checkbox, que é o jeito recomendado. Em código, layer e mask são bitmask: cada camada é um bit. Camada 1 é o bit 1, camada 2 é 2, camada 3 é 4, camada 4 é 8. Pra estar nas camadas 2 e 3 ao mesmo tempo, soma os bits:

collision_layer = 1       # camada 1
collision_mask = 2 | 4    # camadas 2 e 3 (resulta em 6)

Na prática, deixe pelo editor sempre que der. A conta de bits é útil quando você precisa ligar e desligar camada por código em runtime.

Detectando colisões

Cada tipo de corpo informa colisão de um jeito.

CharacterBody: depois do move_and_slide(), você percorre as colisões do frame:

move_and_slide()

for i in get_slide_collision_count():
    var collision = get_slide_collision(i)
    print("bateu em: ", collision.get_collider().name)
    print("normal: ", collision.get_normal())
    print("posição: ", collision.get_position())

RigidBody: conecte os sinais body_entered e body_exited no editor (precisa ligar contact_monitor e subir max_contacts_reported pra eles dispararem):

func _on_body_entered(body) -> void:
    print("bateu em: ", body.name)

func _on_body_exited(body) -> void:
    print("parou de tocar: ", body.name)

Area2D: pra gatilho e detecção sem colisão física. Conecte body_entered ou area_entered:

func _on_body_entered(body) -> void:
    if body.name == "Player":
        coletar_item()

Raycasting

Raycast é uma linha invisível que pergunta "tem algo no caminho entre aqui e ali?". Não é colisão física, é consulta. Serve pra checar linha de visão de inimigo, descobrir onde uma bala acerta, ou confirmar se tem chão embaixo do personagem.

RayCast2D node

O jeito mais simples é adicionar um node RayCast2D como filho e consultar todo frame:

extends Node2D

@onready var raycast: RayCast2D = $RayCast2D

func _physics_process(delta: float) -> void:
    if raycast.is_colliding():
        var alvo = raycast.get_collider()
        var ponto = raycast.get_collision_point()
        var normal = raycast.get_collision_normal()
        print("raycast pegou: ", alvo.name, " em ", ponto)

No Inspector você configura Enabled (ligado), Target Position (pra onde a linha aponta, relativo ao node) e Collision Mask (o que detectar).

Raycast por código

Quando você precisa atirar um ray num ponto qualquer, sem ter um node fixo, use o espaço de física direto. É o que você usa pra "atirar onde o jogador clicou":

func raycast_entre(de: Vector2, ate: Vector2) -> Dictionary:
    var space_state = get_world_2d().direct_space_state
    var query = PhysicsRayQueryParameters2D.create(de, ate)
    query.collision_mask = 1  # só detecta a camada 1

    var resultado = space_state.intersect_ray(query)
    if resultado:
        print("acertou: ", resultado.collider.name)
        print("ponto: ", resultado.position)
        print("normal: ", resultado.normal)

    return resultado

O intersect_ray retorna um dicionário vazio quando não acerta nada, então dá pra testar com if resultado: direto.

ShapeCast

O ShapeCast é um raycast com volume: em vez de uma linha, ele varre uma forma (um círculo, uma cápsula) pelo caminho. Útil pra perguntar "se eu mover o personagem pra cá, encosta em algo?" sem mover de verdade. Tem o node ShapeCast2D/3D, com a mesma ideia do RayCast: is_colliding() e os métodos de colisão.

::blog-cta{title="Domine Godot e Construa Jogos Profissionais" description="Física é apenas uma peça do puzzle. Jogos completos requerem arte, áudio, UI, design de levels e muito mais. Aprenda todas as disciplinas necessárias para criar e publicar jogos de sucesso." buttonText="Candidate-se Agora" icon="fas fa-atom" variant="highlight"}::

Plataformas que você atravessa por baixo

Aquela plataforma que você pula através dela de baixo pra cima, mas pousa em cima, é colisão one-way. No Godot não é truque de código: é uma opção da própria CollisionShape2D.

Selecione a CollisionShape2D da plataforma e ligue One Way Collision no Inspector. A direção do "lado que bloqueia" segue a orientação da shape; se ficar invertido, rotacione o node 180 graus. O One Way Collision Margin controla a tolerância em pixels.

Pra deixar o jogador descer de propósito ao apertar pra baixo, o jeito limpo é desligar a colisão por um instante. Em vez de empurrar a posição na mão (que causa trepidação), use set_deferred pra alterar a colisão de forma segura no meio do passo de física:

@onready var shape: CollisionShape2D = $CollisionShape2D

func _unhandled_input(event) -> void:
    if Input.is_action_just_pressed("ui_down"):
        # Desliga a colisão one-way por um momento pra cair pela plataforma.
        shape.set_deferred("one_way_collision", false)
        await get_tree().create_timer(0.2).timeout
        shape.set_deferred("one_way_collision", true)

Plataformas móveis

Pra uma plataforma que se move e leva o jogador junto, o corpo certo é o AnimatableBody2D. Ele se move por script mas empurra quem está em cima, que é o que o StaticBody não faz.

extends AnimatableBody2D

@export var distancia: float = 200.0
@export var velocidade: float = 100.0

var pos_inicial: Vector2
var pos_alvo: Vector2
var direcao: int = 1

func _ready() -> void:
    pos_inicial = position
    pos_alvo = pos_inicial + Vector2(distancia, 0)

func _physics_process(delta: float) -> void:
    if direcao == 1:
        position.x += velocidade * delta
        if position.x >= pos_alvo.x:
            direcao = -1
    else:
        position.x -= velocidade * delta
        if position.x <= pos_inicial.x:
            direcao = 1

O CharacterBody gruda na plataforma em movimento automaticamente, contanto que is_on_floor() seja verdadeiro. Você não precisa de código extra pra carregar o player.

Otimização de performance

Física custa CPU, e custa rápido quando a contagem de corpos sobe. As medidas abaixo vêm na ordem do maior retorno pro menor.

Use shapes simples. Círculo, retângulo e cápsula são ordens de grandeza mais baratos que polígono côncavo. Se um nível inteiro de colisão é um côncavo gigante, ele vira o gargalo. Quebre em peças simples.

Limpe os masks. Toda checagem de colisão que não precisa acontecer é trabalho jogado fora. Projétil não precisa colidir com projétil. Item não precisa colidir com item. Tire essas camadas das masks e a engine para de testar pares inúteis.

Deixe os RigidBodies dormirem. É padrão, mas confira que você não desligou can_sleep sem motivo. Corpo parado dormindo é corpo de graça.

Reduza o tick de física se der. Em Project Settings > Physics > Common, o Physics Ticks Per Second vem em 60. Em jogo mobile mais leve, baixar pra 30 ou 45 corta o custo quase pela metade. Cuidado: isso muda o "feel" do movimento, então teste antes de fixar.

Use Area2D pra gatilho. Detecção de overlap (zona de dano, coleta de item, sensor) é mais barata em Area2D do que forçar colisão física de verdade só pra saber que algo entrou.

Durma corpos fora da tela. Um VisibleOnScreenNotifier2D pode pausar a simulação de objetos que saíram do quadro:

extends RigidBody2D

func _on_visible_on_screen_notifier_screen_exited() -> void:
    sleeping = true

func _on_visible_on_screen_notifier_screen_entered() -> void:
    sleeping = false

Debug de física

Três ferramentas resolvem quase toda investigação de colisão.

Visible Collision Shapes. No menu do editor, Debug > Visible Collision Shapes. Liga e roda: agora você vê os contornos de colisão durante o jogo. Metade dos bugs de "por que não bate" é a shape estar no lugar errado ou com tamanho errado, e isso mostra na hora.

Desenhar o raycast. Pra enxergar um ray que você criou por código, desenhe a linha:

func _process(delta: float) -> void:
    queue_redraw()

func _draw() -> void:
    if raycast.is_colliding():
        var ponto = raycast.get_collision_point() - global_position
        draw_line(Vector2.ZERO, ponto, Color.RED, 2)

Performance Monitor. Em Debug > Performance Monitor (ou o painel Monitor no editor), acompanhe o tempo de física, a contagem de objetos de colisão e os corpos ativos. A conta que importa: pra rodar a 60 FPS, cada frame tem 16,6 ms no total. Se só a física já come boa parte disso, é onde você corta.

Fechando

O grosso de física no Godot cabe em poucas decisões: rodar tudo em _physics_process, escolher o corpo certo (CharacterBody quando você controla, RigidBody quando a engine controla), e configurar layers e masks pra cada coisa colidir só com o que deve.

O resto é tuning. Aceleração, fricção, coyote time, força de pulo: nenhum desses tem valor "correto", eles têm o valor que faz o seu jogo sentir bem. E isso você só descobre rodando, ajustando e rodando de novo.

Se quiser fixar, monta um controlador de plataforma do zero e vai empilhando: primeiro o movimento horizontal, depois pulo e gravidade, depois aceleração, depois coyote time e jump buffer. Cada camada leva poucos minutos e você sente exatamente o que cada uma muda. É assim que esse conhecimento gruda, fazendo, não lendo.

Dois passos naturais depois daqui: organizar o que colide com o quê usando collision layers e masks no Godot, e aplicar RigidBody num caso concreto aprendendo a empurrar caixas e blocos a partir da colisão do personagem.