Development/Unity Engine

[Retro유니티] Gun 스크립트 - Coroutine을 사용한 Shot, Reload 구현

사이바 미도리 2024. 12. 19. 11:01

Gun과 관련된 메서드를 구현한다.

 

함수

Awake(): 사용할 컴포넌트를 가져온다.

OnEnable(): 총의 상태를 초기화한다. 컴포넌트가 활성화될 대마다 매번 실행된다.

Fire(): Shot()을 안전하게 감싼다.

Shot(): 실제 발사처리를 한다.

ShotEffect(): 발사효과를 재생하고, 탄 궤적을 그린다.

Reload(): 재장전을 한다. 실패시 false 반환.

ReloadRoutine(): 재장전이 가능할 때만 Reolad를 호출한다. IEnumerator 를 반환하는데, 이들은 코루틴 메서드이다.

 

참고) enum flag를 사용하면, 하나의 enum 변수가 2개 이상의 값을 가질 수 있다.

 

코루틴

번쩍이는 탄알궤적을 ShotEffect() 메서드가 구현해야하는데, 그러기 위해서는

1. 라인렌더러 ON 하여 선을 그리고

2. 바로 라인렌더러 OFF하여 선을 꺼야 한다.

1과 2 사이에, 매우 짧은 시간동안 처리를 일시정지해야한다.

라인렌더러를 ON-OFF 하는 처리 사이에 대기시간을 삽입해야 하는데, 이 때 코루틴이 사용된다.

 

유니티의 Coroutine 메서드는, 대기시간을 가질 수 있는 메서드이다.

IEnumerator 타입을 반환해야 하며, 처리가 일시대기할 곳에 yield 키워드를 명시해야 한다.

ex) yield return new WaitForSeconds(10f);

ex) yield return null; // 1frame 쉬기

코루틴이 처리를 쉬는 동안에는 다른 코드가 실행될 수 있다.

 

코루틴 메서드는 StartCoroutine 메서드로 실행해야 한다.

ex) StartCoroutine(SomeCoroutine());

ex) StartCoroutine("SomeCoroutine");

 

Arg에 문자열을 넣었을 경우, StopCoroutine 메서드를 사용해 도중에 종료가능하다.

ex) StopCoroutine("SomeCoroutine");

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

 

ShotEffect를 호출할 Fire 메서드를 완성하자.

Fire 메서드는 public으로, 외부로 노출된 메서드이다.

    public void Fire() {
        if(state == State.Ready && Time.time >= lastFireTime + timeBetFire) {
            lastFireTime = Time.time;
            Shot();
        }
    }

 

Fire 메서드가 마지막 발사지점을 갱신한다면, Shot() 메서드를 호출하여 실제로 총을 발사하자.

 

Raycast란?

탄알을 직선으로 쏘는 것은 보이지 않는 광선을 쏘는 것과 같다.

광선이 다른 collider와 충돌하는지 검사하는 처리이다.

광선을 Ray라고 부르고, Ray타입으로 Ray 정보만 따로 표현할수도있다.

 

만약 Ray가 Collider를 가진 Game Object와 충돌하면, RaycastHit 타입의 충돌정보가 생성된다.

생성된 RaycastHit 오브젝트를 살펴보면, 레이와 충돌한 게임 오브젝트, 충돌한 위치, 충돌한 표면의 방향 등을 알 수 있다.

 

    // Ray를 활용한 실제 발사 처리
    private void Shot() {
        RaycastHit hit;
        Vector3 hitPosition = Vector3.zero; // 레이가 맞은 위치

        // 인수: 시작지점, 방향, 충돌정보 컨테이너, 사정거리
        if(Physics.Raycast(fireTransform.position, fireTransform.forward, out hit, fireDistance)) { // 레이가 다른 물체와 충돌했다면
            var target = hit.collider.GetComponent<IDamageable>();
            if(target != null) {
                target.OnDamage(damage, hit.point, hit.normal);
            }
            hitPosition = hit.point;
        } else {
            hitPosition = fireTransform.position + fireTransform.forward * fireDistance;
        }
        StartCoroutine(ShotEffect(hitPosition));
        magAmmo--;
        if (magAmmo <= 0) {
            state = State.Empty; // 탄창에 남은 탄알이 없다면 Empty 상태로 전환
        }
    }

 

 

인데, 여기서 레이캐스트 함수를 보자.

Physics.Raycast(fireTransform.position, fireTransform.forward, out hit, fireDistance)

Vector3 origin

Vector3 direction

RaycastHit hitInfo

float maxDistance

 

hitInfo는 out 키워드가 있음에 유의하자.

Raycast 함수 자체도 true/false 반환이 있지만, 우린 더 자세한 충돌정보가 필요하다.

 

out 키워드는, 메서드가 return 이외의 방법으로 추가정보를 리턴할 수 있게 해준다.

C의 call by reference 와 유사하다.

 

아무튼, 반환된 hit.collider는 충돌한 게임오브젝트의 Collider 컴포넌트이다.

            var target = hit.collider.GetComponent<IDamageable>();

충돌한 상대방 GameObject로부터 IDamageable 타입 컴포넌트를 가져오겠다는 뜻이다.

 

마찬가지로, Reload도 Coroutine으로 구현하자.

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

        int ammoToFill = magCapacity - magAmmo; // 채워야 할 탄약 계산
        if(ammoRemain < ammoToFill) {
            ammoToFill = ammoRemain;
        }

        magAmmo += ammoToFill; // 탄창 채우기
        ammoRemain -= ammoToFill; // 남은 탄약에서 탄창에 넣은만큼 빼기

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

 

Gun.cs 완성

완성된 Gun.cs 스크립트는 아래와 같다.

using System.Collections;
using UnityEngine;

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

    public State state { get; private set; } // 현재 총의 상태

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

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

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

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

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

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


    public float timeBetFire = 0.12f; // 총알 발사 간격
    public float reloadTime = 1.8f; // 재장전 소요 시간
    private float lastFireTime; // 총을 마지막으로 발사한 시점


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

        bulletLineRenderer.positionCount = 2; // 사용할 점은 2개. 총구위치와 총알이 맞은 위치
        bulletLineRenderer.enabled = false; // 인스펙터 렌더러 컴포넌트를 비활성화했지만, 코드에서도 확실하게 비활성화
    }

    private void OnEnable() {
        // 총 상태 초기화
        magAmmo = magCapacity; // 탄 최대용량으로 채우기
        state = State.Ready; // 발사준비상태
        lastFireTime = 0;
    }

    // 발사 시도
    public void Fire() {
        if(state == State.Ready && Time.time >= lastFireTime + timeBetFire) {
            lastFireTime = Time.time;
            Shot();
        }
    }

    // Ray를 활용한 실제 발사 처리
    private void Shot() {
        RaycastHit hit;
        Vector3 hitPosition = Vector3.zero; // 레이가 맞은 위치

        // 인수: 시작지점, 방향, 충돌정보 컨테이너, 사정거리
        if(Physics.Raycast(fireTransform.position, fireTransform.forward, out hit, fireDistance)) { // 레이가 다른 물체와 충돌했다면
            var target = hit.collider.GetComponent<IDamageable>();
            if(target != null) {
                target.OnDamage(damage, hit.point, hit.normal);
            }
            hitPosition = hit.point;
        } else {
            hitPosition = fireTransform.position + fireTransform.forward * fireDistance;
        }
        StartCoroutine(ShotEffect(hitPosition));
        magAmmo--;
        if (magAmmo <= 0) {
            state = State.Empty; // 탄창에 남은 탄알이 없다면 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);

        int ammoToFill = magCapacity - magAmmo; // 채워야 할 탄약 계산
        if(ammoRemain < ammoToFill) {
            ammoToFill = ammoRemain;
        }

        magAmmo += ammoToFill; // 탄창 채우기
        ammoRemain -= ammoToFill; // 남은 탄약에서 탄창에 넣은만큼 빼기

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

 

Gun 컴포넌트 설정

채워넣자.

 

이제 Shooter를 구현할 것이다.