유니티

3D 총기 구현

잼잼재미 2024. 1. 2. 23:00

 

총기 구현


1. GunHolder 생성

 

Player 자식으로 GunHolder 빈오브젝트, 그 자식으로 Gun 프리팹 생성

 

 

 

Gun 프리팹 자식으로 Amature 빈 오브젝트, 그 자식으로 Gun 빈 오브젝트 생성, 총알 발사 ParticleSystem 추가

* ParticleSystem은 무료 에셋을 다운 받아서 사용하자!

 

 

 

Stop Action 은 None으로 해서 Particle 재생 후, Destroy 되지 않도록 해줌

ParticleSystem이 총구에서 실행 되도록 위치 조정

 

 

2. 스크립트 작성

Gun

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

public class Gun : MonoBehaviour
{
    [field: SerializeField] public Vector3 OriginPos;
    [field: SerializeField] public Vector3 FineSightOriginPos;
    [field : SerializeField] public string GunName { get; private set; }  // 총의 이름
    [field: SerializeField] public float Range { get; private set; }      // 총의 사정 거리
    [field: SerializeField] public float FireRate { get; private set; }   // 연사 속도 (한발과 한발간의 시간 텀)
    [field: SerializeField] public float ReloadTime { get; private set; } // 재장전 속도
    [field: SerializeField] public int Damage { get; private set; }       // 총의 공격력
    [field: SerializeField] public float RetroActionForce { get; private set; }   // 반동 세기
    [field: SerializeField] public float RetroActionFineSightForce { get; private set; }    // 정조준 반동 세기
    [field: SerializeField] public float Accuracy { get; private set; } // 정확도
    [field: SerializeField] public float AccuracyFineSight { get; private set; } // 정조준 정확도

    [field: SerializeField] public int ReloadBulletCount { get; private set; }    // 총의 재장전 개수
    public int CurrentBulletCount;  // 현재 탄창에 남아있는 총알의 개수
    [field: SerializeField] public int MaxBulletCount { get; private set; }       // 총알 최대 소유 개수
    public int CarryBulletCount;    // 현재 소유하고 있는 총알의 총 개수

    [field: SerializeField] public Animator Anim { get; private set; }
    [field: SerializeField] public ParticleSystem MuzzleFlash { get; private set; }    // 화염구 이펙트
    [field: SerializeField] public GameObject HitEffectPrefab { get; private set; }    // 총알 피격 이펙트
    [field: SerializeField] public AudioClip Fire_Sound { get; private set; }      // 총 발사 소리 오디오 클립

}

 

 

GunController

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

public class GunController : MonoBehaviour
{
    [Header("Gun")]
    public Gun CurrentGun;

    [Header("Camera")]
    [SerializeField] private CinemachineVirtualCamera _virtualCamera;

    public bool IsFindSightMode { get; private set; }

    private CinemachinePOV _pov;
    private AudioSource _audioSource;  // 발사 소리 재생기
    private RaycastHit _hitInfo;  // 총알의 충돌 정보
    private float _currentFireRate;
    private bool _isReload = false;
    private float _originFOV;



    private void Awake()
    {
        _audioSource = GetComponent<AudioSource>();
        _pov = _virtualCamera.GetCinemachineComponent<CinemachinePOV>();

    }

    private void Start()
    {
        IsFindSightMode = false;
    }


    void Update()
    {

        GunFireRateCalc();
        TryFire();
        TryReload();
        TryFineSight();

    }

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

    private void TryFire()
    {
        if (Input.GetButton("Fire1") && _currentFireRate <= 0)
        {
            Fire();
        }
    }

    private void Fire()
    {
        if (!_isReload)
        {
            if (CurrentGun.CurrentBulletCount > 0)
                Shoot();
            else
            {
                CancelFineSight();
                StartCoroutine(COReload());
            }
        }
    }

    private void Shoot()
    {
        CurrentGun.CurrentBulletCount--;
        _currentFireRate = CurrentGun.FireRate;
        PlaySE(CurrentGun.Fire_Sound);
        CurrentGun.MuzzleFlash.Play();
        CurrentGun.Anim.SetTrigger("Fire");

        // 피격 처리
        Hit();

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

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

    private void TryReload()
    {
        if (Input.GetKeyDown(KeyCode.R) && !_isReload && CurrentGun.CurrentBulletCount < CurrentGun.ReloadBulletCount)
        {
            CancelFineSight();
            StartCoroutine(COReload());
        }
    }

    private void Hit()
    {
        Vector3 randomRange = new Vector3(Random.Range(-CurrentGun.Accuracy, CurrentGun.Accuracy), Random.Range(-CurrentGun.Accuracy, CurrentGun.Accuracy), 0);
        Vector3 randomRangeFineSight = new Vector3(Random.Range(-CurrentGun.AccuracyFineSight, CurrentGun.AccuracyFineSight), Random.Range(-CurrentGun.AccuracyFineSight, CurrentGun.AccuracyFineSight), 0);

        if (!IsFindSightMode)  // 정조준이 아닌 상태
        {
            Debug.DrawRay(Camera.main.transform.position, (Camera.main.transform.forward + randomRange) * CurrentGun.Range, Color.blue, 0.3f);
            if (Physics.Raycast(Camera.main.transform.position, Camera.main.transform.forward + randomRange, out _hitInfo, CurrentGun.Range)) // 카메라 월드좌표
            {
                GameObject clone = Instantiate(CurrentGun.HitEffectPrefab, _hitInfo.point, Quaternion.LookRotation(_hitInfo.normal));

                Debug.Log(_hitInfo.transform.name);

            }
        }
        else // 정조준 상태
        {
            Debug.DrawRay(Camera.main.transform.position, (Camera.main.transform.forward + randomRangeFineSight) * CurrentGun.Range, Color.blue, 0.3f);
            if (Physics.Raycast(Camera.main.transform.position, Camera.main.transform.forward + randomRangeFineSight, out _hitInfo, CurrentGun.Range)) // 카메라 월드좌표
            {
                GameObject clone = Instantiate(CurrentGun.HitEffectPrefab, _hitInfo.point, Quaternion.LookRotation(_hitInfo.normal));

                Debug.Log(_hitInfo.transform.name);
            }
        }

    }

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

    private void FineSight()
    {
        IsFindSightMode = !IsFindSightMode;
        CurrentGun.Anim.SetBool("FineSightMode", IsFindSightMode);

        if (IsFindSightMode)
        {
            StopAllCoroutines();
            StartCoroutine(COFineSightActivate());
        }
        else
        {
            StopAllCoroutines();
            StartCoroutine(COFineSightDeActivate());
        }
    }

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

    IEnumerator COFineSightActivate()
    {
        _originFOV = _virtualCamera.m_Lens.FieldOfView;

        while (CurrentGun.transform.localPosition != CurrentGun.FineSightOriginPos)
        {
            CurrentGun.transform.localPosition = Vector3.Lerp(CurrentGun.transform.localPosition, CurrentGun.FineSightOriginPos, 0.2f);
            _virtualCamera.m_Lens.FieldOfView = 30f;

            yield return null;
        }
    }

    IEnumerator COFineSightDeActivate()
    {
        while (CurrentGun.transform.localPosition != CurrentGun.OriginPos)
        {
            CurrentGun.transform.localPosition = Vector3.Lerp(CurrentGun.transform.localPosition, CurrentGun.OriginPos, 0.2f);
            _virtualCamera.m_Lens.FieldOfView = _originFOV;
            yield return null;
        }
    }

    IEnumerator COReload()
    {
        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("소유한 총알이 없습니다.");
        }
    }

    IEnumerator CORetroAction()
    {
        Vector3 recoilBack = new Vector3(CurrentGun.OriginPos.x, CurrentGun.OriginPos.y, CurrentGun.OriginPos.z - CurrentGun.RetroActionForce); // 정조준 x
        Vector3 retroActionRecoilBack = new Vector3(CurrentGun.FineSightOriginPos.x, CurrentGun.FineSightOriginPos.y, CurrentGun.FineSightOriginPos.z - CurrentGun.RetroActionFineSightForce);  // 정조준

        if (!IsFindSightMode)  // 정조준이 아닌 상태
        {
            CurrentGun.transform.localPosition = CurrentGun.OriginPos;

            // 반동 시작
            while (CurrentGun.transform.localPosition.z >= CurrentGun.OriginPos.z - CurrentGun.RetroActionForce + 0.02f)
            {
                CurrentGun.transform.localPosition = Vector3.Lerp(CurrentGun.transform.localPosition, recoilBack, 0.4f);
                _pov.m_VerticalAxis.Value += -CurrentGun.RetroActionForce;
                yield return null;
            }

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

            // 반동 시작
            while (CurrentGun.transform.localPosition.z >= CurrentGun.FineSightOriginPos.z - CurrentGun.RetroActionFineSightForce + 0.02f)
            {
                CurrentGun.transform.localPosition = Vector3.Lerp(CurrentGun.transform.localPosition, retroActionRecoilBack, 0.4f);
                _pov.m_VerticalAxis.Value += -CurrentGun.RetroActionFineSightForce;
                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();
    }
}

 

 

3. Gun 설정

 

각 값 설정, Gun이 카메라에 보이도록 Position 설정

OriginPos : 기본 총구 위치와 동일하게 (위 Gun Position과 동일)

FindSightOriginPos : 정조준 시, 위치로 설정

 

 

4. GunHolder 설정

 

Audio Source 컴포넌트 추가, Virtual Camera 사용한다면 추가

CurrentGun, HandGun1 추가

 

 

5. HitEffect 설정

 

HitEffectPrefab이 실행 후, 파괴되도록 Stop Action을 Destroy로 설정

* ParticleSystem은 무료 에셋을 다운 받아서 사용하자!

 

 

6. Animator 설정

 

idle, walk, run, reload, fire, findSightMode 애니메이션 설정

* idle, walk, run, reload, findSightMode 는 Position을 직접 설정해서 애니메이션을 만듦

 

 

'유니티' 카테고리의 다른 글

3D 총기 구현 (2)  (1) 2024.01.05
Rotation 값 변경  (1) 2024.01.03
매니저 관리  (0) 2023.12.29
DontDestroyOnLoad  (0) 2023.12.28
씬 이동  (0) 2023.12.28