Unity Chapter 11-6. 좀비 TPS 게임 만들기 : Player Movement
카테고리: Unity Lesson 1
태그: Unity Game Engine
인프런에 있는 이제민님의 레트로의 유니티 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 처리
- 이 컴포넌트를 통해
- CharacterController 컴포넌트
playerInput
- 📜PlayerInput.cs 스크립트
- 얘로부터 입력 감지를 받아야 함
- 📜PlayerInput.cs 스크립트
playerShooter
- 📜PlayerShooter.cs 스크립트
- 추후 작성함
- 📜PlayerShooter.cs 스크립트
Animator
- Animator 컴포넌트
- 이 컴포넌트를 통해
Player Character
오브젝트의 애니메이션을 제어
- 이 컴포넌트를 통해
- Animator 컴포넌트
followCam
- Camera 컴포넌트
- Main Camera (Brain Camera)
- 플레이어가 움직일때 카메라의 방향을 기준으로 움직이게 되므로 현재 카메라의 방향을 알기 위해서 필요.
- 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 방향 속도를 조정해야 한다.
- 플레이어의 Y 방향 속도
currentSpeed
- Y를 제외한 X, Z 방향에서의 속도. 즉, 플레이어의 지면 상에서의 현재 속도.
- get 프로퍼티 인데 람다식으로
=>
을 사용하여 간단하게 표현했다.currentSpeed
을 리턴하려고 할 땐 (X속도, Z속도) 벡터2의 길이가 리턴되게 된다.- 프로퍼티 람다 함수 참고
- get 프로퍼티 인데 람다식으로
- Y를 제외한 X, Z 방향에서의 속도. 즉, 플레이어의 지면 상에서의 현재 속도.
함수
private void Start()
private void Start()
{
animator = GetComponent<Animator>();
playerInput = GetComponent<PlayerInput>();
playerShooter = GetComponent<PlayerShooter>();
playerShooter =
followCam = Camera.main;
characterController = GetComponent<CharacterController>();
}
📜PlayerMovement
가 사용할 컴포넌트들의 레퍼런스들을 게임 시작시 초기화 해주기
Animator
컴포넌트- 애니메이션
📜PlayerInput
컴포넌트- 유저의 입력 : 플레이어 캐릭터를 이동, 점프, 회전시킬거라 필요
📜PlayerShooter
컴포넌트- 에임 상태가 조준 상태가 되면 카메라가 바라보는 방향으로 플레이어 캐릭터를 회전 시켜놔야 하기 때문에 에임 상태 정보를 추후 작성할 📜PlayerShooter.cs로부터 받아야 한다.
Main Camera
오브젝트- 플레이어가 움직일때 카메라의 방향을 기준으로 회전하고 움직이게 되므로 필요
- 현재
Main
태그가 붙어있는 카메라
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 포스트에 작성함.
- 현재 속도가 0.2 보다 크거나 발사 버튼 입력이 들어왔을 때
- 이동
- 유저의 입력값에 따른 이동
- 매번 물리 갱신 주기마다 실행한다.
- 점프
- 점프 입력이 감지되면 점프 실행
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
스무스하게 지연 시간.
- 캐릭터가 만약 땅에 닿아있다면 👉 기존에 0.1로 초기화 됐던 스무스하게 이동하는 지연 시간인
currentVelocityY
- 플레이어가 중력에 의해서 바닥에 떨어지는 y 방향 속도.
- Rigidbody가 붙어있는게 아니기 때문에 직접 중력 가속도를 설정해주어야 한다.
- y 방향의 중력 가속도(- 9.8)에 시간인 Time.deltaTime 을 곱해준다.
-
속도 = 가속도 X 시간
-
- if (characterController.isGrounded) currentVelocityY = 0;
- 플레이어가 바닥에 닿아있다면 Y 방향의 속도를 0 으로.
isGrounded
는 CharacterController 컴포넌트에 내장된 변수. 바닥에 닿아있으면 True 리턴해 준다.
- 플레이어가 중력에 의해서 바닥에 떨어지는 y 방향 속도.
velocity
- 최종적으로 적용할 속도 벡터
이동하려는 방향 벡터 * 원하는 속도(스칼라)
+위쪽 방향 벡터 * 중력에 의한 Y 방향 속도(스칼라)
- ⭐
이동하려는 방향 벡터 * 원하는 속도(스칼라)
⭐- 유니티 게임 월드 상에서의 Y 방향 속도가 배제되어 있다.
moveInput.x
좌우,moveInput.y
앞뒤 즉, 유니티 게임 월드 상에서는 X, Z방향으로 향하는 속도만 반영 되어 있다.
- ⭐
위쪽 방향 벡터 * 중력에 의한 Y 방향 속도(스칼라)
⭐- 유니티 게임 월드 상에서의 Y 방향 속도
- 중력 가속도 반영
- ⭐
- characterController.Move(
velocity
* Time.deltaTime);- 이동 거리 = 속도 X 시간
- 최종적으로 플레이어 캐릭터를 유니티 게임 상의 월드내에서 평행 이동시켜주기.
- Transform 의 Move() 함수
- 얼만큼 이동시킬지에 대한 거리가 인수로 들어감
- 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 방향의 회전은 앞 뒤로 숙였다 젖혔다 하는 회전이라서.
- X, Z 방향의 회전은 필요가 없다.
- 카메라의 현재 Y 방향의 회전 값과 일치하게 회전할 것이므로 이를 타겟 회전 값으로 설정.
transform.eulerAngles
- 플레이어 캐릭터(나 자신)의 회전값을
var targetRotation
로 스무스하게 지연시간을 거쳐 변경시킨다.- 바로
transform.eulerAngles = Vector3.up * targetRotation
해줘도 되지만 이러면 너무 딱딱하게 보이니까.
- 바로
- Mathf.SmoothDampAngle 함수
- 👉 SmoothDamp 함수와 기능은 동일하나 각도를 고려해서 스무스하게 변경시킨다.
- 360도 체계에서는 예를들어 -270도와 90도는 같은 회전값임을 의미하니까 사실 -270도면 90도만 돌면 되는건데 일반 SmoothDamp 함수를 사용하면 270도씩 돌아버리니까 SmoothDampAngle을 사용하면 의도와 달리 더 많이 회전하는 경우를 막아 줌 !
- 👉 SmoothDamp 함수와 기능은 동일하나 각도를 고려해서 스무스하게 변경시킨다.
- 방향 👉 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
시간 간격동안 부드럽게 변경해주도록 한다.
- 이전에 설정한 파라미터 값에서 방금 설정한 파라미터 값으로 0.05초의 지연시간을 거쳐 이
animationSpeedPercent = currentSpeed / speed
을 곱해주어 적절히 블렌딩 해준다.animationSpeedPercent
👉 현재 속도가 최고 속도 대비 몇 퍼센트인지.- 현재 벽에 부딪치고 있어서 최고 속도가 0 이라면 사실 moveInput 값이 1 로 강하게 들어 오더라도 가만히 있는 애니메이션을 재생해야 한다.
- moveInput이 1로 들어왔지만 최고 속도 대비 50퍼센트 정도의 속도를 가지고 있다면 이와 적절히 블렌딩 된 애니메이션을 재생시켜야 한다.
- 따라서 이를 곱해주어 현재 속도 대비 적절히 블렌딩 해주어야 한다.
🔔 유니티 설정
Player Character
의 Tag 와 Layer 를 각각 모두 Player로 변경해준다.- 자식 오브젝트에겐 적용하지 않는다. 다이얼로그가 뜨면 No 누르기.
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기