본문 바로가기
STEM - 학술세미나/etc

버그 갓겜 포켓몬스터

by STEMSNU 2022. 6. 18.

안녕하세요! 공우 13기, 컴퓨터공학부 이성찬입니다! '버그 갓겜 포켓몬스터'라는 주제로 세미나를 하게 되었습니다!

포켓몬 프랜차이즈는 무척 유명하기 때문에, '피카츄 라이츄 파이리 꼬부기'라는 주제가는 한 번쯤 들어보셨을 것 같습니다. 출시한 이후 25년이 넘게 지난 지금까지도 포켓몬은 엄청난 인기를 끌고 있는데, 이 전설의 시작을 열었던 첫 작품이 바로 포켓몬스터 레드/그린 버전입니다.

이 게임은 1996년 2월 27일에 발매되었고, TIMES가 선정한 50대 비디오 게임 중 하나입니다. (30위) 흔히 1세대 포켓몬이라고 부르는 총 151마리의 포켓몬이 등장하며, 이 1세대 포켓몬들은 지금까지도 포켓몬스터 개발사의 ATM 역할을 해주고 있습니다. '피카츄 라이츄 파이리 꼬부기' 모두 1세대 포켓몬 일만큼 이들은 인지도가 높으며, 결정적으로 귀엽기 때문에 굿즈에 많이 활용되었습니다. 그리고 1세대 게임인 레드/그린은 무려 2번의 리메이크를 받을 정도로 개발사에서 밀어주고 있고요, 올해 돌아온 포켓몬 빵의 띠부띠부 씰에도 1세대 포켓몬만 등장합니다.

여기서 정말 놀라운 사실은 이때 당시 게임 파일의 용량이 일본에서는 512KB, 해외에서는 1MB 였다는 점입니다. 사진 한 장만 찍어도 몇 MB를 훌쩍 넘어가는 시대를 살고 있는 저희들 입장에서 보면 당시 개발자들이 얼마나 힘들었을지 상상하기 어렵습니다.

하지만 이렇게 적은 용량으로 게임을 개발하다 보니 개발자들이 의도하지 않은 버그들이 게임 곳곳에 숨겨져 있는데요, 이 글에서는 흥미로운 몇 가지 버그와 이 버그가 발생하는 이유에 대해서 간략하게 설명하고자 합니다.

버그 포켓몬 미싱노

딱 봐도 버그 포켓몬 처럼 생겼습니다.

포켓몬 게임에는 버그 포켓몬이 존재합니다. 1세대 포켓몬은 총 151마리이지만, 기획 및 개발 당시에는 총 190마리였습니다. 그런데 출시 전에 용량 문제로 인해서 39마리 포켓몬이 아쉽게도 삭제되었고, 사라진 번호(Missing Number)가 되어 이를 미싱노(MissingNo.)라고 부릅니다.

삭제된 포켓몬의 흔적 (출처: Bulbapedia)

실제로 게임의 메모리를 분석해 보면 이렇게 출시 전에 삭제되었던 포켓몬의 자리가 남아있습니다. 

그럼 게임 속에서 미싱노를 실제로 만나는 방법을 보겠습니다. 영상으로도 볼 수 있습니다!

  • 먼저 상록시티의 북서쪽에 있는 노인에게 포켓몬 잡는 방법을 전수받습니다.
  • 공중날기를 사용해 홍련섬으로 날아갑니다.
  • 동쪽 해안가에서 파도타기를 쓰고, 육지와 바다가 만나는 지점을 왔다 갔다 합니다.
  • 야생 포켓몬이 등장하는데, 미싱노가 등장합니다.

버그 파헤치기

이 장면이 문제의 장면입니다.

포켓몬 잡는 방법을 전수받는 장면

"OLD MAN used POKE BALL!" (노인은 몬스터볼을 사용했다!)이라는 메시지가 나옵니다. 보통 이렇게 고정된 텍스트를 보여주는 경우에는 게임 데이터 어딘가에 이 텍스트가 그대로 있습니다. 하지만 개발자들은 용량이 부족했는지, 여기서 약간의 최적화를 합니다.

일반적인 경우라면 "OLD MAN"의 자리에는 플레이어의 이름이 들어가게 됩니다. 플레이어가 실제로 몬스터볼을 사용했을 때 나오는 메시지이기 때문입니다. 그래서 게임 로직 상으로는 이 메시지가 출력될 때 플레이어의 이름을 불러오도록 되어있습니다. 그럼 플레이어의 이름은 어디로 갔을까요?

개발자들은 다음과 같은 방식으로 최적화를 했습니다. 플레이어 이름이 예를 들어 "SCRUMPY"라고 합시다. 플레이어의 이름을 잠시 어딘가에 복사해두고, 플레이어 이름이 저장된 곳을 "OLD MAN"으로 덮어 씌운 다음에 불러오는 것입니다. 그리고 노인의 포켓몬 잡는 방법 설명이 끝난 뒤에는, 다른 곳에 저장해뒀던 플레이어의 이름을 다시 가져와서 원래대로 돌려놓습니다. 여기까지는 아무런 문제가 없습니다.

복사해 뒀다가, 덮어쓴 뒤, 다시 원래대로 돌려놓는 과정입니다.

한편, 개발자들이 플레이어의 이름을 임시로 복사해둔 곳은 현재 플레이어가 위치한 지역의 야생 포켓몬의 출현 정보를 담고 있는 곳입니다. 왜 이렇게 했을까 추측해보면, 플레이어는 마을에 있기 때문에 야생 포켓몬을 만날 일이 없으므로 이 메모리 영역은 사용되지 않는 곳입니다. 사용되지 않는 메모리를 효율적으로 활용한 것이니 좋은 방법인 것 같습니다. 그런데 개발자들은 이 메모리를 사용한 뒤에 초기화하지 않는 실수를 하게 됩니다.

일반적으로 마을은 야생 포켓몬이 등장하는 도로와 연결되어 있기 때문에 마을에서 도로로 나가게 되면 이 영역의 메모리가 초기화됩니다. 하지만 마을에서 마을로 날아가는 경우에는 야생 포켓몬을 만날 일이 없으므로 이 영역의 메모리가 초기화되지 않습니다. 따라서 야생 포켓몬의 정보는 플레이어 이름으로 덮어쓰인 상태가 됩니다.

초기화 하지 않아 플레이어의 이름으로 덮어 씌워진 야생 포켓몬 출현 정보

더불어 홍련섬 동쪽 해안은 야생 포켓몬이 출현 가능한데, 이 또한 버그입니다. 따라서 여기서 만나는 야생 포켓몬은 정상적인 데이터가 아니게 되고, 버그 포켓몬들을 만나게 됩니다.

덮어쓰인 데이터는 게임에 의해 해석될 때 아래와 같이 해석됩니다. 플레이어 이름이 "SCRUMPY"였다면, 레벨 130의 텅구리, 레벨 148의 미싱노, 레벨 143의 아쿠스타를 만날 수 있게 되는 것입니다. 또 예를 들어 그 뒤의 메모리에 50, 0이 저장되어 있었다면 레벨 80의 버그 포켓몬을 만나게 됩니다.

게임이 잘못된 데이터를 해석하는 과정

그리고 실제로 게임 상에서 이들을 만날 수 있습니다.

잘못된 데이터대로 등장하는 야생의 미싱노와 버그 포켓몬

이 동작은 버그인 만큼 게임의 비정상적 작동이므로 기타 부작용도 존재합니다. 보통 새로운 포켓몬을 만나면 자동으로 도감에 만났다는 정보를 기록하는데, 미싱노는 잘못된 번호를 가진 포켓몬이기 때문에 잘못된 메모리 위치에 쓰기가 일어나게 되고, 이로 인해 가방의 6번째 아이템 개수가 128개 추가됩니다.

이상한 사탕이 128개가 된 모습, 두 자리수만 표시해서 깨진 것처럼 보입니다

경험치 버그

다음은 경험치 버그입니다. 포켓몬도 종 별로 성장 속도가 다릅니다. 1세대 당시에는 성장 속도를 Fast, Medium Fast, Medium Slow, Slow 의 4가지 그룹으로 분류했습니다. 각 그룹마다 레벨별 요구 경험치를 표현하는 수식이 있고, 다음과 같습니다.

성장 속도에 따라 달라지는 레벨별 요구 경험치

하필 Medium Slow 그룹의 식만 좀 복잡한데, 여기서 문제가 발생합니다. Medium Slow 그룹의 포켓몬이 레벨 1일 때 가진 경험치를 계산해보면 \(-54\) 로, 음수가 나옵니다. 

그런데 경험치는 일반적으로 음수가 될 수 없기 때문에 메모리에 저장된 값은 부호 없는 3바이트 정수(unsigned integer)로 해석됩니다. 그래서 \(0\) 부터 \(16,777,215\) (\(2^{24} - 1\)) 의 값만 가질 수 있으며, 계산 시 범위가 넘어가면 \(16,777,216\) (\(2^{24}\))로 나눈 나머지를 결과로 사용합니다.

레벨 1 팬텀의 경험치가 777,162? (앞부분이 잘린 것입니다)

그래서 Medium Slow 그룹인 레벨 1의 포켓몬의 경우 초기 경험치가 \(-54 = \text{ 0xFFFFCA } = 16,777,162\) 로 해석되고, 만약 \(54\) 보다 적은 양의 경험치를 받게 되면 한 번에 레벨 100이 되는 버그가 발생하게 됩니다.

단번에 레벨 100!

이야깃거리

다만 이 버그는 쓰기 약간 까다롭습니다. 우선 성장 속도가 Medium Slow 인 포켓몬만 가능하며, 버그를 사용하기 위해서는 레벨 1인 포켓몬을 만나야 하는데 야생에서는 정상적인 방식으로 레벨 1 포켓몬을 만날 수 없습니다. (이 때는 포켓몬이 알에서 부화하면 레벨 5이던 시절입니다.)

마찬가지로 부작용도 있는데요, 한 번에 레벨 100이 되었기 때문에 기술을 하나도 배울 수 없게 됩니다. 일반적으로 1 레벨 이상 올릴 수 있는 경험치를 받아도 레벨이 1씩 오르기 때문에, 레벨이 오를 때마다 배울 수 있는 기술이 있는지 확인하도록 설계되어 있습니다. 중간 레벨을 모두 건너뛰었으므로 기술을 배울 수 없습니다.

레벨은 100이지만 기술은 하나 밖에 모르는 뮤

이 버그는 2세대 게임에서도 고쳐지지 않아 남아있었고, 3세대 게임 이후에는 메모리 기술의 발달로 인해 요구 경험치를 직접 모두 게임 속에 저장할 수 있게 되었고, 버그가 고쳐졌습니다.

장소 이동 버그

일명 어디로든 문 버그입니다. 버그를 사용하는 방법은 다음과 같습니다. 영상으로도 볼 수 있습니다!

  • 포켓몬은 1마리만 지닌 채로 1번 도로로 갑니다.
  • 가방에 아이템을 2개 준비합니다.
  • 아이템 창에서 2번째 아이템으로 커서를 이동합니다.
  • SELECT 버튼을 눌러 아이템을 선택하고, B 버튼을 두 번 눌러 메뉴에서 나갑니다.
  • 야생 포켓몬과의 전투 화면에 진입합니다.
  • '포켓몬' 메뉴를 선택하여 지닌 포켓몬 목록을 확인하는 화면으로 들어갑니다.
  • A 버튼을 눌러 포켓몬을 교체합니다. (여기서 화면이 조금 이상해질 수 있습니다)
  • B 버튼으로 메뉴에서 나가고, 도망가기를 선택해 전투를 종료합니다.
  • 적당한 걸음 수를 걸은 뒤, 라이벌의 집 문으로 들어가면 다른 장소로 순간이동됩니다.

참고로 걸음 수에 따라 이동하는 장소가 달라집니다. 

버그 파헤치기

아이템 목록에서 SELECT 버튼을 누르면 선택한 아이템이 몇 번째인지 메모리 주소 CC35에 저장합니다. 2번째 아이템을 선택했기 때문에 CC35에는 2가 저장됩니다. 원래 메뉴를 나가면 선택이 해제되므로, CC35의 값이 0 (아무것도 선택하지 않음)으로 초기화되어야 합니다. 그런데 버그로 인해 메뉴를 꺼도 CC35의 값이 유지됩니다.

원래는 리셋되어야 합니다

한편, 메모리의 효율적인 활용을 위해 CC35는 2가지 용도로 사용됩니다. 앞서 살펴본 아이템 목록에서 몇 번째 아이템을 선택했는지 저장하기도 하지만, 포켓몬 목록에서 순서를 바꾸기 위해 몇 번째 포켓몬을 선택했는지 저장하기도 합니다.

그런데 또 버그로 인해 야생 포켓몬과의 전투 화면에서 포켓몬 목록으로 들어가도 CC35의 값이 초기화되지 않고 유지됩니다. 그래서 이 상태에서 게임은 사용자가 교체를 위해 2번째 포켓몬을 선택했다고 생각하게 됩니다. 이제 A 버튼을 누르면 실제로는 2번째 포켓몬이 없지만 버그로 인해 포켓몬이 교체가 됩니다.

CC35는 리셋되지 않고, 2번째 포켓몬이 없지만 버그로 인해 교체가 됩니다.

정밀한 분석을 위해 교체하기 전의 메모리 상태를 살펴보겠습니다. D163에는 지닌 포켓몬의 수, D164에는 첫 번째 포켓몬의 ID가 저장되어 있고, 그다음 주소인 D165에는 지닌 포켓몬 목록의 끝을 알리는 플래그로 FF가 저장되어 있습니다.

지닌 포켓몬 목록의 끝을 알리는 FF가 D165에 저장되어 있습니다

그런데 버그로 인해 포켓몬이 강제로 교체되면서 D164와 D165의 값이 뒤바뀝니다. 이제 목록의 끝을 알리는 플래그가 맨 앞에 있는 상황이 되었습니다.

지닌 포켓몬 목록의 끝이 맨 앞에 와있는 상황

포켓몬 게임에서는 최초 포켓몬을 받은 이후 평상시에 포켓몬은 무조건 1마리 이상은 지니고 있게 됩니다. 그래서 게임이 지닌 포켓몬의 수를 파악하려는 경우, D164가 아닌 D165부터 FF(끝을 알리는 플래그)를 찾으려고 합니다.

그런데 현재 버그로 인해 FF가 D164에 있으므로, 게임은 D16A가 지닌 포켓몬 목록의 끝임에도 불구하고 FF를 만날 때까지 메모리를 계속 읽게 됩니다. 이로 인해 게임은 플레이어가 수백 마리의 포켓몬을 가지고 있다고 착각하게 됩니다.

D16A 뒤에는 HP, 레벨, 상태 이상, 타입 등 지닌 포켓몬의 상세 정보를 저장하는 메모리 영역이 있습니다. 이 영역이 6회 반복되고 (최대 6마리 포켓몬 소지 가능) 그 뒤부터는 포켓몬을 최초 잡은 트레이너의 이름, 포켓몬의 닉네임, 도감 정보, 아이템 정보 등등이 이어지는데, 이 정보가 모두 지닌 포켓몬의 상세 정보로 해석됩니다.

현재 지닌 포켓몬의 정보로 잘못 해석되는 정보들

정보를 잘못 해석하던 도중, 독 상태 이상을 나타내는 포켓몬이 있다고 해석하게 되면 게임 내부 로직에 의해 4 걸음을 걸을 때마다 해당 포켓몬의 HP가 1씩 감소하게 됩니다. 그런데 잘못된 위치에서 잘못 해석한 정보이기 때문에 게임은 포켓몬의 HP를 1 감소시킨다고 생각하지만 실제로는 맵 이동 정보를 담고 있는 주소를 수정하는 경우가 생기게 됩니다. 그러면 걸음 수를 조절해서 맵 이동 정보를 마음대로 바꿀 수 있다는 의미가 되고, 이는 곧 원하는 곳으로 순간이동할 수 있다는 의미가 됩니다.

여담으로, 이 버그를 사용하면 빠르게 명예의 전당으로 순간 이동해서 게임을 3분 이내로 클리어하는 스피드런이 가능합니다. 그리고 이 버그는 대량의 데이터를 잘못 해석하는 만큼 게임 데이터에 치명적일 수 있으니 주의해야 합니다.

기타 신기한 버그

명중률이 100%인 기술은 1/256 확률로 빗나갑니다. 난수로 0 ~ 255(FF)를 생성하는데, 비교를 \(\leq\) 가 아닌 \(\lt\)로 하는 바람에 1/256 확률로 빗나갑니다.

위 미싱노 만나는 방법이나 다른 방법으로 레벨 101 이상의 포켓몬을 잡아서 이상한 사탕(레벨 +1)을 먹이면 레벨이 255까지 올라가는 현상이 있습니다. 이상한 사탕을 먹일 수 있는 조건을 레벨이 100인지 아닌지로 검사하기 때문에 레벨 100만 아니면 레벨이 1 올라가게 됩니다. 추가로, 255인 상태에서 한번 더 먹이면 레벨 0이 됩니다. (레벨이 unsigned integer 이므로 경험치 버그와 유사한 원리) 한편, 전투 중 경험치를 획득하여 레벨업 하면 레벨이 100으로 고정됩니다. 아이템 사용은 꼼꼼히 막지 못했지만, 전투 중 최대 레벨은 100으로 잘 막아둔 것 같습니다.


이상으로 버그가 넘치는 1세대 포켓몬 게임에 대해 알아봤습니다. 적은 메모리라는 제약조건 속에서 프로그래밍하고, 최적화해야 했으니, 개발자들이 정말 많은 고생을 했을 것 같습니다. 적은 메모리 위의 프로그래밍도 어려운데, 게임 특성상 사용자가 어떤 동작을 할지 알 수 없고, 모든 경우를 대비해야 하기 때문에 더 어려웠을 것입니다. 저도 회사에서 개발자로 일하면서 많은 버그를 겪어서, 개발자 선배님들의 마음이 어느 정도 이해가 되는 것 같습니다.

현재는 메모리 기술이 많이 발전했기 때문에 버그가 옛날보다는 덜하지만, 버그가 없는 프로그램은 절대 없기 때문에 최근에도 포켓몬이 복사가 되는 등 치명적인 버그가 종종 발생하기도 합니다. 다만 나아진 점이 있다면 옛날에는 버그가 있는 채로 출시하면 수정할 방법이 없었지만, 현재는 추가 소프트웨어 패치로 버그를 고칠 수 있게 되었습니다.

올해 11월에는 포켓몬 9세대가 출시한다고 하는데, 앞으로 또 어떤 버그가 있을지 매우 기대됩니다!

감사합니다!

참고 자료

댓글