언리얼엔진3 에서 사운드를 다루는 방법에 대해 기술한다.
한개 이상의 Wave 음원을 가지고, Attenuation, Delay, Random, … 등의 노드를 조합하여 다양한 효과를 내도록 해주는 리소스이다.
예를 들어, 폭발음이라고 해도 약간 다른 폭발음 3가지를 만들어두고 Random 노드를 사용하여 매 폭발 시 다른 음원을 출력하게할 수도 있다.
그리고 또 하나 중요한 점은 SoundClass 를 지정할 수 있다는 것이다.
... var() editconst Name 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 가 있을 것이고, 배경음을 돋보이게 하기 위해 효과음을 낮추고 배경음을 높이는 등의 행위를 정의할 수 있다. 아래와 같이…
하지만 이렇게 하기 위해서는 모든 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 를 사용하는 것이 적절하다. 루프 사운드를 사용한다는 말은 나중에 사운드를 끄는 것도 고려해야한다는 말이고 때문에 사운드 객체를 인스턴스화 하여 가지고 있어야 한다.
위의 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.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 를 사용하는 것을 알 수 있다.
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 로 지정된다.
[Engine.PlayerController] MaxConcurrentHearSound=32
SoundCue 마다 적절하게 최대 개수를 안배하고 MaxConcurrentHearSounds 로 마무리 짓자.
SoundMode 를 설정하고 저장한 후, 에디터를 다시 띄워 확인하면 그 안에 설정해 놓은 SoundClass 의 이름이 초기화되어 있고, 실제 게임 안에서도 영향을 주지 못하는 현상이 발생한다.
이것은 SoundMode 는 사전 로드 패키지5) 안에 들어 있어야 하기 때문이다.
SoundMode 는 엔진이 부트업됨과 동시에 모두 로드되어 초기화 되는데 그 행위를 하는 함수는 아래와 같다.
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=... // 여기다 사전 로드할 패키지를 추가해도 되고.. 입맛대로