Voltar para o Blog
Quest Log

Raycast 3D no Godot 4: Detecção, Mira e Linha de Visão

Personagem 3D em um cenário com uma linha vermelha de raio saindo da câmera até acertar um alvo à frente

Guia prático de raycast 3D no Godot 4: RayCast3D e PhysicsDirectSpaceState3D para detectar chão, tiro hitscan, linha de visão de inimigo e interação.

Raycast é a ferramenta que responde uma pergunta simples: tem alguma coisa naquela direção? A partir de um ponto, você dispara uma linha reta invisível e o motor te diz o que ela acerta primeiro. Parece pouco, mas é a base de meio jogo 3D: saber se tem chão sob o personagem, acertar um tiro instantâneo, checar se o inimigo enxerga o jogador, decidir com qual objeto o botão de interação vai conversar. Neste tutorial eu mostro raycast 3D no Godot 4 na prática, com os dois caminhos que o motor oferece: o node RayCast3D, para raios fixos, e o raycast por código via PhysicsDirectSpaceState3D, para raios que mudam a cada disparo. Tudo em GDScript tipado, sem API inventada.

Se você já mexeu com o RayCast2D, boa parte do raciocínio se repete. A diferença é dimensional e vale deixar clara desde já: no 3D o target_position é um Vector3 em coordenadas locais, o ponto de colisão volta como Vector3 global, e a collision_mask aponta para as camadas de física 3D, que são um conjunto separado das camadas 2D do projeto. Errar a camada é o motivo número um de raio que atravessa parede ou que não acerta nada.

O node RayCast3D: o sensor fixo

O RayCast3D é um node que você adiciona como filho de algo e que dispara um raio da própria posição até um ponto de destino. Ele é a escolha certa quando a direção do raio é constante em relação ao dono: um sensor apontado sempre para baixo, um detector à frente do personagem, um feeler lateral.

As propriedades que importam:

  • target_position: um Vector3 em coordenadas locais que define até onde o raio vai. Vector3(0, -1.5, 0) dispara 1,5 metro para baixo. É a ponta do raio, não a direção normalizada.
  • enabled: liga ou desliga o raio. Desligado, ele não consome nada.
  • collision_mask: as camadas de física 3D que o raio enxerga. Se a máscara não inclui a camada do objeto, o raio passa reto por ele.
  • exclude_parent: quando ligado (padrão), o raio ignora o corpo do node pai. Evita que o sensor do personagem acerte o próprio personagem.

E os métodos que você usa no dia a dia:

  • is_colliding(): retorna true se o raio acertou algo.
  • get_collider(): devolve o objeto atingido (o corpo, a área), ou null se nada.
  • get_collision_point(): o ponto exato do acerto, como Vector3 em coordenadas globais.
  • get_collision_normal(): a normal da superfície atingida, um Vector3 que aponta para fora dela. Serve para saber a inclinação do que você acertou.

Um detalhe de timing que economiza horas de depuração: o RayCast3D atualiza no passo de física, não a cada frame de renderização. Dentro do _physics_process o resultado do frame já está calculado e você lê à vontade. Se precisar do resultado no mesmo frame depois de mover o raio, chame force_raycast_update() para forçar a leitura imediata.

Caso 1: detectar o chão sob o personagem

O is_on_floor() do CharacterBody3D resolve a maioria dos casos, mas às vezes você quer saber se tem chão logo à frente antes de dar um passo, ou detectar uma beirada. Um RayCast3D apontado para baixo faz isso. Estrutura de cena:

Personagem (CharacterBody3D)
├── CollisionShape3D
└── RaioChao (RayCast3D)

No Inspector do RaioChao, defina target_position como Vector3(0, -1.5, 0) e a collision_mask na camada do cenário. No script:

extends CharacterBody3D

@onready var raio_chao: RayCast3D = $RaioChao

func _physics_process(_delta: float) -> void:
    if raio_chao.is_colliding():
        var chao: Object = raio_chao.get_collider()
        var ponto: Vector3 = raio_chao.get_collision_point()
        var normal: Vector3 = raio_chao.get_collision_normal()
        print("Chao detectado em ", ponto, " inclinacao ", normal)
    else:
        print("Sem chao a frente: beirada ou buraco")

A normal aqui vale ouro: se ela não estiver perto de Vector3.UP, você está em cima de uma rampa íngreme demais e pode tratar isso como parede em vez de piso.

Raycast por código com PhysicsDirectSpaceState3D

Nem todo raio tem posição fixa. Um tiro sai da câmera na direção exata para onde o jogador está olhando, e essa direção muda a cada frame. Criar e destruir um node RayCast3D para cada disparo seria desperdício. Para esses casos, você consulta o espaço de física diretamente.

O objeto que faz isso é o PhysicsDirectSpaceState3D, acessível por get_world_3d().direct_space_state. Você monta uma consulta com PhysicsRayQueryParameters3D.create(origem, fim), passando dois pontos em coordenadas globais, e chama intersect_ray(query). O retorno é um Dictionary: vazio se o raio não bateu em nada, ou preenchido com as chaves collider, position, normal e rid se bateu.

Caso 2: tiro hitscan na direção da câmera

Hitscan é o tiro instantâneo: no momento em que você aperta o gatilho, o motor calcula na hora o que a linha acerta, sem projétil viajando pela cena. É o modelo de tiro de boa parte dos FPS. A origem é a posição da câmera e o fim é esse ponto projetado para frente pela distância máxima do tiro.

extends Node3D

@onready var camera: Camera3D = $Camera3D

const ALCANCE: float = 1000.0

func atirar() -> void:
    var space_state: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
    var origem: Vector3 = camera.global_position
    var direcao: Vector3 = -camera.global_transform.basis.z
    var fim: Vector3 = origem + direcao * ALCANCE

    var query: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(origem, fim)
    query.exclude = [self]
    query.collision_mask = 1

    var resultado: Dictionary = space_state.intersect_ray(query)
    if resultado.is_empty():
        print("Tiro no vazio")
        return

    var alvo: Object = resultado.collider
    var ponto: Vector3 = resultado.position
    print("Acertou ", alvo.name, " em ", ponto)

    if alvo.has_method("levar_dano"):
        alvo.levar_dano(25)

Três pontos merecem atenção. O -camera.global_transform.basis.z é a direção "para frente" da câmera: no Godot o eixo Z local aponta para trás, então a frente é o Z negativo. O query.exclude = [self] evita que o raio bata no próprio corpo do atirador logo na saída. E o has_method("levar_dano") é uma checagem defensiva: você só chama o dano se o objeto atingido de fato souber receber, evitando erro quando o tiro acerta uma parede.

Esse padrão de câmera em primeira pessoa combina direto com o movimento. Se você ainda não montou a base de andar e olhar, o passo a passo está no tutorial de controle em primeira pessoa no Godot 3D, e o raycast entra em cima dele como a camada de tiro e interação.

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

Caso 3: linha de visão do inimigo

Aqui o raycast vira comportamento de IA. A regra que dá vida a um inimigo é simples: ele só persegue se enxerga o jogador. Sem isso, o bicho corre atrás de você através de paredes, o que quebra qualquer sensação de esconde-esconde. A linha de visão é um raio do inimigo até o jogador que precisa chegar limpo, sem parede no caminho.

O ponto crítico é a collision_mask. O raio de visão tem que colidir com duas coisas: o jogador e as paredes. Se ele só enxerga a camada do jogador, passa reto pelo cenário e sempre acha que tem visão. Com as paredes na máscara também, o raio bate na parede primeiro quando ela está no caminho, e o get_collider() devolve a parede, não o jogador. A lógica de "tenho visão" é: o raio acertou algo e esse algo é o jogador.

Usando um RayCast3D como node filho do inimigo, com a collision_mask incluindo a camada do jogador e a das paredes:

extends CharacterBody3D

@onready var raio_visao: RayCast3D = $RaioVisao
@export var jogador: Node3D
@export var alcance_visao: float = 20.0

func tem_visao_do_jogador() -> bool:
    if jogador == null:
        return false

    var distancia: float = global_position.distance_to(jogador.global_position)
    if distancia > alcance_visao:
        return false

    # aponta o raio pro jogador em coordenadas locais
    raio_visao.target_position = raio_visao.to_local(jogador.global_position)
    raio_visao.force_raycast_update()

    if raio_visao.is_colliding():
        return raio_visao.get_collider() == jogador
    return false

func _physics_process(_delta: float) -> void:
    if tem_visao_do_jogador():
        perseguir()
    else:
        patrulhar()

func perseguir() -> void:
    pass

func patrulhar() -> void:
    pass

Repare no force_raycast_update(): como eu reposicionei o target_position no mesmo frame, preciso forçar a atualização antes de ler is_colliding(), senão leio o resultado do frame anterior, de quando o raio ainda apontava para outro lugar. O to_local converte a posição global do jogador para o sistema de coordenadas do raio, que é o que o target_position espera.

O bloco de perseguição em si eu deixei vazio de propósito. Ligar a visão ao deslocamento por um mapa com paredes é trabalho do sistema de navegação, e isso tem tutorial dedicado: veja como fazer o inimigo se mover desviando de obstáculos no guia de pathfinding e perseguição 3D com NavigationAgent3D. Junte os dois e você tem o padrão clássico: o inimigo patrulha, vê o jogador via raycast, e então parte para cima usando pathfinding. Se quiser subir o nível do comportamento, com estados de alerta, última posição conhecida e busca, o post sobre inteligência artificial de inimigos no Godot cobre a máquina de estados que orquestra tudo isso.

Um detalhe honesto sobre linha de visão: um raio único mira o centro do jogador. Se o jogador esconde só o corpo mas deixa a cabeça de fora, o raio para o centro bate na parede e o inimigo não vê, mesmo com a cabeça exposta. Para um cone de visão mais justo, dispare alguns raios para pontos diferentes do corpo (pés, tronco, cabeça) e considere que tem visão se qualquer um deles chega limpo. Para a maioria dos projetos indie, um raio para o centro já entrega um comportamento convincente.

Caso 4: interação por foco

O último padrão é o de olhar para um objeto e apertar um botão: a porta que abre, o item que você pega, a alavanca que puxa. É o mesmo raycast do tiro, mas com alcance curto e sem dano, e normalmente rodando todo frame para atualizar o texto de "Apertar E para abrir" na tela.

extends Node3D

@onready var camera: Camera3D = $Camera3D

const ALCANCE_INTERACAO: float = 3.0

var alvo_atual: Node3D = null

func _physics_process(_delta: float) -> void:
    alvo_atual = detectar_interativo()

func detectar_interativo() -> Node3D:
    var space_state: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
    var origem: Vector3 = camera.global_position
    var direcao: Vector3 = -camera.global_transform.basis.z
    var fim: Vector3 = origem + direcao * ALCANCE_INTERACAO

    var query: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(origem, fim)
    query.exclude = [self]

    var resultado: Dictionary = space_state.intersect_ray(query)
    if resultado.is_empty():
        return null

    var alvo: Object = resultado.collider
    if alvo is Node3D and alvo.has_method("interagir"):
        return alvo
    return null

func _input(event: InputEvent) -> void:
    if event.is_action_pressed("interagir") and alvo_atual != null:
        alvo_atual.interagir()

O has_method("interagir") faz o filtro: só vira alvo o objeto que sabe reagir. Uma parede comum entra no resultado do raio, mas não tem o método, então detectar_interativo devolve null e nenhum prompt aparece. Cada objeto interativo (porta, item, alavanca) implementa seu próprio interagir(), e o jogador não precisa saber com qual tipo está lidando. É polimorfismo resolvendo o design de interação sem nenhum if gigante checando tipos.

Fechando

Raycast 3D no Godot 4 é uma daquelas ferramentas de custo baixo e retorno alto: quatro dos sistemas mais comuns de um jogo (sensor de chão, tiro, visão de inimigo e interação) saem do mesmo conceito de disparar uma linha e ler o que ela acerta. A decisão principal é entre o node RayCast3D, para raios com posição fixa em relação a algo, e o intersect_ray do PhysicsDirectSpaceState3D, para raios pontuais que mudam a cada disparo. Depois disso, o que separa o raio que funciona do raio que atravessa parede é quase sempre a collision_mask nas camadas certas e, quando você mexe no raio no mesmo frame, o force_raycast_update. Comece pelo sensor de chão, que é o mais simples de ver na tela, e vá empilhando os outros três em cima. Bons projetos na CursoGame.Dev.

Perguntas frequentes

Qual a diferença entre RayCast3D e RayCast2D no Godot 4?

A lógica é a mesma, mas o RayCast3D trabalha em três dimensões: o target_position é um Vector3 (não Vector2), o ponto de colisão volta como Vector3 global, e a collision_mask aponta pras camadas de física 3D, que são separadas das camadas 2D. Fora isso, os métodos is_colliding(), get_collider() e get_collision_point() funcionam do mesmo jeito nas duas versões.

Preciso chamar force_raycast_update() sempre que uso um RayCast3D?

Não. O RayCast3D atualiza sozinho a cada passo de física, então dentro do _physics_process o resultado do frame já está pronto. Você só chama force_raycast_update() quando move o raio e precisa ler o resultado no mesmo frame, antes do próximo passo de física, como num tiro instantâneo disparado dentro do _input.

Quando usar o RayCast3D como node e quando usar intersect_ray por código?

Use o node RayCast3D quando o raio tem posição e direção fixas em relação a algo, tipo um sensor de chão embaixo do personagem. Use o intersect_ray do PhysicsDirectSpaceState3D quando o raio é pontual e varia a cada disparo, como um tiro na direção exata da câmera, onde criar e destruir um node seria desperdício.

Por que meu raycast bate no próprio personagem que atirou?

Porque a origem do raio está dentro do CollisionShape3D do atirador. No node, ligue o exclude_parent ou preencha o array add_exception. No raycast por código, passe query.exclude = [self] (ou o RID do corpo) no PhysicsRayQueryParameters3D pra ignorar o próprio corpo na consulta.

O raycast atravessa paredes na hora de checar linha de visão. O que fazer?

Isso é sinal de collision_mask errada. O raio de visão precisa colidir tanto com o jogador quanto com as paredes: se ele só enxerga a camada do jogador, passa reto pelo cenário e acha que sempre tem visão. Coloque as paredes numa camada sólida e inclua essa camada na collision_mask do raio de visão.