Coding Feature.

[Unity3D] FPS 게임 만들기 #1 플레이어 컨트롤러 구현(움직임, 경사면 이동, 점프) 본문

Toy Project/Spinal-Cord-Carnage [Unity3D]

[Unity3D] FPS 게임 만들기 #1 플레이어 컨트롤러 구현(움직임, 경사면 이동, 점프)

codingfeature 2024. 1. 27. 16:36

유니티 3D로 FPS 게임을 만들어보겠습니다.

 

이번 프로젝트를 통해서 다음과 같은 내용을 공부하고 구현해볼 예정입니다.

 

- 1인칭 시점 게임에서의 더블 점프, 대시 등 다양한 액션 구현

- 랜덤 맵 절차 생성

- 적 AI 구현

- 3D 렌더링 (블렌더 등)

 

최종 결과물은 POST VOID 라는 게임과 닮을 것이라 기대됩니다!

https://store.steampowered.com/app/1285670/Post_Void/

 

Post Void on Steam

Post Void is a hypnotic scramble of early first-person shooter design that values speed above all else. Keep your head full and reach the end; Kill what you can to see it mend; Get the high score or try again.

store.steampowered.com

 

 

 

우선 게임 씬에 다음과 같이 플레이어, 카메라를 설정하였습니다.

 

플레이어의 눈 위치에 Eyes라는 게임 오브젝트를 놓고, 발 아래 위치에는  Ground Checker 게임 오브젝트를 놓았습니다.

 

그리고 1인칭 시점에서 캐릭터를 조종하기 위해 먼저 Input 매니저를 작성하였습니다.

 

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

public class InputManager : MonoBehaviour
{
    private static InputManager instance;

    const string xAxis = "Mouse X";
    const string yAxis = "Mouse Y";

    Vector2 rotation = Vector2.zero;

    [Range(0.1f, 9f)] public float sensitivity;

    public float horizontalInput, VerticalInput;
    public Quaternion xQuat, yQuat;

    [Header("Key Bind")]
    public KeyCode jumpKey;

    public static InputManager Instance
    {
        get
        {
            return instance;
        }
    }

    private void Awake()
    {
        if (instance)
        {
            Destroy(instance);
            return;
        }

        instance = this;
        DontDestroyOnLoad(this.gameObject);
    }

    private void Start()
    {
        LockMouse();
    }

    private void Update()
    {
        rotation.x += Input.GetAxis(xAxis) * sensitivity;
        rotation.y += Input.GetAxis(yAxis) * sensitivity;
        rotation.y = Mathf.Clamp(rotation.y, -90f, 90f);

        xQuat = Quaternion.AngleAxis(rotation.x, Vector3.up);
        yQuat = Quaternion.AngleAxis(rotation.y, Vector3.left);

        horizontalInput = Input.GetAxisRaw("Horizontal");
        VerticalInput = Input.GetAxisRaw("Vertical");
    }

    void LockMouse()
    {
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
    }

    void UnlockMouse()
    {
        Cursor.lockState = CursorLockMode.None;
        Cursor.visible = true;
    }
}

 

 

 

Update 함수에서 마우스의 x, y축 이동에 대한 쿼터니언을 구합니다.

그리고 WASD 이동에 대한 값을 구합니다.

 

위에서 구한 Input 내용은 플레이어 컨트롤러 그리고 카메라 컨트롤러에서 사용하게 됩니다.

 

아래는 카메라 컨트롤러 입니다.

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

public class CameraController : MonoBehaviour
{
    private static CameraController instance;
    public static CameraController Instance
    {
        get
        {
            return instance;
        }
    }

    private void Awake()
    {
        if (instance)
        {
            Destroy(instance);
            return;
        }

        instance = this;
        DontDestroyOnLoad(this.gameObject);
    }

    private void Update()
    {
        transform.position = PlayerController.Instance.eyes.transform.position;

        Quaternion xQuat, yQuat;
        xQuat = InputManager.Instance.xQuat;
        yQuat = InputManager.Instance.yQuat;

        transform.localRotation = xQuat * yQuat;
    }
}

 

Update 함수에서 카메라의 위치를 플레이어의 Eyes 게임 오브젝트 위치로 이동시킵니다.

그리고 Input 매니저에서 구한 쿼터니언들을 곱해서 카메라의 회전을 구현합니다.

 

마지막으로 플레이어 컨트롤러를 작성해서 플레이어를 직접 움직일 수 있도록 해야 합니다.

 

이번에는 아래 내용들을 구현할 것입니다.

1) 플레이어의 기본 이동

2) 점프

3) 경사면 이동

 

1번의 경우, Input 매니저에서 구한 WASD 이동 값을 이용해서 이동 벡터를 구하고 벡터 값에 이동 속력을 곱해서 플레이어의 RigidBody에 AddForce 함수를 이용함으로써 구현합니다.

 

2번은 앞서 게임 오브젝트로 구현한 Ground Checker를 이용해서 아래에 땅이 있는 경우에만 점프할 수 있도록 하고, 점프할 때에는 위와 마찬가지로 AddFoce 함수를 사용해서 구현합니다.

 

3번은 아래와 같이 구현합니다.

먼저 플레이어 아래의 땅이 경사면인지 확인합니다. 그리고 경사면인 경우, 현재 플레이어의 이동 벡터를 그 경사면에서의 벡터로 변환시킵니다. 그리고 경사면에서의 벡터를 이용해서 이동시키게 됩니다.

 

위 3 가지 요소를 모두 아래와 같이 구현하였습니다.

 

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

public class PlayerController : MonoBehaviour
{
    private static PlayerController instance;

    public GameObject eyes;
    public GameObject groundChecker;

    public Rigidbody rigidBody;

    Vector3 moveDirection;

    bool isGrounded;
    bool isOnSlope;

    [Header("Ground Check")]
    public float groundCheckingRadius;
    public LayerMask whatIsGround;

    [Header ("Basic Movement")]
    public float speedNormal;
    public float groundDrag;
    public float airDrag;
    public float airSpeedRatio;

    [Header("Jump")]
    public float jumpForce;

    [Header("Slope")]
    public float maxSlopeAngle;
    public float slopeSpeedRatio;
    GameObject slopeGameObject;

    public static PlayerController Instance
    {
        get
        {
            return instance;
        }
    }

    private void Awake()
    {
        if (instance)
        {
            Destroy(instance);
            return;
        }

        instance = this;
        DontDestroyOnLoad(this.gameObject);
    }

    private void Update()
    {
        transform.localRotation = InputManager.Instance.xQuat;

        float horizontalInput = InputManager.Instance.horizontalInput;
        float verticalInput = InputManager.Instance.VerticalInput;

        // 플레이어 이동 방향에 대한 normal vector 구하기.
        moveDirection = transform.forward * verticalInput + transform.right * horizontalInput;
        moveDirection.Normalize();

        isGrounded = IsGrounded();
        isOnSlope = IsOnSlope();

        if (isOnSlope)
            rigidBody.useGravity = false;
        else
            rigidBody.useGravity = true;

        if (isGrounded)
        {
            rigidBody.drag = groundDrag;
        }
        else
        {
            rigidBody.drag = airDrag;
        }

        if (Input.GetKeyDown(InputManager.Instance.jumpKey) && isGrounded)
        {
            Jump();
        }
    }

    void MovePlayer()
    {
        if(isOnSlope)
            rigidBody.AddForce(GetVectorOnSlope(moveDirection) * speedNormal * slopeSpeedRatio, ForceMode.Force);
        else if(isGrounded)
            rigidBody.AddForce(moveDirection * speedNormal, ForceMode.Force);
        else
            rigidBody.AddForce(moveDirection * speedNormal * airSpeedRatio, ForceMode.Force);
    }

    private void FixedUpdate()
    {
        MovePlayer();
    }

    void OnDrawGizmos()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawSphere(groundChecker.transform.position, groundCheckingRadius);
    }

    bool IsGrounded()
    {
        return Physics.CheckSphere(groundChecker.transform.position, groundCheckingRadius, whatIsGround);
    }

    void Jump()
    {
    	rigidBody.velocity = new Vector3(rigidBody.velocity.x, 0, rigidBody.velocity.z);
        rigidBody.AddForce(transform.up * jumpForce, ForceMode.Impulse);
    }

    bool IsOnSlope()
    {
        Collider[] col = Physics.OverlapSphere(groundChecker.transform.position, groundCheckingRadius);
        if(col.Length != 0)
        {
            slopeGameObject = col[0].gameObject;

            // 플레이어가 닿은 땅의 angle 구하기.
            float groundAngle = Vector3.Angle(Vector3.up, slopeGameObject.transform.up);

            Debug.Log(groundAngle);

            if (5f < groundAngle && groundAngle < maxSlopeAngle)
                return true;
        }
        return false;
    }

    Vector3 GetVectorOnSlope(Vector3 vector)
    {
        Vector3 newVector;

        if (isOnSlope)
            newVector = Vector3.ProjectOnPlane(vector, slopeGameObject.transform.up).normalized;
        else
            newVector = vector;

        return newVector;
    }
}

 

 

 

다음은 더블 점프, 대쉬를 구현해보겠습니다!