Unity Chapter 11-9. 좀비 TPS 게임 만들기 : Gun

Date:     Updated:

카테고리:

태그:

인프런에 있는 이제민님의 레트로의 유니티 C# 게임 프로그래밍 에센스 강의를 듣고 정리한 필기입니다. 😀
🌜 [레트로의 유니티 C# 게임 프로그래밍 에센스] 강의 들으러 가기!


Chapter 11. 좀비 TPS 게임 만들기

📜Gun.cs

Gun 오브젝트에 붙인다.

using System;
using System.Collections;
using UnityEngine;

// 총을 구현한다
public class Gun : MonoBehaviour
{
    // 총의 상태를 표현하는데 사용할 타입을 선언한다
    public enum State
    {
        Ready, // 발사 준비됨
        Empty, // 탄창이 빔
        Reloading // 재장전 중
    }
    public State state { get; private set; } // 현재 총의 상태

    private PlayerShooter gunHolder;
    private LineRenderer bulletLineRenderer; // 총알 궤적을 그리기 위한 렌더러

    private AudioSource gunAudioPlayer; // 총 소리 재생기
    public AudioClip shotClip; // 발사 소리
    public AudioClip reloadClip; // 재장전 소리

    public ParticleSystem muzzleFlashEffect; // 총구 화염 효과
    public ParticleSystem shellEjectEffect; // 탄피 배출 효과

    public Transform fireTransform; // 총알이 발사될 위치
    public Transform leftHandMount;

    public float damage = 25; // 공격력
    public float fireDistance = 100f; // 사정거리

    public int ammoRemain = 100; // 남은 전체 탄약
    public int magAmmo; // 현재 탄창에 남아있는 탄약
    public int magCapacity = 30; // 탄창 용량

    public float timeBetFire = 0.12f; // 총알 발사 간격
    public float reloadTime = 1.8f; // 재장전 소요 시간

    [Range(0f, 10f)] public float maxSpread = 3f;
    [Range(1f, 10f)] public float stability = 1f;
    [Range(0.01f, 3f)] public float restoreFromRecoilSpeed = 2f;
    private float currentSpread;
    private float currentSpreadVelocity;

    private float lastFireTime; // 총을 마지막으로 발사한 시점

    private LayerMask excludeTarget;

    private void Awake()
    {
        // 사용할 컴포넌트들의 참조를 가져오기
        gunAudioPlayer = GetComponent<AudioSource>();
        bulletLineRenderer = GetComponent<LineRenderer>();

        // 사용할 점을 두개로 변경
        bulletLineRenderer.positionCount = 2;
        // 라인 렌더러를 비활성화
        bulletLineRenderer.enabled = false;
    }

    public void Setup(PlayerShooter gunHolder)
    {
        this.gunHolder = gunHolder;
        excludeTarget = gunHolder.excludeTarget;
    }

    private void OnEnable()
    {
        currentSpread = 0;
        // 현재 탄창을 가득채우기
        magAmmo = magCapacity;
        // 총의 현재 상태를 총을 쏠 준비가 된 상태로 변경
        state = State.Ready;
        // 마지막으로 총을 쏜 시점을 초기화
        lastFireTime = 0;
    }

    private void OnDisable()
    {
        StopAllCoroutines();
    }

    public bool Fire(Vector3 aimTarget)
    {
        // 현재 상태가 발사 가능한 상태
        // && 마지막 총 발사 시점에서 timeBetFire 이상의 시간이 지남
        if (state == State.Ready
            && Time.time >= lastFireTime + timeBetFire)
        {
            var xError = Utility.GetRandomNormalDistribution(0f, currentSpread);
            var yError = Utility.GetRandomNormalDistribution(0f, currentSpread);


            var fireDirection = aimTarget - fireTransform.position;

            fireDirection = Quaternion.AngleAxis(yError, Vector3.up) * fireDirection;
            fireDirection = Quaternion.AngleAxis(xError, Vector3.right) * fireDirection;

            currentSpread += 1f / stability;

            // 마지막 총 발사 시점을 갱신
            lastFireTime = Time.time;
            // 실제 발사 처리 실행
            Shot(fireTransform.position, fireDirection);

            return true;
        }

        return false;
    }

    // 실제 발사 처리
    private void Shot(Vector3 startPoint, Vector3 direction)
    {
        // 레이캐스트에 의한 충돌 정보를 저장하는 컨테이너
        RaycastHit hit;
        // 총알이 맞은 곳을 저장할 변수
        var hitPosition = Vector3.zero;

        // 레이캐스트(시작지점, 방향, 충돌 정보 컨테이너, 사정거리)
        if (Physics.Raycast(startPoint, direction, out hit, fireDistance, ~excludeTarget))
        {
            // 레이가 어떤 물체와 충돌한 경우

            // 충돌한 상대방으로부터 IDamageable 오브젝트를 가져오기 시도
            var target =
                hit.collider.GetComponent<IDamageable>();

            // 상대방으로 부터 IDamageable 오브젝트를 가져오는데 성공했다면
            if (target != null)
            {
                DamageMessage damageMessage;

                damageMessage.damager = gunHolder.gameObject;
                damageMessage.amount = damage;
                damageMessage.hitPoint = hit.point;
                damageMessage.hitNormal = hit.normal;

                // 상대방의 OnDamage 함수를 실행시켜서 상대방에게 데미지 주기
                target.ApplyDamage(damageMessage);
            }
            else
            {
                EffectManager.Instance.PlayHitEffect(hit.point, hit.normal, hit.transform);
            }
          // 레이가 충돌한 위치 저장
            hitPosition = hit.point;
        }
        else
        {
            // 레이가 다른 물체와 충돌하지 않았다면
            // 총알이 최대 사정거리까지 날아갔을때의 위치를 충돌 위치로 사용
            hitPosition = startPoint + direction * fireDistance;
        }

        // 발사 이펙트 재생 시작
        StartCoroutine(ShotEffect(hitPosition));

        // 남은 탄환의 수를 -1
        magAmmo--;
        if (magAmmo <= 0)
            // 탄창에 남은 탄약이 없다면, 총의 현재 상태를 Empty으로 갱신
            state = State.Empty;
    }

    // 발사 이펙트와 소리를 재생하고 총알 궤적을 그린다
    private IEnumerator ShotEffect(Vector3 hitPosition)
    {
        // 총구 화염 효과 재생
        muzzleFlashEffect.Play();
        // 탄피 배출 효과 재생
        shellEjectEffect.Play();

        // 총격 소리 재생
        gunAudioPlayer.PlayOneShot(shotClip);

        // 선의 시작점은 총구의 위치
        bulletLineRenderer.SetPosition(0, fireTransform.position);
        // 선의 끝점은 입력으로 들어온 충돌 위치
        bulletLineRenderer.SetPosition(1, hitPosition);
        // 라인 렌더러를 활성화하여 총알 궤적을 그린다
        bulletLineRenderer.enabled = true;

        // 0.03초 동안 잠시 처리를 대기
        yield return new WaitForSeconds(0.03f);

        // 라인 렌더러를 비활성화하여 총알 궤적을 지운다
        bulletLineRenderer.enabled = false;
    }

    // 재장전 시도
    public bool Reload()
    {
        if (state == State.Reloading ||
            ammoRemain <= 0 || magAmmo >= magCapacity)
            // 이미 재장전 중이거나, 남은 총알이 없거나
            // 탄창에 총알이 이미 가득한 경우 재장전 할수 없다
            return false;

        // 재장전 처리 시작
        StartCoroutine(ReloadRoutine());
        return true;
    }

    // 실제 재장전 처리를 진행
    private IEnumerator ReloadRoutine()
    {
        // 현재 상태를 재장전 중 상태로 전환
        state = State.Reloading;
        // 재장전 소리 재생
        gunAudioPlayer.PlayOneShot(reloadClip);

        // 재장전 소요 시간 만큼 처리를 쉬기
        yield return new WaitForSeconds(reloadTime);

        // 탄창에 채울 탄약을 계산한다
        var ammoToFill = magCapacity - magAmmo;

        // 탄창에 채워야할 탄약이 남은 탄약보다 많다면,
        // 채워야할 탄약 수를 남은 탄약 수에 맞춰 줄인다
        if (ammoRemain < ammoToFill) ammoToFill = ammoRemain;

        // 탄창을 채운다
        magAmmo += ammoToFill;
        // 남은 탄약에서, 탄창에 채운만큼 탄약을 뺸다
        ammoRemain -= ammoToFill;

        // 총의 현재 상태를 발사 준비된 상태로 변경
        state = State.Ready;
    }

    private void Update()
    {
        currentSpread = Mathf.SmoothDamp(currentSpread, 0f, ref currentSpreadVelocity, 1f / restoreFromRecoilSpeed);
        currentSpread = Mathf.Clamp(currentSpread, 0f, maxSpread);
    }
}


멤버 변수

    public enum State
    {
        Ready, // 발사 준비됨
        Empty, // 탄창이 빔
        Reloading // 재장전 중
    }
    public State state { get; private set; } // 현재 총의 상태

    private PlayerShooter gunHolder;
    private LineRenderer bulletLineRenderer; // 총알 궤적을 그리기 위한 렌더러

    private AudioSource gunAudioPlayer; // 총 소리 재생기
    public AudioClip shotClip; // 발사 소리
    public AudioClip reloadClip; // 재장전 소리

    public ParticleSystem muzzleFlashEffect; // 총구 화염 효과
    public ParticleSystem shellEjectEffect; // 탄피 배출 효과

    public Transform fireTransform; // 총알이 발사될 위치
    public Transform leftHandMount;

    public float damage = 25; // 공격력
    public float fireDistance = 100f; // 사정거리

    public int ammoRemain = 100; // 남은 전체 탄약
    public int magAmmo; // 현재 탄창에 남아있는 탄약
    public int magCapacity = 30; // 탄창 용량

    public float timeBetFire = 0.12f; // 총알 발사 간격
    public float reloadTime = 1.8f; // 재장전 소요 시간

    [Range(0f, 10f)] public float maxSpread = 3f;
    [Range(1f, 10f)] public float stability = 1f;
    [Range(0.01f, 3f)] public float restoreFromRecoilSpeed = 2f;
    private float currentSpread;
    private float currentSpreadVelocity;

    private float lastFireTime; // 총을 마지막으로 발사한 시점

    private LayerMask excludeTarget;
  • state
    • 3 가지 상태
      1. Ready : 발사 준비 상태
      2. Empty : 탄창이 빈 상태
      3. Reloading : 재장전 중인 상태
    • 프로퍼티를 통하여 get은 public, set은 private으로 해두었다.
      • 📜Gun 내부에서만 총의 상태 값을 변경할 수 있고 외부에서만 변경 불가능 하고 값을 불러오는 것만 가능하게끔.
  • gunHolder
    • 총을 쏘는 주체인 Plyaer Shooer 타입
    • 총의 주인이 누구인지 알려주는 역할을 한다.
  • bulletLineRenderer
    • LineRenderer 타입으로 총알 궤적을 그리기 위한 렌더러
  • 오디오 소스
    • gunAudioPlayer
      • AudioSource 타입으로 총 소리 재생기
    • shotClip
      • AudioClip 타입으로 발사 소리.
    • reloadClip
      • AudioClip 타입으로 재장전 소리
  • 파티클
    • muzzleFlashEffect
      • ParticleSystem 타입으로 총구 화염 파티클 효과
    • shellEjectEffect
      • ParticleSystem 타입으로 탄피 배출 효과 파티클 효과
  • Transform
    • fireTransform
      • Transform 타입으로 총알이 발사될 위치
    • leftHandMount
      • Transform 타입으로 왼손의 위치
  • 총 데이터
    • damage 👉 총의 데미지
    • fireDistance 👉 사정 거리. 총알 발사 체크를 할 거리.
  • 탄창 데이터
    • ammoRemain 👉 남은 전체! 총 탄약
    • magAmmo 👉 현재 탄창에 남아있는 탄약
    • magCapacity 👉 탄창 용량.
  • 총알 데이터
    • timeBetFire 👉 총알 발사 사이의 간격. 적으면 적을 수록 빠르게 연사가 가능하다.
    • reloadTime 👉 재장전 소요 시간
  • 탄 퍼짐
    • maxSpread 👉 0 ~ 10 범위에서 결정되며 총알이 퍼지는 최대 범위를 결정. 이 값이 크면 클 수록 총알이 넓게 흩어진다.
    • stability 👉 1 ~ 10 범위에서 결정되며 안정성, 즉 반동이 증가하는 속도. 높을 수록 반동이 증가하는 속도가 낮아져 안정성이 높아진다. 낮으면 낮을 수록 탄 퍼짐의 속도가 높아진다.
    • restoreFromRecoilSpeed 👉 0.01 ~ 3 범위에서 결정되며 연사를 멈춘 후 탄 퍼짐이 0 이 되기까지 걸리는 시간. 회복 속도.
    • currentSpread 👉 현재 탄 퍼짐의 정도값
    • currentSpreadVelocity 👉 현재 탄 퍼짐의 반경이 실시간 변화량. smoothDamp 연산에 사용할 것.
  • lastFireTime
    • 가장 최근에 발사가 이루어진 시점을 기록
  • excludeTarget
    • LayerMask 타입으로 총알을 쏴서는 안되는 대상을 거르기 위한 Layer
      • 총알을 맞으면 안되는 대상들이 여기에 할당될 것.


멤버 함수

private void Awake()

가장 먼저 실행되는 함수인만큼 Gun 게임 오브젝트로부터 필요한 컴포넌트들을 가져올 것

    private void Awake()
    {
        // 사용할 컴포넌트들의 참조를 가져오기
        gunAudioPlayer = GetComponent<AudioSource>();
        bulletLineRenderer = GetComponent<LineRenderer>();

        // 사용할 점을 두개로 변경
        bulletLineRenderer.positionCount = 2;
        // 라인 렌더러를 비활성화
        bulletLineRenderer.enabled = false;
    }
  • 사용할 컴포넌트들의 참조를 가져오기
  • 총알의 궤적을 그릴 선에 사용할 정점을 2 개로 한다.
    • LineRendererpositionCount
    • 선에 위치한 정점의 수를 Set 하고 Get 할 수 있는 프로퍼티.
    • 정점 1️⃣ 총구의 위치
    • 정점 2️⃣ 총알이 닿은 위치
  • 총알의 궤적 그리는 것을 비활성화 해두고 시작


public void Setup(PlayerShooter gunHolder)

총 주인인 PlayerShooter측에서 실행할 함수라 public. Gun 게임 오브젝트의 컴포넌트들을 대상으로 총에 대해 초기화를 실행하는 메서드.

  • 현재 총(Gun입장에서 나 자신)을 쥐고있는 shooter가 누구인지 알 수 있도록 초기화를 하는 처리가 들어감.
    public void Setup(PlayerShooter gunHolder)
    {
        this.gunHolder = gunHolder;
        excludeTarget = gunHolder.excludeTarget;
    }
  • 📜PlayerShooter 스크립트는 추후에 작성할 것!
  • 인수로 📜PlayerShooter 를 받는다.
  • this.gunHolder = gunHolder;
    • 인수로 입력 받은 gunHolder를 자기 자신의 gunHolder로 지정.
  • excludeTarget = gunHolder.excludeTarget;
    • 📜PlayerShooter 스크립트에도 Player총을 쏘지 않을 대상을 할당 할 LayerMask 타입의 excludeTarget 멤버 변수가 따로 들어가 있음.
      • 이를 총의(📜Gun) excludeTarget에 할당.
        • 📜PlayerShooter 에서 쏘지 않기로 결정된 대상들을 가져와서 할당


private void OnEnable()

📜Gun스크립트(컴포넌트)가 활성화되는 순간마다 실행되는 이벤트 함수

    private void OnEnable()
    {
        currentSpread = 0;
        
        magAmmo = magCapacity;  // 현재 탄창을 가득채우기
        state = State.Ready; // 총의 현재 상태를 총을 쏠 준비가 된 상태로 변경
        lastFireTime = 0; // 마지막으로 총을 쏜 시점을 초기화
    }
  • 총의 상태 초기화
    • 탄창 가득 채우기
    • 상태는 Ready
    • 마지막으로 총을 쏜 시점을 초기화
    • 탄퍼짐 0


private void OnDisable()

📜Gun스크립트(컴포넌트)가 비활성화되는 순간마다 실행되는 이벤트 함수

  • 현재 실행되고 있는 코루틴 함수들이 있다면 모조리 종료. StopAllCoroutines()
    private void OnDisable()
    {
        StopAllCoroutines();
    }


public bool Fire(Vector3 aimTarget)

Gun 클래스 외부에서 총을 사용해서 발사를 시도하는 메소드.

  • Shot 함수를 안전하게 감싸는 역할
    • Shot 함수 는 private으로서 실제로 발사를 처리하는 함수
  • 발사 성공하면 true 리턴 실패하면 false 리턴

  • 발사 하려면 현재 상태가 발사 가능한 상태인지를 체크 해야 함
    • 조건 1️⃣ Ready 상태여야 함
    • 조건 2️⃣ 현재 시간이 마지막 총 발사 시점에서 발사 간격을 더한 시간보다 많이 지나 있어야 함
  • 실제로 발사를 처리하는 함수인 Shot 을 실행시키려면 1️⃣발사 위치, 2️⃣발사 방향을 넘겨야 함
    • 발사 방향 fireDirection 구하기
      • 발사 방향 = 목표 지점 - 시작 지점 var fireDirection = aimTarget - fireTransform.position;
  • 마지막으로 총을 발사한 시점을 현재 시간으로 갱신
  • 실제 발사 처리 실행 👉 Shot 함수 실행
        if (state == State.Ready
            && Time.time >= lastFireTime + timeBetFire)
        {

            var fireDirection = aimTarget - fireTransform.position;

            lastFireTime = Time.time; // 마지막 총 발사 시점을 갱신
            
            Shot(fireTransform.position, fireDirection); // 실제 발사 처리 실행

            return true;
        }
  • 위와 같이만 구현하면
    • 총알은 무조건 조준한 방향으로만 날아가게 된다.
    • 총 연사와 그로 인한 반동에 의한 정확도 하락 & 탄퍼짐이 구현되지 않음
  • 👉 fireDirection에 적절한 오차 값을 더해서 탄 퍼짐을 구현해야 한다.
📜Utility.cs

image

using UnityEngine;
using UnityEngine.AI;

public static class Utility
{
    public static Vector3 GetRandomPointOnNavMesh(Vector3 center, float distance, int areaMask)
    {
        var randomPos = Random.insideUnitSphere * distance + center;
        
        NavMeshHit hit;
        
        NavMesh.SamplePosition(randomPos, out hit, distance, areaMask);
        
        return hit.position;
    }
    
    public static float GetRandomNormalDistribution(float mean, float standard)  // 정규 분포로 부터 랜덤값을 가져오는 함수 
    {
        var x1 = Random.Range(0f, 1f);
        var x2 = Random.Range(0f, 1f);
        return mean + standard * (Mathf.Sqrt(-2.0f * Mathf.Log(x1)) * Mathf.Sin(2.0f * Mathf.PI * x2));
    }
}
  • 표준 정규 분포도
    • 평균에 가까울 수록 출현 확률이 높아지고
    • 평균에서 멀어질 수록 출현 확률이 낮아진다.
    • 표준 편차가 클 수록 곡선이 완만해지며 평균에서 멀어져도 출현 확률이 높아진다.
탄퍼짐 구현하기 👉 정규분포도 사용

정규 분포도로부터 원래 조준했던 위치가 출현할 확률이 가장 높고, 원래 조준했던 위치에서 먼 위치일 수록 출현 확률이 낮은 랜덤 값을 생성한다.

  • 원래 조준했던 위치에 가깝게 나올 확률이 높은 랜덤값들을 생성할 수 있다.
    • 📜Utility.cs 에 정규분포로부터 랜덤값을 가져오는 함수 GetRandomNormalDistribution를 구현해놨고 이 함수를 사용하면 된다.
      • 인수 👉 평균 mean, 표준편차 standard
      • 정규 분포 공식을 사용함
        • 인수로 넘기는 mean이 0 이면 ‘표준 정규 분포’
          • 0 에 가까운 값이 가장 많이 출현하게 된다.
    • Utility.GetRandomNormalDistribution(0f, currentSpread);
      • 표준편차로 currentSpread를 넣어서 탄퍼짐이 심하면 심할 수록 mean인 0 에서 먼 값이 출현할 확률도 높아지게끔 한다.
    public bool Fire(Vector3 aimTarget)
    {
        // 현재 상태가 발사 가능한 상태
        // && 마지막 총 발사 시점에서 timeBetFire 이상의 시간이 지남
        if (state == State.Ready
            && Time.time >= lastFireTime + timeBetFire)
        {
            var xError = Utility.GetRandomNormalDistribution(0f, currentSpread);
            var yError = Utility.GetRandomNormalDistribution(0f, currentSpread);


            var fireDirection = aimTarget - fireTransform.position;

            fireDirection = Quaternion.AngleAxis(yError, Vector3.up) * fireDirection;
            fireDirection = Quaternion.AngleAxis(xError, Vector3.right) * fireDirection;

            currentSpread += 1f / stability;

            // 마지막 총 발사 시점을 갱신
            lastFireTime = Time.time;
            // 실제 발사 처리 실행
            Shot(fireTransform.position, fireDirection);

            return true;
        }

        return false;
    }

정규 분포에 의한 오차 생성. 탄퍼짐이 심하면 심할 수록 0 에서 부터 먼 값의 난수가 발생할 확률이 올라간다.

  • xError 👉 X 방향쪽의 랜덤 오차
  • yError 👉 Y 방향쪽의 랜덤 오차
  • 위에서 구한 랜덤 오차에 따라서 fireDirection 방향 회전시켜 움직여주기
    • 원래 총알이 향하던 방향인 fireDirection에서 오차만큼 회전시킨다.
      • 쿼터니언 X 벡터 : 벡터를 해당 쿼터니언값만큼 회전한 결과가 나온다.
      • y 방향 회전 👉 위쪽으로 yError 각도만큼 회전한 결과인 쿼터니언에 fireDirection을 곱한다.
        • fireDirection = Quaternion.AngleAxis(yError, Vector3.up) * fireDirection;
      • x 방향 회전 👉 오른쪽으로 xError 각도만큼 회전한 결과인 쿼터니언에 fireDirection을 곱한다.
        • fireDirection = Quaternion.AngleAxis(xError, Vector3.right) * fireDirection;
  • currentSpread += 1f / stability;
    • 한번 발사하고나면 탄퍼짐이 더 심해지도록 1/안정성 만큼 증가시킨다.
      • 안정성이 낮을 수록 탄퍼짐 정도가 빠르게 증가할 것이고
      • 안정성이 높을 수록 탄퍼짐 정도가 느리게 증가할 것이다.


private void Shot(Vector3 startPoint, Vector3 direction)

총 발사를 실제로 처리.

  • 1️⃣ 발사 시작 지점, 2️⃣ 총이 날아가는 방향을 인수로 받음
    // 실제 발사 처리
    private void Shot(Vector3 startPoint, Vector3 direction)
    {
        // 레이캐스트에 의한 충돌 정보를 저장하는 컨테이너
        RaycastHit hit;
        // 총알이 맞은 곳을 저장할 변수
        var hitPosition = Vector3.zero;

        // 레이캐스트(시작지점, 방향, 충돌 정보 컨테이너, 사정거리)
        if (Physics.Raycast(startPoint, direction, out hit, fireDistance, ~excludeTarget))
        {
            // 레이가 어떤 물체와 충돌한 경우

            // 충돌한 상대방으로부터 IDamageable 오브젝트를 가져오기 시도
            var target =
                hit.collider.GetComponent<IDamageable>();

            // 상대방으로 부터 IDamageable 오브젝트를 가져오는데 성공했다면
            if (target != null)
            {
                DamageMessage damageMessage;

                damageMessage.damager = gunHolder.gameObject;
                damageMessage.amount = damage;
                damageMessage.hitPoint = hit.point;
                damageMessage.hitNormal = hit.normal;

                // 상대방의 OnDamage 함수를 실행시켜서 상대방에게 데미지 주기
                target.ApplyDamage(damageMessage);
            }
            else
            {
                EffectManager.Instance.PlayHitEffect(hit.point, hit.normal, hit.transform);
            }
            // 레이가 충돌한 위치 저장
            hitPosition = hit.point;
        }
        else
        {
            // 레이가 다른 물체와 충돌하지 않았다면
            // 총알이 최대 사정거리까지 날아갔을때의 위치를 충돌 위치로 사용
            hitPosition = startPoint + direction * fireDistance;
        }

        // 발사 이펙트 재생 시작
        StartCoroutine(ShotEffect(hitPosition));

        // 남은 탄환의 수를 -1
        magAmmo--;
        if (magAmmo <= 0)
            // 탄창에 남은 탄약이 없다면, 총의 현재 상태를 Empty으로 갱신
            state = State.Empty;
    }
  • RaycastHit hit
    • 레이캐스트에 의한 충돌 정보를 저장하는 컨테이너
    • 레이캐스트로 총알이 물체에 맞았는지를 체크할 것
  • hitPosition
    • 총알이 맞은 위치 벡터
  • if (Physics.Raycast(startPoint, direction, out hit, fireDistance, ~excludeTarget))
    • 인수 👉 시작점, 방향, 충돌 정보를 저장할 hit, 레이 캐스트 사정거리, ~을 붙여 특정 대상의 레이어 마스크는 제외
      • 감지할 레이어에 ~을 붙이면 이 레이어가 붙은 것들은 레이 캐스트 처리에서 제외하라는 뜻
    • 1️⃣ 레이 캐스트 성공했다면 `hit`에 그 충돌 정보들을 저장한 후 true 리턴
      • 총의 데미지를 받을 수 있는 오브젝트 인지를 검사해야 함
        • var target = hit.collider.GetComponent<IDamageable>();
          • IDamageable 타입의 오브젝트이라면 데미지를 받을 수 있는 타입이라는뜻.
            • if (target != null)데미지를 받을 수 있는 타입이라면
              • 데미지 메세지 생성
                • DamageMessage damageMessage
                • 클래스가 아닌 구조체라서 new 사용할 필요 없음
              • 데미지를 주는 사람은 gunHolder.gameObject
              • 데미지 양은 damage
              • 타격이 가해진 위치는 hit.point
              • 타격이 가해진 방향은 hit.normal
              • target에 데미지 주기 target.ApplyDamage(damageMessage);
                • 이 ApplyDamage 함수에서 파티클 효과를 재생할 것이다
            • 데미지를 받을 수 없는 타입이라면
              • 직접 여기서 파티클 프리팹을 생성 하고 재생하는 📜EffectManager.cs 스크립트(싱글톤이라 클래스 이름으로 바로 접근 가능)의 함수 실행
      • 레이가 충돌한 위치 저장
        • hitPosition = hit.point;
    • 2️⃣ else 👉 레이 캐스트 충돌이 일어나지 않았다면
      • 총알이 최대 사정거리까지 날아갔을때의 위치를 충돌 위치로 사용.
        • 즉 뭐 별다른 충돌이 일어나지 않았다면 총알은 발사 지점으로부터 날아간 방향으로의 최대 사정 거리 위치에서 파티클 효과를 재생할 것이다.
        • hitPosition = startPoint + direction * fireDistance
  • 발사 이펙트 재생 시작
    • hitPositionStartCoroutine 함수의 인수로 넘겨 실행
      • 파티클 효과 + 총격 소리 재생 + 총알 궤적 그리기
  • 발사했으니 탄환 수 업뎃
    • 탄환 개수 1 감소
    • 탄환 개수가 0 에 도달했다면 총의 현재 상태를 Empty으로 갱신


private IEnumerator ShotEffect(Vector3 hitPosition)

총알이 맞은 지점을 인수로 받아 총알과 관련된 Effect 재생하는 코루틴 함수(지연시간을 들여서 이루어짐)

파티클 효과 + 총격 소리 재생 + 총알 궤적 그리기

    private IEnumerator ShotEffect(Vector3 hitPosition)
    {
        // 총구 화염 효과 재생
        muzzleFlashEffect.Play();
        // 탄피 배출 효과 재생
        shellEjectEffect.Play();

        // 총격 소리 재생
        gunAudioPlayer.PlayOneShot(shotClip);

        // 선의 시작점은 총구의 위치
        bulletLineRenderer.SetPosition(0, fireTransform.position);
        // 선의 끝점은 입력으로 들어온 충돌 위치
        bulletLineRenderer.SetPosition(1, hitPosition);
        // 라인 렌더러를 활성화하여 총알 궤적을 그린다
        bulletLineRenderer.enabled = true;

        // 0.03초 동안 잠시 처리를 대기
        yield return new WaitForSeconds(0.03f);

        // 라인 렌더러를 비활성화하여 총알 궤적을 지운다
        bulletLineRenderer.enabled = false;
    }
  • 발사시 두 파티클 효과를 재생시킨다.
  • 총격 소리를 1회 재생한다.
    • 발사 소리인 shotClip 클립 할당
      • AudioSourcePLayOneShot 함수
        • 그냥 Play()함수와의 차이점
          • 미리 clip이 오디오 소스에 할당이 되어 있어 이를 재생만 하면 되는 Plqy()와는 달리 PlayOneShot은 재생할 클립을 직접 인수로 넣어주어야 한다.
          • Plqy()는 소리 중첩이 되지 않기 때문에 직전 소리를 중지한 후 재생한다. 반면에 PlayOneShot은 소리 중첩이 된다. 발사 소리같이 연달아 재생되는 소리 같은 경우에는 Play() 함수는 부적합.
  • 선의 정점은 현재 2 개 (위에서 positionCount로 설정 했었음
    • 인덱스 0, 1 인 정점이 2 개
      • 첫번째 정점, 즉 선의 시작점이 되는 총구의 위치인 fireTransform.position로 설정
      • 두번째 정점, 즉 인수로 받은 총알의 충돌 위치인 hitPosition로 설정
  • 짧은 시간동안 탄알 궤도 그림 👉 총알궤도가 번쩍이는 효과
    • 잠시동안 총알 궤적을 그린 채로 다시 대기하고 다시 총알 궤적을 지운다.
      • 1️⃣총알 궤적 활성화 해서 그리고 2️⃣0.03초 대기 하고 3️⃣비활성화
        • 대기를 안해주면 활성화되자마자 바로 비활성화 되기 때문에 총알이 그려졌는지도 알 수가 없다. 그려지기도 전에 비활성화 되서..


public bool Reload()

Gun 클래스 외부에서 재장전을 시도하는 public 메소드

  • 재장전 성공시 true, 실패시 false 리턴
  • 실제 재장전 처리는 private한 코루틴 함수인 ReloadRoutine() 에서 할 것.
    public bool Reload()
    {
        if (state == State.Reloading ||
            ammoRemain <= 0 || magAmmo >= magCapacity)
            // 이미 재장전 중이거나, 남은 총알이 없거나
            // 탄창에 총알이 이미 가득한 경우 재장전 할수 없다
            return false;

        // 재장전 처리 시작
        StartCoroutine(ReloadRoutine());
        return true;
    }
  • 재장전이 가능한 경우를 검사해야 함
    • 재장전을 할 수 없는 경우
      1. 이미 재장전 중이거나
      2. 남은 총알이 없거나
      3. 탄창에 총알이 이미 가득한 경우
    • 재장전을 할 수 있는 상태일 때만 실제 재장전 처리를 하는 ReloadRoutine() 함수 실행


private IEnumerator ReloadRoutine()

실제 재장전 처리.

    private IEnumerator ReloadRoutine()
    {
        // 현재 상태를 재장전 중 상태로 전환
        state = State.Reloading;
        // 재장전 소리 재생
        gunAudioPlayer.PlayOneShot(reloadClip);

        // 재장전 소요 시간 만큼 처리를 쉬기
        yield return new WaitForSeconds(reloadTime);

        // 탄창에 채울 탄약을 계산한다
        var ammoToFill = magCapacity - magAmmo;

        // 탄창에 채워야할 탄약이 남은 탄약보다 많다면,
        // 채워야할 탄약 수를 남은 탄약 수에 맞춰 줄인다
        if (ammoRemain < ammoToFill) ammoToFill = ammoRemain;

        // 탄창을 채운다
        magAmmo += ammoToFill;
        // 남은 탄약에서, 탄창에 채운만큼 탄약을 뺸다
        ammoRemain -= ammoToFill;

        // 총의 현재 상태를 발사 준비된 상태로 변경
        state = State.Ready;
    }
  • 현재 상태를 재장전 중 상태로 전환 Reloading
    • 재장전 도중에 발사하거나 재장전 도중에 또 재장전 하는 것을 막기 위하여 상태 변경
  • 재장전 소리 재생
  • 재장전 소요 시간 만큼 대기 시간 가지기
  • 탄창에 채울 탄약을 계산한다
    • 탄창에 채워야할 탄약이 남은 탄약보다 많다면, 채워야할 탄약 수를 남은 탄약 수에 맞춰 줄인다
  • 탄창을 채운다.
  • 남은 탄약에서, 탄창에 채운만큼 탄약을 뺸다
  • 총의 현재 상태를 발사 준비된 상태로 변경 Ready


private void Update()

현재 탄 퍼짐(반동) 값을 상태에 따라 매 프레임마다 갱신

    private void Update()
    {
        currentSpread = Mathf.SmoothDamp(currentSpread, 0f, ref currentSpreadVelocity, 1f / restoreFromRecoilSpeed);
        currentSpread = Mathf.Clamp(currentSpread, 0f, maxSpread);
    }
  • currentSpread 증가는 Fire 함수에서 이루어 진다.
  • 가만히 있는 상태라면 `currentSpread`를 0 으로 서서히 스무스하게 초기화 해야 한다.
    • Mathf.Clamp 를 통하여 currentSpreadmaxSpread를 넘기지 못하도록 한다.
      • 아무리 반동이 누적이 되더라도 매 프레임마다 currentSpreadmaxSpread를 넘기지 못하도록 막아 준다.
    • Mathf.SmoothDamp 를 통하여 currentSpread✨증가하는 것과 별개로 매 프레임마다 스무스 하게 0 에 가까워 지게 만들어야 한다.
      • 지연 시간으로는 1 / restoreFromRecoilSpeed
        • restoreFromRecoilSpeed 가 높으면 높을 수록 지연 시간이 줄어들어서 빠르게 탄퍼짐이 0 에 도달 함.


Gun 컴포넌트 설정

image



🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기


Unity Lesson 1 카테고리 내 다른 글 보러가기

댓글 남기기