Skip to content

Velog.io bak

LostMyCode edited this page Jan 13, 2024 · 1 revision

온라인 게임을 분석하여 브라우저에 이식해 보았다 | Nothing's impossible

문득 생각이 났다.

PC 온라인 게임의 대명사 'REDSTONE(붉은보석)'을 컴퓨터에 설치하지 않고 누구나 브라우저로 플레이할 수 있다면 얼마나 좋을까?

그렇게 되면 서비스를 종료해도 영원히 즐길 수 있지 않겠는가!

**좋아, 브라우저에서 작동하는 레드스톤을 만들자! **

이렇게 해서 목표가 보이지 않는 역대 최대 규모의 프로젝트 '붉은보석 브라우저 버전 개발 계획'이 시작되었다.

결과물만 보고 싶은 바쁜 분들을 위해

"기사 읽기 귀찮으니까 결과물만 보여줘요!" '라고 생각하시는 분들은 이 영상을 참고하세요.

이게 뭐야? : REDSTONE이라는 온라인 게임을 GoogleChrome 등에서 열기만 하면 실행되도록 만든 '브라우저 이식 버전'의 테스트 플레이 영상입니다.

YouTube: 붉은보석 브라우저 버전 2023년 말 앱데

<iframe width="560" height="315" src="https://www.youtube.com/embed/EDfWIh6I244" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

이 프로젝트의 소스코드는 GitHub에 모두 공개되어 있습니다.

https://github.com/LostMyCode/redstone-js

사이트에 접속하면 브라우저 이식 버전을 시연해 볼 수 있다.

https://rs.sigr.io/


이상, 바쁜 분들을 위한 섹션이었습니다.

자세한 내용이나 개발의 기술적인 이야기에 관심이 없으시다면, 여기서 브라우저를 뒤로 돌려도 괜찮습니다!

전제: 애초에 "붉은보석"이란?

'MapleStory (메이플스토리)'는 예전부터 PC게임을 접한 사람이라면 한 번쯤은 들어본 적이 있을 텐데, 붉은보석도 같은 시기에 시작되어 인기를 끌었던 온라인 게임이다.

정확히 기억은 나지 않지만, 벌써 출시 1주년 정도 된 오래된 온라인 게임입니다.

예상대로 플레이어는 해마다 줄어들고 있고, 예전의 활기찬 분위기는 사라지고 파티 사냥 등은 거의 사라졌다(거의 솔로 플레이로 이뤄진다).

그래픽은 스티커를 붙인 듯한 2D 게임이다.

2D 필드 위를 마우스로 움직여 스킬을 사용해 몬스터를 공격해 경험치를 획득하고 레벨을 올리는 정통 MMORPG.

원하는 것: 브라우저에서 사이트를 열고 REDSTONE을 플레이하고 싶다.

단순히 브라우저를 여는 것만으로 자신이 좋아하는 게임이 돌아가는 모습을 보고 싶을 뿐입니다.

브라우저에서 작동한다는 것은 기본적으로 환경에 구애받지 않고, MacOS든 Windows든, 심지어는 PC가 아닌 스마트폰에서도 작동한다는 뜻입니다.

**윈도우가 설치된 PC에서 설치해야만 플레이할 수 있었던 게임이 사이트만 열면 어떤 환경에서도 작동한다면 감동적이지 않나요? **

아마 "쇼모나!"라고 생각하시는 분들도 많으실 겁니다. '라고 생각하시는 분들도 많을 거라고 생각하지만, 개인적으로는 '야! 멋지다!" 라고 생각하게 되네요!

게다가 이식하면 설치가 필요 없을 뿐만 아니라, 본사가 서비스를 종료해도 영구적으로 무료로 계속 플레이할 수 있습니다.

이렇게 멋진 일이 있을 수 있을까요?

브라우저 포팅의 계기가 된 사건

브라우저에서 구동하고 싶다는 생각은 오래전부터 가지고 있었는데, 어느 해외 포럼에서 REDSTONE의 서버 파일(몇 년 전의 파일)이 밀실하게 유출되고 있다는 것을 알게 된 것이 계기가 되었습니다.

원래 redgem(이었던가?) 라는 REDSTONE의 에뮬 서버가 존재했기 때문에, 공식적인 사람이 아닌 다른 사람이 서버 파일을 가지고 있겠거니 하고 생각했었다. 하지만 지금에야 서버 파일이 공개될 줄은 몰랐다.

에뮬 서버: 에뮬레이터 서버, 어떤 경로를 통해 입수한 온라인 게임 프로그램을 공식 서비스와는 별도로 제3자가 운영하는 서버. 경험치 2배, 아이템 2배 등 원래는 불가능한 설정으로 게임을 플레이할 수 있기 때문에 에뮬서버를 선호하는 플레이어도 적지 않다. 참고로, 보통은 회색(혹은 아웃)

바이너리 파일의 구조를 알 수 있는 기회가 온다

서버 파일이 있다고 해서 별 문제가 되지 않지만, 조사해보니 누군가가 리버스 엔지니어링을 통해 게임 서버를 다시 만들려고 했던 잔여 소스코드도 발견됐다.

기업이 개발, 운영하는 규모의 게임을 리버스 엔지니어링만으로 모두 다시 만든다는 것은 엄청난 작업이기 때문에 결국 그 소스코드도 미완성이었는데, 게임의 맵 데이터가 저장된 바이너리 파일을 불러오는 처리에 대한 설명이 있는 것을 발견했다. 를 발견했다.

image.png

구체적으로는 지면의 텍스처 배치, 오브젝트의 배치와 그 텍스처, NPC의 배치와 역할 등의 정보가 담긴 파일이 있고, 그 파일을 불러오는 작업을 하는 부분이 소스 코드에 적혀있던 거군요.

**기존 온라인 게임의 이식 난이도가 높은 이유는 기존 게임의 바이너리 파일이 각각 어떤 데이터를 가지고 있는지, 또 어떤 데이터 구조로 저장되어 있는지 파악하는 것이 상당히 어렵고 시간이 많이 걸리기 때문입니다. **.

공략 사이트에서 보통 게임을 플레이하면 알 수 없는 자세한 정보를 상세히 설명해 주는 곳이 있는데, 그건 게임의 바이너리 파일을 꾸준히 분석해서 뽑아낸 정보를 보기 좋게 시각화해 놓은 것일 수도 있습니다. (생각해보면 대단하네요)

파일 구조를 알면 장벽이 상당히 낮아진다

온라인 게임의 바이너리 파일 구조는 흔히 볼 수 있는 파일과 달리 독창적인 구조가 많기 때문에 이를 아무것도 모르는 상태에서 이해한다는 것은 상당히 어려운 일이지만, 그 단초를 잡은 소스코드에 적혀 있었기 때문에 난이도가 많이 낮아졌다.

그래도 여전히 난이도가 높긴 하지만요.

자바스크립트로도 바이너리를 읽을 수 있기 때문에 예제를 보고 똑같이 데이터를 읽으면 맵 데이터의 텍스처 정보나 배치 정보를 얻을 수 있습니다.

이를 바탕으로 Canvas에 이미지를 그리면 브라우저에서 게임 맵을 표시할 수 있게 되는 거죠.

원래 브라우저에서 돌아가는 온라인 게임(주로 io 게임이라는 계열의 게임)을 개발, 운영해왔기 때문에, 원 정보만 있으면! 라고 생각하던 차에 이거였기 때문에 할 수 밖에 없었어요 ㅋㅋ

REDSTONE 바이너리 분석의 선구자가 있었다

사실 텍스처 파일(이미지 데이터가 들어있는 파일)의 읽기 처리는 제가 가져온 소스코드에 나와있지 않아 구조를 알 수 없었는데, 다행히도 몇 년 전에 문성준님이 텍스트를 분석해서 텍스처를 브라우저에서 볼 수 있는 뷰어를 만들어서 공개하신 분이 계셨어요.

제작자는 kohu님이라는 분입니다.

브라우저에서 열고 REDSTONE의 텍스처 파일을 드래그앤드롭으로 끌어다 놓기만 하면 바로 볼 수 있는 훌륭한 기능입니다. 몇 년 전에 이런 것을 만들어낸 사람이 있었다니! 너무 대단하다!

더 이상 손대지 않고도 텍스처를 브라우저에서 불러올 수 있는 구조가 거기에 있었기 때문에, 이 부분에 대해서는 솔직하게 빌려오기로 했습니다. (일단 트위터로도 본인에게 DM을 보냈습니다.)


이러한 요소들이 복합적으로 작용하여 마침내 브라우저 버전 REDSTONE의 개발이 시작되었다.

본 프로젝트 기술 선정

**언어: JavaScript **

브라우저에서 구동하는 시점에서는 자바스크립트 한 가지를 선택했다. rust나 c++로 작성해서 wasm으로 만들 수도 있겠지만, 굳이 그렇게 할 필요성을 느끼지 못했기 때문에 포기했다.

TypeScript로 쓰고 싶은 마음도 있지만, 타입 정의 등으로 시간 낭비하는 것보다는 일단 일단 치고 움직이는 것까지만 목표로 하고 싶어서 이번엔 포기했다. (라고 해야 하나, 매번 일단 움직여보자는 생각이 앞서기 때문에 평소처럼 포기.)

기본 구성: webpack5 + babel + webpack-dev-server

js를 작성하면 번들로 묶어서 핫로딩해주는 최소한의 구성이다.

그리기: pixi.js 라이브러리 활용 표준 CanvasAPI를 사용해도 좋았지만, pixi.js라는 라이브러리를 사용하기로 했다.

PixiJS는 웹 브라우저의 canvas 요소에 그리는 크로스 브라우저 대응 경량 자바스크립트 라이브러리로, 자바스크립트에서 GPU를 다루는 WebGL 기술을 2D에 특화하여 쉽게 사용할 수 있다.

webgl은 3D 드로잉용이라는 인식이 강하지만, pixi.js는 2D 드로잉을 webgl로 할 수 있기 때문에, 경우에 따라서는 표준 CanvasAPI보다 더 높은 성능으로 부드럽게 움직이는 것을 만들 수 있습니다.

하지만 그런 부분은 그다지 중요하게 생각하지 않고, 단순히 그림 처리할 때 pixi.js를 사용하는 것이 이해하기도 쉽고, 쉽게 작성할 수 있다는 과거의 경험을 바탕으로 효율을 중시하는 의미에서 채택한 것입니다.

끝없는 리버스 엔지니어링

누군가가 다시 작성한 소스코드도 미완성된 상태였기 때문에, 모든 답이 거기에 있는 것이 아니라 직접 분석해서 새로운 답을 찾아야 했다.

**"아무래도 맵 데이터에는 타일 정보 같은 것이 파일 맨 위부터 ◯◯바이트째에 저장되어 있는 것 같다"**라는 확신할 수 없는 단서.

이미지↓

이렇게 타일 정보로 보이는 바이트열이 있었습니다.

그리고 또 하나, 지면의 타일 텍스처가 많이 저장된 파일이 있다라는 단서. 이미지↓

이미지와 같이 작은 타일이 잔뜩 쌓여있는 파일이 있었습니다.

여기서 가설을 세웁니다.

**"지도 데이터의 타일 정보 같은 부분에 기재된 '숫자'와 '타일 이미지의 번호'가 일치하는 것은 아닐까?" ** "혹시?

이 가설을 검증하기 위해 숫자와 해당 번호의 타일 이미지를 순서대로 나열해봤습니다.

그랬더니...

오! 게임 맵(도시)의 지형이 완성되었네요!

이제 가설이 맞을 것 같다는 것을 알 수 있습니다.


여기서는 성공한 사례만 썼지만, 세운 가설이 맞는 것보다 틀린 경우가 훨씬 더 많습니다.

그래서 이 작업을 하는 것만으로도 하루가 끝날 때도 있습니다.

게다가 가설을 검증하는 것도 힘들기도 하고요!


일련의 흐름

바이너리 에디터로 지도 데이터 일부 재작성하기          ↓ 게임을 열고 해당 맵으로 이동          ↓ 재작성 전과 달라진 부분을 보고, 재작성된 부분이 무엇을 의미하는지 가설을 세운다(예를 들어, 오브젝트의 위치가 바뀌었다면 좌표 정보일 것이다).          ↓ 브라우저에서 같은 지점을 읽고, 오브젝트를 그릴 때 x, y 좌표로 읽은 값을 반영해 본다.          ↓ 그려진 것이 기대한 위치에 있으면 OK, 그렇지 않으면 다시 시도


그 외에도 게임 실행파일(exe)이 어떻게 맵 데이터를 읽어들이는지 알아보기 위해 리버스 엔지니어링 툴을 사용하기도 했지만, 자세한 내용은 생략한다.

솔직히 이 부분은 경험이 거의 없어서 한계가 있어서 커뮤니티에 도움을 요청해 팁이나 리버스 엔지니어링 방법을 알려주기도 했다.

하지만 정말 힘든 작업입니다.

끝이 보이지 않고, 때로는 정답을 얻지 못할 때도 있어서 그렇게 만들다 보니 타협한 부분도 적지 않았어요.

지도 데이터를 바탕으로 Canvas에 그리기

그리고 맵 데이터를 기반으로 타일, 오브젝트, NPC 등을 브라우저에 배치할 수 있었습니다.

맵의 그래픽은 거의 대부분 이식할 수 있게 된 것이다.

여기에 플레이어를 배치하고 맵을 돌아다닐 수 있게 되었을 때 나온 영상이 아래 영상입니다.

https://youtu.be/SgPJ1gGHntc?si=7vjMSra1Jt-7RHCj

솔직히 맵을 달릴 수 있을 뿐이라 게임성은 전혀 없습니다. 그래도 여기까지 도달할 수 있었다는 것이 기뻤기 때문에 하나의 분기점으로 삼았습니다.

pixi.js 라이브러리를 이용해 그림을 그렸는데, 도움이 된 건 AnimatedSprite였어요.

레드스톤 맵의 오브젝트 중에는 분수 등 애니메이션이 반복되는 오브젝트가 있습니다.

이를 브라우저 드로잉으로 재현할 때 AnimatedSprite가 큰 도움이 되었습니다.

// https://pixijs.download/dev/docs/PIXI.AnimatedSprite.html

import { AnimatedSprite, Texture } from 'pixi.js';

const alienImages = [
    'image_sequence_01.png',
    'image_sequence_02.png',
    'image_sequence_03.png',
    'image_sequence_04.png',
];
const textureArray = [];

for (let i = 0; i < 4; i++)
{
    const texture = Texture.from(alienImages[i]);
    textureArray.push(texture);
}

const animatedSprite = new AnimatedSprite(textureArray);

이렇게 이미지별로 만든 여러 개의 PIXI.Texture를 배열로 전달해 주면 알아서 애니메이션을 만들어 주는 스프라이트가 만들어집니다.

속도만 지정해 주면 자동으로 프레임이 갱신되기 때문에 매우 편리합니다.

드디어 스킬을 사용하여 몬스터를 사냥할 수 있게 된 연말 압데

이번 업데이트로 드디어 브라우저 이식 버전에서도 맵에 있는 몬스터와 전투를 할 수 있게 되었습니다.

https://youtu.be/EDfWIh6I244

드디어 게임성이 생겼어요 (아직은 부족하지만)

레드스톤 본가와는 달리 브라우저 버전은 조정이 자유자재로 가능하기 때문에, 영상에서처럼 치트급으로 할 수 있게 되었습니다.

이렇게까지 화려한 수정은 에뮬 서버에서도 볼 수 없는 풍경이겠죠?

https://github.com/LostMyCode/redstone-js

바이너리 파일 구조체 읽기에서ハマったこと

이번에는 플레이어가 스킬을 사용하여 몬스터를 공격하는 구현을 위해 스킬 데이터가 저장된 바이너리 파일을 읽어들이는 처리를 추가했습니다.

간단히 말해서, 이 파일에는 스킬의 개수와 그 개수에 해당하는 스킬 구조체가 저장되어 있습니다.

구조체는 아래와 같은 이미지입니다.

struct __cppobj CSkillDefine
{
  unsigned __int16 m_wSerial;                                              // offset: 0000, size: 0002
  unsigned __int16 m_wIconIndex;                                           // offset: 0002, size: 0002
  unsigned __int16 m_wType;                                                // offset: 0004, size: 0002
  unsigned __int16 m_wAction;                                              // offset: 0006, size: 0002
  unsigned __int16 m_wAction2;                                             // offset: 0008, size: 0002
  unsigned __int16 m_wOverlapAction;                                       // offset: 000A, size: 0002
  unsigned __int16 m_wOverlapAction2;                                      // offset: 000C, size: 0002
  unsigned __int16 m_wReiterationDamageCountSyncWithOverlapAction;         // offset: 000E, size: 0002
  unsigned __int16 m_wEnableJob;                                           // offset: 0010, size: 0002
  unsigned __int16 m_wSpeed;                                               // offset: 0012, size: 0002
  ...

참고로 이 구조체는 리버스 엔지니어링 툴을 이용하여 추출한 것이다.

가장 첫 번째 멤버 m_wSerial은 타입이 unsigned int16이므로 2bytes임을 알 수 있습니다.

따라서 바이너리 파일의 구조체 시작 위치에서 2bytes를 읽으면 그것이 m_wSerial의 값이 됩니다. 그리고 다음 m_wIconIndexunsigned int16로 2bytes이므로 시작 위치에서 2bytes 진전된 위치에서 2bytes를 읽으면 m_wIconIndex의 값을 얻을 수 있습니다.

형식대로 순서대로 읽으면 되겠구나! 라고 생각했는데, 중간중간 읽은 값이 맞지 않는 것을 발견했다.

어라? 위에서부터 순서대로 정확하게 읽었는데...?

여기서 꽤 시간을 낭비했는데, REDSTONE이 원래 C++로 개발된 것이고, C++에서는 구조체를 다룰 때 정렬이라는 것을 의식해야 하는 것 같습니다.

예를 들어 이 예제에서 구조체의 멤버는 char, int, char, short이므로 구조체의 크기는 char(1byte) + int(4bytes) + char(1byte) + short(2bytes)로 8bytes일 것이라고 생각했는데, 실제로는 12bytes입니다.

데이터가 빈틈없이 빽빽하게 들어차 있는 줄 알았는데, 메모리 공간 상에서는 최적화를 위해 적절히 빈 공간(패딩)이 만들어져 있습니다.

파일에 저장된 구조체 데이터도 메모리에 복사만 하면 각 멤버에 값을 할당할 수 있도록 동일하게 간격을 유지한 채로 저장되어 있다고 생각됩니다.

평소 자바스크립트만 만지다 보면 이런 것들을 의식할 기회가 거의 없어서 여기서 처음 알게 되었습니다.

그래서 'char 타입을 읽었으니까 1byte 앞으로 이동해서 다음 값을 읽으면 되겠지! 라는 생각으로 순서대로 읽으면 막히는 것입니다.

아래 코드는 자바스크립트에서 구조체 읽기 처리의 일부입니다. 이렇게 패딩이 들어가는 부분이 있기 때문에 그 부분은 건너뛰는 식으로 작성해야 합니다.

        this.dodgeAngle = br.readUInt16LE();
        this.hitAngleRange = br.readUInt16LE();
        this.hitAngleRangePerLevel = br.readUInt16LE();
        this.dodgeDistance = br.readUInt16LE();
        this.paletteIndex = br.readUInt16LE();

        br.offset += 2; // padding

        this.enchantedEffectMask = br.readUInt32LE();
        this.enchantedImage = br.readUInt16LE();
        this.dustImageRange = br.readUInt16LE();

자바스크립트에서는 보통 이런 어려운 것을 의식하지 않아도 되는 만큼 반대로 힘들었습니다. (C++이라면 구조체 만들어서 거기에 파일에 저장된 데이터만 넣으면 되는데, JS는 하나하나 읽어들여서 패딩이 들어있는 부분도 생각해야 하기 때문에)

어려울 것 같아서 전부 JS로 썼는데, 차라리 C++로 작성하고 Emscripten에서 WebAssembly로 컴파일하는 것이 더 쉬울 수도 있지 않을까? 아직 최적의 해답을 찾지 못했습니다.

치명적: 애초에 과소평가된 게임이라 외면당했다.

지금까지 여러 가지 어려움 속에서도 REDSTONE의 브라우저 이식 프로젝트를 진행해왔다. 하지만 안타깝게도 본사가 출시된 지 1년 정도 지나서 과疎(소멸)이 진행되고 있는 게임이기 때문에 브라우저에서 플레이할 수 있게 되어도 외면당하고 있는 실정입니다.

** 슬프네요! **

그래도 제가 배운 것으로 여기까지 할 수 있다는 것을 알게 됐고, 도전하는 것이 즐겁습니다.

다음에는 좀 더 HOT한 분야에서 뭔가 재미있는 일을 해보고 싶어요.

마무리

지도 그리기뿐만 아니라 스킬과 사냥까지 구현할 수 있었어요.

즉, 불가능은 없다!

이상입니다.

무료 온라인 게임 '레드스톤'을 해본 적이 있는 분들은 이번 기회에 브

브라우저 버전: https://rs.sigr.io/

소스코드를 모두 공개하고 있습니다.

GitHub: https://github.com/LostMyCode/redstone-js

**향후 전개 (실제 구현 여부는 미정) **

  • 다른 캐릭터(직업) 구현
  • 사용 가능한 스킬 증가
  • 온라인화
  • 장비
  • 플레이어 상태, HP, CP

이미지 출처: Microcontroller Embedded C Programming Lecture 149| Calculating structure size manually with and without padding

Structures in C: From Basics to Memory Alignment

Clone this wiki locally