plitri

[번역] 어떻게 리듬게임을 음악이랑 맞추나요? (레딧 댓글)

2016. 1. 31. 19:34기타

이 글은 번역입니다. 대충 의역한 부분이 많으니 찰떡같이 알아들으세요.
https://www.reddit.com/r/gamedev/comments/13y26t/how_do_rhythm_games_stay_in_sync_with_the_music/c78aawd

반가워요. 저는 리듬게임을 작업한 적이 있습니다. (영상, 영상, 영상)

고려할 게 두 가지가 있습니다. 가장 중요한 하나는 어떻게 플레이어의 입력을 정확하게 받아들이는 걸 보장해서 플레이어가 정확하게 보상받는 것처럼 느끼게 하느냐이고, 약간 덜 중요한 하나는 그래픽이 음악에 맞도록 보장해서 노트와 음악이랑 사용자 동작이 들어맞는 것처럼 보이게 하는 것입니다.

두 번째 거, 그러니까 사용자 동작/그래픽이랑 음악이 맞도록 하는 것부터 시작하겠습니다. 만드는 게임이 DDR이나 기타히어로랑 비슷해서, 음악이 재생하는 것에 따라 노트가 "판정선"(strum bar)을 향해 떨어지고, 노트가 그 막대기에 닿는 순간 플레이어는 키를 눌러야 한다고 가정해봅시다. 쉽네요. 그쵸? 그냥 이런 함수를 쓸 수 있겠죠.

(역주: 쓰지 마세요! 이후를 위한 예제입니다.)

renderNoteFallingDownScreen(id:int) {
    note[id].y = strumBar.y - (mySong.position - note[id].strumTime);
    // 노트[id].y = 판정선.y - (노래.현재위치 - 노트[id].판정시간);
}

이제 이런 함수를 썼고, 컴파일을 할 테지만, 놀랍고도 무섭게도, 모든 게 엉망진창입니다. 노트는 버벅버벅 더듬거리고[각주:1], 마침내 가까스로 아래로 내려와도, 특히 프레임이 떨어진[각주:2] 동안에는 판정선에 노래의 한 0.5초 이후에 도달할 겁니다. 그래서 이게 뭔 말인데요?

먼저, 노래 파일을 재생하는 대부분의 환경에서 (아니면 최소한 제가 작업했던 환경에서 : AS3, javascript, C#), 음악 파일의 정확한 재생위치, 그러니까 충분한 비율로 갱신되는 (~60FPS) 재생 위치를 얻기란 매우 어렵습니다. 모든 게 완벽한 세계에서, 매 프레임마다 음악의 재생 위치를 추적하면, 이런 결과가 나올겁니다.

0, 17, 33, 50, 67, 83, 100, 117, 133...

하지만 실제 세계에서는, 결과는 이런식으로 나올 겁니다.

0, 0, 0, 0, 83, 83, 83, 133, 133, 133, 133, 200, 200...

정확하고 일관적인 결과를 주는 대신, 재생위치는 건너뛰며[각주:3] 갱신될 겁니다. 이제 멀티플레이어 게임에서 보간하는 것과 똑같이 이 건너뛴 사이들을 보간[각주:4]해야겠죠.

이걸 하는 가장 쉬운 방법은 재생 위치를 특정 변수에 보관해놓고, 매 프레임마다 자동으로 시간을 더하는 거겠죠. 다음은 별로 안 좋은 방법입니다.

everyFrame() {
    songTime += 1000/60; // 1초에 1000ms, 1초당 60프레임
}

그리고 이걸 하는 약간 더 나은 방법입니다.

songStarted() {
    previousFrameTime = getTimer();
}

everyFrame() {
    songTime += getTimer() - previousFrameTime;
    previousFrameTime = getTimer();
}

// OR:

songStarted() {
    startTime = getTimer();
}

everyFrame() {
    songTime = getTimer() - startTime;
}

하지만, 이 세 방법은 모두 완벽하지 않습니다. 아니, 좀 더 정확히 하자면, 여러분의 음악 재생 방식이 완벽하지 않습니다. 어느쪽이 됐던, 갑작스럽게 여러분의 쬐끄만 변수 songTime은 실제 노래의 재생위치와 어긋나게 될 거라는걸 의미합니다. 특히 재생 환경이 음악을 건너뛰고, 버퍼링하고, 뭉개고[각주:5] 하는 환경이라면 이런 일이 일어날 가능성이 높습니다 - 웹 게임이나 파일에서 노래를 재생하는 대신 스트리밍해서 노래를 재생하는 게임 같은 경우 말이죠. 또, 대부분의 음원 재생 루틴은 극초반에 재생할 때 머뭇거리기[각주:6] 때문에, 어긋난 상태로 재생될수도 있습니다 - 특히 인코딩 데이터를 구워놓은 MP3 파일을 쓰고있다던가[각주:7], 느린 하드디스크에서 오디오 파일을 읽어오고 있다던가, 여러분의 게임이 쓰레기같은[각주:8] 크롬의 내장 "pepperflash" 플러그인을 쓰고있다면 말이죠.

그래서, songTime을 불러오고 실제 음원 파일의 재생위치에 맞도록 유지하기 위해, 매번 재생 위치를 새로 가져올 때마다 그 값을 교정하기 위한 기본적인 보간[각주:9] 알고리즘을 사용하고 싶군요. 이렇게요.

songStarted() {
    previousFrameTime = getTimer();
    lastReportedPlayheadPosition = 0;
    mySong.play();
}

everyFrame() {
    songTime += getTimer() - previousFrameTime;
    previousFrameTime = getTimer();
    if(mySong.position != lastReportedPlayheadPosition) {
        songTime = (songTime + mySong.position)/2;
        lastReportedPlayheadPosition = mySong.position;
    }
}

이 함수는 수동으로 추적중인 songTime 변수를 자동으로 가져와서 새 재생위치를 알게 되었을 때마다 실제 알아낸 재생위치와 평균을 낼 것입니다. 새로운 재생위치를 받았을 때에만 이렇게 하는데요, 새로운 값[각주:10]을 받는 사이사이에 "계단진" 값을 향해 계속 보간[각주:11]하다보면, 또다시 버벅거리는[각주:12] 재생위치를 얻게 될 것이기 때문입니다. 그 대신, 새로이 값을 받아올 때까지는 계속 수동으로 songTime을 증가시킬 것입니다.

하지만 아직 재밌는 부분이 끝난 건 아니죠!


※ 이 부분은 @Tis_Lenia 님에 의한 번역입니다. 늦은 반영 죄송합니다. 검토는 하지 않았습니다.

보시면 알다시피, 모든 렌더링 경로(pipelines)는 각각 상이한 지연 시간을 갖습니다: 지연 시간에는 영상(scene) 자체를 렌더링하는데 소요되는 시간과 사용자의 모니터 자체에 띄우는 작은 딜레이가 포함이 됩니다. (만일 사용자가 일반 모니터가 아닌 TV와 같은 디스플레이를 사용할 경우 후자의, 화면에 띄울 때 소요되는 시간은 늘어납니다.) 그래픽(영상) 화면에 뜨는데 발생하는 딜레이에 더하여 플레이어가 키보드를 누르고 그 입력이 프로그램에 전송될 때에도 딜레이가 발생합니다. 대부분의 게임에서 키 입력상에서 발생하는 딜레이는 무시해도 큰 문제가 되지 않으나, 리듬게임에 있어서는 이 딜레이는 철저한 계산이 요구되며 이는 제작자가 다른 게임에서는 무시되는 키 입력상에서 발생하는 이 딜레이를 고려해야함을 의미합니다.

허나 불행하게도, 모든 플레이어가 같은 모니터와 입력 장치를 사용하지 않는데, 이는 playhead에 삽입할 일정한 공통 기준이 존재하지 않음을 의미합니다. 따라서 타 리듬게임, Rock Band나 Guitar Hero에서 채용하고 있는 것과 같이 플레이어가 딜레이를 시각적으로 확인/조정할 수 있는 테스트가 필요합니다. 앞에서 언급된 두 리듬게임은 두 테스트를 사용하는데 이 중 하나에 대해 먼저 언급한 후 나머지를 설명하겠습니다.

이 테스트를 하는데에는 다양한 방식을 사용할 수 있습니다. 화면을 일정한 패턴으로 깜빡이는 것으로 딜레이 시간을 측정하는 것이나, 혹은 메트로놈(연주에 사용하는 박자기) 같이 시각적인 지표(노트)를 주어 플레이어에게 이에 맞춰 키를 누르게 하는 방법을 사용할 수도 있습니다. 이 테스트를 할 때에는 영상의 딜레이를 확인하기 위함이지 음향의 딜레이를 확인하는 것이 아니기 때문에 음악을 재생하여서는 안됩니다. 매 순간 화면이 깜빡일 때 (혹은 메트로놈이 똑딱일 때) 그 사이의 시간을 측정합니다. 그 다음에 플레이어로부터 키 입력을 받고 그 사이의 시간을 또 측정합니다. 화면을 깜빡일 때 발생한 딜레이를 키 입력까지 발생한 딜레이에서 뺀 시간이 시각적(영상) 딜레이입니다.

이론적으로는 시각적(영상) 딜레이는 렌더링 딜레이와 키입력의 딜레이의 합이기 때문에 마이너스의 값이 발생할 수 없으나 플레이어가 습관적으로 실제로 입력해야한다고 생각하는 순간보다 노트를 빨리 입력하는 버릇이 있을 수도 있습니다. 만일 플레이어가 이와 같은 습관을 가지고 있고 모니터와 입력 장치 모두 딜레이가 짧은 장비들이라면 마이너스의 딜레이를 갖는 것도 불가능하지는 않습니다. 따라서 게임을 제작할 떄 이 이론상으로 존재할 수 없는 마이너스 값의 딜레이에도 대응할 수 있도록 해야합니다.

또한 중요한 점은, 15초에서 30초 정도는 이 테스트를 진행해야 신뢰할 수 있는 테스트 결과가 나온다는 것입니다. 다양한 변수로 인하여 일관적인 딜레이 값을 측정하는데 방해가 발생할 수 있기 때문에 한번의 일련의 딜레이 측정 테스트로는 신뢰할 수 있는 결과를 도출하기 어렵습니다. 이런 방해 요소는 렌더링, 키 입력 과정과 더불어 플레이어의 테스트 중 규칙적인 키 입력에서도 발생할 수 있습니다. 그렇기 때문에 반복된 검사를 진행하여 영상 딜레이의 평균값을 구합니다.


글을 대충 읽고 저는 이런 걸 짰는데요, 이것도 나쁘지 않은 것 같지만,

자세히 읽어보니 반으로 나누는 보간하는 부분이 있었군요. 보간이 있는 쪽이 없는것보다 나을 것 같긴 하네요.

아무튼 앞부분의 중요 내용은 "값이 계단지게 띄엄띄엄 나온다" 이고, 그 부분을 커버해줄 수 있는 알고리즘이 있으면 충분하지 싶습니다. 이렇게 해놓으면 싱크가 어긋나는 일은 없겠죠.

뒷부분의 딜레이 관련 내용은 귀찮으면 그저 유저에게 맡기면 되는 문제... 니까요. 오프셋 조절하셈 하고 던져주면 되는 (..

  1. jittery/stuttery [본문으로]
  2. dip [본문으로]
  3. in steps, "단계별로"가 일반적인 번역이겠지만 의미가 와닿지 않을 것 같아서 수정 [본문으로]
  4. interpolate. "선형 보간"을 검색해보세요. 찾을 시간이 없으시면, 사잇값을 구한다는 느낌으로 받아들이시면 될 듯. [본문으로]
  5. skip, buffer, or crash [본문으로]
  6. hiccup [본문으로]
  7. 이 구문은 무슨 말인지 몰라 그대로 적었습니다. if you're using MP3 files that have encoding data baked in [본문으로]
  8. piece of shit (똥조각) [본문으로]
  9. easing [본문으로]
  10. 원문에 valuable이라고 되어있는데, variable의 오타일 겁니다. (자동완성의 폐해...) [본문으로]
  11. easing [본문으로]
  12. stuttery [본문으로]

'기타' 카테고리의 다른 글

리듬게임 아이디어  (0) 2016.02.08
스킨 통일에 관하여 + 잡담  (0) 2015.11.12
 / 
1···78910111213···17