Physics 

레이어로 Collider의 충돌 범위를 설정해서 특정한 충돌만 받아들이거나 무시하기

 

작성 기준 버전 :: 2019.2

 

[이 포스트의 내용은 유튜브 영상으로도 시청하실 수 있습니다]

 

이번 포스트에서는 유니티의 레이어로 Collider의 충돌 범위를 설정하는 방법을 알아보도록 하자.

 

두 개의 Collider가 충돌하면 OnCollision 혹은 OnTrigger 이벤트가 발생하며 개발자는 이 이벤트를 통해서 충돌을 감지했을 때 처리되어야 할 기능을 구현한다는 것을 기억할 것이다.

 

과연 게임에서는 어떤 상황과 이유에서 Collider의 충돌 범위를 설정해서 특정한 충돌을 받아들이거나 무시해야 할까?

 

만약 두 캐릭터가 서로를 향해서 총알을 발사한다고 생각해보자. 그런데 서로에게 발사한 총알끼리의 충돌을 무시하지 않고 그대로 두면 어떻게 될까? 총알끼리 충돌하면 서로에게 발사된 총알이 없어져 버리거나 튕겨져 나갈 것이다.

 

와! 총알을 쏴서 상대방의 총알을 막을 수 있는 게임이라니! 잘 만들면 꽤나 재밌고 멋있을 것 같은 컨셉이다.

 

하지만 대부분의 게임에서는 총알끼리의 충돌같은 건 구현하지 않고 무시하게 만들어버린다.

 

그 이유는 여러 가지가 있을 수 있는데 수많은 총알이 발생시키는 충돌로 게임의 성능이 저하될 수 있다는 것과 이런 총알로 총알을 맞출 수 있는 컨트롤 중심적인 시스템에서는 팬티만 입고 권총을 든 무시무시하고 고일 대로 고여버린 고인물이 달려와서 초보자가 쏜 총알을 모조리 막아버리고 초보자의 뚝배기를 터뜨려 버릴 수 있다는 것이다.

 

총알과 총알이 부딪히는 것 외에도 많은 문제가 있다.

 

어떠한 예외가 있을 수 있는지 살펴보자면 열심히 체력을 깎아놓은 몬스터가 힐팩에 스쳐서 건강해진다던가 플레이어는 던전 입구에서 헤매고 있는데 다른 층으로 넘어가는 콜라이더 앞에서 서성거리던 몬스터가 그 트리거를 건드려서 플레이어가 다음 층으로 넘아가던가 하는 많은 문제가 발생할 수 있다.

 

이걸 코드 레벨에서 막으려면 총알 클래스에는 충돌 검사를 할 때 충돌한 대상이 같은 총알이면 무시하는 코드를, 힐팩 클래스과 던전 층 이동용 트리거 클래스에서는 트리거에 닿은게 플레이어가 아니면 무시하는 코드를 작성해야 할 것이다. 이렇게 수동으로 일일이 예외를 막아야하는 경우가 많으면 많을수록 앞에서 언급한 것과 같은 어처구니가 없게 느껴지는 버그가 발생할 확률이 상승한다.

 

만약 이걸 별도의 코드 작업 없이 간단하고 일괄적으로 막을 수 있다면 당연히 그 방법을 써야될 것이다.

 

그게 바로 이번에 배울 유니티 레이어를 이용한 Collider 충돌 무시하기이다.

 

본격적인 내용에 들어가기에 앞서 아래에 있는 unity-mouse-input-practice.zip 파일을 다운로드 받아서 패키지를 임포트하도록 한다.

 

unity-mouse-input-practice.zip
다운로드

 

그리고 패키지에 포함되어 있는 Simple Character Test 씬을 열도록 한다.

 

 

먼저 게임를 플레이시키고 게임 뷰에 클릭해보면 클릭을 한 번 할 때마다 총알이 한 발씩 나가는 것을 볼 수 있다.

 

void Fire()
{
    //if(Input.GetMouseButtonDown(0))
    if(Input.GetMouseButton(0))
    {
        Vector3 firePos = transform.position + animator.transform.forward + new Vector3(0f, 0.5f, 0f);
        var bullet = Instantiate(bulletPrefab, firePos, Quaternion.identity).GetComponent<Bullet>();
        bullet.Fire(animator.transform.forward);
    }
}

 

SimpleCharacterController 스크립트를 열어서 Fire 함수 안에 있는 GetMouseButtonDown 함수를 GetMouseButton으로 바꾼 뒤, 코드를 저장하고 에디터로 돌아간다.

 

 

다시 게임을 플레이시키고 게임 뷰에서 마우스 왼쪽 버튼을 꾹 누르고 있으면 총알이 쏟아져 나오는 것을 볼 수 있다.

 

private void OnTriggerEnter(Collider other)
{
    if(other.GetComponent<Bullet>() == null)
    {
        Destroy(gameObject);
    }
}

 

그 다음에는 Bullet을 찾아서 스크립트 에디터를 열어보면 아래 쪽에 있는 트리거 감지 이벤트인 OnTriggerEnter에 앞에서 말한 것처럼 충돌 감지 예외를 코드 레벨에서 수동으로 처리하고 있는 것이 보일 것이다. 같은 총알끼리 부딪혔을 때는 무시하도록 작성되어 있다.

 

private void OnTriggerEnter(Collider other)
{
    //if(other.GetComponent<Bullet>() == null)
    {
        Destroy(gameObject);
    }
}

 

이 부분을 주석 처리 해버리면 어떻게 될까? 한 번 테스트 해보자.

 

 

게임을 플레이시키고 마우스를 클릭해보면 발사된 총알끼리 부딪혀서 앞으로 나가지 못하고 바로 사라져버리는 걸 볼 수 있다.

 

그럼 이걸 어떻게 코드 레벨의 예외처리 없이 원래대로 동작하게 만들 수 있을까?

 

이제부터 그걸 알아보자.

 

  

프로젝트 뷰에서 Prefabs 폴더 안에 있는 Bullet 프리팹을 더블클릭해서 프리팹 수정 씬을 열어보자. 그럼 선택된 Bullet 프리팹 게임 오브젝트의 내용을 인스펙터 뷰에서 볼 수 있는데 게임 오브젝트의 이름 아래를 보면 태그와 함께 Default라고 표시된 레이어를 찾을 수 있다.

 

레이어를 클릭해보면 Default, TrasparentFX, Ignor Raycast, Water, UI가 있다.

 

이 레이어에는 여러가지 역할이 있지만 대표적인 것이 바로 지금 배우고 있는 충돌 무시 설정이다.

 

항목들 중에서 제일 아래에 있는 [Add Layer]를 선택하면 레이어를 직접 만들 수 있다.

 

 

Bullet이라는 레이어를 만들어보자.

 

 

그리고 다시 Bullet 프리팹으로 돌아가서 레이어를 Bullet으로 설정한다.

 

그 다음은 레이어의 충돌 설정을 할 차례이다.

 

 

상단 메뉴 바에서 [Edit > Project Settings]를 선택해서 프로젝트 세팅 뷰를 연다.

 

 

이중에 Physics 탭으로 들어가면 레이어끼리의 충돌 설정을 할 수 있다. 가장 아래 쪽을 보면 직각삼각형 형태로 배치된 체크박스들을 볼 수 있는데 행이나 열마다 레이어의 이름이 적혀있는 것을 알 수 있다.

 

제일 앞 칸을 기준으로 보면 Default와 Bullet 사이는 체크가 되어있는데 이것은 Bullet 레이어와 Default 레이어의 오브젝트가 충돌하면 이 충돌을 감지하겠다는 뜻이다. 반대로 체크가 해제되면 두 레이어 사이의 충돌을 무시하겠다는 뜻이 된다.

 

우리는 Bullet끼리의 충돌을 무시할 계획이기 때문에 Bullet과 Bullet이 만나는 지점의 체크를 해제해준다.

 

프로젝트 세팅 뷰를 닫고 플레이를 시킨 뒤 총알을 발사해보자.

 

 

그럼 총알끼리 충돌해서 사라지지 않고 아주 잘 발사되는 것을 볼 수 있다.

 

단 레이어를 사용할 때 주의할 점은 사용자가 만들 수 있는 최대 레이어 갯수는 총 24개 뿐이기 때문에 불필요한 레이어를 함부로 남발하면 나중에 정작 필요한 레이어의 자리가 부족해지는 문제가 발생할 수 있다는 것이다.

 

반응형
  1. 료용 2020.04.22 01:51 신고

    베르님 유투브 파셨네요 좋은영상 기대하겠습니다.

    • wergia 2020.04.22 17:02 신고

      글만으로는 모자란 감이 있어서 구현 과정을 보여드리려고 만들었어요ㅎㅎ

Physics 

Collider 컴포넌트로 충돌체크하기

 

작성 기준 버전 :: 2019.2

 

[본 포스트의 내용은 유튜브 영상을 통해서도 확인할 수 있습니다]

 

게임에서는 많은 것들이 충돌한다.

 

캐릭터가 총알이나 화살에 맞기도 하고, 달리던 자동차가 건물에 부딪히기도 하며 약간은 다른 개념으로 보안용 레이저에 도둑인 캐릭터가 감지되어 경보가 울리기도 한다.

 

이런 오브젝트의 충돌들을 처리하기 위해서는 물리적인 충돌을 처리하는 방법을 알아야 한다.

 

Collider 컴포넌트

 

유니티 엔진에서 Collider라는 컴포넌트를 이용해서 충돌을 체크한다.

 

 

유니티 엔진에서 가장 대표적으로 사용되는 콜라이더로는 Box Collider, Sphere Collider, Capsule Collider가 있다.

 

 

씬 뷰에서 Collider 컴포넌트가 붙어있는 게임 오브젝트를 선택해보면 위의 이미지와 같은 초록색 선으로 표시되는 영역이 보이는데, 이것은 Collider 컴포넌트가 충돌을 감지하는 영역의 크기를 보여준다.

 

Collider 컴포넌트의 프로퍼티

 

이번에는 각 Collider 컴포넌트의 프로퍼티들을 살펴보자.

 

우선 Collider 컴포넌트들은 공통적으로 Is Trigger 프로퍼티와 Material 프로퍼티, Center 프로퍼티를 가지고 있다. 하지만 이 중에서 Is Trigger와 Material 프로퍼티는 좀 더 뒤에서 설명하도록 하고 먼저 Center 프로퍼티부터 하나씩 살펴보자.

 

Center 프로퍼티와 그 아래에 있는 프로퍼티들은 콜라이더 영역의 위치와 크기를 조절하는데 쓰인다.

 

Center

 

 

먼저 Center 프로퍼티는 게임 오브젝트의 중심으로부터 어느 위치에 콜라이더의 중심을 둘 것인가를 설정한다. 위 이미지처럼  Y 값을 바꾸면 게임 오브젝트의 중심보다 위쪽에 약간 위쪽에 콜라이더 영역이 표시된다. 단 이때의 좌표 기준을 게임 오브젝트의 로컬 좌표를 기준으로 동작한다.

 

Box Collider의 프로퍼티

 

Center 아래의 프로퍼티들은 콜라이더의 종류마다 조금씩 다르니 하나씩 설명해보도록 하겠다.

 

Size

 

 

Box Collider의 Size 프로퍼티는 콜라이더 영역의 크기를 정하는데 쓰인다. Vector3 타입이며 xyz 각 값은 게임 오브젝트의 로컬 좌표계의 축에 일치하게 동작하며, 이 사이즈의 1 단위는 1 유니티 미터를 의미한다.

 

Sphere Collier의 프로퍼티

 

Radius

 

 

Sphere Collider의 Radius 프로퍼티 역시 콜라이더 영역의 크기를 정하는데 쓰이는데, 이 값은 구체의 반지름으로 동작한다. Radius를 1로 정하면 콜라이더 영역의 지름은 2유니티 미터가 된다.

 

Capsule Collider

 

Radius

 

 

Capsule Collider의 Radius 값은 콜라이더 영역의 두께를 정하는데 쓰인다. 참고로 이 값이 Height 값보다 커지면, 콜라이더의 영역이 Sphere Collider처럼 그냥 구체 모양이 되버린다.

 

Height

 

 

Height 값은 Capsule Collider의 길이를 정하는데 쓰인다.

 

Direction

 

 

Direction 프로퍼티는 Capsule Collider의 Height를 변경했을때 길어지는 방향을 정하는 프로퍼티다. 값으로는 X-Axis, Y-Axis, Z-Axis가 있는데, 단어 그대로 로컬 좌표계의 각 축의 방향을 따른다.

 

참고로 사람이 서있을 때의 모양이 위 아래로 길쭉한 모양이 일반적이기 때문에 사람 형태의 캐릭터에 콜라이더를 부착할 때는 Capsule Collider를 주로 사용한다. 그리고 뚱뚱한 캐릭터면 Radius 값을 늘려서 Capsule Collider의 두께를 두껍게하고, 키가 큰 캐릭터면 Height 값을 키워서 길이를 늘리는 방식으로 사용된다.

 

Is Trigger

 

그럼 이제 잠시 뒤로 미뤄두었던 Is Trigger 옵션을 보자. 이 옵션은 콜라이더가 트리거(Trigger)로 동작할지, 콜리전(Collision)으로 동작할지를 정하는 프로퍼티이다.

 

콜리전은 벽이나 바닥처럼 다른 물체를 통과하지 못하게 가로막는 장애물을 뜻하고, 트리거는 마트의 도난 방지 장치처럼 물체를 통과시키되 지나가는 물체를 감지하는 것을 의미한다.

 

 

Sphere 게임 오브젝트를 Cube 게임 오브젝트 위로 떨어지게 만든 뒤 Cube 게임 오브젝트가 가진 Box Collider의 Is Trigger 옵션을 켜둔 상태와 꺼둔 상태로 각각 플레이를 해보면 Is Trigger가 꺼져있을 때는 Sphere가 Cube위에 멈추고, 켜져있을 때는 Cube를 통과해버리는 것을 볼 수 있다.

 

 

참고로, 이렇게 게임 오브젝트가 중력과 같은 물리효과를 받게 하기 위해서는 Rigidbody 컴포넌트를 붙여줘야 한다.

 

Material

 

Material 프로퍼티는 콜라이더가 충돌할 때, 어떤 재료의 물리적인 특성을 보여줄 지를 설정할 수 있는 프로퍼티이다. 이런 종류의 머티리얼을 Physics Material이라고 하는데, 프로젝트 뷰에 우클릭하고 [Create > Physics Material]을 선택해서 생성할 수 있다.

 

 

오브젝트가 고무공처럼 튀는 것을 구현하기 위해 생성한 피직스 머티리얼에 여러가지 옵션이 있지만, 지금은 간단하게 Bounciness 옵션을 0.8로 변경하고 Bounce Combine을 "Maximum"으로 설정한다.

 

 

Sphere 게임 오브젝트의 Sphere Collider에 넣고 플레이 버튼을 눌러보면 아까 전에는 Cube 위에 얌전히 멈췄던 Sphere가 마치 고무공처럼 튀어오르는 것을 볼 수 있다.

 

Collider 충돌 감지하는 스크립트 작성하기

 

이제 이 충돌을 스크립트에서 감지하는 방법을 알아보자.


유니티 엔진에서는 콜라이더끼리 충돌했을 때, 특정한 함수를 호출해준다. 그런데 앞에서 Is Trigger 프로퍼티에 대해서 설명할 때, 이 옵션의 상태에 따라서 트리거와 콜리전으로 나뉜다고 이야기했던 것을 기억할 것이다.

 

유니티 엔진은 이 트리거와 콜리전이 충돌하는 경우를 다르게 취급하고 다른 함수를 호출해준다.

 

OnCollision 이벤트

 

public class ColliTest : MonoBehaviour

{

    // Collider 컴포넌트의 is Trigger가 false인 상태로 충돌을 시작했을 때

    private void OnCollisionEnter(Collision collision)

    {

        Debug.Log("충돌 시작!");

    }

 

    // Collider 컴포넌트의 is Trigger가 false인 상태로 충돌중일 때

    private void OnCollisionStay(Collision collision)

    {

        Debug.Log("충돌 중!");

    }

 

    // Collider 컴포넌트의 is Trigger가 false인 상태로 충돌이 끝났을 때

    private void OnCollisionExit(Collision collision)

    {

        Debug.Log("충돌 끝!");

    }

}

 

콜리전끼리 충돌했을 때는 OnCollision 계열의 이벤트가 호출된다. 충돌 과정은 3단계로 나눠서 호출되는데, 충돌이 시작할 때는 Enter, 충돌이 지속되는 동안에는 Stay, 충돌이 끝나는 순간에는 Exit가 호출된다.

 

private void OnCollisionEnter(Collision collision)

{

    // 이 컴포넌트가 부착된 게임 오브젝트의 콜라이더와 충돌한 게임 오브젝트 가져오기

    var obj = collision.gameObject;

    // 특정 컴포넌트 가져오기

    var component = collision.gameObject.GetComponent<SomeComponent>();

    // 콜라이더 가져오기

    var collider = collision.collider;

    Debug.Log("충돌 시작!");

}

 

이 콜리전과 충돌한 게임 오브젝트를 가져오기 위해서는 매개변수로 전달되는 collision에서 .gameObject 프로퍼티를 호출하면 된다. 그리고 .GetComponent 함수를 사용하면 이 게임 오브젝트에 부착된 컴포넌트를 가져올 수도 있다.

 

OnTrigger 이벤트

 

public class ColliTest : MonoBehaviour

{

    // Collider 컴포넌트의 is Trigger가 true인 상태로 충돌을 시작했을 때

    private void OnTriggerEnter(Collider other)

    {

        Debug.Log(other.name + "감지 시작!");

    }

 

    // Collider 컴포넌트의 is Trigger가 true인 상태로 충돌중일 때

    private void OnTriggerStay(Collider other)

    {

        Debug.Log(other.name + "감지 중!");

    }

 

    // Collider 컴포넌트의 is Trigger가 true인 상태로 충돌이 끝났을 때

    private void OnTriggerExit(Collider other)

    {

        Debug.Log(other.name + "감지 끝!");

    }

}

 

OnTrigger 이벤트 역시 Enter, Stay, Exit의 과정을 거친다. 이 OnTrigger 이벤트는 콜리전이 트리거에 닿았을 때, 콜리전과 트리거 양 쪽에서 모두 호출될 수 있고, 트리거와 트리거가 닿았을 때도 호출된다.

 

private void OnTriggerEnter(Collider other)

{

    // 이 컴포넌트가 부착된 게임 오브젝트의 콜라이더와 충돌한 게임 오브젝트 가져오기

    var obj = other.gameObject;

    // 특정 컴포넌트 가져오기

    var component = other.gameObject.GetComponent<SomeComponent>();

    Debug.Log(other.name + "감지 시작!");

}

 

OnTrigger 이벤트에서도 OnCollision 이벤트에서처럼 매개변수로 전달받은 Collider 타입의 other 변수를 통해서 감지한 게임 오브젝트와 거기에 부착된 컴포넌트를 가지고 올 수 있다.

 

기초적인 Collider의 사용법을 이해했다면 링크된 유튜브 영상을 통해서 이 Collider 컴포넌트의 기능을 응용해서 간단한 게임 기능을 구현하는 방법을 확인할 수 있다.

반응형

RPG :: 캐릭터를 가리는 벽 투명하게 만들기


작성 기준 버전 :: 4.21.1


탑/다운에 가까운 쿼터뷰에서 내려보는 RPG에서는 건물이나 벽, 기둥 같은 오브젝트에 캐릭터가 가려지는 경우가 많다.



이런 경우 플레이어는 자신의 캐릭터의 위치를 찾기가 어려워지고 플레이에 매우 큰 지장을 준다. 그렇기 때문에 개발자들은 이런 문제를 해소하기 위해서 여러 가지 테크닉을 사용하는데 그 중 대표적인 것이 캐릭터를 가리고 있는 벽을 투명하게 만드는 것이다.


이 섹션에서는 캐릭터를 가리고 있는 벽을 투명하게 만드는 방법에 대해서 배워볼 것이다.


본격적인 내용에 들어가기에 앞서, 이 섹션은 진행하기 위해서는 다음과 같은 선행 지식이 필요하다.


머티리얼 인스턴싱 :: 머티리얼 파라미터와 머티리얼 인스턴스

C++ 코드에서 머티리얼 인스턴스 다이내믹 생성하고 다루기

콜리전과 콜리전 이벤트


이번 섹션은 지난 섹션 중 RPG :: 마우스 입력 이동 구현하기 섹션에 이어서 진행되는 섹션이다.



설계


이 기능을 구현하기 위한 설계는 다음과 같다.


1. 벽이나 기둥 같은 오브젝트가 가리는 위치에 캐릭터가 존재하고 있는지 확인하기 위한 콜리전을 깔아줄 SeeingThroughCollision 클래스


2. SeeingThroughCollision에게 신호를 받아서 투명하게 만들어질 SeeingThroughActor



이렇게 바닥에 콜리전을 까는 방법 이외에도 캐릭터와 카메라 사이에 콜리전을 두고 여기에 벽이나 기둥같은 오브젝트가 닿으면 투명하게 만드는 방법도 생각해볼 수 있다.



기능 구현


SeeingThroughActor


SeeingThroughActor 구현


새 C++ 클래스를 하나 추가하자. Actor 클래스를 상속방아서 SeeingThroughActor라는 이름으로 클래스를 생성한다.


 

이 클래스는 콜리전으로부터 통지를 받아서 자신의 머티리얼을 투명하게 하는 역할을 한다.


SeeingThroughActor.h에 다음 멤버 변수들을 추가한다.


private:
    UPROPERTY(EditAnywhere, meta = (AllowPrivateAccess = "true"))
    class UStaticMeshComponent* SeeingTroughMesh;

    UPROPERTY(EditAnywhere, meta = (AllowPrivateAccess = "true"))
    float SeeingThroughTime;

    bool bSeeingThrough;

    float RunningTimer;


먼저 SeeingThroughMesh는 메시가 가진 머티리얼을 다이내믹 인스턴스로 만들어서 투명화 작업을 하기 위한 변수이다.


SeeingThroughTime은 메시가 투명해지는데까지 걸리는 시간에 대한 변수이다. 이 변수는 외부에 공개해서 다른 개발자가 수정할 수 있게 하였다.


bSeeingThrough는 지금 메시가 투명해지는 중인지 아니면 불투명해지는 중인지에 대한 변수이다.


RunningTimer는 진행도를 위한 변수이다.


그 다음에는 SeeingThroughActor.cpp로 가서 스태틱 메시 컴포넌트의 기능을 사용하기 위해서 다음 전처리기를 추가한다.


#include "Engine/Classes/Components/StaticMeshComponent.h"


그리고 ASeeingThroughActor::ASeeingThroughActor() 생성자 함수로 가서 멤버 변수들을 초기화하는 코드를 추가한다.


SeeingTroughMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("SeeingThroughMesh"));
RootComponent = SeeingTroughMesh;
SeeingTroughMesh->CreateDynamicMaterialInstance(0);

SeeingTroughMesh->SetCollisionProfileName(TEXT("InvisibleWall"));

SeeingThroughTime = 0.3f;
RunningTimer = 0.0f;
bSeeingThrough = false;


SeeingThroughMesh를 생성한 뒤, RootComponent로 설정해주고 다이내믹 머티리얼 인스턴스를 생성한다. 그리고 콜리전 프로필을 InvisibleWall로 설정해주는데, 이것은 벽이 물체는 가로막고 마우스를 클릭했을때 발생하는 트레이스는 통과시키기 위함이다.


그리고 투명해지는데 걸리는 시간은 기본으로 0.3초로 설정한다.


BeginPlay() 함수에 다음 코드를 추가한다.


PrimaryActorTick.SetTickFunctionEnable(false);


게임 시작했을때 Tick() 함수를 작동하지 않도록 만들어서 성능 낭비가 없도록 한다.


그 다음 Tick() 함수를 다음과 같이 구현한다.


void ASeeingThroughActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    RunningTimer += DeltaTime;

    float Opacity = 0.0f;
    if (bSeeingThrough)
    {
        Opacity = FMath::Lerp(1.0f, 0.0f, RunningTimer * (1.0f / SeeingThroughTime));
    }
    else
    {
        Opacity = FMath::Lerp(0.0f, 1.0f, RunningTimer * (1.0f / SeeingThroughTime));
    }

    SeeingTroughMesh->SetScalarParameterValueOnMaterials(TEXT("Opacity"), Opacity);

    if (RunningTimer > SeeingThroughTime)
    {
        RunningTimer = 0.0f;
        PrimaryActorTick.SetTickFunctionEnable(false);
    }
}


위의 코드는 틱이 작동하는 동안 bSeeingThrough의 상태에 따라서 다이내믹 머티리얼 인스턴스의 머티리얼 파라미터 "Opacity"를 0에서 1로 만들거나 1에서 0으로 만들고 작동이 끝나면 Tick() 함수의 작동을 멈추게 한다. 참고로 머티리얼 파라미터 "Opacity"는 이후 작업에서 추가한다.


다음 함수의 선언을 헤더에 추가한다.


public:

    void SetShowSeeingThrough(bool bThroughShow);


그리고 SeeingThroughActor.cpp로 가서 함수 구현을 완료한다.


void ASeeingThroughActor::SetShowSeeingThrough(bool bThroughShow)
{
    bSeeingThrough = bThroughShow;
    if (RunningTimer != 0.0f)
    {
        RunningTimer = SeeingThroughTime - RunningTimer;
    }
    PrimaryActorTick.SetTickFunctionEnable(true);
}


SetShowSeeingThrough() 함수는 bShow 변수를 받아서 액터가 투명해지기 시작하는지 불투명해지기 시작하는지 결정한 뒤 Tick() 함수를 작동시킨다.


코드 작업이 끝났다면 솔루션 탐색기에서 프로젝트를 빌드한 뒤, 에디터로 돌아간다.


투명해지는 만들기


이번에는 SeeingThroughActor가 사용할 머티리얼을 만들 차례이다. Props 폴더 안에 Materials 폴더를 만든 다음, 콘텐츠 브라우저 패널의 파일 창에 우클릭해서 머티리얼을 선택한다. 그리고 생성된 머티리얼의 이름을 M_SeeingThrough로 한다.



머티리얼을 더블클릭해서 머티리얼 에디터를 열고 디테일 패널에서 Material 카테고리의 Blend Mode를 Translucent로 설정한다.



그리고 TextureSamleParameter2D와 ScalarParameter를 추가하고 각각 이름을 Texture와 Opacity로 한다.



그리고 Opacity 파라미터 노드를 선택한 뒤, 디테일 패널에서 Default Value를 1로 설정한다.


그 다음, 적용과 저장을 하고 머티리얼 에디터를 닫는다.


언리얼 에디터로 돌아와서, 콘텐츠 브라우저 패널에서 방금 만든 머티리얼을 우클릭하고 머티리얼 인스턴스 생성을 선택한다.



머티리얼 인스턴스가 만들어지면 더블클릭해서 머티리얼 인스턴스 에디터를 열고 디테일 패널에서 Opacity를 체크해주고 머티리얼 인스턴스를 저장한 뒤, 머티리얼 인스턴스 에디터를 닫는다.



SeeingThroughActor 배치


이제 SeeingThroughActor를 배치할 차례이다. 콘텐츠 브라우저 패널에서 SeeingTroughActor를 찾아서 벽 토대 위에 배치한다.



SeeingThroughActor의 SeeingThroughMesh를 선택하고 Static Mesh에 Box를 할당한다.



그 다음, Material에 방금 전에 만든 M_SeeingThrough_Inst를 할당한다.


 

SeeingThroughActor의 스케일을 {1.0, 12.0, 5.0}으로 설정한다.



액터를 복사해서 다음과 같이 만든다.






SeeingThroughCollision


SeeingThroughCollision 구현


Actor 클래스를 상속받아서 SeeingThroughCollision 이라는 이름으로 클래스를 생성한다.


 

SeeingThroughCollision.h에 다음 멤버 변수 선언을 추가한다.


private:
    UPROPERTY()
    class UBoxComponent* SeeingThroughCollision;

    UPROPERTY(EditAnywhere, meta = (AllowPrivateAccess = "true"))
    TArray<class ASeeingThroughActor*> SeeingThroughActors;


SeeingThroughActor를 배열로 선언한 이유는 SeeingThroughCollision과 SeeingThroughActor를 1:1 매치를 시킬 수도 있지만, 1:N의 매치도 가능하게 유연성을 주기 위한 것이다.


SeeingThroughCollision.cpp로 가서 박스 컴포넌트와 SeeingThroughActor의 기능을 쓰기 위해서 다음 전처리기들을 추가한다.


#include "Engine/Classes/Components/BoxComponent.h"

#include "SeeingThroughActor.h"


ASeeingThroughCollision::ASeeingThroughCollision() 생성자 함수에 초기화 코드를 추가한다.


SeeingThroughCollision = CreateDefaultSubobject<UBoxComponent>(TEXT("SeeingThroughCollision"));
RootComponent = SeeingThroughCollision;
SeeingThroughCollision->SetCollisionProfileName(TEXT("OverlapOnlyPawn"));


콜리전의 프로필 네임을 OverlapOnlyPawn으로 설정해서 폰이 오버랩되었을 때만 반응하도록 만든다.


다시 SeeingThroughCollision.h로 가서 겹침 이벤트를 받아서 처리할 함수를 덮어씌우는 코드를 추가한다.


protected:
    virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;

    virtual void NotifyActorEndOverlap(AActor* OtherActor) override;


그리고 SeeingThroughCollision.cpp로 가서 두 함수를 구현한다.


void ASeeingThroughCollision::NotifyActorBeginOverlap(AActor * OtherActor)
{
    ARpgCharacter* Character = Cast<ARpgCharacter>(OtherActor);
    if (Character)
    {
        for (auto SeeingThroughActor : SeeingThroughActors)
        {
            SeeingThroughActor->SetShowSeeingThrough(true);
        }
    }
}

void ASeeingThroughCollision::NotifyActorEndOverlap(AActor * OtherActor)
{
    ARpgCharacter* Character = Cast<ARpgCharacter>(OtherActor);
    if (Character)
    {
        for (auto SeeingThroughActor : SeeingThroughActors)
        {
            SeeingThroughActor->SetShowSeeingThrough(false);
        }
    }
}


코드 작업이 끝나면 솔루션 탐색기에서 프로젝트를 빌드하고 언리얼 에디터로 넘어간다.


SeeingThroughCollision 배치


콘텐츠 브라우저 패널에서 SeeingThroughCollision을 드래그해서 레벨에 배치한다.



배치한 SeeingThroughCollision의 위치를 {-420.0 -790.0 0.0}으로, 스케일을 {4.0, 22.0, 1.0}으로 수정한다. 그렇게 하면 벽 너머에 캐릭터가 들어갔을 때 가려지는 영역에 콜리전이 위치하게 된다.



그 다음, SeeingThroughActors 배열에 + 버튼을 눌러서 엘리먼트를 추가해준 다음, 투명해져야 하는 벽을 할당한다.



 

테스트


플레이 버튼을 누르고 캐릭터를 벽 뒤로 이동시켜보면 벽이 투명해지고 그 지역을 벗어나면 다시 벽이 불투명해지는 것을 확인할 수 있다.



나머지 기둥과 벽으로 가려지는 부분에도 적당하게 SeeingThroughCollision을 배치하고 SeeingThroughActor와 매칭시켜서 벽이 투명해지도록 만들어보자.

반응형

콜리전과 콜리전 이벤트

 

작성 기준 버전 :: 4.21.1

 

콜리전(Collision)은 언리얼 엔진에서 물리적인 충돌이나 레이 캐스팅 실시간 처리를 해준다. 이러한 물리 시뮬레이션은 Collision Response(콜리전 반응) 및 Trace Response(트레이스 반응) 설정을 통해서 다른 오브젝트 유형과 어떻게 상호작용할지 정의된다.

 

블록(Block), 겹침(Overlap), 무시(Ignore)

 

충돌 작용은 블록(Block), 겹침(Overlap), 무시(Ignore)로 나누어진다.

 

 

블록은 충돌하는 두 오브젝트가 모두 블록이어야 두 오브젝트가 충돌했을 때, 겹치지 않고 서로에게 막히게 된다. 콜리전 옵션 중에 Simulation Generates Hit Event 프로퍼티를 true로 설정하면 이러한 충돌이 발생했을 때 Event Hit을 받아서 블루프린트나 디스트럭터블 액터, 트리거 등에 사용할 수 있고 이를 응용해서 총알에 맞아서 깨지는 유리창 등을 구현할 수 있다.

 

 

겹침은 다른 오브젝트를 통과시키지만, 만약 Generate Overlap Event가 활성화된 상태라면 Overlap Event를 발생시킨다. 이 겹침 이벤트가 발생하려면 두 오브젝트 모두 겹침 이상으로 설정되어 있어야 한다. 만약 한 쪽은 겹침이고 다른 한 쪽이 무시인 상태라면 겹침 이벤트는 발생하지 않는다.

 

 

무시는 모든 오브젝트와 트레이스를 통과시키며 어떠한 이벤트도 발생시키지 않는다.

 

 

콜리전 이벤트

 

콜리전이 발생시키는 이벤트를 다루는 방법에 대해서 배워보자.

 

히트 이벤트(Hit Event)

 

히트 이벤트는 블록 상태인 오브젝트들이 서로 충돌했을 때 발생하는 이벤트로 해당 오브젝트에게 통지된다. 히트 이벤트를 발생하게 하기 위해서는 우선 콜리전의 프로퍼티 중에 Simulation Generates Hit Event를 true로 설정해줘야 한다.

 

우선 히트 이벤트를 받아서 처리하기 위한 클래스를 BlockCollisionActor라는 이름으로 Actor 클래스를 상속받아서 생성하자.

 

 

그리고 비주얼 스튜디오로 가서 BlockCollisionActor.h에 다음 멤버 변수와 함수 선언 코드를 추가한다.

 

protected:
    virtual void NotifyHit(UPrimitiveComponent *MyComp, AActor *Other, UPrimitiveComponent *OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult &Hit) override;

public:   
    UPROPERTY(EditAnywhere)
    class UStaticMeshComponent* BoxMesh;

 

NotifyHit() 함수는 부모 클래스에서 상속받은 것으로 이것을 덮어씌워 작성함으로써 충돌이 발생했을 때 개발자가 원하는 동작을 처리하도록하는 것이 가능해진다. BoxMesh는 다른 물체와 충돌할 메시를 담을 변수이다.

 

그 다음엔 BlockCollisionActor.cpp로 가서 스태틱 메시 컴포넌트를 사용하기 위해서 다음 전처리기를 추가한다.

 

#include "Engine/Classes/Components/StaticMeshComponent.h"

 

ABlockCollisionActor::ABlockCollisionActor() 생성자 함수에 다음과 같이 BoxMesh 변수를 초기화하는 코드를 추가한다.

 

ABlockCollisionActor::ABlockCollisionActor()
{
    PrimaryActorTick.bCanEverTick = true;

    BoxMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BoxMesh"));
    RootComponent = BoxMesh;
}

 

그리고 아까 선언한 NotifyHit() 함수를 다음과 같이 충돌이 발생했을 때 로그를 출력하도록 구현한다.

 

void ABlockCollisionActor::NotifyHit(UPrimitiveComponent *MyComp, AActor *Other, UPrimitiveComponent *OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult &Hit)
{
    UE_LOG(LogTemp, Log, TEXT("NotifyHit"));
}

 

코드 작업이 마무리되었다면 프로젝트를 빌드하고 에디터로 돌아가서 히트 이벤트 테스트를 위한 작업을 진행해보자.

 

먼저 BlockCollisionActor를 레벨에 배치한다.

 

 

BlockCollisionActor의 디테일 패널에서 BoxMesh를 선택하고 비어있는 스태틱 메시 프로퍼티에 모드 패널의 큐브 스태틱 메시를 드래그래서 넣는다.

 

 

그 다음 디테일 패널에서 Collision 카테고리를 찾아서 Simulation Generates Hit Event를 체크해준다.

 

 

BlockCollisionActor 배치가 끝났다면 BlockCollisionActor와 충돌할 액터를 추가해준다. 모드 패널에서 구체 메시 하나를 BlockCollisionActor 위에 배치한다.

 

 

그 다음 배치한 구체의 모빌리티를 무버블로 하고, Physics 카테고리에서 Simulate Physics를 체크해준다.

 

 

다음 이미지와 같이 세팅이 모두 끝났다면 플레이 버튼을 눌러보자.

 

 

그러면 공이 떨어져서 상자에 맞는 순간 NotifyHit 로그가 출력되는 것을 볼 수 있다.

 

 

 

 

 

겹침 이벤트(Overlap Event)

 

겹침 이벤트는 겹침으로 설정된 오브젝트에 겹침이나 블록으로 설정된 다른 오브젝트가 겹쳐지면 발생하는 이벤트이다. 겹침 이벤트를 발생시키기 위해서는 Collision 카테고리에서 Generate Overlap Event를 true로 설정해줘야 한다.

 

겹침 이벤트를 처리할 클래스를 OverlapCollisionActor라는 이름으로 Actor 클래스를 상속해서 생성한다.

 

 

OverlapCollisionActor.h에 다음 멤버 변수와 함수 선언 코드를 추가한다.

 

protected:
    virtual void NotifyActorBeginOverlap(AActor *OtherActor) override;
    virtual void NotifyActorEndOverlap(AActor *OtherActor) override;

public:   
    UPROPERTY(EditAnywhere)
        class UStaticMeshComponent* BoxMesh;

 

NotifyActorBeginOverlap() 함수는 겹침이 시작되었을 때 실행되는 함수이고 NotifyActorEndOverlap() 함수는 겹침이 끝났을 때 실행되는 함수이다. 이 두함수를 통해서 우리는 언제 겹침이 시작되었는지, 언제 겹침이 끝났는지를 알 수 있다.

 

BoxMesh는 사실 큰 필요는 없지만 구체가 통과해서 지나갔음을 보여주기 위해서 추가한다. 하지만 만약 스태틱 메시를 사용하지 않을 것이라면 BoxComponent나 SphereComponent, CapsuleComponent 같은 콜리전 컴포넌트를 사용해야 한다.

 

그 다음엔 OverlapCollisionActor.cpp로 가서 스태틱 메시 컴포넌트를 사용하기 위해 다음 전처리기를 추가한다.

 

#include "Engine/Classes/Components/StaticMeshComponent.h"

 

AOverlapCollisionActor::AOverlapCollisionActor() 생성자 함수에 BoxMesh를 초기화하는 코드를 추가한다.

 

AOverlapCollisionActor::AOverlapCollisionActor()
{
    PrimaryActorTick.bCanEverTick = true;

    BoxMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BoxMesh"));
    RootComponent = BoxMesh;
}

 

그리고 NotifyActorBeginOverlap() 함수와 NotifyActorEndOverlap() 함수를 구현한다.

 

void AOverlapCollisionActor::NotifyActorBeginOverlap(AActor * OtherActor)
{
    UE_LOG(LogTemp, Log, TEXT("NotifyActorBeginOverlap"));
}

void AOverlapCollisionActor::NotifyActorEndOverlap(AActor * OtherActor)
{
    UE_LOG(LogTemp, Log, TEXT("NotifyActorEndOverlap"));
}

 

코드 수정이 끝나면 솔루션 탐색기에서 프로젝트를 빌드하고 에디터로 돌아가서 겹침 이벤트를 테스트하기 위한 작업을 한다.

 

우선 아까 전에 배치한 BlockCollisionActor보다 높은 공중에 구체가 통과해서 지나갈 수 있게 OverlapCollisionActor를 배치한다.

 

 

배치된 OverlapCollisionActor를 선택하고 디테일 패널에서 BoxMesh 컴포넌트를 선택한 다음에 비어있는 스태틱 메시에 모드에서 큐브 메시를 드래그 해서 할당해준다.

 

 

그 다음 Collision 카테고리에서 Generate Overlap Event를 체크해주고 Collision Preset을 OverlapAll로 설정한다.

 

 

마지막으로 구체를 선택한 다음 디테일 패널의 Collision 카테고리에서 Generate Overlap Event가 체크되어 있지 않다면 체크를 해준다. 겹침 이벤트가 발생하려면 겹치는 양 오브젝트에 모두 Generate Overlap Event가 true여야 한다.

 

 

플레이 버튼을 눌러보면 구체가 OverlapCollisionActor를 통과하면서 겹침이 시작되면 NotifyActorBeginOverlap 로그가 출력되고, 겹침이 끝나면 NotifyActorEndOverlap 로그가 출력되는 것을 확인할 수 있다.

 

 

반응형

제대로 따라가기 (8) C++ 프로그래밍 튜토리얼 :: 일인칭 슈팅 C++ 튜토리얼 (3)

작성버전 :: 4.21.0

언리얼 엔진 튜토리얼인 일인칭 슈팅 C++ 튜토리얼에서는 C++ 코드 작업을 통해서 기본적인 일인칭 슈팅(FPS) 게임을 만드는 법을 배울 수 있다.

 

이번 튜토리얼은 각 하위 섹션들의 길이가 길어서 분할되어 작성된다.

 

튜토리얼대로 하면 문제가 발생해서 제대로 따라갈 수 없는 부분으로 동작이 가능하게 수정해야하는 부분은 빨간 블럭으로 표시되어 있다.


이번 튜토리얼에서 새로 배우게 되는 내용은 글 제일 끝에 "이번 섹션에서 배운 것"에 정리된다.

 

수정

 

지난 섹션에서 VisibleDefaultOnly는 버전이 바뀌어서 사라진 지정자라고 했던 부분은 잘못된 부분입니다.

 

VisibleDefaultsOnly는 정상적으로 존재하는 UPROPERTY 지정자입니다. 제가 실수로 VisibleDefaultOnly로 오타를 내서 컴파일러가 지정자가 없다고 에러를 띄웠었습니다. 잘못된 정보로 혼동을 드린 점에 대해서 사과드립니다. 다음부터는 제대로된 확인을 거친 후, 글을 올리도록 하겠습니다.

 

지난 섹션의 잘못된 부분은 수정되었습니다.(지난 섹션 :: 제대로 따라가기 (7) C++ 프로그래밍 튜토리얼 :: 일인칭 슈팅 C++ 튜토리얼 (2))

 

3. 발사체 구현

 

이번 섹션에서는 일인칭 슈팅 게임에서 발사체를 구현하는 방법에 대해서 배울 수 있다.

 

3-1. 게임에 발사체 추가

 

이전 섹션에서 캐릭터 구성을 마쳤으니, 이제 발사체 무기를 구현하여 발사하면 단순한 수류탄 같은 발사체가 화면 중앙에서 발사되어 월드에 충돌할 때까지 날아가도록 만들어보자. 이번 단계에서는 발사체(Projectile)에 쓸 입력을 추가하고 새 코드 클래스를 만들 것이다.

 

발사 액션 매핑 추가

 

편집 메뉴에서 프로젝트 세팅 창을 연다. 그리고 엔진 섹션에서 입력을 선택한 뒤, 액션 매핑에 아래와 같이 "Fire" 라는 입력 세팅을 추가 한다.

 

 

발사체(Projectile) 클래스 추가

 

파일 메뉴에서 새로운 C++ 클래스... 를 선택하고 Actor 클래스를 부모 클래스로 선택하고 다음을 클릭한다.

 

 

새 클래스 이름을 "FPSProjectile"로 하고 클래스 생성을 클릭한다.

 

 

USphereComponent 추가

 

FPSProjectile.h로 가서 USphereComponent의 선언을 다음처럼 추가해준다.

 

UPROPERTY(VisibleDefaultsOnly, Category = "Projectile")
USphereComponent* CollisionComponent;

 

USphereComponenet가 정의되지 않았다고 에러가 발생한다면, USphereComponent 앞에 class 키워드를 붙여주자.

class USphereComponent* CollisionComponent;

 

그 다음엔 FPSProjectile.cpp의 AFPSProjectile::AFPSProjectile() 생성자 함수에서 다음 코드를 추가한다.

CollisionComponent = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComponent"));
CollisionComponent->InitSphereRadius(15.0f);
RootComponent = CollisionComponent;

 

CollisionComponent에서 함수를 호출하려고 할 때, 불완전한 형식은 사용할 수 없다는 에러가 발생하면 "Engine/Classes/Components/SphereComponent.h"를 cpp의 전처리기에 추가해주자.

#include "Engine/Classes/Components/SphereComponent.h"

 

프로젝타일 무브먼트 컴포넌트(Projectile Movement Component) 추가

 

FPSProjectile.h의 하단에 다음 코드를 추가한다.

UPROPERTY(VisibleAnywhere, Category = "Movement")
UProjectileMovementComponent* ProjectileMovementComponent;

 

UProjectileMovementComponent가 정의되지 않았다고 에러가 발생한다면, UProjectileMovementComponent 앞에 class 키워드를 붙여주자.

class UProjectileMovementComponent* ProjectileMovementComponent;

 

다시, FPSProjectile.cpp의 AFPSProjectile::AFPSProjectile() 생성자 함수에서 다음 코드를 추가한다.

ProjectileMovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovementComponent"));
ProjectileMovementComponent->SetUpdatedComponent(CollisionComponent);
ProjectileMovementComponent->InitialSpeed = 3000.0f;
ProjectileMovementComponent->MaxSpeed = 3000.0f;
ProjectileMovementComponent->bRotationFollowsVelocity = true;
ProjectileMovementComponent->bShouldBounce = true;
ProjectileMovementComponent->Bounciness = 0.3f;

 

ProjectileMovementComponent에서 함수를 호출하려고 할 때, 불완전한 형식은 사용할 수 없다는 에러가 발생하면 "Engine/Classes/GameFramework/ProjectileMovementComponent.h"를 cpp의 전처리기에 추가해주자.

#include "Engine/Classes/GameFramework/ProjectileMovementComponent.h"

 

발사체 초기 속도 설정

 

FPSProjectile.h로 가서 다음의 함수 선언을 추가한다.

void FireInDirection(const FVector& ShootDirection);

 

이 함수가 발사체의 발사를 담당한다.

 

FPSProjectile.cpp에 함수의 정의부를 추가한다.

void AFPSProjectile::FireInDirection(const FVector& ShootDirection)
{
    ProjectileMovementComponent->Velocity = ShootDirection * ProjectileMovementComponent->InitialSpeed;
}

 

발사체의 속력은 ProjectileMovementComponent에 의해 정의되므로 발사 방향만 제공해주면 된다.

 

발사 입력 액션 바인딩

 

FPSCharacter.h로 가서 다음의 함수 선언을 추가한다.

UFUNCTION()
void Fire();

 

FPSCharacter.cpp의 SetupPlayerInputComponent() 함수에 다음 바인딩을 추가한다.

InputComponent->BindAction("Fire", IE_Pressed, this, &AFPSCharacter::Fire);

 

그리고, Fire() 함수의 정의부 역시 추가한다.

void AFPSCharacter::Fire()
{
}

 

발사체의 스폰 위치 정의

 

FPSProjectile 액터를 스폰하는 Fire() 함수 구현을 위해서는 발사체를 스폰할 위치와, 스폰해야할 발사체의 클래스를 고려해야 한다.

 

FPSCharacter.h에 다음 코드를 추가한다.

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GamePlay)
FVector MuzzleOffset;

UPROPERTY(EditDefaultsOnly, Category = Projectile)
TSubclassOf<class AFPSProjectile> ProjectileClass;

 

코드 컴파일 및 검사

 

이제 새로 구현된 발사체 코드를 컴파일하고 검사할 차례다. 솔루션 탐색기에서 FPSProject에 우클릭하고 빌드(Build)를 선택하여 프로젝트를 컴파일한다.

 

 

 

3-2. 발사 구현

 

이번 단계에서는 캐릭터가 발사체 발사를 할 수 있도록 Fire() 함수를 구현해본다.

 

Fire 함수 구현

 

FPSCharacter.cpp 상단에 "FPSProjectile.h"를 포함시킨다.

#include "FPSProjectile.h"

 

FPSCharacter.cpp에 만들어둔 Fire() 함수의 바디를 다음과 같이 수정한다.

void AFPSCharacter::Fire()
{
    if (ProjectileClass)
    {
        FVector CameraLocation;
        FRotator CameraRotation;
        GetActorEyesViewPoint(CameraLocation, CameraRotation);

        FVector MuzzleLocation = CameraLocation + FTransform(CameraRotation).TransformVector(MuzzleOffset);
        FRotator MuzzleRotation = CameraRotation;

        MuzzleRotation.Pitch += 10.0f;
        UWorld* World = GetWorld();
        if (World)
        {
            FActorSpawnParameters SpawnParams;
            SpawnParams.Owner = this;
            SpawnParams.Instigator = Instigator;
            AFPSProjectile* Projectile = World->SpawnActor<AFPSProjectile>(ProjectileClass, MuzzleLocation, MuzzleRotation, SpawnParams);
            if (Projectile)
            {
                FVector LaunchDirection = MuzzleRotation.Vector();
                Projectile->FireInDirection(LaunchDirection);
            }
        }
    }
}

 

변경사항을 저장하고 프로젝트를 빌드한다.

 

 

프로젝타일 블루프린트 빌드하기

 

다음 링크에서 샘플 메시를 받아서 압축을 풀고 진행해야 한다. "프로젝타일 메시"

 

콘텐츠 브라우저의 파일 창에 우클릭해서 /Game에 임포트... 를 선택해서 임포트 대화창을 연다.

 

 

Sphere.fbx 메시 파일을 찾아서 선택하고 임포트를 선택해서 메시를 프로젝트에 추가하고, 저장 버튼을 클릭해서 임포트된 스태틱 메시를 저장한다.

 

콘텐츠 브라우저에서 Blueprints 폴더에 들어간다.

 

신규 추가 버튼을 클릭하고 블루프린트 클래스를 선택한다.

 

모든 클래스 드롭다운 메뉴를 펼친다음 검색창에 FPSProjectile을 입력하고 FPSProjectile 클래스를 선택한다.

 

 

새 블루프린트의 이름을 BP_FPSProjectile로 정하고 아이콘을 더블클릭해서 블루프린트 에디터를 연다.

 

 

컴포넌트 탭에서 CollisionComponent를 클릭하고 컴포넌트 추가 드롭다운 목록에서 Static Mesh를 찾아서 추가하고 이름은 ProjectileMeshComponent로 한다.

 

 

디테일 탭의 Static Mesh 섹션에서 "없음"이라고 된 드롭다운 메뉴를 클릭해서 Sphere 스태틱 메시를 선택한다.

 

 

X, Y, Z 스케일 값을 0.09로 설정한다. 자물쇠 아이콘을 클릭하면 세 축을 모두 고정시켜 상대 비율이 유지된다.

 

 

ProjectileMeshComponent의 콜리전 프리셋 값을 NoCollision으로 설정한다. (콜리전에 이 스태틱 메시가 아닌 SphereComponent를 사용한다.)

 

 

블루프린트를 컴파일, 저장한 뒤 블루프린트 에디터를 닫는다.

 

BP_FPSCharacter를 더블클릭해서 블루프린트 에디터를 열고 Projectile Class 프로퍼티를 찾은 다음 BP_FPSProjectile로 설정한다.

 

 

그리고 Muzzle Offset 프로퍼티를 {100, 0, 0}으로 설정하여 발사체를 카메라 약간 앞에서 생성되게 한다.

 

 

블루프린트를 컴파일, 저장 후 블루프린트 에디터를 닫는다.

 

게임 내 발사체 발사 테스트

 

레벨 에디터에서 플레이 버튼을 클릭하여 PIE 모드에 들어가서 발사체를 발사해보자.

 

좌클릭하면 발사체가 월드로 발사된다.

 

 

 

Esc키를 누르거나 중지 버튼을 클릭하여 PIE 모드를 빠져나가자.

 

 

 

 

3-3. 발사체 콜리전 및 수명 구성

 

지금 만들어진 발사체는 아무리 긴 시간이 지나도 사라지지 안고, 월드의 다른 오브젝트와 충돌하지 않는 상태이다. 이번 단계에서는 발사체의 충돌과 수명을 구성한다.

 

발사체의 수명 기간 제한

 

FPSProjectile.cpp에서 AFPSProjectile::AFPSProjectile() 생성자 함수에 다음 코드를 추가하여 발사체의 수명을 설정한다.

InitialLifeSpan = 3.0f;

 

발사체의 충돌 세팅 편집

 

언리얼 엔진에는 여러가지 프리셋 콜리전 채널이 포함되어 있으나, 커스터마이징 프리셋 콜리전 채널을 만들 수도 있다.

 

프로젝트 세팅 창의 엔진 섹션에서 콜리전을 선택하면 콜리전 채널 프리셋들을 확인할 수 있다.

 

 

 

 

새 오브젝트 채널... 을 선택하여 콜리전 채널을 새로 만든다. 새로운 콜리전 채널 이름을 "Projectile"이라 하고 기본 반응(Default Response)를 Block으로 설정하여 수락을 누른다.

 

 

프리셋에 새... 버튼을 클릭해서 새 프로필의 이름을 "Projectile"로 하고, 다음 이미지를 참고해서 콜리전 프리셋을 설정하자.

 

 

새 콜리전 채널 세팅 사용

 

FPSProjectile.cpp의 생성자에서 CollisionComponent 설정 코드의 아래에 다음 줄을 추가한다.

CollisionComponent->BodyInstance.SetCollisionProfileName(TEXT("Projectile"));

 

변경사항을 저장하고 솔루션 탐색기에서 FPSProject를 우클릭해서 빌드를 선택해서 프로젝트를 컴파일한다.

 

 

 

3-4. 월드와 상호작용하는 프로젝타일

 

프로젝타일의 콜리전 상호작용을 감지할 수 있게 되었으니, 이제 그 콜리전에 어떻게 반응할지를 결정할 차례다. 이번 단계에서는 콜리전 이벤트에 반응하는 FPSProjectile에 OnHit() 함수를 추가할 것이다.

 

프로젝타일이 콜리전에 반응하도록 만들기

 

FPSProjectile.h를 열어서 클래스 정의에 다음 코드를 추가한다.

UFUNCTION()
void OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit);

 

FPSProjectile.cpp에서 OnHit() 함수의 동작을 구현한다.

void AFPSProjectile::OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit)
{
    if (OtherActor != this && OtherComponent->IsSimulatingPhysics())
    {
        OtherComponent->AddImpulseAtLocation(ProjectileMovementComponent->Velocity * 100.0f, Hit.ImpactPoint);
    }
}

 

그리고 FPSProjectile 생성자에 CollisionComponent 생성 코드 뒤에 다음 코드를 추가한다.

CollisionComponent->OnComponentHit.AddDynamic(this, &AFPSProjectile::OnHit);

 

변경사항들을 저장하고 솔루션 탐색기에서 FPSProject에 우클릭한 뒤, 빌드를 선택해서 프로젝트를 컴파일한다.

 

 

프로젝타일 콜리전 테스트

 

빌드 완료 후, 언리얼 에디터로 돌아가서 Floor 스태틱 메시를 복사 & 붙여넣기를 한다.

 

이름이 Floor2인 바작 메시 사본의 비율 고정이 풀렸는지 확인하고 스케일을 {0.2, 0.2, 3.0}으로 설정하고 위치를 {320, 0, 170}으로 조정한다.

 

 

Floor2의 디테일 패널에서 Physics 섹션을 찾은 뒤 Simulate Physics 옵션을 체크한다.

 

 

맵을 저장하고 BP_FPSProjectile을 더블클릭하여 블루프린트 에디터를 열고 컴포넌트 탭에서 ProjectileMeshComponent를 클릭한다.

 

디테일 패널에서 Collision 아래의 Collision Presets 프로퍼티를 Projectile로 설정한다.

 

 

블루프린트를 컴파일, 저장한 뒤 블루프린트 에디터를 닫는다.

 

플레이 버튼을 눌러 PIE 모드에 들어간다. 좌클릭으로 상자를 향해 발사체를 발사하면 상자가 발사체에 맞고 튕겨나가는 모습을 볼 수 있다.

 

 

레벨 에디터에서 중지 버튼을 눌러서 PIE 모드에서 빠져나간다.

 

 

3-5. 뷰포트에 조준선 추가

 

이번 단계에서는 게임에 조준선 HUD 요소를 추가하려 조준할 수 있도록 해보자.

 

조준선 애셋 임포트

 

시작하기에 앞서, 다음 링크에서 샘플 이미지를 다운로드하고 압축을 푼다. "샘플 조준선 이미지"

 

콘텐츠 브라우저의 파일 창에 우클릭하여 /Game에 임포트... 를 눌러서 임포트 대화 상자를 연다.

 

 

crosshair.TGA 이미지 파일을 찾아서 임포트한다.

 

새 HUD 클래스 추가

 

파일 메뉴에서 새로운 C++ 클래스... 을 선택한다.

 

부모 클래스 선택 메뉴가 열리면 HUD 클래스를 찾아서 부모 클래스로 선택하고 다음을 클릭한다.

 

 

클래스 이름에 "FPSHUD"를 입력하고 클래스를 생성한다.

 

 

FPSHUD.h에 다음 변수와 함수 코드를 추가한다.

protected:
    UPROPERTY(EditAnywhere)
    UTexture2D* CrosshairTexture;

public:
    virtual void DrawHUD() override;

 

FPSHUD.cpp에 DrawHUD() 함수를 구현한다.

void AFPSHUD::DrawHUD()
{
    Super::DrawHUD();
   
    if (CrosshairTexture)
    {
        FVector2D Center(Canvas->ClipX * 0.5f, Canvas->ClipY * 0.5f);

        FVector2D CrossHairDrawPosition(Center.X - (CrosshairTexture->GetSurfaceWidth() * 0.5f), Center.Y - (CrosshairTexture->GetSurfaceHeight() * 0.5f));

        FCanvasTileItem TileItem(CrossHairDrawPosition, CrosshairTexture->Resource, FLinearColor::White);
        TileItem.BlendMode = SE_BLEND_Translucent;
        Canvas->DrawItem(TileItem);
    }
}

 

DrawHUD() 함수를 구현할 때, Canvas 기능과 관련해서 불완전한 형식의 클래스에 대한 에러가 발생할 수 있다. 이 부분은 "Engine/Canvas.h"를 포함시켜주면 해결된다.

#include "Engine/Canvas.h"

 

변경사항을 저장하고 솔루션 탐색기에서 FPSProject를 우클릭해서 빌드를 선택하고 프로젝트를 컴파일한다.

 

 

C++ HUD 클래스를 블루프린트로 확장

 

콘텐츠 브라우저에서 FPSHUD 클래스에 우클릭해서 FPSHUD 기반 블루프린트 클래스 생성을 선택한다.

 

 

BP_FPSHUD라는 이름으로 Blueprints 폴더에 블루프린트 클래스를 생성한다.

 

 

기본 HUD 클래스 설정

 

프로젝트 세팅의 맵 & 모드에서 HUD를 새로 생성한 BP_FPSHUD로 설정해준다.

 

 

세팅이 끝났으면 BP_FPSHUD 블루프린트 에디터를 열고 Crosshair Texture를 crosshair로 설정해준다.

 

 

마지막으로, 블루프린트를 저장하고 블루프린트 에디터를 닫는다.

 

HUD 확인

 

레벨 에디터에서 플레이 해보면 새로 추가한 조준선이 보일 것이다.

 

 

레벨 에디터의 중지 버튼을 눌러서 PIE 모드를 빠져나오자.

 

 

 

 

 


 

이번 섹션에서 배운 것

 

1. UProjectileMovementComponent

UProjectileMovementComponent* ProjectileMovementComponent;

발사체의 이동을 처리해주는 컴포넌트

ProjectileMovementComponent->InitialSpeed = 3000.0f;

발사체의 초기 속도 변수. 이 값이 0이면 Velocity 값을 속도로 사용하고, 0이 아니면 Velocity 값을 무시한다.

ProjectileMovementComponent->MaxSpeed = 3000.0f;

발사체에 허용되는 최대 속력

ProjectileMovementComponent->bRotationFollowsVelocity = true;

이 값이 참이면, 발사체의 회전이 이동 방향에 맞춰 매 프레임 업데이트된다.

ProjectileMovementComponent->bShouldBounce = true;

이 값이 참이면, 간단한 바운스가 시뮬레이션 된다. 접촉 시뮬레이션을 중지하려면 이 값을 false로 설정한다.

ProjectileMovementComponent->Bounciness = 0.3f;

충돌의 법선 방향으로 반발이 발생한 이후에 유지되는 속도의 비율

ProjectileMovementComponent->Velocity;

발사체의 속도

 

2. AActor

FVector EyeLocation;
FRotator EyeRotation;

GetActorEyesViewPoint(EyeLocation, EyeRotation);

액터의 시점을 가져오는 함수. 이 시점은 액터의 붙은 카메라의 위치와 회전이 아니라 실제 액터의 눈에 해당하는 위치와 회전이다.

InitialLifeSpan = 3.0f;

액터의 수명을 결정하는 변수. 0으로 설정하면 시간이 지나도 소멸하지 않는다. 이 값은 초기 값이기 때문에, 플레이가 시작된 이후에는 수정해서는 안된다.

 

3. FActorSpawnParameters

FActorSpawnParameters SpawnParams;

액터를 스폰할 때 사용되는 매개변수들을 담는 구조체

SpawnParams.Owner;

이 액터를 생성한 액터. 생성한 액터가 따로 없다면 NULL로 둘 수 있다.

 

4. UWorld

GetWorld()->SpawnActor<T>(Class, Location, Rotation, SpawnParams);

월드에 액터를 스폰하는 함수이다.

 

5. UPrimitiveComponent

Target->AddImpulseAtLocation(Impulse, Location);

대상 물체에 충돌 효과를 주는 함수.

 


 

일인칭 슈팅 C++ 프로그래밍 튜토리얼은 이 다음에 한 섹션이 더 남아있지만, 다음 섹션은 프로그래밍 작업보다는 에디터에서 진행되는 애니메이션 작업 튜토리얼이다. 그렇기 때문에 이번 제대로 따라가기 :: 일인칭 슈팅 C++ 튜토리얼은 여기서 끝마친다.

 

다음 섹션을 이어서 배우고 싶디만 링크를 통해서 섹션 4. 캐릭터 애니메이션 추가를 따라가면 된다.

반응형
  1. qqq 2020.01.12 16:14

    그대로 따라했는데 발사체가 사라지질 않습니다... 뭐가 문제일까요?
    혹시나 해서 공식문서의 완성 코드로 덮어 씌워봤으나 여전히 사라지지 않습니다.
    언리얼 엔진 4.23 버전입니다.

    • qqq 2020.01.15 21:32

      처음부터 다시 했더니 또 되네요...
      혼란을 드려 죄송합니다.

    • wergia 2020.01.22 10:22 신고

      차근차근 따라가다보면 잘 되는 경우가 많습니다. ㅎㅎ

  2. NogameNoHope 2020.09.25 03:25

    다른 버전은 모르겠는데 4.25에서는
    SpawnParams.Instigator = Instigator;
    이 부분에서 에러가 뜹니다.
    확인 결과 Actor.h 내부에 Instigator 가 private로 지정되있어서 직접 접근을 금지시켰는데
    SpawnParams.Instigator = GetInstigator();
    이렇게 바꿔주니까 해결되는 것 같습니다.

    항상 잘 보고 있습니다.

    • wergia 2020.10.20 00:10 신고

      언리얼은 자꾸 버전마다 바뀌는 부분이 심하네요 ㅜㅜ

제대로 따라가기 (3) C++ 프로그래밍 튜토리얼 :: 컴포넌트와 콜리전

 

작성버전 :: 4.21.0

 

언리얼 엔진 튜토리얼인 컴포넌트와 콜리전에서는 컴포넌트를 만들어 계층구조에 넣고 게임플레이 도중 제어하는 법과, 컴포넌트를 사용하여 폰이 입체 오브젝트로 된 월드를 돌아다니도록 만드는 법을 배울 수 있다..

 

튜토리얼대로 하면 문제가 발생해서 제대로 따라갈 수 없는 부분으로 동작이 가능하게 수정해야하는 부분은 빨간 블럭으로 표시되어 있다.
 

이번 튜토리얼에서 새로 배우게 되는 내용은 글 제일 끝에 "이번 섹션에서 배운 것"에 정리된다.

 

 

1. 컴포넌트 만들고 붙이기(문서)

 

프로젝트를 새로 생성하고 Pawn 클래스를 상속받는 "CollidingPawn"을 생성한다. 이 폰은 컴포넌트를 가지고 레벨 안에서 이동하고 입체 오브젝트와 충돌하게 된다.

 

 

 

 

CollidingPawn.h의 클래스 정의 하단부에 UParticleSystemComponent를 추가한다.

 

UParticleSystemComponent* OurParticleSystem;

 

UParticleSystemComponent가 정의되어 있지 않다고 에러가 발생한다면, CollidingPawn.generated.h 포함 전처리기 위쪽에서 "Engine/Classes/Particles/ParticleSystemComponent.h"을 포함시켜 주면 된다.

 

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "Engine/Classes/Particles/ParticleSystemComponent.h"
#include "CollidingPawn.generated.h"

 

여기에 대한 또 다른 해결책으로는 UParticleSystemComponent 타입의 변수를 선언할 때, 아래처럼 앞에 class를 붙여주면 헤더를 .h에 포함하지 않아도 에러가 발생하지 않는다.

 

class UParticleSystemComponent* OurParticleSystem;

 

대신 이 경우에는 .cpp에서 해당 타입의 변수를 사용할 때, 불완전한 형식을 사용할 수 없다는 에러가 발생할 것이기 때문에 .cpp의 헤더 포함 전처리기에 "Engine/Classes/Particles/ParticleSystemComponent.h"를 포함하는 코드를 추가시켜주어야 한다.

 

멤버 변수로 만들지 않아도 컴포넌트를 만들 수 있지만, 코드에서 컴포넌트를 사용하려면 클래스 멤버 변수로 만들어야 한다.

 

이 다음에는 CollidingPawn.cpp의 ACollidingPawn::ACollidingPawn() 생성자 함수를 편집해서 필요한 컴포넌트들을 스폰할 코드를 추가하고 계층구조로 배치해야 한다. 물리 월드와 상호작용을 위한 Sphere Component, 콜리전 모양을 시각적으로 보여줄 Static Mesh Component, 시각적인 효과를 더하며 켜고 끌 수 있는 Particle System Component, 게임 내의 시점 제어를 위해 Camera Component에 덧붙일 Spring Arm Component를 만든다.

 

먼저 계층구조에서 루트가 될 컴포넌트를 결정해야 한다. 이 튜토리얼에서는 Sphere Component가 루트 컴포넌트가 된다. 물리적으로 실존이 있고, 게임 월드와의 상호작용이 가능하기 때문이다. 참고로 액터에는 계층구조 안에서 다수의 물리 기반 컴포넌트가 있을 수 있지만, 이 튜토리얼에서는 하나만 사용한다.

 

USphereComponent* SphereComponent = CreateDefaultSubobject(TEXT("RootComponent"));
RootComponent = SphereComponent;
SphereComponent->InitSphereRadius(40.0f);
SphereComponent->SetCollisionProfileName(TEXT("Pawn"));

 

이 파트에서는 두 가지 문제로 진행이 방해받는다. 언리얼 튜토리얼 문서의 고질적인 문제로 CreateDefaultSubobject() 함수 문제와 USphereComponent가 정의되어 있지 않다고 하는 문제이다.

 

CreateDefaultSubobject() 함수 문제는 템플릿 매개변수에 값을 반환받는 변수에 맞는 타입을 넣어주면 해결된다.

 

USphereComponent* SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("RootComponent"));

 

USphereComponent가 정의되지 않은 문제는 CollidingPawn.cpp의 전처리기에 "Engine/Classes/Components/SphereComponent.h"를 포함시켜주면 된다.

 

// Fill out your copyright notice in the Description page of Project Settings.

#include "CollidingPawn.h"
#include "Engine/Classes/Components/SphereComponent.h"

 

다음은, 구형의 스태틱 메시 컴포넌트를 만들어서 적절한 크기와 위치로 만들어서 루트 컴포넌트에 붙여준다.

 

UStaticMeshComponent* SphereVisual = CreateDefaultSubobject(TEXT("VisualRepresentation"));
SphereVisual->SetupAttachment(RootComponent);
static ConstructorHelpers::FObjectFinder SphereVisualAsset(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));
if (SphereVisualAsset.Succeeded())
{
    SphereVisual->SetStaticMesh(SphereVisualAsset.Object);
    SphereVisual->SetRelativeLocation(FVector(0.0f, 0.0f, -40.0f));
    SphereVisual->SetWorldScale3D(FVector(0.8f));
}

 

UStaticMeshComponent 정의되지 않음 문제는 CollidingPawn.cpp에 "Engine/Classes/Components/StaticMeshComponent.h"를 포함시켜주면 해결된다.

 

#include "Engine/Classes/Components/StaticMeshComponent.h"

 

CreateDefaultSubobject() 함수 문제는 템플릿 매개변수에 UStaticMeshComponent 타입을 넣어주면 해결된다.

 

UStaticMeshComponent* SphereVisual = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("VisualRepresentation"));

 

ConstructorHelpers가 정의되어 있지 않은 문제는 CollidingPawh.cpp에 "ConstructorHelpers.h"를 포함시켜주면 된다.

 

#include "ConstructorHelpers.h"

 

여기까지 해결하고 나면 ConstructorHelpers::FObjectFinder에서 [클래스 템플릿 "ConstructorHelpers::FObjectFinder"에 대한 인수 목록이 없습니다.] 라는 에러가 발생할 것이다. 이 문제를 해결하기 위해서 ConstructorHelpers::FObjectFinder의 원형을 살펴보면 ConstructorHelpers::FObjectFinder는 템플릿을 사용하는 것을 알 수 있다. 그렇다면 여기서 중요한 점은 템플릿 인자에 어떤 타입이 들어가야 하는가가 문제인데, 이 것은 SphereVisualAsset의 선언 2줄 아래를 보면 이 변수가 SetStaticMesh() 함수에 대입되는 것을 알 수 있다. 이 함수가 받는 매개변수의 타입은 UStaticMesh로서 SphereVisualAsset.Object는 UStaticMesh 타입임을 유추할 수 있다.

 

static ConstructorHelpers::FObjectFinder<UStaticMesh> SphereVisualAsset(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));

 

이번엔 Particle System Component를 붙인다. 이 컴포넌트는 코드를 통해서 켜고 끄는 등의 제어를 할 수 있으며, 루트가 아닌 스태틱 메시에 붙어있으며 게임 플레이 도중에 더 잘보이게 하기 위해 메시의 정중앙이 아닌 약간 아래쪽에 오프셋되어 있다.

 

OurParticleSystem = CreateDefaultSubobject(TEXT("MovementParticles"));
OurParticleSystem->SetupAttachment(SphereVisual);
OurParticleSystem->bAutoActivate = false;
OurParticleSystem->SetRelativeLocation(FVector(-20.0f, 0.0f, 20.0f));
static ConstructorHelpers::FObjectFinder ParticleAsset(TEXT("/Game/StarterContent/Particles/P_Fire.P_Fire"));
if (ParticleAsset.Succeeded())
{
    OurParticleSystem->SetTemplate(ParticleAsset.Object);
}

 

CreateDefaultSubobject() 함수 문제는 템플릿 매개변수에 UParticleSystemComponent 타입을 넣어주면 해결된다.

 

OurParticleSystem = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("MovementParticles"));

 

SetTamplate() 함수의 매개변수를 확인해본 결과 ParticleAsset의 템플릿 인자는 UParticleSystem 타입임을 알 수 있다.

 

static ConstructorHelpers::FObjectFinder<UParticleSystem> ParticleAsset(TEXT("/Game/StarterContent/Particles/P_Fire.P_Fire"));

 

Spring Arm Component는 폰보다 느린 가속/감속을 따라다니는 카메라에 적용시킬 수 있기 때문에, 카메라의 부드러운 부착점이 된다. 또한 카메라가 입체 오브젝트를 뚫고 지나가지 못하게 하는 기능을 내장하고 있어서, 삼인칭 게임에서 구석에서 벽을 등지는 상황에 유용하게 사용된다.

 

USpringArmComponent* SpringArm = CreateDefaultSubobject(TEXT("CameraAttachmentArm"));
SpringArm->SetupAttachment(RootComponent);
SpringArm->SetRelativeRotation(FRotator(-45.0f, 0.0f, 0.0f));
SpringArm->TargetArmLength = 400.0f;
SpringArm->bEnableCameraLag = true;
SpringArm->CameraLagSpeed = 3.0f;

 

USpringArmComponent가 정의되지 않은 문제는 CollidingPawn.cpp에 "Engine/Classes/GameFramework/SpringArmComponent.h"를 포함시켜주면 해결된다.

 

#include "Engine/Classes/GameFramework/SpringArmComponent.h"

 

CreateDefaultSubobject() 함수 문제는 템플릿 매개변수에 USpringArmComponent 타입을 넣어주면 해결된다.

 

USpringArmComponent* SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraAttachmentArm"));

 

Camera Component를 생성해서 Spring Arm Component에 붙여준다. Spring Arm Component에는 소켓이 내장되어 있어서 베이스가 아닌 소켓에 카메라를 붙일 수 있다.

 

UCameraComponent* Camera = CreateDefaultSubobject(TEXT("ActualCamera"));
Camera->SetupAttachment(SpringArm, USpringArmComponent::SocketName);

 

UCameraComponent가 정의되지 않은 문제는 CollidingPawn.cpp에 "Engine/Classes/Camera/CameraComponent.h"를 포함시켜주면 해결된다.

 

#include "Engine/Classes/Camera/CameraComponent.h"

 

CreateDefaultSubobject() 함수 문제는 템플릿 매개변수에 UCameraComponent 타입을 넣어주면 해결된다.

 

UCameraComponent* Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("ActualCamera"));

 

모든 컴포넌트를 붙인 뒤에는, 기본 플레이어가 이 폰을 조종하도록 설정해야 한다.

 

AutoPossessPlayer = EAutoReceiveInput::Player0;

 

위의 작업이 모두 끝났다면 언리얼 에디터로 돌아가자.

 

 

 

 

2. 입력 환경설정 및 폰 무브먼트 컴포넌트 생성(문서)

 

언리얼 에디터로 돌아왔다면, 프로젝트의 입력 세팅을 할 차례다. 이 세팅은 편집 드롭다운 메뉴의 프로젝트 세팅에서 찾을 수 있다.

 

 

 

프로젝트 세팅 창을 열었다면, 좌측의 엔진 섹션에서 입력을 찾아서 클릭한 뒤 아래와 같이 입력 매핑을 세팅하자.

 

 

 

이번에는 Pawn에서 모든 이동 처리를 하는 대신에, Movement Component를 만들어서 관리를 시키도록 해보자. 이 튜토리얼에서 Pawn Movement Component 클래스를 확장해서 사용한다.[각주:1] 파일 드롭다운 메뉴의 [새로운 C++ 클래스] 명령을 선택한다.

 

 

 

Pawn 클래스와 달리 Pawn Movement Component 클래스는 기본적으로 보이지 않기 때문에 모든 클래스 보기 옵션을 선택해야 한다.

 

 

 

검색창에 movement를 검색하면 찾고자 하는 클래스의 범위를 빠르게 좁힐 수 있다.

 

 

우리가 만든 Pawn 클래스의 이름이 "CollidingPawn"이기 때문에 이 Movement Component의 이름은 "CollidingPawnMovementComponent"로 정하자.

 

 

입력 환경설정에 대한 정의와 CollidingPawnMovementComponent의 생성으로 모두 끝마쳤으므로, 비주얼 스튜디오로 돌아가서 다시 코드 작업을 해야한다.

 

 

3. 폰 무브먼트 컴포넌트의 작동방식 코딩(문서)

 

비주얼 스튜디오로 돌아왔으면 이제 커스텀 폰 무브먼트 컴포넌트의 작동방식을 코딩하면 된다. Actor의 Tick() 함수 역할을 하는 TickComponent() 함수가 각 프레임 별로 어떻게 동작할지를 정의해야 한다. 우선은 부모 클래스의 TickComponent() 함수를 덮어쓰는 것으로 시작한다.

 

public:
    virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

 

정의한 함수를 CollidingPawnMovementComponent.cpp에 구현한다.

 

void UCollidingPawnMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    if (!PawnOwner || !UpdatedComponent || ShouldSkipUpdate(DeltaTime))
    {
        return;
    }

    FVector DesiredMovementThisFrame = ConsumeInputVector().GetClampedToMaxSize(1.0f) * DeltaTime * 150.0f;
    if (!DesiredMovementThisFrame.IsNearlyZero())
    {
        FHitResult Hit;
        SafeMoveUpdatedComponent(DesiredMovementThisFrame, UpdatedComponent->GetComponentRotation(), true, Hit);

        if (Hit.IsValidBlockingHit())
        {
            SlideAlongSurface(DesiredMovementThisFrame, 1.0f - Hit.Time, Hit.Normal, Hit);
        }
    }
}

 

이 코드는 적합한 면을 미끄러져 다니며 월드를 부드럽게 움직이도록 폰을 이동시킨다. 폰에는 중력이 적용되지 않으며, 최대 속력은 초당 150 언리얼 유닛 으로 하드코딩되어 있다.

 

 

4. 폰과 컴포넌트 함께 사용하기(문서)

 

CollidingPawnMovementComponent를 CollidingPawn 클래스에서 사용하기 위해서 CollidingPawn.h의 클래스 정의 내에 다음 코드를 추가한다.

 

class UCollidingPawnMovementComponent* OurMovementComponent;

 

그리고 CollidingPawn.cpp에 "CollidingPawnMovementComponent.h"를 포함시킨다.

 

#include "CollidingPawnMovementComponent.h"

 

그 다음엔 CollidingPawn.cpp의 ACollidingPawn::ACollidingPawn() 생성자 함수 하단에서 CollidingPawnMovementComponent의 인스턴스를 생성하고 루트 컴포넌트를 업데이트하게 코드를 작성한다.

 

OurMovementComponent = CreateDefaultSubobject(TEXT("CustomMovementComponent"));
OurMovementComponent->UpdatedComponent = RootComponent;

 

CreateDefaultSubobject() 함수 문제는 템플릿 매개변수에 UCollidingPawnMovementComponet 타입을 넣어주면 해결된다.

 

OurMovementComponent = CreateDefaultSubobject<UCollidingPawnMovementComponent>(TEXT("CustomMovementComponent"));

 

이 컴포넌트는 다른 컴포넌트들과 달리 컴포넌트 계층구조에 붙일 필요가 없다. 다른 컴포넌트들의 경우에는 모두 씬 컴포넌트로 물리적인 위치가 필요한 것들이었지만, 이 컴포넌트는 물리적 오브젝트를 나타내는 것이 아니기 때문에, 물리적인 위치에 존재한다든가 다른 컴포넌트에 덧붙인다던가 하는 개념을 가지지 않는다.

 

Pawn 클래스에는 GetMovementComponent() 라는 함수가 있는데 이것은 엔진의 다른 클래스들이 현재 Pawn이 사용중인 Pawn Movement Component에 접근할 수 있도록 하는데 사용된다. 이 함수가 커스터마이징한 CollidingPawnMovementComponent를 반환하도록 하려면 이 함수를 덮어씌워야 한다. CollidingPawn.h에 다음 코드를 추가한다.

 

virtual UPawnMovementComponent* GetMovementComponent() const override;

 

그리고 CollidingPawn.cpp에 이 함수의 구현을 추가한다.

 

UPawnMovementComponent * ACollidingPawn::GetMovementComponent() const
{
    return OurMovementComponent;
}

 

Pawn Movement Component에 대한 구성이 끝났다면, Pawn이 받을 입력 처리에 대한 코드를 만들자. CollidingPawn.h에 함수 몇 개를 선언한다.

 

void MoveForward(float AxisValue);
void MoveRight(float AxisValue);
void Turn(float AxisValue);
void ParticleToggle();

 

그리고 CollidingPawn.cpp에 함수들을 구현한다.

 

void ACollidingPawn::MoveForward(float AxisValue)
{
    if (OurMovementComponent && OurMovementComponent->UpdatedComponent == RootComponent)
    {
        OurMovementComponent->AddInputVector(GetActorForwardVector() * AxisValue);
    }
}

void ACollidingPawn::MoveRight(float AxisValue)
{
    if (OurMovementComponent && OurMovementComponent->UpdatedComponent == RootComponent)
    {
        OurMovementComponent->AddInputVector(GetActorRightVector() * AxisValue);
    }
}

void ACollidingPawn::Turn(float AxisValue)
{
    FRotator NewRotation = GetActorRotation();
    NewRotation.Yaw += AxisValue;
    SetActorRotation(NewRotation);
}

void ACollidingPawn::ParticleToggle()
{
    if (OurParticleSystem && OurParticleSystem->Template)
    {
        OurParticleSystem->ToggleActive();
    }
}

 

남은 것은 함수들을 입력 이벤트에 바인딩하는 것이다. 다음 코드를 ACollidingPawn::SetupPlayerInputComponent() 함수에 추가하자.

 

InputComponent->BindAction("ParticleToggle", IE_Pressed, this, &ACollidingPawn::ParticleToggle);
InputComponent->BindAxis("MoveForward", this, &ACollidingPawn::MoveForward);
InputComponent->BindAxis("MoveRight", this, &ACollidingPawn::MoveRight);
InputComponent->BindAxis("Turn", this, &ACollidingPawn::Turn);

 

이로써 프로그래밍 작업은 모두 끝났다. 에디터로 돌아가서 컴파일을 진행하고 테스트해보자.

 

 

 

 

 


 

이번 섹션에서 배운 것

 

1. UParticleSystemComponent

 

UParticleSystemComponent* ParticleSystemComponent;

 

액터에 파티클 시스템을 덧붙일 수 있는 컴포넌트

 

ParticleSystemComponent->bAutoActivate = true;

 

파티클 시스템이 생성되자마자 자동으로 켜질지에 대한 변수

 

ParticleSystemComponent->SetTemplate(ParticleAsset.Object);

 

파티클 시스템 컴포넌트의 파티클을 설정하는 함수

 

ParticleSystemComponent->ToggleActive();

 

파티클을 켜고 끄는 함수

 

2. USphereComponent

 

USphereComponent* SphereComponent;

 

액터에 구형 충돌 물리 효과를 줄 수 있는 컴포넌트

 

SphereComponent->InitSphereRadius(40.0f);

 

스피어 컴포넌트의 반지름은 설정하는 함수

 

SphereComponent->SetCollisionProfileName(TEXT("Pawn"));

 

콜리전의 프로필을 설정하는 함수. [프로젝트 세팅>엔진>콜리전] 하단에 Preset을 열어보면 각 콜리전 프로필마다 어떤 물리 설정을 가지고 있는지 확인할 수 있다.

 

3. UStaticMeshComponent

 

UStaticMeshComponent* StaticMeshComponent;

 

월드에 렌더링되는 스태틱 메시를 가진 컴포넌트

 

StaticMeshComponent->SetStaticMesh(SphereVisualAsset.Object);

 

스태틱 메시 컴포넌트의 스태틱 메시를 설정하는 함수

 

4. ConstructorHelpers::FObjectFinder<T>

 

static ConstructorHelpers::FObjectFinder<T> Asset(TEXT("AssetPath"));

 

프로젝트에서 필요한 콘텐츠나 리소스, 에셋을 불러오는데 쓰이는 구조체

 

Asset.Succeeded();

 

에셋을 불러오는데 성공했는지를 반환하는 함수

 

Asset.Object;

 

불러온 에셋을 담고 있는 변수

 

5. USpringArmComponent

 

USpringArmComponent* SpringArmComponent;

 

부모 오브젝트와 자식 오브젝트 사이에 일정한 거리를 유지하게 도와주는 컴포넌트. 충돌이 있는 경우라면 유연하게 부모와 자식 사이의 거리를 좁혔다가 충돌이 사라지면 다시 원래대로 돌아가게하는 기능을 제공한다.

 

SpringArmComponent->TargetArmLength = 400.0f;

 

아무런 충돌이 없을 때, 스프링 암의 자연적인 거리를 정할 수 있는 변수

 

SpringArmComponent->bEnableCameraLag = true;

 

true인 경우, 카메라가 목표 위치보다 뒤떨어져서 따라가도록 한다.

 

SpringArmComponent->CameraLagSpeed = 3.0f;

 

bEnableCameraLag가 true인 경우, 카메라가 목표 위치에 도달하는 속도를 제어한다.

 

6. UPawnMovementComponent

 

Pawn의 움직임을 업데이트하는데 사용되는 컴포넌트

 

PawnOwner;

 

이 컴포넌트를 소유하고 있는 폰

 

UMovementComponent::UpdatedComponent;

 

UPawnMovementComponent의 부모 클래스인 UMovementComponent 클래스에 속하는 변수로 이 무브먼트 컴포넌트가 이동시키고 업데이트 해야할 컴포넌트

 

UMovementComponent::ShouldSkipUpdate(DeltaTime);

 

이동된 컴포넌트가 이동할 수 없거나 렌더링되지 않은 경우인지를 판별하여 알려주는 함수

 

ConsumeInputVector();

 

대기중인 입력을 반환하고 다시 0으로 설정하는 함수

 

SafeMoveUpdatedComponent(DesiredMovementThisFrame, UpdatedComponent->GetComponentRotation(), true, Hit);

 

언리얼 엔진 피직스를 이용해서 입체 장애물을 피해서 폰 무브먼트 컴포넌트를 이동시키는 함수

 

SlideAlongSurface(DesiredMovementThisFrame, 1.0f - Hit.Time, Hit.Normal, Hit);

 

컴포넌트가 이동하다가 충돌이 발생했을 때, 제자리에 멈추는 대신 충돌체의 표면을 타고 미끄러지듯이 이동하도록 도와주는 함수

 

AddInputVector(Vector);

 

매개변수로 받은 벡터를 누적 입력에 더하는 함수

 

7. FVector

 

FVector Vector;

 

언리얼 엔진에서 3D 상의 위치나, 속도를 나타내는데 쓰이는 구조체

 

Vector.GetClampedToMaxSize(Value);

 

길이가 Value인 이 벡터의 복사본을 만들어서 반환하는 함수

 

Vector.IsNearlyZero();

 

지정된 허용오차 내에서 벡터의 길이가 0에 근접하는지 확인하는 함수

 

8. FHitResult

 

FHitResult Hit;

 

충돌에 대한 정보를 담고 있는 구조체

 

Hit.Time;

 

Hit가 발생했을 때, TraceStart와 TraceEnd 사이의 충돌이 발생한 시간을 의미한다. (0.0~1.0)

 

Hit.Normal

 

충돌이 발생한 오브젝트의 월드 공간 상의 법선 방향

 

Hit.IsValidBlockingHit();

 

막히는 충돌이 발생했을 때 true를 반환하는 함수

 

9. AActor

 

GetActorRotation();

 

액터의 현재 회전을 반환하는 함수

 

SetActorRotation(FRotator());

 

액터의 회전을 설정하는 함수

 

 

  1. Pawn Movement Component 에는 흔한 물리 함수성에 도움이 되는 강력한 내장 기능이 몇 가지 들어있어, 여러가지 폰 유형에 무브먼트 코드를 공유하기가 좋다. 컴포넌트 를 사용하여 함수성을 분리시켜 놓는 것은 매우 좋은 습관인데, 프로젝트의 덩치가 커지면서 폰 도 복잡해 지기 때문이다. [본문으로]
반응형

+ Recent posts