Трассировка лучей¶
Введение¶
Одна из самых частых задач в разработке игр — это испускание луча (или собственного объекта с формой) и проверка того, что он пересекает. Это позволяет создавать сложные поведения, ИИ и т.д. В этом учебном пособии объясняется, как это сделать в 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)
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.
Исключения столкновений¶
Распространенный способ использования трассировки лучей это сбор данных об окружающем мире для персонажа. Одна из проблем с этим возникает когда у персонажа есть коллайдер, и луч будет сталкиваться с ним, как показано на следующем рисунке:
Для избежания самопересечения функция 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 Godot.Collections.Array { 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 Godot.Collections.Array { this }, CollisionMask);
}
}
См. Пример кода для получения дополнительной информации о том, как установить маску столкновений.
Трассировка лучей из экрана в 3D¶
Трассировка луча из экранного в 3D физическое пространство полезно для выбора объекта. Не требуется много усилий для выполнения этого поскольку CollisionObject имеет сигнал "input_event" который способен уведомить вас при клике, но в случае если вам понадобится сделать это вручную, здесь показано как.
Для трассировки луча с экрана, вам нужен узел ref:Camera <class_Camera>. Camera
может быть в двух режимах проекции: перспективном и ортогональном. Из-за этого, необходимо предоставить и начальную точку (origin
) луча и направление (нормаль). Начальная точка (origin
) изменяется в ортогональном режиме, а нормаль изменяется в перспективном:
Для его получения с помощью камеры можно использовать следующий код:
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 = GetNode<Camera>("Camera");
var from = camera.ProjectRayOrigin(eventMouseButton.Position);
var to = from + camera.ProjectRayNormal(eventMouseButton.Position) * rayLength;
}
}
Помните, что во время выполнения _input()
пространство может быть заблокировано, поэтому на практике этот запрос должен выполняться в _physics_process()
.