유니티 숙련 개인 과제 - 3D 게임 캐릭터 이동과 물리 (1)

2025. 5. 21. 23:59·스파르타코딩클럽_Unity개발과정

 

3D 게임 플레이어 기준 시점이라면 보통은 FPS, TPS 2가지 시점을 사용합니다.

 

강의로는 FPS 플레이어 구현을 가르쳐 주어 이를 학습하였기에

전에 만들어 보았던 TPS 플레이어 조작에 유니티 인풋 시스템까지 적용하여

캐릭터 이동, 점프, 카메라 워크까지 만들어 보았습니다.

 

모션이 있는 캐릭터 중에 무료 에셋인 유니티쨩을 가져왔습니다.

 

https://ybbro.tistory.com/18

 

유니티 짱 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
'스파르타코딩클럽_Unity개발과정' 카테고리의 다른 글
  • 유니티 숙련 개인 과제 - 3D 게임 캐릭터 이동과 물리 (3)
  • 유니티 숙련 개인 과제 - 3D 게임 캐릭터 이동과 물리 (2)
  • 유니티 숙련 (3) - 서바이벌 강의 완강, 새로 배운 내용 정리
  • 유니티 숙련 (2) - 코루틴
ybbro
ybbro
대부분의 포스팅은 pc에서 작성되었습니다. 모바일에서 볼 때 설명이 잘리면 데스크탑 모드를 사용해보길 바랍니다.
  • ybbro
    어떻게든 굴리는 게임 공방
    ybbro
  • 전체
    오늘
    어제
    • 전체 N
      • 스파르타코딩클럽_Unity개발과정 N
      • Unity 2D
        • 카드게임
        • 플랫포머 게임
        • 뱀서라이크
      • Unity 3D
        • 닷지
        • 유니티 짱
        • 디펜스 게임
      • Unity 에러 노트
      • 기능 구현 방법 정리
      • 셰이더 그래프
        • 2D
        • 3D
      • 프로그래머스
      • 자료구조
      • 기타
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    세이브
    갤럭시 S24
    무료스킨
    유니티
    앱이 휴대전화와 호환되지 않아 설치되지 않았습니다
    sprite mask
    직렬화
    UI
    unity
    스파인
    다크모드
    룰렛
    64비트
    텍스트매시프로
    잔상
    마스크
    유니티 애니메이터 파라미터 초기화
    대시
    hello
    삭제
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
ybbro
유니티 숙련 개인 과제 - 3D 게임 캐릭터 이동과 물리 (1)
상단으로

티스토리툴바