FPS서바이벌 디펜스

5-2.재장전, 정조준, 반동

ruripanda 2025. 4. 17. 10:44

본 강좌는 케이디님의 유튜브강좌 영상을 보고 유니티6로 구현한 것 입니다 이렇게 멋진 강좌를 만드신 케이디님을 존경합니다

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

public class GunController : MonoBehaviour
{

    [SerializeField]
    private Gun currentGun;

    private float currentFireRate;

    private bool isReload = false;
    public bool isFineSightMode = false;

    // 본래 포지션 값.
    [SerializeField]
    private Vector3 originPos;

    private AudioSource audioSource;

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

    // Update is called once per frame
    void Update()
    {
        GunFireRateCalc();
        TryFire();
        TryReload();
        TryFineSight();
    }

    private void GunFireRateCalc()
    {
        if (currentFireRate > 0)
            currentFireRate -= Time.deltaTime;
    }

    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.reloadTiem);

            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();
    }
}
using System.Collections;
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 isCrouch = false;
    private bool isGround = true;

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

    //땅 착지 여부
    private CapsuleCollider capsuleCollider;

    //카메라 민감도
    [SerializeField]
    private float lookSensitivity;
    [SerializeField]

    //카메라 한계
    private float cameraRotationLimit;
    private float currentCameraRotationX = 0;

    //필요 컴포넌트
    [SerializeField]
    private Camera theCamera;//카메라
    private Rigidbody myRigid;//리지드바디
    [SerializeField]
    private GunController theGunController;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        capsuleCollider = GetComponent<CapsuleCollider>();
        myRigid = GetComponent<Rigidbody>();//Player에 있는 리지드바디 장착
        applySpeed = walkSpeed;
        theGunController = FindAnyObjectByType<GunController>();
        //초기화
        originPosY = theCamera.transform.localPosition.y;
        applyCrouchPosY = originPosY;
    }

    // Update is called once per frame
    void Update()
    {
        IsGround();
        TryJump();
        TryRun();
        TryCrouch();
        Move();
        CameraRotation();
        CharacterRotation();
    }
    
    private void TryCrouch()//앉기 시도
    {
        if (Input.GetKeyDown(KeyCode.LeftControl))
        {
            Crouch();
        }
    }

    private void Crouch()//실제 앉기
    {
        isCrouch = !isCrouch;
        //if (isCrouch)
        //    isCrouch = false;
        //else
        //    isCrouch = true;위 한줄짜리 코드 축약임
        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.5f);
            theCamera.transform.localPosition = new Vector3(0, _posY, 0);
            if (count > 15)
                break;
            yield return new WaitForSeconds(0.1f);
        }
        theCamera.transform.localPosition = new Vector3(0, applyCrouchPosY, 0f);
    }

    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.linearVelocity = 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 Move()//걷기 실행
    {
        float _moveDirX = Input.GetAxisRaw("Horizontal");//좌우 방향키를 입력시 +1 ~ -1이 반환된다
        float _moveDirZ = Input.GetAxisRaw("Vertical");//상하 움직임 입력시 +1 ~ -1이 반환된다

        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 CharacterRotation()//좌우 캐릭터 회전
    {
        float _yRotation = Input.GetAxisRaw("Mouse X");
        Vector3 _characterRotationY = new Vector3(0f, _yRotation, 0f) * lookSensitivity;
        myRigid.MoveRotation(myRigid.rotation * Quaternion.Euler(_characterRotationY));
        //Debug.Log(myRigid.rotation);
        //Debug.Log(myRigid.rotation.eulerAngles);
    }

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

 

지난 시간에 이은 글 입니다

 

 

 

먼저 재장전을 공부합시다

 

Shoot()함수를 수정해줍니다

currentGun.currentBulletCount--로 총알의 숫자를 1빼줍니다

69번줄은 Shoot 함수가 실행될때 사용하기 위해 Fire()함수에서 잘라서 붙여넣어 줬습니다

 

Fire()함수를 수정해줍니다

52번줄에 조건문으로 !isReload를 써서 isReload가 false일때

54번에 조건문 currentGun.currentBulletCount > 0 으로 총알이 0보다 많을때

Shoot()함수를 호출하여 발사할 수 있게 해줍니다

56번에 else는 CancelFineSight()함수를 호출하고 코루틴 ReloadCoroutine()를 실행합니다

 

ReloadCoroutine() 코루틴을 보겠습니다

조건문으로 carryBulletCount > 0 으로 즉 남은 총알이 0보다 클때 사용할 수 있게 해줍니다

92번줄에 isReload를 true로 바꿔주고

94번줄에 SetTrigger로 애니메이터에 트리거로 등록된 Reload를 실행합니다

 

실행을 위해 애니메이터에 애니메이션 Gun0_Reload를 넣고 Reload를 Trigger형식으로 파라미터에 넣어줍니다

그리고 오른쪽 이미지를 참고해서 파란색으로 강조된 화살표의 속성을 수정해줍시다

 

97번 줄에 currentGun.carrBulletCount += currentGun.currentBulletCount; 으로 현재 탄알을 채워줍니다

그 뒤 98번줄에 currentBulletCount = 0으로 초기화를 해줍니다

 

그뒤 100번 줄에 reloadTime만큼 프레임을 쉬어줍니다

그뒤 102번 줄에 조건문 총 총알 잔량(carrBulletCount)이 리로드시 채워주는 총알(reloadBulletCount)보다 크거나 같으면

104번 줄에 현재 장전된 탄알(currentBulletCount)와 리로드 탄알(reloadBulletCount)와 같게해줍니다

그리고 105번줄에 총 탄알(currentBulletCount)에서 리로드 탄알(reloadBulletCount)을 빼줍니다

 

그리고 107에 else에

총 장전된 총알(currentBulletCount)와 총 탄알(carryBulletCount)를 같게해주고

110번 줄에 남은 총 탄알을 0으로 만들어 줍니다

 

그리고 114번줄에 isReload = false로 해주고

118번에 총알이 없으면 "소유한 총알이 없습니다" 메시지를 띠워줍니다

이제 변수를 추가해줍니다

11번줄, 14번줄, 17~18번 줄 입니다

다음은 CancelFineSight()함수를 보겠습니다

132번에 if문으로 isFineSightMode가 true일때

FineSight() 함수를 호출해줍니다

 

다음 FineSight()  함수입니다

138번 줄에 isFineSightMode = !isFineSightMode; 함수로 bool값을 on/off를 간편하게 해줍니다

그리고 애니메이터에 등록된 SetBool값 FineSightMode를 실행해줍니다 왼쪽에 bool값이므로 isFineSightMode를 작성해줬습니다

 

Bool값으로 파라미터에 FineSightMode를 넣어주고 파란색으로 강조된 Gun0_FineSight 박스를 확인합니다

그뒤 Idle 화살표를 수정해줍니다

왼쪽은 FineSight가 Idle로 바뀔때 속성이고 오른쪽은 Idle가 FineSight로 바뀔때 속성입니다

 

그리고 마지막은 Walk에서 FineSight로 화살표가 갈때 속성입니다

 

141번줄에 if문으로 isFineSightMode가 true일때

모든 코루틴을 종료하고 FineSightActivateCoroutine() 코루틴을 실행해줍니다

 

 

FineSightActivateCoroutine() 코루틴 입니다 정조준을 위해 작성되었습니다

while 반복문으로 currentGun.transform.localPosition != current.fineSightOriginPos를 조건문으로 위치 값이 틀릴때 반복하게

하며 currentGun.transform.localPosition 위치 값을

Vector3.Lerp로 움직임이 표현되게 하여 currentGun.transform.localPosition, currentGun.fineSightOriginPos, 0.2f로 움직이는 값을 보여줍니다

그리고 한턴 쉬어줍니다

 

IEnumerator FineSightDeactivateCoroutine() 함수입니다

while 조건문으로 currentGun.transform.localPosition ! = originPos로 originPos가 아닐때

Vector3.Lerp로 움직임이 표현되게 하여 originPos로 시점이 돌아가게 하는 함수입니다

 

RetroActionCoroutine() 함수입니다

 Vector3 recoilBack = new Vector3(currentGun.retroActionForce, originPos.y, originPos.z);
 Vector3 retroActionRecoilBack = new Vector3(currentGun.retroActionFineSightForce, currentGun.fineSightOriginPos.y, currentGun.fineSightOriginPos.z);

일단 위 두줄의 Vector3 변수를 선언해줍니다

 

178번에 조건문으로 !isFineSightMode를 선언해주고

currentGun.transform.localPosition = originPos; 작성하여 값을 넣어줍니다

그 다음 184번줄에 while문으로 로컬포지션 값이 retroActionRecoilBack - 0.02f 한 값이 아닐때

186번 줄의 Vector3.Lerp(currentGun.transform.localPosition, recoilBack, 0.4f)가 실행되게 해줍니다

 

그리고 반복문이 끝나면 한 프레임 쉬어주고

191번줄의 whlie문으로 원래 위치 값으로 돌아오게해 줍니다

그리고 한 프레임 쉬게해 줍니다

이걸로 총을 사격할때 반동이 구현됩니다

 

197번의 else도 또 반복문을 구현한 것 입니다

같은 요령으로 해주면 됩니다

 

이제 인스팩터에 FineSightOrigin...의 값과 Retro. Action Force와 Retro Action Fine...의 값을 넣어주고 테스트를 하면

이렇게 구현됩니다

 

마지막으로 우리가 전에 작성한 PlayerController 스크립트로 열어봅시다

밑줄친 부분을 작성해주고

void Start()에 추가해 줍시다

Running()함수에 밑줄친 부분을 추가해주면 우리가 정조준하다가 달리기를 하면 정조준이 풀리게 됩니다

 

 

ps.

저가 재대로 쓴건지 이번 로직은 복잡해서 해깔리네요...

열심히 공부하겠습니다