[Unity3D 액션 게임 제작기] 1. 캐릭터 이동
이번 목표
요약하자면 이것이 되게 하자.
이번에 사용한 에셋
3D -> Animation -> 무료로 가면 뜨는 목록 중 네모 박스 친 2개를 사용.
이동 방식은 정면 바라보기를 유지하며 움직이기(걷는 상태), 이동하는 방향으로 몸 돌리기(달리기 상태) 둘 다 적용
Rigidbody가 아닌 CharacterController 사용.
이번에 다루는 주된 기능
- 이동, 달리기
- 카메라 조작, 카메라 벽 관통 보정, 카메라 방향을 정면으로 삼기
원래 점프도 넣었는데 점프를 넣으니 고려해야 할 요소가 배는 늘어나서 일단 뺌.
추후 기본적인 기능 다 넣고나서 마지막 즈음에 추가할 수도 있음.
Heirarchy 창에 보여야 할 필수 요소. (빛, 환경은 자유 구성)
Main Camera는 기본 카메라 그대로고 Create Empty로 빈 오브젝트를 생성하고 그 밑에 둔다.
Player는 현재 심플하게 이렇게 구성하면 끝. Player 스크립트는 아래 후술한다.
Model은 위에서 받은 Meele Warrior 모델을 넣었다. 컴포넌트는 Animator밖에 없다.
Animator에 적용할 Animator Controller 하나 만들어주자.
현재 필요한 파라미터와 모션들. 심플하다.
Jump는 isGrounded가 false가 되면 진입하고 true가 되면 나온다.
- 점프가 없는데 점프라 지은건 애니메이션을 점프 모션에서 따왔기 때문. 점프는 안해도 고지대에서 떨어질 때 쓴다.
Run은 isRun이 ture가 되면 진입하고 false가 되면 나온다.
Idle 없는데 정상이냐고 묻는다면 비밀은 Walk에 있다.
Walk는 블렌드 트리로 되어있어 이 안에 Idle을 포함한 8방향에 대응하는 걷기 모션이 들어있다.
사실 이렇게 할 생각 없었는데 무료로 받은 Meele Warrior의 모션이 이렇게 제공 되길래 해봤다.
Pos X, Pos Y의 값들도 다 입력했다면 애니메이터 설정도 끝이다.
달리기 모션은 정면 하나밖에 없어서 평소에는 위 블렌드 트리로 정면을 유지한 채 다니고 달리는 중에는 이동하는 방향을 쳐다보게 했다.
맵은 적당히 마음 가는대로 꾸미자.
점프가 없으니 떨어지는 모션을 보려면 경사로를 잘 내려오는지도 확인할 겸사겸사 경사로를 배치하는게 좋다.
오를수 있는 각도는 CharacterController의 Slope Limit 수치를 조절하면 된다.
그리고 대망의 Player 스크립트
대부분 요소에 주석을 달아두었고 딱히 어려운 함수를 사용한 것도 없으니 이해에 문제는 없을것이라 생각한다.
using UnityEngine;
namespace Kupa
{
public class Player : MonoBehaviour
{
[SerializeField] [Tooltip("걷는 속도")] private float walkSpeed = 5.0f;
[SerializeField] [Tooltip("달리는 속도")] private float runSpeed = 10.0f;
[SerializeField] [Tooltip("카메라 거리")] [MinMax(0.1f, 10f, ShowEditRange = true)] private MinMaxCurrentValue cameraDistance = new MinMaxCurrentValue(1f, 5f);
private Transform modelTransform;
private Transform cameraPivotTransform;
private Transform cameraTransform;
private CharacterController characterController;
private Animator animator;
private Vector3 mouseMove; //카메라 회전값
private Vector3 moveVelocity; //이동 속도
private bool isRun; //달리기 상태
private bool IsRun { get { return isRun; } set { isRun = value; animator.SetBool("isRun", value); } } //값을 변경하면 애니메이터 값도 자동으로 변경되도록
private void Awake()
{
characterController = GetComponent<CharacterController>();
modelTransform = transform.GetChild(0);
animator = modelTransform.GetComponent<Animator>();
cameraTransform = Camera.main.transform;
cameraPivotTransform = cameraTransform.parent;
}
private void Update() //캐릭터 조정 및 컨트롤 반영은 여기서 진행
{
if (Time.timeScale < 0.001f) return; //일시정지 등 시간을 멈춘 상태에선 입력 방지
FreezeRotationXZ(); //CharacterController 캡슐이 어떤 이유로든 기울어지지 않도록 방지
CameraDistanceCtrl(); //카메라 거리 조작
RunCheck(); //달리기 상태 체크
if (characterController.isGrounded) //지면에 발이 닿아있는 경우
{
animator.SetBool("isGrounded", true);
CalcInputMove(); //이동 입력 계산. 땅에서만 컨트롤 가능
if (GroundCheck()) //밑으로 Raycast를 쏘아 땅을 한 번 더 확인.
moveVelocity.y = IsRun ? -runSpeed : -walkSpeed; //isGounded는 다음 프레임때 velocity.y 만큼 내려가도 바닥에 닿지 않으면 false를 리턴한다. 평지에선 속도와 비례해서 y 힘을 주어야 경사로에서도 isGrounded 값이 true가 된다.
else
moveVelocity.y = -1; //Raycast는 캡슐의 중앙에서 쏘기에 모서리에 걸치면 Raycast는 false이나 isGrounded는 true인 경우가 발생한다. 보통 높은 곳에서 떨어질때 발생하므로 y값을 최소화 하여 자연스럽게 떨어지도록 한다.
}
else
{
animator.SetBool("isGrounded", false);
moveVelocity += Physics.gravity * Time.deltaTime; //중력 가산
}
characterController.Move(moveVelocity * Time.deltaTime); //최종적으로 CharacterController Move 호출
}
private void LateUpdate() //최종 카메라 보정은 여기서 진행
{
if (Time.timeScale < 0.001f) return; //일시정지 등 시간을 멈춘 상태에선 입력 방지
float cameraHeight = 1.3f;
cameraPivotTransform.position = transform.position + Vector3.up * cameraHeight; //캐릭터의 가슴 높이쯤
mouseMove += new Vector3(-Input.GetAxisRaw("Mouse Y") * PreferenceData.MouseSensitivity, Input.GetAxisRaw("Mouse X") * PreferenceData.MouseSensitivity, 0); //마우스의 움직임을 가감
if (mouseMove.x < -60) //상하 각도는 제한을 둔다.
mouseMove.x = -60;
else if (60 < mouseMove.x)
mouseMove.x = 60;
cameraPivotTransform.localEulerAngles = mouseMove;
RaycastHit cameraWallHit; //카메라가 벽 뒤로 가서 화면이 가려지는 것을 방지
if (Physics.Raycast(cameraPivotTransform.position, cameraTransform.position - cameraPivotTransform.position, out cameraWallHit, cameraDistance.Current, ~(1 << LayerMask.NameToLayer("Player"))))
cameraTransform.localPosition = Vector3.back * cameraWallHit.distance;
else
cameraTransform.localPosition = Vector3.back * cameraDistance.Current;
}
private void FreezeRotationXZ()
{
transform.eulerAngles = new Vector3(0, transform.eulerAngles.y, 0); //기울어짐 방지
}
private void CameraDistanceCtrl()
{
cameraDistance.Current -= Input.GetAxisRaw("Mouse ScrollWheel"); //휠로 카메라의 거리를 조절
}
private void RunCheck()
{
if (IsRun == false && Input.GetKeyDown(KeyCode.LeftShift)) //왼쪽 쉬프트를 누르면 달리기 상태
IsRun = true;
if (IsRun && Input.GetAxisRaw("Horizontal") == 0 && Input.GetAxisRaw("Vertical") == 0) //이동 입력이 없으면 달리기 취소
IsRun = false;
}
private bool JumpCheck()
{
return Input.GetButtonDown("Jump");
}
private bool GroundCheck()
{
//CharacterController의 isGrounded는 이전 프레임 때 Move 등의 함수로 땅 쪽으로 향하였을때 접지가 되어야만 true를 리턴한다.
//이는 경사로를 내려가거나 울퉁불퉁한 지면을 다닐때 수시로 false 값을 리턴하므로 Raycast로 땅을 한 번 더 체크하여 안정성을 강화한다..
return Physics.Raycast(transform.position, Vector3.down, 0.2f);
}
private void CalcInputMove()
{
//가속 과정이 조작감을 떨어뜨린다고 생각하여 GetAxisRaw를 사용하여 가속 과정 생략. normalized를 사용하여 대각선 이동 시 벡터의 길이가 약 1.41배 되는 부분 보정
moveVelocity = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical")).normalized * (IsRun ? runSpeed : walkSpeed);
animator.SetFloat("speedX", Input.GetAxis("Horizontal")); //모션은 GetAxis을 써야 자연스러우므로 GetAxis 값 사용
animator.SetFloat("speedY", Input.GetAxis("Vertical"));
moveVelocity = transform.TransformDirection(moveVelocity); //입력 키를 카메라가 보고 있는 방향으로 조정
//조작 중에만 카메라의 방향에 상대적으로 캐릭터가 움직이도록 한다.
if (0.01f < moveVelocity.sqrMagnitude)
{
Quaternion cameraRotation = cameraPivotTransform.rotation;
cameraRotation.x = cameraRotation.z = 0; //y축만 필요하므로 나머지 값은 0으로 바꾼다.
transform.rotation = cameraRotation;
if (IsRun)
{
//달리기 상태에선 이동 방향으로 몸을 돌린다.
Quaternion characterRotation = Quaternion.LookRotation(moveVelocity);
characterRotation.x = characterRotation.z = 0;
//모델 회전은 자연스러움을 위해 Slerp를 사용
modelTransform.rotation = Quaternion.Slerp(modelTransform.rotation, characterRotation, 10.0f * Time.deltaTime);
}
else
{
//통상 상태에선 정면을 유지한채 움직인다.
modelTransform.rotation = Quaternion.Slerp(modelTransform.rotation, cameraRotation, 10.0f * Time.deltaTime);
}
}
}
}
}
만약 위 코드만 복사한다면 MinMaxCurrentValue, PreferenceData 클래스가 없다고 오류가 날텐데 이 둘은 깃허브 코드를 참고하자.
MinMaxCurrentValue 는 에디터 화면에서 좀 더 직관적으로 보기위해 추가한 Attribute이고
PreferenceData는 PC 내에 저장될 데이터를 관리할 클래스이다. 추후 옵션 UI를 제작하며 관련 설정들을 게임을 껏다켜도 남아있도록 한다.
게임을 바로 플레이해보거나 깃허브 소스를 보고싶으면 아래 페이지를 참고하자.
https://jab724.github.io/Kupa3DRPG/
'Unity 3D 액션 게임 제작' 카테고리의 다른 글
개요 (0) | 2021.06.07 |
---|