Unity로 리듬 게임을 구현하면 오디오랑 화면이랑 싱크가 안 맞는 문제가 있습니다.  

이 글은 그 문제를 분석하고, Unity 환경에서 오디오와 입력 지연을 최소화하여 정확도를 높이는 방법을 공유합니다.

독자적인 연구 부분이 많아 잘못된 내용이 있을 수도 있습니다.

예제



39b5c52be7dc39af62f1c6bb11f11a392af94afcafe4f84fc8


태고갤에 소개한 태고의 달인 연습 프로그램 TaikoPractice

Git Repo

리듬 게임이 어떻게 구현되는지도 궁금했지만 사실 태고의 달인 연습하는 프로그램을 만들고 싶었습니다.

매번 틀리는 구간만 틀려서 특정 패턴만 연습하고 싶었거든요. 자세한 내용은 해당 글 참고 바랍니다.

1. 문제의 본질: 세 가지 지연


리듬 게임에서 타격감과 싱크를 해치는 주요 원인은 크게 세 가지입니다.

  1. 오디오 지연 (30~100ms, 가장 큼)
  2. 입력 지연 (~8ms, 코어 게이머 아니면 못 느끼는 정도)
  3. 모니터 디스플레이 지연 (오래된 모니터 아니면 미미함)


입력·화면 지연은 보정으로 일정 수준까지 맞출 수 있습니다. 하지만 "키음이 있어 키를 누르면 바로 소리가 나야 하는 게임의 경우" 오디오 지연은 게임 플레이에 치명적입니다.

2. 오디오 지연 줄이기


오디오 백엔드(Audio Backend) 개념

그래픽에 DirectX/OpenGL/Metal이 있듯, 오디오에도 백엔드가 있습니다.

  • ASIO
    • 음악 제작용으로 널리 쓰이는 저지연 드라이버.
    • Windows 오디오 스택을 거치지 않고 장치로 직접 출력 → 딜레이가 거의 없음.
    • 단점: 다른 앱에서 소리를 캡처하기 어렵고 (녹화 프로그램을 못 씀), 일부 메인보드 사운드 칩셋은 지원하지 않음.
  • WASAPI
    • Windows 기본 드라이버. 호환성은 좋지만 지연이 큼.
    • 독점 모드로 약간 줄일 수 있으나 Unity 기본 오디오 엔진에서는 지원되지 않음



Unity 기본 오디오 시스템은 WASAPI 기반이라 정밀한 판정을 요구하는 리듬게임에는 부적합합니다. 따라서 ASIO 지원 엔진을 선택해야 합니다.


하지만 모든 기본 메인보드 사운드 칩셋이 ASIO 를 지원하지는 않아 유저 입장에선

  • 오디오 인터페이스 구매
  • 혹은 가이드 를 참고하면 어느정도 비슷하게 흉내낼 수 있음.


2가지 방법중 하나를 고려해야 합니다.

FMOD 도입


대표적인 대안은 외부 사운드 엔진 FMOD입니다.

  • 상업 리듬게임(DJMAX, EZ2ON 등)도 FMOD을 사용하는 경우가 많음.
  • ASIO를 지원 → 오디오 지연을 크게 줄일 수 있음.


이런 고급 엔진을 쓰는게 오버킬인 것 같기도 해서 다른 오디오 엔진도 찾아서 시도해 보았습니다만 적용이 힘들었습니다.

최근 게임 시장에서 많이 사용되고 있는 추세라서 배워두면 좋을 것 같습니다.

FMOD 설정

기본적인 프로젝트 설정은 아래의 글을 참고해보시고.. sicilian-najdorf.tistory.com/49

프로젝트에서

 


1dbcc332e0d678af63bed1b05683746fa940aedf268877cbc8f97cf2115705fef8cc66141a903167f5ba



키음을 Assets에 등록하고 간단한 2D Action Event를 생성합니다.


DSP Buffer Size 와 OutputType (Audio Backend) 설정 DPS Buffer
  • 오디오 데이터 내보내기전 데이터를 담아주는 공간.
  • 버퍼 크기가 크면 클 수록 연산량은 줄어들지만 그만큼 지연.



1dbcc332e0d678af63bed1b05683746fa940aedf268877cbcafe7bf2115705fec3809882bf7658fd4144



상단 메뉴 FMOD > Edit Setting 에서 Platform Specific을 눌러보시면 유니티 에디터랑 빌드에서 사용될 설정을 볼 수 있고 Auto 를 풀고 2^k 형태로 값을 적어주시면 됩니다.

64 정도가 적당한 것 같습니다.

런타임 바꾸는 방법도 있는데 FMOD 시스템 전체를 재시작해야 해서 좀 복잡합니다.

Backend의 경우 스크립트로 바꿔줘야 하는데

var coreSystem = RuntimeManager.CoreSystem; var result = coreSystem.setOutput(AudioBackend.ASIO);

런타임에도 여러번 바꿀 수 있습니다.


이벤트 출력 public EventReference donRef; donIns = RuntimeManager.CreateInst ance(donRef); donInst.start();

위와 같이 오디오 출력이 가능합니다.


런타임에 사운드 생성

www.fmod.com/docs/2.00/api/core-api-system.html#system_createsound 를 사용하여 URL 로 사운드를 로드하거나 메모리에 올라간 데이터로 사운드를 만들 수 있습니다.

float[] samples; CREATESOUNDEXINFO exinfo = new() { cbsize = Marshal.SizeOf(typeof(CREATESOUNDEXINFO)), length = (uint)samples.Length, numchannels = 2, // 사운드에 따라 변경 필요 defaultfrequency = 44100, // format = SOUND_FORMAT.PCMFLOAT }; RESULT result = RuntimeManager.CoreSystem.createSound(Marshal.UnsafeAddrOfPinnedA rrayElement(samples, 0), MODE.OPENMEMORY | MODE.OPENRAW | MODE.CREATESAMPLE, ref exinfo, out var sound); 3. 입력(Input) 지연 줄이기


방법은 입력만 담당하는 입력 스레드를 만드는 겁니다.

다만 유의해야 할 점은




입력 스레드 void Loop() { while (!quit) { Update(); Thread.Sleep(1); // 최소 1ms 만큼 스레드를 멈춤 } } thread = new Thread(Loop); thread.Start();

이렇게 하면 Update 메소드를 1ms 단위로 실행할 수 있습니다. 스레드를 1ms 만큼 재웠지만 더 걸릴 수 있습니다. 실제로 초당 1000번 실행되는게 아니라 600-700번 정도 실행되는 것 같습니다.


Windows OS 로 부터 키 상태 입력 받기 // Determines whether a key is up or down at the time the function is called, and whether the key was pressed after a previous call to **GetAsyncKeyState**. [DllImport("user32.dll")] private static extern short GetAsyncKeyState(int vKey);

user32.dll에 Key상태를 알 수 있는 API가 있습니다. GetAsyncKeyState(c) & 0x8000 가 0 이 아니면 현재 내려가 있는 상태고 그렇지 않으면 올라가 있는 상태입니다. Dictionary에 사용하는 키 마다 이전 상태를 저장하도록 하고 상태가 바뀌면 키가 눌린 것을 감지할 수 있습니다.

foreach(char c in keys) { var prevState = state[c]; var currState = (GetAsyncKeyState(c) & 0x8000) != 0; if (!prevState && currState) { OnPressed.Invoke(c); } state[c] = currState; }

인개갤에도 리듬 게임 만드시는 분들이 계시는것 같은데 도움이 되셨으면 합니다.