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

Введение

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

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

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

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

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

В итоге пространство RID может быть использовано в PhysicsServer и Physics2DServer соответственно для 3D и 2D .

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

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

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

Используйте следующий код в 2D:

func _physics_process(delta):
    var space_rid = get_world_2d().space
    var space_state = Physics2DServer.space_get_direct_state(space_rid)

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

func _physics_process(delta):
    var space_state = get_world_2d().direct_space_state

И для 3D:

func _physics_process(delta):
    var space_state = get_world().direct_space_state

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

Для выполнения такого запроса для 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))

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

if result:
    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])

Массивы исключений могут содержать объекты или 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)

См. Пример кода для получения дополнительной информации о том, как установить маску столкновений.

Трассировка лучей из экрана в 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

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