이 글은 Claude Opus 5 를 이용해 초안이 작성되었으며, 이후 퇴고를 거쳤습니다.


세 번째 길#

앞의 두 편에서 본 전략을 정리하면 이렇습니다.

  • Part 2 슈퍼 마리오브라더스: 데이터를 작게 만든다. 레벨 오브젝트 2바이트 인코딩.
  • Part 3 엘리트: 데이터를 갖지 않는다. 씨앗 6바이트에서 우주 전체를 계산.

둘 다 “요구사항은 그대로 두고 구현을 비튼다” 는 접근입니다. 1987년 코나미의 MSX2 팀은 세 번째 길을 갔습니다. 요구사항 자체를 바꿨습니다.

결과물이 메탈기어이고, 그 결과물이 만든 것은 게임 하나가 아니라 장르 하나 였습니다.

이 글은 GuillianSeed 가 공개한 메탈기어(RC750, MSX2, 1987) 전체 주석 disassembly 를 근거로 씁니다. 카트리지는 코나미 MegaROM 방식의 128KB 이고, 뱅크가 0번부터 F번까지 있습니다.


1. 하드웨어가 거부한 것#

Part 1에서 야마하 V9938 데이터시트로 확인한 제약을 다시 꺼내 보겠습니다.

제약 내용
스프라이트 총 개수 32개
한 수평 라인당 8개 (9번째부터 안 그려짐)
스프라이트 색 수평 라인마다 한 색
가로 스크롤 레지스터 없음 (V9958 에서야 추가)
세로 스크롤 R#23 으로 가능

여기에 메탈기어가 고른 화면 모드가 더해집니다. disassembly 의 하드웨어 초기화 코드에 답이 그대로 있습니다.

;----------------------------------------------------------------------------
; Initialize hardware
;
; Enable PSG channels
; Disable sprites
; Set SCREEN 5 mode
; Clear VRAM pages 0 and 1
; ...
;----------------------------------------------------------------------------

InitHardware:
		    ...
		    call    DisableSprites

		    ld	    a, 5
		    call    CHGMOD			    ; Screen 5

SCREEN 5, 즉 V9938 매뉴얼의 GRAPHIC 4 모드입니다. 사양을 다시 보겠습니다.

항목
종류 Bit-mapped Graphics Mode
화면 크기 256 x 212 (또는 256 x 192)
512색 중 16색
스프라이트 모드 Sprite mode 2
화면 하나당 VRAM 32KB

비트맵입니다. 픽셀을 자유롭게 찍을 수 있는 대신 화면 한 장이 32KB 입니다.

이 두 가지를 겹치면 결론이 하나 나옵니다.

  • 가로 스크롤 레지스터가 없으므로 하드웨어가 화면을 밀어 주지 않는다.
  • 소프트웨어로 밀려면 32KB 를 옮겨야 한다.
  • 3.58MHz Z80 의 한 프레임은 약 6만 T-state 다. 32KB 를 옮기는 것은 불가능하다.
  • MSX1 처럼 타일맵 768바이트만 옮기는 수법은 비트맵 모드에서 쓸 수 없다.

1987년에 MSX2 로 가로로 흐르는 액션 게임을 만들라는 것은, 하드웨어가 거부하는 일을 하라는 것과 같았습니다.

당시 기획자였던 코지마 히데오는 2005년 MSX 매거진 인터뷰에서 이렇게 회고합니다.

당시에 캡콤의 인기 아케이드 게임 커맨도 가 있어서, 그것과 비슷한 게임을 MSX 로 만들라는 지시를 받았습니다. 그때는 여섯 방향으로밖에 움직일 수 없었던 걸로 기억합니다. 여섯 방향은 무리였고, 네 방향으로 줄여도 너무 갑갑했습니다. 적을 내보내지 않고도 성립하는 게임을 만드는 수밖에 없었습니다. 그리고 그것이 메탈기어를 만들 수 있게 해 준 것입니다.

같은 인터뷰에서 총알에 대한 언급도 나옵니다.

(진행자) 메탈기어의 게임 시스템은 MSX 의 하드웨어 제약을 오히려 자기 것으로 썼습니다. 플레이해 보면 화면에 총알이 그렇게 많지 않다는 게 눈에 띕니다.

(코지마) 가로로도 안 나갔습니다.

참고로 이 인터뷰에서 코지마는 MSX1 이라고 말하지만 실제 메탈기어는 MSX2 용으로 나왔습니다. 번역자도 이 점을 각주로 지적하면서, 기억 착오이거나 구형 하드웨어에서 돌던 프로토타입 얘기일 수 있다고 적어 두었습니다. 어느 쪽인지는 확인되지 않으므로 그대로 옮깁니다.


2. 화면이 끊어진다, 그래서 방이 생긴다#

가로로 스크롤할 수 없다면 답은 하나입니다. 화면 단위로 뚝뚝 끊어서 전환합니다.

이건 후퇴처럼 보이지만, 데이터 관점에서는 엄청난 이득입니다. 스크롤하지 않는다면 화면 하나를 통째로 한 단위 로 다룰 수 있고, 그러면 화면 하나를 아주 작게 표현할 수 있습니다.

disassembly 의 방 렌더링 루틴에 구조가 주석으로 적혀 있습니다.

;----------------------------------------------------------------------------
; Render room tiles
;
; A room is defined by 8x6 metatiles
; Unpack room metatiles into normal tiles and draw them
;----------------------------------------------------------------------------

그리고 메타타일을 푸는 루틴을 보면 메타타일 하나가 4 x 4 타일 입니다.

UnpackMetatiles3:
		    ld	    b, 4			    ; 4	tiles width

UnpackMetatiles4:
		    ld	    a, (hl)
		    ld	    (de), a			    ; Transfer tiles of	the metatile
		    inc	    hl
		    inc	    de
		    djnz    UnpackMetatiles4
		    ...
		    dec	    c				    ; c 는 4로 시작 = 4 tiles height
		    jr	    nz,	UnpackMetatiles3

계층을 정리하면 이렇습니다.

방 (8 x 6 메타타일)  =  48바이트
        ↓
메타타일 (4 x 4 타일) =  16바이트, 여러 방이 공유
        ↓
타일 (8 x 8 픽셀)
        ↓
비트맵 화면 32,768바이트

8 x 6 메타타일 x 4 x 4 타일 x 8 x 8 픽셀 = 256 x 192 픽셀. SCREEN 5 의 화면 크기와 정확히 맞습니다.

방 하나가 48바이트입니다. 화면 하나를 비트맵으로 저장하면 32,768바이트인데, 그 화면을 48바이트로 적었습니다.

실제 데이터를 세어 봤습니다.

항목 크기
방 지도 (165개 방) 8,382 바이트
메타타일 정의 (6개 타일셋) 9,280 바이트
방 연결 관계 624 바이트
방별 타일셋 지정 401 바이트
방별 적 배치 536 바이트
방별 아이템 배치 209 바이트
경비병 순찰 경로 814 바이트
게임 세계 합계 약 20,246 바이트

165개 방 중 161개가 정확히 48바이트 입니다. 나머지 네 개만 96, 144, 192바이트로 더 큽니다.

숫자를 비교해 봅시다. 165개 방을 전부 비트맵으로 저장하면 165 x 32,768 = 약 5.4MB 입니다. 128KB 카트리지에 들어갈 리가 없습니다. 방 지도로 저장하니 8,382바이트, 화면 한 장의 4분의 1입니다.

월드 1-1 이 101바이트였던 마리오와 발상이 같습니다. 다만 마리오는 스크롤하기 위해 컬럼 단위로 잘게 조립해야 했고, 메탈기어는 스크롤하지 않기 때문에 방 하나를 한 번에 통째로 그리면 됩니다. 방을 넘어갈 때 잠깐 화면이 멈추는 그 순간이, 32KB 비트맵을 새로 채워 넣는 시간입니다.

하드웨어가 못 하는 일을 포기했더니 데이터가 작아졌습니다.


3. 스프라이트 32개는 사실 16개다#

이제 캐릭터입니다. V9938 은 스프라이트를 32개 지원하고 한 라인에 8개까지 그립니다. 넉넉해 보이지만 실제로는 그렇지 않습니다.

문제는 색입니다. Part 1 에서 봤듯 SPRITE MODE 2 의 색 지정은 수평 라인 단위 입니다. 한 가로줄 안에서는 여전히 한 색뿐입니다. 캐릭터의 같은 높이에 살색 얼굴과 다른 색 옷을 함께 둘 수 없습니다.

메탈기어가 이 문제를 어떻게 처리했는지, 스프라이트 갱신 루틴의 주석에 그대로 적혀 있습니다.

;----------------------------------------------------------------------------
; Update sprites colors	and attributes in VRAM from RAM
; Shuffles the sprite layers to	avoid horizontal sprites limit (flickering)
; Sprites are grouped in pairs in order	to get 3 colors	per line
;----------------------------------------------------------------------------

두 가지 기법이 한 문단에 들어 있습니다.

짝지어서 색을 늘린다#

“스프라이트를 쌍으로 묶어 라인당 3색을 얻는다.”

스프라이트 두 장을 같은 자리에 겹쳐 놓고, 한 장은 옷 부분만, 다른 한 장은 얼굴 부분만 그립니다. 각각은 라인당 한 색이지만 겹치면 두 색이 됩니다. 여기에 CC 비트로 색을 OR 하면 겹친 부분이 세 번째 색이 됩니다.

대가는 명확합니다. 캐릭터 하나가 스프라이트 두 장을 먹습니다.

하드웨어 사양 메탈기어의 실질
스프라이트 총 개수 32개 캐릭터 16개
한 라인당 8개 한 라인에 캐릭터 4개

한 가로줄에 캐릭터 넷. 스네이크 하나, 경비병 셋이면 이미 꽉 찹니다. 여기서 총알 하나만 더해도 무언가가 사라집니다.

순서를 섞어서 나눠 깜빡인다#

“스프라이트 레이어를 섞어 가로 스프라이트 제한(깜빡임)을 피한다.”

한 라인에 아홉 번째 스프라이트가 걸리면 그놈은 안 그려집니다. 그런데 매 프레임 순서를 바꾸면 안 그려지는 놈이 매번 달라집니다. 전부가 절반씩 보이게 되고, 사람 눈에는 깜빡임으로 보입니다.

UpdateSpritesShuf:
		    ld	    hl,	SprShuffleOffset
		    ld	    a, (hl)
		    add	    a, 68h
		    and	    78h
		    ld	    (hl), a

매 프레임 오프셋에 $68 을 더하고 $78 로 마스킹해서 순환시킵니다. 그 오프셋부터 스프라이트를 VRAM 에 올립니다.

여기서 Part 2를 떠올려 봅시다. 슈퍼 마리오브라더스에도 SpriteShuffler 라는 루틴이 있었고, 하는 일이 정확히 같았습니다.

패미콤과 MSX2, 다른 회사, 다른 CPU, 다른 비디오 칩. 그런데 스캔라인당 스프라이트 8개라는 같은 제약 앞에서 두 팀이 똑같은 답을 냈습니다. 아무것도 안 보이는 것보다는 깜빡이는 게 낫다는 판단입니다. 1980년대 게임의 그 특유의 깜빡임은 기술 부족의 흔적이 아니라, 제한된 자원을 공평하게 나누는 스케줄러의 흔적입니다.


4. 총알을 못 그리면, 총을 쏘지 않게 만든다#

여기까지가 재료입니다. 이제 이 재료로 무엇을 만들었는지 봅니다.

커맨도 같은 게임을 만들려면 화면에 적이 여럿 있고 총알이 사방으로 날아다녀야 합니다. 그런데 방금 계산했듯 한 가로줄에 캐릭터 넷 이 한계입니다. 스네이크와 경비병 둘, 총알 하나면 끝입니다.

보통의 해법은 “그래도 어떻게든 많이 그린다” 입니다. 깜빡임을 감수하고 밀어붙이거나, 총알을 스프라이트가 아닌 다른 방법으로 그리거나.

메탈기어의 해법은 달랐습니다.

적을 내보내지 않고도 성립하는 게임을 만드는 수밖에 없었습니다.

총을 쏘지 않으면 총알을 그릴 필요가 없습니다. 적과 마주치지 않으면 적을 여럿 그릴 필요가 없습니다. 화면 하나에 경비병 한둘만 있으면 스프라이트 예산이 남아돕니다.

그래서 게임의 목표가 뒤집힙니다. 적을 없애는 것이 아니라 적에게 들키지 않는 것 이 목표가 됩니다. 전투는 실패의 신호이지 게임의 내용이 아닙니다.

이 뒤집기의 놀라운 점은, 포기한 것이 하나도 게임의 손해가 아니라는 것입니다.

하드웨어가 못 하는 것 보통의 대응 메탈기어의 대응 결과
화면을 가로로 스크롤 타일 모드로 후퇴 방 단위로 끊는다 방 = 경비 구역이라는 단위가 생김
적과 총알을 많이 그리기 깜빡임 감수 싸우지 않는 게임 긴장감이 전투가 아니라 회피에서 나옴
넓은 시야 표현 화면 밖은 안 보임 다음 방에 뭐가 있는지 모르는 공포
동시 입력 기능키에 기능 배분 무전기, 아이템 메뉴 같은 별도 계층

마지막 줄도 코지마의 회고에 나옵니다.

기능키가 다섯 개 있어서 각각을 쓰기로 했습니다. 예를 들어 F4 키는 무전기를 부릅니다. 컨트롤러에서 키 두 개를 동시에 누르는 건 생각할 수도 없었습니다.

컨트롤러의 버튼이 부족해서 키보드 기능키로 도망친 결과가, 시리즈의 상징이 된 무전기 시스템 입니다.


5. 시야는 원뿔이 아니라 복도다#

경비병이 플레이어를 발견하는 판정을 봅시다. 오늘날이라면 시야각을 정하고 내적을 계산하고 레이캐스트를 쏘겠지만, 3.58MHz Z80 에는 그럴 여유가 없습니다.

메탈기어의 판정은 네 단계입니다.

ChkLookRight:
		    call    ChkViewHorizontal		    ; 플레이어가 같은 가로 띠 안에 있는가
		    ret	    nc

		    call    ChkPosLeftRight
		    ret	    nc				    ; 적의 등 뒤에 있으면 볼 수 없다

		    neg					    ; 거리를 양수로
		    and	    0F8h			    ; 8의 배수로 맞춤
		    rra
		    rra
		    rra					    ; 타일 개수 단위의 거리
		    jr	    nz,	ChkLookRight2
		    inc	    a

ChkLookRight2:
		    ld	    c, 1			    ; 다음 타일로 가는 오프셋
		    ; 이어서 ChkViewObstacles 로 떨어진다
  1. 같은 가로 띠 안에 있는가.
  2. 바라보는 방향 쪽에 있는가. (등 뒤면 못 봄)
  3. 거리를 타일 개수로 환산한다. and $F8 후 오른쪽 시프트 세 번, 즉 8로 나눕니다. 나눗셈 명령 없이 마스크와 시프트만 씁니다.
  4. 그 타일들을 하나씩 훑으며 장애물이 있는지 본다.

여기서 “가로 띠” 의 폭이 흥미롭습니다.

ChkViewHorizontal:
		    ld	    a, (ix+ACTOR.ID)
		    cp	    ID_CAMERA

		    ld	    hl,	408h			    ; 카메라: H = 시야폭/2, L = 시야폭
		    jr	    z, ChkViewHorizontal2
		    ld	    hl,	60Ch			    ; 경비병: H = 시야폭/2, L = 시야폭
상황 시야 폭
경비병이 좌우를 볼 때 12픽셀 ($0C)
경비병이 위아래를 볼 때 16픽셀 ($10)
감시 카메라 8픽셀 ($08)

시야가 원뿔이 아닙니다. 폭이 고정된 복도입니다. 각도가 없으니 삼각함수도, 내적도, 나눗셈도 필요 없습니다. 비교 몇 번과 타일 훑기가 전부입니다.

Go 로 옮기면 이 정도입니다.

type Dir int

const (
    Up Dir = iota + 1
    Down
    Left
    Right
)

// 경비병이 바라보는 방향으로만, 정해진 폭의 복도 안에서만 본다.
// 각도 계산이 없으므로 곱셈도 삼각함수도 쓰지 않는다.
func (g Guard) CanSee(p Player, room *Room) bool {
    var band int // 시야 복도의 폭 (픽셀)
    switch g.Dir {
    case Left, Right:
        band = 12
    default:
        band = 16
    }
    if g.IsCamera {
        band = 8
    }

    // 1) 같은 띠 안에 있는가
    switch g.Dir {
    case Left, Right:
        if p.Y < g.Y-band/2 || p.Y > g.Y+band/2 {
            return false
        }
    default:
        if p.X < g.X-band/2 || p.X > g.X+band/2 {
            return false
        }
    }

    // 2) 바라보는 방향 쪽에 있는가 (등 뒤면 못 본다)
    var dist int
    switch g.Dir {
    case Right:
        dist = p.X - g.X
    case Left:
        dist = g.X - p.X
    case Down:
        dist = p.Y - g.Y
    case Up:
        dist = g.Y - p.Y
    }
    if dist < 0 {
        return false
    }

    // 3) 거리를 타일 개수로. 나눗셈 대신 마스크와 시프트.
    tiles := (dist & 0xf8) >> 3
    if tiles == 0 {
        tiles = 1
    }

    // 4) 경비병에서 플레이어 쪽으로 타일을 하나씩 훑으며 시선이 막히는지 본다
    return !room.HasObstacle(g.X, g.Y, g.Dir, tiles)
}

그리고 이 판정 앞에 예외 처리가 붙습니다. disassembly 의 ChkSeePlayer 를 보면 이런 것들입니다.

ChkSeePlayer:
		    ...
		    ld	    a, (AlertMode)
		    or	    a
		    ret	    nz				    ; 경보 중에는 이미 위치를 알고 있다

		    ld	    a, (SnakeSprId)
		    ld	    b, 37			    ; 깊은 물 그림자 스프라이트
		    cp	    b
		    ret	    z				    ; 깊은 물속의 스네이크는 보이지 않는다
		    ...
		    ld	    a, (PlayerAnimation)	    ; 7 = 골판지 상자
		    cp	    7
		    jr	    nz,	ChkSeePlayer2

; 골판지 상자가 움직이고 있는지 검사한다
		    ld	    hl,	PlayerSpeedY
		    ld	    a, (hl)
		    inc	    hl
		    or	    (hl)			    ; 위아래로 움직이는가
		    jr	    nz,	ChkSeePlayer2
		    ...
		    ret	    z				    ; 움직이지 않으면 발각되지 않는다

시리즈의 상징이 된 골판지 상자 가 여기 있습니다. 규칙은 단순합니다. 상자를 쓰고 가만히 있으면 안 보이고, 움직이면 보입니다. 속도 변수 네 개가 전부 0인지만 확인합니다.

깊은 물에 들어가도 안 보입니다. 이것도 스프라이트를 물 그림자 스프라이트로 갈아 끼우고 그 ID 를 비교하는 것으로 끝냅니다. 별도의 은신 상태 변수도 없습니다.


6. 경보는 방에 붙어 있다#

메탈기어에서 가장 중요한 시스템은 경보입니다. 그리고 그 경보가 방 단위 로 관리된다는 사실이, 하드웨어 제약과 게임 디자인이 만나는 지점입니다.

경보를 켜는 코드입니다.

SetAlertMode:
		    ld	    a, 1
		    ld	    (AlertMode), a
		    ld	    (AlertModeCopy), a

		    ld	    a, (Room)
		    ld	    (RoomAlert), a		    ; 경보가 발생한 방

RoomAlert경보에 방 번호가 붙습니다. 경보는 전역 상태가 아니라 특정 방의 속성입니다.

그리고 경보를 끄는 코드입니다.

ChkAlarmEnd:
		    ld	    a, (TransmiTaken)
		    or	    a
		    ret	    nz				    ; 발신기를 들고 있으면 경보가 절대 끝나지 않는다

		    ld	    a, (AlertMode)
		    or	    a
		    ret	    z				    ; 경보 중이 아님

		    ld	    a, (Room)
		    cp	    0F0h			    ; 엘리베이터인가
		    jr	    nc,	StopAlert		    ; 엘리베이터에 들어가면 경보 해제
		    ...

ChkAlarmEnd2:
		    ld	    a, (Room)
		    ld	    hl,	RoomAlert
		    cp	    (hl)
		    jr	    nz,	StopAlert		    ; 지금 방이 경보 걸린 방이 아니면 해제
		    ...

ChkEnemyCount:
		    call    CountEnemyType
		    cp	    10h
		    jr	    z, StopAlert		    ; 적이 너무 많으면 해제

		    or	    a
		    ret	    nz				    ; 적이 남아 있으면 경보 유지

규칙을 정리하면 이렇습니다.

조건 결과
발신기를 소지 중 경보가 절대 끝나지 않음
엘리베이터에 진입 경보 해제
지금 방 != 경보 걸린 방 경보 해제
그 방에 적이 남아 있음 경보 유지
적이 16마리 이상 경보 해제 (안전장치)

세 번째 줄이 핵심입니다. 경보가 걸린 방을 벗어나면 경보가 풀립니다.

플레이 감각으로 옮기면 이렇습니다. 들켰다. 경비병이 몰려온다. 옆방으로 도망친다. 조용해진다. 이 리듬이 메탈기어의 기본 박자이고, 그 박자는 가로 스크롤이 안 돼서 화면을 방 단위로 끊었기 때문에 생겼습니다.

경보 중 몰려오는 경비병의 수도 계산됩니다.

		    ld	    hl,	Card8Taken
		    ld	    b, 8			    ; 카드 8장

SetAlertMode2:
		    bit	    0, (hl)			    ; 이 카드를 획득했는가
		    jr	    nz,	SetAlertMode3
		    dec	    hl
		    djnz    SetAlertMode2

SetAlertMode3:
		    ld	    a, b
		    add	    a, 3			    ; 최소 리스폰 경비병 수

플레이어가 가진 가장 높은 등급 카드 번호에 3을 더한 값 이 리스폰 경비병 수가 됩니다. 게임이 진행될수록 카드 등급이 올라가고, 경보가 걸렸을 때 몰려오는 적도 늘어납니다. 난이도 곡선을 별도 테이블 없이 진행도 변수 하나로 만들어 냈습니다.

Go 로 옮기면 이렇게 됩니다.

type AlertState struct {
    Active    bool
    Room      int // 경보가 걸린 방. 이 방을 벗어나면 경보가 풀린다.
    Respawns  int
}

func (a *AlertState) Trigger(g *Game) {
    a.Active = true
    a.Room = g.CurrentRoom

    // 가진 카드 중 가장 높은 등급 + 3 이 몰려올 경비병 수가 된다.
    highest := 0
    for i := 8; i >= 1; i-- {
        if g.HasCard(i) {
            highest = i
            break
        }
    }
    a.Respawns = highest + 3
}

func (a *AlertState) Update(g *Game) {
    if !a.Active {
        return
    }
    switch {
    case g.HasTransmitter:
        return // 발신기를 들고 있으면 절대 풀리지 않는다
    case g.IsElevator(g.CurrentRoom):
        a.Active = false
    case g.CurrentRoom != a.Room:
        a.Active = false // 경보가 걸린 방을 벗어났다
    case g.EnemyCount() >= 16:
        a.Active = false // 안전장치
    case g.EnemyCount() == 0:
        a.Active = false
    }
}

상태 머신이 열 줄입니다. 여기에 “방” 이라는 개념이 없었다면 이 코드는 성립하지 않습니다. 그리고 방이라는 개념은 게임 디자이너가 발명한 것이 아니라 V9938 에 가로 스크롤 레지스터가 없어서 생긴 것 입니다.


7. 제약이 문법이 되다#

메탈기어가 만든 규칙들을 나열해 보면, 거의 전부가 하드웨어의 그림자입니다.

게임 문법 유래한 제약
방 단위 경비 구역 가로 스크롤 레지스터 없음 + 비트맵 32KB
옆방으로 도망치면 경보 해제 위와 같음
전투가 아니라 회피가 목표 스캔라인당 캐릭터 4개
총알이 적고 느림 위와 같음
가만히 있으면 안 보이는 골판지 상자 속도 변수 검사 한 번으로 끝나는 판정
시야가 직선으로만 뻗음 곱셈 명령 없음, 4방향 이동
무전기와 아이템 메뉴 컨트롤러 동시 입력 불가
화면 밖은 알 수 없음 스크롤 없음

그리고 이 문법들은 하드웨어가 사라진 뒤에도 살아남았습니다. 1998년 플레이스테이션의 메탈기어 솔리드는 3D 폴리곤을 쓰면서도 구역 단위 경계 상태, 경보 타이머, 시야에 들어가면 느낌표, 숨을 곳 이라는 문법을 그대로 가져갔습니다. 그다음의 스텔스 게임들도 대부분 그렇습니다.

코지마 본인도 같은 인터뷰에서 이렇게 말합니다.

3D 폴리곤이 되는 가정용 기기가 나온다는 얘기를 들었을 때, 제일 먼저 든 생각은 “드디어 진짜 숨바꼭질 게임을 만들 수 있겠다” 였습니다.

제약이 사라지자 하려던 것을 마저 했을 뿐, 하려던 것 자체가 제약에서 태어난 것입니다.


마치며#

Part 4 에서 확인한 것을 정리하면 이렇습니다.

  • 메탈기어는 SCREEN 5(GRAPHIC 4) 비트맵 모드를 씁니다. 화면 한 장이 32KB 이고, V9938 에는 가로 스크롤 레지스터가 없습니다. 가로로 흐르는 화면은 물리적으로 불가능했습니다.
  • 그래서 화면을 방 단위로 끊었고, 그 결과 방 하나가 48바이트가 됐습니다. 8 x 6 메타타일, 메타타일 하나가 4 x 4 타일입니다. 165개 방 전체가 8,382바이트, 게임 세계 전부가 약 20,246바이트입니다. 같은 것을 비트맵으로 저장하면 5.4MB 입니다.
  • 스프라이트 32개는 실질적으로 캐릭터 16개입니다. 라인당 한 색이라는 제약 때문에 두 장을 겹쳐 3색을 만들고, 그 대가로 한 가로줄에 캐릭터 넷이 한계가 됩니다.
  • 순서를 매 프레임 섞어 깜빡임으로 분산합니다. 슈퍼 마리오브라더스의 SpriteShuffler 와 정확히 같은 해법입니다. 다른 회사, 다른 하드웨어, 같은 제약, 같은 답.
  • 시야는 원뿔이 아니라 폭이 고정된 복도입니다. 경비병 12픽셀, 위아래 16픽셀, 카메라 8픽셀. 거리는 마스크와 시프트로 타일 수로 환산하고, 그 타일들을 훑어 장애물을 검사합니다.
  • 경보에는 방 번호가 붙어 있습니다. 그 방을 벗어나면 경보가 풀립니다. 이 한 줄의 비교가 메탈기어의 기본 리듬을 만듭니다.

세 편을 관통하는 대비를 마지막으로 정리하겠습니다.

마리오 엘리트 메탈기어
부족했던 것 ROM 용량 RAM 용량 스프라이트, 스크롤
전략 압축 생성 전복
무엇을 했나 데이터를 작게 데이터를 안 가짐 그 데이터가 필요 없게
세계 데이터 4,460 바이트 6 바이트 20,246 바이트
대가 표현 가능한 오브젝트가 제한됨 세부를 손댈 수 없음 원래 만들려던 게임을 포기함

메탈기어의 대가가 가장 큽니다. 만들라고 지시받은 게임을 아예 만들지 않았습니다. 그런데 결과적으로 셋 중 가장 오래 남은 것도 메탈기어의 선택입니다. 마리오의 레벨 인코딩과 엘리트의 씨앗 알고리듬은 하드웨어가 나아지면서 필요 없어졌지만, “싸우지 않는 게임” 이라는 아이디어는 하드웨어와 무관하기 때문 입니다.

Part 5 에서는 이 시리즈를 마무리합니다. 제약이 사라진 시대에 이 세 가지 전략 중 무엇이 남았고, 무엇이 없어졌고, 왜 어떤 사람들은 제약을 일부러 되살리는지를 봅니다.


References#