Трассировка лучей
Введение
Одна из самых частых задач в разработке игр — это испускание луча (или собственного объекта с формой) и проверка того, что он пересекает. Это позволяет создавать сложные поведения, ИИ и т.д. В этом учебном пособии объясняется, как это сделать в 2D и 3D пространстве.
Godot хранит всю низкоуровневую игровую информацию на серверах, тогда как сцена является лишь внешним интерфейсом. Таким образом, трассировка лучей (ray casting), как правило, является задачей более низкого уровня. Для простых raycasts подойдут такие узлы, как RayCast3D и RayCast2D, поскольку они возвращают каждый кадр результат raycast.
Во многих случаях, однако, трассировка лучей требует более сложной обработки так что должен существовать способ делать это через код.
Пространство
В мире физики, Godot хранит всю низкоуровневую информацию о столкновениях и физике в space. Текущее двумерное пространство (для двумерной физики) можно получить, обратившись к CanvasItem.get_world_2d().space. Соответственно для 3D это Node3D.get_world_3d().space.
Полученное пространство RID можно использовать в PhysicsServer3D и PhysicsServer2D соответственно для 3D и 2D.
Доступ к пространству
Физика Godot по умолчанию выполняется в том же потоке, что и игровая логика, но может быть настроена на выполнение в отдельном потоке для повышения эффективности. В связи с этим безопасный доступ к пространству возможен только во время обратного вызова Node._physics_process(). Попытка доступа к пространству извне этой функции может привести к ошибке из-за блокировки пространства.
Для выполнения запросов в физическое пространство необходимо использовать PhysicsDirectSpaceState2D и PhysicsDirectSpaceState3D.
Используйте следующий код в 2D:
func _physics_process(delta):
var space_rid = get_world_2d().space
var space_state = PhysicsServer2D.space_get_direct_state(space_rid)
public override void _PhysicsProcess(double delta)
{
var spaceRid = GetWorld2D().Space;
var spaceState = PhysicsServer2D.SpaceGetDirectState(spaceRid);
}
Или более прямо:
func _physics_process(delta):
var space_state = get_world_2d().direct_space_state
public override void _PhysicsProcess(double delta)
{
var spaceState = GetWorld2D().DirectSpaceState;
}
И для 3D:
func _physics_process(delta):
var space_state = get_world_3d().direct_space_state
public override void _PhysicsProcess(double delta)
{
var spaceState = GetWorld3D().DirectSpaceState;
}
Запрос трассировки лучей
Для выполнения двумерного запроса raycast можно использовать метод PhysicsDirectSpaceState2D.intersect_ray(). Например:
func _physics_process(delta):
var space_state = get_world_2d().direct_space_state
# use global coordinates, not local to node
var query = PhysicsRayQueryParameters2D.create(Vector2(0, 0), Vector2(50, 100))
var result = space_state.intersect_ray(query)
public override void _PhysicsProcess(double delta)
{
var spaceState = GetWorld2D().DirectSpaceState;
// use global coordinates, not local to node
var query = PhysicsRayQueryParameters2D.Create(Vector2.Zero, new Vector2(50, 100));
var result = spaceState.IntersectRay(query);
}
Результатом будет словарь. Если луч ни с чем не столкнулся, словарь будет пуст. Если же столкнулся, то будет содержать информацию о столкновении:
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
}
Аналогичные данные представлены в трёхмерном пространстве с использованием координат Vector3. Обратите внимание: для включения столкновений с Area3D логический параметр collide_with_areas должен быть установлен в значение true.
const RAY_LENGTH = 1000
func _physics_process(delta):
var space_state = get_world_3d().direct_space_state
var cam = $Camera3D
var mousepos = get_viewport().get_mouse_position()
var origin = cam.project_ray_origin(mousepos)
var end = origin + cam.project_ray_normal(mousepos) * RAY_LENGTH
var query = PhysicsRayQueryParameters3D.create(origin, end)
query.collide_with_areas = true
var result = space_state.intersect_ray(query)
private const int RayLength = 1000;
public override void _PhysicsProcess(double delta)
{
var spaceState = GetWorld3D().DirectSpaceState;
var cam = GetNode<Camera3D>("Camera3D");
var mousePos = GetViewport().GetMousePosition();
var origin = cam.ProjectRayOrigin(mousePos);
var end = origin + cam.ProjectRayNormal(mousePos) * RayLength;
var query = PhysicsRayQueryParameters3D.Create(origin, end);
query.CollideWithAreas = true;
var result = spaceState.IntersectRay(query);
}
Исключения столкновений
Распространенный способ использования трассировки лучей это сбор данных об окружающем мире для персонажа. Одна из проблем с этим возникает когда у персонажа есть коллайдер, и луч будет сталкиваться с ним, как показано на следующем рисунке:
Чтобы избежать самопересечения, объект параметров intersect_ray() может принимать массив исключений через своё свойство exclude. Вот пример использования этого из узла CharacterBody2D или любого другого объекта столкновения:
extends CharacterBody2D
func _physics_process(delta):
var space_state = get_world_2d().direct_space_state
var query = PhysicsRayQueryParameters2D.create(global_position, player_position)
query.exclude = [self]
var result = space_state.intersect_ray(query)
using Godot;
public partial class MyCharacterBody2D : CharacterBody2D
{
public override void _PhysicsProcess(double delta)
{
var spaceState = GetWorld2D().DirectSpaceState;
var query = PhysicsRayQueryParameters2D.Create(globalPosition, playerPosition);
query.Exclude = [GetRid()];
var result = spaceState.IntersectRay(query);
}
}
Массивы исключений могут содержать объекты или RIDs.
Маска столкновения
Метод исключений хорошо работает, когда нужно исключить родительское тело, но очень неудобен, если это нужно сделать для больших и/или динамических списков исключений. Для этого случая гораздо эффективнее использовать систему слоев/масок столкновений.
Объекту параметров intersect_ray() также можно передать маску столкновений. Например, чтобы использовать ту же маску, что и у родительского тела, используйте переменную-член collision_mask. Массив исключений также можно передать в качестве последнего аргумента:
extends CharacterBody2D
func _physics_process(delta):
var space_state = get_world_2d().direct_space_state
var query = PhysicsRayQueryParameters2D.create(global_position, target_position,
collision_mask, [self])
var result = space_state.intersect_ray(query)
using Godot;
public partial class MyCharacterBody2D : CharacterBody2D
{
public override void _PhysicsProcess(double delta)
{
var spaceState = GetWorld2D().DirectSpaceState;
var query = PhysicsRayQueryParameters2D.Create(globalPosition, targetPosition,
CollisionMask, [GetRid()]);
var result = spaceState.IntersectRay(query);
}
}
См. Пример кода для получения дополнительной информации о том, как установить маску столкновений.
Трассировка лучей из экрана в 3D
Проецирование луча с экрана в трёхмерное физическое пространство полезно для выбора объектов. В этом нет особой необходимости, поскольку у CollisionObject3D есть сигнал "input_event", который сообщит о щелчке по объекту. Но если вы хотите сделать это вручную, вот как это сделать.
Для проецирования луча с экрана необходим узел Camera3D. Camera3D может работать в двух режимах проекции: перспективном и ортогональном. Поэтому необходимо получить как начало координат, так и направление луча. Это связано с тем, что origin изменяется в ортогональном режиме, а normal — в перспективном:
Для его получения с помощью камеры можно использовать следующий код:
const RAY_LENGTH = 1000.0
func _input(event):
if event is InputEventMouseButton and event.pressed and event.button_index == 1:
var camera3d = $Camera3D
var from = camera3d.project_ray_origin(event.position)
var to = from + camera3d.project_ray_normal(event.position) * RAY_LENGTH
private const float RayLength = 1000.0f;
public override void _Input(InputEvent @event)
{
if (@event is InputEventMouseButton eventMouseButton && eventMouseButton.Pressed && eventMouseButton.ButtonIndex == MouseButton.Left)
{
var camera3D = GetNode<Camera3D>("Camera3D");
var from = camera3D.ProjectRayOrigin(eventMouseButton.Position);
var to = from + camera3D.ProjectRayNormal(eventMouseButton.Position) * RayLength;
}
}
Помните, что во время выполнения _input() пространство может быть заблокировано, поэтому на практике этот запрос должен выполняться в _physics_process().