Unity Chapter 11-6. 좀비 TPS 게임 만들기 : Player Movement

Date:     Updated:

카테고리:

태그:

인프런에 있는 이제민님의 레트로의 유니티 C# 게임 프로그래밍 에센스 강의를 듣고 정리한 필기입니다. 😀
🌜 [레트로의 유니티 C# 게임 프로그래밍 에센스] 강의 들으러 가기!


Chapter 11. 좀비 TPS 게임 만들기

📜PlayerMovement.cs

플레이어의 입력에 따라 플레이어를 움직인다. 입력을 감지하는건 📜PlayerInput.cs가 하기 때문에 입력을 감지하는 부분은 빠져있다.

using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    private CharacterController characterController;
    private PlayerInput playerInput;
    private PlayerShooter playerShooter;
    private Animator animator;
    
    private Camera followCam;
    
    public float speed = 6f;
    public float jumpVelocity = 20f;
    [Range(0.01f, 1f)] public float airControlPercent;

    public float speedSmoothTime = 0.1f;
    public float turnSmoothTime = 0.1f;
    
    private float speedSmoothVelocity;
    private float turnSmoothVelocity;
    
    private float currentVelocityY;
    
    public float currentSpeed =>
        new Vector2(characterController.velocity.x, characterController.velocity.z).magnitude;
    
    private void Start()
    {
        animator = GetComponent<Animator>();
        playerInput = GetComponent<PlayerInput>();
        followCam = Camera.main;
        characterController = GetComponent<CharacterController>();
    }

    private void FixedUpdate()
    {
        if (currentSpeed > 0.2f || playerInput.fire) Rotate();

        Move(playerInput.moveInput);
        
        if (playerInput.jump) Jump();
    }

    private void Update()
    {
        UpdateAnimation(playerInput.moveInput);
    }

    public void Move(Vector2 moveInput)
    {
        var targetSpeed = speed * moveInput.magnitude;
        var moveDirection = Vector3.Normalize(transform.forward * moveInput.y + transform.right * moveInput.x);
        
        var smoothTime = characterController.isGrounded ? speedSmoothTime : speedSmoothTime / airControlPercent;

        targetSpeed = Mathf.SmoothDamp(currentSpeed, targetSpeed, ref speedSmoothVelocity, smoothTime);
        currentVelocityY += Time.deltaTime * Physics.gravity.y;

        var velocity = moveDirection * targetSpeed + Vector3.up * currentVelocityY;

        characterController.Move(velocity * Time.deltaTime);

        if (characterController.isGrounded) currentVelocityY = 0;
    }

    public void Rotate()
    {
        var targetRotation = followCam.transform.eulerAngles.y;

        transform.eulerAngles = Vector3.up * Mathf.SmoothDampAngle(transform.eulerAngles.y, targetRotation,
                                    ref turnSmoothVelocity, turnSmoothTime);
    }

    public void Jump()
    {
        if (!characterController.isGrounded) return;
        currentVelocityY = jumpVelocity;
    }

    private void UpdateAnimation(Vector2 moveInput)
    {
        var animationSpeedPercent = currentSpeed / speed;

        animator.SetFloat("Horizontal Move", moveInput.x * animationSpeedPercent, 0.05f, Time.deltaTime);
        animator.SetFloat("Vertical Move", moveInput.y * animationSpeedPercent, 0.05f, Time.deltaTime);
    }
}


멤버 변수

    private CharacterController characterController;
    private PlayerInput playerInput;
    private PlayerShooter playerShooter;
    private Animator animator;
    
    private Camera followCam;
    
    public float speed = 6f;
    public float jumpVelocity = 8f;
    [Range(0.01f, 1f)] public float airControlPercent;

    public float speedSmoothTime = 0.1f;
    public float turnSmoothTime = 0.1f;
    
    private float speedSmoothVelocity;
    private float turnSmoothVelocity;
    
    private float currentVelocityY;
    
    public float currentSpeed =>
        new Vector2(characterController.velocity.x, characterController.velocity.z).magnitude;
  • characterController
    • CharacterController 컴포넌트
      • 이 컴포넌트를 통해 Player Character 오브젝트의 Collider 처리
  • playerInput
    • 📜PlayerInput.cs 스크립트
      • 얘로부터 입력 감지를 받아야 함
  • playerShooter
    • 📜PlayerShooter.cs 스크립트
      • 추후 작성함
  • Animator
    • Animator 컴포넌트
      • 이 컴포넌트를 통해 Player Character 오브젝트의 애니메이션을 제어
  • followCam
    • Camera 컴포넌트
      • Main Camera (Brain Camera)
      • 플레이어가 움직일때 카메라의 방향을 기준으로 움직이게 되므로 현재 카메라의 방향을 알기 위해서 필요.
  • speed
    • 움직임 속도 (초당 6)
  • jumpVelocity
    • 점프하는 순간에 사용될 속도 (초당 8)
  • airControlPercent
    • 점프해서 플레이어가 공중에 체류하는 동안 원래 속도의 몇 퍼센트의 속도를 통제할 수 있을지에 대한 퍼센트.
      • Range를 사용해서 0.01 ~ 1 사이의 값을 유니티에서 선택할 수 있도록 함.
    • 이 값이 0이면 플레이어가 공중에 있을 땐 조작을 할 수 없다.
    • 이 값이 1이면 플레이어가 공중에 있어도 원래 속도로 조작할 수 있다.
  • speedSmoothTime
    • 이동하는데 있어 스무스하게 댐핑하는 지연 시간
  • turnSmoothTime
    • 회전하는데 있어 스무스하게 댐핑하는 지연 시간
  • speedSmoothVelocity
    • Mathf의 SmoothDamp 함수를 사용할때 스무스하게 변화하는 값을 계속해서 리턴하기 때문에 그 값을 받기 위해 선언.
  • turnSmoothVelocity
    • Mathf의 SmoothDamp 함수를 사용할때 스무스하게 변화하는 값을 계속해서 리턴하기 때문에 그 값을 받기 위해 선언.
  • currentVelocityY
    • 플레이어의 Y 방향 속도
      • Rigidbody와 달리 Character Controller 컴포넌트는 외부의 물리적인 힘을 받지 않으므로 중력을 받아 떨어지지 않기 때문에 개발자가 직접 Y 방향 속도를 조정해야 한다.
  • currentSpeed
    • Y를 제외한 X, Z 방향에서의 속도. 즉, 플레이어의 지면 상에서의 현재 속도.
      • get 프로퍼티 인데 람다식으로 =>을 사용하여 간단하게 표현했다.


함수

private void Start()

    private void Start()
    {
        animator = GetComponent<Animator>();
        playerInput = GetComponent<PlayerInput>();
        playerShooter = GetComponent<PlayerShooter>();
        playerShooter = 
        followCam = Camera.main;
        characterController = GetComponent<CharacterController>();
    }

📜PlayerMovement사용할 컴포넌트들의 레퍼런스들을 게임 시작시 초기화 해주기

  1. Animator 컴포넌트
    • 애니메이션
  2. 📜PlayerInput 컴포넌트
    • 유저의 입력 : 플레이어 캐릭터를 이동, 점프, 회전시킬거라 필요
  3. 📜PlayerShooter 컴포넌트
    • 에임 상태가 조준 상태가 되면 카메라가 바라보는 방향으로 플레이어 캐릭터를 회전 시켜놔야 하기 때문에 에임 상태 정보를 추후 작성할 📜PlayerShooter.cs로부터 받아야 한다.
  4. Main Camera 오브젝트
    • 플레이어가 움직일때 카메라의 방향을 기준으로 회전하고 움직이게 되므로 필요
    • 현재 Main 태그가 붙어있는 카메라
  5. CharacterController 컴포넌트
    • 플레이어 캐릭터를 이동, 점프, 회전시킬거라 필요


private void FixedUpdate()

  • ⬜Rotate()
  • ⬜Move()
  • ⬜Jump()
    private void FixedUpdate()
    {
        if (currentSpeed > 0.2f || playerInput.fire || playerShooter.aimState == PlayerShooter.AimState.HipFire) Rotate();

        Move(playerInput.moveInput);

        if (playerInput.jump) Jump();
    }

물리 갱신 주기에 맞춰 자동 실행. 회전(Rotate), 이동(Move) 담당

  • 회전 Rotate
    • 현재 속도가 0.2 보다 크거나 발사 버튼 입력이 들어왔을 때
      • 즉, 플레이어가 조금이라도 움직이고 있거나 공격을 하려 할 때는 카메라의 방향괴 플레이어의 방향을 일치시켜준다.
      • 0.2 보다 더 크게 하면 예를 들어 달릴때나 플레이어가 카메라와 일치하는 방향으로 회전할 것
      • 플레이어가 움직이지 않을 땐 플레이어 캐릭터를 카메라에 맞춰 회전시키지 않아야 한다.
        • 배그에서 조작 안하고 가만히있을때 Alt 로 카메라를 둘러 보아도 캐릭터가 카메라를 따라 회전하는건 아니듯이.
    • 또는 플레이어의 에임 상태가 조준상태 일 때
      • 에임 상태가 조준 상태가 되면 카메라가 바라보는 방향으로 플레이어 캐릭터를 회전 시켜놔야 하기 때문에 에임 상태 정보를 추후 작성할 📜PlayerShooter.cs로부터 받아야 한다.
      • 이에 대한 이유를 📜PlayerShooter.cs 포스트에 작성함.
  • 이동
    • 유저의 입력값에 따른 이동
    • 매번 물리 갱신 주기마다 실행한다.
  • 점프
    • 점프 입력이 감지되면 점프 실행


public void Move(Vector2 moveInput)
    public void Move(Vector2 moveInput)
    {
        var targetSpeed = speed * moveInput.magnitude;
        var moveDirection = Vector3.Normalize(transform.forward * moveInput.y + transform.right * moveInput.x);

        var smoothTime = characterController.isGrounded ? speedSmoothTime : speedSmoothTime / airControlPercent;

        targetSpeed = Mathf.SmoothDamp(currentSpeed, targetSpeed, ref speedSmoothVelocity, smoothTime);

        currentVelocityY += Time.deltaTime * Physics.gravity.y;

        var velocity = moveDirection * targetSpeed + Vector3.up * currentVelocityY;

        characterController.Move(velocity * Time.deltaTime);

        if (characterController.isGrounded) currentVelocityY = 0;
    }

유저의 입력 값 moveInput에 따라 실제로 플레이어가 움직이도록.

  • targetSpeed
    • 유저가 가고 싶은, 원하는 속도 벡터 (입력에 따른)
      • 세게 누르면 더 빨리 가고싶은것
    • moveInput.magnitude 를 곱해준 이유
      • 게임 패드 스틱을 살짝만 민 경우에는 moveInput 벡터의 길이가 1보다 작게 들어올 수 있다. 그런 경우엔 최대 속도보다 작은 값을 쓰기 위하여.
  • moveDirection
    • 이동하려는 방향 벡터
      • transform.forward * moveInput.y + transform.right * moveInput.x
      • 앞 쪽 방향 입력 moveInput.y이 클수록 transform.forward 즉, 앞쪽으로 많이 이동할 것이고
        • transform.forward 플레이어 캐릭터의 앞쪽 방향
      • 옆 쪽 방향 입력 moveInput.x이 클수록 transform.right 즉, 오른쪽으로 많이 이동할 것이다.
        • transform.right 플레이어 캐릭터의 오른쪽 방향
      • moveInput.y, moveInput.x 둘다 최대 세기로 누르면 50 : 50 비율의 방향이 될 것.
    • 길이 1이 되게끔 노말라이즈 해준다.
  • 스무스 하게 댐핑하기 👉 현재 속도에서 targetSpeed 까지 부드럽게 이어주기
    • 캐릭터가 만약 땅에 닿아있다면 👉 기존에 0.1로 초기화 됐던 스무스하게 이동하는 지연 시간인 speedSmoothTime을 그대로 사용함. 즉 아주 느리게!
    • 캐릭터가 만약 땅에 닿아있지 않다면, 즉 점프 혹은 낙사 중이라면 👉 airControlPercent로 나눈 값으로 업데이트 해준다. airControlPercent은 1보다 작은 값이므로 나누니까 1보다 커지게 된다!!
      • 즉, 공중에서는 지연시간이 매우 길도록 한다. 공중에서는 이동 조작이 빠릿빠릿하게 안되도록 하는 것.
    • targetSpeed = Mathf.SmoothDamp(currentSpeed, targetSpeed, ref speedSmoothVelocity, smoothTime);
      • currentSpeed 👉 targetSpeed 스무스하게 지연 시간.
  • currentVelocityY
    • 플레이어가 중력에 의해서 바닥에 떨어지는 y 방향 속도.
      • Rigidbody가 붙어있는게 아니기 때문에 직접 중력 가속도를 설정해주어야 한다.
    • y 방향의 중력 가속도(- 9.8)에 시간인 Time.deltaTime 을 곱해준다.
      • 속도 = 가속도 X 시간

    • if (characterController.isGrounded) currentVelocityY = 0;
      • 플레이어가 바닥에 닿아있다면 Y 방향의 속도를 0 으로.
      • isGrounded는 CharacterController 컴포넌트에 내장된 변수. 바닥에 닿아있으면 True 리턴해 준다.
  • velocity
    • 최종적으로 적용할 속도 벡터
    • 이동하려는 방향 벡터 * 원하는 속도(스칼라) + 위쪽 방향 벡터 * 중력에 의한 Y 방향 속도(스칼라)
      • 이동하려는 방향 벡터 * 원하는 속도(스칼라)
        • 유니티 게임 월드 상에서의 Y 방향 속도가 배제되어 있다.
        • moveInput.x 좌우, moveInput.y 앞뒤 즉, 유니티 게임 월드 상에서는 X, Z방향으로 향하는 속도만 반영 되어 있다.
      • 위쪽 방향 벡터 * 중력에 의한 Y 방향 속도(스칼라)
        • 유니티 게임 월드 상에서의 Y 방향 속도
        • 중력 가속도 반영
    • characterController.Move(velocity * Time.deltaTime);
      • 이동 거리 = 속도 X 시간
      • 최종적으로 플레이어 캐릭터를 유니티 게임 상의 월드내에서 평행 이동시켜주기.
        • Transform 의 Move() 함수
          • 얼만큼 이동시킬지에 대한 거리가 인수로 들어감


public void Rotate()
    public void Rotate()
    {
        var targetRotation = followCam.transform.eulerAngles.y;

        transform.eulerAngles = Vector3.up * Mathf.SmoothDampAngle(transform.eulerAngles.y, targetRotation,
                                    ref turnSmoothVelocity, turnSmoothTime);
    }

현재 플레이어 캐릭터가 바라보고 있는 방향을 카메라가 바라보고 있는 방향으로 회전시키기.

  • var targetRotation = followCam.transform.eulerAngles.y;
    • 카메라의 현재 Y 방향의 회전 값과 일치하게 회전할 것이므로 이를 타겟 회전 값으로 설정.
      • transform.rotateion.eulerAngles.y 도 가능하고 transform에서 eulerAngles를 바로 접근하여 transform.eulerAngles.y 로 호출할 수도 있다.
    • Y 방향의 회전 값을 가져와야 한다.
      • X, Z 방향의 회전은 필요가 없다.
        • X 방향의 회전은 양 옆으로 무지개 포물선을 그리는듯 하는 회전이고
      • Z 방향의 회전은 앞 뒤로 숙였다 젖혔다 하는 회전이라서.
  • transform.eulerAngles
    • 플레이어 캐릭터(나 자신)의 회전값을 var targetRotation로 스무스하게 지연시간을 거쳐 변경시킨다.
      • 바로 transform.eulerAngles = Vector3.up * targetRotation 해줘도 되지만 이러면 너무 딱딱하게 보이니까.
    • Mathf.SmoothDampAngle 함수
      • 👉 SmoothDamp 함수와 기능은 동일하나 각도를 고려해서 스무스하게 변경시킨다.
        • 360도 체계에서는 예를들어 -270도와 90도는 같은 회전값임을 의미하니까 사실 -270도면 90도만 돌면 되는건데 일반 SmoothDamp 함수를 사용하면 270도씩 돌아버리니까 SmoothDampAngle을 사용하면 의도와 달리 더 많이 회전하는 경우를 막아 줌 !
    • 방향 👉 Vector3.up
    • 크기 👉 Mathf.SmoothDampAngle(transform.eulerAngles.y, targetRotation, ref turnSmoothVelocity, turnSmoothTime);


public void Jump()
    public void Jump()
    {
        if (!characterController.isGrounded) return;
        currentVelocityY = jumpVelocity;
    }

플레이어를 점프 시킴

  • 바닥에 닿아있지 않다면 점프 시도를 못하게 해야 함 !
    • if (!characterController.isGrounded) return;
  • 바닥에 닿아있다면 현재 속도를 점프 속도로 설정 함.


private void Update()

    private void Update()
    {
        UpdateAnimation(playerInput.moveInput);
    }

매 프레임마다 애니메이션 갱신

  • 유저의 moveInput 입력에 따라 애니메이션을 갱신시킬 것.
⬜FixedUpdate() 와의 차이점
  • Update 👉 매 프레임마다 강제 실행되기 때문에 여기서 물리 처리를 하면 오차가 발생할 수 있다.
    • 프레임 속도는 환경마다 다르기 때문에 프레임에 맞춰 물리처리를 하면 불규칙하게 실행될 수 있음
  • FixedUpdate 👉 물리 갱신 주기에 맞춰 실행되기 때문에 더 정확하게 물치 처리가 가능
    • 프레임에 기반하지 않고 고정적인, 동일한 시간에 맞춰 반복 실행되는 이벤트 함수.
    • 따라서 물리처리를 하기에 좋다. 어떤 환경이든 간에 오차 없이 동일하게 나타낼 수 있으므로.


private void UpdateAnimation(Vector2 moveInput)
    private void UpdateAnimation(Vector2 moveInput)
    {
        var animationSpeedPercent = currentSpeed / speed;

        animator.SetFloat("Horizontal Move", moveInput.x * animationSpeedPercent, 0.05f, Time.deltaTime);
        animator.SetFloat("Vertical Move", moveInput.y * animationSpeedPercent, 0.05f, Time.deltaTime);
    }

사용자 입력에 맞춰서 플레이어 애니메이션을 갱신

  • SetFloat 애니메이션 파라미터 값을 설정해준다.
    • 0.05f, Time.deltaTime
      • 이전에 설정한 파라미터 값에서 방금 설정한 파라미터 값으로 0.05초의 지연시간을 거쳐 이 Time.deltaTime 시간 간격동안 부드럽게 변경해주도록 한다.
  • animationSpeedPercent = currentSpeed / speed 을 곱해주어 적절히 블렌딩 해준다.
    • animationSpeedPercent 👉 현재 속도가 최고 속도 대비 몇 퍼센트인지.
    • 현재 벽에 부딪치고 있어서 최고 속도가 0 이라면 사실 moveInput 값이 1 로 강하게 들어 오더라도 가만히 있는 애니메이션을 재생해야 한다.
    • moveInput이 1로 들어왔지만 최고 속도 대비 50퍼센트 정도의 속도를 가지고 있다면 이와 적절히 블렌딩 된 애니메이션을 재생시켜야 한다.
    • 따라서 이를 곱해주어 현재 속도 대비 적절히 블렌딩 해주어야 한다.


🔔 유니티 설정

  • Player CharacterTagLayer 를 각각 모두 Player로 변경해준다.
    • 자식 오브젝트에겐 적용하지 않는다. 다이얼로그가 뜨면 No 누르기.


🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기


Unity Lesson 1 카테고리 내 다른 글 보러가기

댓글 남기기