본문 바로가기

로블록스 개발 중급

Blaster 제작 : 레이저빔 Detect(충돌 감지)(1/3)

이 포스팅은 이전 포스팅: Player Tools 사용하기와 내용이 이어진다. 이전 포스팅을 참고하길 바란다.

이전 포스팅은 Blaster를 로블록스 시스템의 Player Tools와 연동하여 장착하고 해제하는 방법에 대해서 알아보았다. 이번 포스팅에서는 실제 Blaster의 사용 : 레이저 빔을 쏘고, 상대가 맞았는지를 검증하는 방법과 실제 로블록스 시스템, 즉, 멀티 플레이어 게임인 로블록스에서 어떻게 다른 플레이어들과 소통하는지에 대한 내용까지 이어지는 꽤 긴 내용의 첫부분이다. 이 포스팅에서는 레이저 빔에 상대가 맞았는지를 확인하는 코드에 집중하자. 

충돌 감지에 사용될 Raycasting

우선, Raycasting 은 번역을 할 만한 적당한 단어가 없으므로 그냥 레이캐스팅이라고 하겠다. 레이캐스팅의 의미는 보이지 않는 어느 점과 점 사이에 직선이 있다고 가정하고 그 사이에 어떤 물체가 있는지를 감지하고 그 위치와 어떤 물체인지를 반환해주는 행위를 의미한다. 그냥 레이캐스팅을 하면, 아래의 A와 B점 사이에 어떤 물체의 포지션과 물체의 정체를 알려주는 기능이라고 생각하면 쉽다. 함수로 보통 사용되고 클래스로 정의될 수도 있다. 로블록스에서는 workspace의 함수로 제공된다.

A와 B사이의 벽에 의해 충돌이 생긴 레이캐스팅

마우스 위치 찾기

여기서 레이캐스팅의 A와 B는 무엇인가? A는 마우스의 스크린 위치일 것이고, B는 마우스 스크린 위치에서 부터 원하는 길이만큼 멀리 있는 지점일 것이다. 실제로는 A의 포지션과 그 포지션에서의 A와 B지점만큼의 길이를 가지는 방향 벡터를 가지고 레이캐스팅을 사용한다. 이제 A포지션과 길이를 가지는 방향 벡터를 얻는 방법에 대해 살펴보자.

1. 이전 포스팅: Player Tools 사용하기에서 사용한 ToolController 스크립터를 사용하자.

2. 마우스의 3D 포지션을 알아내기 위한 적당한 수준의 깊이값을 정의하자. 이 값은 충돌 감지를 할 최대 길이라고 생각하면 된다. MAX_MOUSE_DISTANCE 는 1000. 그리고 getWorldMousePosition() 함수를 정의하자. 이 커스텀 함수의 반환 값은 충돌이 감지된 지점의 포지션이거나 스크린상의 마우스 위치에서 MAX_MOUSE_DISTANCE 만큼 떨어진 곳의 포지션을 반환한다. 충돌이 있으면 그 위치를, 충돌이 없었다면 광선의 맨 끝 위치(B 포인트)을 반환한다는 말이다.

local MAX_MOUSE_DISTANCE = 1000
 
local function getWorldMousePosition()
 
end

3. UserInputService 서비스의 GetMouseLocation() 함수을 사용하여 스크린의 마우스의 2D포지션을 얻어와서 mouseLocation 변수에 넣어둔다.

local UserInputService = game:GetService("UserInputService") 
local MAX_MOUSE_DISTANCE = 1000
local function getWorldMousePosition()
    local mouseLocation = UserInputService:GetMouseLocation()
end

이제 2D 마우스의 위치를 파악했으니 이젠 3D 위치를 알아보자.

1. 스크린상의 마우스 위치를 인자로 넣어서 Camera:ViewportPointToRay() 함수를 호출한다. 

local function getWorldMousePosition()
    local mouseLocation = UserInputService:GetMouseLocation()
    -- 마우스의 2D 위치에서 카메라방향으로 유닛 Ray(광선)을 생성
    local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
end

Camera:ViewportPointToRay() 함수는 2D 위치를 인자로 스크린(카메라 스크린)에서부터 카메라의 방향의 광선인 Ray 데이터형을 반환한다. 이 Ray는 스크린상의 마우스위치에서 무한히 뻗어나가는 광선인 것이다. 무한하기 때문에 Ray의 방향 벡터는 법선 벡터로 표현된다. 법선 벡터라는 말은 수학의 벡터... (고등학교 과정에 나오던가...?) 암튼 그 과정을 살펴보길 바란다. 이제 Raycast 함수로 생성된 광선에 물체가 닿는지를 확인할 차례이다. 

무한히 멀리 있는 물체마저 감지할 것이 아니라 위에서 정의한 MAX_MOUSE_DISTANCE의 길이만큼의 범위에서 감지할 것이므로 반환된 Ray의 유닛 방향벡터에 MAX_MOUSE_DISTANCE를 곱하여 감지할 광선의 방향벡터를 재계산한다. 

법선 벡터에 길이를 곱하면 길이를 가지는 방향 벡터를 가진다라는 것이 이해가 안되면 수학의 벡터 섹터를 또 참고하자.

local function getWorldMousePosition()
    local mouseLocation = UserInputService:GetMouseLocation()
    -- 마우스의 2D 위치에서 카메라방향으로 유닛 Ray(광선)을 생성
    local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
    -- MAX_MOUSE_DISTANCE 만큼의 길이를 가지는 방향 벡터를 얻음
    local directionVector = screenToWorldRay.Direction * MAX_MOUSE_DISTANCE

이제 Raycast() 함수를 호출할 차례다. workspace의 함수인 Raycast()함수는 인자는 screenToWorldRay의 Origin(광선이 시작된 포지션)과 위에서 계산한 길이를 가지는 방향벡터 directionVector이다.

local function getWorldMousePosition()
    local mouseLocation = UserInputService:GetMouseLocation()
    local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
    local directionVector = screenToWorldRay.Direction * MAX_MOUSE_DISTANCE
    -- workspace의 Raycast()함수를 호출하여 이제 레이캐스팅된 결과값인 RaycastResult 데이터 형을 얻었다
    local raycastResult = workspace:Raycast(screenToWorldRay.Origin, directionVector)

레이캐스팅된 결과물의 정보 : RaycastResult 데이터형

이제 반환된 RaycastResult의 내용을 살펴보자.

  • RaycastResult.Instance : ray(광선)이 감지한 BasePart나 Terrain(지형)의 Instance
  • RaycastResult.Position : 충돌 감지한 물체(Part 혹은 Terrain)의 표면의 위치 - Vector3
  • RaycastResult.Material : 충돌 지점의 메터리얼 값 - Material
  • RaycastResult.Normal : 충돌 지점의 법선 벡터. 충돌된 물체의 표면에 있는 지점을 어느 방향으로 충돌되었는지를 알려준다 - Vector3

여기서 사용될 데이터는 Position과 Instance이다. Position은 사용한 Ray에 충돌된 물체의 표면의 지점이므로 대략 물체의 위치라고 생각해도 좋다. 만약에 MAX_MOUSE_DISTANCE 의 거리에 충돌한 물체가 없다면 결과값(RaycastResult)은 nil을 반환할 것이다.

따라서 우선, 반환된 RaycastResult가 nil이 아닌지 확인하고 값이 있다면 Position값을 확인하고 getWorldMousePosition() 함수의 결과로써 반환하면 된다. 그럼 RaycastResult가 nil인 상황이라면? 그러면 그냥 광선의 Origin위치인 screenToWorldRay.Origin 과 광선의 길이를 가지는 directionVector를 더한 값을 반환하자. 

타겟을 향해 레이캐스팅하기

우리가 지금껏 한 내용이 무엇이었는지 정리해 보자. 우리가 꽤 길게 알아본 것은 Blaster의 레이저에 상대가 맞았나? 가 아니다. 잠깐 혼란이 올 수도 있겠지만, 우리가 지금껏 알아본 것은 마우스를 클릭한 곳의 게임 월드 위치이다.  마우스가 클릭한 곳에 물체가 있으면 그 물체의 위치, 물체가 없다면 그냥 충돌 감지 최대 길이 만큼 떨어진 곳의 마우스 위치였다. 그 위치를 알아야 그때부터 레이저가 레이저빔을 발사해서 물체에 맞았는지를 확인할 레이캐스팅이 가능해진다. 즉 마우스를 클릭한 곳의 게임 월드 위치를 알아내기 위해 레이캐스팅을 사용하였다.

무슨 말인지 이해해야 된다. Blaster의 레이저는 마우스로 조준할 것이기 때문에 상대에 맞았을 수도 있고 빗나갈 수도 있다.  맞든 빗맞든 레이저의 레이캐스팅을 위해 마우스의 3D상의 게임월드의 위치가 필요하다. 마우스의 3D상의 게임월드의 위치를 이제부터 타겟 위치라고 하겠다.

 

자, 이제 블래스터로부터 타겟위치로의 레이캐스팅을 통해 상대가 레이저에 맞았는지를 확인하는 과정을 살펴보자.

먼저 MAX_LASER_DISTANCE 라는 상수를 값을 500으로 할당한다. 이 값은 레이저 빔의 영향력이 미치는 거리값이다. 이보다 멀리 있는 물체에는 레이저 빔이 정조준되고 있어도 피격되지 않는다.

local MAX_LASER_DISTANCE = 500

fireWeapon()함수를 선언하자. 이 함수 안에서 타겟 위치에 대해 레이캐스팅을 할 것이다.

local function fireWeapon()
    -- 타겟 위치(클릭한 마우스의 월드 위치)
    local mouseLocation = getWorldMousePosition()
end

레이캐스팅을 위한 A지점과 방향 벡터를 계산해보자.

A지점은 블래스터의 총구의 위치이므로 구하기 간편하다. 

local tool = script.Parent
local function fireWeapon()
    -- A 지점 얻기
    local gunLocation = tool.Handle.Position
end

그 다음, 방향 벡터. 방향 벡터는 A에서 B로의 거리를 가지는 벡터이므로 A에서 B로의 유닛 벡터를 구하고나서 거리인 MAX_LASER_DISTANCE를 곱하여 구할 수 있다... 이게 뭐야? 하는 분은 벡터 위키로 고고! (벡터는 여기 포스팅에선 벗어나요. ㅠㅠ)

local function fireWeapon()
    -- A 지점
    local gunLocation = tool.Handle.Position
    -- B 지점
    local mouseLocation = getWorldMousePosition()
 
    -- A 지점에서 B 지점으로의 유닛 벡터 구하기
    local targetDirection = (mouseLocation - gunLocation).Unit
    
    -- 유닛 벡터에 거리를 곱하여 방향 벡터를 구한다
    local directionVector = targetDirection * MAX_LASER_DISTANCE
end

이제 실제 레이캐스팅 함수를 호출할 차례이다. 

이번 workspace:Raycast() 함수에서는 하나의 인자를 더 사용할 것이다. 

  1. A 지점 (ray의 시작 포인트)
  2. 방향 벡터
  3. (이번에 사용할 인자) RaycastParams 

RaycastParams는 레이캐스팅을 사용할 때 여러가지 조건을 건네주는 역할을 한다. 여러가지 속성에 대해서는 RaycastParams의 레퍼런스를 참고하기 바란다. 여러가지 조건 중에서 여기서는 FilterDescendantsInstances(하위 항목 인스턴스 필터링)를 사용한다. FilterType을 BlackList로 정하고 나면, FilterDescendantsInstances 배열에 포함된 인스턴스와 그 하위 인스턴스는 레이캐스팅에 무시된다. 여기서 블랙리스트 배열에 추가시킬 인스턴스는 플레이어 자신이다. 

정신없이 블래스터를 발사하다가 실수로든 어떤 특이한 상황이든 자신의 캐릭터가 레이저빔에 죽는 일은 없게 하기 위해서이다.

local Players = game:GetService("Players")

local function fireWeapon()
    local mouseLocation = getWorldMousePosition()
    local targetDirection = (mouseLocation - tool.Handle.Position).Unit
    local directionVector = targetDirection * MAX_LASER_DISTANCE
   
    -- 자신의 캐릭터가 레이캐스팅에 무시되도록 설정하자.
    local weaponRaycastParams = RaycastParams.new()
    weaponRaycastParams.FilterDescendantsInstances = {Players.LocalPlayer.Character}
    -- Raycast() 의 인자는 A지점, 방향벡터, 레이캐스팅 조건들임
    local weaponRaycastResult = workspace:Raycast(tool.Handle.Position, directionVector, weaponRaycastParams)
end

마지막으로 레이캐스트 작업이 값을 제대로 반환했는지 확인하자. 이 값을 가지고 물체가 명중되고 무기와 명중 위치 사이에 레이저빔을 렌더링하거나, 아무것도 명중되지 않은 경우에도 레이저빔을 렌더링하기 위한 최종 위치를 계산하여야 한다. 

1. 레이저빔 렌더링에 사용될 hitPosition 이라는 변수를 정의하자.

2. 명중했다면 명중된 물체의 위치를, 명중하지 못했다면 A지점과 방향벡터로 계산된 위치를 대입하자. 

    local weaponRaycastResult = workspace:Raycast(tool.Handle.Position, directionVector, weaponRaycastParams)
    local hitPosition
    if weaponRaycastResult then
        -- 명중했다면 레이캐스팅의 결과 위치가 hitPosition에 대입된다.
        hitPosition = weaponRaycastResult.Position
    else
        -- 명중하지 못했다면 A지점과 방향벡터로 계산된 위치가 대입된다.
        hitPosition = tool.Handle.Position + directionVector
end

툴을 사용할 때마다 호출되는 toolActivated() 함수 안에 fireWeapon() 함수를 넣어서 매번 레이캐스팅을 실행하게 한다.

local function toolActivated()
    tool.Handle.Activate:Play()
    fireWeapon()
end

명중 체크하기

레이캐스팅을 통해 물체에 맞았다는 것은 체크했다. 그럼 이번에는 그 물체가 무엇인지를 체크하는 단계로 넘어가자. 여기서 중요한 것은 상대 플레이어냐 아니냐 일 것이다. 상대 플레이어에 대한 명중 체크는 캐릭터의 휴머노이드를 통해 가능해진다. 휴머노이드가 있는 물체이면 플레이어중에 하나일 것이다. 그리고 휴머노이드를 통해서 캐릭터의 어느 부위에 명중했는지도 체크할 수 있다.


캐릭터의 일부가 맞았더라도 그것의 상위노드가 캐릭터일 것이라고 단정할 수 없다. 캐릭터의 계층 구조의 신체 부위, 액세서리 또는 도구에 따라서 계층이 다르기 때문이다. 아래의 계층도를 살펴보자. 캐릭터 아래의 하위노드 아래 하위노드도 있다.

이럴 때는 FindFirstAncestorOfClass() 함수를 사용하자. 약간 이해가 필요한 함수이다. 인스턴스의 함수인 FindFirstAncestorOfClass() 함수는 인자로 주어진 클래스 이름을 가지고 같은 클래스 이름을 가지는 상위노드를 찾아서 윗방향으로 찾아서 올라간다. 위의 이미지의 계층도의 예를 들어 봤을때,

local characterModel = weaponRaycastResult.Instance:FindFirstAncestorOfClass("Model")

이라고 하면, 레이캐스팅의 결과인 weaponRaycastResult의 Instance가 Blaster의 Handle이라고 가정하자. 그러면 FindFirstAncestorOfClass("Model") 함수는 Handle의 상위노드인 Blaster부터 Model과 클래스명이 같은지를 확인하고, 아니면 그 상위노드인 Lord_Mammerhead와 비교하고... 계속 비교해가면서 결국은 DataModel(로블록스의 최상단 노드, game이라는 이름으로 접근된다.)까지 비교하고 못 찾으면 nil을 반환하는 함수이다. 이 예의 경우에는 Lord_Mammerhead의 클래스이름이 Model이므로 Lord_Mammerhead을 반환할 것이다.

 

이제 코드를 확인해보자.

    local hitPosition
    if weaponRaycastResult then
        hitPosition = weaponRaycastResult.Position
    
        -- FindFirstAncestorOfClass("Model")을 통해 찾은 인스턴스. characterModel
        local characterModel = weaponRaycastResult.Instance:FindFirstAncestorOfClass("Model")
        if characterModel then
            -- characterModel가 휴머노이드를 가지고 있다면 상대 캐릭터일것이다.
            local humanoid = characterModel:FindFirstChild("Humanoid")
            if humanoid then
                print("Player hit")
            end
        end
    else
        -- weaponRaycastResult의 값이 nil인 상황은 물체에 명중이 되지 않은 상황이다.
        hitPosition = tool.Handle.Position + directionVector
    end

이 코드를 통해 블래스터의 레이저빔에 상대 캐릭터가 맞으면, Output 창에 "Player hit"가 맞을 때마다 출력될 것이다.

2명 이상의 플레이어간의 테스트

블래스트의 레이저빔의 명중을 테스트하기 위해서는 2명 이상의 플레이어가 필요하다. 혼자 개발중에는 힘든 조건이지만, 로블록스 스튜디오는 멀티 플레이어 상황을 가상으로 만들어주어서 테스트가 가능하다. 로컬 서버를 개발자 피씨에 열어서 두개 이상의 로블록스 스튜디오를 클라이언트로써 접속하는 방법을 사용하다. 즉, 서버 1개, 클라이언트 2개 이상이 스튜디오를 동시에 띄어서 테스트하게 된다.

1. TEST탭 선택

2. LocalServer 아래 드롭다운이 '2 Players' 으로 되어 있는 것을 확인하고 Start 버튼을 누른다. 그러면 세개의 창이 나타나는데 하나는 로컬 서버, 두 개는 각기 Player1과 Player2용의 클라이언트이다.

Player1, 혹은 Player2 클라이언트에서 상대를 향히 클릭하여 테스트하여 명중시에 Player hit가 출력되는 것을 확인하자. 명중이 아닐때는 아무 반응이 없을 것이다.