Sound

Sound

언리얼엔진3 에서 사운드를 다루는 방법에 대해 기술한다.

구조

SoundCue

한개 이상의 Wave 음원을 가지고, Attenuation, Delay, Random, … 등의 노드를 조합하여 다양한 효과를 내도록 해주는 리소스이다.

예를 들어, 폭발음이라고 해도 약간 다른 폭발음 3가지를 만들어두고 Random 노드를 사용하여 매 폭발 시 다른 음원을 출력하게할 수도 있다.

그리고 또 하나 중요한 점은 SoundClass 를 지정할 수 있다는 것이다.

SoundCue.uc
...
var()   editconst    Name     SoundClass;
...

리소스에서 우클릭 함으로써 지정할 수 있다.

SoundClass 는 말 그대로 사운드분류라고 말할 수 있는데 자세한 설명은 아래서..

SoundClass

SoundClass 는 사운드 성격에 따른 분류이다. 사운드는 음원에 따라 여러 성격으로 나뉠 수 있다. 예를 들어 BGM, 무기효과음, 캐릭터발자국소리, 음성 등으로 말이다.

언리얼엔진3에서는 이 사운드의 분류를 SoundClass 라는 객체로 관리한다. SoundClass 객체들은 Volume, Pitch, Stereo Bleed, Voice Center Channel, … 등 사운드 관련 속성들이 있고 이것들을 임의로 지정할 수 있다.

위는 SoundClass 리소스를 더블클릭하면 열리는 Sound Class Editor 이다. 여기서 각각의 SoundClass 들의 계층구조를 정할 수 있으며1) 위에서 말한 속성을 지정할 수 있다. 새로운 SoundClass 를 생성하면 위 Graph 에 핀이 연결되지 않은 노드가 새로 생겨나는데 그것을 계층에 맞게 연결해주면 사용할 수 있게 된다.

SoundClass 는 그 성질에 해당하는 여러 SoundCue 에 대입되어 동시에 영향을 줄 수 있다.

바꿔말하자면 WeaponFriend 이라는 이름2)의 SoundClass 를 여러 SoundCue 에 지정하고 WeaponFriend 의 볼륨을 높이거나 낮추면 여러 SoundCue 에 영향을 줄 수 있다는 말이다. 역할만 보자면 여러 사운드를 성격에 맞게 그룹화하여 일괄적으로 제어하는 것이다.

모든 SoundCue 들은 각각의 성격에 맞는 SoundClass 를 지정해주는 것이 좋다. 그래야 이 섹션 다음에 오는 SoundMode 에 의해서 일괄적으로 제어될 수 있기 때문이다. SoundMode 를 사용하여 모든 효과음의 볼륨을 내리고 음성만 높이는 등의 행위를 할 수 있다.

SoundMode

위에 잠깐 언급했지만 SoundMode 는 현재 사운드들이 어떻게 출력이 되어야 하는지를 정의하는 리소스이다.

효과음의 볼륨들은 모두 낮추고, 음성을 높이는 역할을하는 SoundMode 가 있을 것이고, 배경음을 돋보이게 하기 위해 효과음을 낮추고 배경음을 높이는 등의 행위를 정의할 수 있다. 아래와 같이…

특정 SoundClass 들을 느리게 재생하기 위한 Slow SoundMode. Pitch 가 1 미만인 것을 볼 수 있다.

하지만 이렇게 하기 위해서는 모든 SoundCue 들이 각기 성격에 맞는 SoundClass 가 지정되어 있어야 한다. SoundMode 가 제어하는 녀석은 SoundClass 이기 때문이다.

SoundMode 는 프로그래머가 게임 런타임중 직접 세팅할 수 있으며, kismet 에 의해서 세팅될 수도 있다.

프로그래머가 직접 제어하는 함수

// AudioDevice.uc
native final function bool SetSoundMode( name NewMode );

kismet 으로 제어할 수 있는 노드는 Set Sound Mode 를 참조. kismet 노드도 결국 위 함수를 이용하여 SoundMode 를 세팅한다.

사용

wave 를 import 하여 cue 파일을 생성하는 과정은 생략하고, 그것을 코드레벨에서 사용하는 방법을 설명

크게 두가지 방법으로 사용한다.

AudioComponent

지속적인 루프 사운드는 AudioComponent 를 사용하는 것이 적절하다. 루프 사운드를 사용한다는 말은 나중에 사운드를 끄는 것도 고려해야한다는 말이고 때문에 사운드 객체를 인스턴스화 하여 가지고 있어야 한다.

위의 Actor.PlaySound 는 사운드 객체를 인스턴스화하여 가지고 있는 것이 불가능하므로 AudioComponent 가 그 대안이다.

// AudioDevice.uc
class AudioDevice extends Subsystem
    config( Engine )
    native( AudioDevice )
    dependson( SoundClass )
    transient;
...
 
// UAudioDevice.h
static UAudioComponent* CreateComponent( USoundCue* SoundCue, FSceneInterface* Scene, AActor* Actor = NULL, UBOOL Play = TRUE, UBOOL bStopWhenOwnerDestroyed = FALSE, FVector* Location = NULL );
 
// UnAudio.cpp
UAudioComponent* UAudioDevice::CreateComponent( USoundCue* SoundCue, FSceneInterface* Scene, AActor* Actor, UBOOL bPlay, UBOOL bStopWhenOwnerDestroyed, FVector* Location )
{
    ...
 
    UAudioComponent* AudioComponent = ConstructObject<UAudioComponent>( UAudioComponent::StaticClass() );
 
    ...
 
    if ( Actor )
    {
        // Actor 가 있다면 붙인다.
        AudioComponent->ConditionalAttach( Scene, Actor, Actor->LocalToWorld() );
        Actor->Components.AddItem( AudioComponent );
    }
 
    ...
 
    return AudioComponent;
}

위 코드를 보면 알겠지만 Component 형태로 생성하여 Actor 에 붙이고 인스턴스를 리턴한다. 사용하는 것은 이제 우리의 몫이다.

Actor.CreateAudioComponent 는 위를 좀 더 쉽게 사용하도록 해준다.

Actor.PlaySound

단발적인 재생을 위한 사용은 Actor 의 PlaySound 메쏘드를 사용한다.

// Actor.uc
native noexport final function PlaySound( SoundCue InSoundCue, optional bool bNotReplicated, optional bool bNoRepToOwner, optional bool bStopWhenOwnerDestroyed, optional vector SoundLocation, optional bool bNoRepToRelevant );
 
// UnScript.cpp
void AActor::PlaySound( USoundCue* InSoundCue, UBOOL bNoReplicated, UBOOL bNoRepToOwner, UBOOL bStopWhenOwnerDestroyed, FVector* pSoundLocation, UBOOL bNoRepToRelevant )
{
    ...
 
    // 복제되지 않은 상태에서 네트워크 플레이 중
    if ( !NotReplicated && WorldInfo->NetMode != NM_Standalone && GWorld->GetNetDriver() != NULL )
    {
        ...
        // 복제되어야 하는 상황 bNoRepToRelevant == FALSE 이라면 주변 플레이어에게 들리게끔
        NextPlayer->HearSound( InSoundCue, this, SoundLocation, bStopWhenOwnerDestroyed );
        ...
    }
 
    ...
 
    // 로컬 머신에서 재생
    if ( GWorld->GetNetMode() != NM_DedicatedServer )
    {
        ...
 
        // 나를 포함한 로컬상의 모든 플레이어 (화면분할된 로컬유저까지) 에게 들리게끔 한다.
        if ( NextListener->HearSound( InSoundCue, this, SoundLocation, bStopWhenOwnerDestroyed );
 
        ...
    }
}

기본적으로 복제를 지원한다. 루프사운드보다는 단발적인 효과음에 적절하다.

스택을 추적해 들어가보면 결국에는 위의 AudioComponent 를 사용하는 것을 알 수 있다.

FAQ

같은 음원을 여러개 생성하여 출력하다보면 소리가 나지 않는다?

Actor.PlaySound 를 사용하여 특정 SoundCue 를 여러번 출력하다보면 소리가 나지 않는 경우가 있다.

이 현상의 원인은 해당 SoundCue 의 동시재생 개수를 초과하여 출력하였기 때문이다. 언리얼엔진3는 무분별한 SoundCue 재생을 막기위해 (아마도?) 내부적으로 하나의 SoundCue 가 동시에 재생될 수 있는 최대 개수를 지정하여 관리된다.

물론 SoundCue 의 최대 동시재생 가능 개수3)를 임의로 지정할 수 있다. 언리얼에디터를 열고 SoundCue 리소스에서 우클릭하여 Properties and Sound Attenuation Nodes… 를 클릭하면 속성창이 나온다. 그곳에서 Max Concurrent Play Count 를 고치면 된다.

약간 첨언하자면 SoundCue 마다 동시에 재생될 수 있는 개수4)를 지정할 수 있는만큼 해당 SoundCue 음원의 성격을 이해하고 최소한의 개수를 입력해야 한다. 게임 안에서 재생되는 SoundCue 는 하나가 아니기 때문이다.

좀 더 첨언하자면 SoundCue 가 최종적으로 재생되는 형태의 리소스는 AudioComponent 이고 이것은 PlayerController 에서 Pooling 되어 관리된다.

// PlayerController.uc
var globalconfig int  MaxConcurrentHearSounds;             // 한 Local Machine 에서 들을 수 있는 최대 음원 개수
var array<AudioComponent>  HearSoundActiveComponents;      // 현재 재생되고 있는 오디오 콤포넌트
var array<AudioComponent>  HearSoundPoolComponents;        // 휴면 상태의 오디오 콤포넌트
 
...
 
native function AudioComponent GetPooledAudioComponent( SoundCue ASound, Actor SourceActor, bool bStopWhenOwnerDestroyed, optional bool bUseLocation, optional vector SourceLocation );
 
// UnScript.cpp
UAudioComponent* APlayerController::GetPooledAudioComponent( USoundCue* ASound, AACtor* SourceActor, UBOOL bStopWhenOwnerDestroyed, UBOOL bUseLocation, FVector SourceLocation )
{
    ...       // 직접 소스 참조 ㅋ
}

그리고 위를 보면 알겠지만 MaxConcurrentHearSounds 는 .ini 로 지정된다.

DefaultGame.ini
[Engine.PlayerController]
MaxConcurrentHearSound=32

SoundCue 마다 적절하게 최대 개수를 안배하고 MaxConcurrentHearSounds 로 마무리 짓자.

SoundMode 에 설정한 SoundClass 값이 자꾸 초기화된다?

SoundMode 를 설정하고 저장한 후, 에디터를 다시 띄워 확인하면 그 안에 설정해 놓은 SoundClass 의 이름이 초기화되어 있고, 실제 게임 안에서도 영향을 주지 못하는 현상이 발생한다.

이것은 SoundMode 는 사전 로드 패키지5) 안에 들어 있어야 하기 때문이다.

SoundMode 는 엔진이 부트업됨과 동시에 모두 로드되어 초기화 되는데 그 행위를 하는 함수는 아래와 같다.

UnAudio.cpp
void UAudioDevice::InitSoundModes( void )
{
    SoundModes.Empty();
 
    for ( TObjectIterator<USoundMode> It; It; ++It )
    {
        USoundMode* Mode = *It;
        if ( Mode )
        {
            SoundModes.Set( Mode->GetFName(), Mode );
            Mode->Fixup();
        }
    }
 
    BaseSoundModeName = NAME_Default;
}

위 함수는 엔진이 부트업될 때 호출되며 위 시점에 이미 SoundMode 가 들어있는 모든 패키지들은 사전에 로드되어 있어야 정상적으로 위 로직이 수행되어 초기화된다. 사전에 패키지를 모두 로드시키려면 .ini 파일 Engine.StartupPackages 섹션에 지정하면 된다.

// BaseEngine.ini
[Engine.StartupPackages]
...
Package=DefaultUISkin
Package=EngineMaterials
Package=EngineDebugMaterials
Package=EngineSounds
Package=EngineFonts
Package=EngineBuildings
Package=SoundClassesAndModes   // 요놈이 엔진이 기본적으로 로드해주는 SoundMode 관련 패키지 이름. 이눔을 걍 써도 된다.
 
// DefaultEngine.ini
[Engine.StartupPackages]
+Package=UI_Fonts
+Package=...                   // 여기다 사전 로드할 패키지를 추가해도 되고.. 입맛대로

참조

1) Graph 의 핀의 연결을 조절함으로써
2) 보면 알겠지만 아군이 사용하는 무기를 뜻하는 SoundClass 이다. UTGame 에 기본적으로 있는 SoundClass
3) SoundeCue.MaxConcurrentPlayCount 변수에 지정되며 SoundCue.uc 를 보면 알겠지만 Default 는 16이다.
4) 물론 Local Machine 에서 동시에 들려지는 개수를 뜻한다.
5) EngineMaterials, EngineSounds, EngineFonts 등과 같이 시작과 동시에 사전 로드되어야 하는 패키지들을 말한다. 앞서 말한 패키지들은 BaseEngine.ini 에 [Engine.StartupPackages] 섹션안에 설정되어 있다.