Programming 

TPS 캐릭터 조작 기능 구현하기

 

작성 기준 버전 :: 2019.2

 

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

 

지난 포스트 중 하나에서는 마우스를 이용해서 클릭한 위치로 캐릭터를 이동시키는 RPG나 AOS 게임에서 주로 사용되는 캐릭터 조작 방법을 구현했었다.

 

이번에는 키보드로 이동하고 마우스로 원하는 대상을 바라보게 하는 TPS 게임에서 사용되는 캐릭터 조작 방법을 구현해보도록 하자.

 

리소스 임포트와 기본 애니메이션 세팅

 

본격적인 구현에 들어가기 전에 캐릭터로 사용할 모델링과 애니메이션을 영상 하단의 링크에서 다운로드 받아서 임포트한다.

 

practice-animation.zip
다운로드

 

FBX 파일을 임포트하고 나면 먼저 캐릭터의 이동 애니메이션을 보여주기 위한 애니메이션 세팅을 해보자.

 

 

먼저 임포트된 FBX 파일 중에 Run, Stand의 애니메이션 임포트 세팅 중에서 Loop Time을 체크하고 적용한다.

 

 

그리고 애니메이터 컨트롤러를 생성한 뒤 Bool 타입으로 isMove 파라미터를 추가한다. 

 

 

그 다음에는 Stand 애니메이션을 기본 스테이트로 추가하고 Run 애니메이션을 추가한 뒤 isMove의 상태에 따라서 Stand와 Run을 왔다갔다 하도록 세팅해준다.

 

캐릭터 배치와 카메라 세팅

 

 

이 작업이 끝나면 씬에 캐릭터가 돌아다닐 바닥이 될 Plane을 하나 배치하고 BoxMan@Stand를 배치하고 BoxMan에 붙어있는 Animator 컴포넌트의 비어있는 Controller 프로퍼티에 방금 만든 애니메이터 컨트롤러를 할당해준다.

 

 

애니메이션에 대한 세팅이 끝나면 카메라를 이동시켜서 TPS 게임에서 자주 보이는 카메라 구도를 만들어보자. TPS 시점의 게임에서는 이런 구도의 카메라 시점이 주로 사용된다.

 

일인칭으로 보여지는 FPS 게임은 이것과는 약간 다르게 캐릭터의 몸이 거의 보이지 않고 손 정도만 보이게 시점을 세팅하게 되지만 이후의 기능 구현 자체는 거의 비슷하게 진행하면 된다.

 

본격적인 조작 기능 구현에 들어가기에 앞서 카메라와 캐릭터 구조를 조금 더 살펴보자.

 

지금 시점 자체는 나쁘지 않지만 조작 기능이 들어가기에는 아쉬움이 많다.

 

 

특히 마우스를 상하좌우로 움직이면 카메라 역시 마우스의 움직임에 맞게 회전해야 하는데, 카메라를 잡고 위/아래/좌/우로 회전시켜보면 전혀 적절하지 못하다는 것을 깨달을 수 있을 것이다.

 

해결책 1 : 카메라를 캐릭터의 자식 게임 오브젝트로 만들기

 

 

첫 번째로 생각해볼 만한 방법은 카메라를 캐릭터의 자식 게임 오브젝트로 만드는 것이다.

 

 

카메라를 BoxMan의 자식 게임 오브젝트로 만들면 좌우로 움직이는건 정상적으로 동작한다.

 

 

하지만 위/아래로 움직여보면 캐릭터가 같이 기울어져서 적절하지 못한 것을 알 수 있다. 적절한 카메라 회전을 위해서는 캐릭터를 회전시키지 않으면서도 캐릭터를 중심으로 카메라를 회전시키는 방법을 찾아야 할 것이다.

 

해결책 2 : 카메라 암(Camera Arm) 만들기

 

 

다른 해결책으로는 카메라 암(Camera Arm) 역할을 할 빈 게임 오브젝트를 하나 만들고 BoxMan의 머리 정도의 위치로 이동시켜 주고 카메라를 이 빈 게임 오브젝트의 자식 게임 오브젝트로 만들어준다. 

 

 

이렇게 세팅해주면 상하좌우 모든 방향의 회전에도 문제없이 카메라가 캐릭터를 중심으로 움직이는 것을 확인할 수 있다.

 

 

이대로 Camera Arm을 BoxMan 밑에 배치해도 되지만 그렇게 하면 BoxMan의 회전에 Camera Arm이 묶이게 되기 때문에 Character라는 이름의 빈 게임 오브젝트를 만들어서 BoxMan과 Camera Arm을 그 아래에 배치하자. 이렇게 해주면 BoxMan의 회전과 Camera Arm의 회전이 분리되서 관리하기가 쉬울 것이다.

 

카메라 회전 기능 구현하기

 

이제 본격적으로 캐릭터 조작 기능을 만들어보자.

 

TPSCharacterController라는 이름으로 C# 스크립트를 생성하고 다음 코드를 따라 작성하자.

 

public class TPSCharacterController : MonoBehaviour
{
    [SerializeField]
    private Transform characterBody;
    [SerializeField]
    private Transform cameraArm;

    Animator animator;

    void Start()
    {
        animator = characterBody.GetComponent<Animator>();
    }
}

 

Transform 타입으로 BoxMan 모델을 관리할 characterBody 변수와 카메라의 회전을 관리할 cameraArm 변수를 선언하고 애니메이션을 관리할 Animator 변수를 선언하고 Start 함수에서 characterBody.GetComponent 함수로 BoxMan에 붙어있는 Animator 컴포넌트를 가져와서 저장해둔자.

 

private void LookAround()
{
    Vector2 mouseDelta = new Vector2(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y"));
    Vector3 camAngle = cameraArm.rotation.eulerAngles;

    cameraArm.rotation = Quaternion.Euler(camAngle.x - mouseDelta.y, camAngle.y + mouseDelta.x, camAngle.z);
}

 

먼저 마우스의 움직임에 따라서 카메라를 회전시키는 기능을 구현하기 위해서 LookAround 함수를 만든다. 

 

LookAround 함수에서는 먼저 마우스가 이전 위치에 비해서 얼마나 움직였는지 알아내야 한다. 마우스가 움직인 수치는 Input.GetAxis 함수에 "Mouse X"와 "Mouse Y"를 매개변수로 넣어서 가져올 수 있다. 

 

"Mouse X"로는 마우스를 좌우로 움직인 수치를 가져올 수 있고 "Mouse Y"로는 마우스를 위아래로 움직인 수치를 가져올 수 있다.

 

이 두 값을 Vector2로 묶어서 mouseDelta 변수에 저장한다. 참고로 프로그래밍에서 이전 값과 현재 값의 차이를 주로 delta라는 용어로 표현한다. 

 

그 다음에는 cameraArm의 rotation 값을 오일러 각으로 변환해둔다. 그리고 camAngle과 mouseDelta를 합쳐서 새로운 회전값을 만들어낸 다음 cameraArm.rotation에 넣어줘야 한다.

 

우선 마우스 좌우 움직임으로 카메라 좌우 움직임을 제어해야 하기 때문에 mouseDelta의 x 값을 camAngle의 y에 더해준다. 그리고 마우스 수직 움직임은 카메라 상하 움직임을 제어해야 하기 때문에 mouseDelta의 y 값을 camAngle의 x에 추가해준다.

 

이 마우스 수직 움직임의 경우에는 한국에서는 마우스를 위로 움직이면 위를 쳐다보고 아래로 움직이면 아래를 쳐다보게 하는 방식을 사용하는 유저들이 더 많지만 북미에서는 이 움직임을 반대로 사용하는 경우가 꽤 있다고 한다. 그렇게 때문에 별도의 옵션을 둬서 두 가지 조작 방식 중에 사용자가 원하는 방식을 선택할 수 있도록 하는게 좋다. 

 

우선 한국에서 주로 쓰는 방식으로 구현하자. 마우스 방향과 바라보는 방향을 일치시키려면 mouseDelta.y를 camAngle.x에서 빼주면 된다.

 

void Update()
{
    LookAround();
}

 

그리고 LookAround 함수를 Update 함수에 호출하게 만들어준 뒤 코드를 저장하고 에디터로 돌아가보자.

 

 

그 다음 Character 게임 오브젝트에 생성한 컨트롤러 컴포넌트를 붙이고 프로퍼티들을 할당해준다.

 

 

게임을 플레이시키고 마우스를 움직여보면 우선 카메라는 제대로 움직이는 것 같지만 카메라 상하 회전에 제한이 없어서 그런지 마우스를 움직이다보면 화면이 뒤집어지는 경우가 발생한다.

 

카메라 각도 제한하기

 

이런 문제를 막기 위해서는 카메라의 x 회전을 제한해야 한다.

 

위쪽 회전은 약 70도를 제한하고 아래쪽 회전은 약 25도를 제한하도록 하자. 다만 x 회전 값을 출력해보면 알겠지만 수평을 기준으로 위쪽으로 회전하면 0도에서 90도까지 각도가 나오고 아래쪽으로 회전하면 360도부터 270도까지 각도가 나온다.

 

private void LookAround()
{
    Vector2 mouseDelta = new Vector2(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y"));
    Vector3 camAngle = cameraArm.rotation.eulerAngles;
    
    //추가
    float x = camAngle.x - mouseDelta.y;
    if (x < 180f)
    {
        x = Mathf.Clamp(x, -1f, 70f);
    }
    else
    {
        x = Mathf.Clamp(x, 335f, 361f);
    }

    cameraArm.rotation = Quaternion.Euler(x, camAngle.y + mouseDelta.x, camAngle.z);
}

 

우선 camAngle.x - mouseDelta.y로 만들어낸 값을 바로 회전값에 넣지 말고 따로 x에 저장한다. 그리고 회전값에 넣어주기 전에 x값의 상태에 따라서 처리하는 과정을 구현해야 한다.

 

먼저 x가 180도보다 작은 경우는 위쪽으로 회전하는 경우이므로 x의 값을 -1도에서 70도까지 제한시켜둔다. 여기서 0도가 아닌 -1도로 최저점을 잡는 이유는 0도로 잡으면 카메라가 수평면 아래로 내려가지 않는 문제가 발생하기 때문이다.

 

그리고 x각이 180도보다 큰 경우는 아래쪽으로 회전하는 경우로 25도를 제한하기 위해서 335도에서 361도 사이로 값을 제한시킨다.

 

이렇게 구해진 x값을 camAngle의 x값 자리에 넣어준다.

 

 

카메라 회전을 제한시킨 기능을 에디터에서 테스트해보면 위아래 회전에 제한이 걸려서 카메라가 뒤집어지는 문제가 발생하지 않는 것을 확인할 수 있다.

 

이동기능 추가하기

 

이제 여기에 이동 기능을 추가할 차례이다.

 

이동방향 구하기

 

private void Move()
{
    Debug.DrawRay(cameraArm.position, cameraArm.forward, Color.red);
}

 

움직이는 기능을 추가하기 위해서 Move 함수를 만든다. 

 

캐릭터를 이동시키기 위해서는 먼저 캐릭터가 이동할 방향을 찾아야 한다. 이동할 방향으로 가장 적합한 대상은 카메라가 바라보고 있는 방향일 것이다.

 

cameraArm.forward를 사용하면 될 것 같은데 우선 이 값이 적당한 방향인지 확인하기 위해서 Debug.DrawRay로 카메라 암 위치에서 cameraArm.forward 방향으로 에디터에서 선으로 그려보자.

 

void Update()
{
    LookAround();
    Move();
}

 

Update 함수에서 Move 함수를 호출하게 만든다.

 

 

게임을 플레이시키고 마우스를 움직여보면 씬 뷰에서 Camera Arm 위치에 빨간색 선이 카메라 암이 쳐다보는 방향을 표시하는 것을 볼 수 있다.

 

캐릭터의 이동 방향으로 쓰기에 적당해 보이지만 위쪽이나 아래쪽을 쳐다볼 때는 선이 기울어지는 것을 볼 수 있다. 정상적인 이동 방향을 보장하기 위해서는 이 선을 수평으로 변환해야 한다.

 

private void Move()
{
    Debug.DrawRay(cameraArm.position, 
        new Vector3(cameraArm.forward.x, 0f, cameraArm.forward.z).normalized, Color.red);
}

 

새 벡터를 만들면서 cameraArm.forward의 y값을 넣지 않는 것으로 높이를 없애고 normalized 시켜서 길이를 1로 맞춰준다.

 

 

다시 에디터에서 게임을 플레이시키고 마우스를 움직여보면 위나 아래를 쳐다봐도 빨간 선의 방향이 수평을 유지하는 것을 볼 수 있다.

 

이동기능 구현하기

 

private void Move()
{
    Vector2 moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
    bool isMove = moveInput.magnitude != 0;
    animator.SetBool("isMove", isMove);
    if (isMove)
    {
        Vector3 lookForward = new Vector3(cameraArm.forward.x, 0f, cameraArm.forward.z).normalized;
        Vector3 lookRight = new Vector3(cameraArm.right.x, 0f, cameraArm.right.z).normalized;
        Vector3 moveDir = lookForward * moveInput.y + lookRight * moveInput.x;

        characterBody.forward = lookForward;
        transform.position += moveDir * Time.deltaTime * 5f;
    }
}

 

그럼 다시 스크립트 에디터로 돌아가서 이동 기능을 구현하는 것을 진행해보자.

 

우선 Input.GetAxis 함수로 "Horizontal"과 "Vertical"로 이동 입력 값을 가져와서 moveInput 벡터를 만든다. 그리고 moveInput의 길이로 입력이 발생하고 있는지 판정하여 isMove 변수에 넣어준다.

 

moveInput의 길이가 0이면 이동 입력이 없는 것이고 0이 아니면 이동 입력이 발생하고 있는 것이다.

 

이 isMove를 animator의 "isMove"에 세팅해주면 이동 입력이 발생할 때는 걷는 애니메이션을 재생하고, 이동 입력이 없을 때는 대기 애니메이션을 재생할 것이다.

 

그리고 아까 전에 구해낸 방향을 바라보는 방향으로 저장한다. 거기에 추가로 cameraArm.right 역시 정면과 마찬가지로 평면화시켜서 저장해둔다.

 

이렇게 구해낸 lookForward와 lookRight에 moveInput을 곱해서 더하면 바라보고 있는 방향을 기준으로 이동 방향을 구할 수 있다. 그리고 구해낸 이동 방향과 Time.deltaTime, 이동속도를 모두 곱해서 transform.position에 더해주면 캐릭터를 이동시킬 수 있게 된다.

 

이동시 바라보는 방향 1 : 카메라가 바라보는 방향

 

private void Move()
{
    Vector2 moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
    bool isMove = moveInput.magnitude != 0;
    animator.SetBool("isMove", isMove);
    if (isMove)
    {
        Vector3 lookForward = new Vector3(cameraArm.forward.x, 0f, cameraArm.forward.z).normalized;
        Vector3 lookRight = new Vector3(cameraArm.right.x, 0f, cameraArm.right.z).normalized;
        Vector3 moveDir = lookForward * moveInput.y + lookRight * moveInput.x;

        characterBody.forward = lookForward;
        transform.position += moveDir * Time.deltaTime * 5f;
    }
}

 

마지막으로 캐릭터가 바라보는 방향을 정하는 방법을 여러 가지로 구현할 수 있다.

 

첫 번째 방법은 캐릭터를 이동시킬 때 시선을 정면에 고정시키는 방법이다.

 

characterBody의 forward를 lookForward로 정해주면 된다.

 

 

캐릭터를 움직여보면 캐릭터를 앞뒤좌우 어느 방향으로 움직이든 카메라가 바라보는 방향을 바라본다.

 

이 기능을 위해서는 캐릭터가 좌우로 움직일 때는 옆걸음을 치는 애니메이션을 넣고, 뒤로 움직일 때는 뒷걸음을 치는 애니메이션을 넣어줘야 할 것이다.

 

이런 식으로 움직이는 방법은 캐릭터가 언제나 정면을 바라보기 때문에 좀 더 직관적인 컨트롤을 할 수 있다는 장점이 있다.

 

이동시 바라보는 방향 2 : 이동방향

 

private void Move()
{
    Vector2 moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
    bool isMove = moveInput.magnitude != 0;
    animator.SetBool("isMove", isMove);
    if (isMove)
    {
        Vector3 lookForward = new Vector3(cameraArm.forward.x, 0f, cameraArm.forward.z).normalized;
        Vector3 lookRight = new Vector3(cameraArm.right.x, 0f, cameraArm.right.z).normalized;
        Vector3 moveDir = lookForward * moveInput.y + lookRight * moveInput.x;

        characterBody.forward = moveDir;
        transform.position += moveDir * Time.deltaTime * 5f;
    }
}

 

characterBody.forward를 moveDir로 지정해주면 된다.

 

 

캐릭터를 움직여보면 캐릭터는 카메라가 바라보는 방향을 기준으로 앞뒤좌우로 움직이지만 정면을 바라보지 않고 이동해야하는 방향을 바라보는 것을 알 수 있다.

 

이렇게 구현된 컨트롤은 정면만 바라보는 컨트롤에 비해서 복잡하기는 하지만 좀 더 섬세한 방향 컨트롤이 가능해진다.

 

이 두 가지 방식 중에서 게임에서 필요한 방식을 선택해서 구현하면 된다.

반응형
  1. 테샤르 2020.06.08 12:56 신고

    잘보고갑니다!!

  2. 아무거나 2021.03.13 01:00

    혹시 이동기능 코드좀 올려주실수 있나요? 변수 선언 부분에서 cameraarm 선언부랑 characterbody 부분 선언을 어떻게 해야할지 감이 안와서요

  3. 현신 2021.10.06 22:28

    우오오 매우 감사합니다! 딱 제가 찾던 내용이네요… 이렇게 체계적으로 살펴봐주시고, gif로 어떤 느낌인지도 보여주시니 정말 훌륭한 글 감사합니다.

Programming 

클릭한 위치로 캐릭터를 이동시키는 기능 구현하기

 

작성 기준 버전 :: 2019.2

 

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

 

게임의 조작 방식에는 굉장히 다양한 방법이 있다. 그 중에서 쿼터 뷰 형식의 RPG 게임이나 AOS 장르에서는 맵의 지형을 클릭하고 그 클릭한 위치로 캐릭터를 이동시키는 조작 방식을 사용한다.

 

쿼터 뷰 RPG나 AOS 장르처럼 클릭한 위치로 캐릭터를 이동시키는 기능을 만드는 방법을 알아보자.

 

클릭한 위치로 캐릭터 이동시키기

 

public class ClickMovement : MonoBehaviour
{
    private Camera camera;

    private bool isMove;
    private Vector3 destination;

    private void Awake()
    {
        camera = Camera.main;
    }
}

 

클릭한 위치로 캐릭터를 이동시키는 기능을 구현하기 위해서 먼저 위와 같은 변수들을 선언해준다.

 

Camera는 스크린 좌표계로 표시되는 마우스의 위치를 월드 좌표계로 표시하기 위해서 선언했다. 그리고 isMove는 캐릭터가 움직이고 있는 중인지 판정하기 위해서, destination은 캐릭터가 목표 위치를 기억하기 위해서 선언했다.

 

그리고 Awake 함수에서 씬에 존재하는 메인 카메라를 가져와서 camera 매개변수에 저장해준다.

 

private void SetDestination(Vector3 dest)
{
    destination = dest;
    isMove = true;
}

 

SetDestination 함수를 만들어서 그 안에 매개변수로 받아온 dest를 destination에 넣어주고 isMove를 true로 만들어 주는 코드를 작성한다. SetDestination 함수는 클릭으로 찾아낸 마우스의 위치를 저장해두고 이동을 시작하게 하는 역할을 한다.

 

private void Move()
{
    if (isMove)
    {
        if (Vector3.Distance(destination, transform.position) <= 0.1f)
        {
            isMove = false;
            return;
        }
        var dir = destination - transform.position;
        transform.position += dir.normalized * Time.deltaTime * 5f;
    }
}

 

Move 함수를 만든다. Move 함수에서는 isMove가 true일 때, 즉 SetDestination에서 목적지를 지정한 뒤에 목적지와 캐릭터의 거리가 0.1 유니티 미터보다 작으면 이동을 중지하고, 아직 도착하지 못한 상태라면 캐릭터의 위치에서 목적지로 향하는 방향을 구해서 캐릭터를 이동시키는 작업을 처리한다.

 

void Update()
{
    if (Input.GetMouseButton(1))
    {
        RaycastHit hit;
        if (Physics.Raycast(camera.ScreenPointToRay(Input.mousePosition), out hit))
        {
            SetDestination(hit.point);
        }
    }

    Move();
}

 

SetDestination 함수와 Move 함수를 모두 작성하면 Update 함수로 가서 마우스 오른쪽 버튼을 클릭했을 때 마우스 커서의 위치를 찾아내는 코드를 작성한다.

 

public class ClickMovement : MonoBehaviour
{
    private Camera camera;

    private bool isMove;
    private Vector3 destination;

    private void Awake()
    {
        camera = Camera.main;
    }

    void Update()
    {
        if (Input.GetMouseButton(1))
        {
            RaycastHit hit;
            if (Physics.Raycast(camera.ScreenPointToRay(Input.mousePosition), out hit))
            {
                SetDestination(hit.point);
            }
        }

        Move();
    }

    private void SetDestination(Vector3 dest)
    {
        destination = dest;
        isMove = true;
    }

    private void Move()
    {
        if (isMove)
        {
            if (Vector3.Distance(destination, transform.position) <= 0.1f)
            {
                isMove = false;
                return;
            }

            var dir = destination - transform.position;
            transform.position += dir.normalized * Time.deltaTime * 5f;
        }
    }
}

 

코드의 전문은 위와 같다.

 

 

이 컴포넌트를 캐릭터에 부착하고 콜라이더가 있는 맵에 우클릭하면 클릭한 위치로 캐릭터가 이동하는 것을 볼 수 있다.

 

기능 개선 1 : 카메라가 캐릭터 따라가게 하기

 

하지만 몇 가지 개선해야할 점이 보인다. 우선 카메라가 캐릭터를 따라가지 않아서 위쪽을 클릭하면 캐릭터가 화면 밖을 벗어나버리는 것을 볼 수 있다.

 

 

카메라가 캐릭터를 따라가게 하기 위해서 카메라를 캐릭터의 자식 오브젝트로 만들어보자.

 

 

다시 게임을 플레이시키고 우클릭으로 캐릭터를 이동시켜보면 카메라가 캐릭터를 따라서 움직이는 것을 확인할 수 있다.

 

기능 개선 2 : 캐릭터가 이동할 방향을 바라보게 하기

 

두 번째로 개선할 점은 캐릭터가 이동할 방향을 바라보게 하는 것이다. 지금은 캐릭터를 이동시켜도 게임을 시작할 때 바라보고 있던 방향만을 바라보면서 이상하게 이동하는 것을 볼 수 있다.

 

private void Move()
{
    if (isMove)
    {
        if (Vector3.Distance(destination, transform.position) <= 0.1f)
        {
            isMove = false;
            return;
        }
        var dir = destination - transform.position;
        transform.foward = dir;	// 추가
        transform.position += dir.normalized * Time.deltaTime * 5f;
    }
}

 

Move 함수를 보면 캐릭터가 이동할 방향을 구해둔 부분이 있다. 이 방향을 캐릭터의 transform.forward에 넣어주면 이동하는 방향을 바라보게 할 수 있다.

 

 

그리고 다시 게임을 플레이시키고 이동할 지점을 우클릭 해보면 우클릭한 지점을 캐릭터가 바라보면서 이동하기는 한다. 하지만 카메라가 캐릭터의 자식 오브젝트로 되어있어서 캐릭터의 방향이 바뀔 때 카메라도 함께 돌아가서 굉장히 어지럽다.

 

 

약간 구조를 바꿔야 될 것 같다. 우선 Character Root라는 이름으로 빈 게임 오브젝트를 만든다. 그리고 BoxMan을 그 아래로 옮기고 BoxMan 밑에 있는 카메라 역시 Charactor Root 아래로 옮겨준다.

 

이렇게 구조를 만들면 캐릭터를 이동시킬 때 카메라가 캐릭터가 따라서 움직이는 것을 유지하면서 BoxMan을 정상적으로 회전시킬 수 있다.

 

구조는 완성되었으니 본격적으로 기능을 수정해보자.

 

 

우선 BoxMan에 붙어있는 ClickMovement를 Character Root로 옮겨서 이동하는 주체를 Character Root로 바꿔준다.

 

[SerializeField]
private Transform character;	// 추가

private void Move()
{
    if (isMove)
    {
        if (Vector3.Distance(destination, transform.position) <= 0.1f)
        {
            isMove = false;
            return;
        }
        var dir = destination - transform.position;
        character.transform.foward = dir;	// 변경
        transform.position += dir.normalized * Time.deltaTime * 5f;
    }
}

 

캐릭터 몸체 만을 회전시키기 위해서 Transform 변수를 선언하고 회전 주체를 transform에서 character.transform으로 바꿔준다.

 

 

코드를 모두 작성하면 유니티 에디터로 돌아와서 비어있는 Character 프로퍼티에 회전해야 하는 캐릭터 게임 오브젝트를 넣어준다.

 

 

그 다음 플레이시켜보면 카메라는 회전하지 않고 BoxMan이 우클릭한 방향을 바라보며 이동하는 것을 볼 수 있다.

 

이제 기능이 완성된 것처럼 보인다.

 

 

하지만 여기에 장애물이 추가되면 어떨까?

 

 

Character Root에는 장애물과 충돌할 수 있게 Capsule Collider와 Rigidbody를 추가한다. 그리고 장애물과 충돌했을 때 캐릭터가 넘어지지 않도록 Constraints의 Freeze Rotation을 전부 체크해준다.

 

 

다시 게임을 플레이시키고 캐릭터를 벽 너머로 이동시키기 위해서 벽 너머를 우클릭하면 돌아서 이동하지 못하고 벽에 계속 부딪히기만 하는 것을 볼 수 있다. 게다가 벽 위를 클릭하면 벽 위에 올라서기 위해서 점프까지 하는 것을 볼 수 있다.

 

기능 개선 3 : 길찾기 기능 추가하기

 

꽤 재밌어보이는 기능이 완성됐지만 우리의 목적은 캐릭터가 장애물을 회피해서 이동하는 것이기 때문에 다시 개선하는 작업에 들어가보자.

 

일단 Character Root에 붙어있는 Capsule Collider의 Is Trigger를 체크해준다.

 

일반적인 RPG 게임이나 AOS 게임에서는 장애물 건너편으로 이동하기 위해서 조작을 하면 캐릭터가 자동으로 이동할 수 있는 길을 찾아서 이동하는 것을 볼 수 있다.

 

캐릭터가 장애물을 돌아서 이동하기 위해서는 길찾기 기능이 필요하다. 길찾기 알고리즘을 직접 구현할 수도 있지만, 유니티 엔진에서 제공하는 내비게이션 기능을 사용하면 훨씬 간단하게 구현할 수 있다.

 

 

우선 상단 메뉴 바에서 [Window > AI > Navigation]을 선택해서 내비게이션 뷰를 연다. 그리고 장애물과 바닥을 선택하고 내비게이션 뷰에서 Navigation Static을 체크하고 내비게이션 메시를 구워준다.

 

 

그러면 캐릭터가 이동할 수 있는 영역이 파란색으로 표시된다.

 

 

그 다음에는 캐릭터를 이 내비게이션 메시 위에서 길을 찾아 움직일 수 있는 에이전트로 만들어줘야 한다. Character Root에 NavMeshAgent 컴포넌트를 붙여준다. 

 

using UnityEngine.AI;  // 추가

public class ClickMovement : MonoBehaviour
{
    private Camera camera;
    private NavMeshAgent agent;  // 추가

    private bool isMove;
    private Vector3 destination;

    private void Awake()
    {
        camera = Camera.main;
        agent = GetComponent<NavMeshAgent>();  // 추가
        agent.updateRotation = false;  // 추가
    }

    void Update()
    {
        if (Input.GetMouseButton(1))
        {
            RaycastHit hit;
            if (Physics.Raycast(camera.ScreenPointToRay(Input.mousePosition), out hit))
            {
                SetDestination(hit.point);
            }
        }

        // Move();
        LookMoveDirection();  // 변경
    }

    private void SetDestination(Vector3 dest)
    {
        agent.SetDestination(dest);  //추가
        destination = dest;
        isMove = true;
    }

    // private void Move()
    private void LookMoveDirection()  // 변경
    {
        if (isMove)
        {
            // if (Vector3.Distance(destination, transform.position) <= 0.1f)
            if (agent.velocity.magnitude == 0.0f)  // 변경
            {
                isMove = false;
                return;
            }

            // var dir = destination - transform.position;
            var dir = new Vector3(agent.steeringTarget.x, transform.position.y, agent.steeringTarget.z) - transform.position;  // 변경
            character.transform.foward = dir;
        }
    }
}

 

그리고 스크립트 에디터로 돌아가서 AI 네임스페이스를 추가해주고 NavMeshAgent 변수를 만든다. 그 다음 Awake 함수에서 게임 오브젝트에 붙어있는 NavMeshAgent 컴포넌트를 가져와서 넣어준다.

 

컴포넌트를 가져온 다음에는 agent가 Character Root를 회전시키지 않도록 updateRotation을 false로 바꿔줍니다. 그리고 SetDestination 함수에서는 매개변수로 받아온 목표 위치를 agent.SetDestination 함수에 넣어준다.

 

이렇게 하면 NavMeshAgent가 자동으로 길을 찾아서 캐릭터를 이동하도록 만들어준다.

 

그러면 Move 함수의 원래 목적은 사라지는데, 대신 Move 함수는 BoxMan이 이동 방향을 쳐다보게 하는 기능을 해야한다.

 

변경되는 기능에 따라 함수의 이름은 LookMoveDirection으로 바꿔주면 좋다. 원래 코드는 BoxMan이 destination을 쳐다보게 되어있는데 이렇게 하면 캐릭터가 길을 돌아서 이동하는 와중에도 목적지만 쳐다봐서 부자연스럽게 움직이게 된다.

 

캐릭터는 목적지 대신에 자신이 이동하는 방향을 쳐다봐야 한다. direction을 구할 때 destination 대신 agent.steeringTarget을 사용해야 한다. 다만 아까 전에 캐릭터와 높이가 다른 목적지를 클릭 했을 때 캐릭터가 기울어졌던 것을 기억할 것이다. 캐릭터가 높이가 다른 곳을 쳐다보면서 기울어지는 것을 막기위해서 캐릭터가 agent의 steeringTarget 방향을 바라보되 높이는 캐릭터 자신의 높이를 바라보게 해야 한다. 그리고 도착 판정을 거리 대신에 agent의 속도로 변경하여 목적지에 도착한 경우면 함수를 종료하도록 만들어 준다.

 

이렇게 하면 캐릭터가 목적지에 도착한 다음 엉뚱한 방향을 바라보지 않게 될 것이다.

 

 

게임을 플레이시키고 우클릭으로 캐릭터를 이동시켜보면 잘 동작하는 것을 볼 수 있다. 벽 건너편을 클릭해도 제대로 돌아서 이동하고 벽 위를 클릭해도 캐릭터가 기울어지며 올라가려는 시도를 하지 않는 것을 알 수 있다.

 

반응형
  1. 테샤르 2020.05.14 10:21 신고

    잘보고갑니다 :D
    저도 지금 회사에서 비슷한 작업하고있는데 생각보다 어렵더라고요 ./

    • wergia 2020.05.14 10:54 신고

      조금만 방심하면 버그같은 기능들이 우수수 쏟아지죠 :<

      진짜 이것저것 다해보면서 꼼꼼하게 테스트해야 버그가 안생기는 것 같아요!

  2. 익명 2020.07.22 21:02

    비밀댓글입니다

    • wergia 2020.07.23 09:13 신고

      음 상황만 들어서는 원인을 파악하기 어렵습니다. 프로젝트 전체를 확인해봐야 파악이 가능할 것 같아요.

      우선 간단하게 짐작해보자면 캐릭터가 뒤로 걷거나 바닥에 파묻히는 문제는 땅 바닥 부분과 캐릭터의 발부분 높이가 맞지 않아서 그런 것으로 생각됩니다. 바닥의 높이를 낮추거나 캐릭터의 높이를 높여보세요.
      장애물을 통과하는 부분은 길찾기 쪽 구현에서 문제가 생긴게 아닌가 싶네요.

  3. 익명 2020.07.23 11:44

    비밀댓글입니다

    • wergia 2020.07.23 12:10 신고

      "SetDestination" can only be called on an active agent that has been placed on a NavMesh.
      라는 에러로그는 NavMeshAgent가 Bake된 NavMesh 위에 있지 않다는 뜻입니다.
      내비게이션 뷰를 활성화시킨 상태에서 NavMeshAgent 컴포넌트를 붙인 게임 오브젝트를 선택하고 에이전트의 초록색 선 원기둥이 파란색으로 표시되는 NavMesh 영역 위에 있는지 확인해보세요.

  4. 익명 2020.07.23 13:37

    비밀댓글입니다

    • wergia 2020.07.23 15:48 신고

      ㅎㅎ 도움이 되었다니 정말 기쁩니다
      공부 열심히 하셔서 나중에 재밌는 게임으로 뵙는 날이 있으면 좋겠습니다 ㅎㅎ

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와 매칭시켜서 벽이 투명해지도록 만들어보자.

반응형

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


작성버전 :: 4.21.0

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

 

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

 

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


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

 

VisibleDefaultsOnly는 정상적으로 존재하는 UPROPERTY 지정자입니다. 제가 실수로 VisibleDefaultOnly로 오타를 내서 컴파일러가 지정자가 없다고 에러를 띄웠었습니다. 잘못된 정보로 혼동을 드린 점에 대해서 사과드립니다.

 

 

2. 캐릭터 임포트

 

이번 섹션의 목표는 일인칭 슈팅 캐릭터 구현법을 배우는 것이다.

 

 

2-1. 새 캐릭터 만들기(문서)

 

이번 단계에서는 엔진의 Character[각주:1] 베이스 클래스를 사용해서 새 캐릭터를 만들어보자. Character 클래스에는 걷기, 달리기, 점프와 같은 이족보행 동작이 기본 내장되어 있다.

 

캐릭터 클래스 추가

 

파일 드롭다운 메뉴에서 새로운 C++ 클래스... 를 선택해서 새 부모 클래스를 선택한다.

 

 

부모 클래스 선책 창이 열리면 Character 클래스를 부모로 선택하고 다음 버튼을 누른다.

 

 

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

 

 

FPSCharacter 클래스의 생성이 완료되었다면 FPSCharacter.cpp의 BeginPlay() 함수에 FPSCharacter 클래스가 사용중인지 확인하는 다음 코드를 추가한다.

 

if (GEngine)
{
    GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, 
        TEXT("We are using FPSCharacter!"));
}

 

이전 섹션에서도 언급했듯이 전역에 선언된 GEngine 변수를 사용하기 위해서는 "Engine.h"를 포함시켜야 한다. FPSCharacter.cpp의 전처리기 리스트에 "Engine.h" 포함 선언을 추가해주자.

 

#include "Engine.h"

만약 지난 섹션에서 언급한 다른 방식인 "FPSProject.h"에 선언을 추가해두었다면 "FPSProject.h"를 포함시켜주면 된다.

 

#include "FPSProject.h"

 

코드 작성이 끝났다면 변경사항을 저장하고 에디터로 돌아가 컴파일을 진행한다.

 

C++ FPS 캐릭터 클래스를 블루프린트로 확장

 

콘텐츠 브라우저에서 생성된 FPSCharacter 클래스를 찾아서 우클릭한 뒤 FPSCharacter 기반 블루프린트 클래스 생성을 클릭한다.

 

 

BP_FPSCharacter라고 블루프린트 이름을 정하고 Blueprints 폴더를 선택한 뒤, 블루프린트 생성 버튼을 클릭한다.

 

 

기본 폰 클래스 설정

 

새로운 캐릭터 클래스를 블루프린트로 확장했으니, 이제 이 BP_FPSCharacter를 기본 폰으로 사용하도록 설정할 차례다.

 

프로젝트 세칭 창을 열고 프로젝트 섹셩에 맵 & 모드에서 Default Pawn Class 항목을 BP_FPSCharacter로 설정한다.

 

 

 

세팅이 끝났다면 프로젝트 세팅 창을 닫고 레벨 에디터에서 플레이 버튼을 클릭해서 PIE 모드로 들어가보자. 뷰포트 좌상단에 원래의 로그 메시지와 함께 새로 추가한 "We are using FPSCharacter!"라는 문구가 빨간색으로 5초간 표시될 것이다.

 

 

아직 이동에 대한 기능을 전혀 만들지 않았기 때문에 WASD를 사용해도 움직이지 않는 것이 정상적이며 FPSCharacter가 제대로 적용된 것이다.

 

다음 단계로 넘어가기 전에 Esc키를 눌러 PIE 모드에서 빠져나오자.

 

 

2-2. 축 매핑 구성(문서)

 

일반적으로 축 매핑(Axis Mappings)을 통해서 키보드, 마우스, 컨트롤러 입력을 "친근한 이름"으로 매핑시킨뒤 나중에 이동 등의 게임 동작에 바인딩할 수 있다. 축 매핑은 지속적으로 폴링되어, 부드러운 전환 및 게임 동작이 가능하다. 컨트롤러의 조이스틱 같은 하드웨어 축은 "눌렸다", "안눌렸다" 같은 식의 구분되는 입력이 아닌 연속적인 입력 수치를 제공한다. 컨트롤러 조이스틱 입력 방법이 스케일식 동작 입력을 제공해 주기는 하지만, 축 매핑으로 WASD 처럼 지속적 폴링되는 게임 동작을 위한 일반 이동 키 매핑도 가능하다.

 

프로젝트 세팅 창을 열고 엔진 섹션의 입력을 선택한다. 그리고 입력 매핑 세팅을 다음처럼 구성한다.

 

 

 

2-3. 캐릭터 동작 함수 구현(문서)

 

이번 단계에서는 Player Input Component를 구성하고, FPSCharacter 클래스에 다음 함수들을 구현한다.

 

MoveForward();

MoveRight();

 

무브먼트 함수 인터페이스

 

에디터에서 축 매핑을 구성했으니, 매핑에 바인딩할 함수들을 구현하자.

 

FPSCharacter.h에서 클래스 하단부에 다음 함수들의 선언을 추가한다.

 

UFUNCTION()
void MoveForward(float AxisValue);

UFUNCTION()
void MoveRight(float AxisValue);

 

함수 위에 붙여준 UFUNCTION() 매크로는 엔진에게 이 함수들을 인식시켜 직렬화(Serialization), 최적화, 기타 엔진 함수성에 포함될 수 있도록 해준다.

 

동작 함수 구현

 

전형적인 FPS 조작법에서, 캐릭터의 동작 축은 카메라에 상대적이다. "전방"이란 "카메라가 향하는 방향"을, "우측"이란 "카메라가 향하는 방향의 우측"을 뜻한다. 캐릭터의 제어 방향을 구하는 데는 PlayerController를 사용할 것이다. 또한 MoveForward() 함수는 제어 회전의 피치 컴포넌트를 무시하고 입력을 XY 면으로 제한시켜 위아래를 쳐다보더라도 캐릭터는 땅과 평행으로 움직일 수 있도록 한다.

 

FPSCharacter.cpp에서 AFPSCharacter::SetupPlayerInputComponent() 함수의 하단에 다음 코드를

 

InputComponent->BindAxis("MoveForward", this, &AFPSCharacter::MoveForward);
InputComponent->BindAxis("MoveRight", this, &AFPSCharacter::MoveRight);

 

그리고 MoveForward()함수와 MoveRight()함수를 구현한다.

 

void AFPSCharacter::MoveForward(float AxisValue)
{
    FVector Direction = FRotationMatrix(Controller->GetControlRotation()).GetScaledAxis(EAxis::X);
    AddMovementInput(Direction, AxisValue);
}

void AFPSCharacter::MoveRight(float AxisValue)
{
    FVector Direction = FRotationMatrix(Controller->GetControlRotation()).GetScaledAxis(EAxis::Y);
    AddMovementInput(Direction, AxisValue);
}

 

캐릭터 동작 테스트하기

 

구현이 모두 끝났다면, 변경사항들을 저장하고 에디터로 넘어가서 컴파일을 한다.

 

컴파일이 끝나면 플레이 버튼을 눌러서 PIE 모드를 실행한 뒤, WASD 키를 눌러서 캐릭터가 전후좌우로 움직이는지 확인해보자.

 

성공적으로 움직인다면 Esc를 눌러서 PIE 모드를 빠져나오고 다음 파트로 넘어간다.

 

 

 

 

2-4. 마우스 카메라 컨트롤 구현(문서)

 

이번 단계에서는 캐릭터의 시야 및 이동 방향을 마우스로 조정하는 기능을 추가한다.

 

마우스 축 매핑 추가

 

프로젝트 세팅 창에서 입력 매핑을 다음과 같이 추가한다.

 

 

입력 처리 구현

 

회전과 쳐다보기에 대한 마우스 입력 처리를 하는 코드를 추가할 차례이다. Character 베이스 클래스는 카메라 회전 컨트롤에 대해서 다음과 같은 필수 함수 둘을 제공한다. 그렇기 때문에 FPSCharacter 클래스에 별도의 함수를 정의하고 구현할 필요없이 바로 AFPSCharacter::SetupPlayerInputComponent() 함수에 바인딩하는 코드를 추가하면 된다.

 

InputComponent->BindAxis("Turn", this, &AFPSCharacter::AddControllerYawInput);
InputComponent->BindAxis("LookUp", this, &AFPSCharacter::AddControllerPitchInput);

 

마우스 카메라 컨트롤 테스트

 

변경 사항들을 저장하고 에디터로 넘어가서 컴파일을 진행한 뒤, 플레이 버튼을 누르고 카메라를 움직여서 카메라가 회전하는 것을 확인해보자.

 

확인을 마쳤다면 Esc를 눌러서 PIE 모드에서 빠져나온다.

 

 

2-5. 캐릭터 점프 구현(문서)

 

액션 매핑은 별도의 이벤트에 대한 입력을 다루며, "친근한 이름"에 매핑시켜 나중에 이벤트 주도형 동작에 바인딩시킬 수 있도록 해준다. 최종 효과는 키나 마우스 버튼, 혹은 키패드 버튼에 대해서 누르기/떼기를 통해서 게임 동작을 실행시키도록 하는 거이다.

 

이번 단계에서는, 스페이스 바에 대한 액션 매핑을 구성하여 캐릭터에 점프 능력을 추가하는 것이다.

 

점프 액션 매핑

 

프로젝트 세팅 창을 열고 엔진 섹션에서 입력을 선택한다. 그리고 액션 매핑을 다음과 같이 추가한다.

 

 

입력 처리 구현

 

Character 베이스 클래스의 인터페이스(*.h) 파일 안을 보면, 캐릭터 점프에 대한 지원이 내장되어 있는 것을 볼 수 있다. 캐릭터 점프는 bPressedJump 변수에 묶여 있어서, 점프 키를 누르면, 이 변수를 true로, 떼면 false로 설정해주기만 하면 된다.

 

FPSChararcter.h에 다음 두 함수의 선언을 추가한다.

 

UFUNCTION()
void StartJump();

UFUNCTION()
void StopJump();

 

그리고 FPSCharacter.cpp에 함수의 구현을 추가해준다.

 

void AFPSCharacter::StartJump()
{
    bPressedJump = true;
}

void AFPSCharacter::StopJump()
{
    bPressedJump = false;
}

 

구현이 끝났으면 SetupPlayerInputComponent() 함수에 바인딩 코드를 추가한다.

 

InputComponent->BindAction("Jump", IE_Pressed, this, &AFPSCharacter::StartJump);
InputComponent->BindAction("Jump", IE_Released, this, &AFPSCharacter::StopJump);

 

캐릭터 점프 테스트

 

변경 사항을 저장하고, 에디터로 돌아가서 컴파일 한다. 그리고 플레이 버튼을 눌러서 PIE에 들어가서 스페이스 바를 눌러서 캐릭터의 점프가 정상적으로 동작하는지 확인해보자.

 

점프가 정상적으로 동작한다면 Esc키를 눌러서 PIE 모드에서 빠져나온다.

 

 

2-6. 캐릭터에 메시 추가(문서)

 

이번 단계에서는 캐릭터에 스켈레탈 메시를 추가한다. Character 클래스는 기본적으로 우리가 쓸 SkeletalMeshComponent를 생성해 주므로, 어떤 스켈레탈 메시를 사용할지 알려주기만 하면 된다.

 

이 튜토리얼에서 사용되는 기본 스켈레탈 메시는 파트 제목 옆에 문서 링크를 통해서 들어가면 샘플 메시라는 링크에서 받을 수 있다.

 

스켈레탈 메시 임포트

 

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

 

 

아까 다운받아서 압축해제한 GenericMale.fbx 메시 파일을 찾아 선택하고 열기를 클릭하여 메시를 프로젝트에 임포트 시작한다.

 

콘텐츠 브라우저에서 FBX 임포트 옵션 대화창이 뜬다. 임포트를 클릭하면 프로젝트에 메시가 추가된다.

 

저장 버튼을 클릭해서 임포트된 메시를 저장한다.

 

삼인칭 메시 구성

 

BP_FPSCharacter 아이콘을 더블클릭해서 블루프린트 에디터를 열고 컴포넌트 탭에서 Mesh 컴포넌트를 선택한다.

 

 

디테일 패널의 메시 섹션으로 스크롤해 내려가서 없음이라고 되어있는 드롭다운 메뉴를 클릭해서 GenericMale 스켈레탈 메시를 선택해서 넣는다.

 

 

Z 위치를 -88.0으로 설정해서 SkeletalMeshComponent를 CapsuleComponent에 정렬시킨다.

 

 

SkeletalMeshComponent는 다음과 같은 상태일 것이다.

 

 

 

일반적으로 CapsuleComponent안에 SkeletalMeshComponent이 포함되고, ArrowComponent와의 방향이 일치해야 캐릭터가 월드에서 정상적으로 돌아다닐 수 있다.

 

스켈레탈 메시 설정이 끝났다면 BP_FPSCharacter 블루프린트를 컴파일하고 저장한 뒤에 블루프린트 에디터를 닫자.

 

PIE 모드에서 새 메시 확인

 

레벨 에디터에서 플레이 버튼을 클릭하면 캐릭터를 따라오는 메시의 그림자가 보일 것이다.

 

 

Esc를 눌러 PIE 모드를 빠져나오자.

 

 

 

 

2-7. 카메라 뷰 변경(문서)

 

이전 단계에서 살펴보았을 때, 별도의 설정을 하지 않은 기본 카메라의 위치는 메시의 목 안쪽에 위치했었다. 이번 단계에서는 카메라(위치나 시야같은) 프로퍼티 조정에 사용할 수 있는 적합한 FPS 카메라를 구성해보자.

 

카메라 컴포넌트 붙이기

 

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

 

UPROPERTY(VisibleAnywhere)
UCameraComponent* FPSCameraComponent;

 

그리고 FPSCharacter.cpp의 AFPSCharacter::AFPSCharacter() 생성자 함수에 다음 코드를 추가해준다.

 

FPSCameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("FirstPersonCamera"));
FPSCameraComponent->SetupAttachment(GetCapsuleComponent());
FPSCameraComponent->SetRelativeLocation(FVector(0.0f, 0.0f, 50.0f + BaseEyeHeight));
FPSCameraComponent->bUsePawnControlRotation = true;

 

이 코드는 카메라의 위치를 캐릭터의 눈 살짝 위쪽으로 잡으면서 폰이 카메라 로테이션을 제어할 수 있도록 해준다.

 

 

이대로 빌드하면 Camera Component에서 에러가 발생해서 컴파일에 실패할 수 있다. 코드를 작성할 때는 에러가 뜨지 않아서 방심했지만 이 에러는 충분이 아는 에러일 것이다. 지금 비주얼 스튜디오가 한글 버전이라 로그가 깨져있지만 튜토리얼을 진행하면서 생긴 경험으로 미루어 짐작하건데, 헤더의 30라인에서 발생하는 에러는 UCameraComponent가 정의되지 않았다는 에러일 것이다. UCameraComponent 선언 앞에 class를 붙여주자.

 

UPROPERTY(VisibleAnywhere)
class UCameraComponent* FPSCameraComponent;

 

새 카메라 테스트

 

변경사항을 저장하고, 에디터에서 컴파일한 후, 플레이 버튼을 눌러서 테스트 해보자.

 

 

2-8. 캐릭터에 일인칭 메시 추가(문서)

 

FPS에서 흔히 사용되는 방법은, 전신 바디 메시 하나, 무기와 손 메시하나, 이렇게 별개의 캐릭터 메시 두 개를 사용하는 것이다. 전신 메시는 삼인칭 시첨에서 캐릭터를 보거나 다른 캐릭터를 볼대 사용되고, 플레이어가 일인칭 시점에서 게임을 볼 때는 이 전신 메시를 숨긴다. 그리고 "무기와 손" 메시는 전형적으로 카메라에 붙여 플레이어가 일인칭 시점에서 맵을 볼 때 플레이어에게만 보이는 것이다. 이 파트에서는 캐릭터에 일인칭 메시를 추가해보자.

 

일인칭 캐릭터 메시 추가

 

비주얼 스튜디오로 이동해서 FPSCharacter.h를 열고 다음 코드를 추가한다.

 

UPROPERTY(VisibleDefaultsOnly, Category = "Mesh")
USkeletalMeshComponent* FPSMesh;

 

그리고 FPSCharacter.cpp의 AFPSCharacter::AFPSCharacter() 생성자 함수에 다음 코드를 추가하여 일인칭 메시를 생성하고 설정해준다.

 

FPSMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("FirstPersonMesh"));
FPSMesh->SetOnlyOwnerSee(true);
FPSMesh->SetupAttachment(FPSCameraComponent);
FPSMesh->bCastDynamicShadow = false;
FPSMesh->CastShadow = false;

 

SetOnlyOwnerSee는 이 메시가 이 Character에 빙의(Possession)한 PlayerController에게만 보인다는 의미다. 이 코드는 메시를 카메라에 붙이고 배경 그림자를 끈다. 만약 카메라에 붙은 팔의 그림자가 보이면 기괴한 모습이 될 것이다.

 

그 아래쪽에 다음 코드를 추가하여 소유 캐릭터에서 기존 삼인칭 메시를 숨긴다.

 

GetMesh()->SetOwnerNoSee(true);

 

이제 코드의 변경 사항을 저장하고 에디터로 돌아가서 컴파일을 진행한다.

 

만약 에디터 컴파일로 곧바로 변경 사항이 반영되지 않는다면 에디터를 종료하고 솔루션 탐색기에서 FPSProject를 찾아서 우클릭하고 빌드(Build)를 선택해서 프로젝트를 새로 빌드하고 다시 실행하면 변경 사항이 반영된다.

 

 

빌드 완료 후, PIE 모드에서 살펴보면 캐릭터의 메시가 더 이상 보이지 않는 것을 알 수 있다.

 

 

메시 블루프린트 빌드

 

계속하기 전에, 다음 링크에서 샘플 메시를 다운로드해서 압축을 풀어야 한다. "일인칭 스켈레탈 메시"

 

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

 

 

HeroFPP.fbx 메시 파일을 찾아 임포트 한다.

 

콘텐츠 브라우저에서 Blueprints 폴더로 들어가서 BP_FPSCharacter 아이콘을 더블클릭해서 블루프린트 에디터를 연다.

 

컴포넌트 탭에서 FPSMesh 컴포넌트를 찾는다.

 

 

FPSMesh 컴포넌트는 FPSCameraComponent의 자손이라, 카메라에 항상 붙어있다.

 

컴포넌트 탭에서 FPSMesh를 클릭한다.

 

디테일 탭에서 Mesh 섹션으로 스크롤해 내려가서 "없음"이라는 드롭다운 메뉴를 클릭해서 HeroFPP 스켈레탈 메시를 선택하여 뷰포트에 팔을 추가한다.

 

 

 

새로 추가된 HeroFPP 스켈레탈 메시는 뷰포트 안에서 다음과 같이 보일 것이다.

 

 

새로 추가된 메시의 트랜스폼이 카메라 앞에 오도록 조절한다. 위치를 로, 회전을 {-180, 50, -180}으로 설정한다.

 

 

블루프린트 에디터를 닫기 전에 BP_FPSCharacter 블루프린트를 반드시 컴파일하고 저장하자.

 

게임 내 새 메시 확인

 

레벨 에디터에서 플레이 버튼을 클릭해서 게임 내에서 새 메시를 확인한다.

 

 

Esc키를 눌러서 PIE 모드에서 빠져나오자.

 

다음 섹션에서는 발사체(Projectile) 구현법을 배워보자.

 


 

이번 섹션에서 배운 것

 

1. FRotatorMatrix

 

FRotatorMatrix RotatorMatrix;

 

행렬에서 이동 행렬을 뺀 순수하게 회전에 대한 행렬이다.

 

RotatorMatrix.GetScaledAxis(EAxis::X);

 

행렬의 크기에 의해서 스케일링된 행렬의 축을 가져오는 함수

 

2. ACharacter

 

Controller;

 

캐릭터를 제어하는 컨트롤러에 대한 변수

 

Controller->GetControlRotation();

 

컨트롤 회전을 가져오는 함수. 이것은 카메라 방향(예 : 3 인칭보기)과 다를 수 있는 완전한 목표 회전이며 제어된 Pawn (시각적으로 피치를 두거나 롤하지 않도록 선택할 수 있음)의 회전과 다를 수 있다.

 

AddControllerYawInput();

 

캐릭터 클래스에서 기본적으로 지원하는 Yaw 회전 처리 함수. 이 함수를 마우스 좌우 이동 입력과 바인딩하면 카메라 좌우 회전하는 기능을 만들 수 있다.

 

AddControllerPitchInput();

 

캐릭터 클래스에서 기본적으로 지원하는 Pitch 회전 처리 함수.

 

bPressedJump = true;

 

캐릭터 클래스에서 기본적으로 지원하는 점프 처리 변수. true가 되면 캐릭터 클래스에서 자동으로 점프를 처리해준다.

 

3. UCameraComponent

 

CameraComponent->bUsePawnControlRotation = true;

 

이 카메라 컴포넌트가 폰의 컨트롤 회전을 따라서 회전할 지를 결정하는 변수

 

4. USkeletalMeshComponent

 

USkeletalMeshComponent* SkeletalMeshComponent;

 

애니메이션을 사용할 수 있는 스켈레탈 메시 애셋의 인스턴스를 만드는데 사용되는 컴포넌트.

 

SkeletalMeshComponent->SetOnlyOwnerSee(true);

 

UPrimitiveComponent 클래스에서 상속받은 함수로 이 메시를 소유한 플레이어만 이 메시를 볼 수 있게 하는 함수

 

SkeletalMeshComponent->bCastDynamicShadow = false;

 

사전에 계산되지 않은 실시간 그림자를 그려야하는지에 대한 변수. CastShadow가 true일 때만 작동한다.

 

SkeletalMeshComponent->CastShadow = false;

 

그림자를 그려야하는지에 대한 변수

 

SkeletalMeshComponent->SetOwnerNoSee(true);

 

이 메시를 소유한 플레이어에게만 보이지 않도록 하는 함수

 

  1. Pawn 클래스에서 파생되었다. [본문으로]
반응형
  1. basicProgrammer 2019.04.04 15:43

    ㅠㅠ 헷갈렸네요..
    바로 아래 추가하는줄 알았는데... 혹시 보다가 모르시는분들 있을거같아서
    BeginPlay에 추가해주셔야돼요 ↓
    GetMesh()->SetOwnerNoSee(true);
    잘보고있습니다^_^

  2. ㅁㅇㅁㅇ 2020.01.06 19:22

    공식 튜토리얼에서는 GetMesh()->SetOwnerNoSee(true); 이걸 생성자에 쓰여있는데
    윗댓글분 처럼 Begin에 써야되네요.

    바뀐건지 튜토리얼이이상한건지..

    • wergia 2020.01.07 10:07 신고

      최신버전에서 나중에 다시 확인해봐야겠네요

  3. NogameNoHope 2020.09.25 02:22

    혹, 저와 같은 이유로 컴파일 실패가 되시는 분들을 위해 살짝 적어봅니다.
    캐릭터에 카메라 다는 부분은
    #include "Engine/Classes/Camera/CameraComponent.h" 를 추가해야 에러가 안나는것 같습니다.

    FPSCameraComponent->SetupAttachment(GetCapsuleComponent());
    이 부분에서 형식이 호환되지 않는다고 하는데
    FPSCameraComponent->SetupAttachment(reinterpret_cast<USceneComponent*>(GetCapsuleComponent()));
    형변환을 해주면 해결되는 것 같습니다.

    • wergia 2020.10.20 00:09 신고

      4.21 버전때 작성한거라 4.25 버전에서는 또 어떤지 확인해봐야겠네요 ㅜㅜ

+ Recent posts