Ace Combat Zero: 유니티로 구현하기 #17 : 사운드

Lunetis·2021년 6월 20일
0

Ace Combat Zero

목록 보기
18/27
post-thumbnail


지금까지 이 프로젝트를 진행하면서 음향기기는 전혀 필요가 없었습니다.
게임 플레이가 가능하도록 알고리즘을 짜는 게 주 목표였기 때문입니다.

이제 플레이어의 귀를 즐겁게 해줄 사운드를 넣어줄 차례입니다.
스피커, 헤드폰, 이어폰, 뭐든지 좋으니 어서 챙겨오자고요.



개요

제가 만들고자 하는 게임은 에이스 컴뱃 제로지만,
UI와 효과음은 에이스 컴뱃 7를 기준으로 하려고 합니다.


에이스 컴뱃 제로는 2006년에, 에이스 컴뱃 7은 2019년에 발매된 게임입니다.

개인적으로는 게임성만 따졌을 때 둘이 엎치락뒤치락할 지 모르지만,
그래픽과 사운드는 2006년 리소스보다는 2019년 리소스가 더 낫다고 생각하거든요.


그래픽은 비교가 안 되는 수준이니 넘어가고, 사운드 측면만 이야기를 해보자면...

음향 디자이너들은 게임에 어울리도록 효과음을 만들고, 어떤 소리를 더 강조할 지 결정하고, 그렇게 만들어진 소리들이 게임 내에서 뒤섞여도 각각 명확하게 잘 들리도록 조정하는 일을 합니다.

게임 음향 디자인 분야도 끊임없이 발전하고 있는 만큼, 2019년에 만들어진 음악과 효과음들은 2006년보다 더 정교하게 만들어졌을 것입니다.

"최근에 만들어진 사운드 리소스를 사용하는 것이 더 나은 경험을 제공해주지 않을까" 라는 생각이죠.


한 가지 예시로, 에이스 컴뱃 제로에이스 컴뱃 7미사일 발사 사운드를 비교해봅시다.



에이스 컴뱃 제로는 미사일의 엔진 출력음에 집중한 것 같고, 소리가 약간 뭉개지는 듯한 느낌이 듭니다.
개인적으로는 이게 미사일 발사 소리인지, 그냥 뭔가 폭발하는 소리인지 분간하기가 애매하네요.

반면 에이스 컴뱃 7은 발사할 때의 순간적인 출력음에 집중한 것 같고, 효과음이 더 명확하게 들립니다.


둘 중 하나를 골라서 미사일 발사 소리로 사용하라고 하면,
아마 대부분의 사람들은 아래에 있는 리소스를 가져다 사용할 것 같습니다.


위에 걸 사용하시겠다고요?

그 분들께는 기대치에 못 미치는 결과물을 만들어드릴 것 같아 미리 사과의 말씀을 드립니다.



효과음 가져오기

아무튼 에이스 컴뱃 7의 효과음들을 구해봅시다.

원작 재현을 충실하게 하기 위해 실제 게임에서 쓰였던 효과음들을 사용하고 있으며,
저작권으로 인한 문제를 최소화하기 위해 비영리/교육 목적으로만 사용하도록 하겠습니다.

레딧에 있는 데이터 마이너들이 효과음을 추출해냈더군요.

받아보니까 어떤 건 바로 써도 될 법한 상태이고, 어떤 건 약간의 조정을 거쳐야 했습니다.


엔진 소리를 아무거나 가져와봤는데, 이유는 모르지만 페이드아웃이 적용되어 있었습니다.

어차피 루프되는 효과음이라 일부분만 다 잘라놓겠습니다.

효과음 조정에 사용한 프로그램은 Audacity 입니다.


이렇게 효과음들을 준비해봤습니다.

비행기

  • 애프터버너
  • 엔진 (외부)
  • 엔진 (콕핏)

UI

  • 시간 제한
  • 미사일 없음 경고
  • 미사일 경고 (음성)
  • 미사일 경고 (경보음)
  • 실속 경고
  • 미사일 준비중
  • 미션 성공
  • 미션 실패
  • 미사일 락온중
  • 미사일 락온 완료
  • 미사일로 무기 전환
  • 특수무기로 무기 전환

무기류

  • 다양한 폭발 효과음
  • 기총 발사
  • 미사일 발사
  • 미사일 로켓 엔진 효과음
  • 기총 피격
  • 레이저

UI 효과음 재생하기

먼저 UI 효과음 출력부터 해봅시다.

버튼을 누를 때마다 효과음을 재생하거나, 특정 상황에서만 효과음을 재생하는 등 여러가지 조건이 있을 수 있습니다.


간단한 경고음 추가

몇몇 경고음은 그냥 간편하게 추가할 수 있습니다.

예를 들어서 실속 상태를 알려줄 때는 "STALLING" 이라는 라벨이 깜빡이지 않고 계속 뜨게 되죠.

이 때 "Stall Warning"이라는 경고음이 반복적으로 출력하도록 만들기 위해서...

아무런 코드도 작성하지 않고 그냥 Audio Source를 추가해서 AudioClip을 넣어주면 됩니다.
Play On Awake, Loop를 체크하면 활성화될 때마다 알아서 출력됩니다.


무기 교체

UIController.cs

[Header("Sounds")]
[SerializeField]
AudioClip spwChangeAudioClip;
[SerializeField]
AudioClip mslChangeAudioClip;

AudioSource audioSource;

public void SwitchWeapon(WeaponSlot[] weaponSlots, bool useSpecialWeapon, Missile missile, bool playAudio = true)
{
    mslIndicator.SetActive(!useSpecialWeapon);
    spwIndicator.SetActive(useSpecialWeapon);

    // Justify that weaponSlots contains 2 slots
    leftMslCooldownImage.SetWeaponData(weaponSlots[0], missile.missileFrameSprite, missile.missileFillSprite);
    rightMslCooldownImage.SetWeaponData(weaponSlots[1], missile.missileFrameSprite, missile.missileFillSprite);

    if(playAudio == true)
    {
        AudioClip audioClip = (useSpecialWeapon == true) ? spwChangeAudioClip : mslChangeAudioClip;
        audioSource.PlayOneShot(audioClip);
    }
}

void Awake()
{
    audioSource = GetComponent<AudioSource>();
}

대표적으로 무기를 교체할 때마다 소리를 다르게 들려줘야 하는 부분을 구현하는 부분을 보면,

우선 Awake()에서 객체에 붙여진 AudioSource 컴포넌트를 가져오고,
무기를 교체하는 함수가 불릴 때마다 어떤 무기로 교체하는 지 확인해서
그에 맞는 AudioClipAudioSource가 실행하게 만들면 됩니다.


효과음을 들려드리고 싶은데, 이제 그렇게 애용하던 gif 파일로는 표현할 수가 없네요.

아무튼, 추가한 다음에 이렇게 AudioClip을 할당시켜주면 됩니다.


무기 준비중/무기 없음 효과음

WeaponController.cs

void LaunchMissile(ref int weaponCnt, ref ObjectPool objectPool, ref WeaponSlot[] weaponSlots)
{
    WeaponSlot availableWeaponSlot = GetAvailableWeaponSlot(ref weaponSlots);
    
    // Ammunition Zero!
    if(weaponCnt <= 0)
    {
        if(audioSource.isPlaying == false)
        {
            audioSource.PlayOneShot(ammunationZeroClip);
        }
        return;
    }
    // Not available : Beep sound
    if(availableWeaponSlot == null)
    {
        audioSource.PlayOneShot(cooldownClip);
        return;
    }
    ...
}

어떤 경우에는 소리를 재생하고 있는 중에 중복으로 재생을 허용하지 않아야 할 때도 있습니다.
그럴 때는 audioSource.isPlaying 값을 체크해서 false일 때만 재생시키게 하면 됩니다.



락온

락온할 때 재생되는 효과음은 두 가지로 나뉩니다.

  1. 락온 중일 때

효과음이 "삐 삐 삐 삐..." 거리면서 재생됩니다.
미리 끊어둔 효과음을 반복해서 재생하면 됩니다.

  1. 락온됐을 때

효과음이 "삐----------" 처럼 끊김없이 재생됩니다.
루프 효과음을 반복해서 재생하면 됩니다.


TargetLock.cs

enum LockStatus
{
    NONE,
    LOCKING,
    LOCKED
}

LockStatus lockStatus;

// Set image invisible
void ResetLock()
{
    SetLockAudio(LockStatus.NONE);
    ...
}

void CheckTargetLock()
{
    ResetLock(); 을 부르는 코드들
    ...

    // Locking...
    else
    {
        ...
        // Locked!
        if(lockProgress >= targetAngle)
        {
            SetLockAudio(LockStatus.LOCKED);
            ...
        }
        // Still Locking...
        else
        {
            SetLockAudio(LockStatus.LOCKING);
            ...
        }
        ...
    }
}

void SetLockAudio(LockStatus newStatus)
{
    if(lockStatus == newStatus) return;
    lockStatus = newStatus;
    
    switch(lockStatus)
    {
        case LockStatus.NONE:
            audioSource.Stop();
            break;
        
        case LockStatus.LOCKING:
            audioSource.clip = lockingClip;
            audioSource.Play();
            break;

        case LockStatus.LOCKED:
            audioSource.clip = lockedClip;
            audioSource.Play();
            break;
    }
}

일단 락온이 되고 있지 않은지, 락온 중인지, 락온된 상태인지를 구분할 수 있어야 합니다.

각각의 상태를 나타내는 값을 enum LockStatus로 선언해두고,
CheckTargetLock()에서 상태를 검사하면서 SetLockAudio()를 호출합니다.

SetLockAudio()에서는 상태가 바뀔 때마다 효과음을 멈추거나, 효과음을 교체해서 재생하는 기능을 합니다.

락온 중인 상태에서는 "삐 삐 삐 삐..." 하는 소리가 나고,

락온되면 "삐------" 소리가 나게 됩니다.

(상상 속에서 들어주세요.)



미사일 경고음

미사일 경고는 락온되는 상태와 미사일이 날아오고 있는 상태로 나뉩니다.

락온되는 상태에서는 심각하지 않게 들리는(?) 짧은 경고음이 주기적으로 재생되고,

미사일이 날아오는 상태에서는 "Missile"이라는 음성과 함께 심각하게 들리는(??) 짧은 경고음이 재생됩니다.
그리고 미사일이 일정 거리 미만으로 가까워지면 경고음의 주기가 짧아집니다.

AlertUIController.cs

[Header("Sounds")]
[SerializeField]
float voiceAlertRepeatTime = 1.5f;
[SerializeField]
float missileCautionAlertRepeatTime = 1.0f;
[SerializeField]
float missileWarningAlertRepeatTime = 0.2f;
[SerializeField]
float missileEmergencyAlertRepeatTime = 0.1f;

[SerializeField]
AudioClip warningBeepAlertClip;
[SerializeField]
AudioClip missileBeepAlertClip;
[SerializeField]
AudioClip missileVoiceAlertClip;
[SerializeField]
AudioClip stallVoiceAlertClip;

void ShowAttackAlertUI()
{
    ...

    SetAlertAudio();
}


void SetAlertAudio()
{
    switch(prevWarningStatus)
    {
        case PlayerAircraft.WarningStatus.MISSILE_ALERT_EMERGENCY:
            CancelInvoke("PlayMissileBeepAudio");
            CancelInvoke("PlayWarningBeepAudio");
            InvokeRepeating("PlayMissileBeepAudio", 0, missileWarningAlertRepeatTime);
                if(isPlayingVoiceAlert == false) InvokeRepeating("PlayMissileVoiceAudio", 0, voiceAlertRepeatTime);
            break;
            
        case PlayerAircraft.WarningStatus.MISSILE_ALERT:
            CancelInvoke("PlayMissileBeepAudio");
            CancelInvoke("PlayWarningBeepAudio");
            InvokeRepeating("PlayMissileBeepAudio", 0, missileCautionAlertRepeatTime);
            if(isPlayingVoiceAlert == false) InvokeRepeating("PlayMissileVoiceAudio", 0, voiceAlertRepeatTime);
            break;
            
        case PlayerAircraft.WarningStatus.WARNING:
            CancelInvoke("PlayMissileBeepAudio");
            CancelInvoke("PlayMissileVoiceAudio");
            InvokeRepeating("PlayWarningBeepAudio", missileCautionAlertRepeatTime * 0.2f, missileCautionAlertRepeatTime);
            break;
            
        case PlayerAircraft.WarningStatus.NONE:
            CancelInvoke("PlayMissileBeepAudio");
            CancelInvoke("PlayWarningBeepAudio");
            CancelInvoke("PlayMissileVoiceAudio");
            isPlayingVoiceAlert = false;
            break;
    }
}

void PlayWarningBeepAudio()
{
    alertAudioSource.PlayOneShot(warningBeepAlertClip);
}

void PlayMissileBeepAudio()
{
    alertAudioSource.PlayOneShot(missileBeepAlertClip);
}

void PlayMissileVoiceAudio()
{
    isPlayingVoiceAlert = true;
    voiceAudioSource.PlayOneShot(missileVoiceAlertClip);
}

각각의 경고음을 1회 재생하는 함수를 작성하고 (Play...Audio())
이걸 주기적으로 호출할 때는 InvokeRepeating(...)을 사용합니다.

각 상태에 최초로 진입할 때마다 재생하는 효과음을 바꾸거나 멈추게 하기 위해 CancelInvokeInvokeRepeating을 사용합니다.

재생 주기는 [SerializeField] float ...Time 으로 설정합니다.


*미사일 경고는 사실 3단계로 나뉘어야 하지만 일단 2단계로 설정한 상태입니다.



엔진 소리

여기서부터는 살짝 복잡한 처리가 필요합니다.

어떤 소리는 1인칭(콕핏 시점)일 때와 3인칭(외부 시점)일 때의 소리가 달라야 합니다.
밖에서 듣는 소리와 안에서 듣는 소리의 차이를 구현하기 위함이죠.

지금까지는 UI와 관련된 소리였기 때문에 별다른 처리를 하지 않았지만,

이제부터는 외부 환경과 관련된 소리이므로 시점에 따른 소리 변화에 신경을 써줘야 합니다.


일단 엔진 소리에 사용되는 두 가지의 효과음이 있습니다.

  1. 항상 가동되는 제트 엔진 소리
  2. 가속 시에만 재생되는 애프터버너 소리

두 소리는 내부에 있을 때 조금 더 날카롭게 들려야 합니다.


소리 교체

"1. 항상 가동되는 제트 엔진 소리"는 소리 교체 방식으로 진행해보겠습니다.

이 방법은 내부에서의 엔진 소리와 외부에서의 엔진 소리 효과음이 따로 나뉘어져 있을 때 사용합니다.

CameraController.cs

[Header("Sounds")]
[SerializeField]
AudioClip engineInClip;
[SerializeField]
AudioClip engineOutClip;

[SerializeField]
AudioSource engineAudioSource;

public void ChangeCameraView(InputAction.CallbackContext context)
{
    if(context.action.phase == InputActionPhase.Performed)
    {
        ...
        SetEngineAudio();
    }
}

void SetEngineAudio()
{
    switch((CameraIndex)cameraViewIndex)
    {
        case CameraIndex.FirstView:
            engineAudioSource.clip = engineInClip;
            engineAudioSource.Play();
            break;
            
        case CameraIndex.ThirdView:
            engineAudioSource.clip = engineOutClip;
            engineAudioSource.Play();
            break;

        default:
            break;
    }
}

그냥 단순하게 시점이 바뀌어서 소리를 바꿔야 할 때마다 교체해주면 됩니다.


Inspector 창에서 오디오 관련 설정을 해주면 끝이고요.



오디오 믹서

"2. 가속 시에만 재생되는 애프터버너 소리"는 효과음이 하나밖에 없습니다.

교체할 효과음이 없는데, 어떻게 할까요?


일단 애프터버너 소리는 제트엔진에서 제어하도록 만들려고 합니다.
(제트엔진 부분을 구현하는 과정은 따로 포스트에서 다루지 않았습니다.)

여기서 오디오 효과를 추가할 수 있습니다.

Add Component에서 Audio Low Pass Filter를 선택합니다.

Lowpass는 설정한 주파수보다 높은 음역대를 지워버리는 역할을 합니다.



비유하자면 먹먹한 소리, 물 속에 들어가있는 듯한 소리를 만들죠.

아무튼 컴포넌트를 추가했으면 이렇게 Cutoff Frequency (주파수) 와 Resonance Q (Lowpass Resonance Quality Factor) 라는 값이 있습니다.

아래 값은 무슨 공명값을 제어하는 데에 쓰이는데, 저도 정확히는 알지 못하므로 일단 위에 있는 주파수 값을 건드리겠습니다.


JetEngineController.cs

public class JetEngineController : MonoBehaviour
{
    ParticleSystem.MainModule particleMainModule;
    float initAlpha;
    float throttleAmount;
    Color particleColor;

    [Header("Common")]
    [SerializeField]
    float accelLerpAmount;
    [SerializeField]
    float brakeLerpAmount;

    [Header("Sounds")]
    [SerializeField]
    float maxVolume = 0.2f;
    [SerializeField]
    float lowpassValue = 2500;
    AudioSource audioSource;
    AudioLowPassFilter audioLowPassFilter;

    float inputValue;
    public float InputValue
    {
        set { inputValue = value; }
    }

    void SetEngineAudio()
    {
        audioSource.volume = throttleAmount * maxVolume;
    }

    public void SetAudioEffect(bool is1stView)
    {
        audioLowPassFilter.cutoffFrequency = (is1stView == true) ? lowpassValue : 22000;
    }

    // Start is called before the first frame update
    void Awake()
    {
        audioSource = GetComponent<AudioSource>();
        audioLowPassFilter = GetComponent<AudioLowPassFilter>();

        particleMainModule = GetComponent<ParticleSystem>().main;
        particleColor = particleMainModule.startColor.color;
        initAlpha = particleColor.a;
        throttleAmount = 0;
    }

    // Update is called once per frame
    void Update()
    {
        float lerpAmount = (throttleAmount > inputValue) ? brakeLerpAmount : accelLerpAmount;
        throttleAmount = Mathf.Lerp(throttleAmount, inputValue, lerpAmount * Time.deltaTime);
        particleColor.a = throttleAmount * initAlpha;
        particleMainModule.startColor = particleColor;

        if(audioSource != null) SetEngineAudio();
    }
}

JetEngineController는 액셀과 브레이크 입력에 따라서 엔진 부분의 이펙트를 제어하는 기능을 담당하고 있었습니다.

저 불꽃 부분을 표현할 때요.

이번에 오디오 시스템을 도입하면서 두 가지 기능을 추가했습니다.

  1. 엔진 출력에 따라 애프터버너의 볼륨 조절(SetEngineAudio())
  2. 카메라 상태에 따라 엔진의 Low Pass Filter 값 조정 (SetAudioEffect())

1번은 AircraftController에서 설정해주는 값을 가지고 볼륨을 조절하며,
2번은 CameraController에서 호출해줘야 합니다.


CameraController.cs

[SerializeField]
AudioSource engineAudioSource;

void SetEngineAudio()
{
    switch((CameraIndex)cameraViewIndex)
    {
        case CameraIndex.FirstView:
            ...
            jetEngineController.SetAudioEffect(true);
            break;
            
        case CameraIndex.ThirdView:
            ...
            jetEngineController.SetAudioEffect(false);
            break;

        default:
            break;
    }
}

CameraController의 엔진 소리를 교체하는 부분을 아까 만들어놨었죠.
여기서 jetEngineController.SetAudioEffect()를 호출하도록 코드를 추가합니다.

주파수를 어떻게 설정할 지 몰라서 일단 구별될 수준으로 대충 설정해줬습니다.


시점을 변경할 때마다 Low Pass Filter의 주파수가 변경되면서 소리가 바뀌고 있습니다.

(안 들린다고요? 조금만 기다려주세요)



미사일 발사

여기부터 랜덤 요소를 조금 추가하려고 합니다.

미사일을 발사할 때 사용하는 효과음이 여러 개 있는데,
발사할 때마다 이 효과음들을 랜덤으로 하나 뽑아서 출력하려고 합니다.


SoundManager.cs

public class SoundManager : MonoBehaviour
{
    private static SoundManager instance = null;

    [SerializeField]
    List<AudioClip> missileLaunchClips;

    [SerializeField]
    List<AudioClip> explosionClips;

    public AudioClip GetMissileLaunchClip()
    {
        return missileLaunchClips[Random.Range(0, missileLaunchClips.Count)];
    }
    
    public AudioClip GetExplosionClip()
    {
        return explosionClips[Random.Range(0, explosionClips.Count)];
    }

    void Awake()
    {
        if(instance == null)
        {
            instance = this;
        }
    }

    public static SoundManager Instance
    {
        get
        {
            if (instance == null)
            {
                return null;
            }
            return instance;
        }
    }
}

온갖 효과음들을 관리하는 SoundManager를 만들어줬습니다. Singleton 형식으로 어느 스크립트에서나 접근할 수 있습니다.
지금은 미사일 발사 효과음, 폭발 효과음만 가지고 있는 상태입니다.


WeaponController.cs

void LaunchMissile(ref int weaponCnt, ref ObjectPool objectPool, ref WeaponSlot[] weaponSlots)
{
    ...
    weaponAudioSource.PlayOneShot(SoundManager.Instance.GetMissileLaunchClip());
}

WeaponController에서 미사일을 발사할 때마다 발사 효과음을 출력하게 합니다.

Missile.cs (미사일 스크립트) 에 추가하지 않는 이유는, 발사하자마자 폭발하여 미사일이 비활성화될 때 소리가 끊겨버리는 것을 방지하기 위함입니다.


ExplosionAudio.cs

public class ExplosionAudio : MonoBehaviour
{
    AudioSource audioSource;
    void Awake()
    {
        audioSource = GetComponent<AudioSource>();
    }

    void OnEnable()
    {
        audioSource.PlayOneShot(SoundManager.Instance.GetExplosionClip());
    }
}

폭발할 때 출력할 효과음도 스크립트를 따로 작성해준 다음,

폭발 프리팹에 스크립트와 Audio Source를 추가했습니다.


SoundManager를 생성해서 오디오 클립들을 등록한 다음 미사일을 발사하면 랜덤으로 소리가 출력됩니다.



생각해보니 이건 비행기 터뜨릴 때도 쓸 수 있겠네요.




거리에 따른 음량

멀리서 나는 소리는 작게 들리고, 가까이서 나는 소리는 크게 들려야 합니다.

미사일이 어디서 폭발하느냐에 따라서 음량이 달라져야 하죠.

다행히 아주 간편하게 만들 수 있는 방법이 있습니다.

Audio Source의 밑에 보면 3D Sound Settings라는 항목이 있습니다.

여길 클릭하면 거리에 따른 음량을 비롯한 몇몇 값들을 조절할 수 있습니다.

  • Doppler Level : 도플러 효과의 양을 결정합니다.
  • Spread : 소리의 확산 각도를 설정합니다.
  • Volume Rolloff : 거리에 따른 소리 설정을 결정합니다. (하단의 커브로 조절하거나 선형/로가리즘으로 조정)
  • Max Distance : 소리의 감쇠가 적용되는 최대 거리입니다. 이 거리를 벗어나면 그래프 상에서 Max Distance 위치에 설정된 소리로 들리게 됩니다.

여기도 적당히 그래프를 비롯한 파라미터들을 조절하면 됩니다.


그런데 단순히 위에 설명한 파라미터만 조정하면 끝나는 게 아니라, Spatial Blend의 값도 조절해줘야 합니다.

이 값이 0이라면 위에 설정된 3D Sound Settings가 아예 적용되지 않게 됩니다.
[0 - 1] 사이로 값을 조절해서 위에 적용한 파라미터를 얼마나 적용시킬지 설정해야 합니다.


거리에 따른 딜레이

거리에 따른 음량만 있는 게 아닙니다.
먼 거리에서 나는 소리는 우리 눈에 보여진 즉시 소리가 나지 않습니다.

영상 15 ℃, 1000 hPa 기준 공기 중에서의 음속은 340m/s 입니다.
이 환경에서 340m 떨어진 곳에서 폭발이 일어나는 것을 봤다면, 1초 후에 폭발음이 들리게 됩니다.

이 딜레이를 정확하게 맞출 필요는 없지만, 그래도 어느 정도는 구현하는 게 좋을 겁니다.
이 게임은 근거리 전투가 아닌 수십 km의 광활한 맵에서 하는 전투를 구현해야 하니까요.

SoundManager.cs

[SerializeField]
float distanceMultiplier = 0.001f;

먼저 SoundManager에 거리에 따른 딜레이 값 distanceMultiplier을 추가합니다.

이전에 계산한 바로는 이 값이 1 / 170 = 0.00588...이었는데,
그대로 적용했다가는 딜레이가 너무 과할 수 있기 때문에 일단은 0.001로 놓아보겠습니다.


ExplosionAudio.cs

public class ExplosionAudio : MonoBehaviour
{
    AudioSource audioSource;

    void PlayExplosionClip()
    {
        audioSource.PlayOneShot(SoundManager.Instance.GetExplosionClip());
    }

    void Awake()
    {
        audioSource = GetComponent<AudioSource>();
    }

    void OnEnable()
    {
        float distance = GameManager.Instance.GetDistanceFromPlayer(transform);
        Invoke("PlayExplosionClip", distance * SoundManager.Instance.DistanceMultiplier);
    }

    void OnDisable()
    {
        CancelInvoke();
    }
}

폭발 효과음을 재생하는 코드에서 폭발 위치가 플레이어와 얼마나 떨어져있는지를 계산합니다.
그리고 거리 * distanceMultiplier 만큼 기다린 후에 효과음을 재생하도록 만듭니다.


ParticleAutoDestroy.cs

void OnEnable()
{
    float distance = GameManager.Instance.GetDistanceFromPlayer(transform);
    // When the duration is undefined, get ParticleSystem's duration
    if(duration == 0)
    {
        duration = GetComponent<ParticleSystem>().main.duration;
    }
    Invoke("Disable", duration + distance * SoundManager.Instance.DistanceMultiplier);
}

마지막으로 파티클을 자동으로 비활성화해주는 스크립트인 ParticleAutoDestroy도 수정해줍니다.

이 코드는 파티클 애니메이션이 끝나면 바로 오브젝트를 비활성화하는 기능을 담당합니다.

거리에 따른 딜레이가 지나기 이전에 파티클 애니메이션이 끝나버리면, 멀리 있는 오브젝트의 효과음이 재생되지 않거나, 재생 도중 끊겨버릴 수 있습니다.

비활성화하기까지 대기하는 딜레이에 거리에 따른 딜레이도 더해줘서, 효과음이 재생된 후에 비활성화될 수 있도록 수정해줍니다.



기총 발사

이번에는 기총을 발사할 때의 효과음입니다.

먼저 참고할 영상을 하나 보죠.



영상에서 주목할 부분은 발사를 시작할 때와 발사 중일 때의 피치(음조)가 다르다는 점입니다.

굳이 피치를 건드려야 하는 필요는 없지만, 발사를 시작한다는 효과를 주기 위해서 피치 값을 건드려보고자 합니다.


GunAudio.cs

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

public class GunAudio : MonoBehaviour
{
    [SerializeField]
    float lerpAmount = 5.0f;
    [SerializeField]
    float maxVolume = 0.2f;
    [SerializeField]
    float minPitch = 0.5f;

    [SerializeField]
    AudioSource gunFireAudioSource;

    bool isFiring = false;
    public bool IsFiring
    {
        set
        {
            if(value == true)
            {
                gunFireAudioSource.volume = maxVolume;
                gunFireAudioSource.pitch = minPitch;
            }

            isFiring = value;
        }
    }

    void Awake()
    {
        gunFireAudioSource = GetComponent<AudioSource>();
    }
    
    // Update is called once per frame
    void Update()
    {
        if(isFiring == false) gunFireAudioSource.volume = Mathf.Lerp(gunFireAudioSource.volume, 0, lerpAmount * Time.deltaTime);
        gunFireAudioSource.pitch = Mathf.Lerp(gunFireAudioSource.pitch, 1, lerpAmount * Time.deltaTime);
    }
}

발사를 시작할 때 AudioSource.pitch 값을 minPitch로 조정합니다. (1보다 작음)

발사 중일 때는 피치를 1까지 Lerp를 이용해서 서서히 올리고,
발사가 끝나면 피치는 그대로 두되 음량을 0까지 Lerp를 이용해서 서서히 낮춥니다.


WeaponController.cs

[SerializeField]
GunAudio gunAudio;

public void OnGunFire(InputAction.CallbackContext context)
{
    switch(context.action.phase)
    {
        case InputActionPhase.Performed:
            InvokeRepeating("FireMachineGun", 0, fireInterval);
            gunAudio.IsFiring = true;
            break;

        case InputActionPhase.Canceled:
            CancelInvoke("FireMachineGun");
            gunAudio.IsFiring = false;
            Vibrate(0);
            break;
    }
}

WeaponController의 기총 버튼 입력을 제어하는 함수에서 GunAudio.IsFiring을 바꿔줍니다.



기총 피격음

미사일 뿐만 아니라 총알도 피격음이 필요하겠군요.

SoundManager.cs

[SerializeField]
List<AudioClip> gunHitClips;

public AudioClip GetGunHitClip()
{
    return gunHitClips[Random.Range(0, gunHitClips.Count)];
}

SoundManager에서 소리를 추가한 다음 랜덤으로 뽑는 함수를 만들고,


GunHitAudio.cs

public class GunHitAudio : MonoBehaviour
{
    AudioSource audioSource;

    void Awake()
    {
        audioSource = GetComponent<AudioSource>();
    }

    void OnEnable()
    {
        audioSource.PlayOneShot(SoundManager.Instance.GetGunHitClip());
    }

    void OnDisable()
    {
        CancelInvoke();
    }
}

피격 이펙트에 붙여질 GunHitAudio 스크립트를 만들고 소리를 재생하는 함수를 추가합니다.

미사일 폭발 소리는 거리에 따른 딜레이를 고려했지만, 여기서는 그냥 출력시킵시다.
참고로 지면에 맞을 때는 아직 소리가 나지 않게 했습니다.

그리고 피격 이펙트에 AudioSource와 스크립트를 추가하면 끝입니다.



미션 성공/실패 효과음

이 효과음들이 출력되는 시점은 라벨이 표시되는 시점과 같습니다.
그러면 라벨이 표시될 때 재생할 효과음이 있다면 같이 재생해주면 되겠네요.

LabelInfo.cs

...
[SerializeField]
AudioClip audioClip;

...
public AudioClip AudioClip
{
    get { return audioClip; }
}

LabelInfo의 속성값에 AudioClip 하나를 추가하고,


AlertUIController.cs

[SerializeField]
AudioSource labelAudioSource;

public void SetLabel(LabelEnum labelEnum)
{
    ...

    // Show new label
    if(currentPriority < labelPriority)
    {
        ...

        if(labelInfo.AudioClip != null)
        {
            labelAudioSource.PlayOneShot(labelInfo.AudioClip);
        }
    }
    ...
}

AlertUIController에서 새 라벨을 표시하는 코드에서 LabelInfo.AudioClip이 있으면 재생하는 기능을 추가합니다.

그리고 LabelInfo에 오디오 클립을 연결해줍니다.



구현 상태



효과음이 추가되니 확실히 게임다워진 것 같습니다.
UI 부분에 손 볼 곳이 아직 많긴 하지만...

배경음악은 저작권 이슈가 있을 수 있으니 가장 나중에 넣으려고 합니다.
수익 창출은 필요 없으니 유튜브 업로드가 막히지만 않았으면 좋겠네요.



이 프로젝트의 작업 결과물은 Github에 업로드되고 있습니다.
https://github.com/lunetis/OperationZERO

0개의 댓글