본문 바로가기
C#/UNITY_FPS 3D 서바이벌게임

Chapter 1-1. 캐릭터, 지형 : 마우스, 키보드에 따른 이동과 회전, 1인칭 카메라

by w1z 2024. 3. 11.

Chapter 1. 캐릭터 움직이기 & 지형 제작

캐릭터 움직이기 - 기본

캐릭터의 크기는 캡슐의 크기로 어림 잡으면 편하다.

1 인칭 카메라

Main Camera 오브젝트를 A라는 이름의 오브젝트의 자식으로 넣으면 Main Camera는 A의 1인칭 카메라가 된다. 즉, A 의 시점으로 화면을 비춘다. 카메라의 위치가 A 의 시점이 되도록 Trnasform 값만 조정해주면 A 를 따라다니며(자식이니까) A 의 시점을 촬영하는 1 인칭 카메라가 된다.

 

 

📜PlayerController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [SerializeField]
    private float walkSpeed;

    [SerializeField]
    private float lookSensitivity; 

    [SerializeField]
    private float cameraRotationLimit;  
    private float currentCameraRotationX;  

    [SerializeField]
    private Camera theCamera; 
    private Rigidbody myRigid;

    void Start() 
    {
        myRigid = GetComponent<Rigidbody>();  // private
    }

    void Update()  // 컴퓨터마다 다르지만 대략 1초에 60번 실행
    {
        Move();                 // 1️⃣ 키보드 입력에 따라 이동
        CameraRotation();       // 2️⃣ 마우스를 위아래(Y) 움직임에 따라 카메라 X 축 회전 
        CharacterRotation();    // 3️⃣ 마우스 좌우(X) 움직임에 따라 캐릭터 Y 축 회전 
    }

    private void Move()
    {
        float _moveDirX = Input.GetAxisRaw("Horizontal");  
        float _moveDirZ = Input.GetAxisRaw("Vertical");  
        Vector3 _moveHorizontal = transform.right * _moveDirX; 
        Vector3 _moveVertical = transform.forward * _moveDirZ; 

        Vector3 _velocity = (_moveHorizontal + _moveVertical).normalized * walkSpeed; 

        myRigid.MovePosition(transform.position + _velocity * Time.deltaTime);
    }

    private void CameraRotation()  
    {
        float _xRotation = Input.GetAxisRaw("Mouse Y"); 
        float _cameraRotationX = _xRotation * lookSensitivity;
        
        currentCameraRotationX -= _cameraRotationX;
        currentCameraRotationX = Mathf.Clamp(currentCameraRotationX, -cameraRotationLimit, cameraRotationLimit);

        theCamera.transform.localEulerAngles = new Vector3(currentCameraRotationX, 0f, 0f);
    }

    private void CharacterRotation()  // 좌우 캐릭터 회전
    {
        float _yRotation = Input.GetAxisRaw("Mouse X");
        Vector3 _characterRotationY = new Vector3(0f, _yRotation, 0f) * lookSensitivity;
        myRigid.MoveRotation(myRigid.rotation * Quaternion.Euler(_characterRotationY)); // 쿼터니언 * 쿼터니언
        // Debug.Log(myRigid.rotation);  // 쿼터니언
        // Debug.Log(myRigid.rotation.eulerAngles); // 벡터
    }
}

 

🙋‍♀️ WASD, 방향키 키보드 입력에 따라 이동

    [SerializeField]
    private float walkSpeed;
    
    private Rigidbody myRigid;

    void Start()  // 게임이 처음 시작될 때 1회 실행
    {
        myRigid = GetComponent<Rigidbody>();  
    }

    ...

    private void Move()
    {
        float _moveDirX = Input.GetAxisRaw("Horizontal");  
        float _moveDirZ = Input.GetAxisRaw("Vertical");  

        Vector3 _moveHorizontal = transform.right * _moveDirX; 
        Vector3 _moveVertical = transform.forward * _moveDirZ; 

        Vector3 _velocity = (_moveHorizontal + _moveVertical).normalized * walkSpeed;  

        myRigid.MovePosition(transform.position + _velocity * Time.deltaTime);
    }

유니티에서는 X 축 이동 👉 좌우, Z 축 이동 👉 앞뒤, Y 축 이동 👉 위아래

  • walkSpeed 👉 이동 속도(스칼라)
  • myRigid 👉 Rigidbody 컴포넌트를 할당할 변수로, private 하기 때문에 게임 시작시 GetComponent 함수를 통해 할당해준다.
  • _moveDirX 👉 좌우 이동 크기(스칼라)
    • A 혹은  입력시 -1
    • 입력 X시 0
    • D 혹은  입력시 1
  • _moveDirZ 👉 앞뒤 이동 크기(스칼라)
    • S 혹은  입력시 -1
    • 입력 X시 0
    • W 혹은  입력시 1
  • _moveHorizontal 👉 좌우 이동 벡터 값
    • 방향 * 크기 = transform.right * _moveDirX
      • transform.right 은 나(이 스크립트가 붙어있는 캡슐)의 오른쪽 방향을 나타내는 단위 벡터
  • _moveVertical 👉 앞뒤 이동 벡터 값
    • 방향 * 크기 = transform.right * _moveDirX
      • transform.forward 은 나(이 스크립트가 붙어있는 캡슐)의 앞쪽 방향 을 나타내는 단위 벡터
  • _velocity 👉 속도 벡터
    • 방향 x 속도 크기
      • (좌우 이동 벡터 + 앞뒤 이동 벡터).normalized = 이동할 방향
      • 속도 크기를 나타내는 float 타입인 walkSpeed를 곱한다.
  • (현재의 위치 + 속도 X 델타 타임) 위치로 이동한다.
    • Move() 함수는, 컴퓨터 환경에 따라 다르지만 보통 1 초에 60번 실행되는 Update() 함수 내부에서 실행될 것이기 때문에 1초 동안 총 _velocity이 더해질 수 있도록 1 프레임당 1/60 * _velocity 만큼 더해주어야 한다. Time.deltaTime이 1/60가 된다.

 

🙋‍♂️ 회전축 개념

해당 축을 고정한 체, 해당 축의 입장에서 시계 방향으로 회전한다. 따라서 X 축 회전은 고개를 숙이는게 Positive 방향 회전이다.

  • X 축 회전
    • X 축을 중심으로 회전한다는 의미. X 축 회전값은 변함 없다.
    • X 축은 평면 가로축(좌우)이니 가로로 긴 평행봉을 잡고 구른다고 생각해보자. 앞으로 숙이고 뒤로 넘어가는 회전하는 것과 같다.
    • 고개를 젖히고 숙이는 것이 바로 X 축 회전이라 할 수 있겠다.
      • 따라서 마우스를 위아래로 움직이면 고개를 숙였다 올렸다 하는 1인칭 시점을 구현해야 하므로 오브젝트의 머리에 달려있는 1인칭 카메라인 Main Camera를 X 축 회전해주어야 한다.
        • 캡슐(나 자신)은 가만히 있는 상태에서 카메라만 X 축으로 회전시키는 것이다.
  • Y 축 회전
    • Y 축을 중심으로 회전한다는 의미. Y 축 회전값은 변함 없다.
    • Y 축은 수직 축이니 전봇대를 잡고 빙글 빙글 회전하는 것과 같다. 😭
      • 따라서 마우스를 양옆으로 움직이면 고개를 좌우로 돌리는 1인칭 시점을 구현해야 하므로 나 자신을 Y 축 회전해주어야 한다.
        • 카메라는 가만히 있고 캡슐(나 자신)은 Y 축으로 회전. 몸을 마우스가 향하는 방향으로 돌림.
  • Z 축 회전
    • Z 축을 중심으로 회전한다는 의미. Z 축 회전값은 변함 없다.
    • Z 축은 평면 세로축(앞뒤)이니 양 옆으로 굴러가는 회전을 하는 것과 같다.
  • 회전 축 잘 모르겠다면 직접 오브젝트의 회전축을 잡고 이리저리 조작해보자.

 

🙋‍♀️ 마우스 위아래(Y)로 움직일시 카메라 X 축 회전

    [SerializeField]
    private float lookSensitivity;  // 민감도

    [SerializeField]
    private float cameraRotationLimit;  // 카메라를 일정 각도 이상으로 회전 못시키게 하는 제한 값.
    private float currentCameraRotationX;  // 카메라의 X 축 회전값. 초기값은 정면을 보게끔 기본값 0.

    [SerializeField]
    private Camera theCamera;  

    ...

    private void CameraRotation()  
    {
        float _xRotation = Input.GetAxisRaw("Mouse Y");  
        float _cameraRotationX = _xRotation * lookSensitivity;

        // currentCameraRotationX += _cameraRotationX;  마우스 Y 반전
        currentCameraRotationX -= _cameraRotationX;
        currentCameraRotationX = Mathf.Clamp(currentCameraRotationX, -cameraRotationLimit, cameraRotationLimit);

        theCamera.transform.localEulerAngles = new Vector3(currentCameraRotationX, 0f, 0f);
    }
  • theCamera는 메인카메라가 할당 된다. 컴포넌트가 아니라서 GetComponent로 가져올 수가 없어서 private이지만 유니티 에디터에서 할당해줄 수 있도록 [SerializField] 속성을 붙여주었다.
    • Hierarchy 창에서 해당 하는 오브젝트를 찾아주는 FindObjectOfType 함수를 사용할 수도 있지만 오브젝트가 굉장히 많다면 너무 느리고 카메라도 여러개를 둘 수도 있기 때문에 이 방법은 비추.
  • 마우스 커서는 2D다. X 축, Y 축 이동 밖에 없다.
  • Input.GetAxisRaw(“Mouse Y”) 역시 -1, 0, 1만 나타낸다.
    • 마우스가 아래로 움직이면 -1
    • 마우스가 위아래로 움직이지 않았으면 0
    • 마우스가 위로 움직이면 1
  • 카메라가 X 축으로 회전 할 만큼의 양 _cameraRotationX
    • 마우스가 Y방향으로 움직인 정도(_xRotation) * 민감도
      • _xRotation은 -1, 0, 1 값중 하나다.
    • Update 함수 내부에서 실행될 것이라서 1 프레임에 이만큼 회전하는게 된다.
  • 카메라의 X 축 방향 회전 값 currentCameraRotationX
    • 1️⃣ _cameraRotationX만큼 빼준다.
      • x 축 방향으로 회전할 때 아래로 숙이는게 Positive 방향이고 정면을 보는게 0 이고 위로 젖히는게 Negative 방향이다.
        • 마우스가 아래로 움직이면 _cameraRotationX 값이 Input.GetAxisRaw에 의해 음수가 되기 때문에 _cameraRotationX만큼을 더해주게 되면 currentCameraRotationX 값이 감소하여 카메라가 위로 회전하게 된다. 따라서 +=로 더해주게 되면 오히려 마우스를 아래로 내릴 수록 위를 쳐다보게 된다.
        • 여기선 마우스가 아래로 움직이면 카메라의 x축 회전 값이 커져 카메라가 아래로 숙이게끔 -= 빼주었다.
          • 보통 FPS 게임에 이런 마우스 상하 반전 설정 항목이 있다.
    • 2️⃣ Mathf.Clamp를 사용하여 currentCameraRotationX가 (-cameraRotationLimit ~ cameraRotationLimit) 범위 안에만 있도록 한다.
      • 마우스를 위아래로 움직일때 360도 회전해버리면 안되고 고개를 젖힐때나 숙일때나 어느정도 제한을 두어야 해서
  • 카메라의 회전 값 설정 👉 (currentCameraRotationX, 0, 0)으로 설정해주기
    • 카메라 회전 값 (Vector3로 변환) theCamera.transform.localEulerAngles

 

🙋‍♀️ 마우스 좌우(X)로 움직일시 나 자신을 Y 축 회전

    private void CharacterRotation()  // 좌우 캐릭터 회전
    {
        float _yRotation = Input.GetAxisRaw("Mouse X");
        Vector3 _characterRotationY = new Vector3(0f, _yRotation, 0f) * lookSensitivity;
        myRigid.MoveRotation(myRigid.rotation * Quaternion.Euler(_characterRotationY)); // 쿼터니언 * 쿼터니언

        // Debug.Log(myRigid.rotation);  // 쿼터니언
        // Debug.Log(myRigid.rotation.eulerAngles); // 벡터
    }
  • 두 쿼터니언의 회전량을 더하려면, 두 쿼터니언 값을 곱해야 한다.

 

컴포넌트 설정

 

  • 캡슐 오브젝트에
    • Rigidbody 컴포넌트를 붙여준다.
      • 캡슐 오브젝트는 물리 기능을 가지게 된다. (중력, 마찰력 등등이 적용 됨)
      • 캡슐 오브젝트가 밑이 동그래서 자꾸 양옆(Z축 회전),앞뒤(X축 회전)로 넘어지므로 X, Z 회전은 안되게끔 막아두었다.
    • 📜PlayerController.cs 를 붙여준다.
      • 캡슐 오브젝트는 이제 아래와 같은 기능을 가지게 된다.
        • 키보드 입력에 따른 이동
        • 마우스 Y 회전에 따른 카메라의 X 축 회전
        • 마우스 X 회전에 따른 캡슐의 Y 축 회전