8비트의 벽을 넘은 게임들 Part 1: 그 기계들은 무엇을 못 했는가
이 글은 Claude Opus 5 를 이용해 초안이 작성되었으며, 이후 퇴고를 거쳤습니다.
스크린샷 한 장보다 작은 게임#
슈퍼 마리오브라더스가 담긴 카트리지의 ROM 용량은 40KB 입니다. 프로그램 32KB, 그림 데이터 8KB. 이게 전부입니다.
이 게임의 한 장면을 오늘날의 화면에서 캡처해 PNG 로 저장하면 보통 수백 KB 가 나옵니다. 게임 하나가 그 게임의 스크린샷 한 장보다 작습니다. 32개 스테이지, 여덟 가지 배경음악, 마리오의 물리 엔진, 적 AI, 점수 계산, 타이틀 화면이 전부 저 40KB 안에 있습니다.
이런 얘기는 레트로 게임 커뮤니티에서 흔히 감탄의 소재로 소비됩니다. 그런데 감탄에서 멈추면 남는 게 없습니다. 이 시리즈에서 하려는 것은 “대단하다더라"를 “이렇게 동작한다"로 바꾸는 일 입니다.
그러려면 순서가 있습니다. 어떤 기법이 왜 필요했는지 이해하려면, 먼저 그 기계가 정확히 무엇을 못 했는지 를 알아야 합니다. 그래서 이 첫 편에는 게임이 거의 나오지 않습니다. 대신 하드웨어 데이터시트를 읽습니다.
이 시리즈의 구성#
전체 다섯 편이고, 세 작품을 다룹니다. 세 작품은 각각 다른 방식으로 한계를 넘었습니다.
| 편 | 주제 | 작품 |
|---|---|---|
| Part 1 (이 글) | 무대 설정 — 하드웨어가 못 한 것들 | — |
| Part 2 | 압축 — 데이터를 작게 만든다 | 슈퍼 마리오브라더스 (패미콤, 1985) |
| Part 3 | 생성 — 데이터를 아예 갖지 않는다 | 엘리트 (BBC Micro, 1984) |
| Part 4 | 전복 — 못 그리는 것을 안 그려도 되게 만든다 | 메탈기어 (MSX2, 1987) |
| Part 5 | 마무리 — 제약이 사라진 시대에 남은 것 | — |
이 글에서 인용하는 수치는 전부 1차 출처에서 확인한 것입니다. 패미콤 쪽은 NESdev Wiki 의 하드웨어 문서, MSX2 쪽은 야마하와 ASCII 가 1985년에 낸 V9938 MSX-VIDEO Technical Data Book 을 직접 봤습니다. 인터넷에 흔히 도는 “카더라” 중에는 사실과 다른 것이 섞여 있어서, 확인되지 않은 것은 쓰지 않았습니다.
1. CPU: 곱셈이 없는 세계#
먼저 두 기계의 두뇌부터 봅니다.
| 패미콤 (1983) | MSX / MSX2 (1983 / 1985) | |
|---|---|---|
| CPU | 리코 RP2A03 (MOS 6502 코어) | 자일로그 Z80A |
| 클럭 | 1.789773 MHz (NTSC) | 3.579545 MHz |
| 레지스터 | A, X, Y (각 8비트) | A, B, C, D, E, H, L + 대체 세트 |
MSX 의 3.579545 MHz 라는 어정쩡한 숫자는 NTSC 컬러 부반송파 주파수입니다. 영상 신호와 클럭을 하나로 묶으면 발진기를 하나만 쓰면 되니까, 그 주파수를 그대로 시스템 클럭으로 삼은 것입니다. MSX 표준에서는 이 클럭이 카트리지 슬롯의 42번 핀으로도 나갑니다.
숫자만 보면 Z80 이 두 배 빠릅니다. 다만 Z80 은 명령 하나에 쓰는 클럭 수(M-cycle, T-state)가 6502 보다 많아서, 실제 체감 성능 차이는 클럭 비율만큼 나지는 않습니다.
문제는 속도가 아니라 명령어입니다#
정말 중요한 것은 클럭이 아닙니다. 두 CPU 모두 곱셈 명령이 없다 는 점입니다.
6502 가 제공하는 산술 명령은 딱 두 개입니다.
| 명령 | 뜻 |
|---|---|
ADC |
캐리를 포함해 메모리를 누산기에 더한다 |
SBC |
캐리를 빌림으로 써서 메모리를 누산기에서 뺀다 |
여기에 시프트 네 개(ASL, LSR, ROL, ROR)가 있을 뿐입니다. 곱셈도 나눗셈도 없습니다. Z80 도 마찬가지입니다. 곱셈 명령 MLT 는 한참 뒤에 나온 후속 칩 Z180 에서야 추가됐습니다.
이게 왜 문제냐면, 게임에서 곱셈은 도처에 있기 때문입니다.
- 화면에 무언가를 찍으려면
y * 화면폭 + x로 주소를 계산해야 합니다. - 속도에 시간을 곱해 위치를 갱신해야 합니다.
- 3D 를 그리려면 회전 행렬을 곱해야 합니다.
- 타일맵에서
(행, 열)로 오프셋을 구하려면 행에 열 수를 곱해야 합니다.
곱셈이 없으면 어떻게 해야 할까요. 방법은 세 가지뿐입니다.
방법 1: 시프트와 덧셈으로 직접 만든다. 곱하는 수를 2의 거듭제곱의 합으로 분해하는 고전적인 방법입니다. Go 로 쓰면 이렇게 됩니다.
// 8비트 x 8비트 -> 16비트 곱셈을 시프트와 덧셈만으로 구현합니다.
// 6502/Z80 에서 실제로 쓰던 방식과 같은 구조입니다.
func mulShiftAdd(a, b uint8) uint16 {
var result uint16
acc := uint16(a)
for b != 0 {
if b&1 == 1 { // 현재 비트가 서 있으면 누산기를 더한다
result += acc
}
acc <<= 1 // 다음 자릿수를 위해 두 배
b >>= 1
}
return result
}
동작은 하지만 느립니다. 6502 에서 이 루프를 어셈블리로 짜면 8비트 곱셈 하나에 수십에서 백 사이클 이 듭니다. 뒤에서 보겠지만 한 프레임에 쓸 수 있는 시간이 2,273 사이클밖에 없다는 점을 생각하면, 프레임마다 곱셈을 수십 번 해야 하는 로직은 성립하지 않습니다.
방법 2: 미리 계산해서 ROM 에 넣는다. 곱하는 값의 범위가 좁으면 표를 만들어 두는 편이 압도적으로 빠릅니다. 조회는 LDA table,X 한 번, 4 사이클입니다.
// 화면 폭이 32타일일 때, 행 번호로 오프셋을 구하는 표.
// 곱셈 한 번(수십 사이클)이 배열 조회 한 번(4 사이클)으로 바뀝니다.
var rowOffset = [30]uint16{
0, 32, 64, 96, 128, 160, 192, 224,
256, 288, 320, 352, 384, 416, 448, 480,
512, 544, 576, 608, 640, 672, 704, 736,
768, 800, 832, 864, 896, 928,
}
방법 3: 애초에 곱셈이 필요 없게 설계한다. 가장 급진적이고, 가장 효과가 큰 방법입니다. 화면 폭을 32처럼 2의 거듭제곱으로 잡으면 곱셈이 시프트 다섯 번으로 바뀝니다. 좌표를 절대값이 아니라 증분으로 관리하면 매 프레임 곱할 일이 없어집니다.
Part 2 에서 볼 마리오의 물리 엔진과 Part 3 에서 볼 엘리트의 3D 연산은 둘 다 방법 2와 3의 조합 입니다. 우연이 아닙니다. 곱셈이 없다는 하나의 제약이 두 게임에서 같은 형태의 해법을 강제한 것입니다.
2. 화면: 픽셀을 직접 찍을 수 없다#
오늘날 그래픽 프로그래밍의 기본 모델은 프레임버퍼입니다. 화면 크기만 한 메모리가 있고, 원하는 픽셀에 원하는 색을 씁니다.
패미콤에는 그런 게 없습니다. 화면 크기만 한 메모리를 둘 여유가 없었기 때문입니다. 256 x 240 화면을 픽셀당 1바이트로 잡으면 60KB 입니다. 1983년의 메모리 가격으로 가정용 게임기에 그만한 RAM 을 넣는 것은 어림도 없는 일이었습니다. 참고로 패미콤 본체의 작업용 RAM 은 2KB 입니다.
그래서 패미콤은 완전히 다른 모델을 씁니다. 화면을 미리 정해진 그림 조각(타일)의 격자 로 봅니다.
패미콤 PPU 의 구조#
| 항목 | 값 |
|---|---|
| 해상도 | 256 x 240 |
| 타일 크기 | 8 x 8 픽셀 |
| 네임테이블 | 32 x 30 타일 |
| 타일 종류 | 한 번에 256개 (CHR 4KB 당) |
화면 전체를 표현하는 데이터는 32 x 30 = 960바이트 입니다. 각 바이트는 “여기에 몇 번 타일을 놓아라” 는 지시일 뿐입니다. 60KB 가 960바이트가 됐습니다. 압축률로 치면 64배입니다.
물론 공짜는 아닙니다. 화면에 있는 모든 것이 8픽셀 격자에 정렬돼야 합니다. 격자에서 벗어난 위치에 무언가를 그리고 싶으면 타일이 아니라 스프라이트를 써야 하고, 스프라이트는 개수가 정해져 있습니다.
색은 더 심각한 제약입니다#
타일 데이터는 픽셀당 2비트입니다. 즉 타일 하나가 쓸 수 있는 색은 네 가지 이고, 그중 0번은 투명(배경색)이라 실질적으로 세 가지입니다.
그러면 “어느 네 가지 색"인지는 어떻게 정할까요. 여기가 진짜 제약입니다.
| 항목 | 값 |
|---|---|
| 팔레트 | 배경용 4개 + 스프라이트용 4개 |
| 팔레트당 색 | 4색 |
| 0번 엔트리 | 배경과 스프라이트가 공유 (backdrop) |
| 전체 색상 수 | 6비트 값 = 64가지 출력 |
| 어트리뷰트 1바이트가 담당하는 범위 | 32 x 32 픽셀 (4 x 4 타일) |
| 팔레트를 고를 수 있는 최소 단위 | 16 x 16 픽셀 (2 x 2 타일) |
마지막 줄이 핵심입니다. 팔레트는 타일마다 고를 수 있는 게 아니라 16 x 16 픽셀 블록마다 고를 수 있습니다. 다시 말해 나란히 붙은 타일 네 개는 반드시 같은 팔레트를 씁니다.
이 제약이 패미콤 게임의 그림체를 통째로 결정했습니다. 배경에 색이 화려한 물체와 수수한 물체를 딱 붙여 놓을 수가 없습니다. 그래서 패미콤 배경 그림들은 색 덩어리가 16픽셀 단위로 뭉쳐 있고, 경계선이 격자에 맞춰져 있습니다. 예술적 선택이 아니라 어트리뷰트 테이블의 구조가 그렇게 만든 것입니다.
스프라이트: 격자 밖으로 나가는 유일한 통로#
캐릭터는 8픽셀 격자에 맞춰 움직일 수 없습니다. 그래서 스프라이트가 있습니다.
| 항목 | 값 |
|---|---|
| 스프라이트 총 개수 | 64개 (OAM, 하나당 4바이트) |
| 크기 | 8 x 8 또는 8 x 16 |
| 팔레트 | 스프라이트용 4개 중 하나 (4색, 0번은 투명) |
| 한 스캔라인에 표시 가능한 개수 | 8개 |
마지막 줄이 8비트 게임의 겉모습을 지배한 제약입니다. 가로 한 줄에 스프라이트가 9개 이상 걸치면 9번째부터는 그냥 안 그려집니다.
마리오는 16 x 16 크기라 8 x 8 스프라이트 네 개를 씁니다. 즉 마리오 한 명이 어떤 스캔라인에서 스프라이트 슬롯 두 개를 먹습니다. 적이 셋 있으면 벌써 여덟 개입니다. 여기서 하나만 더 늘어나면 무언가가 사라집니다.
당시 게임에서 캐릭터가 깜빡거리는 현상을 본 적이 있을 겁니다. 그건 버그가 아니라 의도된 해법 입니다. 스프라이트 목록의 우선순위를 매 프레임 회전시키면, 9번째 스프라이트가 매번 다른 놈이 되면서 전부 절반씩 보이게 됩니다. 아무것도 안 보이는 것보다는 깜빡이는 게 낫다는 판단입니다.
참고로 이 8개 제한을 감지하는 오버플로 플래그는 하드웨어 버그가 있어서 오탐과 미탐이 둘 다 발생합니다. 그래서 이 플래그를 신뢰하고 게임 로직을 짠 개발자는 거의 없습니다.
MSX2 는 다른 방식으로 어렵습니다#
MSX2 의 비디오 칩은 야마하 V9938 입니다. 패미콤 PPU 와는 설계 철학이 다릅니다.
야마하 매뉴얼이 말하는 MSX2 의 핵심 신기능은 세 가지입니다.
- Full bit-mapped mode
- Color palette (9비트 x 16패턴)
- More sprites per horizontal line
첫 줄이 중요합니다. MSX2 에는 비트맵 모드가 있습니다. 픽셀을 직접 찍을 수 있다는 뜻이고, 패미콤이 못 하는 일입니다. 메탈기어가 쓴 GRAPHIC 4 모드(BASIC 의 SCREEN 5)의 사양은 이렇습니다.
| 항목 | 값 |
|---|---|
| 종류 | Bit-mapped Graphics Mode |
| 화면 크기 | 256 x 212 (또는 256 x 192) |
| 색 | 512색 중 16색 |
| 스프라이트 모드 | Sprite mode 2 |
| 화면 하나당 VRAM | 32KB |
자유를 얻은 대가가 마지막 줄입니다. 화면 한 장이 32KB 입니다. 이 숫자가 Part 4 에서 메탈기어의 운명을 결정합니다.
V9938 의 스프라이트#
| 항목 | SPRITE MODE 1 (MSX1 호환) | SPRITE MODE 2 (MSX2) |
|---|---|---|
| 총 개수 | 32개 | 32개 |
| 크기 | 8 x 8 또는 16 x 16 | 8 x 8 또는 16 x 16 |
| 한 수평 라인당 | 4개 | 8개 |
| 색 지정 단위 | 스프라이트 하나에 한 색 | 수평 라인마다 한 색 |
| 겹칠 때 | 2색까지 | CC 비트로 색을 OR 하면 4색까지 |
MSX1 시절의 악명 높은 제약이 “스프라이트 하나는 한 가지 색"이었습니다. 캐릭터에 색을 두 개 쓰려면 스프라이트를 두 장 겹쳐야 했고, 그러면 라인당 4개 제한이 순식간에 찹니다.
MSX2 의 SPRITE MODE 2 는 이걸 “라인마다 한 색"으로 완화했습니다. 스프라이트마다 16바이트짜리 색 테이블을 두어서, 16 x 16 스프라이트의 각 가로줄에 다른 색을 줄 수 있습니다. 개선이긴 하지만 한 가로줄 안에서는 여전히 한 색 입니다. 캐릭터의 같은 높이에 살색 얼굴과 빨간 모자를 함께 둘 수 없습니다.
두 기계를 나란히 놓으면#
| 항목 | 패미콤 PPU | MSX2 V9938 (GRAPHIC 4) |
|---|---|---|
| 화면 구성 | 타일 격자 (32 x 30) | 비트맵 |
| 해상도 | 256 x 240 | 256 x 212 |
| 동시 발색 | 팔레트 8개 x 4색 / 전체 64색 | 512색 중 16색 |
| 색 지정 단위 | 16 x 16 픽셀 블록 | 픽셀 단위 |
| 스프라이트 | 64개 / 라인당 8개 | 32개 / 라인당 8개 |
| 스프라이트 색 | 스프라이트당 팔레트 1개 (3색) | 스프라이트의 가로줄당 1색 |
| 화면 데이터 크기 | 약 1KB | 32KB |
정리하면 이렇습니다. 패미콤은 자유도가 낮은 대신 데이터가 작습니다. MSX2 는 자유도가 높은 대신 데이터가 큽니다. 이 차이가 Part 2 와 Part 4 에서 정반대의 해법을 낳습니다.
3. 시간: 한 프레임의 7.6%#
용량과 화면 구조에 이어, 세 번째 제약은 시간입니다. 이게 가장 안 알려져 있고, 가장 가혹합니다.
패미콤에서 VRAM 에 데이터를 쓸 수 있는 시간은 정해져 있습니다. PPU 가 화면을 그리는 동안 CPU 가 VRAM 을 건드리면 어떻게 될까요. NESdev 문서는 이렇게 적고 있습니다.
이 레지스터에 접근하면 VRAM 주소가 증가하므로, 수직 블랭킹 구간이나 강제 블랭킹 구간 밖에서는 접근하면 안 된다. 그래픽 글리치가 발생하고, 쓰기의 경우 VRAM 의 예측할 수 없는 주소에 기록된다.
즉 화면을 그리는 중에는 VRAM 을 못 만집니다. 만질 수 있는 시간은 화면 아래쪽까지 다 그리고 나서 다음 프레임 맨 위로 돌아가기까지의 짧은 틈, 즉 수직 블랭킹(VBlank)뿐입니다.
그 틈이 얼마나 되는지 세어 봅시다.
| 항목 | NTSC 기준 |
|---|---|
| CPU 클럭 | 1.789773 MHz |
| 스캔라인당 CPU 사이클 | 113과 2/3 |
| 프레임당 CPU 사이클 | 29,780.5 |
| NMI 이후 VBlank 길이 | 20 스캔라인 = 약 2,273 사이클 |
2,273 / 29,780.5 = 약 7.6% 입니다. 한 프레임 시간의 13분의 1 만이 화면을 갱신할 수 있는 시간입니다.
2,273 사이클로 무엇을 할 수 있나#
여기서 먼저 빠져나가는 게 있습니다. 스프라이트 정보 256바이트를 OAM 으로 옮기는 DMA 입니다.
OAM DMA 자체는 정렬이 필요한지에 따라 513 또는 514 사이클이 걸린다.
스프라이트를 쓰는 게임이라면 이건 사실상 고정 비용입니다. 남는 시간은 이렇습니다.
2,273 (VBlank)
-514 (OAM DMA, 스프라이트 256바이트)
------
1,759 사이클
이 1,759 사이클로 배경을 갱신해야 합니다. 배경을 갱신하는 코드는 대략 이런 모양입니다.
; VBlank 안에서 배경 타일을 PPU 로 밀어 넣는 전형적인 루프
lda buffer,x ; 4 사이클 - 준비해 둔 데이터를 읽는다
sta $2007 ; 4 사이클 - PPUDATA 에 쓰면 VRAM 주소가 자동 증가
lda buffer+1,x ; 4
sta $2007 ; 4
... ; 루프를 펼쳐서 인덱스 증가와 분기 비용을 없앤다
루프를 펼치면 바이트당 8사이클 정도가 듭니다. 펼치지 않고 인덱스 증가와 분기를 넣으면 13사이클까지 올라갑니다. 8사이클로 잡으면,
1,759 / 8 = 약 220 바이트
한편 화면 한 장을 구성하는 데이터는 네임테이블 960바이트 + 어트리뷰트 64바이트 = 1,024바이트 입니다.
한 프레임에 갱신할 수 있는 양은 화면 전체의 5분의 1 정도입니다.
여기서 결론이 하나 나옵니다. 패미콤 게임은 매 프레임 화면을 다시 그릴 수 없습니다. 그러면 화면이 부드럽게 흐르는 마리오는 도대체 어떻게 만든 걸까요. 답은 두 가지의 조합입니다. 하나는 하드웨어 스크롤 레지스터이고, 다른 하나는 화면 밖에 새로 들어올 세로 한 줄만 매 프레임 채워 넣는 것 입니다. 세로 한 줄은 30바이트니까 220바이트 예산 안에 넉넉히 들어갑니다.
이 “필요한 만큼만, 제때” 라는 원칙이 Part 2 의 중심 주제입니다.
그래서 나온 트릭: 화면 중간에 끼어들기#
VBlank 밖에서는 아무것도 못 한다면, 화면 위쪽은 스크롤하고 아래쪽은 고정하는 것 같은 일은 어떻게 할까요.
두 기계 모두 “지금 화면의 어디를 그리는 중인지” 를 알아내는 장치를 갖고 있습니다. 접근 방식이 재미있게 다릅니다.
패미콤: sprite 0 hit. 0번 스프라이트의 불투명한 픽셀이 배경의 불투명한 픽셀과 겹치는 순간, PPU 가 상태 레지스터에 플래그를 세웁니다. 원래는 충돌 판정용으로 만든 기능입니다. 그런데 개발자들은 이걸 시계로 썼습니다. 화면의 특정 위치에 눈에 안 띄는 스프라이트를 하나 박아 두고, 플래그가 설 때까지 CPU 를 루프에서 대기시키다가, 플래그가 서는 순간 스크롤 레지스터를 바꿔 버리는 것입니다.
wait: lda $2002 ; PPUSTATUS 를 읽는다
bpl wait ; 비트 6(sprite 0 hit)이 설 때까지 계속 대기
; --- 여기가 화면의 특정 스캔라인 ---
; 이 지점에서 스크롤 레지스터를 바꾸면 위아래가 따로 논다
마리오 화면 위쪽의 점수판이 스크롤하지 않고 가만히 있는 이유가 이것입니다. 충돌 감지 기능을 시계로 전용한, 전형적인 8비트식 해법입니다.
MSX2: 라인 인터럽트. V9938 은 훨씬 점잖은 방법을 제공합니다.
| 레지스터 | 기능 |
|---|---|
| R#19 | 인터럽트 라인 레지스터. 지정한 스캔라인을 그리기 시작할 때 인터럽트를 건다 |
CPU 를 루프에 묶어 놓고 기다릴 필요 없이, 원하는 줄 번호를 레지스터에 넣어 두면 됩니다. 2년 늦게 나온 칩답게 설계가 정돈돼 있습니다.
4. 스크롤: 가장 비싼 기능#
마지막 제약은 스크롤입니다. 이것 하나만으로 Part 4 의 이야기가 절반쯤 결정됩니다.
패미콤에는 스크롤 레지스터가 있습니다. 값을 쓰면 화면 전체가 픽셀 단위로 밀립니다. 타일 격자 구조라서 새로 들어오는 줄만 채워 주면 됩니다. 앞서 계산했듯 세로 한 줄은 30바이트니까 예산 안입니다.
MSX 쪽은 사정이 복잡합니다. 세대별로 정리하면 이렇습니다.
| VDP | 세대 | 하드웨어 스크롤 |
|---|---|---|
| TMS9918 | MSX1 | 스크롤 레지스터 없음. 다만 타일 모드라 타일맵 768바이트만 옮기면 8픽셀 단위로 밀 수 있음 |
| V9938 | MSX2 | 세로 스크롤(R#23)만 있음. 가로는 없음 |
| V9958 | MSX2+ | R#26 / R#27 로 가로 스크롤 지원 |
V9938 의 R#23 은 “표시를 시작할 라인 번호” 를 지정하는 레지스터입니다. 세로로는 픽셀 단위로 부드럽게 밀 수 있습니다. 가로로 미는 레지스터는 없습니다. 야마하 매뉴얼의 컨트롤 레지스터 목록은 #0부터 #23까지이고, 그 안에 가로 스크롤에 해당하는 것이 없습니다. 가로 스크롤은 2년 뒤 V9958 에서야 R#26, R#27 로 추가됐습니다.
그래서 MSX2 게임 중 세로 스크롤 슈팅은 많고(알레스테, 쿼스), 가로 스크롤 게임은 상대적으로 드뭅니다. 굳이 가로로 밀려면 두 가지 방법뿐입니다.
방법 1: 타일 모드로 후퇴한다. GRAPHIC 3(SCREEN 4) 같은 타일 기반 모드를 쓰면 타일맵만 옮기면 되니 가볍습니다. 대신 비트맵 모드의 표현력을 포기해야 합니다.
방법 2: R#18(adjust register)을 악용한다. 원래 사용자가 자기 TV 에 맞춰 화면 위치를 조정하라고 만든 레지스터인데, 이걸 1픽셀씩 16단계로 밀면 가로 스크롤처럼 보입니다. 문제가 여럿입니다.
- 16픽셀까지만 가능해서 그 이상은 결국 소프트웨어 스크롤이 필요합니다.
- 화면 좌우 테두리가 같이 밀려서 깜빡입니다. 스프라이트로 가려야 합니다.
- 사용자가 맞춰 놓은 화면 중심 설정을 무시하게 됩니다.
- VDP 의 복사 명령 실행 중에 이 레지스터를 바꾸면 VRAM 이 깨집니다.
이 편법을 쓴 대표적인 게임이 1989년의 스페이스 맨보우입니다. 문제 목록을 보면 알겠지만, 이건 정공법이 아니라 곡예에 가깝습니다.
그리고 GRAPHIC 4 는 비트맵입니다. 소프트웨어로 가로 스크롤을 하려면 화면 32KB 를 통째로 옮겨야 합니다. 3.58MHz Z80 이 한 프레임(약 6만 T-state) 안에 32KB 를 옮기는 것은 불가능합니다. MSX1 처럼 타일맵 768바이트만 옮기는 수법도 비트맵 모드에서는 쓸 수 없습니다.
1987년에 MSX2 로 가로로 흐르는 액션 게임을 만들라는 것은, 하드웨어가 거부하는 일을 하라는 것과 같았습니다. 메탈기어가 화면 단위로 뚝뚝 끊어 전환되는 이유가 여기 있습니다. 그리고 그 강제된 화면 전환이 어떻게 스텔스라는 장르의 문법이 되는지가 Part 4 의 내용입니다.
5. 그래서 남은 선택지는 셋#
지금까지 확인한 제약을 한자리에 모으면 이렇습니다.
| 자원 | 제약 |
|---|---|
| 저장 공간 | 마리오 카트리지 40KB, 패미콤 작업 RAM 2KB |
| 연산 | 곱셈 명령 없음. 1.79MHz / 3.58MHz |
| 화면 | 팔레트는 16 x 16 블록 단위, 스캔라인당 스프라이트 8개 |
| 시간 | 프레임의 7.6%, 약 220바이트만 갱신 가능 |
| 스크롤 | MSX2 는 가로 스크롤 하드웨어가 아예 없음 |
이 벽 앞에서 개발자가 취할 수 있는 태도는 결국 세 가지로 수렴합니다. 이 시리즈의 나머지 세 편은 각각 하나씩을 다룹니다.
압축 — 데이터를 작게 만든다#
가장 직관적인 길입니다. 넣고 싶은 것은 정해져 있으니, 표현을 바꿔서 자리를 줄입니다.
슈퍼 마리오브라더스는 레벨을 타일 배열로 저장하지 않습니다. “여기에 이런 것이 있다” 는 서술 로 저장합니다. 32개 스테이지가 몇 KB 에 들어가는 이유입니다. 같은 그림 데이터를 팔레트만 바꿔 두 번 쓰고, 물리 계산은 미리 만든 표로 대체합니다. Part 2 에서 다룹니다.
생성 — 데이터를 아예 갖지 않는다#
한 걸음 더 나간 길입니다. 압축의 극한은 압축이 아니라 저장을 포기하는 것 입니다.
1984년의 엘리트는 22KB 안에 항성계 2,048개가 있는 우주를 담았습니다. 항성계 데이터를 저장하지 않았기 때문입니다. 씨앗값 몇 바이트에서 이름, 좌표, 경제, 정부 형태, 기술 수준을 매번 다시 계산해서 만들어 냅니다. 저장 공간이 없으면 계산으로 바꾸면 된다는 발상이고, 오늘날 절차적 생성이라 부르는 것의 출발점입니다. Part 3 에서 다룹니다.
전복 — 요구사항 자체를 바꾼다#
가장 급진적인 길입니다. 하드웨어가 못 하는 일을 어떻게든 해내는 대신, 그 일을 할 필요가 없는 게임을 만드는 것 입니다.
1987년 MSX2 에서 적을 여럿 그리려면 스캔라인당 스프라이트 8개 안에 다 넣어야 합니다. 총알까지 그리면 화면이 금방 찹니다. 가로 스크롤도 안 됩니다. 이 상황에서 나온 답이 “싸우지 않는 게임” 이었습니다. 총을 쏘지 않으면 총알을 그릴 필요가 없고, 화면이 끊어져도 그 끊긴 화면 하나가 경비 구역의 단위가 됩니다. 제약이 게임의 문법이 됐고, 그 문법은 하드웨어가 사라진 뒤에도 30년을 살아남았습니다. Part 4 에서 다룹니다.
마치며#
Part 1 에서는 게임 이야기를 거의 하지 않았습니다. 대신 세 가지 숫자를 확보했습니다.
- 연산의 벽. 6502 와 Z80 모두 곱셈 명령이 없습니다. 산술 명령은
ADC와SBC뿐이고, 곱셈은 시프트와 덧셈으로 만들거나 표로 대체해야 합니다. - 화면의 벽. 패미콤은 팔레트를 16 x 16 픽셀 블록 단위로만 고를 수 있고, 스캔라인당 스프라이트가 8개입니다. MSX2 는 픽셀을 자유롭게 찍을 수 있지만 화면 한 장이 32KB 이고, 가로 스크롤 하드웨어가 없습니다.
- 시간의 벽. VRAM 을 만질 수 있는 시간은 한 프레임의 7.6%인 2,273 사이클입니다. 스프라이트 DMA 를 빼면 배경에 쓸 수 있는 것은 약 220바이트, 화면 한 장의 5분의 1입니다.
이 숫자들을 손에 쥐고 있어야 다음 편들의 기법이 왜 그런 모양인지 보입니다. 마리오의 레벨 인코딩은 저 40KB 때문이고, 컬럼 단위 갱신은 저 220바이트 때문이며, 물리 테이블은 곱셈 명령이 없기 때문입니다.
Part 2 에서는 슈퍼 마리오브라더스를 뜯습니다. 40KB 안에 32개 스테이지를 넣기 위해 레벨을 그리지 않고 서술하는 방법, 스크롤하며 컬럼 단위로 조립하는 구조, 그리고 같은 그림을 팔레트만 바꿔 두 번 파는 기법을 봅니다.
References#
- NESdev Wiki — Cycle reference chart — CPU 클럭, 프레임당 사이클, VBlank 길이
- NESdev Wiki — PPU attribute tables — 네임테이블과 어트리뷰트 구조
- NESdev Wiki — PPU palettes — 팔레트 구성
- NESdev Wiki — PPU OAM — 스프라이트 구조와 sprite 0 hit
- NESdev Wiki — PPU sprite evaluation — 스캔라인당 스프라이트 8개 제한
- NESdev Wiki — PPU registers — 렌더링 중 VRAM 접근 문제
- NESdev Wiki — DMA — OAM DMA 사이클 비용
- V9938 MSX-VIDEO Technical Data Book (Yamaha / ASCII, 1985) — MSX2 비디오 칩 공식 매뉴얼
- A guide to scrolling game engines on MSX (MSX Assembly Page) — MSX 세대별 스크롤 구현
- MSX Z80 clock frequency (MSX Resource Center) — MSX 시스템 클럭
- 6502 Instruction Set (masswerk) — 6502 산술 명령
- Zilog Z180 (Wikipedia) — MLT 명령이 Z180 에서 추가된 사실