오늘 한 것: 점수 게임에 소리를 붙였다
2편 마지막에 “콤보가 끊기는 조건, 그다음은 연출”이라고 적어뒀는데 둘 다 손을 못 댔다. 대신 소리 쪽으로 먼저 방향이 잡혔습니다. 도~시 7음계를 전부 위성으로 만들었다. 범위 시작 지점에서 랜덤 간격으로 나오고, 8번 탭하면 모은 음을 곡으로 들려줍니다.
 - 3/gameplay.gif)
위성이 왜 제자리걸음을 했을까?
결론부터 말하면 struct를 List에 넣고 인덱서로 메서드를 불렀기 때문이었다. 공전시키려고 돌렸는데 위성이 전부 제자리에 서 있었다.
public struct SatelliteObject
...
offset += addOffset;
...
m_rotateSatellites[i].RotationSatellite(Time.deltaTime * m_speed, BASE_RADIUS);
List<T>의 인덱서는 저장 공간이 아니라 get 접근자, 즉 메서드라서 T가 struct면 복사본을 돌려줍니다. 필드에 바로 대입했으면 CS1612 에러라도 났을 텐데, 메서드 호출은 C# 사양상 임시 변수를 만들어 거기에 호출하니 에러 없이 넘어간다. offset += addOffset은 버려지는 복사본에만 들어가고 원본은 매 프레임 그대로였던 것.
해결은 struct를 class로 바꾼 한 단어. CS1612 문서에 적힌 해결책이기도 하다. 2편에서 이름이 겹쳐 개명했던 그 struct가 이번엔 발목을 잡았다.
 - 3/body-1.png)
탭 한 번이 노트 하나
var deltaAngle = Mathf.DeltaAngle(obj.offset, CATCH_ZONE_CENTER);
if (deltaAngle > CATCH_ZONE_HALF || deltaAngle <= -CATCH_ZONE_HALF) continue; // 존 밖
int degree = obj.satellite.Data.id;
int octave = deltaAngle > 0 ? 1 : 0; // 리딩 밴드 = +1 옥타브
캐치존은 2편 그대로 상단 90도에서 좌우 60도. 탭하는 순간 존 안에 있는 위성 전부가 한 노트(화음)가 되고, 90도를 아직 안 지난 쪽은 한 옥타브 위로 친다. 같은 음이 양쪽에 동시에 걸리면 하나로 접고 높은 옥타브를 택한다. 빈 탭은 쉼표.
박자는 단순하게 갔습니다. slot에 탭 순서를 그대로 넣고, 실제 탭 간격은 버린 채 8분음표(BPM 100 기준 0.3초) 간격으로 줄 세운다.
아쉬운 건 존에 위성이 잘 안 모인다는 점. 스폰 간격이 Random.Range(1, 8)인데 int 오버로드라 1~7초 정수만 나오고, 화면에 위성이 한두 개뿐인 시간이 길다.
 - 3/screen-1.png)
샘플 하나로 7음을 내고, 절대 시각에 예약한다
int midi = 60 + scale[Mathf.Clamp(n.degrees[v], 0, 6)] + 12 * oct + Transpose;
var z = sf.Nearest(midi);
float ratio = Mathf.Pow(2f, (midi - z.baseMidi) / 12f);
...
src.pitch = ratio;
src.PlayScheduled(when);
음원 파일 이름에서 MIDI 번호를 파싱해 두고, 내야 할 음과 가장 가까운 샘플을 골라 반음 차이만큼 pitch를 조정한다. pitch는 재생 속도를 바꿔 생기는 음높이 변화고 평균율 반음은 주파수 비 2의 12제곱근이니, 2^(n/12)면 n반음 이동이다. 속도가 바뀌는 만큼 멀리 늘릴수록 길이도 달라져서 가까운 샘플부터 고른다(본인 기준).
 - 3/body-2.png)
재생에서 한 번 막혔다. when이 뭐지? PlayScheduled에 넘기는 값은 몇 초 뒤가 아니라 dspTime 타임라인 위의 절대 시각입니다. 지연 초를 넘기면 이미 지난 시각이라 전부 동시에 터진다. 그래서 dspTime + 0.15를 기준점으로 잡고 slot마다 0.3초씩 더한다. 0.15초는 공식 문서가 권하는 100~200ms 여유 안이고, 잡을 때 바로 울리는 피드백은 그러면 늦어서 0.02초로 따로 뒀다. 지금은 곡이 8노트라 한 번에 예약해도 AudioSource 24개 풀로 충분했다.
다음에 할 것
몇 판 돌려보니 속도가 느리고, 위성이 위쪽에 모여 화음이 되는 순간은 좋은데 랜덤이라 붕 뜨는 시간이 많다.
그래서 스폰 템포와 판이 끝나는 조건부터 다시 볼 생각입니다.
참고 소스
- Compiler Error CS1612 - Microsoft Learn
- C# language specification - Expressions - Microsoft Learn
- Random.Range - Unity 공식 Scripting API
- Audio Source component reference - Unity 공식 매뉴얼
- Equal Temperament - SFU Sonic Studio Handbook
- AudioSource.PlayScheduled - Unity 공식 Scripting API
- AudioSettings.dspTime - Unity 공식 Scripting API