사운드 디자이너의 TAD 도전기 #5 - 게임 오디오 코딩 하는 방법 RTPC, Switch, State

사운드 디자이너가 Wwise 툴에서 아무리 화려한 오디오 구조를 짜놓아도, 게임 속 세상은 늘 예측 불가능하고 복잡하게 돌아갑니다. 만약 "주인공이 풀밭을 빠르게 뛰어가면서, 적의 공격을 받아 피가 깎여 죽는 상황"이 동시에 일어난다면 게임 코드는 이 복잡한 상황을 어떻게 정리해서 사운드 엔진으로 배달할까요? 최근 TAD 공부를 위해 언리얼 엔진의 Third Person(삼인칭) 템플릿을 C++ 프로젝트로 세팅하며 그 배후의 작동 원리를 파고들어 보았습니다. 지금 이 파이프라인의 핵심을 파악하지 못하면, 실전 프로젝트에서 오디오 엔진 과부하와 싱크 미스로 연출이 완전히 무너질 수 있으니 꼭 집중해 주세요!



Wwise 바로가기


복합적인 상황 어떻게 소리를 결정할까?

 

 

게임 오디오 구현의 핵심은 엔진의 변화무쌍한 실시간 데이터를 Wwise 사운드 오디오 엔진이 이해할 수 있는 규격화된 신호로 변환하는 데 있습니다. 사운드 디자이너가 의도한 다이내믹한 연출이 끊김 없이 출력되려면, 게임 플레이어가 조작하는 매 순간의 역동적인 물리 상태가 오차 없이 정밀한 타이밍에 전달되어야만 합니다. 이를 처리하기 위해 C++ 코드는 복잡하게 얽힌 시스템들을 철저한 우선순위 규칙에 따라 분리하여 가동합니다.


TAD의 3대 무기


C++ 코드는 이 난해한 상황을 한 번에 처리하지 않고, 데이터의 성격에 따라 세 가지 영역으로 철저하게 분리하여 관리합니다. 이 메커니즘을 명확히 구분해야 오디오 리소스 낭비를 막고 믹싱 구조를 탄탄하게 세팅할 수 있습니다.


구분 데이터 유형 업데이트 방식 주요 활용 예시
RTPC 연속적인 변수 (Float) 매 프레임 감시 (Tick) 이동 속도, 차량 RPM, 캐릭터 체력(HP)
Switch 불연속적 상태 (State/String) 이벤트 트리거 시 (On Demand) 지면 재질(풀, 콘크리트), 장착 무기 종류
State 전역적 상태 (Global State) 게임 전역 환경 변경 플레이어 사망 상태, 일시정지, 맵 전환

* RTPC (실시간 감시): 캐릭터의 속도(Speed)나 체력(HP)처럼 끊임없이 변하는 실시간 숫자는 매 프레임 감시하며 사운드 엔진에 값을 주입합니다.
* Switch (조건부 변경): 지면 재질(Material)처럼 특정 순간에만 툭툭 끊겨서 변하는 상태는, 물리 엔진이 "바닥 재질 바뀜!"이라고 신호를 줄 때만 코드를 실행시킵니다.
* State (전역 환경 변화): 플레이어의 사망(Death) 같은 사건은 게임 전체의 공기(배경음, 필터 등)를 바꾸는 가장 강력한 우선순위를 가집니다.



Unreal 바로가기

복합 사운드 가동 프로세스

 

 

실제 언리얼 C++ 캐릭터 클래스 내부에서 이 상황이 일어날 때, 코드는 아래와 같은 구조로 흐르게 됩니다. 매 프레임 연산이 필요한 RTPC와 특정 조건에서 한 번만 호출되는 Switch 및 State의 분리 구조를 눈여겨보세요.


void AMyTADCharacter::UpdateCharacterAudio()
{
    // [1] 매 프레임 변하는 속도(RTPC) 배달
    float CurrentSpeed = GetVelocity().Size();
    AK::SoundEngine::SetRTPCValue("Character_Speed", CurrentSpeed, GetWwiseGameObjectID());

    // [2] 레이캐스팅으로 감지된 지면 재질(Switch) 업데이트
    FString SurfaceMaterial = CheckSurfaceMaterial(); // 바닥 감지 함수
    AK::SoundEngine::SetSwitch("Footstep_Material", *SurfaceMaterial, GetWwiseGameObjectID());

    // [3] 로직 감시를 통한 사망 상태(State) 및 이벤트 발동
    if (CurrentHP <= 0.0f)
    {
        // 전체 게임 세상을 '사망 모드'로 전환 (BGM 다운, 로우패스 필터 작동)
        AK::SoundEngine::SetState("PlayerLifeState", "Dead");
        
        // 마지막 비명 사운드 트리거 (단 한 번 실행)
        AK::SoundEngine::PostEvent("Play_Death_Scream", GetWwiseGameObjectID());
    }
}

언리얼 사운드 구현하기


이 복잡한 매커니즘을 말로만 들으면 어렵지만, 언리얼의 Third Person 템플릿을 열어보면 왜 코드가 이렇게 짜여야 하는지 직관적으로 이해하게 됩니다. 기본 에셋과 컴포넌트가 완벽하게 맞물려 있어 기초 파이프라인 독학에 최적화되어 있습니다.


템플릿 안에는 이미 캐릭터의 이동 컴포넌트가 실시간 속도 값을 계산하고 있고, 애니메이션 시퀀스에는 발이 땅에 닿는 타이밍(AnimNotify)이 정확히 찍혀 있습니다. TAD는 무(無)에서 데이터를 창조하는 게 아니라, 이미 잘 굴러가고 있는 언리얼의 물리/애니메이션 데이터 호스에 Wwise라는 파이프라인을 뚝딱 연결해 주는 역할을 하는 것임을 이 템플릿을 통해 명확히 배울 수 있었습니다.


도전을 마치며: 사운드의 우선순위 설계


코드가 "풀밭 + 전력 질주 + 사망"이라는 데이터를 한순간에 Wwise로 던지면, 최종적인 소리의 우선순위는 Wwise 툴 안에서 디자이너가 설정한 구조대로 결정됩니다. 데이터를 넘겨주는 C++ 측면의 정밀함과 이를 받아내는 믹싱 구조의 연계가 필수적입니다.


Dead 상태가 켜지는 순간, Wwise 내부 믹서 계층 구조에 의해 풀밭을 뛰던 발소리는 자연스럽게 페이드아웃되고 먹먹한 필터 사운드와 비명이 세상을 지배하게 되죠. 결국 멋진 오디오 연출은 코드가 정밀하게 배달해 준 데이터와, 사운드 디자이너가 설계한 Wwise의 오디오 구조가 완벽하게 맞물릴 때 탄생한다는 것을 다시금 깨닫습니다. 다음에는 이 삼인칭 템플릿에 실제로 Wwise 플러그인을 올리고, 발바닥 아래로 레이(Line Trace)를 쏘아 지면 재질을 코드로 판단하는 실전 구현기를 공유해 보겠습니다!



Wwise SDK API 참조하기

FAQ


Q1. 매 프레임 Tick에서 RTPC를 주입하면 성능 저하가 심하지 않나요?
Wwise의 SetRTPCValue는 내부적으로 값이 변경되었을 때만 이벤트를 처리하도록 최적화되어 있어 크게 무리를 주지 않습니다. 다만, 수백 개의 액터가 동시에 Tick에서 연산하는 것을 방지하기 위해 일정 거리(Distance Culling) 밖에 있는 오브젝트는 업데이트 주기를 낮추는 최적화 코드가 실무에서 반드시 들어갑니다.
Q2. Switch와 State의 가장 결정적인 차이점은 무엇인가요?
가장 큰 차이는 '범위(Scope)'입니다. Switch는 특정 Game Object(예: 해당 캐릭터의 발소리 바닥 재질)에만 영향을 미치지만, State는 게임 전역(Global)(예: 플레이어 사망 시 맵 전체의 배경음 변화 및 로우패스 필터 적용)에 일괄적으로 영향을 미칩니다.
Q3. 언리얼 C++에서 GetWwiseGameObjectID()는 어떻게 가져오나요?
Wwise 언리얼 플러그인을 프로젝트에 올린 후, 해당 액터에 AkComponent가 부착되어 있어야 합니다. 보통 AkGameplayTypes나 AkComponent 내부 API를 통해 현재 액터와 매핑된 고유 사운드 오브젝트 ID를 호출하여 사용합니다.
Q4. 바닥 재질(Switch)을 판별할 때 레이캐스트(LineTrace)는 얼마나 자주 쏘나요?
발소리가 날 때만 쏘는 것이 가장 효율적입니다. 매 프레임 쏘지 않고, 애니메이션 시퀀스에 심어둔 AnimNotify가 트리거되어 C++ 발소리 재생 함수가 호출되는 바로 그 순간에 아래 방향으로 LineTraceSingleByChannel을 한 번만 수행하여 충돌한 피지컬 머티리얼(Physical Material)을 가져옵니다.
Q5. 사망 시 비명 소리가 짤리거나 중복 재생되는 문제는 어떻게 막나요?
C++ 코드 단에서 CurrentHP <= 0.0f 조건문 진입 시, 이미 사망 상태로 전환되었는지 판단하는 부울 변수(bIsDead)를 두어 단 한 번만 PostEvent가 호출되도록 가드(Guard)쳐야 합니다. 또한 Wwise 내부에서도 해당 이벤트의 재생 제한(Playback Limit)을 1로 설정하여 중복 실행을 원천 차단합니다.
Q6. C++이 아닌 블루프린트로만 복합 오디오 시스템을 구현해도 괜찮을까요?
프로토타이핑이나 인디 게임 단계에서는 블루프린트로도 충분히 훌륭하게 작동합니다. 다만 대규모 프로젝트나 수많은 오브젝트의 사운드를 제어해야 하는 테크니컬 오디오 영역에서는 데이터 관리 효율, 성능 최적화, Wwise SDK에 직접 접근하는 유연성 측면 때문에 C++ 파이프라인 구조를 구축하는 것이 훨씬 유리합니다.

다음 이전