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

Chapter 2-3. 무기 : 총 피격 구현

by w1z 2024. 3. 11.

Chapter 2. 무기

총 피격 구현

총알이 맞는 곳의 피격 효과

🚖 피격 위치 확인

📜GunController.cs

// 추가한 멤버 변수

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

    [SerializeField]
    private Camera theCam;  // 카메라 시점에서 정 중앙에 발사할 거라서
    private void Shoot()  // 실제 발사 되는 과정
    {
        // 발사 처리
        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, out hitInfo, currentGun.range))
        {
            Debug.Log(hitInfo.transform.name);
        }
    }

플레이어 머리 위에 달려 있는 카메라로부터 RayCast를 쏴서 충돌 되는 지점을 총알이 맞은 곳이라고 할 것이다. 1 인칭 카메라이기 때문에 사실상 화면 정중앙에 총을 쏘게 되기 때문이다.

  • theCam
    • Main Camera를 이 변수에 할당해주자.
  • Shoot()
    • 발사 처리 후, 반동 처리 전, Hit() 함수 실행
  • Hit()
    • 👉 총알이 맞은 곳을 인지한다.
      • 카메라 위치로부터 카메라의 앞 방향으로 Raycast를 공격 사정 거리인 range 거리만큼 쐈을 때 충돌 되는 것이 있다면 그 정보를 hitInfo에 담고 충돌 된 오브젝트의 이름을 출력!
      • theCam.transform.localPosition이 아닌, theCam.transform.position 을 기준으로 한다. 즉, 카메라의 로컬 위치가 아닌 카메라의 월드 좌표 기준에서의 위치로부터 Raycast를 쏜다.

 

🚖 피격 이펙트 파티클 시스템

 

파티클 시스템 만들기

피격 효과가 될 “Hit Effect” 이름의 파티클 시스템을 만든다.

  • 메인 모듈
    • Duration - 1.0
    • Start Lifetime - 0.5
    • Start Speed - 15
    • Start Color - 색깔 지정
    • Loop 체크 해제
    • Play On Awake 체크 해둔다!!! 👉 이 파티클이 생성되자마자 바로 재생.
    • Gravity Modifier - 0.4
      • 중력의 영향을 받아 파티클 입자가 생성 후 자연스레 떨어지게 된다.
  • Emission 모듈
    • Rate over Time - 0
    • Bursts 추가 - Count는 7로
  • Shape 모듈 - 입자들이 방출되는 모양을 결정한다.
    • Angle - 70
      • 파티클 입자가 더 넓게 튀도록
      • Cone 원뿔 꼭지점의 각도로, 각도가 0 이면 원기둥이 되고 90 이면 평면 원반이 된다.
    • Radius - 0.0001 최소로 해줌. (원 뿔의 반지름)
      • 가장 작은 거의 점에 가까운 반경 내에서 파티클들이 생성 되도록
  • Color Over Lifetime
    • 자연스럽게 사라지도록 수명 주기의 중간 즈음부터는 투명해지게 하였다.
  • Size Over Lifetime
    • 자연스럽게 작아지면서 사라지도록
  • Trails 모듈
    • 잔상 남기기
      • Ratio 는 1 로 설정
        • 잔상을 남길 입자 비율
        • 파티클 모든 입자에게 100 퍼 잔상을 생성한다.
        • 만약 0.5 라면 입자 개수의 절반에 해당하는 랜덤한 입자들만 잔상을 생성한다.
      • Lifetime 은 0.2 로 설정
        • 잔상의 수명
      • Minimum Vertex Distance는 기본값이 0.2
        • 잔상이 어느 거리만큼 가야 Vertex 가 업데이트 될지를 나타내는 설정 값이다.
        • 이 값이 작으면 작을 수록, 0 에 가까울 수록 자주 업데이트 되므로 잔상이 자연스럽다.
        • 크면 클수록 잔상이 업데이트 되는 거리가 길어져 뚝뚝 끊길 것이다.
      • Color Over LifeTime
        • 잔상의 알파값을 0.5 로 해주어 점점 투명해지게 하였다.
  • Renderer 모듈
    • Trails 모듈을 사용하기 위해 Trail Maerial을 설정해준다. - “Sprited Default”로 설정.

이제 이 Hit Effect 파티클 시스템을 📂Assets/Prefabs 로 옮겨 프리팹화 시킨다.

프리팹으로 만든 후 Hierarchy 에서는 지워준다. 총에게 피격 당한 위치에서 Hit Effect 프리팹을 오브젝트로 생성시킬 것이다.

 

📜GunController.cs

피격시 프리팹화 해 두었던 Hit Effect 피격 파티클 시스템 생성하기

// 추가한 멤버 변수

[SerializeField]
private GameObject hitEffectPrefab;
    private void Hit()
    {
        // 카메라 월드 좌표!! (localPosition이 아님)
        if (Physics.Raycast(theCam.transform.position, theCam.transform.forward, out hitInfo, currentGun.range))
        {
            GameObject clone = Instantiate(hitEffectPrefab, hitInfo.point, Quaternion.LookRotation(hitInfo.normal));
            Destroy(clone, 2f);
        }
    }
  • 카메라로부터 Raycast를 쐈을 때 충돌된게 있으면 Hit Effect 피격 파티클 시스템 프리팹을 생성한다.
    • 피격된 위치에 (hitInfo.point)
    • 피격된 위치의 표면의 방향을 쳐다보는 방향으로 (Quaternion.LookRotation(hitInfo.normal))
    • hitEffectPrefab을 생성하여
    • clone 변수에 할당 한다.
  • 게임 플레이 중 무한정 생성하다보면 메모리 부족 문제가 생길 수 있으므로 이 파티클 시스템은 2 초 뒤에 사라지게 한다.
    • Destroy(clone, 2f)
  • 파티클을 생성해주기만 하면 된다. Play On Awake에 체크 해놨기 때문에 파티클이 생성되어 활성화되자마자 자동으로 재생될 것이다. 따라서 Play()를 직접 실행시킬 필요는 없다.

Hit Effect 프리팹을 hitEffectPrefab에 할당.

 

🚖 여기까지 스크립트 정리

📜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;  // 카메라 시점에서 정 중앙에 발사할 거라서

    [SerializeField]
    private GameObject hitEffectPrefab;

    void Start()
    {
        originPos = Vector3.zero;
        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();

        // 피격 처리
        Hit();

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

    private void Hit()
    {
        // 카메라 월드 좌표!! (localPosition이 아님)
        if (Physics.Raycast(theCam.transform.position, theCam.transform.forward, 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);
        
        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();
    }
}