Трассировка лучей

Введение

Одна из самых частых задач в разработке игр это трассировка луча (или собственного объекта с формой) и проверка того что он пересекает. Это позволяет создавать сложные поведения, ИИ, итд. Это руководство объясняет как делать это в 2D и 3D пространстве.

Godot сохраняет всю низкоуровневую игровую информацию в серверах, тогда как сцена это просто внешний интерфейс. Таким образом, трассировка лучей это низкоуровневый процесс. Для простых трассировок, можно использовать такие узлы как RayCast и RayCast2D, так как они будут возвращать результат в каждом кадре.

Во многих случаях, однако, трассировка лучей требует более сложной обработки так что должен существовать способ делать это через код.

Пространство

В пространстве физики, Godot хранит все низкоуровневые столкновения и физическую информацию в пространстве. Текущее 2d пространство (для 2D физики) может быть получено с помощью CanvasItem.get_world_2d().space. Для 3D, это Spatial.get_world().space.

RID и Physics2DServer для 3D и 2D соответственно.

Доступ к пространству

По умолчанию физика Godot запускается в том же потоке что и игровая логика, но также может быть запущена в отдельном потоку для более эффективной работы. В связи с этим, только доступ к пространству безопасен в функции обратного вызова Node._physics_process(). Доступ вне этой функции приведет к ошибке из-за того что пространство закрыто.

Для выполнения запросов в физическом пространстве, нужно использовать Physics2DDirectSpaceState and PhysicsDirectSpaceState.

Используйте следующий код в 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);
}

Или более прямо:

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

И для 3D:

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

Запрос трассировки лучей

Для выполнения такого запроса для 2D, можно использовать метод Physics2DDirectSpaceState.intersect_ray(). Например:

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));
}

Результатом будет словарь. Если луч не столкнулся ни с чем, словарь будет пуст. Если же столкнулся, он будет содержать информацию о столкновении:

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

Словарь result после столкновения будет содержать следующие данные:

{
   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
}

Те же данные подобны и для 3D пространства, но используют координаты в Vector3.

Исключения столкновений

Распространенный способ использования трассировки лучей это сбор данных об окружающем мире для персонажа. Одна из проблем с этим возникает когда у персонажа есть коллайдер, и луч будет сталкиваться с ним, как показано на следующем рисунке:

../../_images/raycast_falsepositive.png

Для избежания само-пересечения, функция intersect_ray() может принимать третий необязательный параметр который представляет массив исключений. Это пример как использовать это для KinematicBody2D или любого другого узла столкновений:

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 });
    }
}

Массивы исключения могут содержать объекты или RIDs.

Маска столкновений

Метод исключений хорошо работает когда нужно исключить родительское тело, но очень неудобен если это нужно сделать для больших и/или динамических списков исключений. Для этого случая, гораздо эффективнее использовать систему слоев/масок столкновений.

Необязательный четвертый параметр для intersect_ray() это маска столкновений. Например, для использования той же маски что и в родительском теле, используйте переменную 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);
    }
}

Трассировка лучей из экрана в 3D

Трассировка луча из экранного в 3D физическое пространство полезно для выбора объекта. Не требуется много усилий для выполнения этого поскольку CollisionObject имеет сигнал «input_event» который способен уведомить вас при клике, но в случае если вам понадобится сделать это вручную, здесь показано как.

Для трассировки луча с экрана, вам нужен узел ref:Camera <class_Camera>. Camera может быть в двух режимах проекции: перспективном и ортогональном. Из-за этого, необходимо предоставить и начальную точку (origin) луча и направление (нормаль). Начальная точка (origin) изменяется в ортогональном режиме, а нормаль изменяется в перспективном:

../../_images/raycast_projection.png

Для получения его через камеру, можно использовать следующий код:

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;
    }
}

Запомните что в течении _input(), пространство может быть закрыто, так что на практике этот запрос должен запускаться в _physics_process().