본문 바로가기
C#/UNITY_FPS 3D 서바이벌게임

Chapter 2-5. 무기 : 크로스헤어

by w1z 2024. 3. 11.

Chapter 2. 무기

크로스헤어

🚖 크로스헤어 UI 만들기

  • CrossHair 빈 게임 오브젝트. 크로스 헤어를 나타내는 아래 5 가지 UI 요소들을 부모로서 묶는다.
    • 크로스 헤어를 나타내는 5 가지의 이미지 UI 요소. 위치를 크로스헤어에 맞게 각각 설정해주었다.
      • Dot 크로스 헤어 가운데 점. 크기 3 X 3.
      • Line Right 크로스 헤어 오른쪽 선. 크기 20 X 3.
      • Line Left 크로스 헤어 왼쪽 선. 크기 20 X 3.
      • Line Up 크로스 헤어 위쪽 선. 크기 3 X 20.
      • Line Down 크로스 헤어 아래쪽 선. 크기 3 X 20.

 

🚖 크로스헤어 애니메이션

 
  • 동작에 따라 크로스헤어가 벌어지거나 투명해지는 등등 변화를 줄 것이라 애니메이션이 필요하다.
    • 발사할 때 크로스 헤어가 벌어짐.
    • 앉으면 크로스 헤어가 좁아짐. (정확도 Up)
    • 걸으면 크로스 헤어가 넓어짐. (정확도 Down)
    • 뛰면 크로스 헤어가 사라짐. (어차피 뛸 땐 발사 못 하니까)
    • 정조준 할 땐 크로스 헤어 사라짐.
  • 크로스 헤어 벌어지는 정도
    • 발사 중이 아닐 때 < 발사 중일 때
      • 발사 중일 때 👉 앉을 때 < 정지했을 때 < 걸을 때
      • 발사 중이 아닐 때 👉 앉을 때 < 정지했을 때 < 걸을 때
  • 뛸 때, 정조준 할 때는 크로스헤어를 사라지게 한다.

CrossHair에 Animator 컴포넌트 추가

 

Animation : 애니메이션 클립 직접 만들기

 

크로스 헤어와 관련된 애니메이션을 만드려는거니까 ✨CrossHair를 선택한 상태에서✨ Window - Animation - Animation 애니메이션 탭을 클릭하면 위와 같이 CrossHair의 애니메이션을 클립을 만들 수 있는 버튼이 뜬다!

 

녹화 하는 방법

  • Preview 옆에 있는 빨간색 녹화 버튼 🔴 을 누른 상태로 이 애니메이션이 적용되 있는 오브젝트의 여러 컴포넌트, 혹은 자식 오브젝트들의 설정 값에 변화를 주게 되면 그 변화 값이 애니메이션 클립으로서 녹화가 된다.
  • 녹화만 할 뿐 현재의 실제 오브젝트의 변경 사항으로 적용 되지는 않는다. (추후에 지금 생성한 이 애니메이션 클립이 재생될 때 적용 되겠지!)
  • 위 사진은 딱 0 프레임에만 Key 프레임이 있으므로 딱 1 프레임 짜리의 애니메이션 클립을 만드든 것과 같다. 딱 0 프레임 그 순간을 녹화는 것과 같다.

녹화 버튼을 누른 후 설정 값을 변경하면 빨간색으로 변한다. 예를 들어 위와 같이 Pos Y 값을 -20 으로 입력 한 상태라면, 작업 중인 이 애니메이션 클립을 재생하면 해당 프레임 때 Pos Y 값이 -20 으로 변화 할거라는 뜻이다.

 

녹화 버튼을 해제하면 애니메이션 클립으로서 동작할 때 변경될 값이 파란색으로 표시된다.

생성할 클립 추가하는 방법

Create New Clip 으로 또 다른 새로운 클립을 생성할 수 있다.

 

1️⃣ CrossHair_Idle 클립

정지 상태일 때 크로스 헤어.

  • 크로스 헤어를 이루는 5 가지의 이미지들의 원래 설정했던값 그대로를 녹화했다.
  • 1 프레임 길이의, 딱 0 프레임 때의 그 순간만 녹화함.

 

2️⃣ CrossHair_Idle_Fire 클립

정지 상태에서 발사했을 때 크로스 헤어.

  • Line Left, Line Right의 경우 Pos X 값이 정지 상태에 비해 -35, 35 로 늘었다.
  • Line Down, Line Up의 경우 Pos Y 값이 정지 상태에 비해 -35, 35 로 늘었다.
  • 1 프레임 길이의, 딱 0 프레임 때의 그 순간만 녹화함.

 

3️⃣ CrossHair_Crouch 클립

앉은 상태일 때 크로스 헤어.

  • Line Left, Line Right의 경우 Pos X 값이 정지 상태에 비해 -15, 15 로 줄었다.
  • Line Down, Line Up의 경우 Pos Y 값이 정지 상태에 비해 -15, 15 로 줄었다.
  • 1 프레임 길이의, 딱 0 프레임 때의 그 순간만 녹화함.

 

4️⃣ CrossHair_Crouch_Fire 클립

앉은 상태에서 발사했을 때 크로스 헤어.

  • Line Left, Line Right의 경우 Pos X 값이 발사 + 정지 상태에 비해 -30, 30 로 줄었다.
  • Line Down, Line Up의 경우 Pos Y 값이 발사 + 정지 상태에 비해 -30, 30 로 줄었다.
  • 1 프레임 길이의, 딱 0 프레임 때의 그 순간만 녹화함.

 

5️⃣ CrossHair_Walk 클립

걷는 상태일 때 크로스 헤어.

  • Line Left, Line Right의 경우 Pos X 값이 정지 상태에 비해 -25, 25 로 늘었다.
  • Line Down, Line Up의 경우 Pos Y 값이 정지 상태에 비해 -25, 25 로 늘었다.
  • 1 프레임 길이의, 딱 0 프레임 때의 그 순간만 녹화함.

 

6️⃣ CrossHair_Walk_Fire 클립

걷는 상태에서 발사 할 때 크로스 헤어.

  • Line Left, Line Right의 경우 Pos X 값이 발사 + 정지 상태에 비해 -45, 45 로 늘었다.
  • Line Down, Line Up의 경우 Pos Y 값이 발사 + 정지 상태에 비해 -45, 45 로 늘었다.
  • 1 프레임 길이의, 딱 0 프레임 때의 그 순간만 녹화함.

 

7️⃣ CrossHair_Run 클립

뛰는 상태일 때 크로스 헤어.

  • 색상을 투명하게 바꿔주었다. 뛸 땐 가운데 점을 제외한 4 개의 선이 보이지 않도록 4 개의 이미지 UI의 색상이 투명해지도록 녹화했다.
  • 투명해져서 안보이게 되긴 하겠지만 일단Line Left, Line Right의 경우 Pos X 값을 100까지 늘려주었다. 뛰면 크로스헤어가 늘려졌다가 투명해지며 사라지도록!
  • 투명해져서 안보이게 되긴 하겠지만 일단 Line Down, Line Up의 경우 Pos Y 값을 100까지 늘려주었다. 뛰면 크로스헤어가 늘려졌다가 투명해지며 사라지도록!
  • 1 프레임 길이의, 딱 0 프레임 때의 그 순간만 녹화함.

 

7️⃣ CrossHair_FineSight 클립

정조준 상태일 때 크로스 헤어.

  • 색상을 투명하게 바꿔주었다. 정조준할 땐 가운데 점을 제외한 4 개의 선이 보이지 않도록 4 개의 이미지 UI의 색상이 투명해지도록 녹화했다.
  • 투명해져서 안보이게 되긴 하겠지만 일단Line Left, Line Right의 경우 Pos X 값을 10까지 좁혀주었다. 정조준 하면 크로스헤어가 좁아졌다가 투명해지며 사라지도록!
  • 투명해져서 안보이게 되긴 하겠지만 일단 Line Down, Line Up의 경우 Pos Y 값을 10까지 좁혀주었다. 정조준 하면 크로스헤어가 좁아졌다가 투명해지며 사라지도록!
  • 1 프레임 길이의, 딱 0 프레임 때의 그 순간만 녹화함.

 

애니메이션 컨트롤러

“CrossHairController” 이름의 애니메이션 컨트롤러를 CrossHair에 Animator 컴포넌트의 컨트롤러로서 할당해준다.

파라미터는 위와 같이 추가해주었다.

  • Idle 👉 Walk, Run, Crouch, FineSight / Crouch, Walk 👉 FineSight / Walk, Crouch 👉 Run / Walk 👉 Crouch
    • 조건
      • 각각의 Bool 파라미터 True
    • Has Exit Time 해제 (파라미터가 True 되면 바로 실행 되야 함)
    • Translation Duration 0.2
  • Walk, Run, Crouch, FineSight 👉 Idle
    • 조건
      • 각각의 Bool 파라미터 False
    • Has Exit Time 해제 (파라미터가 False 되면 바로 실행 되야 함)
    • Translation Duration 0.2
  • Idle, Walk, Crouch 👉 Idle_Fire, Walk_Fire, Crouch_Fire
    • 조건
      • 각각의 Trigger 발동
    • Has Exit Time 해제 (Trigger가 발동되면 바로 실행 되야 함)
    • Translation Duration 0.2
  • Idle_Fire, Walk_Fire, Crouch_Fire 👉 Idle, Walk, Crouch
    • Has Exit Time 체크
      • 별다른 조건이 없이 트리거로 인해 한번 발동했으면, 그냥 해당 애니메이션 끝까지 재생하고 전이
    • Exit TIme 은 0.01 로 설정
      • 1 % 만 재생하고 크로스헤어가 한 순간에 원래대로 돌아오게끔 !!
    • Translation Duration 0.1

 

애니메이션 재생

📜Crosshair.cs

Canvas에 붙인다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Crosshair : MonoBehaviour
{
    [SerializeField]
    private Animator animator;

    private float gunAccuracy;  // 크로스 헤어의 상태에 따른 총의 정확도. 작으면 작을 수록 좋다. 총알이 뻗치는 정도!

    [SerializeField]
    private GameObject go_CrosshairHUD;  // 크로스 헤어 활성화/비활성화를 위한 부모 객체

    public void WalkingAnimation(bool _flag)
    {
        animator.SetBool("Walking", _flag);
    }

    public void RunningAnimation(bool _flag)
    {
        animator.SetBool("Running", _flag);
    }

    public void CrouchingAnimation(bool _flag)
    {
        animator.SetBool("Crouching", _flag);
    }

    public void FineSightAnimation(bool _flag)
    {
        animator.SetBool("FineSight", _flag);
    }

    public void FireAnimaion()
    {
        if(animator.GetBool("Walking"))
        {
            animator.SetTrigger("Walk_Fire");
        }
        else if(animator.GetBool("Crouching"))
        {
            animator.SetTrigger("Crouch_Fire");
        }
        else
        {
            animator.SetTrigger("Idle_Fire");
        }
    }
}

  • gunAccuracy
    • 크로스헤어에 따른 정확도. 총이 튀는 정도.
    • 📜Gun.cs 의 accruracy 총마다 가지고 있는 총의 정확도에서 이 값을 뺀 값에서 부터 총의 정확도에서 이 값을 더해준 값 범위의 사이에서 랜덤한 방향으로 Raycast를 쏠 것이다.
      • 📜GunController.cs 의 Hit() 에서 !
  • go_CrosshairHUD
    • 크로스 헤어 활성화/비활성화를 위한 부모 객체
    • 나중에 다른 총 종류 많아지고 총에서 손으로 바꾸는 등등 무기 바꿀 때 필요함! 이 크로스 헤어를 내리거나 다시 올려야 하므로.
  • WalkingAnimation
    • 인수에 따라 “Walking” 애니메이션 파라미터 값을 바꿔주는 함수
    • 걸을 때 크로스헤어 애니메이션을 실행하거나 해제한다.
  • RunningAnimation
    • 인수에 따라 “Running” 애니메이션 파라미터 값을 바꿔주는 함수
    • 달릴 때 크로스헤어 애니메이션을 실행하거나 해제한다.
  • CrouchingAnimation
    • 인수에 따라 “Crouching” 애니메이션 파라미터 값을 바꿔주는 함수
    • 앉았을 때 크로스헤어 애니메이션을 실행하거나 해제한다.
  • FineSightAnimation
    • 인수에 따라 “FineSight” 애니메이션 파라미터 값을 바꿔주는 함수
    • 정조준 크로스헤어 애니메이션을 실행하거나 해제한다.
  • FireAnimaion()
    • 발사를 해야할 때 걷는지 정조준중인지 등등의 상태에 따른 크로스헤어 애니메이션을 재생하도록 한다.
      • 발사를 처리하는 📜GunController.cs 의 Shoot() 에서 실행할 것이다.
    • 발사하려는데 현재 “Walking” 파라미터 값이 True 라면 “Walk_Fire” 파라미터를 발동 시킨다.
      • 걷기 + 발사시 크로스헤어 애니메이션 재생
    • 발사하려는데 현재 “Crouching” 파라미터 값이 True 라면 “Crouch_Fire” 파라미터를 발동 시킨다.
      • 앉기 + 발사시 크로스헤어 애니메이션 재생
    • 발사하려는데 둘 다 아니라면 “Idle” 상태인 것이다. “Idle” 파라미터를 발동 시킨다.
      • 정지 + 발사시 크로스헤어 애니메이션 재생

 

📜PlayerController.cs

// 추가된 멤버 변수

private bool isWalk = false; 
private Vector3 lastPos;  // 전 프레임의 위치. 현재 프레임의 위치와 비교했을 때 달라지면 움직임이 있었던 것이다.
private Crosshair theCrosshair;
    void Start()
    {
        // 컴포넌트 할당
        capsuleCollider = GetComponent<CapsuleCollider>();
        myRigid = GetComponent<Rigidbody>();
        theGunController = FindObjectOfType<GunController>();
        theCrosshair = FindObjectOfType<CrossHair>();

        applySpeed = walkSpeed;
        originPosY = theCamera.transform.localPosition.y;
        applyCrouchPosY = originPosY;
    }

    void Update()  
    {
        IsGround();
        TryJump();
        TryRun();
        TryCrouch();
        Move();
        MoveCheck();
        CameraRotation();
        CharacterRotation();
    }

    private void MoveCheck()
    {
        if (!isRun && !isCrouch && isGround)
        {
            if (Vector3.Distance(lastPos, transform.position) >= 0.01f)
                isWalk = true;
            else
                isWalk = false;

            theCrosshair.WalkingAnimation(isWalk);
            lastPos = transform.position;
        }  
    }

    // 달리기
    private void Running()
    {
        if (isCrouch)
            Crouch();

        theGunController.CancelFineSight();

        isRun = true;
        theCrosshair.RunningAnimation(isRun);
        applySpeed = runSpeed;
    }

    // 달리기 취소
    private void RunningCancel()
    {
        isRun = false;
        theCrosshair.RunningAnimation(isRun);
        applySpeed = walkSpeed;
    }

    // 앉기 동작
    private void Crouch()
    {
        isCrouch = !isCrouch;
        theCrosshair.CrouchingAnimation(isCrouch);

        if (isCrouch)
        {
            applySpeed = crouchSpeed;
            applyCrouchPosY = crouchPosY;
        }
        else
        {
            applySpeed = walkSpeed;
            applyCrouchPosY = originPosY;
        }

        StartCoroutine(CrouchCoroutine());
    }

    // 지면 체크
    private void IsGround()
    {
        isGround = Physics.Raycast(transform.position, Vector3.down, capsuleCollider.bounds.extents.y + 0.1f);
        theCrosshair.RunningAnimation(!isGround);
    }
  • MoveCheck()
    • 크로스헤어 걷기 애니메이션 재생 하려면 지금 플레이어가 걷는 중인지 상태를 알아야 한다.
      • 매 프레임마다 실행될 수 있도록 Update() 내에 추가
    • 플레이어가 걷는 중인지를 검사하고 isWalk를 업데이트 한다.
      • 단, 걷는 중, 뛰는 중, 앉는 중이라면 isWalk를 업데이트 할 필요가 없다.
      • 플레이어가 걷는 중인지를 검사하는 방법
        • 이전 프레임에서 업데이트 되었던 lastPos는 이전 프레임에서의 즉 과거의 현재 위치
        • 현재 프레임에서의 현재 위치와 lastPos가 같은지 비교해 보면 된다.
          • 같다면 움직임이 없었던 것이므로 isWalk는 False다.
            • 완전히 같다고 보기 보단 0.01 정도의 오차 이내면 같다고 보았다.
            • Vector3.Distance 는 두 Vector3의 차이, 즉 거리를 리턴한다.
          • 다르다면 움직임이 있었던 것이므로 isWalk는 True다.
    • isWalk 업데이트가 완료되면 이 값에 따른 크로스헤어 걷기 애니메이션을 재생한다.
      • 이 값에 따라 “Walking” 파라미터 값을 설정한다.
    • 다음 프레임때의 검사를 위해 lastPos도 현재 위치로 업데이트 해준다.
  • Running()
    • isRun값에 따라 “Running” 파라미터 값을 설정한다. 그에 따른 크로스헤어 달리기 애니메이션 재생.
    • isRun은 True
  • RunningCancel()
    • isRun값에 따라 “Running” 파라미터 값을 설정한다. 그에 따른 크로스헤어 달리기 애니메이션 재생.
    • isRun은 False
  • Crouch()
    • isCrouch값에 따라 “Crouching” 파라미터 값을 설정한다. 그에 따른 크로스헤어 앉기 애니메이션 재생.
  • IsGround()
    • isGround가 False면, 즉 점프 중이면 크로스헤어 앉기 애니메이션 재생시킨다.
      • 점프 중일때도 크로스 헤어가 벌어지면서 투명해지도록 하기 위해서

 

📜GunController.cs

// 추가된 멤버 변수

private Crosshair theCrosshair;
    void Start()
    {
        originPos = Vector3.zero;
        audioSource = GetComponent<AudioSource>();
        theCrosshair = FindObjectOfType<Crosshair>();
    }

    private void Shoot()  // 실제 발사 되는 과정
    {
        theCrosshair.FireAnimaion();
        // 발사 처리
        currentGun.currentBulletCount--;
        currentFireRate = currentGun.fireRate;  // 연사 속도 재계산
        PlaySE(currentGun.fire_Sound);
        currentGun.muzzleFlash.Play();

        // 피격 처리
        Hit();

        // 총기 반동 코루틴 실행
        StopAllCoroutines();
        StartCoroutine(RetroActionCoroutine());
    }

    private void FineSight()
    {
        isFineSightMode = !isFineSightMode;
        currentGun.anim.SetBool("FineSightMode", isFineSightMode);
        theCrosshair.FineSightAnimation(isFineSightMode);

        if (isFineSightMode)
        {
            StopAllCoroutines();
            StartCoroutine(FineSightActivateCoroutine());
        }
        else
        {
            StopAllCoroutines();
            StartCoroutine(FineSightDeActivateCoroutine());
        }
    }
  • Shoot()
    • 발사를 했으니 현재 상태에 맞춰 크로스헤어 애니메이션을 재생시켜주는 📜Crosshair.cs의 FireAnimaion() 함수를 실행시킨다.
  • FineSight()
    • isFineSightModeh값에 따라 “FineSight” 파라미터 값을 설정한다. 그에 따른 크로스헤어 정조준 애니메이션 재생.

 

🚖 크로스헤어에 따른 정확도 수정

📜Crosshair.cs

Canvas에 붙인다.

// 추가된 멤버 변수

[SerializeField]
private GunController theGunController;
    public float GetAccuracy()
    {
        if (animator.GetBool("Walking"))
        {
            gunAccuracy = 0.06f;
        }
        else if (animator.GetBool("Crouching"))
        {
            gunAccuracy = 0.015f;
        }
        else if (theGunController.GetFineSightMode())
        {
            gunAccuracy = 0.001f;
        }
        else
        {
            gunAccuracy = 0.035f;
        }

        return gunAccuracy;
    }
  • GetAccuracy()
    • 각기 상태에 따라총의 튀는 정도인 gunAccuracy 값을 설정한 후 이를 리턴한다.

 

📜GunController.cs

    public bool GetFineSightMode()
    {
        return isFineSightMode;
    }

    private void Hit()
    {
        // 카메라 월드 좌표!! (localPosition이 아님)
        if (Physics.Raycast(theCam.transform.position, 
            theCam.transform.forward + 
                new Vector3(Random.Range(-theCrosshair.GetAccuracy() - currentGun.accuracy, -theCrosshair.GetAccuracy() + currentGun.accuracy),
                            Random.Range(-theCrosshair.GetAccuracy() - currentGun.accuracy, -theCrosshair.GetAccuracy() + currentGun.accuracy),
                            0), 
            out hitInfo, 
            currentGun.range))
        {
            GameObject clone = Instantiate(hitEffectPrefab, hitInfo.point, Quaternion.LookRotation(hitInfo.normal));
            Destroy(clone, 2f);
        }
    }

  • Hit()
    • 발사시 카메라의 위치에서 Raycast를 쏴서 충돌되는게 있다면 그 위치가 바로 피격 위치다.
    • Raycast를 날리는 방향
      • 카메라의 앞 방향(theCam.transform.forward)으로 부터 정확도에 따라 랜덤한 오차를 주어 랜덤한 방향으로 총이 튀게끔 Raycast를 쏜다.
      • 총은 좌우(X축), 수직위아래(Y축)으로 튀므로 X, Y 값만 더해준다.
        • -theCrosshair.GetAccuracy() - currentGun.accuracy 부터
        • -theCrosshair.GetAccuracy() + currentGun.accuracy 범위까지의
        • 랜덤한 값

총의 정확도 Accuracy를 0.02로 주었다.

 

🚔 여기까지 스크립트 정리

📜Crosshair.cs

Canvas에 붙인다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Crosshair : MonoBehaviour
{
    [SerializeField]
    private Animator animator;

    private float gunAccuracy;  // 크로스 헤어의 상태에 따른 총의 정확도. 작으면 작을 수록 좋다. 총알이 뻗치는 정도!

    [SerializeField]
    private GameObject go_CrosshairHUD;  // 크로스 헤어 활성화/비활성화를 위한 부모 객체

    [SerializeField]
    private GunController theGunController;

    public void WalkingAnimation(bool _flag)
    {
        animator.SetBool("Walking", _flag);
    }

    public void RunningAnimation(bool _flag)
    {
        animator.SetBool("Running", _flag);
    }

    public void CrouchingAnimation(bool _flag)
    {
        animator.SetBool("Crouching", _flag);
    }

    public void FineSightAnimation(bool _flag)
    {
        animator.SetBool("FineSight", _flag);
    }

    public void FireAnimaion()
    {
        if(animator.GetBool("Walking"))
        {
            animator.SetTrigger("Walk_Fire");
        }
        else if(animator.GetBool("Crouching"))
        {
            animator.SetTrigger("Crouch_Fire");
        }
        else
        {
            animator.SetTrigger("Idle_Fire");
        }
    }

    public float GetAccuracy()
    {
        if (animator.GetBool("Walking"))
        {
            gunAccuracy = 0.06f;
        }
        else if (animator.GetBool("Crouching"))
        {
            gunAccuracy = 0.015f;
        }
        else if (theGunController.GetFineSightMode())
        {
            gunAccuracy = 0.001f;
        }
        else
        {
            gunAccuracy = 0.035f;
        }

        return gunAccuracy;
    }
}

 

📜PlayerController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    // 스피드 조정 변수
    [SerializeField]
    private float walkSpeed;
    [SerializeField]
    private float runSpeed;
    [SerializeField]
    private float crouchSpeed;
    private float applySpeed;

    [SerializeField]
    private float jumpForce;

    // 상태 변수
    private bool isWalk = false; 
    private bool isRun = false;
    private bool isGround = true;
    private bool isCrouch = false;

    // 움직임 체크 변수
    private Vector3 lastPos;  // 전 프레임의 위치. 현재 프레임의 위치와 비교했을 때 달라지면 움직임이 있었던 것이다.


    // 앉았을 때 얼마나 앉을지 결정하는 변수
    [SerializeField]
    private float crouchPosY;
    private float originPosY;
    private float applyCrouchPosY;

    // 민감도
    [SerializeField]
    private float lookSensitivity;  

    // 카메라 한계
    [SerializeField]
    private float cameraRotationLimit;  
    private float currentCameraRotationX; 

    // 필요한 컴포넌트
    [SerializeField]
    private Camera theCamera; 
    private Rigidbody myRigid;
    private CapsuleCollider capsuleCollider;
    private GunController theGunController;
    private Crosshair theCrosshair;

    void Start()
    {
        // 컴포넌트 할당
        capsuleCollider = GetComponent<CapsuleCollider>();
        myRigid = GetComponent<Rigidbody>();
        theGunController = FindObjectOfType<GunController>();
        theCrosshair = FindObjectOfType<Crosshair>();

        applySpeed = walkSpeed;
        originPosY = theCamera.transform.localPosition.y;
        applyCrouchPosY = originPosY;
    }

    void Update()  
    {
        IsGround();
        TryJump();
        TryRun();
        TryCrouch();
        Move();
        MoveCheck();
        CameraRotation();
        CharacterRotation();
    }

    // 지면 체크
    private void IsGround()
    {
        isGround = Physics.Raycast(transform.position, Vector3.down, capsuleCollider.bounds.extents.y + 0.1f);
        theCrosshair.RunningAnimation(!isGround);
    }

    // 점프 시도
    private void TryJump()
    {
        if (Input.GetKeyDown(KeyCode.Space) && isGround)
        {
            Jump();
        }
    }

    // 점프
    private void Jump()
    {
        if (isCrouch)
            Crouch();

        myRigid.velocity = transform.up * jumpForce;
    }

    // 달리기 시도
    private void TryRun()
    {
        if (Input.GetKey(KeyCode.LeftShift))
        {
            Running();
        }
        if (Input.GetKeyUp(KeyCode.LeftShift))
        {
            RunningCancel();
        }
    }

    // 달리기
    private void Running()
    {
        if (isCrouch)
            Crouch();

        theGunController.CancelFineSight();

        isRun = true;
        theCrosshair.RunningAnimation(isRun);
        applySpeed = runSpeed;
    }

    // 달리기 취소
    private void RunningCancel()
    {
        isRun = false;
        theCrosshair.RunningAnimation(isRun);
        applySpeed = walkSpeed;
    }

    // 앉기 시도
    private void TryCrouch()
    {
        if (Input.GetKeyDown(KeyCode.LeftControl))
        {
            Crouch();
        }
    }

    // 앉기 동작
    private void Crouch()
    {
        isCrouch = !isCrouch;
        theCrosshair.CrouchingAnimation(isCrouch);

        if (isCrouch)
        {
            applySpeed = crouchSpeed;
            applyCrouchPosY = crouchPosY;
        }
        else
        {
            applySpeed = walkSpeed;
            applyCrouchPosY = originPosY;
        }

        StartCoroutine(CrouchCoroutine());
    }

    // 부드러운 앉기 동작
    IEnumerator CrouchCoroutine()
    {
        float _posY = theCamera.transform.localPosition.y;
        int count = 0;

        while(_posY != applyCrouchPosY)
        {
            count++;
            _posY = Mathf.Lerp(_posY, applyCrouchPosY, 0.2f);
            theCamera.transform.localPosition = new Vector3(0, _posY, 0);
            if(count > 15)
                break;
            yield return null;
        }
        theCamera.transform.localPosition = new Vector3(0, applyCrouchPosY, 0);
    }

    private void Move()
    {
        float _moveDirX = Input.GetAxisRaw("Horizontal"); 
        float _moveDirZ = Input.GetAxisRaw("Vertical"); 

        Vector3 _moveHorizontal = transform.right * _moveDirX;
        Vector3 _moveVertical = transform.forward * _moveDirZ; 

        Vector3 _velocity = (_moveHorizontal + _moveVertical).normalized * applySpeed; 

        myRigid.MovePosition(transform.position + _velocity * Time.deltaTime);
    }

    private void MoveCheck()
    {
        if (!isRun && !isCrouch && isGround)
        {
            if (Vector3.Distance(lastPos, transform.position) >= 0.01f)
                isWalk = true;
            else
                isWalk = false;

            theCrosshair.WalkingAnimation(isWalk);
            lastPos = transform.position;
        }  
    }

    private void CameraRotation() 
    {
        float _xRotation = Input.GetAxisRaw("Mouse Y"); 
        float _cameraRotationX = _xRotation * lookSensitivity;

        currentCameraRotationX -= _cameraRotationX;
        currentCameraRotationX = Mathf.Clamp(currentCameraRotationX, -cameraRotationLimit, cameraRotationLimit);

        theCamera.transform.localEulerAngles = new Vector3(currentCameraRotationX, 0f, 0f);
    }

    private void CharacterRotation()
    {
        float _yRotation = Input.GetAxisRaw("Mouse X");
        Vector3 _characterRotationY = new Vector3(0f, _yRotation, 0f) * lookSensitivity;
        myRigid.MoveRotation(myRigid.rotation * Quaternion.Euler(_characterRotationY)); 
    }
}



 

📜GunController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GunController : MonoBehaviour
{
    [SerializeField]
    private Gun currentGun; // 현재 들고 있는 총. 📜Gun.cs 가 할당 됨.

    private float currentFireRate; // 이 값이 0 보다 큰 동안에는 총알이 발사 되지 않는다. 초기값은 연사 속도인 📜Gun.cs의 fireRate 

    private bool isReload = false;  // 재장전 중인지. 
    [HideInInspector]
    public bool isFineSightMode = false; // 정조준 중인지.

    [SerializeField]
    private Vector3 originPos;  // 원래 총의 위치(정조준 해제하면 나중에 돌아와야 하니까)

    private AudioSource audioSource;  // 발사 소리 재생기

    private RaycastHit hitInfo;  // 총알의 충돌 정보

    // 필요한 컴포넌트
    [SerializeField]
    private Camera theCam;  // 카메라 시점에서 정 중앙에 발사할 거라서
    private Crosshair theCrosshair;

    [SerializeField]
    private GameObject hitEffectPrefab;  // 피격 이펙트

    void Start()
    {
        originPos = Vector3.zero;
        audioSource = GetComponent<AudioSource>();
        theCrosshair = FindObjectOfType<Crosshair>();
    }

    void Update()
    {
        GunFireRateCalc();
        TryFire();
        TryReload();
        TryFineSight();
    }

    private void GunFireRateCalc()
    {
        if (currentFireRate > 0)
            currentFireRate -= Time.deltaTime;  // 즉, 1 초에 1 씩 감소시킨다.
    }

    private void TryFire()  // 발사 입력을 받음
    {
        if(Input.GetButton("Fire1") && currentFireRate <= 0 && !isReload)
        {
            Fire();
        }
    }

    private void Fire()  // 발사를 위한 과정
    {
        if (!isReload)
        {
            if (currentGun.currentBulletCount > 0)
                Shoot();
            else
            {
                CancelFineSight();
                StartCoroutine(ReloadCoroutine());
            }       
        }
    }

    private void Shoot()  // 실제 발사 되는 과정
    {
        theCrosshair.FireAnimaion();
        // 발사 처리
        currentGun.currentBulletCount--;
        currentFireRate = currentGun.fireRate;  // 연사 속도 재계산
        PlaySE(currentGun.fire_Sound);
        currentGun.muzzleFlash.Play();

        // 피격 처리
        Hit();

        // 총기 반동 코루틴 실행
        StopAllCoroutines();
        StartCoroutine(RetroActionCoroutine());
    }

    private void Hit()
    {
        // 카메라 월드 좌표!! (localPosition이 아님)
        if (Physics.Raycast(theCam.transform.position, 
            theCam.transform.forward + 
                new Vector3(Random.Range(-theCrosshair.GetAccuracy() - currentGun.accuracy, -theCrosshair.GetAccuracy() + currentGun.accuracy),
                            Random.Range(-theCrosshair.GetAccuracy() - currentGun.accuracy, -theCrosshair.GetAccuracy() + currentGun.accuracy),
                            0), 
            out hitInfo, 
            currentGun.range))
        {
            GameObject clone = Instantiate(hitEffectPrefab, hitInfo.point, Quaternion.LookRotation(hitInfo.normal));
            Destroy(clone, 2f);
        }
    }

    private void TryReload()
    {
        if (Input.GetKeyDown(KeyCode.R) && !isReload && currentGun.currentBulletCount < currentGun.reloadBulletCount)
        {
            CancelFineSight();
            StartCoroutine(ReloadCoroutine());
        }
    }

    IEnumerator ReloadCoroutine()
    {
        if(currentGun.carryBulletCount > 0)
        {
            isReload = true;
            currentGun.anim.SetTrigger("Reload");

            currentGun.carryBulletCount += currentGun.currentBulletCount;
            currentGun.currentBulletCount = 0;

            yield return new WaitForSeconds(currentGun.reloadTime);  // 재장전 애니메이션이 다 재생될 동안 대기

            if (currentGun.carryBulletCount >= currentGun.reloadBulletCount)
            {
                currentGun.currentBulletCount = currentGun.reloadBulletCount;
                currentGun.carryBulletCount -= currentGun.reloadBulletCount;
            }
            else
            {
                currentGun.currentBulletCount = currentGun.carryBulletCount;
                currentGun.carryBulletCount = 0;
            }

            isReload = false;
        }
        else
        {
            Debug.Log("소유한 총알이 없습니다.");
        }
    }

    private void TryFineSight()
    {
        if(Input.GetButtonDown("Fire2") && !isReload)
        {
            FineSight();
        }
    }

    public void CancelFineSight()
    {
        if (isFineSightMode)
            FineSight();
    }

    private void FineSight()
    {
        isFineSightMode = !isFineSightMode;
        currentGun.anim.SetBool("FineSightMode", isFineSightMode);
        theCrosshair.FineSightAnimation(isFineSightMode);

        if (isFineSightMode)
        {
            StopAllCoroutines();
            StartCoroutine(FineSightActivateCoroutine());
        }
        else
        {
            StopAllCoroutines();
            StartCoroutine(FineSightDeActivateCoroutine());
        }
    }

    IEnumerator FineSightActivateCoroutine()
    {
        while(currentGun.transform.localPosition != currentGun.fineSightOriginPos)
        {
            currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, currentGun.fineSightOriginPos, 0.2f);
            yield return null;
        }
    }

    IEnumerator FineSightDeActivateCoroutine()
    {
        while (currentGun.transform.localPosition != originPos)
        {
            currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, originPos, 0.2f);
            yield return null;
        }
    }

    IEnumerator RetroActionCoroutine()
    {
        Vector3 recoilBack = new Vector3(currentGun.retroActionForce, originPos.y, originPos.z);     // 정조준 안 했을 때의 최대 반동
        Vector3 retroActionRecoilBack = new Vector3(currentGun.retroActionFineSightForce, currentGun.fineSightOriginPos.y, currentGun.fineSightOriginPos.z);  // 정조준 했을 때의 최대 반동

        if(!isFineSightMode)  // 정조준이 아닌 상태
        {
            currentGun.transform.localPosition = originPos;

            // 반동 시작
            while(currentGun.transform.localPosition.x <= currentGun.retroActionForce - 0.02f)
            {
                currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, recoilBack, 0.4f);
                yield return null;
            }

            // 원위치
            while (currentGun.transform.localPosition != originPos)
            {
                currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, originPos, 0.1f);
                yield return null;
            }
        }
        else  // 정조준 상태
        {
            currentGun.transform.localPosition = currentGun.fineSightOriginPos;

            // 반동 시작
            while(currentGun.transform.localPosition.x <= currentGun.retroActionFineSightForce - 0.02f)
            {
                currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, retroActionRecoilBack, 0.4f);
                yield return null;
            }

            // 원위치
            while (currentGun.transform.localPosition != currentGun.fineSightOriginPos)
            {
                currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, currentGun.fineSightOriginPos, 0.1f);
                yield return null;
            }
        }
    }

    private void PlaySE(AudioClip _clip)  // 발사 소리 재생
    {
        audioSource.clip = _clip;
        audioSource.Play();
    }

    public Gun GetGun()
    {
        return currentGun;
    }

    public bool GetFineSightMode()
    {
        return isFineSightMode;
    }
}