게임은 일종의 랜덤의 연속적인 놀이입니다.
디아블로3 같은 게임을 생각해 봅시다.
이 게임의 줄거리는 악마를 잡는 게임입니다.
게임 진행도 악마를 죽이고, 그 악마가 괜찮은 장비를 들고 있다면, 그걸 주워다가 착용해서 좀 더 강해지고, 강해지면 더 강한 악마를 처치하는 드래곤볼 시스템을 탑재 하고 있습니다.

 

오른쪽 스샷은 2014년 9월 1일 기준으로 필자가 정복자 레벨 (최고 레벨 = 70 이후로 다시 1부터 시작하는 레벨) 277이 되도록 하나밖에 못 먹은 아이템입니다.
자랑은 아니고, 제가 죽인 몬스터들의 숫자를 보도록 하겠습니다.

 

친절하게 디아블로3은 이런 것도 카운팅이 되고 있어요. 디아블로3은 몬스터 죽일 때마다 캐릭터 객체 어딘가에서 카운팅이 된다는 거겠죠. 위의 창에 나타난 정보로는 제가 정예 24665마리를 처치했고 일반 몬스터는 642560마리를 잡았다고 기록되어 있습니다. 

 

 

앞서 말씀드렸듯이 디아블로3은 무수히 많은 아이템이 있고, 몬스터를 죽일 때마다 굉장히 낮은 확률로 높은 아이템을 떨어뜨립니다. 그럼 이 전설 아이템이 드랍되는 프로그램을 어떻게 작성하면 좋을까요? 간단히 생각한다면 아래와 같이 작성할 수 있지 않을까요?

 

이렇게?

#include <cstdlib>

#include <ctime>

#define ITEM_ID_MAX     (1000000)   //아이템 최대 ID 번호

 

 ... //랜덤 아이템 ID를 생성

int randomDropItem()

{

     return (rand() % ITEM_ID_MAX) + ITEM_ID_START;

}

 

//랜덤 시드 생성

srand((unsigned int)time(NULL));

 

//드랍 아이템 생성

Item *item = new Item(randomDropItem());

 

랜덤 함수를 써서 적절히 처리했습니다만, 이러면 정말 될까요?
혹시 rand 함수가 어느 숫자까지 랜덤으로 뽑을 수 있는지 알고 계시나요?

 

레퍼런스 사이트인 cplusplus.com나 MSDN에서 함수 정의를 보도록 하죠 .


음... 리턴 값으로 0 ~ RAND_MAX까지
의사 난수를 준다고 하네요.

RAND_MAX…… 0x7FFF 로 정의되었다고 합니다.
계산기로 보니 0x7FFF, 2byte short의 최댓값이네요.

 

 

16bit 컴퓨터 당시의 잔재로 생각됩니다만, 왜 하필이면 0~0x7FF로 제한을 두었을까요?
C++의 rand 함수는 아래의 레머(Lehmer)가 고안한 알고리즘을 활용합니다.

 

f(x) = (A * f(x-1) + C ) mod M

즉, 계산을 통한 난수를 만듭니다. 이를 pseudo-random number, 의사 난수라 합니다.
의사 난수 공식에 사용되는 A, C, M은 정수이며, A가 8로 나눌 때 나머지가 5인 수, C 가 홀수 이면, 0 ~ (M-1)까지의 정수가 M 주기로 한 번씩 나타나는 성질을 이용하죠.

 
이제 무엇이 문제인지 눈치를 채셨나요?

 

아까 디아블로3의 마법사의 로망 “워의 마법봉” 드랍 확률을 1/1000이라고 합시다. 
그리고 위의 함수 처리로 아이템을 드랍시킨다면,
int itemRatio = rand() % 1000; if (itemRatio == 987) return “워의 마법봉”; 으로 작성되겠죠.


여기서 레퍼런스 랜덤 함수 처리, 0~32767 에서 나오는 숫자 중 %1000을 하게 되면 확률적으로 0~767 id가 33번 나올 때, 768~999까지 32번 나오겠네요.

 

이거 게임 밸런스가 미묘하게 틀어지기 시작하는군요.
현실적으로 기획자가 이 아이템은 십만 분의 1 확률로 나오도록 처리하도록 해봅시다.
코드 상으로는 아마 int itemRatio = rand() % 100000; 이 되겠군요.
그럼, 과연 32768 ~ 99999의 숫자가 나올까요? 위 id의 게임 아이템은 절대 얻을 수 없겠죠.
또, rand는 시드 값 위 식에서 f(0) 값으로 srand 함수를 사용하여 지금 시각을 주도록 코딩하는데, 이 srand 시드 값은 프로그램 전체에 영향을 미칩니다.

 

그런데 rand의 난수 범위가 short 최대치 인지라, 이 랜덤 순열은 1주 ~ 2, 3주 러닝 타임을 가진 게임 서버 프로그램에서 난수 순환이 빨리 오겠죠.
결국, 이런 여러 문제로 기본 C언어 (POSIX C이라고 합니다)의 rand 함수는 게임 프로그램에 사용하는 것은 위험한 요소가 많습니다.

 

그럼 어떻게 하면 좋을까요?

수학자들은 이미 해결을 했습니다.
보통 메르센 트위스터 랜덤(Mersenne Twister random)이라고 해서 RandomMT라고 표현하기도 합니다.

(MT는 Multi Thread 약자가 아닙니다) 난수의 반복주기가 메르센 소수(Mp = 2p - 1 )라 그 품질이 좋고,

이 주기가 2의 19937 제곱 -1이므로 위의 예와 같은 rand() % 1000으로 몇몇 숫자 범위 대에 확률이 낮아지지 않습니다. (오차 범위에 수렴하죠).

게다가 rand 함수보다 빠르기도 합니다. (비트연산 코드입니다) 실제 구현 소스는 아래 사이트에서 열람할 수 있습니다.

 

 http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html


C++0x11 이후 버전에서는 이 함수를 아예 표준 라이브러리에 포함 시켰습니다.
이를 활용한 클래스는 아래와 같습니다.

 

 RandomMt.h

#pragma once

#include "stdafx.h"

 

#define RAND(type, maxVal)       (type) RandomMT::getInstance().rand(maxVal)

 

class RandomMT : public Singleton<RandomMT>

{

public:

    uint64_t rand(int maxVal)

    {

        //MT19937 난수 엔진

        std::mt19937 engine((uint32_t)time(nullptr)
 + (uint32_t)std::this_thread::get_id().hash());

       

        std::uniform_int_distribution<uint64_t> distribution(0, UINT64_MAX);

        //rand 생성 함수포인터 bind

        auto generator = bind(distribution, engine);

 

        return (uint64_t)(generator() % maxVal);

    }

};

 

잘 보시면 엔진 초기화 할 때, srand 처럼 현재 시각을 넣는데 거기에 현 쓰레드의 hash 번호를 더해서 넣어 주었습니다. 각각 쓰레드 마다 다른 값을 줘야 하기 때문이죠.
아마 현업에서는 이 코드를 직접 구현한 곳이 많을 겁니다.

 

아니면 이것을 또 개선한 RandomWell을 쓸 겁니다.
여기서는 메르센 트위스터 19937을 사용했지만, 심화로 Random Well 소스를 위의 사이트에서 구해서 작성해 보시는 것도 좋은 경험이라 생각합니다. (생각보다 몇 줄 안 합니다)  

 

위 내용은 제가 집필한 "게임 서버 프로그래밍 입문" 책의 내용중 일부 부분에 대한 내용입니다.

전체 소스 코드와 책 구입에 대해서는 http://rosagigantea.tistory.com/589 에 링크 시켰습니다.

서버 프로그램은 복잡한 시스템이라 많은 로직들이 산재 하고 있습니다.
하지만, 이들 로직을 정리하고 공용화 한다면 라이브러리로 모을 수 있을 것입니다.

 

여기 라이브러리는 이런 도움되는 간단한 로직 처리들이라고 보시면 됩니다.

드라이버, 니퍼, 펜치, 몽키 스페어나 크레인 없이 철, 고무, 유리만 떨렁 주고 자동차를 만들라고 하면 만들 수 있을까요?
프로그램도 마찬가지 입니다. 거대한 시스템을 구축하기 위해서는 그 시스템에 필요한 기초 로직들이 필요합니다.

 

보통 Util 이라고 칭하거나, [회사이름]Lib  혹은 [특허명]라이브러리 같은 식으로 사용합니다.
음…… 그런데 무엇부터 만들어야 할까요?
이럴 땐 이전에 말씀드렸던 것처럼, 마인드맵으로 생각을 정리해 봅시다.
전 대략 아래와 같이 생각나서 아래의 것만 여기서 설명해 드리려 합니다.
만약 빠진 것이 있다면 여기서 더 추가하셔서 구현시켜 놓는 편이 좋을 거 같네요.

 

위 내용은 제가 집필한 "게임 서버 프로그래밍 입문" 책의 내용중 일부 부분에 대한 내용입니다.

전체 소스 코드와 책 구입에 대해서는 http://rosagigantea.tistory.com/589 에 링크 시켰습니다.

 

제가 작성한 책의 코드들을 보시기 전에 아래의 규칙에 의해 책의 소스가 작성되어 있으니 미리 읽어 두셔서 코드 보시는 데 도움이 되셨으면 합니다.
비단 이 책뿐만 아니라 현업에서도 많이 통용되는 규칙이니 소개해 드리려고 합니다.

 

 1

SVN에 소스를 커밋(업 로딩)은 반드시 컴파일 성공한 결과만 넣어야 합니다.
현업에서는 자신의 작업을 올릴 때 다른 팀원의 방해를 주면 안되므로 당연히 컴파일에 문제없는 파일을 올려야 합니다.

 

 2

소스 커밋 시 최대한 로그를 자세히 달아놓아야 합니다.
무엇을 수정했는지, 버그는 이랬고, 이런 문제로 이렇게 수정했다 등, 이후 새로운 버그 발생시 해결할 실마리를 줍니다.

 

 3

코드 주석
주석은 될 수 있으면 쓰지 않습니다.
코드 수정 시, 매번 주석도 갱신해야 하는데 이런 관리문제도 있으므로 나중에는 부작용이 더 커집니다.

오히려 코드 자체만으로 주석을 쓰듯 나타내는 코드가 좋은 코드라고 생각합니다.
단 꼭 필요한 경우에는 사용해야 하며, 여기서는 “읽기 좋은 코드가 좋은 코드다.” 책에서 소개된 내용과 같이 주석을 작성할 것입니다.

//TODO: 추가로 코드 작업을 해야 할 때 작성
//FIXME: 뭔지 모르겠지만, 이 코드에 문제가 있다. 고쳐야 한다.
//HACK: 문제 해결로 작성하다 보니 코드가 엉켜 있음 (아름답지 않은 해결)
//XXX: 주의! 여기 건드리면 큰일 남!

 

 4

이 소스에는 컨벤션이 있습니다. 컨벤션을 맞춰서 작성되었으니 참고 바랍니다.

 

 1. If 띄고 (를 쓰고 {는 그 행에 사용한다.
 If ( a = b) {
        printf(“…”);
    }

 

2. switch case에서 case 는 switch 와 들여쓰기를 맞춰야 합니다.
 switch (temp) {
    case AAA:
        printf(“…”);

 

3. 클래스 맴버 변수는 마지막에 _를 사용합니다.
class TestClass
{
     int index_;
};

 

4. 문장은 낙타(camelCasing)을 주로 사용하며, 컴퓨터에 의해 생성되는 소스는 스네이크(snake_value)표기 법을 사용합니다.
익숙하신 헝가리안 표기법(pValue)은 사용하지 않습니다.

 

5. 카멜 캐스팅이므로 변수 중간에 _를 붙이지 않습니다.
단 맴버 변수만 뒤에 _를 붙입니다.
 bool active_;      //클래스 맴버변수 active_
 bool active;       //함수 안에서만 쓰는 변수 active

 

6. 클래스는 반드시 대문자로 시작하고,
변수는 반드시 소문자로 시작합니다.
Enum, define등은 모두 대문자로 표기합니다.

 

 7. enum 값은 앞에 E를 붙입니다.

 

 8. 클래스 내부 함수를 같은 클래스 안에서 쓸 때는 this->를 반드시 명시합니다.
 Class A {
    Int index_;
    void func();
    Void too() {
       Index_ = x;
       this->func();
}

 

 

여기서 보이는 소스의 “if (a = b) {“ 와 같이 {를 내리지 않고 쓰는 스타일을 Egyptian Brackets이라고 불립니다. K&R 스타일인데, 제가 거쳐 왔던 회사가 모두 저런 스타일로 코드를 작성해서 제가 익숙하다 보니 그렇게 작성하였습니다.

 (책 분량 조절도 해야 하고요)


 

또 많이들 클래스 맴버 변수에 m_ 나, 포인터라는걸 알리는 p 같은 헝가리안 표기법을 사용하시겠지만, 요즘 IDE에서는 변수의 타입을 바로 다른 창이나 툴 팁에 띄울 수 있고, 이러한 표기법은 가끔 코드 리더빌리티(code readability)를 떨어트릴 수 있어서 사용하지 않는 곳도 많습니다.

아래는 이와 관련 토론한 게시글인데, 한 번은 읽어 보시길 추천합니다.
 https://kldp.org/node/46915

 

표기법은 다른 책들에서 자주 나오는 헝가리안 표기법(pValue)이 대세를 이루기도 하지만,
여기서는 카멜 표기법(valuePointer), 생성된 코드는 스네이크 표기법(value_pointer)을 사용합니다.
익숙하지 않을 것 같다고 생각은 듭니다만, 회사에서는 회사 코드 스타일로 소스를 작성해야 다른 작업 하는 사람들도 헷갈리지 않으므로 미리 연습한다고 생각하시면 좋을 것 같습니다.

 

회사에서 이렇게 규칙을 정하는 이유는 코드 유지 보수 차원상 팀의 개개인의 제각각의 스타일을 통일하여 비상시 누구라도 다른 사람 feedback을 해주는 시스템을 갖추기 위함입니다.
뉴스에서 표준어를 사용하는 것과 같은 거로 생각해 주시면 좋을 것 같습니다.

 

위 내용은 제가 집필한 "게임 서버 프로그래밍 입문" 책의 내용중 일부 부분에 대한 내용입니다.

전체 소스 코드와 책 구입에 대해서는 http://rosagigantea.tistory.com/589 에 링크 시켰습니다.

 

입문 책이므로  접근성도 좋고, 비교적 쉽게 개발할 수 있는 Windows에서 개발을 진행하도록 하겠습니다.

윈도우 개발 툴이라면, 역시 Visual Studio를 빼놓을 수 없겠네요.
게다가 최근에는 거의 무료로 풀어버린 Visual Studio Community 2013이 있으니 이 버젼으로 진행하도록 하겠습니다.
물론 이클립스나 다른 툴을 사용해도 컴파일만 되면 큰 문제는 없습니다.

저는 서브 에디터기로 노트패드 플러스를 사용하고 있습니다.

 

 IDE 컴파일러 – Visual Studio Community 2013

 http://www.visualstudio.com/ko-kr/downloads/download-visual-studio-vs#DownloadFamilies_2

 

 

 

다음은 소스 관리 툴입니다.

SVN은 범용적인 소스 관리 툴로서 이전 작업과 현재 작업 비교등
프로그램 작성에 있어서 undo를 마음껏 해줄 수 있는 툴 입니다.

저도 이 서버 프로그램 작성에 svn을 이용했으며, 최종 리비젼은 130 으로 나오네요. 

 http://tortoisesvn.net/

 

 

 

다음은 UML툴인 StarUML입니다.
서버 프로그램처럼 복잡한 시스템들이 얽힌 프로그램은 구조를 제대로 작성해 놔야
나중에 더 복잡한 처리, 응용등이 안정성있게 동작하게 됩니다.

이를 시각적으로 구현 시켜주는 툴로서 StarUML을 사용합니다.

 

 http://staruml.io

 

 

마지막으로 원노트 입니다.

프로그램이 크다 보니까 이것 저것 작업 공유를 남길 필요가 있습니다.
DB서버 IP는 무엇이다, 암호는 무엇이다, 어떤 작업을 할때 주의점등...

이런 여러 자료를 문서/캡쳐로 공유 가능하게 해주는 솔루션을 많이 사용하는데,

가정에서는 흔히 접할 수 있는것이 원노트정도로 보시면 좋을 것 같네요.

 

 원노트

 

 

 

 

위 내용은 제가 집필한 "게임 서버 프로그래밍 입문" 책의 내용중 일부 부분에 대한 내용입니다.

전체 소스 코드와 책 구입에 대해서는 http://rosagigantea.tistory.com/589 에 링크 시켰습니다.

 

온라인 게임 서버가 어떤 프로그램인지 먼저 생각해 봅시다.

 

 

 블리자드의 디아블로3

넥슨GT의 테일즈 위버 

보통 온라인 게임 하면 위와 같이 다수와 접속해서 즐기는 MMORPG 온라인 게임도 있을 것이고,  디아블로3처럼 특정 몇 명만 같이 즐기는게임도 있습니다.

하지만 겉으로 보이는 클라이언트로는 서버 내부에서 어떤처리를 하는지 잘 알 수 없죠.
클라이언트 패킷을 받고, 내부 메모리를 조작하여, 반영내용을 DB에 넣고, 결과 패킷을 돌려주는 일련의 행위가
게임 서버 프로그램이죠.
이런 프로그램들을 만들고 관리하는 서버 프로그래머들은 평소에 어떤 일을 할까요?


만약 신규 게임 프로젝트에 배속 되었다면 당연히 게임 서버를 만들겠죠?
기획자들이 이것저것 새로운 게임에 대한 콘텐츠 기획을 제안할 것이고 그것에 맞게 프로그램 구조를 고려해 서버 프로그램을 작성할 것입니다.

보통 신규 게임이면 지금 서비스하고 있는 타 온라인 게임과 최대한 차별화를 두거나,
많은 부분 차용해서 만들 테니(WOW, 디아, 리니지, 던파, 서든 등) 그 게임을 열심히 해보고
시스템 설계를 할 것입니다.

 

반대로, 이미 서비스 중인 게임의 유지보수를 하는 서버 프로그래머라면 어떤 일을 할까요?
이 유지보수를 할 정도이면 성공한 온라인 게임이고, 고정 마니아 층이 있어서, 매일 매일 일정 수익이 나옵니다.

대부분 유저 레벨이 높은 레벨에 포진하기 때문에 신규 유저 보다는 기존 유저들이 질려서 나가는 비율이 높습니다. 즉 CCU(Concurrent connected User)가 꾸준히 떨어지는데, 이들을 잡기위한 콘텐츠 추가 (유행하는 LOL과 같은 게임 넣기라던가, 이벤트, 신규 케릭 추가등...), 시스템의 부분 개편 등을 주로 작업하며, 기획자들이 원하는 콘텐츠를 추가하기 위한 기능 추가 작업이 대부분입니다.

프로그래머 입장에서는 기존 코드 위에 개선작업을 해야 하므로 기존 코드를 분석하는 능력과 리펙토링(코드 구조를 좀 더 효율적으로나 보기 좋게 개선)을 할 줄 아는 능력이 요구됩니다.

 

제 책을 읽으시는 분께서 게임 회사에 입사하셔서 처음 맡는 프로젝트가 신규 개발팀일지, 라이브 유지 팀일지는 잘 모르겠지만, 결국 어느 쪽 길을 선택하든 프로그램 개발을 해야 하고 녹녹하지 않은 길입니다.

신규 게임의 경우 구현 할 수 있는 모든 가능성을 염두 해서 일정 내에 작성해야 할 것이고, 라이브 팀의 경우 기존의 엔진을 요구사항에 맞도록 리펙토링 할 수 있는 능력이 필요합니다.

물론 공통으로 게임 서버가 어떻게 돌아가는지 내부 움직임을 잘 알고 있어야 서버 프로그래머로서 원활하게 일 할 수 있겠죠?

 

그런 의미로 제 책에서는 서문에 말씀드렸던 cubesat같은 간단한 온라인 서버 코어를 만들어보고 이들 시스템이 어떻게 유기적으로 연결되고 데이터 처리를 하는지 기술하려고 합니다.

물론 여기 책은 게임 서버의 아주 극단적인 작은 부분입니다. 이것으로 게임 서버 만들 줄 압니다. 하기에는 턱없이 부족합니다만, 바퀴와 자체의 어렴풋한 모습이라도 알아야 자동차 비슷한 것이라도 만들 수 있다고 생각합니다.

겨우 이정도 인가? 라기 보다는 여기에 어떠한 철학으로 프로그램 코딩을 했는지 찬찬히 살펴봐 주셨으면 합니다.


<필자가 이 책을 기술하면서 작성한 프로그램 입니다>

 

 

위 내용은 제가 집필한 "게임 서버 프로그래밍 입문" 책의 내용중 일부 부분에 대한 내용입니다.

전체 소스 코드와 책 구입에 대해서는 http://rosagigantea.tistory.com/589 에 링크 시켰습니다.

+ Recent posts