조작감·확률
점프·반동·입력이 손에 붙게 만드는 숨은 보정과, 표기대로 나오게 하는 확률 설계다.
목차 (10)
점프가 손에 붙는 이유: 코요테 타임과 점프 버퍼
같은 중력과 점프력인데 어떤 게임의 점프는 답답하고 어떤 게임은 손에 붙는다. 차이는 대개 눈에 안 보이는 몇 프레임짜리 보정이다. 이 보정은 초보를 위한 배려라기보다 지연 보상에 가깝다. 플레이어가 보는 화면은 늘 조금 과거다. 입력 장치, 게임 루프, 렌더 대기열, 모니터를 거치는 동안 수십 ms가 지나고(게임 성능 노트의 입력 지연), 60fps에서 그것은 몇 프레임이다. 화면에서 캐릭터가 발판 끝에 선 것을 보고 바로 눌러도, 입력이 게임에 닿을 때 캐릭터는 이미 허공에 있다.
- 코요테 타임(coyote time): 땅에 닿아 있는 동안 타이머를 N프레임으로 채우고, 공중에서는 매 프레임 줄인다. 점프 조건을 “땅에 있음”에서 “타이머가 남음”으로 바꾸면 발판을 벗어난 뒤 잠깐 동안의 입력도 받는다. 점프가 나가는 순간 타이머를 0으로 비워야 공중에서 한 번 더 뛰지 않는다. 이름은 절벽 밖으로 달려 나간 뒤 잠깐 허공에 멈춰 있는 만화 캐릭터에서 왔다.
- 점프 버퍼(jump buffer): 버튼을 누른 프레임에 버퍼를 N프레임으로 채우고 매 프레임 줄인다. 점프 조건을 “이번 프레임에 눌렀나”에서 “버퍼가 살아 있나”로 바꾸면, 착지 직전에 조금 일찍 누른 입력이 버려지지 않고 착지 프레임에 실행된다.
두 보정은 여는 방향이 반대다. 코요테 타임은 늦은 쪽을, 점프 버퍼는 이른 쪽을 받는다. 발판 끝에서 뛰고 착지하자마자 다시 뛰는 연속 점프는 두 타이밍을 모두 요구하므로, 하나만 있으면 반쪽이다. 사람의 타이밍 오차는 앞뒤로 퍼지기 때문에, 몇 프레임만 열어 줘도 “분명히 눌렀는데 안 뛰었다”가 크게 준다. 그렇다고 난이도가 사라지지는 않는다. 막 누른 입력까지 구제하는 것이 아니라 의도한 입력을 통과시키는 장치다.
나머지 두 가지는 점프의 모양을 다듬는다.
- 가변 점프 높이: 버튼 하나로 낮게도 높게도 뛰게 한다. 버튼을 떼는 순간 아직 올라가는 중이면 상승 속도를 한 번 깎거나(매 프레임 곱하면 금방 0이 되니 뗀 순간에만), 반대로 누르고 있는 짧은 시간 동안 상승 속도를 유지시킨다.
- 상승·하강 비대칭 중력: 내려올 때 중력을 키우면 최고 높이는 그대로인 채 체공 시간이 줄어, 공중에서 할 일이 없는 시간이 짧아진다. 정점 근처에서 버튼을 누르고 있으면 중력을 절반으로 줄여 정점을 늘리는 방법도 있다.
셀레스트(Celeste)는 플레이어 코드를 공개했는데, 코요테 타임 0.1초(60fps에서 6프레임), 누르고 있는 동안 상승을 유지하는 가변 점프, 정점 근처의 절반 중력, 낙하 속도 상한이 그대로 들어 있다. 개발자는 이런 장치들을 “타이밍과 위치의 창을 넓히는 일”로 묶어 설명했고, 벽 모서리에 머리를 살짝 박았을 때 옆으로 밀어 주는 모서리 보정도 같은 계열이다.
반동 패턴과 탄 퍼짐: 외워지는 쪽과 외울 수 없는 쪽
경쟁 FPS의 연사 반동은 연습장에서 외워진다. 외워진다는 것은 반동이 랜덤이 아니라는 뜻이다. 총기 반동은 보통 두 층으로 나뉜다.
| 층 | 하는 일 | 성격 |
|---|---|---|
| 반동 표(spray pattern) | 몇 번째 발인지를 넣으면 조준점이 얼마나 밀리는지가 나오는 배열. 보통 처음 몇 발은 가파르게 오르고 그 뒤 좌우로 흔든다 | 정해져 있다. 마우스를 반대로 당겨 상쇄하는 법을 배울 수 있다 |
| 탄 퍼짐(spread) | 밀린 조준점을 중심으로 한 원뿔 안에서 실제 탄착점을 뽑는다 | 난수다. 아무리 외워도 없앨 수 없다 |
반동을 통째로 난수로 하면 코드는 짧지만, 연습이 보상받지 못한다. 못하는 사람은 난수가 있든 없든 비슷하게 못하고, 패턴을 다 익힌 사람만 무너진다. 즉 난수는 난이도를 낮추는 것이 아니라 실력의 천장을 낮춘다. 또 경쟁 게임은 클라이언트를 믿지 않으므로 서버가 같은 탄착을 다시 계산할 수 있어야 하는데, 발 번호로 찾는 고정 표나 시드를 공유한 난수면 그게 쉽다. 리플레이와 킬캠도 같은 이유로 결정적이어야 한다.
반대로 전부 고정하면 다 외운 사람이 연사만 하면 되는 게임이 된다. 그래서 외울 수 없는 성분을 필요한 만큼만 얹는다. 발로란트는 패치 노트(11.08 등)에서 처음 몇 발(보호 탄수)은 수평 반동이 정해져 있다가 그 뒤 확률로 좌우가 바뀌는 구조를 숫자와 함께 공개해 왔다.
- 퍼짐은 머리에서 보인다: 원뿔이 표적보다 작으면 퍼짐은 명중에 영향을 거의 못 준다. 같은 퍼짐이 몸통 판정에서는 안 보이고 머리 판정에서만 드러나, 헤드샷 게임과 몸통 게임의 체감이 여기서 갈린다.
- 회복과 리셋: 쏘다 멈추면 조준점이 서서히 돌아오고, 일정 시간 이상 쉬면 패턴이 첫 발로 되돌아간다. 점사·단발이 정확한 것은 조준이 좋아서가 아니라 매번 첫 발을 쏘기 때문이다. 이 리셋 시간이 탭·점사·연사 사이의 균형을 정하는 손잡이다.
- 자세·이동 배수: 앉기·정지·조준에 따라 퍼짐(과 반동)에 배수를 곱한다. 연사 구간은 패턴이 지배하므로 퍼짐 배수는 주로 첫 발과 점사에서 체감된다. “첫 발 정확도”가 따로 스펙으로 존재하는 이유다.
- 분포 모양: 같은 반지름이라도 원판 위에서 고르게 뽑느냐, 중심에 몰린 정규분포로 뽑느냐에 따라 체감이 다르다. 원판에서 고르게 뽑으려면 반지름에 √(난수)를 곱해야 한다. 그냥 난수를 곱하면 중심에 과하게 몰린다.
그래서 반동 설계의 핵심은 세기 숫자보다, 외워서 잡을 수 있는 부분과 운에 맡기는 부분을 어떤 비율로 섞느냐다. 그 비율이 플레이어의 연습이 얼마나 보답받는지를 정한다. 마우스 감도·센서는 마우스 노트, 서버 판정은 멀티플레이 노트에 있다.
선입력: 인풋 버퍼와 링크 창
공격 동작 중에 회피를 눌렀는데 아무 일도 없다가, 동작이 끝나는 순간 회피가 나가는 게임이 있다. 눌린 입력을 들고 있다가 쓸 수 있게 되는 순간 꺼내는 인풋 버퍼(input buffer, 선입력)다. 켜고 끄는 스위치가 아니라 서로 다른 숫자 몇 개의 조합이다.
| 숫자 | 뜻 | 너무 작으면 | 너무 크면 |
|---|---|---|---|
| 버퍼 길이 | 눌린 입력을 몇 프레임 들고 있나 | 정확한 한 프레임에 눌러야 해서 결국 연타하게 된다 | 당황해서 여러 번 누른 입력이 남아 원치 않는 회피가 한 번 더 나간다 |
| 버퍼 개방 시점 | 지금 동작의 몇 프레임째부터 입력을 받나. 그 전 입력은 버린다 | 너무 늦게 열면 끝나기 직전 입력까지 씹힌다 | 공격을 시작하자마자 회피를 예약할 수 있어, 큰 공격을 지르는 위험(커밋)이 사라진다 |
| 링크(캔슬) 창 | 공격 N의 회수 동작을 끊고 N+1로 이어 붙일 수 있는 구간 | 콤보가 잘 안 이어진다 | 콤보가 너무 빨라지고 공격 사이의 틈이 사라진다 |
| 콤보 유지 시간 | 공격이 끝난 뒤 “다음은 2타”라는 상태를 얼마나 유지하나 | 공격 사이에 이동이나 회피를 끼우면 처음부터 다시 1타 | 한참 뒤에 눌러도 콤보가 이어져 리듬이 흐려진다 |
버퍼가 없으면 플레이어는 둘 중 하나를 고른다. 가장 빠른 타이밍을 노리면 오차의 절반이 창 앞으로 새서 씹히고, 안전하게 창 가운데를 노리면 매번 몇 프레임씩 느려진다. 실제로는 셋째 답, 연타를 고른다. 대충 여러 번 두들기면 그중 하나는 창에 걸리기 때문이다. 버퍼를 몇 프레임만 주면 창 앞으로 샌 입력이 기다렸다가 들어가서, 한 번 정확히 누르는 쪽이 연타보다 빠르고 정확해진다. 그래서 버퍼는 게임을 쉽게 만드는 장치가 아니라 연타하지 않아도 되게 만드는 장치다.
버퍼 길이 하나로는 “씹히지 않게”와 “커밋은 남게”를 동시에 맞추기 어렵다. 길이로 늦은 입력을 살리고, 개방 시점으로 너무 이른 입력을 잘라 내야 둘이 함께 선다. 콤보 유지 시간은 시간이라기보다 콤보 사이에 끼워 넣을 수 있는 행동 길이의 예산으로 읽으면 쉽다. 유지 시간이 회피 동작보다 짧으면 회피를 섞은 콤보가 끊긴다. 캐릭터마다 회피 길이가 다르면 함께 손봐야 한다.
값은 게임마다, 상황마다 다르다. 스트리트 파이터 6는 패치 노트에서 다운 뒤 일어날 때나 특정 기술 뒤처럼 상황별로 버퍼 프레임 수를 고쳐 왔고, 버퍼가 열리는 시점이 1프레임 어긋나 콤보가 끊기던 버그를 따로 고친 적도 있다. 정답이 하나 있는 것이 아니라, 무엇을 지키고 무엇을 포기할지의 선택을 프레임 단위로 내리는 일이다. 패드 입력의 데드존은 컨트롤러 노트에 있다.
가속 곡선: 최고 속도보다 거기 닿는 시간
캐릭터의 무게감은 최고 속도보다 얼마나 빨리 붙고 얼마나 빨리 서는지에서 나온다. 같은 최고 속도라도 0.05초 만에 닿으면 가볍고 날렵하고, 0.5초가 걸리면 무겁고 미끄럽다. 그래서 이동은 보통 값 몇 개로 따로 튜닝한다.
- 가속과 감속: 스틱을 밀 때 속도가 붙는 빠르기와 손을 뗄 때 멈추는 빠르기다. 감속을 가속보다 크게 두면 멈춰야 할 곳에서 잘 멈춘다.
- 방향 전환: 달리던 반대쪽으로 꺾을 때의 감속이다. 크게 두면 즉시 돌아서고, 작게 두면 미끄러지며(skid) 관성이 보인다.
- 공중 조작: 공중에서는 같은 가속에 1보다 작은 배율을 곱해, 점프의 방향은 바꿀 수 있되 땅만큼 마음대로는 안 되게 한다.
- 최고 속도를 넘었을 때: 대시나 발판에서 받은 속도로 최고 속도를 넘으면, 평소 가속보다 약하게 깎아 내려 그 속도가 잠시 남게 한다. 넘친 속도를 바로 잘라 내면 기술로 얻은 속도가 아무 쓸모 없게 느껴진다.
공개된 셀레스트 플레이어 코드에서 숫자를 볼 수 있다. 달리기 최고 속도는 초당 90픽셀, 가속은 초당 1,000픽셀²이라 멈춘 상태에서 0.09초(60fps에서 5~6프레임) 만에 최고 속도에 닿는다. 공중에서는 이 가속에 0.65를 곱하고, 최고 속도를 넘은 속도는 초당 400픽셀²으로 더 천천히 깎는다. 시작과 멈춤이 거의 즉각적이어서 정밀 플랫포머다운 날렵함이 나오고, 대시로 얻은 속도는 조금 오래 남는다. 같은 구조를 다시 만들어 보는 과정은 Recreating Celeste's Movement 영상에서 볼 수 있다.
해상도가 낮던 시절의 게임도 같은 일을 했다. 화면 좌표는 픽셀 단위여도 내부 위치와 속도는 픽셀보다 잘게 나눈 고정 소수점(서브픽셀)으로 들고 있어, 한 프레임에 1픽셀도 안 되는 가속이 몇 프레임씩 쌓여 서서히 빨라지는 움직임이 나온다. 위치를 정수 픽셀로만 들고 있으면 가속은 “0픽셀, 0픽셀, 1픽셀”처럼 계단으로만 표현된다.
카메라: 창, 내다보기, 부드럽게 따라가기
카메라를 캐릭터 한가운데에 못 박으면 작은 움직임에도 화면 전체가 흔들려 어지럽고, 점프할 때마다 배경이 위아래로 출렁인다. 2D 게임 카메라는 대개 세 가지 장치로 “따라가되 덜 움직이게” 만든다.
- 카메라 창(camera window, 데드존): 화면 가운데에 보이지 않는 상자를 두고, 캐릭터가 그 안에 있는 동안은 카메라를 움직이지 않는다. 캐릭터가 가장자리를 밀면 민 만큼만 상자째 따라간다. 세로 방향은 더 아껴서, 점프 중에는 따라가지 않다가 새 발판에 내려선 뒤에야 그 높이로 맞추는 방식(platform snapping)을 많이 쓴다.
- 내다보기(lookahead): 달리는 방향 앞쪽이 더 중요하므로, 카메라 목표를 진행 방향으로 얼마간 옮겨 앞을 넓게 보여 준다. 방향을 바꿀 때마다 목표가 확 넘어가면 화면이 출렁이므로, 일정 거리 이상 그쪽으로 가거나 일정 시간 이상 같은 방향을 유지할 때만 넘기고, 넘길 때도 천천히 옮긴다.
- 부드럽게 따라가기(smoothing): 목표가 정해지면 카메라는 그 자리로 감쇠하며 다가간다. 이때 프레임마다 고정 비율로 lerp하면 프레임률에 따라 카메라 감각이 달라지므로, dt를 넣은 지수 감쇠나 감쇠 스프링을 쓴다. 식은 애니메이션 노트의 따라가기 절에 있다.
여기에 보스나 숨은 길처럼 보여 주고 싶은 곳으로 카메라를 잠시 끌어당기는 관심 지점, 여러 플레이어가 모두 화면에 들도록 확대·축소하는 처리가 더해진다. 3D에서는 캐릭터와 카메라 사이를 막는 벽에 카메라가 파묻히지 않게 선을 쏴 카메라를 당겨 오는 처리가 기본이다. 수많은 2D 게임의 카메라를 갈래별로 나눠 보여 주는 How Cameras in Side-Scrollers Work 발표가 이 분야의 좋은 지도다.
주스: 화면 흔들림과 히트스톱
같은 규칙, 같은 판정의 게임이라도 맞았을 때 화면이 반응하면 훨씬 손맛이 난다. 이런 덧붙임을 흔히 주스(juice)라고 부른다. 게임 규칙은 하나도 안 바꾸고 “맞았다”는 사실을 더 크게 전하는 장치들이다.
- 화면 흔들림(screen shake): 프레임마다 난수로 카메라를 튕기면 떨림이 프레임률에 따라 달라지고 지직거리는 잡음처럼 보인다. 그 대신 시간을 입력으로 받는 매끄러운 노이즈(펄린 노이즈 등)에서 흔들림 값을 읽는다. 세기는 충격이 쌓는 0~1 사이 값(흔히 trauma라 부른다)을 두고 시간에 따라 줄이되, 실제 흔들림은 그 값의 제곱이나 세제곱에 비례하게 하면 작은 충격은 살짝, 큰 충격은 확실히 흔들린다. 2D에서는 위치에 작은 회전을 섞고, 3D 1인칭에서는 위치를 옮기기보다 시선 각도를 흔드는 편이 벽에 파묻히지 않고 덜 어지럽다.
- 히트스톱(hitstop, hit pause): 공격이 맞는 순간 때린 쪽과 맞은 쪽을 몇 프레임 동안 멈춘다. 짧은 정지가 충돌에 무게를 실어 주고, 강한 공격일수록 길게 멈춰 세기 차이를 몸으로 느끼게 한다. 격투 게임은 이 정지 동안에도 입력을 받아 두어 연계를 잇게 한다(위 선입력 절의 인풋 버퍼).
- 그 밖의 양념: 맞은 대상이 한 프레임 하얗게 번쩍이기, 넉백, 파편과 먼지, 짧은 진동, 낮은 음의 타격음이 함께 간다.
주스는 넘치면 정보를 가린다. 흔들림이 크면 다음 위협을 못 보고, 멀미가 나는 사람도 있다. 그래서 흔들림 세기를 줄이거나 끌 수 있는 접근성 설정을 두는 게임이 늘었다. 같은 게임에 주스를 하나씩 더해 가며 달라지는 느낌을 보여 주는 Juice it or lose it와 The art of screenshake, 노이즈 흔들림과 카메라 감쇠의 수학을 다루는 Juicing Your Cameras With Math 발표가 좋은 참고다.
확률이 표기와 달라지는 곳: 모듈러 편향과 거부 샘플링
“1%”라고 적힌 확률이 정말 1%로 나오려면 코드 몇 곳이 맞아떨어져야 한다. 흔히 의심하는 소수점 오차는 대개 범인이 아니다. 게임은 확률을 보통 정수로 뽑는다. 0~9999 가운데 하나를 뽑아 100 미만이면 당첨, 이런 식이라 비율 자체는 정확하다. 문제는 그 정수를 만드는 방법이다.
난수 함수는 큰 범위의 정수를 준다. C의 rand()는 표준상 최소 0~32767이고(윈도우 C 런타임은 딱 그 범위, 리눅스 glibc는 0~2³¹−1), 요즘 생성기는 32·64비트 정수를 준다. 이걸 작은 범위로 접는 가장 짧은 코드가 나머지 연산 rand() % n이다. 큰 범위의 개수가 n으로 나눠떨어지지 않으면, 남는 몫이 앞쪽 값들에 한 번씩 더 돌아간다. 이것이 모듈러 편향(modulo bias)이다.
- 범위가 작으면 편향도 작다: 32768개를 100칸에 접으면 앞 68칸이 328번, 나머지가 327번씩 나와 차이가 0.3% 정도다.
- 범위가 크면 치명적이다: 같은 난수를 20000칸에 접으면 앞 12768칸은 두 번, 나머지는 한 번씩 나온다. 앞쪽 값이 뒤쪽 값보다 두 배 잘 나온다. 정밀도를 높이려고 칸 수를 늘리는 순간 편향도 함께 커진다. 32비트 난수도 칸을 10억 개쯤 잡으면 같은 일이 생긴다.
- 방향은 표 순서가 정한다: 가중치 표는 보통 앞 항목부터 누적으로 구간을 잡는다. 그러면 표 앞에 적힌 등급이 “더 자주 나오는 앞쪽 값”을 받아 표기보다 높아지고, 뒤에 적힌 등급은 낮아진다. 표를 정리하며 순서만 바꿔도 확률이 바뀐다.
다른 흔한 실수는 경계다. 마지막 칸을 “이하”여야 할 자리에 “미만”으로 걸면 그 칸은 한 번도 안 나온다. 같은 “범위” 함수라도 끝을 넣는지 빼는지가 API마다 달라서 생긴다. 유니티의 Random.Range는 정수판은 최댓값을 빼고 실수판은 넣으며, 언리얼의 FMath::RandRange(정수)는 양끝을 넣고, .NET의 Random.Next(min, max)는 최댓값을 빼고, 파이썬은 randint가 넣고 randrange가 뺀다.
고치는 방법은 셋이다.
- 거부 샘플링(rejection sampling): n으로 나눠떨어지는 가장 큰 범위를 넘는 값은 버리고 다시 뽑는다. 남은 값은 모든 칸에 똑같이 나뉘어 정확하다. 다시 뽑을 확률은 버리는 비율만큼이라 평균 비용이 작다. OpenBSD의
arc4random_uniform이 이렇게 한다. - 곱해서 접기: 난수를 0~1 실수로 바꿔 n을 곱하거나, 정수 곱셈의 상위 비트를 쓰는 방법(Lemire 방식)이다. 남는 몫이 앞쪽에 몰리지 않고 고르게 흩어져 “표 앞 등급이 유리한” 문제는 사라지지만, 원래 난수의 해상도만큼의 미세한 차이는 남는다. 정확해야 하면 거부 샘플링과 함께 쓴다.
- 충분히 큰 난수: 64비트 난수를 작은 범위로 접으면 편향이 실용상 무시할 만큼 작다. 다만 습관이 남아 있으면 칸 수를 키울 때 같은 문제가 돌아온다.
대부분의 엔진 난수 API는 범위 정수를 줄 때 이미 이런 처리를 한다. 직접 %로 접는 코드가 위험 지점이다.
덜 랜덤한 랜덤: 7-백, 히스토리 재추첨, PRD
테트리스에서 같은 조각이 세 번 연속 나오거나 I 조각이 한참 안 오는 일이 거의 없는 것은 우연이 아니다. 일곱 조각 가운데 하나를 매번 독립으로 뽑으면(진짜 랜덤) 같은 조각이 바로 또 나올 확률이 1/7, 세 번 연속도 1/49로 꽤 자주 생기고, 무엇보다 특정 조각이 수십 개 동안 안 오는 “가뭄”이 한 판에 한 번쯤은 생긴다. 평균은 맞는데 짧은 구간에서 뭉치고 마른다. 사람은 짧은 구간에서도 고르기를 기대하므로, 랜덤은 공평한데 공평하게 느껴지지 않는다.
- 7-백(7-bag): 테트리스 가이드라인의 Random Generator다. 일곱 조각을 전부 가방에 넣고 섞어 차례로 꺼내고, 다 꺼내면 다시 채워 섞는다. 정의에서 바로 보장이 나온다. 같은 조각 연속은 앞 가방의 끝과 뒤 가방의 처음이 같을 때뿐이라 최대 2번이고, 같은 조각 사이 간격은 최대 12개다. 대가는 예측이 된다는 것인데, 다음 조각 미리 보기와 합쳐져 계획을 세울 수 있게 해 오히려 현대 테트리스의 재료가 됐다. 순서가 시드 하나로 정해지므로 대전에서 두 사람에게 같은 순서를 줄 수도 있다.
- 히스토리 재추첨: 가방 이전의 방식이다. NES판 테트리스는 직전 조각과 같은 게 나오면 한 번만 다시 뽑아 연속은 줄이지만 가뭄은 그대로다. TGM 시리즈는 최근 조각 4개를 기억하고 그 안의 조각이 나오면 몇 번까지 다시 뽑아, 연속과 가뭄을 함께 줄인다. 히스토리가 길수록 가방에 가까워진다. 가방은 정의로 없애고, 히스토리는 확률로 줄인다.
같은 생각이 RPG의 치명타에도 들어간다. PRD(pseudo-random distribution)는 실패할 때마다 확률을 올리고 성공하면 되돌린다. k번째 시도의 확률을 k × C로 두고, 평균이 원하는 확률 p가 되도록 C를 푼다. 워크래프트 3와 도타 2가 쓴 방식으로, 25%면 C는 약 8.5%다. 첫 시도는 8.5%, 두 번째는 17%, … 열두 번째에는 확정이 된다. 평균은 그대로 25%인데 연속 치명타와 긴 불운이 크게 줄어든다. 대신 첫 시도의 확률이 표기보다 낮아, 싸움을 여는 첫 타가 치명타일 확률은 오히려 줄어든다.
세 방법 모두 평균은 건드리지 않고 짧은 구간의 뭉침만 깎는다. 뽑기의 천장(일정 횟수 안에 반드시 나오게 하는 장치)도 같은 계열이다.
가중치 루트 테이블: 행운은 어디를 고치나
무엇이 나올지를 정하는 흔한 구조가 가중치 루트 테이블(weighted loot table)이다. 항목마다 가중치(뽑힐 몫)를 적고, 가중치만큼 칸을 차지하게 한 줄로 이어 붙인 뒤 0부터 합 미만의 난수 하나가 떨어진 칸의 항목을 준다. 확률은 가중치 ÷ 합이다. 항목이 다시 다른 표를 가리키게 해 “보물로 정해지면 보물 표에서 한 번 더 뽑기”처럼 겹쳐 쓸 수 있다. 위 절의 모듈러 편향을 피하려면 난수를 합 범위로 정확히 뽑아야 한다.
마인크래프트(자바 에디션)는 이 표를 게임 데이터 파일에 그대로 적어 둔다. 낚시 표는 물고기 85, 쓰레기 10, 보물 5이고, 항목마다 퀄리티(quality)라는 값이 하나 더 있다. 행운 값이 1 오를 때 가중치를 얼마나 바꿀지다. 고친 가중치는 max(⌊가중치 + 퀄리티 × 행운⌋, 0)이고, 낚싯대의 바다의 행운 인챈트는 단계마다 행운 값을 1씩 올린다.
그래서 행운은 보물 확률을 올리는 동시에 쓰레기 확률을 깎는다. 행운 3이면 보물이 두 배 넘게 늘고 쓰레기는 절반 아래로 준다. 쓰레기의 퀄리티가 −2라 행운 5에서는 가중치가 0이 되어 아예 나오지 않는다. 인챈트로 얻을 수 있는 것은 3단계까지라, 인챈트만으로는 쓰레기가 완전히 사라지지 않는다.
“좋은 것이 더 잘 나오게” 하는 방법은 이것만이 아니다. 고치는 곳에 따라 결과가 다르다.
| 방식 | 고치는 곳 | 결과 |
|---|---|---|
| 가중치에 더하고 빼기(마인크래프트 낚시) | 표 자체 | 나쁜 항목을 0까지 깎을 수 있다 |
| 개수에 곱하기(마인크래프트 행운 곡괭이) | 뽑힌 뒤의 개수 | 무엇이 나올지는 그대로이고 개수만 는다. 3단계 행운으로 다이아몬드 광석을 캐면 40%는 그대로 1개, 나머지는 2·3·4배가 각각 20%다 |
| 두 번 뽑아 좋은 쪽(D&D 5판의 유리함) | 뽑는 횟수 | 좋은 결과가 늘고 나쁜 결과는 제곱으로 줄지만 0은 되지 않는다 |
| 단계마다 확률을 따로 적기(마인크래프트 자갈의 부싯돌) | 표를 단계별로 통째로 | 원하는 값을 직접 정할 수 있다. 3단계 행운이면 부싯돌이 확정으로 나온다 |
두 번 뽑기는 계산이 간단하다. 가장 나쁜 결과의 확률이 p면 두 번 다 그것이어야 받으므로 p²로 줄고, 가장 좋은 결과의 확률이 q면 한 번이라도 나오면 되므로 1 − (1 − q)²로 는다. 같은 “행운”이라도 표를 고치느냐, 개수를 고치느냐, 뽑는 횟수를 고치느냐에 따라 나쁜 결과가 사라지는지가 갈린다.
프레임에 묶인 난수: 고전 게임의 운은 조작된다
옛 콘솔 게임의 난수는 대개 비트 몇 개를 밀고 더하는 짧은 계산 하나다. 상태값을 프레임마다, 또는 난수를 쓸 때마다 한 칸씩 굴려 다음 값을 얻는다. 전원을 켤 때의 시작값이 정해져 있고 계산도 정해져 있으니, 결과는 순전히 몇 번째 프레임에, 몇 번 굴렸는가에 달려 있다.
- 사람에게는 충분히 랜덤하다. 같은 프레임에 같은 입력을 넣는 일은 사람 손으로 거의 불가능하기 때문이다.
- 도구로는 조작된다. 프레임 단위로 입력을 적는 TAS(tool-assisted speedrun)는 원하는 결과가 나올 때까지 버튼을 누르는 프레임을 옮기거나, 난수를 한 번 더 굴리는 행동(적을 한 마리 더 움직이게 하기 등)을 끼워 넣는다. 이것을 난수 조작(RNG manipulation)이라 한다. 사람이 하는 스피드런도 난수를 굴리는 행동을 외워 운을 줄인다.
- 같은 성질이 장점이 되기도 한다. 시작값과 입력 기록만 있으면 한 판이 그대로 다시 돌아가므로, 리플레이를 영상이 아니라 입력으로 저장할 수 있다. 이것은 멀티플레이 노트의 결정적 재생과 같은 원리다.
오늘의 게임에서 배울 점은 난수 줄기를 나누는 것이다. 파티클이나 풀잎 흔들림 같은 꾸밈용 난수와 전리품·치명타 같은 결과용 난수가 한 생성기를 함께 쓰면, 그래픽 옵션을 바꾸거나 이펙트가 하나 더 터지는 것만으로 다음 전리품이 바뀐다. 시스템마다 생성기를 따로 두면 결과는 결과끼리만 순서를 지켜, 재현과 디버깅이 쉬워지고 의도치 않은 조작 경로도 줄어든다. 고전 게임 하나의 난수 루틴을 명령어 단위로 따라가는 Super Mario World - Random Number Generation 영상을 보면 이 구조가 얼마나 단순한지 알 수 있다.
- 이 게임 점프는 왜 손에 붙나 - 코요테 타임과 점프 버퍼
- 스프레이 패턴은 왜 외워지나 - 고정 반동 표와 랜덤 탄 퍼짐
- 액션 게임 조작감은 어떻게 튜닝되나 - 인풋 버퍼와 링크 창
- 게임 확률은 왜 표기와 달라질 수 있나 - 모듈러 편향과 거부 샘플링
- 테트리스는 왜 같은 조각이 연속으로 안 나올까 - 7-Bag과 PRD
- 마인크래프트 행운 낚싯대는 쓰레기 확률을 깎습니다 - 가중치 루트 테이블
- Recreating Celeste's Movement — Mix and Jam
- Super Mario World - Random Number Generation — Retro Game Mechanics Explained
- How Cameras in Side-Scrollers Work — GDC
- Math for Game Programmers: Juicing Your Cameras With Math — GDC
- Juice it or lose it — 강연 녹화
- The art of screenshake — INDIGO Classes 2013
- Celeste 플레이어 코드 — GitHub
- Celeste & Forgiveness — 셀레스트 개발자 글
- VALORANT Patch Notes 11.08 — VALORANT