광선 투사 (Ray-casting)

소개

게임 개발에서 가장 일반적인 작업 중 하나는 광선(또는 사용자 지정 모양의 물체)를 투사하고 그것이 부딪히는 것을 확인하는 것입니다. 이를 통해 복잡한 행동, AI 등이 발생할 수 있습니다. 이 튜토리얼에서는 2D 및 3D로 이 작업을 수행하는 방법에 대해 설명합니다.

씬이 단지 프론트엔드일 동안, Godot는 모든 낮은 레벨의 게임 정보를 서버에 저장합니다. 따라서 일반적으로 레이 캐스팅은 하위 작업입니다. 단순 레이캐스트의 경우, RayCastRayCast2D 와 같은 노드는 모든 프레임의 레이캐스트의 결과가 어떤 것인지 반환함으로써 작동 할 것입니다.

그러나, 많은 경우, 광선 투사는 보다 상호작용적인 과정이 필요하므로 코드로 이를 수행하는 방법이 있어야만 합니다.

공간

물리 세계에서 Godot은 모든 낮은 수준의 충돌과 물리학 정보를 한 공간 에 저장합니다. 현재 2D 공간(2D 물리용)은 CanvasItem.get_world_2d().space 에 액세스하여 얻을 수 있습니다. 3D의 경우 Spatial.get_world().space 입니다.

결과적인 공간 RIDPhysics2DServer 에서 사용할 수 있습니다.

공간에 접근하기

Godot physics runs by default in the same thread as game logic, but may be set to run on a separate thread to work more efficiently. Due to this, the only time accessing space is safe is during the Node._physics_process() callback. Accessing it from outside this function may result in an error due to space being locked.

물리 공간에 대한 쿼리를 수행하려면 Physics2DDirectSpaceStatePhysicsDirectSpaceState 를 사용해야 합니다.

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

광선 투사 쿼리

For performing a 2D raycast query, the method Physics2DDirectSpaceState.intersect_ray() may be used. For example:

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

The result is a dictionary. If the ray didn't hit anything, the dictionary will be empty. If it did hit something, it will contain collision information:

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

충돌이 발생할 경우 결과 딕셔너리에는 다음 데이터가 포함됩니다:

{
   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 좌표를 사용하는 3D 공간에서와 유사합니다.

충돌 예외

광선 투사의 일반적인 용도는 캐릭터가 주변 세계에 대한 데이터를 수집할 수 있도록 하는 것입니다. 이것의 한 가지 문제점은 다음 이미지와 같이 같은 캐릭터가 충돌체를 가지고 있어서, 광선은 부모의 충돌체만 감지한다는 것입니다:

../../_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 });
    }
}

예외 배열에는 개체 또는 RID가 포함될 수 있습니다.

충돌 마스크

예외 방법은 부모 body를 제외하고도 잘 작동하지만, 많은 and/or 동적 예외 목록이 필요한 경우 매우 불편하게 됩니다. 이 경우 충돌 계층/마스크 시스템을 사용하는 것이 훨씬 효율적입니다.

The optional fourth argument for intersect_ray() is a collision mask. For example, to use the same mask as the parent body, use the collision_mask member variable:

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 ray casting(광선 투사)

Casting a ray from screen to 3D physics space is useful for object picking. There is not much need to do this because CollisionObject has an "input_event" signal that will let you know when it was clicked, but in case there is any desire to do it manually, here's how.

화면에서 광선을 캐스팅하려면 Camera 노드가 필요합니다. 카메라 는 두 가지 투영 모드일 수 있습니다: 원근법과 직교. 이로 인해 광선 원점과 방향을 모두 얻어야 합니다. 이는 직교 모드의 원래 변화, 원근법 모드에서의 정상 변화 때문입니다:

../../_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()``에서 실행해야 한다는 점을 기억하십시오.