Gun의 확장성의 문제

스크립터블 오브젝트가 필요한 이유를 이해하기 위해 Gun의 다양성 구현을 생각해 봅시다.

총마다 다음과 같은 특성이 있습니다.

총사운드, 공격력, 탄창용량, 연사력과 재장전시간등

이런 다른 특성을 가진 Gun을 관리하기 위해 수많은 프리팹을 만들어야 하고 런타임에 수치 데이터 변경이 어렵습니다. 이런 문제점을 해결하기 위해 Gun클래스에서 데이터에 해당하는 부분을 개별 클래스 GunData로 추출하여 일부 해결할 수 있습니다.

스크립터블 오브젝트

GunData오브젝트가 유니티 에디터에서 편집할 수 있는 형태로 존재해야 합니다. 씬위의 게임오브젝트가 아닌 형태로 존재해야 합니다. 씬위의 게임오브젝트는 해당 씬을 열어야 편집할 수 있기 대문입니다.

GunData는 단순한 데이터 컨테이너입니다. 따라서 MonoBehaviour를 상속받아서는 안됩니다. 스크립터블 오브젝트는 다음과 같은 경우 유용합니다.

여러 오브젝트가 공유하여 사용할 데이터를 에셋 형태로 분리

데이터를 유니티 인스펙터 창에서 편집 가능한 형태로 관리

GunData구현하기

Gundata 스크립트 열기

using UnityEngine;

public class GunData
{
    public AudioClip shotClip; // 발사 소리
    public AudioClip reloadClip; // 재장전 소리

    public float damage = 25; // 공격력

    public int startAmmoRemain = 100; // 처음에 주어질 전체 탄약
    public int magCapacity = 25; // 탄창 용량

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

현재 GunData는 단순 클래스입니다. 이걸 스트립터블 오브젝트 타입상속하고 CreateAssetMenu()를 이용 에셋 생성메뉴에 추가하겠습니다.

using UnityEngine;
[CreateAssetMenu(menuName = "Scriptable/GunData",fileName = "Gun Data")]
public class GunData : ScriptableObject
{ 생략 }

GunData를 선택후 인스펙터뷰에서 Shot Clip, Reload Clip필드의 소스를 선택해 오디오클립을 지정해줍니다.

Audios.zip
3.71MB

1. Gun 스크립트 변수들

총의 상태를 나타내는 state변수가 있습니다. Gun의 상태는 Ready, Empty, Reloading중 하나입니다.

Gun 스크립트

이제 Gun스크립트를 Gun게임 오브젝트에 추가하고 스크립트를 완성합니다. 하이라키의 FirePosition, MuzzleFlashEffect와 ShellEjectEffect를 끌어다 연결해줍니다.

 

Gun게임 오브젝트 준비가 끝났습니다. 이제 총의 동작을 구현하는 Gun 스크립트를 Gun게임 오브젝트에 추가하고 스크립트를 분석해 보겠습니다.

Gun의 메서드

Awake() : 사용할 컴포넌트를 가져오고

OnEnable() : 총의 상태를 초기화

Fire() : 총을 발사를 시도

Shot() : 발사가능 상태에서만 발사, 안전하게 감싸는 껍떼기

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

Reload() : r을 누르면 재장전이 가능한지 체크하고 다음 루팅을 Call합니다. 성공하면 true를 리턴

ReloadRoutine(): 실제 장전하는 곳

ShotEffect()와 ReloadRoutine()은 IEnumerator라는 코루틴메서드 타입을 반환

Gun의 필드

이제 Gun의 스크립트의 변수를 살펴봅시다.

public enum State {
        Ready, // 발사 준비됨
        Empty, // 탄창이 빔
        Reloading // 재장전 중
    }
public State state { get; private set; } // 현재 총의 상태 외부에서 변경 불가능

public Transform fireTransform; // 총알이 발사될 위치와 방향을 나타내는 변수 Fire Position과 연결

public ParticleSystem muzzleFlashEffect; // 총구 화염 효과

public ParticleSystem shellEjectEffect; // 탄피 배출 효과

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

 

다음은 총소리를 위한 변수가 있습니다.

private AudioSource gunAudioPlayer; // 총 소리 재생기

음원및 총의 데이터는 Gundata로 따로 만들어 놓고 연결해 씁니다.

인스펙트창에서 연결해 줍니다.

public GunData gunData; // 총의 현재 데이터

 

 Awake()메서드

Gun 스크립트의 Awake()함수를 완성합니다.

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

    bulletLineRenderer.positionCount = 2; // 렌더러에서 사용할 점을 두개로 변경
    bulletLineRenderer.enabled = false; // 라인 렌더러를 비활성화, 스크립터에서 총을 쏠때만 나타나게 함
}

3. OnEnable()메써드

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

4. 코루틴

발사효과를 재생하는 ShotEffect()메서드는 시각효과를 재생하고 탄알 궤적을 그리기위해 라인렌더러를 켜서 선을 그린다음 라인렌더러를 꺼야 합니다. 이때 매우 짧은 시간동안 처리를 일시정지합니다. 따라서 라인렌더러를 끄고 켜는 대기시간이 필요합니다. 이때 코루틴이 사용됩니다.

유니티의 코루틴은 대기시간을 가질 수 있는 메서드입니다. 유니티에서 코루틴메서드는 IEnumerator타입을 반환해야하며, 처리가 일시 대기할 곳에 yield키워드를 명시해야합니다.

yield return new WaitForSecond(float 초); // 초단위로 쉬기

yield return null; // 한프레임 쉬기

코루틴은 StartCoroutine()메서드로 실행됩니다. 2가지 방법이 있습니다.

StartCoroutine(메서드());

StartCoroutine("메서드이름"); // 꼭 StopCroutine("메서드이름")으로 종료시켜야합니다.

5. ShotEffect()메서드

나중에 Shot()에서 불리워집니다.

private IEnumerator ShotEffect(Vector3 hitPosition) {
    muzzleFlashEffect.Play();  // 총구 화염 효과 재생
    shellEjectEffect.Play(); // 탄피 배출 효과 재생
    gunAudioPlayer.PlayOneShot(gunData.shotClip);  // 총격 소리 재생
    bulletLineRenderer.SetPosition(0, fireTransform.position); // 선의 시작점은 총구의 위치
    bulletLineRenderer.SetPosition(1, hitPosition);  // 선의 끝점은 입력으로 들어온 충돌 위치
    bulletLineRenderer.enabled = true;  // 라인 렌더러를 활성화하여 총알 궤적을 그린다
    yield return new WaitForSeconds(0.03f);   // 0.03초 동안 잠시 처리를 대기
    bulletLineRenderer.enabled = false;  // 라인 렌더러를 비활성화하여 총알 궤적을 지운다
}
6. Fire()메서드

State.Ready를 검사해 총을 발사 가능한 상태에서만 Shot()메서드가 실행되도록 합니다.

7. 레이캐스트

LineRenderer를 이용 광선을 그리기위해 총이 충돌하는 끝점을 알아야 합니다 .

충돌 처리를 위해 Ray라는 검사용 광선을 사용하며 충돌하면 RaycastHit 타입의 충돌정보가 생성됩니다.

충돌한 게임오브젝트 위치,표면의 방향등의 정보가 들어 있습니다.

8. Shot()메서드

레이캐스트를 이용해 총을 쏘고 총에 맞은 오브젝트를 찾아 데미지를 주는 Shot()메서드입니다.

RaycastHit hit; // 레이캐스트에 의한 충돌 정보를 저장하는 컨테이너

Physics.Raycast(시작지점, 방향, out hit , 사정거리)

- 리턴값이 bool Raycast의 이외에 더 자세한 정보를 얻기위해 hit앞에 out 키워드를 사용하고 있습니다.

hit에 채워지는 정보는 origin(총구위치), direction(총구앞쪽방향), hitInfo(충돌정보), maxDistance(사정거리)

// 레이가 어떤 물체와 충돌한 경우 충돌한 상대방으로부터 IDamageable 오브젝트를 가져오기 시도

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

탄알이 맞은 위치는 hit.point, 표면의 방향은 hit.normal을 통해 알수 있습니다.

9.Reload()메서드

재장전을 시도하는 코루틴 메서드입니다.

yield return new WaitForSeconds()를 사용 재장정할 동안 state를 Reloading으로 고정시켜

다른 기능을 동작하지 않게 합니다.

10. Gun 컴포넌트 설정

하이라키의 Fire Position 게임오브젝트및 MuzzleFlashEffect, ShellEjectEffect게임오브젝트를 연결합니다.

GunData는 Scriptable Data에 있습니다. (이건 개정판 버전입니다)

Gun프리팹 갱신하기

인스펙터 상단 Overrides>Apply All 클릭

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 GunData gunData; // 총의 현재 데이터
    
    private float fireDistance = 50f; // 사정거리

    public int ammoRemain = 100; // 남은 전체 탄약
    public int magAmmo; // 현재 탄창에 남아있는 탄약
    
    private float lastFireTime; // 총을 마지막으로 발사한 시점
    
    private void Awake() {
        // 사용할 컴포넌트들의 참조를 가져오기
        gunAudioPlayer = GetComponent<AudioSource>();
        bulletLineRenderer = GetComponent<LineRenderer>();

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

    private void OnEnable() {
        // 전체 예비 탄약 양을 초기화
        ammoRemain = gunData.startAmmoRemain;
        // 현재 탄창을 가득채우기
        magAmmo = gunData.magCapacity;

        // 총의 현재 상태를 총을 쏠 준비가 된 상태로 변경
        state = State.Ready;
        // 마지막으로 총을 쏜 시점을 초기화
        lastFireTime = 0;
    }

    // 발사 시도
    public void Fire() {
        // 현재 상태가 발사 가능한 상태
        // && 마지막 총 발사 시점에서 timeBetFire 이상의 시간이 지남
        if (state == State.Ready && Time.time >= lastFireTime + gunData.timeBetFire)
        {
            // 마지막 총 발사 시점을 갱신
            lastFireTime = Time.time;
            // 실제 발사 처리 실행
            Shot();
        }
    }

    // 실제 발사 처리
    private void Shot() {
        RaycastHit hit; // 레이캐스트에 의한 충돌 정보를 저장하는 컨테이너
        Vector3 hitPosition = Vector3.zero; // 총알이 맞은 곳을 저장할 변수
        // 레이캐스트(시작지점, 방향, 충돌 정보 컨테이너, 사정거리)
        if (Physics.Raycast(fireTransform.position,
            fireTransform.forward, out hit, fireDistance)){
            // 레이가 어떤 물체와 충돌한 경우
            // 충돌한 상대방으로부터 IDamageable 오브젝트를 가져오기 시도
            IDamageable target =
                hit.collider.GetComponent<IDamageable>();

            // 상대방으로 부터 IDamageable 오브젝트를 가져오는데 성공했다면
            if (target != null){
                // 상대방의 OnDamage 함수를 실행시켜서 상대방에게 데미지 주기
                target.OnDamage(gunData.damage, hit.point, hit.normal);
            }

            // 레이가 충돌한 위치 저장
            hitPosition = hit.point;
        } else { // 레이가 다른 물체와 충돌하지 않았다면
            // 총알이 최대 사정거리까지 날아갔을때의 위치를 충돌 위치로 사용
            hitPosition = fireTransform.position +
                          fireTransform.forward * fireDistance;
        }

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

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

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

        // 총격 소리 재생
        gunAudioPlayer.PlayOneShot(gunData.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 >= gunData.magCapacity)
        {
            // 이미 재장전 중이거나, 남은 총알이 없거나
            // 탄창에 총알이 이미 가득한 경우 재장전 할수 없다
            return false;
        }

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

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

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

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

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

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

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

 

'유니티좀비게임 > 총과슈터' 카테고리의 다른 글

Player Shooter 스크립  (0) 2023.04.28
총 게임오브젝트  (0) 2023.04.28
인터페이스  (0) 2023.04.28

+ Recent posts