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

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.
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.


