Voltar para o Blog
Quest Log

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

Personagem 3D visto por cima do ombro, com a câmera flutuando atrás dele em um cenário de teste com paredes e caixas de colisão

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.

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

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:

  1. Girar o corpo junto com a câmera. Chamar rotate_y no 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 com lerp_angle, o corpo não gira nunca.
  2. Clamp errado que vira a câmera de cabeça pra baixo. Limitar rotation.x com valores como -60 e 30 sem deg_to_rad nã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.
  3. 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.
  4. 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 a collision_mask do 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.