Chapter 4-2. 충돌 : Raycast & LayerMask & 투영의 개념

Date:     Updated:

카테고리:

태그:

인프런에 있는 Rookiss님의 [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part3: 유니티 엔진 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click

🚖 Raycast

광선을 쏴서 충돌되는 물체가 있는지 없는지를 검사하는 것이 Raycasting

  • 예를 들어
    • 게임 속 물체를 클릭한다는 것은 클릭한 화면의 위치로부터, 즉 카메라의 위치로부터 레이저(Raycast)를 쏴서 닿은 물체를 활성화시키는 것과 같다.
    • 3인칭 카메라에서 플레이어를 향해 Raycast를 쐈을 때 장애물이 있다면 플레이어를 가리지 않고 제대로 찍을 수 있도록 카메라 위치를 장애물을 넘어가는 곳으로 이동시키는 식으로 구현할 수 도 있다.
    • 롤이나 스타크래프트처럼 마우스로 화면상에 찍은 위치에서 카메라가 Raycast를 월드 좌표로서 쏴서 그 곳으로 캐릭터를 이동시키게도 할 수 있다.

하나만 부딪치면 끝난다. 하나 충돌되면 더 이상 관통하지 않는다.

    void Update()
    {
        RaycastHit hit;

        Vector3 look = transform.TransformDirection(Vector3.forward);
        // 로컬 좌표로서의 (0, 0, 1)을 월드 좌표로 변환하여 리턴함

        Debug.DrawRay(transform.position, look * 10, Color.red); 
        // transform.position 에서 빨강색 광선을 (transform.position + look * 10) 위치까지를  쏜다. 
        // DrawRay 는 Raycast 와 다르게 두번째 인수가 방향만을 나타내는건 아니라 크기도 필요함.
        // Raycast 와 다르게 start에서 dir 방향으로 쏘는게 아니라 start부터 start+dir 위치까지를 쏘고 이를 시각적으로 그림

        if (Physics.Raycast(transform.position + Vector3.up, look))
        {
            Debug.Log("Raycast!");
        }
        if(Physics.Raycast(transform.position + Vector3.up, look, 20))
        {
            Debug.Log("Raycast!");
        }
        if(Physics.Raycast(transform.position + Vector3.up, look, out hit, 20))
        {
            Debug.Log("Raycast!");
        }
    }
Physics.Raycast(transform.position + Vector3.up, look, out hit, 20)
  • transform.position + Vector3.up 위치로부터 광선을 쏜다.
    • Vector3.up 을 더해준 이유는 이 스크립트의 주인 오브젝트의 Pivot이 발에 가있어서 그냥 transform.position 에서 쏘면 광선이 발에서 나가므로, 조금 위에서 광선을 쏘기 위해 (0, 1, 0)만큼을 더해주었다. (Vector3.up)
  • transform.TransformDirection(Vector3.forward) 방향으로 광선을 쏜다.
    • 로컬 좌표로서의 (0, 0, 1)를 월드 좌표로 변환한 것.
    • 즉, 이 스크립트의 주인 오브젝트 기준에서의 앞 방향이 월드 좌표계로 변환된 것
      • 광선의 시작 위치와 방향을 월드 좌표 기준으로 설정해 주어야 하니까
  • 광선에 충돌한 오브젝트의 정보는 hit에 담긴다.
    • position, normalized 등등 충돌한 오브젝트의 위치 방향 등등의 정보가 담긴다.
      RaycastHit hit;
      
  • 광선을 20 만큼의 길이로 쏜다.
    void Update()
    {

        Vector3 look = transform.TransformDirection(Vector3.forward);

        RaycastHit[] hits;
        hits = Physics.RaycastAll(transform.position + Vector3.up, look, 20);

        foreach (RaycastHit hit in hits)  // 관통되서 광선에 닿은 모든 물체들이 hits 배열에 담긴다. for문으로 모든 원소에 접근
        {
            Debug.Log("Raycast!");
        }
    }

그냥 Raycast는 하나만 충돌되면 더 이상 관통되지 않기 때문에 광선이 관통되게 하고 충돌한 모든 오브젝트를 알고 싶다면 RaycastAll 함수를 써야 한다.

  • Raycast와 다르게 광선에 관통된 모든 오브젝트의 배열을 리턴한다.
            RaycastHit[] hits;
          hits = Physics.RaycastAll(transform.position + Vector3.up, look, 20);
    


🚖 투영의 개념

화면 좌표계(Screen, Viewport)

  • 좌표계 종류
    • Local
    • World
    • 화면 좌표계 👉 카메라 오브젝트가 촬영해서 보여주는 게임(Game) 화면 상의 좌표. 2 D 좌표(x, y) 이다.
      • Screen (픽셀)
        • 게임 화면의 왼쪽 끝 하단의 좌표는 (0, 0)
        • 게임 화면의 오른쪽 끝 상단의 좌표는 예를 들어 해상도가 1280 X 900 이라면 (1280, 900) 이 된다.
        • 해상도 크기, 즉 픽셀의 개수로 화면 좌표를 사용한다.
          • (0, 0) ~ (Screen.width, Screen.height)
      • Viewport (픽셀을 비율로)
        • 게임 화면의 왼쪽 끝 하단의 좌표는 (0, 0)
        • 게임 화면의 오른쪽 끝 상단의 좌표는 (1, 1)
        • 즉, 화면 상 픽셀 좌표를 0 ~ 1 사이의 비율로 나타낸 화면 좌표다.
void Update()
{
    Debug.Log(Input.mousePosition);  // 마우스 커서 위치 좌표가 Screen 좌표계로 출력 
    Debug.Log(Camera.main.ScreenToViewportPoint(Input.mousePosition));  // Screen 좌표계로 표시된 마우스 커서 위치 좌표를 Viewport 좌표계, 즉 0 ~ 1 사이의 비율 좌표계로 변환한다.
}
  • Input.mousePosition
    • 현재 마우스 위치를 (0, 0) ~ (Screen.width, Screen.height) 로 나타낼 수 있는 픽셀 좌표(Screen)로서의 나타낸다.
  • Camera.ScreenToViewportPoint
    • (0, 0) ~ (Screen.width, Screen.height)로 나타낼 수 있는 픽셀 좌표(Screen)를 (0, 0) ~ (1, 1)로 나타내는 Viewport, 즉 비율 좌표로 변환하여 리턴한다.
    • ScreenToViewportPoint(Input.mousePosition)
      • 마우스 위치를 픽셀 좌표계로 나타낸 좌표를 (0, 0) ~ (1, 1) 비율 좌표계로 변환해 줌
    • Camera.main 메인 카메라


✨ 게임 화면에 마우스를 클릭하면 그 위치를 월드 좌표계로 변환하기

롤이나 스타크래프트처럼 게임 화면 상에서 특정 오브젝트 혹은 위치를 마우스로 클릭하면 반응하게 하고 싶을 때

  1. 마우스 커서의 Screen 좌표 👉 World 좌표로 변환
  2. 카메라 현재 위치로부터, 위에서 변환했던 클릭한 마우스 위치의 World 좌표로 향하는 방향을 구한다.
  3. 카메라로부터 해당 방향으로 Raycast를 쏘면 마우스로 클릭했던 그 오브젝트가 충돌 될 것이다!

image

image

image

  • 카메라 컴포넌트의 near
    • 카메라로부터 렌더링을 시작할 위치까지의 거리
      • z 축에서의 거리다. (렌더링을 하기 시작할 위치는 카메라 오브젝트의 앞이므로)
    • 카메라 위치로부터 Z 축으로 near 값을 더한 위치부터 게임 화면으로서 담기게 된다.
      • 첫번재 사진의 노란 네모(2D) 부분부터 렌더링 시작.
  • 카메라 컴포넌트의 far
    • 카메라로부터 딱 여기까지만 렌더링 하여 카메라에 담겠다는 위치까지의 거리
      • z 축에서의 거리다. (렌더링 한계 위치는 카메라 오브젝트의 앞이므로)
    • 카메라 위치로부터 Z 축으로 far 값을 더한 위치까지의 게임 월드를 게임 화면에 렌더링 하여 그린다.
      • 두 번째 사진의 노란 네모(2D) 부분까지를 렌더링. 더 뒤에 있는건 게임 화면에 담기지 않는다.
        if(Input.GetMouseButton(0))  // 게임 화면에 마우스 좌클릭 하면
        {
            Vector3 mousePos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, Camera.main.nearClipPlane));
            Vector3 dir = mousePos - Camera.main.transform.position;
            dir = dir.normalized;

            Debug.DrawRay(Camera.main.transform.position, dir * 100.0f, Color.red, 1.0f); // 1 초동안 Camera.main.transform.position 위치로부터 dir * 100.0f 만큼을 더한 위치까지의 광선을 그림

            RaycastHit hit;
            if (Physics.Raycast(Camera.main.transform.position, dir, out hit, 100.0f))
            {
                Debug.Log(hit.collider.gameObject.name);
            }
        }


1️⃣ 마우스를 클릭한 곳의 화면상의 좌표를 월드 좌표로 변환

 Vector3 mousePos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, Camera.main.nearClipPlane)); 
  • ScreenToWorldPoint 함수 👉 화면 좌표계를 월드 좌표계로 변환하고 리턴함
    • ScreenToWorldPoint 에다가
      • 마우스를 클릭한 화면상의 좌표(X, Y)와
        • (0, 0) ~ (Screen.width, Screen.height) 범위를 가진다.
      • 화면 상 좌표는 Z 값이 없다. 따라서 월드 좌표로서의 Z 값을 정할 때 고려할 카메라와의 거리 를 함께 Vector3로 묶어 넘겨 주어야 한다.
        • 현재 카메라의 위치인 camera.main.transform.position.z값으로부터 Vector3로 함께 묶어서 넘긴 z 값(카메라부터의 거리)을 더해준 값을 화면 좌표계를 월드 좌표계로 변환했을 때의 z값으로 삼는다.
          • 단지 인수로 넘겨주는 Vector3 의 z값은 이 값으로 월드 z 좌표로 삼겠다는게 아니라 이걸 카메라와의 거리로 받아들이고 이걸 고려해서 월드 좌표계 기준 z로 만들겠다는 것이다. 착각 노노!
          • z값 없이 화면 좌표계(X, Y)를 가지던 마우스 클릭한 곳을 월드 기준으로 전환한 좌표의 z값은 위의 예로는 camera.main.transform.position.z + Camera.main.nearClipPlane 가 된다.
            • 카메라의 nearClipPlane 은 카메라 컴포넌트의 near. 카메라가 카메라로부터 렌더링을 시작하는 곳 까지의 z 축 기준의 거리
    • 이렇게 세 가지 요소를 Vector3 로 묶어서 인수로 넘겨주면 적절한 월드 좌표계로 변환해준다.

image

예를 들어 게임 화면 상에서 화면상 픽셀 좌표 기준으로 (160, 180)에 해당하는 픽셀에 마우스 클릭을 했다고 가정해보자. 이 좌표는 Vector3 로 보면 (160, 180, 0) 상태일 것이다. 화면상 좌표는 z 값 없이 2D 니까.. 클릭했던 이 (160, 180) 픽셀 좌표를 게임 월드 기준으로 변환하고자 ScreenToWorldPoint(Vector3(160, 180, 3)) 로 실행시켰다고 가정해보자. z값으로 넘긴 3 을 카메라와의 ‘거리’를 3 이라고 고려하여 화면상 좌표인 (160, 180)를 월드 기준 좌표로 변환했더니 (3.7, 4.8, -0.6)이 되었다. 현재 카메라 위치의 z 값 + 3 👉 - 0.6.


그리고 인수로 넘겨준 z 값, 즉 고려할 카메라와의 거리를 렌더링 시작 위치까지의 거리인 Camera.main.nearClipPlane로 넘겨준 이유는! 카메라 현재 위치로부터, 위에서 변환했던 클릭한 마우스 위치의 World 좌표로 향하는 방향을 구해서 그 방향으로 Raycast를 쏠 것이기 때문에 어차피 방향이 중요하지 z 값은 크게 중요하지 않아서 렌더링 시작 거리인 Camera.main.nearClipPlane로 선택하신 듯 하다


마치 Z 축은 무시하고 X, y 좌표만 고려한 한 장의 사진으로 찍듯이 카메라는 3 D 의 실제 게임 월드를 2 D 인 게임 화면으로 투영 하게 된다. 마치 단면을 잘라 버린 듯이. 그래도 실제 게임 월드 위치 상에서의 비율이 지켜지는 것이 핵심 포인트다


2️⃣ 카메라의 현재 위치로부터 변환한 월드 좌표로 향하는 방향 구하기

Vector3 dir = mousePos - Camera.main.transform.position;
dir = dir.normalized;
  • 카메라 현재 위치로부터 mousePos 위치로 향하는 방향 벡터 dir
    • 이 방향으로 3️⃣ 에서 Raycast를 쏠 것이다.
    • mousePos 👉 위에서 구했던, 마우스를 클릭한 화면상 좌표를 월드 좌표로 변환한 좌표


3️⃣ 그 방향으로 Raycast 쏴서 충돌 검사 후 충돌한 오브젝트 정보 받아 오기

            Debug.DrawRay(Camera.main.transform.position, dir * 100.0f, Color.red, 1.0f); // 1 초동안 Camera.main.transform.position 위치로부터 dir * 100.0f 벡터 만큼을 더한 위치까지의 광선을 그림

            RaycastHit hit;
            if (Physics.Raycast(Camera.main.transform.position, dir, out hit, 100.0f))
            {
                Debug.Log(hit.collider.gameObject.name);
            }

image

  • Physics.Raycast
    • 메인 카메라의 현재 위치로부터
    • dir 방향으로
      • 메인 카메라의 현재 위치로부터 마우스 클릭한 화면상의 좌표를 월드 좌표로 변환한 곳을 향하는 방향
    • 100.0f 길이의 광선을 쏘고
    • 충돌한 오브젝트가 있다면
      • 그 정보를 hit에 담은 후
      • True 를 리턴한다.

Ray

        if (Input.GetMouseButton(0))
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); 

            Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f);

            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 100.0f))
            {
                Debug.Log(hit.collider.gameObject.name);
            }
        }

  • ScreenPointToRay
    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); 
    
    • 호출한 카메라 위치로부터 픽셀 단위의 화면상 좌표의 실제 월드 좌표로 향하는 방향의 Ray (광선)을 리턴한다.
      • 아래 과정은 위 함수 한 줄과 비슷하다. 아래 과정에다가 추가로 해당 dir로 향하는 Ray를 직접 리턴한다.
              Vector3 mousePos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, Camera.main.nearClipPlane));
              Vector3 dir = mousePos - Camera.main.transform.position;
              dir = dir.normalized;
        
    • 마찬가지로 화면상 좌표(X, Y)와 함께 월드 좌표의 Z 값을 계산할 때 고려할 카메라와의 거리 또한 Z 로 함께 Vector3로 묶어 넘겨 준다.
  • Raycast 에 시작 위치와 방향을 넘기는 대신 광선 자체인 ray를 넘겨준다.


🚖 LayerMask

특정 오브젝트들에 대해서만 Raycasting, 카메라 렌더링시 사용하는 CullingMask 등등의 어떤 물리 처리를 하기 위해 사용한다.

  • 오브젝트에게 해당 Layer를 붙이면
    • 그 Layer에 해당하는 오브젝트들을 대상으로만 Raycasting 할 수 있다.
    • 카메라 오브젝트에 Culling Mask 로 어떤 Layer Mask를 지정하면 그 Layer를 가진 오브젝트들만 해당 카메라에 렌더링 된다.
  • 부하를 줄여 성능을 높인다.
    • Raycasting은 부하가 꽤 많은 연산이다. 안그래도 많으니 모든 오브젝트들이 Raycast 에 충돌될 수 있다면 성능이 떨어진다.
      • 그래서 와이어가 복잡하게 이루어져 있는 Mesh 의 경우, Mesh 를 바탕으로 Collider를 따는 Mesh Collider를 콜라이더로 사용하면 연산이 어마어마하게 소요 될 것이다.
        • 이처럼 좀 세밀하게 Mesh 모양 그대로를 충돌 처리를 받아야 하는 경우엔 Mesh Collider를 쓰기도 하지만 충돌 전용 Mesh를 따로 만들기도 한다.


LayerMask 는 사실 int 이며 비트 플래그다!

image

  • LayerMask 는 32 비트의 int형이다.
    • 비트 플래그로, 비트로 각각의 아이템을 상징하며 구분된다.
        • 몬스터 레이어는 0000000000000000000000001000000 👉 8 번 레이어 (1 « 8)
        • NPC 레이어는 0000000000000000000000000000100 👉 3 번 레이어 (1 « 3)
    • 따라서 LayerMask는 위와 같이 32 개까지만 만들 수 있다.
    • LeyerMask 내에 연산자 오버로딩과 int로의 변환이 다 구현이 되어 있기 때문에 LayerMask라는 이름으로 편하게 접근할 수 있는 것 뿐이다.
            int mask = (1 << 8); // 8 번 레이어 

            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 100.0f, mask))
            {
                Debug.Log(hit.collider.gameObject.name);
            }

  • 8 번 레이어(사진에선 “Monster”)를 가진 오브젝트들에 대해서만 Raycasting이 진행된다.
  • (1 « 8) 은 십진수로 변환하면 256 이므로 int mask = 256; 도 가능하다.
            int mask = (1 << 8) | (1 << 9); // 8 번 레이어 + 9 번 레이어

            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 100.0f, mask))
            {
                Debug.Log(hit.collider.gameObject.name);
            }
  • (1 << 8) | (1 << 9) 👉 000000000000000000000000110000000
    • 즉 8번 레이어 혹은(or) 9 번 레이어(사진에선 “Monster”와 “Wall”)를 가진 오브젝트들에 대해서만 Raycasting이 진행된다.
  • 이처럼 비트 플래그를 활용한 비트 연산으로 Layer를 지정할 수 있다.
  • (1 << 8) | (1 << 9) 은 십진수로 변환하면 768 이므로 int mask = 768; 도 가능하다.
            LayerMask mask = LayerMask.GetMask("Monster") | LayerMask.GetMask("Wall")/
            // int mask = (1 << 8) | (1 << 9); 와 동일

            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 100.0f, mask))
            {
                Debug.Log(hit1.collider.gameObject.name);
            }
  • LayerMask.GetMask(string)
    • 해당 문자열로 저장된 LayerMask 즉, int 크기의 비트를 가져오게 된다.
    • 이처럼 함수를 사용해 편하게 레이어 이름으로 지정한 문자열로 비트를 가져올 수 있다.


🚖 Tag

별다른 기능은 없고 그냥 추가적으로 오브젝트들을 그룹지어 구분해주기 위한 정보

GameObject.FindGameObjectWithTag("Monster");
Debug.Log(hit.collider.gameObject.tag);

위와 같이 게임 오브젝트를 Tag 를 사용하여 찾아낼 수 있다.

LayerMask 와 Tag 의 차이

  • LayerMask
    • int 크기의 비트이며
      • 따라서 구분을 위해 32 개까지만 만들 수 있도록 제한 됨
    • 비트 혹은 비트플래그 연산으로서 오브젝트들이 구분된다.
    • 물리적인 처리에 대한 영향 여부를 구분한다.
  • Tag
    • 만들 수 있는 갯수가 제한되지 않는다.
    • 이름과 같이 문자열로서 오브젝트들이 그룹으로서 구분된다.


🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

댓글남기기