3D 게임 플레이어 기준 시점이라면 보통은 FPS, TPS 2가지 시점을 사용합니다.
강의로는 FPS 플레이어 구현을 가르쳐 주어 이를 학습하였기에
전에 만들어 보았던 TPS 플레이어 조작에 유니티 인풋 시스템까지 적용하여
캐릭터 이동, 점프, 카메라 워크까지 만들어 보았습니다.
모션이 있는 캐릭터 중에 무료 에셋인 유니티쨩을 가져왔습니다.
유니티 짱 01) 에셋 불러오기 및 툰 셰이더 적용 (3D ver1.4.0)
유니티 짱(Unity Chan)이란? > 유니티 재팬에서 만든 오픈소스 고품질 카툰풍 캐릭터 라이센스 정보 및 다운로드 링크 > 라이센스는 꼼꼼하게 읽어보시고 사용하려는 용도에 맞는지 확실하게 체크
ybbro.tistory.com
사용법은 예전 게시글을 참고하면 됩니다.
유니티의 인풋 시스템에 키를 맵핑한 내용입니다.
플레이어, 카메라를 따로 두었습니다.
Move : WASD 이동
Jump : Space 입력
OnLook : 마우스 이동값
OnInit : Q 입력 >> 캐릭터가 바라보는 방향으로 카메라 초기화
상황에 맞게 애니메이션을 재생하도록 애니메이터 구성
1. TPS 플레이어 컨트롤 코드
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerControl : MonoBehaviour
{
[SerializeField] float moveSpeed;
[SerializeField] float jumpForce;
[SerializeField] private LayerMask notPlayer;
Transform camera;
Rigidbody _rigidbody;
AnimationControl _animationControl;
private bool isOnGround;
private bool isJumpEntered;
Vector3 moveVector;
private Vector2 inputDir;
private float h, v;
// 현재 회전의 기준이 되는 값 (CameraControl 에서 변경)
public float currentDirection = 0f;
private void Awake()
{
TryGetComponent(out _rigidbody);
TryGetComponent(out _animationControl);
camera = Camera.main.transform;
}
private void Start()
{
Cursor.lockState = CursorLockMode.Locked;
}
private void Update()
{
// 체공 여부 체크
GroundCheck();
if (isOnGround)
{
// 땅에서만 점프 가능
Jump();
}
else
{
// 점프 키 입력 여부 초기화
isJumpEntered = false;
}
// 키 입력 방향으로 캐릭터 회전
PlayerRotate();
// 캐릭터 이동
Move();
}
// 이동 키를 눌렀을 때 호출
// 문제 : 점프 후 착지했을 때 일시적으로 속도를 0으로 만드는데 그때 이동키 입력 중이면 그 입력이 이어지지 않음
public void OnMove(InputAction.CallbackContext context)
{
// 문제 : 점프 중에 이동 키를 꾹 누르고 있어도 점프가 끝나면 이동이 동작하지 않음..
if (context.performed)
{
// 입력한 좌표로부터 이동값 연산
inputDir = context.ReadValue<Vector2>();
// 걷기 애니메이션 재생
_animationControl.Animator.SetBool(_animationControl.state[(int)AnimState.Running], true);
}
// 이동 버튼에서 손을 떼었다면, 이동 애니메이션 정지
else if (context.canceled)
{
inputDir = Vector2.zero;
_animationControl.Animator.SetBool(_animationControl.state[(int)AnimState.Running], false);
}
}
void Move()
{
// 카메라가 좌표를 기준으로 움직여야 자연스러움
Vector3 dir = camera.forward * inputDir.y + camera.right * inputDir.x;
dir *= moveSpeed;
dir.y = _rigidbody.velocity.y;
_rigidbody.velocity = dir;
}
// 점프 키를 눌렀을 때 호출
public void OnJump(InputAction.CallbackContext context)
{
// 땅 위에서만 점프
if (isOnGround && context.started)
{
isJumpEntered = true;
}
}
void Jump()
{
if (isJumpEntered)
{
// 윗 방향으로 힘을 가하여 점프
_rigidbody.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
// 점프 애니메이션 재생
_animationControl.Animator.SetBool(_animationControl.state[(int)AnimState.Jump], true);
isJumpEntered = false;
}
}
// 이동 키 입력에 따른 캐릭터 회전
void PlayerRotate()
{
float h = inputDir.x; // 좌우 방향 입력
float v = inputDir.y; // 앞뒤 방향 입력
if (v > 0.1f) // W
{
if (h > 0.1f) // +D >> 오른쪽 앞 방향
transform.eulerAngles = Vector3.up *
(currentDirection + 45f);
else if (h < -0.1f) // +A >> 왼쪽 앞 방향
transform.eulerAngles = Vector3.up *
(currentDirection -45f);
else // W만 입력 >> 앞 방향
transform.eulerAngles = Vector3.up * currentDirection;
}
else if (v < -0.1f) // S
{
if (h > 0.1f) // +D >> 오른쪽 뒷 방향
transform.eulerAngles = Vector3.up *
(currentDirection + 135f);
else if (h < -0.1f) // +A >> 왼쪽 뒷 방향
transform.eulerAngles = Vector3.up *
(currentDirection -135f);
else // S만 >> 뒷 방향
transform.eulerAngles = Vector3.up * (currentDirection + 180f);
}
else if (h > 0.1f) // D만 입력 >> 오른쪽
transform.eulerAngles = Vector3.up * (currentDirection + 90f);
else if (h < -0.1f) // A만 입력 >> 왼쪽
transform.eulerAngles = Vector3.up * (currentDirection - 90f);
}
void GroundCheck()
{
// 직전의 착지 체크 상태
bool isOnGround_before = isOnGround;
// 캐릭터 발보다 살짝 위의 지점
Vector3 RayStartPos = transform.position + Vector3.up * 0.01f;
// 캐릭터 발 아래 방향으로 빔을 쏴서 맞은 콜라이더가 있는지 여부에 따라 체공 여부 써주기
isOnGround = Physics.Raycast(RayStartPos, Vector3.down, 0.15f, notPlayer);
// 공중에 떠 있다 지상에 안착했을 때 1번만
if (!isOnGround_before && isOnGround)
{
// 점프애니메이션 끄고
_animationControl.Animator.SetBool(_animationControl.state[(int)AnimState.Jump], false);
// 착지 애니메이션 재생
_animationControl.Animator.SetBool(_animationControl.state[(int)AnimState.Land], true);
}
else
{
// 착지 애니메이션 끄기(트리거로 하려고 했으나 게임 시작부터 활성화되는 문제가 발생.. 그냥 bool로 관리)
_animationControl.Animator.SetBool(_animationControl.state[(int)AnimState.Land], false);
}
}
}
2. TPS 카메라 컨트롤 코드
(카메라는 플레이어 오브젝트 회전에는 영향을 받지 않아야 하기에 자식으로 넣으면 안됩니다)
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Camera))]
public class CameraControl : MonoBehaviour
{
[SerializeField] private PlayerControl player;
[SerializeField] private Transform camPos;
// 카메라 초기화 시 목표 위치, 회전 각도로 할 유니티짱 자식 오브젝트
// 유니티짱을 기준으로 항상 동일한 로컬 위치 (등 뒤 약간 위에서 유니티짱 정면을 약간 내려다보도록)
Transform initPos;
// 카메라와 플레이어의 이격 좌표
Vector3 positionDistance;
// 카메라 현재 회전 값
Vector3 rotation_temp;
// 시점 원점 이동
// 원점 이동 시 카메라 이동 속도
public float originSpeed = 20f;
// 원점 이동 기능 off 카운터
float count;
// 원점 이동 기능 지속 시간
const float bQuickSwitch_toggle_delay = 1f;
// 마우스 조작으로 인한 시점 이동
// 카메라 원점 이동 기능 on/off
bool isCameraInit;
// 카메라 회전 값
float xRotate, yRotate;
// 마우스 이동 초기화 값
float xRotateOrigin, yRotateOrigin;
// 마우스 좌우 이동에 따른 카메라 공전 지름
float diameter_LR;
// 마우스 이동에 따른 카메라 위아래, 좌우 회전 속도 !!!!! 시점 이동 민감도 설정에서 조절할 값
public float cameraSensitivity = 0.5f;
// 위, 아래 시점 이동 상하한
public float[] UpDownRotateClamp = new float[2] { -20f, 20f };
// 인풋 시스템에서 마우스 움직임을 받아올 변수
Vector2 _mouseDelta;
void Start()
{
// 카메라의 원점으로 할 유니티짱 자식 오브젝트
initPos = camPos;
// 원점 위치, 회전값과 동일하게
transform.position = initPos.position;
transform.forward = initPos.forward;
positionDistance = initPos.localPosition;
// 원점 위치를 토대로 회전 지름 값을 초기화
diameter_LR = Mathf.Abs(initPos.localPosition.z) * 2f;
// 원점으로 할 xRotate, yRotate 값 초기화
xRotateOrigin = initPos.eulerAngles.x;
yRotateOrigin = diameter_LR;
rotation_temp = transform.eulerAngles;
// 바로 초기화하면 되지 않았기에 약간의 시간 텀을 두고 진행
Invoke("InitCamera", 0.1f);
}
void InitCamera()
{
// 마우스 현재 위치를 원점으로
xRotate = xRotateOrigin;
yRotate = yRotateOrigin;
}
private void LateUpdate()
{
// 카메라 리셋 중이면
if (isCameraInit)
{
// 원점으로 카메라 이동, 회전
transform.position = Vector3.Slerp(transform.position, initPos.position, Time.deltaTime * originSpeed);
transform.forward = Vector3.Lerp(transform.forward, initPos.forward, Time.deltaTime * originSpeed);
// 기능 켠 후 잠시 후 끄기(그 사이 원점으로 이동)
count += Time.fixedDeltaTime;
if (count > bQuickSwitch_toggle_delay)
{
count = 0f;
isCameraInit = false;
// 원점 리셋이 끝날 때, 1번만 리셋할 파라미터
initMouseOrigin();
}
}
// 카메라 리셋 중이 아닐 때는,
else
CameraMove();
}
// 현재 마우스 위치를 0점으로 조정
void initMouseOrigin()
{
// 유니티짱의 기준 각도를 0점으로 잡은 각도로 리셋
player.currentDirection = player.transform.eulerAngles.y;
// 원점과 유니티짱의 떨어진 위치값을 리셋(회전값에 따라 달라진다)
positionDistance = initPos.position - player.transform.position;
// 원점의 회전 값으로 리셋
rotation_temp = initPos.eulerAngles;
// 리셋 후 positionDistance의 위치, initPos의 상하 회전값에 맞게 마우스 위치 리셋
if (positionDistance.z >= 0f)
yRotateOrigin = positionDistance.x;
else if (positionDistance.x >= 0f)
yRotateOrigin = -positionDistance.x + diameter_LR;
else
yRotateOrigin = -positionDistance.x - diameter_LR;
xRotate = xRotateOrigin;
yRotate = yRotateOrigin;
}
public void OnLook(InputAction.CallbackContext context)
{
_mouseDelta = context.ReadValue<Vector2>();
}
public void OnInit(InputAction.CallbackContext context)
{
// 카메라 리셋 키를 누르면 기능 on
isCameraInit = true;
}
// 플레이 도중 카메라 무브
void CameraMove()
{
// 유니티짱의 위치를 따라 이동하도록 위치 갱신
transform.position = player.transform.position + positionDistance;
// 마우스 좌우 이동에 따라 캐릭터 주변을 공전
yRotate += _mouseDelta.x * cameraSensitivity;
transform.RotateAround(player.transform.position + Vector3.up * initPos.position.y, Vector3.up,
yRotate);
// 좌우 회전에 맞춰 유니티짱을 바라보게
transform.LookAt(player.transform.position);
// 현재의 회전 값을 저장
rotation_temp = transform.eulerAngles;
// 마우스 앞뒤 이동에 따라 위아래 시점 이동값 산출
xRotate += -_mouseDelta.y * cameraSensitivity;
// 위, 아래 상한 각 고정
xRotate = Mathf.Clamp(xRotate, UpDownRotateClamp[0], UpDownRotateClamp[1]);
// 마우스 상하 이동에 대해 시점 회전
transform.eulerAngles = new Vector3(xRotate, rotation_temp.y, rotation_temp.z);
// 유니티짱의 이동좌표계 정면 각도를 카메라 y회전에 맞게 변화
player.currentDirection = rotation_temp.y;
}
}
'스파르타코딩클럽_Unity개발과정' 카테고리의 다른 글
유니티 숙련 개인 과제 - 3D 게임 캐릭터 이동과 물리 (3) (0) | 2025.05.23 |
---|---|
유니티 숙련 개인 과제 - 3D 게임 캐릭터 이동과 물리 (2) (0) | 2025.05.22 |
유니티 숙련 (3) - 서바이벌 강의 완강, 새로 배운 내용 정리 (0) | 2025.05.20 |
유니티 숙련 (2) - 코루틴 (0) | 2025.05.19 |
유니티 숙련 - (1) 스카이박스, 인풋 시스템 (0) | 2025.05.16 |