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

Chapter 2-4. 무기 : 총알 HUD

by w1z 2024. 3. 11.

Chapter 2. 무기

총알의 상태를 알 수 있는 HUD

🚖 총알 HUD 만들기

캔버스

총알 HUD 를 그릴 종이 마련

  • 캔버스 추가 Create - UI - Canvas
  • Canvas의 Render Mode는 Screen Space -Overlay 로 해준다.
    • 캔버스는 언제나 게임 화면 위에 덮어 씌워 그려지게 된다.

###

 

이미지

왼쪽 하단에 현재 총알 상태를 나타낼 배경 이미지

  • 캔버스 위에 이미지 UI 를 추가해준다.
    • 이미지 UI 는 Sprite 만 추가할 수 있다.
  • 이미지 UI에 추가할 이미지 파일 설정
    • Texture Type 을 Sprite 로 설정
    • 이미지 크기 맞추기
      • 예를 들어 Sprite 이미지 파일의 너비 높이가가 182 x 38 이라면 182 를 커버할 수 있도록 Max Size를 256으로 맞춰주는게 좋다.
        • Max Size가 굳이 더 클 필요도 없으며 182 보다 작게 설정한다면 이미지가 잘린다.
  • 이미지 UI에 Sprite 이미지 파일을 Source Image에 할당
    • Rect Transform 조정
      • Sprite 이미지 파일의 크기에 맞게 너비와 높이를 똑같이 맞춰주는게 좋겠다.
        • 예를 들어 Sprite 이미지 파일의 너비 높이가가 182 x 38 이라면 Rect Transform 의 Width 와 Height 도 182, 38 로 해준다.
      • PosX, PosY, PosZ 로 위치를 조정한다.

 

텍스트

 

총알 정보를 알려주는 3 개의 텍스트를 추가

  • 모두 이미지의 자식으로 추가해주었고 알맞는 위치에 배치했다.
  • 폰트 사이즈와 정렬 등등 설정해줌.
  • RayCast 체크 해제
    • 이 텍스트가 어떤 충돌을 감지할 필요가 없기 때문에

  • 총알 UI 전체를 대표하는 빈 오브젝트 HUD
    • 이미지 BulletUIImage
      • 텍스트 CurrentBullet
      • 텍스트 ReloadBullet
      • 텍스트 CarryBullet
  1. 현재 탄창 안에 있는 총알의 개수를 표시할 텍스트 CurrentBullet
  2. 최대 재장전 개수를 표시할 텍스트 ReloadBullet
  3. 현재 소유하고 있는 총알의 총 개수를 표시할 텍스트 CarryBullet

 

🚖 총알 개수 UI 업데이트

 

📜GunController.cs

    public Gun GetGun()
    {
        return currentGun;
    }

📜HUD.cs에서 📜GunController.cs의 currentGun이 필요한데 private 변수라서 이렇게 currentGun을 리턴해주는 함수를 추가했다.

📜HUD.cs

총알 UI 전체를 대표하는 빈 오브젝트 HUD 에 붙여준다.

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

public class HUD : MonoBehaviour
{
    // 총알 정보를 얻기 위해 📜Gun.cs, 📜GunController.cs 가 필요
    [SerializeField]
    private GunController theGunController;
    private Gun currentGun;

    // 총알 텍스트 UI들을 담았던 이미지 UI를 할당할 것이다. 필요할 때 HUD를 호출하고 필요 없을 땐 비활성화 할 것이다.
    [SerializeField]
    private GameObject go_BulletHUD;

    // 총알 개수를 텍스트 UI에 반영
    [SerializeField]
    private Text[] text_Bullet;  

    void Update()
    {
        CheckBullet();
    }

    private void CheckBullet()
    {
        currentGun = theGunController.GetGun();
        text_Bullet[0].text = currentGun.carryBulletCount.ToString();
        text_Bullet[1].text = currentGun.reloadBulletCount.ToString();
        text_Bullet[2].text = currentGun.currentBulletCount.ToString();
    }
}

using UnityEngine.UI 필요

  • currentGun
    • 📜GunController.cs (theGunController)의 📜Gun.cs타입 멤버 변수 currentGun을 가져와 할당할 것이다.
    • 📜Gun.cs 에서 현재 탄창안에 있는 총알 수, 재장전 총알 수, 현재 소유하고 있는 총알 수 변수를 가지고 있기 때문에 필요한 작업이다.
      • currentBulletCount
      • reloadBulletCount
      • carryBulletCount
    • 나중에 📜WeaponManager.cs 코드를 작성하면 그 곳에서 관리 될 것이다.
  • go_BulletHUD
    • 무기를 바꾸면 그 총에 맞는 총알 HUD로 바뀌어야 한다. 현재 들고 있는 총을 해제하면 총알 HUD를 비활성화 해야 하고 총을 새로 들게 되면 HUD를 활성화 새로 해야 하므로 필요
    • 총알 텍스트 UI들과 함께 배경 이미지를 담당하는 BulletUIImage 이미지가 할당 될 것임
    • HUD를 활성화 해야한느지 비활성화 해야하는지에 대한 신호는 📜WeaponManager.cs 로부터 받게 될 것이다.
  • text_Bullet
    • 3 가지 텍스트 UI 배열
    • using UnityEngine.UI 필요
  • 매 프레임마다 CheckBullet() 을 실행하여 총알 HUD 텍스트를 업데이트 한다.

  • theGunController에 📜GunController.cs 가 붙어있는 Holder 할당
  • go_BulletHUD에 총알 텍스트 UI들의 부모인 BulletUIImage 이미지 할당
  • text_Bullet 배열에 3 개의 텍스트 UI를 각각 원소로 할당

총을 발사할 때마다 총알 텍스트 UI가 업데이트 되는 것을 확인할 수 있다.

 

 

🚖 여기까지 스크립트 정리

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

    public Gun GetGun()
    {
        return currentGun;
    }
}

📜HUD.cs

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

public class HUD : MonoBehaviour
{
    // 총알 정보를 얻기 위해 📜Gun.cs, 📜GunController.cs 가 필요
    [SerializeField]
    private GunController theGunController;
    private Gun currentGun;

    // 총알 텍스트 UI들을 담았던 이미지 UI를 할당할 것이다. 필요할 때 HUD를 호출하고 필요 없을 땐 비활성화 할 것이다.
    [SerializeField]
    private GameObject go_BulletHUD;

    // 총알 개수를 텍스트 UI에 반영
    [SerializeField]
    private Text[] text_Bullet;  

    void Update()
    {
        CheckBullet();
    }

    private void CheckBullet()
    {
        currentGun = theGunController.GetGun();
        text_Bullet[0].text = currentGun.carryBulletCount.ToString();
        text_Bullet[1].text = currentGun.reloadBulletCount.ToString();
        text_Bullet[2].text = currentGun.currentBulletCount.ToString();
    }
}