보통 게임 서버에서는 클라이언트 데이터를 직접 저장 하지 않고 DataBase라는 프로그램에 데이터를 저장 합니다.

 

그런 의미에서 저도 DB에 데이터를 저장하고 로드 하는 방법을 알려드리려 합니다.

우선 무료로 설치 할 수 있는 MS-SQL Express 2014를 설치해 봅시다.  (가능하면 다른 PC나 가상PC에서 설치하세요)


 http://msdn.microsoft.com/ko-kr/evalcenter/dn434042.aspx
 

 

설치하면 중간에 기능 선택 기능이 있는데 관리 도구는 반드시 체크합시다.
그리고 인스턴스 루트 디렉터리는 DB가 IO 문제가 민감해서 웬만하면 OS가 설치 안된 다른 하드 디스크를 선택하는 것이 좋습니다.


 

특별히 설치하는 데는 큰 어려움이 없다고 생각합니다.
가끔 설치했는데, Management Studio가 없는 경우도 있는데, 이는 설치 파일을 잘 못 받은 것입니다. 설치 파일을 확인하시고 다시 설치하시기 바랍니다.
여기까지 설치되면 DB를 사용할 준비는 완료된 것입니다.


 

 

그럼 여기에 데이터를 어떻게 넣고 조회는 어떻게 할까요?

데이터베이스에는 이런 데이터를 조회하고 넣는등의 스크립트들을 정립해서 표준화 시켰습니다.

이를 SQL이라고 하죠.

게임 서버에서 자주 사용되는 SQL문법은 아래와 같습니다.

 

1. Use
Database를 선택할 때 사용합니다.
ex: > USE [데이터 베이스 이름];


2. GO
쿼리 문 실행을 끝냄을 의미합니다. 즉 GO가 나오기 전까지는 일종의 {}에 묶여 있는 형태로 보시면 좋을 것 같습니다.


3. Create
DB, 테이블, 프로시저, 트리거등을 만드는 문입니다.
ex: > CREATE TABLE [테이블 명] (칼럼 명, 데이터 타입 및 제약 조건 ……)


4. Alter
DB, 테이블, 프로시저 등을 수정할 때 사용합니다.
ex: > ALTER PROC [프로세서 이름]


5. Insert
레코드를 테이블에 넣을 때 사용합니다.
ex: > INSERT INTO [테이블 명] (칼럼1, 칼럼2, ……) VALUES(값1, 값2……)


6. Select
테이블 검색에 사용합니다.
ex: > SELECT * FROM [테이블] WHERE [조건식] ORDER BY [정렬 방법]
ex2: > SELECT TOP 10 FROM [테이블]            -- 테이블 항중 10개만 출력
ex3: > SELECT DISTINCT [칼럼A] FROM [테이블]   -- 칼럼A의 중복 값 제거한 레코드 출력


7. Update
레코드 수정에 사용합니다.
ex: > UPDATE [테이블] SET [칼럼] = [값], …… WHERE [조건식]


8. Delete
레코드를 삭제 합니다.
ex: > DELETE [테이블] WHERE [조건식]


9. Truncate
레코드를 비웁니다. Delete와 다른 점은 트렌젝션 로그를 남기지 않습니다.
즉, DELETE [테이블A]; ROLLBACK 하면 롤백이 가능합니다만,
TRUNCATE TABLE [테이블A]; ROLLBACK 하면 롤백이 불가능합니다.


10. Drop
테이블, 프로시저, DB를 완전히 삭제 합니다.
DELETE, TRUNCATE 는 데이터를 삭제하는데, Drop은 객체 자체를 지웁니다.
ex: > DROP TABLE [테이블A]
    > DROP DB [DB이름]
    > DROP PROC [프로시저 이름]


11. Join
테이블과 테이블을 어느 조건에 맞춰 합치게 합니다.
예를 들어 아래와 같은 것을 생각할 수 있습니다.


<User Table>
userOid name job level money
1 롱스틱 마법사 280 10034
2 사과크림 전사 100 592
3 포도크림 소환사 39 110
… … … … …
<Item Table>
itemOid name lv userOid
1 절대반지 50 3
2 엑스칼리버 120 2
3 파멸의 손짓 240 1
… … … …
여기서 userOid로 묶을 수 있으니까 아래와 같이 입력하면
> SELECT * FROM UserTable AS A1
  INNER JOIN ItemTable AS A2.
  ON A1.userOid = A2.userOid

 


userOid name job level money itemOid name lv
1 롱스틱 마법사 280 10034 3 파멸의 손짓 240
2 사과크림 전사 100 592 2 엑스칼리버 120
3 포도크림 소환사 39 110 1 절대반지 50


이런 식으로 조합이 가능합니다.
그리고 눈치채셨겠지만 Oid를 기점으로 데이터를 합칠 수 있으니 Oid가 아닌 것들은 데이터 수정에서 자유로울 수 있는 장점이 있습니다. (캐릭터 이름 변경이라던가……)|
위에서는 교집합인 Inner의 예가 빈번히 사용돼서 설명 드렸고, 그 외의 합집합인 Outer join 문은 물론 Cross Apply도 살펴보시길 바랍니다.


12. Sub Query
일종의 Select 문 안에 Select 를 사용하는 방법입니다.
ex: > SELECT * FROM [테이블A] WHERE [칼럼]
       = (SELECT * FROM [테이블] WHERE [칼럼] = ‘조건’)
조금 변형하면 이런 것들도 가능합니다.


> SELECT [칼럼1], [칼럼2] INTO [테이블B] FROM [테이블A] WHERE [칼럼] = ‘조건’
  -> 테이블A의 조건을 만족하는 칼럼1, 칼럼2의 테이블 B를 생성합니다.
> UPDATE A SET A.[칼럼] = B.[칼럼] + x
 FROM [테이블A] AS A JOIN [테이블B] AS B ON A.[칼럼] = B.[칼럼] WHERE [칼럼] = ‘조건
 -> 특정 A 테이블 중, A와 B의 join한 테이블의 특정 조건의 레코드의 항목만 갱신


13. Set
변수를 설정하는 명령어 입니다.
ex: > DECLARE @tmp1 AS INT
      SET @tmp1 = 1000


14. Print
메시지를 출력합니다. 굉장히 길고 처리 시간이 오래 걸리는 쿼리 문 작성할 때, 중간에 Print 메시지를 넣어서 어디까지 실행 되었는지 파악할 때 사용합니다.
ex: > PRINT ‘쿼리 A까지 완료’


15. PROCEDURE
위의 기본 쿼리 문 (SELECT, INSERT, DELETE, UPDATE) 하나만으로 데이터 넣는 처리를 하는데 어느 정도 한계가 있습니다. 그래서 이 코드들을 하나의 함수처럼 선언 할 수 있는데, 이를 프로시저라고 합니다.
프로시저를 선언하게 되면, 해당 코드들을 MSSQL내부에서 미리 컴파일 해 가지고 있기 때문에 실행 속도가 훨씬 빠릅니다.
그리고 응용단계로 프로시저 안에서 프로시저를 생성해서 실행 할 수 있는데, 이를 동적 프로시저(Dynamic)라고 합니다.


ex: > CREATE PROC [프로시저 명] [@파라 미터], ……
      AS BEGIN
         SELECT * FROM … 쿼리 문
      END


16. BEGIN TRANSACTION / COMMIT TRANSACTION / ROLLBACK TRANSACTION
트렌젝션은 어느 업무 처리 단계를 두는 행위로, 데이터에 어떤 행위(수정, 삭제)를 했고, 이 결과가 실패할 경우 이전 단계로 복구해 주는 기능을 뜻합니다.
만약 성공했다면, COMMIT, 실패해서 이전 단계로 복구하려면 ROLLBACK을 사용합니다.


17. EXEC
프로시저를 실행합니다.
ex: > EXEC [프로시저 명] [값1], [값2] ……

 

이밖에 알아두면 좋은 것은 Index, 트리거, 권한주기, X쿼리나 내부 함수인 DATEADD, COUNT, MAX, MIN, GROUP BY 등을 공부하면 좋겠지만, 위의 17가지 + 알파만 알고 있어도 서버 프로그래머 업무상 큰 지장은 없고 모르면, 누누이 말씀드리지만 그때그때 구글로 찾아도 참고할 자료는 많이 있습니다.

 

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

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

보통 온라인 게임에서는 패킷이 몇 개나 필요할까요?
게임에 따라 다르지만 보통 적게는 300 ~ 2,000개 정도 사용하는 거 같네요.
음…… 즉, 패킷 클래스를 300 ~ 몇천 개 +를 만들어야 하는데…… 이걸 어느 세월에 만들어야 할까요?


이런 단순 반복적인 비생산적인 코딩 작업은 컴퓨터에 대신해주면 좋겠죠?
그래야 단순 반복적인 작업에서 오는 에러를 막을 수 있을 겁니다.
패킷 클래스 cpp, h를 특정 규칙에 따라 만들어주는 프로그램을 짜보도록 합시다.
그리고 관리가 쉽도록 엑셀 파일을 사용해 봅시다.


패킷을 관리해야 하니 당연히 용도에 맞는 작명 법이 필요합니다. 저는 아래와 같은 규칙으로 작성하도록 하겠습니다.
1. C로 시작하는 건 (Client)클라이언트가 보내는 패킷입니다.
2. S로 시작하는 건 (Server)서버가 보내는 패킷입니다.
3. I로 시작하는 건 (Inter)서버들간 주고 받는 내부 패킷입니다.
4. 위의 단어 다음은 패킷의 속성을 기술합니다. REQ는 요청, ANS은 답변, NOTIFY는 전달 등의 의미 입니다.
5. 대문자로 작성하되, 단어 사이에는 스페이스 대신 _(스네이크)를 붙입니다.
6. #이 가장 먼저 나오면 그 행은 주석입니다.

 

위의 룰에 맞춰 아래와 같이 작성합니다. (시트 명 packetData)
 이것만 해도 상관은 없으나, 클래스 구조를 하드코딩으로 소스에 넣으면 나중에 수정하기 힘들어지므로 일정 양식(포멧)을 만들어서 프로그램이 이 포멧을 기반으로 소스를 뽑도록 만들어 봅시다.

 

 

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

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

 

네트워크 프로그래밍 중에 어떻게 보면 가장 쉬우면서 어려운 개념이 왔습니다.
네트워크 프로그래밍에서 데이터를 보내거나 받을 때 어떤 함수를 사용했는지 기억나시나요?
네, send와 recv함수, 또는 read, write 함수를 사용하죠.

 

그리고 이들의 데이터를 전달하는 방식은 char / byte와 같은 1byte 데이터 배열을 사용합니다.
이제 이 char *을 데이터 흐름 (stream)이라고 정의합니다.
패킷을 이해하시기엔 아래와 같이 메모리 블록을 생각하시면 좋을 것 같군요.

 

이 방법은 심플하기는 하고 굉장히 고전적인 방법입니다. 코드로 표현하면 아래와 같군요.

 

초창기에 많이 사용했습니다만, 프로그램이 커지면서 다음과 같은 관리 문제가 발생하고 맙니다.


1. 콘텐츠 변경 등으로 패킷을 변경할 때, 일일이 사용하는 곳을 찾아 수정해야 합니다.
위의 예를 보시면 레벨 다음에 이름을 보내는데, 중간에 캐릭터 타입에 대한 정보를 넣어야 한다면? 어떻게 해야 할까요? 추가 정보는 무조건 뒤에 추가하면 점점 의미 없는 복잡한 단어가 난무하는 코드가 떠오르기 시작되는군요.


2. 패킷 데이터 변경, 당연히 서버만 변경하면 안됩니다. 변경내용을 클라이언트 파트에 알려 줘야 하고, 클라이언트의 부수적인 작업이 들어가야 합니다.


3. 예를 들어 패킷 경험치 항목이 현재 4byte(int32)로 쓰고 있는데, 유저들의 폭업으로 unsigned int32의 최대값 약 42억을 넘길 필요가 있다면 어떻게 해야 할까요?
unsigned long long 으로 int64_t 로 바꾸면 캐릭터 이름 앞부분이 침범당하겠네요.
이뿐만 아니라 프로그래머 실수로 int32_t 를 float으로 읽어 버렸다면? offset 변경을 수정하지 않으면 데이터가 밀리겠군요.


4. 이런 패킷 방식이면 정리가 잘 안됩니다. 작업 중 여러 패킷 데이터가 필요하고 그때그때 저런 식으로 패킷을 만들면 이전에 만들었던 패킷도 또 만들게 되는 경우가 발생합니다. (패킷이 보통 1천~3천개의 종류가 있고 초창기 개발진이 퇴사하면 이 패킷에 대해서는 어디 문서화 하기 전까지는 아무도 모르는 코드가 됩니다)

2번은 컨탠츠 추가 시 수반되어야 하는 일이니 그렇다 쳐도 3, 4번은 유지 보수 하는데, 막막해질 거 같네요. 연관성을 모두 조사해서 같이 바꿔줘야 하니까요.
위에 sprintf 예에서 3번째 변수 수정을 해야 한다고 생각해 보세요.
3번에 대응하기 위해서는 encode / decode를 알아서 해주는 클래스를 만들면 어떻게든 해결되지 않을까요? 이 모든 것을 깔끔하게 해결하기 위해 흔히 알고 계시는 패킷 제네레이터 프로그램을 만들어서 소스코드를 만드는 프로그램을 짜면 해결될 것 같네요.

 

그럼 이제 패킷이 있다고 하면, 패킷을 서버에서 어떻게 처리해야 하는지 생각해 봅시다.
이전 장의 네트워크 프로그램 장에서의 다이어그램은 패킷 데이터를 어떻게 처리할지 였고, 이것은 스트림 데이터를 어떻게 분석해서 어떻게 넣을지를 생각해 보았습니다.


간단히 설명하면, 온전한 스트림 데이터를 받고, 이를 분석해서, 맡은바 처리에 넘기면 간단히 해결될 것 같군요.
이제 아까 네트워크 장에 나온 클래스 다이어그램에 나온 것처럼 하나하나 클래스를 만들어 봅시다.

 

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

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

일반적인 네트워크 프로그래밍의 마지막 부분 IOPC 소스는 간단한 에코 서버를 구현한 예제들이 많은데요

대체로 이런 소스를 기능별로 잘 살펴보시면 크게 2가지로 구분되어 있습니다.

 

 

우선 윈도에서 통신을 구현하는 API가 있겠고, 각각의 클라이언트 세션 정보를 가지고 있는 객체들이 있습니다.

이 세션에 대해서 직접 제어해야 다른 인위적인 행위 (특정 세션에 데이터 송수신 및 접속 종료 같은)를 할 수 있겠군요.

위의 소스는 정말 간단히 핵심만 구현시켜놓은 형태로 볼 수 있으며, 이를 자동차로 바꿔 보면, 차체 프레임에 바퀴 끼고 엔진만 올린 상태로 볼 수 있습니다. 차야 굴러가겠지만, 실제 공도에 나왔다간 큰일 날 겁니다.
어쨌든 위의 마인드맵과 같이 각 분야를 객체로 표현해 봅시다, 그러면 어느 정도 코드 정리도 되고, 코드가 정리돼야 추가 기능 확장도 쉽겠죠.


그럼 각 분야별로 어떻게 만들어 보면 좋을지 생각해 봅시다.

1. 네트워크 API: 전체 네트워크를 통제하는 역할이 주로 이루어집니다.
2. 세션 정보: 세션은 2 단말기 간의 통신이 연결됨을 나타냅니다.
                   클라이언트에게서 오는 데이터를 받고 전송해야 하는 역할을 해야 합니다.
3. 2번에서 데이터를 받아서 그대로 돌려주면. 에코 서버겠죠?
   여기에 데이터를 변형하는 로직을 분리해서 서버 기능 확장을 하도록 합시다.

이렇게 하면 대략 아래와 같이 생각할 수 있을 것 같군요.

 

 

조금은 눈에 들어오시나요?
이제 마인드맵을 그려서 대략 그림이 나왔으니, 각각 설계해봅시다.
설계는 맨 처음 언급했듯이 간단한 Activity랑 Class 다이어그램을 그려 보시기 바랍니다. 그리고 이후 제가 한 것과 비교해 보는 것이 좋을 것 같네요. 



  

서버는 아래와 같이 처리가 이루어지면 좋을 것 같군요.
Active 다이어그램과 같이 에코 코드의 기본 옵션을 역할별로 나눠 기술했습니다.
여기서 ContetnsProcess, Package 같은 것은 이다음에 기술되어 있으니 같이 보시기 바랍니다.
다음은 Session 역할입니다.


  
어떻게 보면 꽤 당연한 것을 Active로 나타내니 왜 이것을 할 까란 생각이 드실지도 모르겠습니다만, Activity를 그려야 모듈에서 어떤 일이 필요한지 정리되고 이를 바탕으로 Class 다이어그램을 그려야 좀 더 정교하게 그릴 수 있습니다. 물론 구현하다 보니 생각지도 못한 기능을 구현할 때도 있습니다만, 이는 그때그때 다시 그리면 좋겠죠. 한입에 다 먹으려고 하면 체합니다.
지금 설계의 기능을 보시면 소켓 데이터를 가지고 있고, 각각을 구별한 id 값, 접근, 전송, 수신, 끊기 기능을 넣고 있습니다.


다음은 데이터를 어떻게 처리할 것인가에 대해서 생각해 보겠습니다.
데이터는 세션으로부터 와서 이걸 패키징하는 처리와 이 패키징된 데이터를 가지고 실제 프로그램 처리 하는 부분으로 나눠 생각해 봅시다.


패킷 흐름은 recv를 한 이후, 그 byte 데이터(stream이라고 표현합니다)를 패키지로 만들어 낸 뒤, 이것을 패킷 처리 전담 처리에 넣는 과정을 담았습니다.
패키지(Package)는 말 그대로 소포의 의미입니다. 왜 소포 개념을 넣었느냐면, 소포처럼 받은 사람, 보낸 사람의 정보(세션 데이터)와 내용 물(데이터)가 있어야 반대로 나에게 보낸 클라이언트에 결과값을 줄 수 있기 때문입니다.


객체 지향은 말 그대로 소스를 현실 사물과 투과시킴으로써 사람이 좀 더 이해하기 편함에 목적을 두었다고 생각합니다. 그러므로 이런 용어를 사용하였습니다 패키지화하지 않고 내용을 분석해 처리해도 되지만, 네트워크 IO 처리에 부하가 가는 것을 막기 위해 일반적으로는 따로 처리 프로세스를 나눠서 처리합니다.
(보통 소포 물은 집에 와서 확인하지 직접 우체국 가서 찾은 뒤, 내용물 확인하지는 않죠?)


패킷 프로세스 처리는 간단합니다.
초기환경 설정이라 했지만, 내부적으로는 서버마다 오는 패킷을 처리하는 함수들을 등록하는 과정이며, 이를 큐에서 적절히 꺼내 내부 함수에 넘겨주는 역할을 담당합니다.

 


위의 Active 다이어그램을 바탕으로 클래스 도식화를 해보았습니다.

ContentsProcess Packet을 받아서 처리하는 곳입니다.
밑에 ServerContetsProcess가 있는데, 서버마다 패킷을 받으면 처리할 내용을 담을 것입니다.
Packet도 마찬가지입니다. 이 앞 패킷 처리에서 더 자세히 다룰 예정입니다만, Packet을 상속받아 각각의 ContentsPacket들을 만들어 보낼 것입니다.
Stream이라는 것이 보이는데 이는 데이터를 시리얼라이징(직렬 화) 하는 데 도움을 주는 클래스이며, 역시 이다음 장에 자세히 기술하겠습니다.


아직 저 클래스만으로는 어떻게 돌아가는지 잘 파악이 안되실 거 같지만, 위의 클래스를 바탕으로 서버, 세션, 패킷 클래스를 만들어 보도록 하겠습니다.
그리고 네트워크 라이브러리이므로 다시 라이브러리 프로젝트에서 작업하도록 합시다.

 

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

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

게임은 일종의 랜덤의 연속적인 놀이입니다.
디아블로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 에 링크 시켰습니다.

+ Recent posts