Ray casting

Introducción

Una de las tareas más comunes en el desarrollo de juegos es lanzar un rayo (o un objeto con forma personalizada) y comprobar con qué choca. Esto permite que ocurran comportamientos complejos (IA, etcétera). Este tutorial explicará cómo hacer esto en 2D y 3D.

Godot almacena toda la información de bajo nivel del juego en servidores, mientras que la escena es solo una interfaz. Como tal, el ray casting es generalmente una tarea de bajo nivel. Para raycasts simples, los nodos RayCast y RayCast2D funcionarán, ya que devolverán a cada cuadro el resultado de un raycast.

Sin embargo, muchas veces el ray-casting debe ser un proceso más interactivo, así que es necesario que exista una forma de hacer esto por código.

Espacio

En el mundo físico, Godot almacena toda la información de bajo nivel sobre colisiones y física en un espacio. El espacio 2d actual (para Física 2D) se puede obtener accediendo a CanvasItem.get_world_2d().space. Para 3D, accediendo a Spatial.get_world().space.

El espacio resultante RID puede usarse en PhysicsServer y Physics2DServer respectivamente para 3D y 2D.

Accediendo al espacio

La física de Godot se ejecuta de manera predeterminada en el mismo hilo que la lógica del juego, pero puede configurarse para que se ejecute en un hilo separado y así funcione de manera más eficiente. Debido a esto, el único momento en que acceder al espacio es seguro es durante la devolución de llamada (callback) de Node._physics_process(). Acceder desde fuera de esta función puede provocar un error debido a que el espacio está bloqueado.

Para realizar consultas en el espacio de física, se deben utilizar Physics2DDirectSpaceState y PhysicsDirectSpaceState.

Utilice el siguiente código en 2D:

func _physics_process(delta):
    var space_rid = get_world_2d().space
    var space_state = Physics2DServer.space_get_direct_state(space_rid)
public override void _PhysicsProcess(float delta)
{
    var spaceRid = GetWorld2d().Space;
    var spaceState = Physics2DServer.SpaceGetDirectState(spaceRid);
}

O más directamente:

func _physics_process(delta):
    var space_state = get_world_2d().direct_space_state
public override void _PhysicsProcess(float delta)
{
    var spaceState = GetWorld2d().DirectSpaceState;
}

Y en 3D:

func _physics_process(delta):
    var space_state = get_world().direct_space_state
public override void _PhysicsProcess(float delta)
{
    var spaceState = GetWorld().DirectSpaceState;
}

Consulta de Raycast

Para realizar una consulta de raycast 2D se puede utilizar el método Physics2DDirectSpaceState.intersect_ray(). Por ejemplo:

func _physics_process(delta):
    var space_state = get_world_2d().direct_space_state
    # use global coordinates, not local to node
    var result = space_state.intersect_ray(Vector2(0, 0), Vector2(50, 100))
public override void _PhysicsProcess(float delta)
{
    var spaceState = GetWorld2d().DirectSpaceState;
    // use global coordinates, not local to node
    var result = spaceState.IntersectRay(new Vector2(), new Vector2(50, 100));
}

El resultado es un diccionario. Si el rayo no le dio a nada, el diccionario estará vacío. Si chocó con algo, contendrá información de colisión:

if result:
    print("Hit at point: ", result.position)
if (result.Count > 0)
    GD.Print("Hit at point: ", result["position"]);

El diccionario resultante result cuando ocurre una colisión contiene los siguientes datos:

{
   position: Vector2 # point in world space for collision
   normal: Vector2 # normal in world space for collision
   collider: Object # Object collided or null (if unassociated)
   collider_id: ObjectID # Object it collided against
   rid: RID # RID it collided against
   shape: int # shape index of collider
   metadata: Variant() # metadata of collider
}

Los datos son similares en el espacio 3D, usando coordenadas Vector3.

Excepciones de colisión

Un caso de uso común de ray casting es un permitir que un personaje reúna datos sobre el mundo que lo rodea. Un problema que surge en estos casos es que ese mismo personaje tiene un Colisionador, por lo que el rayo sólo detectará el Colisionador de su padre, como se muestra en la siguiente imagen:

../../_images/raycast_falsepositive.png

Para evitar la autointersección, la función intersect_ray() puede recibir un tercer parámetro opcional que es una matriz de excepciones. Aquí se muestra un ejemplo de cómo utilizarlo desde un KinematicBody2D o desde cualquier otro nodo de objeto de colisión:

extends KinematicBody2D

func _physics_process(delta):
    var space_state = get_world_2d().direct_space_state
    var result = space_state.intersect_ray(global_position, enemy_position, [self])
class Body : KinematicBody2D
{
    public override void _PhysicsProcess(float delta)
    {
        var spaceState = GetWorld2d().DirectSpaceState;
        var result = spaceState.IntersectRay(globalPosition, enemyPosition, new object[] { this });
    }
}

La matriz de excepciones puede contener objetos o identificadores relativos (RIDs).

Máscara de colisión

Mientras que el método de excepciones funciona bien para excluir el cuerpo padre, resulta muy incómodo si se necesita una lista grande y dinámica de las excepciones. En ese caso, es mucho más eficiente utilizar el sistema de máscaras/capas de colisión.

El cuarto argumento opcional para intersect_ray() es una máscara de colisión. Por ejemplo, para usar la misma máscara que el cuerpo padre, usa la variable collision_mask:

extends KinematicBody2D

func _physics_process(delta):
    var space_state = get_world().direct_space_state
    var result = space_state.intersect_ray(global_position, enemy_position,
                            [self], collision_mask)
class Body : KinematicBody2D
{
    public override void _PhysicsProcess(float delta)
    {
        var spaceState = GetWorld2d().DirectSpaceState;
        var result = spaceState.IntersectRay(globalPosition, enemyPosition,
                        new object[] { this }, CollisionMask);
    }
}

Ray casting 3D desde la pantalla

Lanzar un rayo de la pantalla al espacio de físicas 3D es útil para recoger objetos. No hay demasiada necesidad de hacer esto porque CollisionObject tiene una señal de «input_event» que permite saber cuando ha hecho clic, pero en el caso de querer hacerlo manualmente, esta es la forma.

Para lanzar un rayo desde la pantalla, se necesita un nodo Camera. Una Camera (cámara) puede estar en dos modos de proyección: perspectiva y ortogonal. Debido a esto, deben obtenerse el origen del rayo y su dirección. Esto es porque en el modo ortogonal origin (origen) cambia, mientras que en el modo perspectiva normal cambia:

../../_images/raycast_projection.png

Para obtenerlo mediante una cámara, se puede utilizar el siguiente código:

const ray_length = 1000

func _input(event):
    if event is InputEventMouseButton and event.pressed and event.button_index == 1:
          var camera = $Camera
          var from = camera.project_ray_origin(event.position)
          var to = from + camera.project_ray_normal(event.position) * ray_length
private const float rayLength = 1000;

public override void _Input(InputEvent @event)
{
    if (@event is InputEventMouseButton eventMouseButton && eventMouseButton.Pressed && eventMouseButton.ButtonIndex == 1)
    {
        var camera = (Camera)GetNode("Camera");
        var from = camera.ProjectRayOrigin(eventMouseButton.Position);
        var to = from + camera.ProjectRayNormal(eventMouseButton.Position) * rayLength;
    }
}

Recuerde que durante _input(), el espacio puede estar bloqueado, por lo que en la práctica esta consulta debe ejecutarse en _physics_process().