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

Chapter 2-2. 무기 : 총 구현 2️⃣ (재장전, 정조준, 반동)

by w1z 2024. 3. 11.

Chapter 2. 무기

총 구현

🚖 재장전

📜Gun.cs

    public int reloadBulletCount;   // 총의 재장전 개수. 재장전 할 때 몇 발씩 될지. 총의 종류마다 다름.
    public int currentBulletCount;  // 현재 총 안의 탄창에 남아있는 총알의 개수.
    public int maxBulletCount;      // 총알을 최대 몇 개까지 소유할 수 있는지. 
    public int carryBulletCount;    // 현재 총 바깥에서 소유하고 있는 총알의 총 개수.

📜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;  // 재장전 중인지. 재장전 중이 아닌 false 일 때만 발사가 이루어 져야 한다.

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

    void Start()
    {
        audioSource = GetComponent<AudioSource>();
    }

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

    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
                StartCoroutine(ReloadCoroutine());
        }
    }

    private void Shoot()  // 실제 발사 되는 과정
    {
        currentGun.currentBulletCount--;
        currentFireRate = currentGun.fireRate;  // 연사 속도 재계산
        PlaySE(currentGun.fire_Sound);
        currentGun.muzzleFlash.Play();
        Debug.Log("총알 발사 함");
    }

    private void TryReload()
    {
        if (Input.GetKeyDown(KeyCode.R) && !isReload && currentGun.currentBulletCount < currentGun.reloadBulletCount)
        {
            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 PlaySE(AudioClip _clip)  // 발사 소리 재생
    {
        audioSource.clip = _clip;
        audioSource.Play();
    }
}

재장전 하고 싶을 때 수동으로 재장전

R 키를 누르면 재장전 할 수 있도록 한다.

  • TryReload()
    • 👉 매 프레임마다 아래와 같은 3 가지 조건을 검사하고 3 가지 조건이 모두 만족할 때만 실제 재장전 처리를 하는 ReloadCoroutine() 함수를 실행시킨다.
      • R키 입력이 들어와야 하고
      • isReload 값이 False 여야 하고 (이미 재장전 중이라면 재장전 하면 안된다.)
      • 현재 탄창 안에 있는 총알의 개수와 재장전 되는 총알의 개수가 같다면 재장전 되면 안된다. 현재 탄창 안에 있는 총알의 개수 보다 재장전 되는 총알의 개수가 클 때만 재장전이 되여 한다. 예를 들어 현재 탄창에 3발 있는데 한번 재장전 될 때 40발로 채워진다면 재장전이 가능하다. 근데 현재 탕창에 이미 40발 있다면 재장전 될 필요가 없다.
        currentGun.currentBulletCount < currentGun.reloadBulletCount)
        
  • ReloadCoroutine()
    • 👉 실제 재장전을 처리하는 코루틴 함수
      • isReload를 True 로 설정.
        • 이미 재장전 중일땐 또 재장전 하면 안되기 때문에 이런 boolean 멤버 변수가 필요 하다.
      • 재장전 애니메이션을 재생한다.
        • 이 함수가 코루틴인 이유! 재장전 애니메이션이 다 재생될 때까지 대기해야 하기 때문에. 대충 reloadTime 동안.
          yield return new WaitForSeconds(currentGun.reloadTime);
          
      • 탄창에 있는 총알 제외하고, 현재 소유하고 있는 총알의 개수에다가 현재 탄창에 있는 총알을 더해준다. 그리고 현재 탄창에 있는 총알을 0 으로 만들어 준다.
        • 예륻 들어 현재 탄창에 3발 있고 현재 소유하고 있는 총알이 7발이라면, 현재 탄창을 0 발로 만들고 현재 소유하고 있는 총알을 10 발로 만든다.
        • 이미 탄창안에 총알이 있는 상태에서 재장전 할 경우를 대비하여 이런 작업을 해주는 것이다. 기존에 탄창 안에 있던 총알은 총알 소유분으로 옮겨주고 재장전 할 수 있는 탄창 수만큼 더 해주면 기존에 탄창 안에 있던 총알이 날라가지 않는다. 현재 탄창 14/20 상태에서 20발 재장전 한다면 14발은 총알 소유분에 더해주고 20/20 으로 만들어 주어 14 발을 날라가지 않게 보존한다.
          currentGun.carryBulletCount += currentGun.currentBulletCount;
          currentGun.currentBulletCount = 0;
          
      • 현재 소유하고 있는 총알에서 재장전 할 총알들을 끌어오기 때문에
        • (현재 소유하고 있는 총알 >= 재장전 총알 개수) 라면
          • 탄창에 있는 총알을 재장전 총알 개수 만큼 넣어 주고
            • 위의 과정 때문에 탄창에 있는 총알은 0 인 상태. 0/20 에서 20/20 이 된다.
          • 현재 소유하고 있는 총알에서 재장전 한 총알 개수만큼 차감한다.
        • (현재 소유하고 있는 총알 < 재장전 총알 개수) 라면
          • 탄창에 있는 총알을 현재 소유하고 있는 총알 개수만큼 넣어 주고
          • 현재 소유하고 있는 총알 개수를 0 으로 만들어 준다.
      • 재장전 처리가 끝났으니 isReload 를 다시 False 로 만들어 준다.

 

발사를 시도 할 때 재장전

총을 발사를 하려는데 현재 탄창에 있는 총알이 하나도 없다면 자동으로 재장전을 하도록 한다.

  • TryFire()
    • 발사가 가능한지를 체크하는 함수
    • 재장 전이 아닐 때만 발사가 가능해야 한다.
      • 조건에서 !isReload인지를 체크함.
  • Fire()
    • 재장전 아닐 때만 발사가 이루어 져야 한다.
      • 현재 탄창 안에 총알이 있을 때만 Shoot() 실제 발사 처리를 하고
      • 현재 탄창 안에 총알이 아예 없을 땐 ReloadCoroutine() 실제 재장전 처리를 한다.
  • Shoot()
    • 실제 발사를 처리하는 함수
    • 발사가 되면 현재 탄창 안에 있는 총알을 1 감소시켜야 한다.
    • 재장전과 상관 없는 얘기지만 연사 속도를 리셋하는 부분을 그냥 Shoot() 안에로 옮겨 주었다.

SubMachineGun1의 📜Gun.cs 에 재장전 관련 멤버 함수 값 할당.

 

🚖 정조준

정조준 애니메이션

  • 파라미터로 Bool 타입의 FindSightMode 를 추가한다.
    • Reload처럼 Trigger가 아닌 이유는, 재장전은 연속적이 아닌 딱 한번만 이루어져야 하기 때문 Trigger로 애니메이션을 재생시키는게 적합하다.
    • 반면에 정조준은 한번 우클을 눌러 정조준 상태가 되면 정조준 상태를 유지해야 한다. 다시 우클을 또 눌러 정조준을 해제하지 않는 이상! 따라서 Boolean 이 적합하다.
      • 따라서 Any Stage 👉 Gun0_FindSightMode 또한 적합하지 않다. FindSightMode가 Boolean 타입이기 때문에 한번 True 로 변경되면 수동적으로 바꿔주지 않는 한 True 를 유지하기 때문에 어떤 상태이든간에 Any Stage 👉 Gun0_FindSightMode 이 무한적으로 실행되기 때문이다.
  • Gun0_FindSightMode 👉 Gun0_Idle
    • 조건
      • FindSightMode 값이 True 가 되면 발동한다.
      • 즉시 재생하게 Hax Exit Time 체크 해제
      • Translation Duration 은 0.1
  • Gun0_Idle 👉 Gun0_FindSightMode
    • 조건
      • FindSightMode 값이 False 가 되면 발동한다.
      • 즉시 재생하게 Hax Exit Time 체크 해제
      • Translation Duration 은 0.1
  • Gun0_Walk 👉 Gun0_FindSightMode
    • 조건
      • FindSightMode 값이 True 가 되면 발동한다.
      • 즉시 재생하게 Hax Exit Time 체크 해제
      • Translation Duration 은 0.1
      • Gun0_FindSightMode 👉 Gun0_Walk 은 없어도 된다.

 

📜GunController.cs

// 추가한 멤버 변수

    private bool isFindSightMode = false; // 정조준 중인지.

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

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

    private void TryFindSight()
    {
        if(Input.GetButtonDown("Fire2"))
        {
            FindSight();
        }
    }

    private void FindSight()
    {
        isFindSightMode = !isFindSightMode;
        currentGun.anim.SetBool("FindSightMode", isFindSightMode);
        
        if(isFindSightMode)
        {
            StopAllCoroutines();
            StartCoroutine(FindSightActivateCoroutine());
        }
        else
        {
            StopAllCoroutines();
            StartCoroutine(FindSightDeActivateCoroutine());
        }
    }

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

    IEnumerator FindSightDeActivateCoroutine()
    {
        while (currentGun.transform.localPosition != originPos)
        {
            currentGun.transform.localPosition = Vector3.Lerp(currentGun.transform.localPosition, originPos, 0.2f);
            yield return null;
        }
    }
  • 매 프레임마다
    • 🚗 TryFindSight()
      • 마우스 우클릭 입력이 들어오는지를 검사하고 마우스 우클 입력이 들어오면 FindSight() 함수를 실행시킨다.
    • 🚗 FindSight()
      • 정조준 중이였다면 정조준을 해제하고, 정조준 안된 상태였다면 정조준을 한다. 즉, 우클 한번 누른 후에 정조준 상태를 유지하고 우클을 다시 누르면 정조준을 해제 하게 한다.
        isFindSightMode = !isFindSightMode;
        
      • FindSightMode 파라미터 값을 isFindSightMode값으로 설정한다. True면 FindSightMode = True 조건에 따른 정조준 애니메이션이 재생되고 FindSightMode = False 라면 정조준 애니메이션이 더 이상 재생되지 않는다.
      • 정조준 해야 한다면 (우클 홀수번째 클릭)
        • 우선 현재 실행중인 코루틴 함수를 모두 중단한다.
          • 중단 하는 이유
            • 밑에 후술할 2 개의 코루틴 함수는 각각 다음과 같은 일을 한다.
              • 정중앙 위치로 총(자기 자신)의 위치를 설정,
              • 정조준 해제를 위해 원래의 위치로 총(자기 자신)의 위치를 설정.
            • 위치를 변하게 할 때 while 문 안에서 Lerp 함수를 통해 보간하며 부드럽게 점점 변화시킬 것인데, 이전 포스트에서 배웠듯이 보간은 딱 떨어져서 되지 않는 경우가 대다수다. 그래서 변화를 마치고 원하는 위치로 딱 정확하게 일치할 때까지 반복시킨다는 그 while문 조건을 만족하기가 힘들다.
            • 따라서 while문이 영원히 끝나지 않아 두 코루틴 함수가 내내 끝나지 않고 두 코루틴 함수가 병행해서 계속 while 문에 갇혀 보간을 무한 반복하고 있는 상태가 되어 두 함수가 계속 실행되기 때문에 총(자기 자신)의 위치는 계속해서 변한다.
            • 이런 상황에서 또 우클 입력이 들어와 정조준을 해야 하면 변화 중인 위치에서부터 또 보간을 시작하기 때문에 정조준이 제대로 되지 않는다. 중앙이 아닌 엉뚱한 곳으로 총이 움직일 것이다.
            • 따라서!!!!!!! 정조준을 위한 위치 설정 or 원래 위치로 설정 작업을 하기 전에 기존에 실행중이던 코루틴 함수들이 있다면 다 중단을 시켜야 정확하게 위치하게 할 수 있다!
        • FindSightActivateCoroutine() 함수를 실행한다.
          • 정조준을 하기 위해서 현재 총(자기 자신)의 위치를 정조준시 총의 위치인 📜Gun.cs의 findSightOriginPos로 변화시키는데, 이를 부드럽게 업뎃하는 일을 수행한다.
            • FindSightActivateCoroutine() 함수
              • 현재 위치가 정조준시 총이 위치가 될 때까지 현재 위치를 업데이트 하고 한 프레임 쉬기를 계속 반복한다.
              • 부드럽게 5 번 보간되어 업뎃된다.
      • 정조준을 해제해야 한다면 (우클 짝수번째 클릭)
        • 우선 현재 실행중인 코루틴 함수를 모두 중단한다.
          • 이유 위와 동일
        • FindSightDeActivateCoroutine() 함수를 실행한다.
          • 정조준 해제를 하기 위해서 현재 총(자기 자신)의 위치를 원래의 총의 위치인 originPos로 변화시키는데, 이를 부드럽게 업뎃하는 일을 수행한다.

📜Gun.cs findSightOriginPos 정조준시 총이 있어야할 위치를 (0.55, 0.335, -0.586) 으로 설정한다. 총이 딱 정중앙으로 오는 위치다.

📜GunController.cs originPos는 그대로 (0, 0, 0)으로 둔다. SubMachineGun1의 원래 위치(LocalPosition)가 (0, 0, 0) 이기 때문에.

 

🚖 반동

📜GunController.cs

    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 Shoot()  // 실제 발사 되는 과정
    {
        currentGun.currentBulletCount--;
        currentFireRate = currentGun.fireRate;  // 연사 속도 재계산
        PlaySE(currentGun.fire_Sound);
        currentGun.muzzleFlash.Play();

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

        Debug.Log("총알 발사 함");
    }

구현하려는 반동은 총이 올라가는 수직 반동이 아니라 총을 발사 할 때마다 총이 앞 뒤로 왔다 갔다 하는 반동을 구현할 것이다. GunHolder를 Y 축으로 90 도 회전했었던 탓에, 총의 앞,뒤 이동 축은 Z 축이 아닌 X 축이 되었다. 사진에서 화살표 방향, 즉 플레이어 쪽으로 총이 당겨지는 쪽이 X 축 positive 방향이다. 발사할 때마다 총의 X 축 방향으로 이동값을 주어서 반동시키고 다시 원래 위치로 되돌아가는 식으로 반동을 표현한다. X 방향 Position 값만 변경해주면 된다. 정조준 하고 쐈을 때와 정조준 안하고 쐇을 때 이렇게 두 케이스로 나눈다.

  • recoilBack
    • 정조준 안 했을 때의 최대 반동.
    • 즉 X 방향으로 📜Gun.cs의 retroActionForce(정조준 안 했을 때의 반동 세기) 위치로 이동시킨다. (델타값 아님. 절대적인 x 방향 좌표값임)
      • 현재 위치로부터 Vector3(retroActionForce, 원래Y위치, 원래Z위치) 로 Lerp 보간하여 부드럽게 업뎃시킬 것이다. X 위치 값만 변화를 준것을 확인할 수 있다.
        Vector3 recoilBack = new Vector3(currentGun.retroActionForce, originPos.y, originPos.z);
        
  • retroActionRecoilBack
    • 정조준 했을 때의 최대 반동.
    • 즉 X 방향으로 📜Gun.cs의 retroActionFineSightForce(정조준 했을 때의 반동 세기) 위치로 이동시킨다. (델타값 아님. 절대적인 x 방향 좌표값임)
      • 현재 위치로부터 Vector3(retroActionFineSightForce, 정조준시Y위치, 정조준시Z위치) 로 Lerp 보간하여 부드럽게 업뎃시킬 것이다. X 위치 값만 변화를 준것을 확인할 수 있다.
        Vector3 retroActionRecoilBack = new Vector3(currentGun.retroActionFineSightForce, currentGun.fineSightOriginPos.y, currentGun.fineSightOriginPos.z);
        
  • 정조준이 아닌 상태일 때
    • 반동이 이루어지고 있는 상태에서 또 반동이 들어오면(또 발사가 들어 오면) 중복 처리가 되기 때문에(코루틴 함수라 병렬적으로 실행하는게 가능하므로) 반동을 시작하고 원 위치로 되돌리는 총체적인 반동 작업을 시작 하기에 앞서 총 위치를 정조준이 아닌 원래 위치에서 시작하게끔 한다.
      currentGun.transform.localPosition = originPos;
      
    • 1️⃣ 반동을 시작한다. 👉 총의 현재 절대 위치 값을 시작으로 recoilBack Vector3 값을 목표로 총의 현재 위치 값을 부드럽게 업뎃 해나간다.
      • 부드럽게 보간하는 작업은 총의 X 방향 위치가 recoilBack 보다 커질 때까지(반복 종료조건) 반복한다. 단, 보간 때문에 영원히 이렇게 안될 수도 있어서 recoilBack에서 0.02 를 빼준값 보다 커질 때까지만 반복하도록 살짝 보정해주었다.
        • 현재 위치가 recoilBack 위치로 될 때까지 40 퍼센트 비율로 보정한다.
        • 한 프레임씩 쉰다.
    • 2️⃣ 원 위치로 돌아간다. 👉 한번 recoilBack Vector3 근처까지 당겨졌었으면 다시 원래 위치로 돌아 가야 한다. 총의 현재 위치 값을 시작으로 원래 위치인 originPos Vector3 값을 목표로 총의 현재 위치 값을 부드럽게 업뎃 해나간다.
      • 부드럽게 보간하는 작업은 총의 현재 위치가 원래 위치와 일치하게 될 때까지 ! (사실 이것도 보간 때문에 무한 루프에 빠질 수 있는 부분인데 나중에 코루틴 중지하는 작업을 해줄거라서 괜찮다.)
        • 현재 위치가 originPos 위치로 될 때까지 10 퍼센트 비율로 보정한다. (반동 때보다는 좀 더 느리게 돌아오게끔)
        • 한 프레임씩 쉰다.
  • 정조준 상태일 때
    • 위와 방식이 동일하다. 반동 값은 retroActionRecoilBack, 원래 위치는 총의 정조준 위치인 currentGun.fineSightOriginPos으로 쓰는 것 빼고는 같다.

  • 정조준이 아닐 때 총의 위치는 (0, 0, 0)
    • 발사시 반동으로 (0.3, 0, 0) 에 근접해졌다가 다시 (0, 0, 0)으로 돌아온다.
  • 정조준 중 일 때 총의 위치는 (0.55, 0.33, -0.557)
    • 발사시 반동으로 (1.25, 0.33, -0.557) 에 근접해졌다가 다시 (0.55, 0.33, -0.557)으로 돌아온다. (0.55 + 0.7 = 1.250)

 

🚔 그 밖의 개선

재장전 하는 도중에 우클(정조준) 하고 나면 발사가 안되는 버그Permalink

이런 버그가 나타나는 이유

  • 재장전 애니메이션을 실행하는 도중에, 즉, ReloadCoroutine() 코루틴 함수에서 currentGun.anim.SetTrigger(“Reload”); 재장전 애니메이션을 재생하고 yield return new WaitForSeconds(currentGun.reloadTime); reloadTime동안 대기 시간을 가지는 동안에
  • 위 대기 시간 동안 우클 입력이 들어와 정조준을 하게 되어서 재장전 애니메이션을 재생하는 대기 시간동안 FindSight() 함수를 실행하고, StopAllcoroutines() 실행을 만나기 때문이다.
    • 따라서 isReload가 다시 False 가 되기 전에 ReloadCoroutine() 코루틴 함수가 중단이 되어 버려서 isReload는 True 인 상태로 남게 되어서 발사가 불가능해진 것이다. !isReload 재장전 중이 아닐 때만 발사가 가능하므로.

📜GunController.cs

따라서 ReloadCoroutine() 실행 중일 때 ReloadCoroutine() 함수는 StopAllcoroutines() 실행으로 중단되지 않도록 해야 한다. 재장전 중일 땐 정조준/정조준해제 가 이루어지면 안된다.

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

정조준은 재장전 중이 아닐 때만 실행할 수 있도록 한다.

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

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

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

정조준 상태에서 재장전 우클 입력이 들어온다면 정조준을 취소한다.

  • CancelFineSight() 함수
    • 정조준 중이라면 FineSight() 한번 더 실행하여 정조준을 해제시킨다.
  • 재장전 하기 전에 CancelFineSight() 함수를 한번 실행하여 정조준 상태라면 해제시킨다.
    • 재장전 실행시키는 곳
      • TryReload()
      • Fire() 발사하려는데 탄창이 비어있을 때

 

정조준 상태에서 뛰면 정조준 해제 시키기

📜PlayController.cs

// 추가한 멤버 변수
private GunController theGunController;
    void Start()
    {
        ...
        
        theGunController = FindObjectOfType<GunController>();

        ...
    }

    private void Running()
    {
        if (isCrouch)
            Crouch();

        theGunController.CancelFineSight();

        isRun = true;
        applySpeed = runSpeed;
    }
  • 📜GunController.cs 의 CancelFineSight() 함수를 public으로 바꾼 후
  • 📜PlayController.cs 의 Running() 함수에서 이를 실행.
    • 달리는 중에 정조준 중인 상태라면 이를 해제 하도록

 

🚔 여기까지 스크립트 정리

📜PlayController.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 isRun = false;
    private bool isGround = true;
    private bool isCrouch = false;

    // 앉았을 때 얼마나 앉을지 결정하는 변수
    [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;

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

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

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

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

    // 점프 시도
    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;
        applySpeed = runSpeed;
    }

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

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

    // 앉기 동작
    private void Crouch()
    {
        isCrouch = !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 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;  // 재장전 중인지. 
    private bool isFineSightMode = false; // 정조준 중인지.

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

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

    void Start()
    {
        audioSource = GetComponent<AudioSource>();
    }

    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()  // 실제 발사 되는 과정
    {
        currentGun.currentBulletCount--;
        currentFireRate = currentGun.fireRate;  // 연사 속도 재계산
        PlaySE(currentGun.fire_Sound);
        currentGun.muzzleFlash.Play();

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

        Debug.Log("총알 발사 함");
    }

    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);
        
        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();
    }
}