유니티 리듬게임 알고리즘 - yuniti lideumgeim algolijeum

Be-Music용 음악을 제작하고 연주하기 위한 포맷으로 BMS의 경우에는 음악을 듣는 것 뿐만 아닌, 간단한 애니메이션을 표시함과 더불어 자신이 연주 및 게임까지 진행 가능한 음악 형식입니다.


BMS들의 확장자는 세가지 형태가 있습니다.


1. BMS - Be-Music Script 

- 플레이어 당 6키의 키 (건반 5개와 턴테이블, 보통 5키로 통칭)를 사용할 수 있습니다.


2. BME - Be-Music Extended

- BMS 확장자와 갔지만 플레이어 당 최대 8개의 키 (건반 7개와 턴테이블, 보통 7키로 통칭)을 지원한다. 7키를 지원하면서 확장자가 BMS인 것도 있기 때문에 5키, 7키의 구분은 6/7 번 건반이 등장하느냐로 판단하는 것이 일반적입니다.


3. BML - Be-Music Longnote

- BME와 같습니다만. 롱노트 오브젝트를 지원합니다. 옛날 프로그램들은 롱노트를 아예 인식하지 못할 수 있기 때문에 롱노트를 사용함을 알리기 위해 확장자를 바꿔서 사용하는 경우가 많습니다.





BMS의 구성요소


일반적으로 BMS파일은 단 하나만 있는것이 아닌 아래와 같은 요소로 구성되어 있습니다.


- 해당 곡의 메타 데이터(게임 플레이와 상관 없든 데이터): 제목, 제작자, 장르 등.

- 게임 플레이를 좌우하는 요소들 : BPM, 판정 난이도 등.

- 사용할 사운드/이미지 데이터 등의 위치와 파일 이름.

- 배경음 (BackGroundMusic, BGM; 오브젝트를 누르는 지 여부와 상관없이 항상 재생되는 소리)들이 재생될 순서.

- 배경 동영상(BGA)들이 등장할 순서.

- 오브젝트들의 배열 방법 (패턴, 채보 등으로도 불림.)


1. BMS파일(확장자 *.BMS)

BMS음악의 핵심이며 내용은 우리가 메모장으로 볼수 있는 텍스트 파일로 되어있습니다.
여기에는 곡제목, 제작자, 곡난이도 등의 기본정보.
게임상에서의 음악 출력과 그림 출력형태, 떨어지는 오브젝트 노트의 배치, 어떤 것을 누르면 어떤 소리가 난다는 등의 정보들이 배치되어 있습니다.
일반적으로 BMS파일은 별도의 제작기로 만들지만 그 포맷을 알고 있다면 텍스트 파일로 만드는 것도 가능합니다.
(이는 HTML문서를 텍스트상에서 만드느냐 웹에디터로 만드느냐의 이치와 비슷합니다.)
BMS이외에 BME라는 파일도 있는데 이것은 7건반 이상을 지원한다는 뜻이며 그 이외에는 BMS와 완전히 같은 것입니다.


2. WAV파일(확장자 *.WAV)
윈도우에서 쓰는 표준 사운드 파일이며 BMS에서 음을 출력하는 기본 요소이기도 합니다.
이 WAV화일들은 크게 백그라운드에 출력하는 배경음과 버튼을 누를 때 출력하는 건반음으로 나뉩니다.
건반음들도 건반 위치에 따라, 혹은 곡의 진행에 따라 계속 바뀌기 때문에 거기에 해당하는 WAV들이 따로따로 있어야 합니다.
BMS자료안에 WAV파일이 하나가 아닌 많이 있는 것도 그러한 이유입니다.
간혹 WAV 대신에 MP3를 사용하는 경우도 있지만 BMS에서는 표준이라고는 볼수 없습니다.

3. 미디화일(확장자 *.MID)
미디음악을 출력하기 위한 음악파일이며 BMS에서는 백그라운드에 출력하는 배경음악용으로 쓰입니다.
하지만 PC가 미디를 지원해야 하며 제대로 지원되며 사운드카드 혹은 미디음원의 종류에 따라서 소리가 다르게 나는 수도 있어서
최근의 BMS에서는 거의 쓰이지 않는 추세이기도 합니다.

4. BMP파일(확장자 *.BMP)
윈도우에서 쓰는 표준 그림 파일이며 BMS에서는 플레이중에 화면 애니메이션(BGA:Back Ground Animation)을 출력하기 위해서 사용합니다.
앞에서도 말씀드렸지만 BMS에서는 애니메이션을 동영상 등을 사용하지 않고 그림 한장한장을 교대 혹은 반복해서 보여주는 방식이므로.
그러한 애니메이션의 한컷한컷을 BMP한 장씩으로 해서 만들게 됩니다.
물론 그림이 아예 안나오는 BMS에서는 이러한 BMP파일들이 없습니다.
간혹 BMP대신에 JPG를 사용하는 경우도 있는데
이 경우에는 용량에서 이득을 보지만 게임에 따라서 특수한 플러그인을 사용해야 제대로 지원할수 있습니다.


모든 프로그램에서 똑같이 동작하는 BMS 파일을 만들려면, bmp/wav파일만을 사용해야 합니다. 대부분 프로그램들은 기본적으로 지원하지않는 jpg등 다른 포맷들을 Susie32 플러그인이나 Window Media 등 외부 라이브러리를 사용하여 처리하지만. 별도의 라이브러리 없이 자체적으로 이러한 파일 포맷들을 지원하는 경우도 있습니다.

실질적으로 파싱떄는 노트 기 값과 노트 채널 값, 노트 beat값을 채우게 된다. beat값은 #aaabb에서 aaa가 base beat이므로 이를 이용하여 값을 쉽게 만들 수 있다.

이렇게 얻은 데이터들은 반드시, 적어도 변속BPM/STOP/노트 이 셋 데이터는 단 하나의 List로 묶어서 저장해야 한다. 이 셋은 순차적으로 읽어져야만 하기 때문에 sorting 되어 있어야 한다.

노트 키 값의 경우, 확장 채널을 지원하게 되면서 일반적인 Hex decoding이 아닌 0~9,A~Z의 36진수의 값을 가지게 된다. decoding 함수를 직접 만들어서 xx꼴의 키 값을 integer로 만들어 저장할 수 있도록 한다.

기타 사항은 여러 용도로 쓰이는데, 롱노트임을 표시하거나 눌린 노트/눌리지 않은 노트임을 확인하거나 등의 다양한 용도로 쓰일 수 있다.

노트 time의 경우 파싱이 모두 끝난 후에 별개로 계산하는 과정을 거치는데 이에 대해서는 (2)에서 쓰도록 한다.

일부 특정한 역할을 담당하는 채널(#02 beat 길이와 같이...)이 있는데, beat 길이 채널은 별도로 파싱하여 배열에 저장하도록 한다. (해당 배열은 물론 1.0 값으로 초기화 되어 있어야 한다)



(2) 파싱한 데이터 처리

각각의 키값에 시간을 매긴다. 방법은 다음과 같다.

1. 먼저 첫 비트부터 마지막 비트까지 도는 for 구문을 작성하고

2. 다음 key beat에 도달하기 전에 정수 비트 값을 체크하는 반복 루틴을 만든다. 무슨 의미냐면, 지금 비트가 2.5고 다음 비트가 4.2이면 3비트 ,4비트를 거쳐야 한다는 의미이다.

3. 해당 비트를 거칠 때 비트 구간에 대한 시간을 합산한다. 즉 time += (beat difference) * (1.0/bpm*60*4) * (beat 길이[정수값 beat]) (초)

이렇게 하는 이유는 beat 길이 채널이 존재하기 때문이다.

4. 해당 비트의 시간을 매긴다 (data.time = time). 이때 해당 비트가 STOP이면 time에 합산 해주고, BPM변속이면 bpm 값을 갱신한다.


이렇게 각 비트마다 시간을 얻어내는 이유는 판정을 위해서이다.


파싱한 데이터는 이제 beat에 따라 sorting 한다. 이제 배열은 시간(beat) 순서대로 정렬되어 있기 때문에 순차적으로 접근이 가능하다.



(3) 데이터 그리기

먼저 현재 시간으로부터 비트를 얻어낸다. 방법은 다음과 같다.


1. 첫 비트부터 마지막 비트까지 도는 for 구문에서

2. 각 비트마다 (2)의 방법으로 시간을 도출해낸다. 

3. 시간을 도출해낼때마다 해당 시간이 현재 시간보다 더 값이 큰지를 확인하고, 값이 더 클 경우

4. 비트를 도출해낸다. [현재까지의 beat + (시간차)*(bpm/60/4)/beat 길이[정수값 beat] ]


이제 이 beat로부터 pos값을 얻어낸다. 값은 누적해서 합산한다.


1. 첫 비트부터 마지막 비트까지 도는 for 구문에서

2. 다음 key beat에 도달하기 전에 매 정수값 beat를 체크하는 반복 루틴을 거친다. position은 계속 합산한다. 방법은 3을 참조.

3. position은 다음과 같이 합산한다 - (beat 차이) * speed * bpm * beat길이

4. 이제 position에 해당 노트를 그려주거나, bpm을 변경한다. STOP의 경우는 그냥 무시.

5. 2나 4 과정에서 beat가 정수가 될 때가 있는데 그 때 마디를 표시하는 수평선을 그려줄 수 있다.

6. position이 화면을 벗어나면 반복 구문에서 break;


이 때 현재 beat보다 작은 노트 값들은 miss 처리를 해 줄 수 있다.

miss된 노트임을  확인하기 위해 노트 구조체의 [기타 사항]란에 적당한 데이터를 삽입할 수 있다.



(4) 판정

다음과 같은 조건으로 첫 비트부터 검색을 시도한다 - 누른 키 값과 일치한 유효한 최소 beat를 가진 노트

sorting을 했으면 최소 beat임은 쉽게 파악할 수 있고, 유효한 것은 [기타 사항]란을 참조할 수 있고, 누른 키 값과의 일치성은 channel(key) 값을 참조할 수 있다.

그리고 앞에서 얻은 해당 노트의 시간과 현재 시간과의 차이를 얻어낸 후, 최고 판정부터 유효 시간 이내인지 확인을 한다.

이 이외의 부분은 본인이 자율적으로 처리를 하기 때문에 별도의 설명은 없음.



(5) 롱노트

롱노트는 처리 방법이 조금 까다롭기 때문에 별도로 다룬다.

사실 이렇게 처리하는게 좋은 방법인지는 나도 모른다... 코드가 너무 지저분하기 때문에 -_-.


먼저 파싱 부분부터 쓴다.


<1> 파싱 및 처리

일단 #LNTYPE 1 을 기준으로 데이터를  처리하게 되는데, #LNTYPE 2는 다음과 같이 처리하도록 한다.

1. LNTYPE 2에 해당하는 키 값 발견! 이제 for 구문을 돌리자.

2. 다음부터 검색하는 키 값이 현재 키 값과 일치하지 않으면, 구문에서 빠져나간다.

일치하면, 기존의 키 값을 list에서 빼 버린다.

3. (2)를 반복한다.


그러면 가장 처음과 끝 부분의 키 값만 남아 #LNTYPE 1의 형식의 데이터가 된다.


#LNOBJ의 경우, 노트를 집어넣을 때 추가적인 확인 과정이 필요하다.

1. #LNOBJ로 설정한 노트가 확인되었다 -> list에서 거꾸로 탐색을 시작한다.

2. 현재 channel과 동일한 가장 최근의(반대 순서) note를 찾았다 -> 이 노트와 1의 노트를 50~60대 롱노트 channel로 변경하여 저장한다.


이제 해당 롱노트의 처음과 끝을 마킹한다 (꼭 필요한 과정은 아닌 듯 하지만 난 그렇게 했다)

방법은 쉬우니 언급하지 않겠다. 처음인지 끝인지의 여부는 [기타 사항]란에 저장이 된다.


<2> 그리기

앞에서 완성한 그리는 루틴에 롱노트 루틴을 추가하면 된다.


먼저 첫 비트부터 마지막 비트까지 도는 for 구문에서

1. 롱노트 시작 - 시작할 때의 pos와 시작하는 롱노트 개체(인덱스)를 저장한다. 해당 pos가 아직 화면 표시 단계가 아니라면, lain의 시작 값으로 저장한다.

2. 롱노트 끝(화면 바깥으로 나간 것 포함) - 시작~끝 구간 크기만큼의 노트를 그리도록 한다. 노트를 miss로 그릴지, press로 그릴지, unpress로 그릴지는 '롱노트 시작'이 가지고 있는 데이터에 따른다. 끝 pos가 화면 표시 단계가 아니라면, 무시한다. 롱노트 state를 다시 초기화시킨다.


for 구문이 끝나면, 롱노트 상태를 확인한다. 끝이 나지 않은 롱노트가 있다면 ( 1의 상태), lain의 끝까지 그려주면 된다.


롱노트의 시작 부분이 화면 바깥을 지나가서 BAD 이상의 시간이 지나갔으면 miss 상태로 만든다. 롱노트의 끝 부분이 화면 바깥을 지나가면 화면 바깥을 지난 롱노트 상태로 만들어 준다. (이는 판정 부분의 수월함을 위함이다)


<3> 판정

유효한 롱노트를 눌렀을 때, BAD가 아닌 이상 곧바로 판정을 내지 않는다. 해당 판정은 저장한다.

롱노트를 땠을 때, 너무 일찍 떼지(BAD 판정) 않은 이상 저장된 판정을 표시하도록 한다.

참조로, 롱노트를 누른 상태에서 롱노트의 끝이 화면 바깥으로 나가면(시간이 지나면) 자동으로 떼는 것으로 처리한다.



(6) etc

1. bpm은 말 그대로 1초당 들어있는 비트 수이다. 이를 60으로 나누어 주면 bps가 될 것인데, 4로 또 나누는 이유는 한 비트는 4개의 박자로 이루어져 있기 때문이다(4/4박자). 즉 한 박자의 길이(1/4beat)는 bpm/60/4이며, 이 값으로 1을 나누어 주면 박자 당 초(spb)가 나오게 될 것이다.

2. BMS 파일 포멧에 관한 문서는 여기나 저기를 읽어보라.

3. 내가 썼지만 설명이 참 개판같다. Rhythmus 코드를 참조하라.

4. autoplay의 경우 화면 바깥으로 나간 노트들 중 눌리지 않은 노트가 있을 때 press/release를 시키면 간단하게 구현이 된다.





오래된 글이지만, 몇가지 질문사항이 들어온 김에 글을 업데이트합니다. 위에 잡소리보다 이게 더 도움이 될 것 같네요.


  • 프로그래밍 기술은 눈 깜짝할 새에 바뀌고 있고, 똑같은 특정 기능을 구현하는 방법은 사람마다 천지차이입니다. 다른 사람이 만든 방법은 단순 참조용으로만 쓰시고, 그걸 이해하여 본인의 방법을 구현하는 것이 좋다고 생각합니다. bmx2wav 파일을 임의로 약간 개조한 제 bms 파일 로드 코드나, stepmania repo에 들어가서 코드를 읽어보는 것을 추천합니다.
  • bms 파일 로드 및 처리는 결코 간단하고 쉬운 구현이 아니라고 생각합니다. bms는 수많은 복잡한 객체들로 이루어져 있고, 이에 대한 일대일 혹은 일대다 대응 관계를 잘 파악해야 비로소 파서(게임)를 짤 수 있다고 생각합니다. 그리고, 이를 구현하는 데 도움될만한 몇가지 모델들이 존재하는데, tinyxml2 같은 경우에는 objpool을 만들어서 메모리 누수나 객체의 재생성으로 인한 낭비 없이 굉장히 효율적으로 객체를 관리합니다. 줄여서, 이 글을 읽고 bms에 대한 이해를 충분히 하시고 난 이후에 작업을 진행하세요. 자료구조 공부는 덤.
  • bms의 마디 길이라는 개념은, bms 편집기를 써 보시면 알겠지만 노트의 위치를 변화하지 않고 단순히 measure(마디) 위치만을 변화시킵니다. 따라서 bms object(노트, stop/bpm 명령 등 ...)는 마디가 아닌 다른 절대값을 기준으로 위치시켜야 합니다. 이제 이 값을 바(bar)라고 하겠습니다.
  • 마디 길이 이야기를 마저 하자면, 바의 일반적인 길이를 10240이라고 하면, 각 마디에서 구분할 수 있는 최대한의 노트 밀도, 즉 해상력은 10240개가 되겠죠. 만약 마디 길이가 0.5라면 그 마디의 해상도는 5120이 될 것입니다. 너무 작으면 노트를 제대로 표현할 수 없고, 너무 크면 마디 끝까지 갈 수 없게 될 것입니다(999마디의 bar index가 INT_MAX값을 넘어가게 되겠죠?). 제가 위에서 언급한 아이디어에 대한 이해를 돕기 위해 씁니다.[각주:1]
  • 일전의 자료구조 이야기에 이어서, bms object는 항상 정렬되어 있어야 하며, 순차적으로 접근하게 될 것입니다. 따라서 sort를 쓸 수도 있지만, std::map을 이용하여 iteration을 해도 좋을 것입니다.


누차 이야기하지만 다른 사람이 만든 좋은 코드를 읽어보는 것이 큰 도움이 됩니다. stepmaina가 꽤 좋은 코드를 가지고 있지만 꽤 크기가 커서 읽기에 어려운 점이 있습니다. 제가 만든 코드는 다른 사람(CHILD님의 코드를 차용, 게다가 본인의 발코딩 실력!)의 코드를 빌려온 거라 꽤 더러워서 추천하기는 어렵네요. 현재 bmsbelplus 프로젝트를 리팩토링하여 ojm/bmx/vos/sm 등의 파일들을 읽게 만드는 프로젝트를 진행중인데, 그때가 되면 좀 읽을만한 코드가 나오지 않을까 싶습니다.