8비트의 벽을 넘은 게임들 Part 3: 엘리트, 우주를 6바이트에 담다
이 글은 Claude Opus 5 를 이용해 초안이 작성되었으며, 이후 퇴고를 거쳤습니다.
저장하지 않으면 용량도 들지 않는다#
Part 2에서 본 슈퍼 마리오브라더스의 전략은 데이터를 작게 만드는 것 이었습니다. 레벨 오브젝트를 2바이트로 인코딩해 게임 전체의 스테이지 데이터를 4,460바이트에 넣었습니다.
1984년 BBC Micro 로 나온 엘리트는 한 걸음 더 나갑니다. 데이터를 아예 갖지 않습니다.
먼저 규모를 봅시다. 엘리트에는 은하가 8개 있고, 은하마다 항성계가 256개 있습니다. 총 2,048개 입니다. 각 항성계에는 고유한 이름이 있고, 좌표가 있고, 경제 형태, 정부 형태, 기술 수준, 인구, 총생산, 행성 반지름, 지배 종족이 있습니다.
이걸 평범하게 저장한다고 해 봅시다. 항성계 하나에 필요한 최소한만 잡아도 이름 8바이트, 좌표 2바이트, 나머지 속성 대여섯 바이트, 합쳐서 20바이트 정도입니다. 2,048개면 약 40KB 입니다.
BBC Micro Model B 의 RAM 은 32KB 입니다. 그중 화면 메모리와 운영체제 작업 영역을 빼고 나면 게임이 쓸 수 있는 건 더 적습니다.
즉 항성계 데이터만으로도 이미 기계에 안 들어갑니다. 게임 코드는 넣을 자리도 없습니다.
Mark Moxon 이 원본 소스를 주석 처리해 공개한 자료에서 카세트판 엘리트의 메모리 사용량을 그대로 옮기면 이렇습니다.
| 항목 | 주소 범위 | 크기 |
|---|---|---|
| 메인 게임 코드 | &0F40~&5639 |
18,170 바이트 |
| 우주선 도면 (파이썬 제외) | &563A~&5FFF |
2,502 바이트 |
| 게임 텍스트 | &0400~&07FF |
1,024 바이트 |
| 파이썬 도면 등 | &7F00~&7FFF |
256 바이트 |
| 게임 코드 합계 | 21,952 바이트 | |
| 참고: 화면 메모리 | &6000~&7EFF |
7,936 바이트 |
21,952바이트. 이 안에 3D 렌더러도, 전투도, 무역 시장도, 도킹도, 은하 8개도 전부 들어 있습니다.
그리고 항성계 2,048개를 저장하는 데 쓴 용량은 6바이트 입니다.
1. 트리보나치 수열이라는 우주#
엘리트의 항성계 하나는 16비트 숫자 세 개 로 완전히 기술됩니다. 이 세 숫자를 씨앗(seed)이라고 부르고, s0, s1, s2 로 씁니다. 6바이트입니다.
이 6바이트에서 이름, 좌표, 경제, 정부, 기술 수준, 인구, 총생산, 반지름, 종족이 전부 계산되어 나옵니다. 어느 것도 저장돼 있지 않습니다.
그리고 다음 항성계로 넘어가는 방법이 이 게임의 심장입니다.
트위스팅#
세 씨앗은 트리보나치 수열 의 연속한 세 항입니다. 피보나치 수열이 앞의 두 항을 더하는 것이라면, 트리보나치는 앞의 세 항을 더합니다.
0 0 1 1 2 4 7 13 24 44 81 149 ...
^ ^ ^
지금 씨앗이 1, 2, 4 를 가리키고 있다면, 한 칸 밀어서 2, 4, 7 로 만드는 것이 트위스팅 입니다.
s0' = s1
s1' = s2
s2' = s0 + s1 + s2
제자리에서 하려면 임시 변수 하나면 됩니다.
tmp = s0 + s1
s0 = s1
s1 = s2
s2 = tmp + s1
이게 전부입니다. 16비트 덧셈 두 번과 대입 세 번. 6502 에서 몇십 사이클이면 끝납니다.
다음 항성계로 가려면 트위스트를 네 번 합니다. 이걸 255번 반복하면 은하 하나의 항성계 256개가 전부 나옵니다.
첫 번째 은하의 0번 항성계는 Tibedied 이고, 그 씨앗은 이렇습니다.
s0 = &5A4A s1 = &0248 s2 = &B753
게임 전체의 우주 2,048개 항성계가 이 여섯 바이트에서 나옵니다.
은하를 바꾸는 방법#
은하 8개는 어떻게 만들까요. 은하 하이퍼드라이브를 쓰면 씨앗의 각 바이트를 왼쪽으로 한 비트 회전 시킵니다.
01234567 -> 12345670
8번 회전하면 원래 값으로 돌아옵니다. 그래서 은하가 정확히 8개입니다. 더 만들 수도 있었지만, 저자들은 여기서 멈추는 편이 낫다고 판단했습니다.
은하가 8개인 이유가 게임 디자인이 아니라 비트 회전의 주기입니다.
2. 씨앗에서 우주를 꺼내기#
이제 6바이트에서 어떻게 그 많은 정보가 나오는지 봅니다. 답은 비트를 잘게 나눠 쓰는 것 입니다. 48비트를 겹치지 않게 나눠서 각 조각에 의미를 부여했습니다.
| 데이터 | 사용하는 비트 |
|---|---|
| 은하 좌표 x | s1_hi 8비트 전부 |
| 은하 좌표 y | s0_hi 8비트를 오른쪽으로 1비트 시프트 |
| 경제 번영도 | s0_hi 의 0~2비트 |
| 경제 유형 (공업/농업) | s0_hi 의 2비트 |
| 정부 형태 | s1_lo 의 3~5비트 |
| 기술 수준 | s1_hi 의 0~1비트 + 경제와 정부에서 유도 |
| 이름 길이 | s0_lo 의 6비트 |
| 이름 토큰 | s2_hi 의 0~4비트 |
| 평균 반지름 | s1_hi 8비트 + s2_hi 의 0~3비트 |
| 종족 | s2_hi 전부 + s0_hi, s1_hi 의 하위 비트 + s2_lo 의 7비트 |
| 행성 및 항성 배치 | s0_hi, s1_hi, s2_hi 의 하위 비트들 |
같은 비트가 여러 용도로 재사용되는 것도 보입니다. s0_hi 의 하위 3비트는 경제이면서 동시에 행성 거리 계산에도 들어갑니다. 비트 하나도 놀리지 않습니다.
파생 값들은 이렇게 계산됩니다.
기술 수준 = (경제 비트 반전) + (s1_hi & %11) + 올림(정부 / 2)
인구 = 기술 수준 * 4 + 경제 + 정부 + 1 (단위: 0.1억)
총생산 = (경제 비트 반전 + 3) * (정부 + 4) * 인구 * 8
반지름 = ((s2_hi & %1111) + 11) * 256 + s1_hi (단위: km)
곱셈이 있긴 하지만 항성계 정보 화면을 열 때만 계산하면 되므로 부담이 없습니다.
이 식들에는 게임 디자인이 슬쩍 섞여 있습니다. 경제 비트를 반전 시켜서 기술 수준에 넣는 이유는, 부유한 경제일수록 큰 값이 나오게 하기 위해서입니다. 그리고 정부가 무정부(0)나 봉건제(1)면 경제 값의 1번 비트를 강제로 세워서 부유한 무정부 상태가 나오지 않게 막습니다. 난수에 그럴듯함을 입히는 장치입니다.
3. 이름도 생성합니다#
가장 놀라운 부분은 이름입니다. 항성계 2,048개의 이름이 어디에도 저장돼 있지 않습니다.
방법은 이렇습니다. 먼저 두 글자 토큰 32개 를 테이블로 갖습니다. 이게 유일한 저장 데이터이고, 64바이트입니다.
AL LE XE GE ZA CE BI SO US ES AR MA IN DI RE A?
ER AT EN BE RA LA VE TI ED OR QU AN TE IS RI ON
그리고 이름을 만드는 절차입니다.
s0_lo의 6번 비트를 본다. 서 있으면 네 쌍(8글자), 아니면 세 쌍(6글자)을 만든다.s2_hi의 0~4비트를 꺼낸다. 0이면 건너뛰고, 아니면 그 번호의 토큰을 붙인다.- 씨앗을 한 번 트위스트하고 2번으로 돌아간다.
Lave 를 예로 들어 보겠습니다. Lave 의 씨앗은 이렇습니다.
s0_hi s0_lo s1_hi s1_lo s2_hi s2_lo
10101101 00111000 00010100 10011100 00010101 00011101
| 단계 | s2_hi 의 0~4비트 |
계산 | 결과 |
|---|---|---|---|
| 트위스트 전 | %10101 = 21 |
토큰 21 | LA |
| 트위스트 1회 후 | %10110 = 22 |
토큰 22 | VE |
| 트위스트 2회 후 | %00000 = 0 |
건너뜀 | — |
LAVE. 게임을 시작하는 그 항성계입니다.
같은 규칙으로 은하 1에는 두 글자짜리 이름이 딱 하나(Ra), 세 글자짜리가 둘(Ara, 그리고 은하 3의 Rea) 나옵니다. 토큰 번호가 0이 나오면 그 쌍을 통째로 건너뛰기 때문에 길이가 들쭉날쭉해지고, 그 덕분에 기계가 만든 이름 같지 않은 이름들이 나옵니다.
4. Go 로 은하를 만들어 보기#
여기까지의 내용을 Go 로 옮기면 은하 하나가 통째로 나옵니다. 실제로 돌려서 검증한 코드입니다.
package main
import (
"fmt"
"strings"
)
// 두 글자 토큰 32개. 엘리트가 이름을 위해 저장하는 유일한 데이터(64바이트).
var tokens = [32]string{
"AL", "LE", "XE", "GE", "ZA", "CE", "BI", "SO",
"US", "ES", "AR", "MA", "IN", "DI", "RE", "A?",
"ER", "AT", "EN", "BE", "RA", "LA", "VE", "TI",
"ED", "OR", "QU", "AN", "TE", "IS", "RI", "ON",
}
// 항성계 하나를 완전히 기술하는 씨앗. 6바이트가 전부다.
type Seed struct{ S0, S1, S2 uint16 }
// 트리보나치 수열을 한 칸 민다.
func (s Seed) Twist() Seed {
tmp := s.S0 + s.S1 // uint16 이라 자동으로 65536 에서 순환한다
return Seed{S1: s.S2, S0: s.S1, S2: tmp + s.S2}
}
func (s Seed) Name() string {
pairs := 3
if byte(s.S0)&0x40 != 0 { // s0_lo 의 6번 비트
pairs = 4
}
var b strings.Builder
cur := s
for i := 0; i < pairs; i++ {
if v := byte(cur.S2>>8) & 0x1f; v != 0 { // s2_hi 의 0~4비트
b.WriteString(strings.ReplaceAll(tokens[v], "?", ""))
}
cur = cur.Twist()
}
n := b.String()
return n[:1] + strings.ToLower(n[1:])
}
type System struct {
Name string
X, Y int // 은하 지도상의 좌표
Economy int // 0~7
Government int // 0~7
TechLevel int // 화면 표시는 여기에 1을 더한 값
Population int // 0.1억 단위
}
func (s Seed) System() System {
s0hi := int(s.S0 >> 8)
s1hi, s1lo := int(s.S1>>8), int(byte(s.S1))
econ := s0hi & 0x07
gov := (s1lo >> 3) & 0x07
if gov <= 1 { // 무정부와 봉건제는 부유해질 수 없다
econ |= 0x02
}
// 경제 비트를 반전시켜 부유할수록 큰 값이 되게 한다
tech := (^econ & 0x07) + (s1hi & 0x03) + (gov+1)/2
return System{
Name: s.Name(),
X: s1hi,
Y: s0hi >> 1, // 지도가 가로의 절반 높이라 반으로 줄인다
Economy: econ,
Government: gov,
TechLevel: tech,
Population: tech*4 + econ + gov + 1,
}
}
func main() {
// 은하 1의 0번 항성계 Tibedied. 이 여섯 바이트가 우주의 전부다.
s := Seed{S0: 0x5A4A, S1: 0x0248, S2: 0xB753}
for i := 0; i < 256; i++ {
sys := s.System()
if i < 8 {
fmt.Printf("%3d %-10s x=%3d y=%3d tech=%2d pop=%.1fB\n",
i, sys.Name, sys.X, sys.Y, sys.TechLevel+1, float64(sys.Population)/10)
}
for t := 0; t < 4; t++ { // 다음 항성계까지 네 번 트위스트
s = s.Twist()
}
}
}
출력입니다.
0 Tibedied x= 2 y= 45 tech= 9 pop=3.6B
1 Qube x=152 y=102 tech= 7 pop=3.7B
2 Leleer x= 77 y=121 tech= 8 pop=3.5B
3 Biarge x= 83 y=104 tech=12 pop=4.7B
4 Xequerin x=180 y= 65 tech= 7 pop=3.2B
5 Tiraor x=172 y= 88 tech=10 pop=4.1B
6 Rabedira x= 69 y=124 tech= 9 pop=3.6B
7 Lave x= 20 y= 86 tech= 5 pop=2.5B
7번이 Lave 입니다. 좌표 (20, 86), 기술 수준 5, 인구 2.5억. 1984년 게임을 실행해 Lave 의 정보 화면을 열면 나오는 값과 같습니다. 256개를 다 돌리면 Zaonce, Riedquat, Diso, Isinor 같은 익숙한 이름들이 전부 나오고, 256개가 전부 서로 다릅니다.
이 프로그램에서 데이터라고 할 만한 것은 토큰 32개와 씨앗 6바이트뿐입니다. 나머지는 전부 코드입니다. 40년 전 사람들이 22KB 안에 우주를 넣은 방법이 이것입니다.
5. 곱셈 없이 3D 를 돌리기#
Part 1에서 확인한 대로 6502 에는 곱셈 명령이 없습니다. 그런데 엘리트는 와이어프레임 3D 를 실시간으로 돌립니다. 우주선이 회전하고, 다가오고, 총알이 날아갑니다.
곱셈은 시프트와 덧셈으로#
엘리트의 곱셈 루틴은 시프트 앤 애드 입니다. 원리는 초등학교 곱셈과 같습니다.
p * a = %00101001 * a
= (%00100000 + %00001000 + %00000001) * a
= (a << 5) + (a << 3) + (a << 0)
즉 p 의 비트를 훑으면서 1이 있는 자리마다 a 를 그만큼 시프트해서 더합니다.
여기서 엘리트는 두 가지 최적화를 겹칩니다.
첫째, a 를 왼쪽으로 시프트하는 대신 결과를 오른쪽으로 시프트합니다. 8비트 레지스터에서 왼쪽 시프트는 넘침이 나지만, 오른쪽으로 밀면서 떨어지는 비트를 결과에 모으면 넘칠 일이 없습니다.
둘째, 결과 비트를 별도 공간에 저장하지 않고 p 의 왼쪽 끝에 붙입니다. p 를 오른쪽으로 밀 때마다 왼쪽에 안 쓰는 자리가 하나씩 생기니, 거기에 결과 비트를 밀어 넣습니다. 변수 하나를 아낍니다.
Go 로 쓰면 이렇습니다.
// 8비트 x 8비트 -> 16비트. 6502 의 MULT1 루틴과 같은 구조다.
// lo 는 처음에 p 를 담고 있다가, p 가 오른쪽으로 밀려 나가는 만큼
// 왼쪽부터 결과의 하위 바이트로 채워진다. 변수 하나를 아끼는 대목이다.
func multiplyShiftAdd(p, a uint8) uint16 {
lo := p
carry := lo & 1 // LSR P: p 의 최하위 비트를 캐리로 빼낸다
lo >>= 1
var hi uint8
for i := 0; i < 8; i++ {
var addCarry uint8
if carry == 1 { // p 의 이 자리가 1이면 a 를 더한다
sum := uint16(hi) + uint16(a)
hi = uint8(sum)
addCarry = uint8(sum >> 8)
}
// ROR A: 덧셈의 자리올림이 상위 바이트의 최상위 비트로 들어온다
fallen := hi & 1
hi = hi>>1 | addCarry<<7
// ROR P: 상위에서 떨어진 비트가 lo 의 최상위로 들어오고,
// 동시에 lo 의 최하위 비트가 다음 회차의 캐리가 된다
carry = lo & 1
lo = lo>>1 | fallen<<7
}
return uint16(hi)<<8 | uint16(lo)
}
ROR P 한 줄이 두 가지 일을 동시에 합니다. 결과 비트를 왼쪽에 밀어 넣으면서, 다음 회차에 검사할 p 의 비트를 캐리로 빼냅니다. 8비트 두 개를 곱해 65,536가지 경우를 전부 돌려 봐도 결과가 일반 곱셈과 정확히 일치합니다.
참고로 나중에 나온 6502 세컨드 프로세서판과 BBC Master 판에서는 로그 테이블 을 씁니다. log(a) + log(q) 를 더한 뒤 역로그 테이블에서 찾아오는 방식으로, 시프트 앤 애드보다 빠릅니다. 하지만 테이블 두 개를 넣을 메모리가 필요합니다. 1984년의 32KB BBC Micro 카세트판은 그 사치를 부릴 수 없었고, 그래서 원판은 시프트 앤 애드입니다.
회전은 삼각함수 없이#
3D 회전에는 사인과 코사인이 필요합니다. 테이블을 넣으면 되지만 그것도 메모리입니다. 엘리트는 다른 길을 갔습니다.
먼저 관점을 뒤집습니다. 플레이어의 우주선을 회전시키는 대신 우주 전체를 반대로 회전시킵니다. 어차피 모든 것이 조종석 시점에서 그려지고 우주에는 위아래가 없으니 결과는 같습니다. 대신 내 우주선의 방향을 다른 모든 계산에 끌고 다닐 필요가 없어집니다.
그리고 회전 자체는 이렇게 합니다.
y = y - alpha * x
x = x + alpha * y <- 방금 갱신한 y 를 쓴다
두 번째 줄에서 방금 바꾼 y 를 그대로 쓰는 것 이 핵심입니다. 수학적으로는 틀린 회전 행렬입니다. 그런데 이렇게 하면 오차가 누적되지 않고 궤적이 닫힌 타원을 그립니다. 민스키 원 알고리듬 으로 알려진 것이고, 사인도 코사인도 테이블도 없이 회전이 됩니다.
// 민스키 회전. sin/cos 도, 테이블도 없다.
// alpha 를 작게 잡으면 (x, y) 는 원점 주위를 닫힌 궤도로 돈다.
func minskyRotate(x, y, alpha float64) (float64, float64) {
y = y - alpha*x
x = x + alpha*y // 갱신된 y 를 쓰는 것이 핵심
return x, y
}
이 함수로 (100, 0) 을 alpha = 0.125 로 10만 번 돌려 보면, 반지름의 제곱이 9,412 와 10,667 사이에 계속 갇혀 있습니다. 발산하지도 수축하지도 않습니다. 부동소수점 오차가 누적되는 일반적인 반복 회전과 정반대의 성질이고, 8비트 정수 연산에서 이 성질은 그냥 편리한 정도가 아니라 필수입니다.
엘리트는 여기에 피치까지 섞어서 x, y, z 를 한 번에 굴립니다.
1. K2 = y - alpha * x
2. z = z + beta * K2
3. y = K2 - beta * z
4. x = x + alpha * y
곱셈 결과의 하위 바이트를 버리기 때문에 모든 곱셈이 256으로 나눠집니다. 그래서 저장된 각도 alpha(0~31)와 beta(0~8)가 실제로는 0~0.125 라디안과 0~0.03125 라디안 이 됩니다. 작은 각도이므로 민스키 근사가 잘 맞습니다.
정확한 회전 행렬 대신 근사식을 쓰되, 그 근사가 안정적인 영역 안에서만 쓴다. 이게 8비트 3D 의 요령입니다.
6. 한 화면에 두 그래픽 모드#
엘리트를 처음 본 사람들이 놀란 것 중 하나가 화면 구성입니다. 위쪽 우주 화면은 고해상도 흑백이고, 아래쪽 계기판은 저해상도 컬러입니다. BBC Micro 는 화면 하나에 그래픽 모드를 하나만 쓸 수 있습니다.
방법은 두 단계입니다.
첫째, 표준 모드가 아닌 커스텀 모드를 만듭니다. 로더가 6845 CRTC 칩의 레지스터를 직접 다시 씁니다. 표준 모드 4 가 40열 x 32행인데, 엘리트는 이를 32열 x 31행 으로 줄입니다. 문자 하나가 8 x 8 픽셀이니 256 x 248 픽셀 흑백 화면입니다. 화면 크기를 줄인 만큼 메모리가 남고, 그 메모리가 게임 코드 자리가 됩니다.
둘째, 화면을 그리는 도중에 색 수를 바꿉니다.
- 수직 동기 신호가 올 때 6522 System VIA 의 T1 타이머를 14622 에서 카운트다운하도록 설정한다.
- 화면이 위에서부터 그려지는 동안 타이머가 줄어든다.
- 계기판 영역을 그리기 시작할 즈음 타이머가 0이 되어 인터럽트가 걸린다.
- 인터럽트 핸들러가 Video ULA 를 다시 프로그래밍해 픽셀당 색 수를 2에서 4로 바꾼다.
- 그 아래부터는 4색으로 그려진다.
타이머 값이 왜 14622 인지도 근거가 있습니다. 타이머는 1MHz 로 똑딱이고, 화면 한 행은 64문자 = 512픽셀이며 모드 4 에서 픽셀은 1MHz 로 찍히므로 행당 512틱 입니다. 전체 39행이면 한 화면에 19,968틱입니다. 여기서 계기판이 시작하는 위치를 역산하면 대략적인 값이 나오고, 나머지는 실기에서 맞춰 넣은 것으로 보입니다.
Part 2 에서 본 마리오의 sprite 0 hit 와 목적이 같습니다. 화면을 그리는 도중에 끼어들어 하드웨어 설정을 바꾼다. 다만 마리오는 스프라이트 충돌 플래그를 폴링했고, 엘리트는 타이머 인터럽트를 썼습니다. 기계가 다르면 시계도 다릅니다.
7. 남은 바이트는 144개#
레트로 게임 전설 중에 “엘리트는 BBC Micro 의 메모리를 단 한 바이트도 남기지 않고 썼다” 는 이야기가 있습니다.
Moxon 이 전체 메모리 지도를 바이트 단위로 집계한 결과는 이렇습니다.
| 구간 | 크기 | 미사용 |
|---|---|---|
| ROM (운영체제, 페이지드 ROM) | 32,768 | — |
| 운영체제 작업 영역 | 703 | 64 |
| 공유 메모리 (스택, 화면) | 8,080 | — |
| 게임 코드 | 21,952 | 46 |
| 작업 영역 | 2,033 | 34 |
| 합계 | 65,536 | 144 |
144바이트가 남습니다. 전설은 사실이 아닙니다.
다만 그 144바이트의 내역을 보면 전설이 왜 생겼는지 알 수 있습니다. 64바이트는 게임이 프린터를 안 쓰기 때문에 비어 있는 운영체제 프린터 버퍼입니다. 24바이트는 쓰이지 않는 곱셈 루틴의 중복본이고, 6바이트는 개발 중에 위아래 레이저를 넣으려다 접은 흔적 입니다. 4바이트는 넣으려다 만 장비의 자리로 추정됩니다.
남은 것이 아니라, 정리를 안 한 것에 가깝습니다.
마치며#
Part 3 에서 확인한 것을 정리하면 이렇습니다.
- 저장을 계산으로 바꿉니다. 항성계 하나는 16비트 씨앗 세 개, 6바이트입니다. 이름, 좌표, 경제, 정부, 기술 수준, 인구, 총생산, 반지름, 종족이 전부 이 6바이트에서 계산돼 나옵니다.
- 다음 항성계는 트리보나치 수열을 네 칸 미는 것입니다. 16비트 덧셈 두 번이면 됩니다. 게임 전체의 항성계 2,048개가 Tibedied 의 씨앗 6바이트에서 파생됩니다.
- 은하가 8개인 것은 비트 회전의 주기입니다. 씨앗의 각 바이트를 왼쪽으로 한 비트 돌리면 다음 은하가 되고, 여덟 번 돌면 처음으로 돌아옵니다.
- 이름도 만듭니다. 두 글자 토큰 32개(64바이트)와 씨앗만으로 2,048개의 고유한 이름이 나옵니다.
- 곱셈은 시프트와 덧셈으로, 회전은 민스키 근사로. 결과 비트를 피연산자의 빈 자리에 쌓아 변수 하나를 아끼고, 갱신된 값을 다음 계산에 그대로 써서 사인과 코사인을 없앱니다.
- 화면을 그리는 도중에 하드웨어를 바꿉니다. 6845 CRTC 를 다시 프로그래밍해 커스텀 모드를 만들고, 타이머 인터럽트로 화면 중간에 색 수를 2에서 4로 바꿉니다.
마리오와 엘리트를 나란히 놓으면 압축과 생성의 차이가 분명해집니다.
| 슈퍼 마리오브라더스 | 엘리트 | |
|---|---|---|
| 전략 | 데이터를 작게 만든다 | 데이터를 갖지 않는다 |
| 레벨/우주 데이터 | 4,460 바이트 | 6 바이트 |
| 대가 | 표현할 수 있는 것이 정해진 오브젝트 목록으로 제한됨 | 세부를 사람이 손댈 수 없음 |
| 해제 비용 | 0 (파서가 곧 렌더러) | 매번 계산 (트위스트 4회) |
엘리트의 대가가 중요합니다. 생성된 우주는 디자이너가 손댈 수 없습니다. Lave 를 조금 더 부유하게 만들고 싶어도 방법이 없습니다. 씨앗을 바꾸면 이름부터 바뀌어 버립니다. 그래서 엘리트에는 손으로 만든 스테이지가 없고, 대신 어디를 가도 그럴듯한 우주 가 있습니다. 이 맞바꿈은 오늘날 절차적 생성을 쓰는 모든 게임이 똑같이 치르고 있습니다.
Part 4 에서는 세 번째 길을 봅니다. 1987년 MSX2 의 메탈기어는 데이터를 작게 만들지도, 계산으로 바꾸지도 않았습니다. 못 하는 일을 할 필요가 없는 게임을 만들었습니다.
References#
- Elite on the 6502 — Mark Moxon 의 주석 disassembly — 이 글의 모든 알고리듬과 수치의 근거
- Twisting the system seeds — 트리보나치 트위스팅과 Tibedied 씨앗
- Galaxy and system seeds — 씨앗의 비트 배치와 Lave 예시
- Generating system data — 경제, 정부, 기술 수준, 인구, 총생산 계산식
- Generating system names — 두 글자 토큰 이름 생성
- QQ16 — 두 글자 토큰 테이블 — 토큰 32개 원본
- Shift-and-add multiplication — 곱셈 루틴
- Multiplication and division using logarithms — 후기 버전의 로그 테이블 방식
- Rotating the universe — 민스키 회전
- The split-screen mode — 6845 CRTC 커스텀 모드와 타이머 인터럽트
- Elite memory map — 바이트 단위 메모리 사용량과 미사용 144바이트
- 8비트의 벽을 넘은 게임들 Part 1: 그 기계들은 무엇을 못 했는가
- 8비트의 벽을 넘은 게임들 Part 2: 슈퍼 마리오브라더스, 레벨을 그리지 않고 서술하다