룰렛 돌리기를 누르면 5초 남짓 판이 돌다가 서서히 멈춥니다. 그런데 사실 버튼을 누른 그 순간 어느 칸에서 멈출지는 이미 정해져 있어요. 이 얘기를 하면 "그럼 조작 아니야?" 하는 반응이 나오는데, 반대입니다.
돌다가 정해지면 오히려 불공정해진다
룰렛을 진짜처럼 만들려면 처음 속도를 무작위로 주고, 마찰로 점점 느려지다 멈춘 자리를 결과로 삼으면 될 것 같습니다. 그런데 이렇게 하면 결과가 "처음 속도"와 "감속 공식"에 묶여요. 속도 범위를 어떻게 잡느냐에 따라 특정 칸에서 멈추는 비율이 미묘하게 달라지고, 그걸 고르게 맞추기가 꽤 까다롭습니다.
그래서 순서를 뒤집었어요. 먼저 rnd(n)으로 당첨 칸을 고르고, 그 칸에서 멈추도록 회전 각도를 거꾸로 계산합니다. 칸이 6개면 각 칸이 정확히 6분의 1 확률이에요. 계산이 단순하니 틀릴 곳도 없습니다.
멈출 각도 계산하기
var n = S.items.length, seg = Math.PI * 2 / n, win = RT.rnd(n);
var off = (.15 + RT.rnd(70) / 100) * seg; // 칸 경계에 걸치지 않게
var target = -(win * seg + off);
var delta = ((target - angle) % TAU + TAU) % TAU;
var to = angle + TAU * (5 + RT.rnd(3)) + delta;
한 줄씩 보면 이래요.
seg는 칸 하나가 차지하는 각도입니다.off는 칸 안에서 바늘이 가리킬 위치예요. 칸의 15%~85% 사이 아무 데나 고릅니다. 정확히 경계에서 멈추면 "이거 옆 칸 아니야?" 하는 시비가 생기니까 양 끝 15%는 피했어요. 그리고 매번 칸 한가운데에 멈추면 너무 기계 같아 보여서 위치를 흔들었습니다.delta는 지금 각도에서 목표 각도까지 남은 거리예요. 판이 이전 결과 자리에서 출발하니까 매번 다시 계산합니다.- 마지막으로 5~7바퀴를 더 돌게 해서 충분히 돌았다는 느낌을 줍니다.
자연스럽게 멈추는 곡선
시작각과 끝각이 정해졌으니 그 사이를 어떤 속도로 채우느냐만 남습니다. 일정한 속도로 가다 뚝 멈추면 어색해서, 처음엔 빠르고 끝으로 갈수록 느려지는 곡선을 씁니다.
var k = (now - t0) / dur; // 0 → 1
var e = 1 - Math.pow(1 - k, 4); // 끝에서 천천히
angle = from + (to - from) * e;
4제곱을 쓴 건 여러 값을 직접 돌려 보고 고른 거예요. 2제곱은 너무 빨리 멈추고, 5제곱 이상은 마지막 1초가 지루했습니다.
바늘이 칸 경계를 넘을 때마다 "틱" 소리를 내고 바늘을 살짝 튕기는 효과도 넣었어요. 끝으로 갈수록 틱 간격이 길어지는 게 들리면서 긴장감이 생깁니다. 이건 원리라기보다 연출이지만, 체감에는 이게 제일 컸어요.
정리하면
- 결과를 먼저 뽑고 애니메이션을 결과에 맞추면, 확률은 계산 그대로 공정합니다.
- 경계에서 멈추지 않게 여유를 두고, 감속 곡선과 소리로 진짜처럼 보이게 합니다.
- 같은 방식은 제비뽑기에도 그대로 씁니다. 제비 순서를 먼저 섞어 두고, 펼치는 건 보여 주기일 뿐이에요.
직접 해 보기: 룰렛 돌리기