Câmera em Terceira Pessoa no Godot: Tutorial com SpringArm3D e GDScript

Aprenda a fazer uma câmera em terceira pessoa no Godot 4 com SpringArm3D: rotação com mouse, colisão com parede e movimento relativo à câmera em GDScript.
Câmera em Terceira Pessoa no Godot: Tutorial com SpringArm3D e GDScript
Câmera em terceira pessoa no Godot é daqueles sistemas que todo mundo acha que sabe fazer até a primeira parede entrar no caminho. Em mais de 20 anos trabalhando com jogos eu perdi a conta de quantos protótipos promissores afundaram por câmera que atravessa cenário, vira de cabeça pra baixo ou treme atrás do personagem. A boa notícia é que o Godot 4 já traz a peça central desse quebra-cabeça pronta: o SpringArm3D.
Neste tutorial eu monto o sistema completo: hierarquia de nós, captura do mouse, rotação com limite, colisão com o cenário, movimento relativo à câmera e os ajustes de game feel que separam uma câmera funcional de uma câmera gostosa. Todo o código é GDScript tipado e roda como está. Se você ainda está montando sua primeira cena 3D, vale passar antes pelo guia de seu primeiro jogo 3D no Godot e voltar aqui com o personagem já de pé.
A hierarquia de nós da câmera em terceira pessoa no Godot
A estrutura clássica, que eu uso em todo projeto, tem quatro camadas:
- CharacterBody3D: o corpo físico do personagem, com a colisão e o movimento.
- PivoCamera (Node3D): um nó vazio posicionado na altura do peito. É ele que gira na horizontal.
- SpringArm3D: filho do pivô, é o braço que segura a câmera a uma distância configurável e gira na vertical.
- Camera3D: filha do SpringArm3D, fica na ponta do braço. Não precisa de script nenhum.
Cada camada tem um papel único, e é isso que mantém o sistema limpo. O corpo cuida da física e nunca gira com o mouse. O pivô cuida do giro horizontal, o yaw. O braço cuida do giro vertical, o pitch, e da distância até o personagem. A câmera só vai junto.
Quem já montou um controle em primeira pessoa 3D no Godot vai reconhecer o padrão na hora: a lógica de rotação é praticamente a mesma. A diferença é o SpringArm3D no meio da hierarquia, empurrando a câmera pra trás do personagem em vez de deixá-la colada nos olhos dele.
Uma dica antes do código: posicione o PivoCamera em Y = 1.5, mais ou menos na altura do ombro de um personagem de escala humana. Pivô no chão faz a câmera orbitar os pés, e ninguém quer passar o jogo inteiro olhando pra canela do protagonista.
Capturar o mouse e girar com clamp
O script vai no CharacterBody3D, na raiz da cena do player:
extends CharacterBody3D
@export var sensibilidade: float = 0.003
@onready var pivo: Node3D = $PivoCamera
@onready var spring_arm: SpringArm3D = $PivoCamera/SpringArm3D
func _ready() -> void:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
girar_camera(event.relative)
if event.is_action_pressed("ui_cancel"):
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
func girar_camera(movimento: Vector2) -> void:
pivo.rotate_y(-movimento.x * sensibilidade)
spring_arm.rotation.x -= movimento.y * sensibilidade
spring_arm.rotation.x = clampf(spring_arm.rotation.x, deg_to_rad(-60.0), deg_to_rad(30.0))
Três decisões aqui merecem explicação.
Primeiro, _unhandled_input em vez de _input. Eventos de mouse passam pela interface antes de chegar no jogo. Se o jogador estiver com um menu ou inventário aberto, a UI consome o evento e a câmera fica parada, que é exatamente o que você quer. Com _input puro, a câmera giraria por baixo do menu.
Segundo, o sinal negativo no rotate_y. O event.relative cresce quando o mouse vai pra direita, e rotação positiva em Y no Godot gira pra esquerda. Sem inverter o sinal, a câmera anda ao contrário do mouse e o playtest vira um festival de reclamação.
Terceiro, o clamp em radianos. A propriedade rotation.x trabalha em radianos, então os limites precisam passar por deg_to_rad. O valor negativo controla o quanto a câmera sobe e olha o personagem de cima; o positivo controla o quanto ela desce e olha de baixo. Eu dou mais espaço pra cima porque câmera abaixo da linha da cintura acaba enfiada no chão. Ajuste sensibilidade no inspector até o giro ficar confortável, e considere expor esse valor no menu de opções do jogo depois.
A captura do mouse no _ready é obrigatória. MOUSE_MODE_CAPTURED esconde o cursor e trava ele no centro da janela, garantindo movimento relativo infinito. O ui_cancel no Esc solta o cursor pra você conseguir testar sem ficar refém da janela do jogo.
SpringArm3D: a câmera que não atravessa parede
Se você pendurar a Camera3D direto num Node3D a quatro metros do personagem, no primeiro corredor apertado a câmera vai pro outro lado da parede e o jogador passa a ver o avesso do cenário. Resolver isso na mão exige raycast, interpolação de distância e tratamento de canto. O SpringArm3D faz tudo isso sozinho.
O funcionamento é simples: o braço lança um raycast do pivô até o comprimento configurado. Se alguma coisa entra no caminho, ele encurta e puxa a câmera pra frente do obstáculo. Quando o caminho libera, ele estica de volta. A configuração cabe em três linhas, chamadas a partir do _ready:
func configurar_spring_arm() -> void:
spring_arm.spring_length = 4.0
spring_arm.margin = 0.25
spring_arm.add_excluded_object(get_rid())
O spring_length é a distância padrão da câmera, e 4 metros é um bom ponto de partida pra jogo de ação. O margin mantém uma folga entre a câmera e a superfície atingida, evitando que o near plane corte a parede e cause aquele flicker de geometria. E o add_excluded_object(get_rid()) é a linha que quase todo mundo esquece: o raycast do braço nasce dentro do colisor do próprio personagem, então sem essa exclusão o braço detecta o player como obstáculo e a câmera gruda na nuca dele. O get_rid() retorna o RID do corpo físico, que é o identificador que o SpringArm3D usa pra ignorar o colisor.
Se o seu cenário tem muitos objetos pequenos, vale também ajustar a collision_mask do SpringArm3D pra colidir só com a camada de geometria sólida. Câmera que pula por causa de um arbusto ou de uma moeda giratória é irritante na mesma medida que câmera dentro da parede.
Movimento relativo à câmera
Com a câmera girando, aparece o segundo problema clássico: apertar pra frente e o personagem sair andando pro norte do mapa, independente de pra onde a câmera aponta. O movimento precisa ser relativo à câmera, e a chave pra isso é a transform.basis do pivô:
const VELOCIDADE: float = 5.0
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity += get_gravity() * delta
var entrada: Vector2 = Input.get_vector("esquerda", "direita", "frente", "tras")
var direcao: Vector3 = (pivo.transform.basis * Vector3(entrada.x, 0.0, entrada.y)).normalized()
if direcao:
velocity.x = direcao.x * VELOCIDADE
velocity.z = direcao.z * VELOCIDADE
else:
velocity.x = move_toward(velocity.x, 0.0, VELOCIDADE)
velocity.z = move_toward(velocity.z, 0.0, VELOCIDADE)
move_and_slide()
O Input.get_vector devolve um Vector2 já com deadzone tratada, o que importa quando o jogo roda com controle analógico. Esse vetor vive no espaço da tela: X é esquerda e direita, Y é frente e trás. Multiplicar por pivo.transform.basis converte esses eixos pro espaço do mundo, alinhados com a direção atual da câmera. Como o pivô só gira em Y, o resultado continua no plano horizontal e a gravidade fica intacta.
O normalized() no final garante que a diagonal não fique mais rápida que o movimento reto. E repare que quem entra na conta é a base do pivô, não a do corpo: o CharacterBody3D nunca girou, então a base dele seria inútil aqui.
Game feel: virar o personagem, FOV e câmera de ombro
Funcionalmente a câmera está pronta, mas o personagem ainda desliza de costas quando anda pra trás da câmera. Em terceira pessoa, o esperado é o modelo virar pra direção do movimento, e virar suave, não teleportar. Eu giro só o nó visual do personagem, nunca o corpo:
@onready var visual: Node3D = $Visual
func virar_personagem(direcao: Vector3, delta: float) -> void:
if direcao.length() < 0.01:
return
var angulo_alvo: float = atan2(-direcao.x, -direcao.z)
visual.rotation.y = lerp_angle(visual.rotation.y, angulo_alvo, 12.0 * delta)
Chame essa função no fim do _physics_process, passando direcao e delta. O lerp_angle interpola respeitando a volta do círculo, então o personagem sempre gira pelo caminho mais curto em vez de dar um giro de 350 graus pra alcançar um alvo a 10 graus de distância. O fator 12.0 multiplicado por delta controla a rapidez da virada: valores entre 8 e 15 cobrem da virada pesada de personagem grandão à resposta seca de jogo de ação.
Dois ajustes finos que rendem muito pelo pouco que custam:
- FOV: a Camera3D vem com 75 graus, que funciona. Descer pra 65 aproxima a ação e dá clima mais cinematográfico; subir pra 85 abre o campo e aumenta a sensação de velocidade. Teste no seu jogo, não existe número mágico.
- Offset de ombro: desloque o SpringArm3D inteiro no eixo X, algo como
position.x = 0.5, e a câmera passa a olhar por cima do ombro do personagem, estilo shooter moderno. Isso tira o modelo do centro exato da tela e melhora a visão do que está à frente.
Erros comuns que eu vejo toda semana
Depois de revisar muito código de aluno e de equipe, esses quatro erros respondem por quase todos os bugs de câmera em terceira pessoa:
- Girar o corpo junto com a câmera. Chamar
rotate_yno CharacterBody3D parece inocente, mas o pivô é filho do corpo: girar o corpo gira a câmera de novo, e o giro dobra. O sintoma é uma câmera que acelera sozinha ao virar. A regra é fixa: mouse gira o pivô, movimento vira o visual comlerp_angle, o corpo não gira nunca. - Clamp errado que vira a câmera de cabeça pra baixo. Limitar
rotation.xcom valores como -60 e 30 semdeg_to_radnão limita nada, porque 30 radianos são quase cinco voltas completas. A câmera passa do zênite, inverte, e o jogador fica olhando o mundo de ponta-cabeça. Clamp sempre em radianos, sempre logo depois de aplicar o movimento do mouse. - Esquecer de capturar o mouse. Sem
MOUSE_MODE_CAPTURED, o cursor escapa da janela no primeiro giro mais longo e a câmera congela, ou pior, o jogador clica fora e o jogo perde o foco. No editor o problema quase não aparece porque a janela de teste é pequena, aí o bug estreia junto com a build. - Spring arm colidindo com o próprio player. Sem o
add_excluded_object(get_rid()), o braço detecta a cápsula do personagem e encolhe até a câmera entrar na cabeça do modelo. O sintoma clássico é a câmera funcionar de longe e surtar quando o personagem anda. Exclua o RID do corpo e confira acollision_maskdo braço.
Próximo passo
Câmera em terceira pessoa é um daqueles sistemas que valem ser montados do zero pelo menos uma vez, porque tocam em tudo: input, hierarquia de nós, transformações e física. Com as quatro camadas desse tutorial funcionando, você tem uma base que aguenta jogo de ação, aventura e RPG sem retrabalho.
Se você quer ir além de copiar snippet e entender por que cada peça funciona, do primeiro nó até publicar um jogo completo, veja o comparativo de melhor curso de Godot que mantemos aqui no CursoGame.Dev. É o caminho estruturado pra transformar esses sistemas isolados em jogos inteiros.
Perguntas frequentes
Como fazer a câmera seguir o personagem no Godot 3D?
Coloque a câmera dentro da hierarquia do personagem: CharacterBody3D com um Node3D como pivô, um SpringArm3D filho do pivô e a Camera3D na ponta. Como a câmera é filha do corpo, ela segue o personagem automaticamente, sem script de perseguição. A rotação do mouse gira só o pivô, então o corpo continua livre pra física.
O que é o SpringArm3D no Godot?
É um nó que funciona como um braço extensível entre o pivô e a câmera. Ele lança um raycast na direção da câmera e, quando detecta uma parede no caminho, encurta o braço e puxa a câmera pra frente do obstáculo. É o jeito nativo do Godot 4 de impedir que a câmera atravesse a geometria do cenário.
Como evitar que a câmera atravesse a parede no Godot?
Use um SpringArm3D entre o pivô e a Camera3D. Configure spring_length com a distância desejada, um margin em torno de 0.25 pra câmera não encostar na superfície e chame add_excluded_object(get_rid()) pra ignorar o colisor do próprio personagem. Com isso o braço encurta sozinho sempre que algo entra entre o player e a câmera.
Como mover o personagem na direção da câmera?
Leia o input com Input.get_vector, monte um Vector3 com esses eixos e multiplique pela transform.basis do pivô da câmera antes de normalizar. Isso converte o input de tela pra direção do mundo: apertar pra frente sempre empurra o personagem pra onde a câmera aponta, não pra um eixo fixo do mapa.
Por que minha câmera vira de cabeça pra baixo no Godot?
Quase sempre é clamp errado na rotação vertical. A propriedade rotation.x trabalha em radianos, então limitar com valores em graus não limita nada e a câmera dá a volta completa. Use clampf com deg_to_rad, algo como deg_to_rad(-60.0) até deg_to_rad(30.0), aplicado toda vez que o mouse se move.


