본문 바로가기

졸프-오만과 편경

[기술블로그] 딜레이에도 적절히 작동하는 리듬게임 채점 구현

프로젝트 :  '편경'이라는 전통 타악기를 이용한 리듬게임 구현

 

배경 : 초기에는 채점을 delta time 값을 이용하여, 음악의 속도(bpm)과 비교해 올바른 시간 간격으로 타격이 이루어지는지를 기준으로 채점을 구현하였다. 하지만 매번 씬에 작고 랜덤한 딜레이가 생겨서 unity play mode에서 돌려볼 때 마다 각각 다른 딜레이가 생겼고 (음악이 뒤로 갈수록 실제 시간에 비해 점점 느려지거나 빨라짐) 그래서 실제 시간 값(초 단위)이나 프레임 시간 단위인 deltaTime값을 가지고는 정확한 채점을 구현할 수 없었다.

 

목표 : 사용자가 들리는 음악에 맞추어 정확하게 연주하였을 때, 딜레이와 무관하게 그에 맞는 점수 부여하기.

 

먼저, 가장 중요한 포인트는 audio source의 time samples값을 활용하는 것이다.

- 음악이 느려지거나 빨라질 때에 실제 시간을 랜덤한 그 딜레이에 맞춰 적용하는 것은 불가능하므로 현재 음악이 얼만큼 재생되었는가를 나타내는 필드인 AudioSource.timeSamples 값을 이용하였다.

 

1. SoundSystem.cs의 update 함수.

 

void Update(){
        if(Data.answers_tsample[Data.selected_song][NoteManager.noteCnt]-bgm.timeSamples<=note_term
                                    && NoteManager.noteCnt < Data.answers[Data.selected_song].Length){
            NoteManager.noteCnt++;
            AnsManager.CurrentAns=Data.answers[Data.selected_song][NoteManager.noteCnt-1];
        }
    

 

- 모든 '시간'값은 실제 시간과는 무관하게 timeSample값 사용하여 돌려볼 때 마다 딜레이에 함께 영향을 받는다. 딜레이가 길어져도 서로 엇갈리지 않고 함께 미뤄지기 때문에 최소한의 플레이가 가능한 환경을 유지한다. 이를 위해 직접 편경 돌을 연주하여, 타격 시의 timeSamples값을 측정하고, 여러 트라이얼의 값을 평균 내어 활용하였다.

 

- 모든 음의 타이밍이 지나갈 때 까지, 음이 연주되어야 할 차례 (완전한 정답 시간으로부터 약 +-0.5초 가량, timeSample값이기 때문에 정확한 초단위 시간으로 환산할 수는 없음)가 오면 정답값에 현재 연주되어야 하는 돌의 인덱스 값을 넣어주고 정답 순서를 1 증가해준다.

 

Data: 여러 스크립트에서 사용되는 변수들을 모아서 저장해놓은 스크립트

answers_tsample: 각 곡, 각 음의 완전 정확한 정답 기준 timeSamples값을 저장해둔 배열

selected_song: 현재 연주하고 있는 곡의 인덱스

bgm: 배경 음악 오디오 소스

note_term: 정답 음이 업데이트 되는 시간 간격

NoteManager: 전체적인 play씬 진행을 담당하는 스크립트

noteCnt: 현재 연주되어야 하는 정답 값의 순서를 저장하는 변수

CurrentAns: 현재 연주되어야 하는 돌의 인덱스 값

answers: 각 곡의 연주되어야 하는 각 음을 순서대로 돌의 인덱스 번호로 저장해놓은 배열

 

2. 타격시 호출되는 함수

 

public void OnCollisionEnter(Collision col){
        for (int i = 0; i < 16; i++){
            if (col.collider.CompareTag(i)){
                hitCheck = i;
                break;
            }
        }
        Data.hit_tsample=SoundSystem.bgm.timeSamples;
}

 

- 타격 시의 timeSamples값을 저장해준다.

 

- 추후 옳은 음이 연주되었는지를 채점에 반영하기 위해 타격된 돌의 번호를 저장한다.

 

hit_tsample: 가장 최근에 타격이 이루어진 시점의 timeSample값을 저장하는 변수

hitCheck: 타격된 돌의 번호를 저장하는 변수

 

3. PlayerInput.cs의 타격 점수를 계산하는 함수

 

public int CheckTiming()
    {
        double interval=Math.Abs(Data.hit_tsample-Data.answers_tsample[Data.selected_song][NoteManager.noteCnt-1]);
        if(interval<=1000){
            return 0;
        }
        if(interval<=3000){
            return 1;
        }
        if(interval<=7000){
            return 2;
        }
        if(interval<=13000){
            return 3;
        }
        return 5;//miss
    }

 

- 더 좋은 타이밍, 즉 더 정답 기준값에 가까운 타이밍에 타격이 이루어질 수록 작은 값을 리턴한다. (점수 계산에 이용한다.)

 

- 정답값 앞 뒤로 1000 timeSamples 이내에 타격이 이루어졌다면 perfect, 3000 timeSamples 이내라면 cool, 7000 timeSamples 이내라면 good, 13000 timeSamples이내라면 bad로 판정하고 적절한 값을 리턴시켰다. 만약 그렇지 못했을 경우에는 miss로 간주하고 4를 리턴하여 점수 계산에 반영하지 않았다.

 

- 이 때, 1000, 3000, 7000, 13000의 간격은 실험적으로 여러번 플레이 하여 perfect 하게 연주할 수 있지만 매번 잘했다고만 판정이 나오진 않도록 임의로 설정하였다. 점점 채점의 간격이 넓어지도록 하여 bad 판정은 실수를 한 경우가 아니라면 웬만하면 나오지 않도록 조절하였다.

 

interval: 정답 기준 값과 타격이 이루어진 시점의 timeSamples값의 차의 절댓값

 

4. PlayerInput.cs에서 사용자의 점수를 계산하는 부분

 

if (collision.hitCheck!=-1&&NoteManager.noteCnt>=1)
        {
            timeVal=CheckTiming();//가장 맞는 것 부터 0 1 2 3
            if (AnsManager.CurrentAns == collision.hitCheck)
            {
                playScore += (10 - timeVal * 2);//점수를 더해서 넣어준다.
            }
                collision.hitCheck = -1;//검사 완료 후 돌 상태 -1로 돌려놓기
        }

 

- 해당 스크립트의 update함수 안에 위치하여 매 프래임마다 호출된다. hitCheck값은 타격된 돌의 값을 저장하는 동시에 타격이 이루어지지 않는 동안은 -1로 해두어 타격이 이루어졌는지 여부를 판단하는 용도로도 사용하였다.

 

- 먼저 정답이 얼마나 정확한 타이밍에 연주되었는지 계산하여 timeVal에 넣는다.

 

- 현재 연주되어야 하는 음과 연주된 음이 일치하는지 확인한다. 일치할 경우 점수를 계산한다. 하나의 음 당 만점은 10점이다. 정답 기준값에서 멀리 연주하였다면 timeVal값이 1, 2, 3으로 커지기 때문여 timeVal*2 만큼씩 10에서 빼주면 정확도에 따라 차등 점수를 부여할 수 있다.

 

- 실수 타격에 의해 CheckTiming함수에서 5를 리턴 받았다면 10-5*2의 값이 점수에 추가되므로 채점에 반영되지 않는 셈이다.

 

- 정답음과 일치하지 않는 돌을 무작위로 연주하거나 실수하였을 경우에는 아무 일도 일어나지 않는다.

 

playScore: 해당 플레이 씬 동안 사용자가 획득한 점수를 누적 저장하는 변수

timeVal: 타이밍 채점 값을 담는 변수

 

4. 결과화면을 세팅하는 ResultManager.cs의 점수반영 별 세팅 부분

 

 if(PlayerInput.playScore > 0.5 * Data.max_scores[Data.selected_song])
        {
            left.isOn = true;
            if(PlayerInput.playScore > 0.7 * Data.max_scores[Data.selected_song])
            {
                right.isOn = true;
                if (PlayerInput.playScore > 0.9 * Data.max_scores[Data.selected_song])
                {
                    center.isOn = true;
                }
            }
        }

 

- 사용자가 획득한 최종 점수 값이 곡 당 만점의 50퍼센트를 넘을 경우 별 1개, 70퍼센트를 넘을 경우 1개를 더 켜서 총 2개, 90퍼센트를 넘을 경우 3개의 별이 켜지도록 하였다.

 

- 각 곡당 연주되는 음의 개수가 다르기 때문에 곡 마다 획득 가능 최대 점수가 다르다. 그러므로 만점에 얼마나 가까운지를 상대적으로 계산하여 별로 표시하였다.

 

max_scores: 각 곡의 만점을 저장해놓은 배열

left, right, center: 결과 화면에 위치한 별(toggle) 객체

 

다음과 같은 결과화면을 출력한다.

 

다음과 같은 결과화면을 출력한다.

 

 

 

 

 

전체 코드:

https://github.com/imHyejinPark/OMGPG

 

참고하면 좋을 문서:

유니티 timeSamples 도큐먼트

https://docs.unity3d.com/kr/530/ScriptReference/AudioSource-timeSamples.html