Parte: 2 — Desarrollo 3D: motores, escenas y transformaciones · Fuente: Documentación oficial de Godot Engine 4.x — Ray-casting ⏱️ Duración estimada: 60 min · Nivel: Intermedio
Aprender a lanzar rayos en 3D de dos formas —con el nodo RayCast3D para chequeos continuos y con intersect_ray para consultas puntuales— y aplicarlos a tres problemas clásicos: seleccionar objetos bajo el ratón, disparar hitscan desde la cámara y detectar el suelo o la pendiente bajo el personaje.
Al finalizar, el alumno podrá:
RayCast3D con target_position, is_colliding(), get_collider() y get_collision_point().PhysicsRayQueryParameters3D y direct_space_state.intersect_ray.project_ray_origin / project_ray_normal.| # | Tema | Por qué importa |
|---|---|---|
| 1 | RayCast3D (nodo) | Chequeo continuo cada frame de física |
| 2 | target_position local | Define largo y dirección del rayo |
| 3 | intersect_ray | Rayos puntuales bajo demanda |
| 4 | Selección con el ratón | Interacción típica de estrategia/edición |
| 5 | Hitscan | Disparo instantáneo sin proyectil físico |
| 6 | Normal de colisión | Orientar impactos y medir pendientes |
| 7 | Capas de colisión en rayos | Filtrar qué puede golpear el rayo |
| 8 | collide_with_areas/bodies | Elegir si el rayo detecta áreas o cuerpos |
target_position (en espacio local). Clave: se actualiza en física; lee resultados en _physics_process.Vector3(0, -2, 0) lanza 2 m hacia abajo.get_collider().direct_space_state. Clave: devuelve un diccionario vacío si no golpea nada.create(from, to) construye lo básico.Godot 4.x con una escena 3D que tenga cámara, suelo y algunos objetos seleccionables (StaticBody3D con BoxShape3D). Ten activo Debug > Visible Collision Shapes. Consulta Ray-casting y la clase PhysicsRayQueryParameters3D.
Haremos disparo hitscan desde la cámara que marca el impacto, y selección de objetos con clic.
1) Marca de impacto. Crea una pequeña escena Marca.tscn (un MeshInstance3D con SphereMesh de radio 0.1, material rojo). La instanciaremos en cada disparo.
2) Disparo hitscan por código. En el controlador con Camera3D, adjunta:
extends Node3D
@export var marca: PackedScene
@export var alcance := 100.0
@onready var camara: Camera3D = $Camera3D
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
_disparar_hitscan(event.position)
func _disparar_hitscan(pos_raton: Vector2) -> void:
var espacio := get_world_3d().direct_space_state
var origen := camara.project_ray_origin(pos_raton)
var destino := origen + camara.project_ray_normal(pos_raton) * alcance
var consulta := PhysicsRayQueryParameters3D.create(origen, destino)
consulta.collision_mask = 1 # solo capa World, por ejemplo
var res := espacio.intersect_ray(consulta)
if res: # diccionario no vacío = hubo impacto
var m := marca.instantiate() as Node3D
get_tree().current_scene.add_child(m)
m.global_position = res.position
print("Impacto en ", res.collider.name, " normal ", res.normal)
Al hacer clic, aparece una esfera roja exactamente donde el rayo golpea, y la consola imprime el objeto y la normal.
3) Selección con clic (resaltar). Reutiliza el rayo para resaltar el objeto seleccionado. Da a los seleccionables un método resaltar(activo). En el controlador:
var seleccionado: Node = null
func _seleccionar(pos_raton: Vector2) -> void:
var espacio := get_world_3d().direct_space_state
var origen := camara.project_ray_origin(pos_raton)
var destino := origen + camara.project_ray_normal(pos_raton) * alcance
var res := espacio.intersect_ray(PhysicsRayQueryParameters3D.create(origen, destino))
if seleccionado and seleccionado.has_method("resaltar"):
seleccionado.resaltar(false)
seleccionado = null
if res and res.collider.has_method("resaltar"):
seleccionado = res.collider
seleccionado.resaltar(true)
4) Detección de suelo con nodo RayCast3D. En el personaje, añade un RayCast3D hijo con target_position = Vector3(0, -1.2, 0) (algo más largo que media cápsula). En su script:
@onready var rayo_suelo: RayCast3D = $RayCast3D
func _physics_process(_delta: float) -> void:
if rayo_suelo.is_colliding():
var normal := rayo_suelo.get_collision_normal()
# Ángulo de la pendiente respecto a la vertical
var pendiente := rad_to_deg(normal.angle_to(Vector3.UP))
if pendiente > 40.0:
print("Pendiente empinada: ", round(pendiente), "°")
5) Observable. Disparas y aparecen marcas en muros y suelo con la normal correcta; al hacer clic sobre una caja se resalta; al pisar una rampa muy inclinada, la consola avisa de la pendiente.
look_at con la normal).consulta.exclude = [self] para que el rayo no golpee al propio tirador.collision_mask del disparo para que solo golpee enemigos y no el escenario.RayCast3D nodo fijo delante de la cámara y compara continuidad vs consulta puntual.collide_with_areas en una consulta y detecta un Area3D como zona de disparo.origen.distance_to(res.position)) en un Label.Implementa una "linterna de inspección": un rayo desde la cámara que, cada frame, muestre en un Label el nombre del objeto que hay en el centro de la pantalla y la distancia a él; al hacer clic, coloca una marca persistente en ese punto.
Criterio de aceptación: apuntando al centro, el Label actualiza en tiempo real el nombre y la distancia del objeto enfocado; al hacer clic se deja una marca exactamente en la superficie apuntada, orientada según su normal.
| Síntoma | Causa y arreglo |
|---|---|
intersect_ray siempre vacío |
Origen/destino iguales o máscara sin capa. Verifica dirección y collision_mask. |
RayCast3D no detecta nada |
enabled en false o rayo demasiado corto. Actívalo y alarga target_position. |
| El rayo golpea al propio jugador | No excluyes el cuerpo. Usa exclude = [self] o ajusta capas. |
| La marca aparece en el aire | Lees res.position cuando el diccionario está vacío. Comprueba if res: primero. |
| Selección con clic errática | Usas coordenadas de mundo en lugar de event.position (pantalla). Pasa la posición del ratón. |
¿RayCast3D o intersect_ray? El nodo es cómodo para chequeos fijos y continuos (suelo, pared frontal); intersect_ray es mejor para eventos puntuales como un disparo.
¿Por qué el rayo atraviesa un Area3D? Por defecto los rayos ignoran áreas; activa collide_with_areas.
¿El nodo RayCast3D se actualiza al instante? Se actualiza en el paso de física; si necesitas un valor inmediato tras mover, llama force_raycast_update().
¿Cómo limito qué golpea? Con collision_mask, igual que en los cuerpos; usa las capas nombradas de la clase 057.
Clase 058 - RigidBody3D, fuerzas e interacción física
Clase 060 - Animación 3D: esqueletos, skinning y AnimationPlayer