소소한 개발 공부

FPS 캐릭터 이동 CharacterController 본문

개발/Unity

FPS 캐릭터 이동 CharacterController

이내내 2022. 3. 31. 01:29

아래 영상을 참고해 작성했습니다.

https://www.youtube.com/watch?v=_QajrabyTJc 

 

CharacterController를 이용해 FPS 시점의 캐릭터의 이동을 구현해본다.

(unity 버전 2020.3.25f1 버전 사용)

 

1. 캐릭터 생성

먼저 캐릭터로 쓸 Capsule 오브젝트와 땅 Plane 오브젝트를 생성한다.

현재 플레이어에는 Capsule collider가 있는 상태인데,

여기에 AddComponent로 Character Controller를 추가하고 Capsule collider는 제거한다. (오른쪽 이미지처럼..)

CharacterController는 기본적으로 Capsule collider를 가지고 있고 또한, rigidbody가 없이 충돌에 의한 움직임을 다루기 쉽도록 해준다.

힘(관성, 중력 등)에 의해 영향을 받지 않고, Move함수가 호출되었을 때만 움직이게 된다. 충돌에 의해 힘이 가해졌을 때에만 움직임을 수행한다.

https://docs.unity3d.com/kr/530/ScriptReference/CharacterController.html

 

Unity - 스크립팅 API: CharacterController

Success! Thank you for helping us improve the quality of Unity Documentation. Although we cannot accept all submissions, we do read each suggested change from our users and will make updates where applicable. 닫기

docs.unity3d.com

 

CharacterController의 property 를 먼저 보도록 하자.

 

- Slope Limit : 캐릭터가 오를 수 있는 경사의 한계를 각도로 나타낸다. 이 보다 작은 값의 경사만 오를 수 있다.

- Step Offset : 캐릭터가 오를 수 있는 계단의 높이를 나타낸다. 이 보다 작은 높이의 계단만 오를 수 있는데, 이 값은 CharacterController의 height 보다 크면 오류가 발생한다.

- Skin Width : 캐릭터의 충돌 스킨 너비를 나타낸다. 두 콜라이더가 서로 skin width 만큼 관통할 수 있다. skin width가 커지면 지터(jitter, 부딪힐 때 덜덜 떠는것)가 줄어든다. 작은 양수여야 한다. 또, skin width가 너무 낮으면 캐릭터가 움직일 수 없게 되는 경우가 있다. Radius의 10%로 설정하는 것이 좋다.

- Min Move Distance : 캐릭터가 표시된 값 미만으로 움직이려고 하면 움직일 수 없게 된다. 지터를 줄이기 위해 사용하는데, 대부분 0으로 둔다.

- Center : 월드 공간에서 Capsule collider의 좌표 

- Radius : Capsule collider의 반경

- Height : Capsule collider의 높이

 

2. 캐릭터에 카메라 심기

카메라를 캐릭터의 하위로 둬서 카메라 시점을 캐릭터의 머리 쯤으로 둔다.

파란색으로 표시한 곳 주목

player 오브젝트의 하위로 MainCamera를 넣고, MainCamera의 좌표를 (0,0.8,0) 으로 캐릭터의 머리 위치에 놨다.

이제 카메라가 캐릭터의 눈이 되었다.

 

마우스를 움직임으로써 카메라를 회전 시키면 캐릭터가 고개를 돌리는 것처럼 게임 내를 볼 수 있다.

이는 스크립트로 구현한다.

아래의 스크립트는 MainCamera의 컴포넌트로 추가한다.

MouseLook.cs

using UnityEngine;

public class MouseLook : MonoBehaviour
{
    public float mouseSesitivity = 100f;
    public Transform playerBody;
    float xRotation = 0f;

    private void Start() {
        Cursor.lockState = CursorLockMode.Locked;
    }

    private void Update() {
        float mouseX = Input.GetAxis("Mouse X") * mouseSesitivity * Time.deltaTime;
        float mouseY = Input.GetAxis("Mouse Y") * mouseSesitivity * Time.deltaTime;

        xRotation -= mouseY;
        xRotation = Mathf.Clamp(xRotation, -45f, 45f);

        transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);
        playerBody.Rotate(Vector3.up * mouseX);
    }
}

mouseSesitivity는 마우스 이동으로 화면을 회전 시키는데 그 화면 회전 정도에 영향을 미친다.

playerBody는 상위의 캐릭터의 위치이다. 마우스 좌우 이동으로 카메라를 회전시킴으로써 캐릭터 방향을 돌리는 효과를 주기 위해 필요하다. 상위의 캐릭터 오브젝트를 에디터에서 할당해준다.

xRotation은 카메라의 위-아래 회전 값이다. 화면이 위-아래로 움직이려면 x축을 기준으로 카메라가 회전해야 하기 때문에 xRotation이라 명명한다. 이는 mouseY 값(마우스를 y축으로 이동한 것을 입력받은 것)에 영향을 받는다.

x축 기준으로 회전하면 위-아래로 회전할 수 있다

 카메라는 mouseY가 양수일 때 (마우스를 위 방향으로 이동할 때) 위를 바라봐야한다. 카메라가 위를 바라볼 때의 world 상의 Rotation x 값은 음수이다. (마우스 이동 값과 카메라 회전 값은 반대)

이렇게 위를 볼 때 rotation x 값이 음수이다.

따라서 이를 계산할 때

xRotation -= mouseY 로 마우스 이동 값의 음수를 더해주는 것이다. 

 

또 너무 아래 혹은 위로 카메라가 돌아가면 안되니까 Clamp로 회전 최솟값, 최댓값을 정해준다.

 

그 후 xRotation 값이 정해지면 transform.localRotation을 Quaternion으로 각도 값으로 넣어준다.

 

마우스가 좌우로 움직일 때는 캐릭터의 좌우 방향도 바뀌게 해주는데 playerBody.Rotate(Vector3.up * mouseX) 으로 회전시킨다. Vector3.up 은 new Vector3 (0,1,0)와 같다.

 

Cursor.lockState = CursorLockMode.Locked; 는 커서를 화면에서 숨기게 해준다. ESC를 누르면 커서가 다시 나타나고 게임뷰를 클릭하면 다시 사라진다.

 

3. 캐릭터 이동하기

키보드 WASD 를 누름으로써 캐릭터를 앞뒤좌우로 움직이게 하려고 한다.

아래의 스크립트를 Player 오브젝트의 컴포넌트로 추가한다.

using UnityEngine;

public class CharacterMovement : MonoBehaviour
{
    private CharacterController controller;

    public float speed = 2f;			// 속력
    public float gravity = -9.81f;		// 중력
    public float jumpHeight = 0.4f;		// 점프 높이

    public Transform groundCheck;		// 땅 위치 재는 Transform
    public float groundDistance = 0.4f;		// 땅이 근처에 있는 지 알아보는 범위
    public LayerMask groundMask;		//  layer - ground 만 인식하기 위한 mask

    Vector3 velocity;				// 중력을 받아 아래로 떨어지기 위한 속도
    bool isGrounded;				// 땅에 닿아있나?

    private void Start() {
        controller = GetComponent<CharacterController>();
    }

    private void Update() {
        isGrounded = Physics.CheckSphere(groundCheck.position, groundDistance, groundMask);

        if (isGrounded && velocity.y < 0)
        {
            velocity.y = -2f;
        }

        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");

        Vector3 move = transform.right * x + transform.forward * z;
        controller.Move(move * speed * Time.deltaTime);

        if (Input.GetButtonDown("Jump") && isGrounded) 
        {
            velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
        }

        velocity.y += gravity * Time.deltaTime;
        controller.Move(velocity * Time.deltaTime); // delta y = 1/2g*t^2 라서 Time.deltaTime 제곱
    }
}

캐릭터의 앞뒤 좌우 이동을 먼저 보자면,

Horizontal 값(A, D)은 transform.right = (1,0,0)과 곱해서 좌(x의 음의 값) 우(x의 양의 값) 이동을 할 수 있다.

Vertical 값(W, S)은 transform.forward = (0,0,1)과 곱해서 앞(z의 양의 값) 뒤(z의 음의 값) 이동을 할 수 있다.

move 는 속력과 Time.deltaTime (프레임률 적용 시간) 과 연산되어 CharacterController의 Move 함수를 통해 이동을 할 수 있게 된다.

float x = Input.GetAxis("Horizontal");
float z = Input.GetAxis("Vertical");

Vector3 move = transform.right * x + transform.forward * z;
controller.Move(move * speed * Time.deltaTime);

캐릭터의 추락에 대한 내용이다.

높은 곳에 있다가 지면이 없는 곳으로 가면 떨어져야 하는데 현재는 떨어지지 않기 때문에

아래의 코드를 덧붙여, 중력 값을 Move 함수에 넣어 캐릭터가 추락(y축 이동)하게 한다.

velocity.y += gravity * Time.deltaTime;
controller.Move(velocity * Time.deltaTime); // delta y = 1/2g*t^2 라서 Time.deltaTime 제곱

하지만 위 코드까지만 작성하면 너무 빨리 떨어지게 된다.

땅에 닿아있을 때도 velocity 값이 연산되어 계속 떨어지기 때문이다. (계속 뺄셈이 된다.)

땅에 닿아있을 때 velocity.y값은 고정되어 있어야하므로 아래 코드처럼 isGruond 한지 판정하고

isGround 하며 velocity.y가 음수라면 velocity.y를 -2f로 고정해준다.

-> -2f로 해주는 이유는 플레이어를 아래로 끌어내리게 해주는 가장 작은 수라서 해주는데 -1f로 해도 추락하긴 한다. 근데 땅과의 거리가 0.4 이하가 될 때 추락 속력이 1f가 되어 갑자기 스무스하게 떨어지는 느낌이 들게 된다. 이건 테스트하면서 바꿔도 좋을 것 같다.

 

Physics.CheckSphere은 groundCheck.position을 기준으로 groundDistance 길이의 Radius를 가진 Sphere를 만들어서 거기에 groundMask에 해당하는 layer가 있다면 true를 반환하는 함수이다.

isGrounded = Physics.CheckSphere(groundCheck.position, groundDistance, groundMask);

if (isGrounded && velocity.y < 0)
{
    velocity.y = -2f;
}

groundCheck은 캐릭터의 발 부분에 놔서 바닥을 체크하게 한다.

groundCheck은 player의 하위 오브젝트로 놓고 CharacterMovement 컴포넌트에 할당해준다. 또한 땅 오브젝트(Plane)의 layer를 Ground로 설정한다. (먼저 Layer에 Ground를 추가해놓는다.)

 이제 플레이어가 바닥과 닿아있을 때 추락 속도가 변하지 않아 난간에서 떨어질 때 갑자기 떨어지는 느낌이 들지 않는다.

 

점프에 대해 살펴 본다. 스페이스바를 눌러 캐릭터가 점프할 수 있게 해보자.

땅과 닿아있을 때 스페이스바를 누르면 velocity.y 가 (-2f * -9.81f * 점프 높이) 의 루트가 된다.

이 식은 물리와 닿아있는데 위치에 따른 속도 식으로

½mv² = mgh -> v = √(2gh) 에서 따온 것이다.

if (Input.GetButtonDown("Jump") && isGrounded) 
{
    velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
}

 

이렇게, 앞뒤좌우 이동, 추락, 점프가 되는 FPS 캐릭터가 완성되었다.