실전 Unity3D Engine Programing 과정 8일차 (2013.07.31 (수))


네트워크 강의를 한다.

아래 내용은 6개월치인데 간단히 하겠다.


네트워크란 통신, 온라인게임 등.....

네트워크에 연결된 기기는 모두 IP로 통신하고 이것이 주소이다.   Infomation Provider

IP는 공유가능하나, 포트는 유일하다. 



소켓 프로그램

- 네트워크를 연결하는 입구..

- 시스템 안에 



http://www.nettention.com/ 

사이트에 가보자.. 

- 유니티 상용서버   

- 프로젝트당 1900만원

- 샘플을 보자.

  http://www.nettention.com/sample

- 이것의 좋은 점은 한글 네트워크 엔진으로 설명문이 한글이다.. 전화로 요청하면 출장서비를 한다.

- 평가판으로 할 수 있다. 


RakNet  http://www.jenkinssoftware.com/

- 오픈 소스

- 멀티플레이어 게임

- P2P 기능이 뛰어나다.

- 이것이 유니티에 기본 포함되어 있다.

- 그렇지만 최근에는 유니티에서 더이상의 업데이트는 하지 않는다고 선언함


유니티  menu > Edit > Project Setting > Network

두개 항목이 나온다.

 Debug Level  : off, infomation, Full 의 조건이 있다.

Sendrate : 15 <== 서버와 통신을 1초에 15회 한다. 



RPC - Remote Procedure Call 원격 제어 콜

- 서로 다른 시스템에서 유니티의 함수를 호출할 수 있다. 별도의 패킷이 없어도 된다.



네트워크 뷰(Network Views)

- State Synchronizaton

   - 두 시스템이 매번 동기화 되어 있어야 한다. 이동, 상태등이..

   - tranforma, 리지디바디, 등(물리시뮬레이션 정보를 추가할 수 있다. 스크립트도 가능) 초당 15번 주고 받는다..

   - 이것은 RCP 정보와 별개로 동기화 한다.

- Reliable Delta Compressed

   

- Observerd

- GameObject

- View ID 

   게임오브젝트에 대해서 아디를 부여해서 필요한 것만. 주고 받을 수 있다.



적용방법은

   GameObject 선택 menu > Componet > Relatvie > Network 

   이렇게 하면 통신이 가능하게 된다.




네트워크_강의.doc



유니티 네트워크

 

한때 PC플랫폼에서의 전유물이었던 대규모 온라인게임은 모바일 하드웨어와 통신 인프라의 발전으로 수많은 현대인들의 필수품이 된 스마트폰에까지 MMO(massive multi-user online)의 개발 열풍이 불고 있습니다.

그런 흐름에 맞추어 유니티3D엔진에서도 많은 MMO게임이 개발되었고 더더욱 많은 MMO 게임이 개발되고 있습니다. 이제 게임 개발에 있어서 필수불가결 항목이 된 네티워크 프로그래밍과 유니티에서 할 수 있는 네트워크 프로그래밍 기법들에 대해서 일부나마 소개하고자 합니다.

 

유니티의 네트워크 프로그래밍에 대해서 소개하기에 앞서 네트워크 프로그래밍이 생소한 개발자들과 독자를 위해 소켓 프로그래밍에 대해서 간단히 소개를 하고 실제 유니티에 돌아가는 소켓 프로그래밍 예제를 설명하고자 합니다.

그리고 나서 유니티에서 제공하는 NetworkView에 대해서 소개하고 실무에 사용되었던 예제 코드를 설명한 후에, 실무에서 주로 사용한 상용 네트워크 라이브러리들에 대해서 소개하고 그중 포톤(Photon)에 대한 사용법과 실무에서 개발하면서의 노하우를 소개하도록 하겠습니다.

 

 

1.  소켓 프로그래밍

 

소켓(Socket)은 소프트웨어로 작성된 통신 접속점입니다. 네트워크 응용 프로그램은 그 소켓을 이용하여 통신망으로 IP 패킷(Packet)을 송수신하게 됩니다. 소켓은 응용 프로그램에서 TCP/IP 계층을 이용하는 창구 역할을 하고 있으며 응용 프로그램과 소켓 사이의 인터페이스를 소켓 인터페이스라고 합니다.

 

유닉스의 예를 들면, 유닉스의 경우 파일을 열면(open) int 타입의 정수를 리턴하는데 이것을 파일 기술자(file descriptor)라고 합니다. 응용프로그램에서 파일을 액세스할 때 해당 파일 기술자를 사용하면 됩니다.

 

파일 기술자는 기술자 테이블(descriptor table) index 번호입니다. 기술자 테이블이란, 현재 open되어 있는 파일의 각종 정보를 포함하고 있는 구조체를 가리키는 포인터들로 구성된 테이블입니다.

 

프로그램에서 소켓을 개설하면 파일 기술자와 똑 같은 기능을하는 소켓 기술자(socket descriptor)가 리턴됩니다. 응용프로그램에서 이 소켓을 통하여 목적지 호스트와 패킷을 송수신할 때 이 소켓 기술자(소켓번호)를 사용하게 됩니다.

 

소켓을 이용하는 네트워크 응용 프로그램에서 상대방 세션과 IP 패킷을 주고 받기 위해서는 다음의 다섯 가지 정보가 정해져야 합니다.

1.     통신에 사용할 프로토콜(TCP 또는 UDP)

2.     자신의IP주소

3.     자신의 Port 번호

4.     상대방의 IP 주소

5.     상대방의 Port 번호

 

이 소켓을 윈도우 환경에서 사용할 수 있도록 한 것을 윈도우 소켓(WinSock)l라 부릅니다.

 

1.     유닉스 소켓과 다른점

A.     윈도우 소켓은 DLL을 통해 대부분의 기능이 제공되므로DLL초기화와 종료 작업을 위한 함수가 필요합니다.

B.     보통 GUI 를 기반으로 하기 때문에 확장 함수가 존재합니다.

C.     멀티쓰레드를 지원하므로 멀티쓰레드 환경에서 안정적으로 동작하기 위한 구조와 함수가 필요합니다.

2.     윈도우 소켓의 장점

A.     호환성이 높아 기존 프로그램을 포팅하기 쉽습니다.

B.     가장 널리 쓰이는 네트워크 프로그래밍 인터페이스

C.     TCP/IP 외에도 다양한 종류의 프로토콜을 지원

D.     저수준, 중간 정도의 프로그래밍 인터페이스므로, 세부적인 제어가 가능

3.     윈도우 소켓의 단점

A.     어플리케이션 수준의 프로토콜을 프로그래머가 직접 설계해야 합니다. 즉 데이터 포맷, 전송 절차등을 고려해야하며 ,프로토콜 변경시 코드 수정이 불가피 합니다.

B.     서로 다른 바이트 정렬방식을 사용하거나 데이터 처리 단위가 서로 다른 중단 시스템간 통신을 할 경우 어플리케이션 수준에서 데이터 변환을 처리해야 합니다.

4.     윈도우 소켓의 구조

A.     대부분의 기능은 WS2_32.DLL MSWSOCK.DLL 로 제공됩니다.

B.    어느 경우든 어플리케이션은 소수의 DLL과 링크되어 실행됩니다.

 

TCP 소켓 프로그래밍 절차

 

TCP 소켓 프로그래밍의 절차는 아래 그림을 참고하시면 됩니다

 

[그림]1 TCP 소켓 통신 순서도

클라이언트-서버(CS) 통신 모델에서는 항상 서버 프로그램이 먼저 수행되고 있어야 하는데, 서버는 Socket()함수를 호출하여 통신에 사용할 소켓을 개설하고 이때 리턴된 소켓 번호와 자신의 소켓 주소를 Bind()를 호출하여 서로 연결 시켜 둡니다. 서버에서 Bind()가 필요한 이유는 소켓 번호는 응용 프로그램이 알고 있는 통신 창구 번호이고, 소켓 주소는 네트워크 시스템이 알고 있는 주소 이므로 이들의 관계를 묶어 두어야(Bind) 응용 프로세스와 네트워크 시스템 간의 패킷 전달이 가능하기 때문입니다.

 

다음에 서버는 Listing()을 호출하여 클라이언트로 부터의 연결 요청을 기다리는 수동 대기 모드로 들어가며, 클라이언트로부터 연결 요청이 왔을 때 이를 처리하기 위하여 Accept()를 호출합니다. 서버는 Accept() 시스템 콜에서 기다리고 있다가 클라이언트가 Connect()를 호출하여 연결 요청을 해오면 이를 처리합니다. 이때 연결이 성공적으로 이루어지면 서버와 클라이언트가 데이터를 송수신할 수 있게 됩니다.

 

한편 클라이언트는 Socket()을 호출하여 소켓을 만든 후 Bind()를 부를 필요 없이, 서버에게 연결 요청을 보내기 위하여 Connect()를 호출합니다. 이때 클라이언트는 접속할 상대방의 서버의 소켓 주소 구조체를 만들어 Connect()의 인자로 주어야 합니다.

 

클라이언트에서 Bind()를 호출할 필요가 없는 이유는 클라이언트 프로그램은 자신의 IP 주소나 포트 번호를 다른 클라이언트 또는 서버가 미리 알고 있을 필요가 없기 때문입니다. , 대부분의 클라이언트는 포트 번호를 특정한 값으로 지정할 필요가 없습니다. 그러나 서버는 미리 지정한 포트 번호를 통하여 클라이언트가 알고 있는 포트 번호를 Bind()로 연결해 두는 것이 필수 적입니다.

 

클라이언트가 Bind()를 사용하면 오히려 클라이언트 프로그램의 범용성이 떨어지게 되는데 왜냐하면 같은 포트번호를 사용하는 클라이언트 프로그램이 하나의 컴퓨터에 두 개 이상 실행되면 포트 번호 중복 사용으로 인하여 에러가 발생하기 때문입니다. 하지만 경우에 따라 이것은 게임 유저의 클라이언트 중복 사용을 막기 위해서 사용되기도 합니다.

 

클라이언트는 Connect()를 호출하기 전에 연결하고자 하는 서버의 주소를 지정하여야 하는데 string 타입의 IP주소를 이용하여 IPAddress 클래스의 IP, 정수 타입의 소켓번호를 이용하여 IPEndPoint라는 클래스를 생성하여 Connect()를 호출합니다.

 

자세한 예제는 아래와 같습니다.

 

코드1의 서버소스를 살펴보시면 Awake()함수에서 소켓을 생성하고 Bind()함수를 호출하고 Listen()함수가 호출된 것을 확인 하실 수 있습니다. 그리고 Update() 함수에서 Select함수를 정기적으로 호출하면서 연결 요청이 왔다면 Accept()함수를 호출해서 클라이언트와 연결을 합니다.

코드2의 클라이언트의 소스를 살펴보시면 Awake()함수에서 소켓을 생성하고 Connect()함수를 호출해서 서버에 연결 요청을 하는 것을 확인 하실 수 있습니다. Update()함수에서는 마우스 클릭을 하면 서버로의 마우스 위치정보를 Send()함수를 호출해서 전송합니다.

코드3 MessageData.cs 를 살펴보시면 MessageData라는 패킷 클래스를 보실 수 있습니다. Serializable 속성을 주어서 MessageData는 객체직렬화 되도록 합니다. 다른 패킷을 만드시거나 테스트를 할 시에 참고하시면 됩니다.

[그림] 2. SimpleNetwork Server Client가 실행된 화면

 

 

 

MMORPG서버개발패턴

|

Game Programming

서버 구조

서버분산패턴

·        MMORPG는 많은 사람들이 접속해서 플레이하는 게임이기 때문에 CPU 부하의 측면에서나 네트워크 전송량의 측면에서나 한대의 머신으로는 충분한 수의 동시접속자를 받아들이기 어렵다. 따라서, 여러대의 머신이 역할을 분담하게 하는 분산처리가 필수적인데, 이 섹션에서는 어떤 방식으로 각 머신들이 역할을 분담하게 할 것인가에 대한 패턴을 소개하고자 한다.

·        Spatial distributed server - 공간단위로 분할된 서버

·        agent thru server - 에이전트서버를 거치는 접속

·        single login server - 단일 로그인 서버

·        multiple login server - 복수개로 분할된 로그인 서버

·        integrated NPC server - 게임서버와 NPC처리 서버가 통합된 형태

·        Separated NPC server - 게임서버와 NPC처리 서버가 분리된 형태

버프로그램구조패턴

·        게임 서버 프로그램은 크게 (분산되어 있는 경우, 각각의 프로그램을 의미한다) 네트워크 처리와 메인 로직처리, DB 처리의 3 가지 역할을 담당해야 한다. 이러한 각각의 역할을 효율적인 방법 (여러개의 CPU를 풀활용하면서, CPU, 메모리, 네트워크 대역폭등의 자원을 최적화하는 것이 목적이다)과 향후 업데이트등에 쉽게 적응할 수 있는 유연한 구조를 본 섹션에서 다루고자 한다.

·        modular structure - 모듈화된 구조

·        acceptor/connector/logic thread - 접속처리기/접속기/로직처리기

·        proactor - 적극적 송수신처리

·        message queue - 메시지 큐

·        ref-counted packet - 참조카운트 패킷

·        ref-counted packet buffer - 참조카운트 패킷버퍼

오브젝트 구조

오브젝트구조패턴

·        게임에 등장하는 오브젝트에는 여러가지 종류가 있다. 그러한 오브젝트를 서버프로그램 안에서 구체적인 클래스로 나타내는 방법을 다룬다.

·        dynamic property object - 가변적 오브젝트 속성

·        static property object - 고정적 오브젝트 속성

·        schema based type - xml 스키마로부터 속성 타입을 정의

·        index based handle - 핸들을 오브젝트 컨테이너 내의 인덱스 기준으로 발급

·        sequential handle - 핸들을 일렬로 발급

·        GUID handle - 글로벌 유니크 핸들 발급

·        ref-counted long reference - 장기적 참조 관계를 카운팅하기

오브젝트관리패턴

·        게임 안에는 복수개의 다양한 오브젝트들이 존재한다. 그러한 오브젝트들을 어떤 방식으로 다뤄야 효율적이고 정확하게 다룰 수 있는지를 논한다.

·        object manager - 오브젝트 관리자

·        pooled object factory - 풀 기반의 오브젝트 생성기

·        section map - 지역단위로 세분화된 맵

·        unique object - 유니크 오브젝트

·        type-safety handle - 타입정보가 포함된 핸들

·        double dispatch - 2중 관계처리

오브젝트행동패턴

·        게임 내의 오브젝트는 유저의 요청 또는 게임적 흐름에 따라 다양한 일을 수행하고, 다른 오브젝트와 상호작용한다. 그러한 행동을 어떻게 구체적으로 정의할 것인지와 어떻게 서로 상호작용하게 할 것인지를 논한다.

·        synchronous messaging - 동기화된 메시지 전송

·        asynchronous messaging - 비동기식 메시지 전송

·        scripted calculation - 계산식의 스크립트화

·        state machine script - 상태머신의 스크립트 기술

·        latent script function - 스크립트상에서의 레이턴트 함수 구현

·        transaction script - 스크립트에 트랜잭션 개념 포함

오브젝트저장패턴

·        MMORPG의 게임 세션은 지속적으로 유지되어야 하므로 게임 서버상의 내용을 DB에 저장하고 다시 복원하여야 하며, 그러한 모든 작업은 사용자의 플레이를 방해하지 않도록 실시간으로 이루어져야 한다. 그러한 효율적인 처리가 가능하도록 DB를 구성하고 억세스하는 방식에 대해 논한다.

·        row data gateway - 테이블 행단위 연결

·        CLOB serialized objects - 인코딩/디코딩 과정을 이용해 다수의 오브젝트를 LOB필드에 저장

·        periodic dumping - 주기적 덤프

·        offline access - 오프라인 액세스

·        middle server - 미들 서버

커뮤니케이션 구조

클라이언트-서버 커뮤니케이션패턴

·        사용자는 주로 TCP/IP 의 클라이언트 서버 모델로 이루어진 네트워크 환경하에서 게임에 접속하는데, 이에는 네트워크 지연과 대역폭제한등의 제약조건들이 수반한다. 그러한 제약조건하에서 최대한 자연스러운 게임적 처리를 가능케 하는 클라이언트와 서버간의 연결방식에 대해 논한다.

·        bit-condensed packet

·        start-end movement

·        dead reckoning movement

·        Client based Collison Detection

·        client proxy

·        neighborhood cache

·        appearance cache

·        string table

서버-서버 커뮤니케이션패턴

·        global/local daemon

·        publisher/subscriber

·        DB based session

·        zone-cross arbiter

기타

보안/치트방지패턴

·        thin client

·        double login prevention

·        direct login prevention

·        speed hack prevention

·        object unique code

·        packet tempering prevention

분류되지 않은 패턴/Idiom

 

 

프라우드넷 샘플 보기

http://www.nettention.com/proudnet/index.aspx?service=sample

 

 

2.  유니티 네트워크

네트워크(Network) 클래스

유니티의 Network 클래스는 네트워크의 핵심 클래스이며 기본 기능들을 제공하는 클래스입니다. 네트워크에 사용되는 많은 인터페이스와 파라메터를 가지고 있으면 서버와 클라이언트를 세팅할 때 쓰이는 클래스입니다. 네트워크 클래스에 많이 쓰이는 함수와 인자를 알아보면 아래와 같습니다.

이벤트에 따라 호출되는 함수

동작 내용

OnPlayerConnected

새로운 플레이어가 접속하면 서버에서 호출 됨

OnServerInitialized

네크워크 초기화가 완료되면 서버에서 호출 됨

OnConnectedToServer

서버로의 연결이 완료되면 클라이언트에서 호출 됨

OnPlayerDisconnected

서버로부터 플레이어의 연결이 끊어지면 서버에서 호출 됨

OnDisconnectedFromServer

서버로의 연결이 끊어지면 클라이언트에서 호출 됨

OnFailedToConnect

서버로의 연결이 실패할 시에 클라이언트에서 호출 됨

OnNetworkInstantiate

서버에서 Network.Instantiate가 호출되면 클라이언트에서 호출 됨

자주 사용되는 변수

변수 설명

connections

모든 연결된 Player

isClient

현재 작동하는 네트워크 모드가 클라이언트인지, 아닌지

isServer

현재 작동하는 네트워크 모드가 서버인지, 아닌지

중요 클래스 함수

함수 동작 설명

InitializeServer

서버로써의 초기화

Connect

특정 주소와 포트를 이용하여 연결

Disconnect

모든 열려있는 연결들을 닫고 네트워크 인터페이스를 종료

AllocateViewID

이용 가능한 네트워크 View ID 숫자를 가져와서 할당한다.

Instantiate

프리팝(Prefab) 네트워크 인스턴스화

[]1. Network 클래스

네트워크 매니저(Network Manager) 소개

유니티3D Edit ->Project Settings -> Network 를 선택하면 Inspector 창에서 아래와 같은 항목을 볼 수 있습니다.

[그림]3 Network Manager

Debug Level 에서 Off를 선택하면 네트워크 에러 메세지만 볼 수 있고, Informational을 선택하면 통신을 하면서 발생하는 이벤트들을 메시지로 볼 수 있고, Full을 선택하면 모든 이벤트들을 확인 할 수 있습니다.

SendRate는 초당 몇회까지 통신을 하는지 조절할 수 있는 파라메터입니다. 이를 높일시 양질의 네트웤 통신이나 데드레커닝 같은 네트워크 클라이언트간의 위치 보간 등이 쉬우나 그만큼 초당 패킷량이 늘어 부하가 발생하게 됩니다.

 

네트워크 뷰(Network Views) RPC(Remote Procedure Call) 소개

네트워크 뷰는 네트워크 간의 데이터를 공유하게 해주는 운송수단이라고 할 수 있습니다. 통신 방법은 상태동기(State Synchronization)와 원격 프로시져 호출(Remote Procedure Calls)이 있습니다.

많은 클라이언트가 같은 게임을 실행시키고 있을경우, 각 클라이언트는 게임을 이루는 오브젝트들을 각각 가지고 있게 됩니다. 이 둘이상의 클라이언트들이 동일하도록 보이도록 할려면 오브젝트들의 데이터들이 공유로 인해서 동기화가 이루어져야 합니다. 이것을 상태 동기화(State Synchronization)라고 합니다. 상태동기화를 위해서는 많은 양의 데이터들을 주고 받아야 하며 게임 플레이가 가능하도록 네트워크를 통해서 데이터를 신뢰성있고 견고하게 유지시켜주어야 합니다.

유니티의 네트워크 뷰(Network View)에서는 이런 상태 동기화를 위해 특정 부분을 공유하도록 할 수 있게 해주고, 각각의 오브젝트들을 동기화 시켜줍니다.

이렇게 위치라든가, 상태라든가 항상 지켜보고 즉각적으로 동기화 시켜주어야 하는게 있는 반면, 그때 그때 이벤트에 따라 일회성 동기화가 필요한 경우가 있습니다. 예를 들어, 어떤 플레이어가 아이템을 얻었다고 한다면 다른 클라이언트에게 먹었다는 것을 한번만(여러 번 알리게 되면 한번에 여러 개의 아이템을 얻은 셈이 된다)알리어서 다른 클라이언트들 화면에 똑같이 먹은 아이템을 사라지게 하는 이벤트 같은 것이 일어나야 합니다. 이런 경우, 항상 일어나는 이벤트도 아닐뿐더러 일회성이어서 항상 아이템의 상태를 지켜보고 있을 순 없는 일이다. 보통의 경우 이럴 때는 서버에서 주변 클라이언트에게 패킷을 날리어 화면에 있는 아이템을 어떤 클라이언트 얻었으니 알아서 지워라 라는 명령을 내립니다.

유니티의 RPC(Remote Procedure Call)를 사용하게 되면 서버 또는 클라이언트에서 다른 서버 또는 클라이언트들이 갖고 있는 특정 함수를 호출하게 하여 RPC를 받은 서버 또는 클라이언트들의 상태나 이벤트등을 동기화 할 수 있습니다.

상세 설명

유니티의 네트워크 뷰는 네트워크뷰아이디(NetworkViewID)라는 것을 통해 구분을 짓게 됩니다. 한 네트워크뷰아이디는 기본적으로 모든 네트웤에 연결된 머신들 사이에서 유일한(Unique) 아이디입니다. 128비트 정수로 구현됐지만 네트워크 전송시에는 자동으로 16비트로 압축되어 보내지게 됩니다. 그렇게 보내지는 각 패킷은 네트워크 뷰가 적용되어 있는 클라이언트 사이드 측으로 도착하게 되고 네브워크뷰아이디를 통해 네트워크뷰를 구분지어서 네트워크뷰아이디에 해당하는 적절한 오브젝트에 패킷으로 날라온 데이터를 적용시키게 됩니다.

만약에 Network.Instantiate()함수를 사용해서 네트워크 오브젝트를 인스턴스화한다면, 네트워크뷰 아이디에 대한 걱정은 할 필요가 없습니다. 인스턴스화 하면서, 즉 생성되면서 고유의 네트워크 아이디를 갖도록 되어 있기 때문입니다.

또한, 수동적으로 네트워크뷰아이디를 각각의 네트워크 뷰에 Network.AllocateViewID를 활용해서 설정하는 것도 가능합니다.

네트워크 뷰(Network View) 컴포넌트

유니티의 네트워크뷰 컴포넌트에 대해서 상세히 알아보도록 하겠습니다. 유니티 네트워크 시스템을 쓴다고 하면 네트워크 뷰 컴포넌트는 반드시 잘 알아두셔야 합니다. 좀더 상세한 정보는 유니티 홈페이지 매뉴얼 페이지를 참조하시고 이 책에서는 꼭 알아 두어야 할 것만 짚고 넘어가도록 하겠습니다.


[그림]4. 네트워크 뷰 컴포넌트

상태동기화(State Synchronization) 또는 원격 프로시져 호출(Remote Procedure Calls)을 쓸려고 한다면 반드시 해당 게임 오브젝트에 네트워크뷰(Network View) 컴포넌트를 붙여주어야 합니다.

위의 그림을 보시면 네트워크 뷰에는 상태동기(State Synchronization)라는 속성과 관찰자(Observed)라는 속성 , View ID라는 속성이 있습니다. 상태 동기 속성의 타입엔 Off Reliable DeltaCompressed Unreliable 이라는 것들이 있는데 각각 상태 동기를 끄는 것과 지난 상태와 달라진 점만 압축해서 보내는 것과 모든 상태를 보내주어서 좀더 많은 네트워크의 대역폭이 필요하지만, 중요한 패킷 손실은 적습니다. Off의 경우는 실무에서 보통 RPC만 사용할 경우 Off로 설정하고 사용하고 있습니다.

상세 설명

게임오브젝트에 네트워크 뷰를 붙여줄 때 크게 두 가지를 고려해야 합니다.

1.    무슨 종류의 데이터를 네트워크 뷰를 통해 주고 받을 것인지

2.    데이터를 어떻게 보낼 것인지

보낼 데이터를 정하기

네트워크 뷰의 관찰자 속성은 단일 컴포넌트를 가르킬 수 있습니다. , 어떤 하나의 컴포넌트만 골라서 네트워크뷰가 주시하고 있는 것입니다. 여기서 주시가 가능한 컴포넌트는 Transform, Animation, RigidBody 또는 스크립트 입니다. 주시하고 있는 컴포넌트가 어떤 것이든지 네트워크를 통해 데이터를 주고 받게 될 것입니다. 만약에 직접적으로 데이터를 주고 받고 싶지 않다면 RPC의 호출을 통해서도 데이터 전송이 가능합니다.

어떻게 데이터를 보낼 것인지

주시하고 있는 컴포넌트의 데이터를 보내는 두 가지 방법은 앞에서도 말했다시피 상태동기화(State Synchronization)RPC(Remote Procedure Calls)입니다.

상태동기화를 사용한다면 타입을 Reliable Delta Compressed Unreliable, 이 둘 중 하나를 선택하면 됩니다. 그렇게 주시하고 있는 컴포넌트의 데이터들은 자동적으로 데이터를 주고 받게 됩니다.

Reliable Delta Compressed 타입은 순서 지향형입니다. 패킷을 받은 순서대로 보내주기 때문인데만약 패킷이 유실이 된다면 그 패킷은 재전송하게 됩니다. 어떤 패킷이 도착했을 때 기존에 받은 패킷이 있었다면 패킷큐에 자동으로 쌓이게 됩니다지난 전송시에 보낸 상태와 다른 상태만이 전송이 되므로 주시하고 있는 컴포넌트의 상태가 바뀐 것이 없다면 보내는 데이터가 없게 됩니다.

만약 스크립트를 주시하고 있다면 반드시 주시하고자 하는 변수를 시리얼라이즈화 해야하는 데 이것은  OnSerializeNetworkView() 함수를 통해 할 수 있습니다. 다음의 예제를 살펴보도록 하죠, 다음의 예제들은 유니티 홈페이지를 참고한 것입니다.

[코드] 4. SerializeNetworkView 예제 - 1

function OnSerializeNetworkView (stream : BitStream, info : NetworkMessageInfo) {

             var horizontalInput : float = Input.GetAxis ("Horizontal");

             stream.Serialize (horizontalInput);

}

위에 함수는 Horizontal 입력 값을 받아서 매 프레임 들어오는 stream에 입력 값을 더해서 보내는 함수입니다. 만약 받는 것과 보내는 스트림 데이터를 다르게 하고 싶다면 BitStream클래스의 isWriting 속성을 사용하시면 됩니다.

[코드] 5. SerializeNetworkView 예제 – 2

function OnSerializeNetworkView (stream : BitStream, info : NetworkMessageInfo) {

             var horizontalInput : float = 0.0;

             if (stream.isWriting) {

                           // Sending

                           horizontalInput = Input.GetAxis ("Horizontal");

                           stream.Serialize (horizontalInput);

             } else {

                           // Receiving

                           stream.Serialize (horizontalInput);

                           // ... do something meaningful with the received variable

             }

}

 

OnSerializeNetworkView sendRate수치에 따라 호출되고 동작하는 것이 정해집니다. sendRate 수치는 앞에서 설명한 네트워크 매니저에서 설정할 수 있습니다. 기본적으로 초당 15회로 세팅되어 있습니다.

만약 원격 프로시져 호출을 스크립트에서 사용하고자 한다면 게임오브젝트에 네트워크뷰 컴포넌트를 붙여야 합니다. 그리고 호출하고자 하는 함수에 자바스크립트라면 @RPC C#스크립트라면 [RPC]를 함수 구현 전에 붙여야 합니다. 그리고 나선 어떤 스크립트가 붙여져 있던 게임오브젝트라면 networkView.RPC()를 호출해서 원격 프로시져 콜을 수행할 수 있습니다.

[코드] 6. 자바스크립트 - RPC 예제

var playerBullet : GameObject;

function Update () {

             if (Input.GetButtonDown ("Fire1")) {

                           networkView.RPC ("PlayerFire", RPCMode.All);

             }

}

@RPC

function PlayerFire () {

             Instantiate (playerBullet, playerBullet.transform.position, playerBullet.transform.rotation);

}

 

또 다른 예제를 살펴 보도록 하겠습니다.

[코드] 7. RPC예제 2 – 자바스크립트

var cubePrefab : Transform;
function OnGUI () {
if (GUILayout.Button("SpawnBox")) {
    var viewID : NetworkViewID= Network.AllocateViewID();
    networkView.RPC("SpawnBox", RPCMode.AllBuffered, viewID, transform.position);
   }
}

@RPC
function SpawnBox (viewID : NetworkViewID, location : Vector3) {
   // Instantate the prefab locally
   var clone : Transform;
   clone = Instantiate(cubePrefab, location, Quaternion.identity) as Transform;
   var nView : NetworkView;
   nView = clone.GetComponent(NetworkView);
   nView.viewID = viewID;
}

 

 

[코드] 8. RPC 예제 2 - C# 스크립트

using UnityEngine;
using System.Collections;

public class example : MonoBehaviour {
public Transform cubePrefab;
void OnGUI() {
   if (GUILayout.Button("SpawnBox")) {
      NetworkViewID viewID = Network.AllocateViewID();
      networkView.RPC("SpawnBox",RPCMode.AllBuffered, viewID, transform.position);
   }
}
[RPC]
void SpawnBox(NetworkViewID viewID, Vector3 location) {
  Transform clone;
  clone = Instantiate(cubePrefab, location, Quaternion.identity) as Transform as Transform;
  NetworkView nView;
  nView =clone.GetComponent<NetworkView>();
  nView.viewID = viewID;
 }
}

 

 

RPC 상세 설명

원격 프로시져 호출(Remote Procedure Call) 은 원격 머신에 함수를 호출할 수 있게 해줍니다. 마치 일반 함수를 호출하듯이 쉽게 사용할 수 있지만 몇 가지 중요한 차이점은 꼭 알아 두셔야 합니다.

1.     RPC 호출은 많은 파라메터들을 가질 수 있습니다. 모든 파라메터들은 네트워크를 통해 전송이 됩니다. 많은 파라메터들을 보낼수록 전송 대역폭은 커집니다. 그러므로 할 수 있는한 최대한 최소의 파라메터만으로 통신하는 것이 좋습니다.

2.     RPC 호출을 할 때 어떤 대상에게 보내는 지를 정하는 것도 중요합니다. 몇가지 RPC 호출 모드로 모든 경우의 호출을 할 수 있는데 RPC함수를 사용해서 쉽게 특정 클라이언트만 , 서버에게만, 서버/클라이언트 모두에게 호출 전송이 가능합니다.

RPC호출은 보통 온라인 게임에서 특정 이벤트를 발생시킬 때 사용하거나, 경쟁하는 두 팀 사이에 특정 정보를 주고 받기 위해 사용합니다. 다만, 자동적으로 이루어지는 것은 아니고 개발자가 직접 RPC 함수를 생성하고 사용해야 합니다. 앞에 예를 든 것 중에 서버에 여러 클라이언트들이 접속해 있고 하나의 클라이언트가 아이템을 얻었다면 모든 클라이언트 화면에 아이템이 사라져야 하고 서버에선 아이템을 얻은 클라이언트에게 해당 아이템 정보를 인벤토리에 넣어주어야 합니다. 이런 경우, 아이템을 얻은 클라이언트가 서버에게 아이템을 얻었다는 RPC콜을 하고 서버는 RPC콜을 한 클라이언트에겐 아이템을 인벤토리에 넣으라는 RPC콜을 하고 다른 클라이언트들에겐 화면 안의 아이템을 다른 클라이언트가 먹었으니 화면에서 삭제해라라는 RPC콜을 합니다.

또 다른 경우로, 서버의 브로드캐스팅(BroadCasting)을 줄여서 클라이언트가 호스트(Host)처럼 동작하기 위해서도 사용합니다. 예를 들면,게임안의 마을 같은 곳에서 어떤 클라이언트가 춤을 추는 애니메이션을 한다던가 인사를 하는 제스쳐를 취한다던가 하는 게임에는 영향을 끼치진 않지만 그 클라이언트를 보고 있는 클라이언트들은 같은 상황을 볼 수 있게 할려면 춤을 추거나 인사를 하고자 하는 클라이언트가 주변 클라이언트에게 내 캐릭터가 이제부터 어떤 행동을 하는 애니메이션을 할 것이다라는 RPC콜을 하면 주변 클라이언트는 해당하는 화면 안의 클라이언트의 캐릭터가 애니메이션이 되는 것을 볼 수 있게 됩니다. 이렇게 서버가 꼭 알아야 할 필요가 없는 경우엔 클라이언트가 직접 주변 클라이언트에게 RPC호출을 하는 경우에도 사용할 수 있겠습니다.

RPC 사용법

이제 두 단계로 어떻게 RPC호출을 셋팅하는지를 살펴보도록 하겠습니다. 일단 RPC호출을 할 함수를 선언하고 함수 앞에 Prefix RPC 속성(attribute)을 주면 됩니다.

아래의 소스코드는 RPC 속성과 함께 함수를 정의하는 예제 입니다.

[코드] 9. RPC의 속성과 함께 함수를 정의하는 예제

// All RPC calls need the @RPC attribute!

@RPC

function PrintText (text : String)

{

    Debug.Log(text);

}

 

RPC 속성을 정의해준 것 만으로 앞으로 PrintText함수를 RPC함수로써 호출 할 수 있게 됩니다. 모든 RPC 네트워크 통신은 네트워크뷰가 붙어있는 같은 스크립트가 붙어있는 게임오브젝트들을 통해 이루어지게 됩니다. 다시말하면, RPC 통신을 하기 위해선 네트워크뷰 컴포넌트를 꼭 게임오브젝트에 추가시켜줘야 하는 것입니다.

RPC 함수의 인자로는 다음과 같은 타입을 사용할 수 있습니다. 꼭 유의하세요:

*       int

*       float

*       string

*       NetworkPlayer

*       NetworkViewID

*       Vector3

*       Quaternion

RPC함수를 호출하는 구문은 아래와 같이 사용하면 됩니다..

networkView.RPC ("PrintText", RPCMode.All, "Hello world");

이 코드는 네트워크뷰가 붙어있는 모든 게임오브젝트중에 PrintText()라는 함수를 가지고 있는 스크립트의 함수를 호출하게 됩니다.

RPC 함수의 첫번째 파라미터는 호출될 함수의 이름을 뜻합니다. 두번째 파라미터는 어떤 대상들이 호출하게 될지 결정하는 것이고, 세번째 파라미터는 호출할 함수에 넘겨줄 인자입니다. 이와 같이 RPCMode.All,즉 연결된 모든 대상들을 상대로 RPC 호출을 할 경우 연결된 대상들에겐 즉각적으로 호출을 요청하지만, 아직 접속중인 클라이언트의 경우 Buffer에서 기다렸다고 접속이 완료되는 대로 호출합니다.함수에서 요구하는 모든 파라메터들은 네트워크를 통해서 RPC함수에 전달이 됩니다. 위와 같은 경우 “Hello World” 라는 파라메터가 텍스트 파라메터로써 PrintText함수에 전달이 됩니다.

invoked RPC 호출을 할 때 외부 파라메터 NetworkMessageInfo라는 구조체의 변수를 외부 파라메터로써 호출이 가능합니다. 딱히 필요 없는 정보라면 전송을 하지 않아도 됩니다.

NetworkMessageInfo 파라메터를 사용한 예제 코드는 아래를 참고하세요

[코드] 10. NetworkMessageInfo 파라메터를 사용한 예제

@RPC

function PrintText (text : String, info : NetworkMessageInfo)

{

    Debug.Log(text + " from " + info.sender);

}

이미 언급했다시피, 스크립트 안에서 RPC 함수 호출 동작 순서는 네트워크뷰가 반드시 게임오브젝트에 붙어 있어야 하고 해당 스크립트를 포함하고 있어야 합니다.

RPC Buffer

RPC 호출은 버퍼(Buffer)에 저장될 수 있습니다. 버퍼화된 RPC호출은 저장되고 새로 연결된 클라이언트의 설정 순서에 맞추어서 차례차례 실행이 됩니다이것은 이미 존재하는 게임에 새로운 유저의 로그인 기능을 쉽게 만들 수 있습니다. 또한 로그인하는 모든 유저에게 특정 레벨을 바로 로딩하도록 할 수도 있습니다. RPC호출은 연결된 모든 클라이언트에게 전송이 되면서 버퍼에 세팅했다가 새로 들어오는 클라이언트에게 똑같은 과정을 거치도록 할 수 있는 것입니다. 또한 새로 들어오는 클라이언트에게 기존의 다른 클라이언트들을 자동적으로 보내어지게 되어 있어서 , 새로 들어온 클라이언트에게 기존의 연결 전체를 보내야 하는 과정이 필요 없습니다.

유니티는 또한 RPC버퍼로 부터의 호출을 제거할 수 있는 기능을 제공합니다. 예를들어 레벨을 로딩한다고 할때, 만약에 게임이 여러 레벨들로 이루어져 진행이 된다고 하면, 새로 연결된 플레이어를 현재 진행중인 레벨을 로딩하도록 할 수 있습니다. 위와 같은 경우엔, 기존에 이미 로드된 레벨 로드 함수들을 RPC 버퍼에서 제거해서 현재 레벨만 로드하도록 구현하면 됩니다.

 

NetworkView RPC의 실습예제

 

그러면 이번 챕터에서는 앞에서 알아본 네트워크뷰와 RPC호출을 사용해서 실제 사용될 수 있는 유니티 네트워크 코드를 살펴보겠습니다.

아래 예제를 실행토록 할려면 먼저 main, Scene stage1 Scene을 만들어야 합니다. Main Scene에서는 먼저 UnityNetwork.js(C# 버전으론 UnityNetworkCS.cs)스크립트 파일을 만들어서 아래의 소스코드를 입력합니다. 그리고 MainCamera에 만든 스크립트 파일(자바스크립트와 C#스크립트 둘 중 하나만)을 붙여 놓습니다. 그리고 캐릭터로 쓸 Prefab을 하나 만들어서 DummyKnight.js(C#버전으론 DummyKnightCS.cs) Prefab에 붙여 놓고 만든 Prefab Main SceneMainCamera UnityNetwork 스크립트 파일의 Inspector 창에서 Knight Player Prefab 이라는 변수에 앞에서 만든 Prefab을 드래그앤드롭으로 붙여 놓아야 합니다.

 

[그림] 4. Main Scene의 세팅

[그림] 5. DummyKnight (임시 캐릭터 프리팝) 세팅

 

 

 

[그림] 7.실행화면

[그림] 8.Console Window 출력 메시지

3.  상용 유니티 네트워크 엔진

 

1.   상용 네트워크 엔진?!

유니티에서 네트워크를 제공하긴 하지만 역시 대규모 게임 서버로썬 부족한 부분이 있습니다. 향후 유니티엔진의 추가되는 기능들의 개발 스케쥴을 보아도 많은 지원을 하진 않는다는 아쉬운 점이 있습니다. 그리고 대형 네트워크 시스템엔 어쩔수 없는 게임마다의 독특한 네트워크 레이어를 로우 레이어(Low Layer) 부터 커스터마이징해서 개발하고자 하는 게임에 맞춤형 시스템을 만들수 밖에 없는데 유니티 엔진에서는 네트워크 레이어정도의 커스터마이징이 불가능합니다.

그래서 유니티 엔진을 선택한 많은 온라인 게임회사들이 네트워크 시스템과 게임 서버를 자체 개발 또는 이미 만들어져서 라이선스를 유료로 판매하는 좋은 상용 네트워크 엔진들을 사용합니다.

 

이 단원에서는 상용 네트워크 엔진엔 어떤 것들이 있는지 알아보기 위해 현존하는 네트워크 엔진중 가장 각광을 받고 있는 두가지 네트워크 엔진인 스마트폭스(SmartFox)와 포톤(Photon)을 소개하고, 그중 포톤이라는 서버 엔진을 직접 사용해본 후기등을 이야기해 보려 합니다.

 

2.   스마트폭스서버(SmartFoxServer)

[그림] 9. 스마트폭스서버

 

스마트폭스서버는 다양한 플랫폼의 서버로써 유명한 네트워크엔진입니다. 최근에 유니티3D .Net 등을 지원하면서 유니티의 네트워크 엔진으로써 주목을 받고 있습니다.

 

특징

스마트폭스서버는 Adobe Flash and Flex 2용 네트워크 엔진으로써 디자인되었고, 또한 자바(Java)와 쇼크웨이브(Shockwave)와 닷넷(.Net)까지 지원합니다.

 

스마트폭스서버는 큰규모의 MMO 게임서버부터 간단한 채팅과 턴베이스(turn-based) 게임까지 커버가 가능하도록 해줍니다. 쉬운 사용법과 더불어 게임에 필요한 고급적인 기능들이 제공되어 네트워크 엔진으로써 각광을 받고 있습니다.

 

[그림] 10. 스마트폭스의 라이선스 형태

스마트폭스서버의 The BASIC edition은 멀티플레이어 응용어플리케이션 개발이 가능하지만 서버측면에서의 코딩은 불가능합니다. 간단한 채팅 게임이나 버디 메신저, 아바타 채팅등을 개발목적으로 만들시에 좋습니다. 서버측면에서의 코딩은 불가능 하다는 것은 제공되는 기능만 사용하고 추가적인 서버 기능은 만들어 넣을 수 없는 것을 의미합니다.

 

-       스마트폭스서버의 The PRO edition 은 대부분의 리얼타임 네트워크 엔진에 사용되며 큰규모의 게임의 개발 목적시에 사용됩니다..

 

-       200유로에 31만원(2011.08기준)정도라고 볼 때 네트워크 엔진을 사용하는 개발사입장에선 충분히 고려한 후에 최종 사용결정을 내리는 것이 중요합니다.

 

-       서버사이드의 기능확장은 액션스크립트(Actionscript),자바스크립트(Javascript),파이썬(Python)과 자바(Java)라는 언어를 사용합니다.

 

-       데이터베이스(DB) 엔진으로 접근이 쉬운 편입니다.( MySQL, Access, MS SQL) . 그리고, 관계형DB시스템 서비스를 서버측면에서 높은 성능으로 지원하고 있습니다.

 

-       웹서버의 기능을 사용해 파일 업로드와 서버의 컨텐츠들을 쉽게 제공할 수 있습니다.

 

-       The BlueBox 라는 모듈이 있는데, 이것은 방화벽과 프록시들에 상관없이 모든 클라이언트들과 연결을 하도록 도와줍니다.

 

-       서버의 프레임워크(framework)는 복잡한 상호 작용을 만드는 하이레벨(high-level) API와 확장 기능을 제공하고 고급 서버 기능에 대한 모든 액세스 권한을 제공합니다

 

-       제이썬(JSON) Raw 문자열(Raw string) 프로토콜을 사용함으로써 빠른 데이터 전송과 실시간 동작에 따른 패킷 전송량을 줄일 수 있습니다.

 

-       친구 목록 매니저는 친구 등록등에 필요한 기능 및 모든 주요 인스턴트 메신저와 같은 기능을 제공합니다

 

스마트폭스의 유니티엔진에서의 기본 사용법 소개

 

먼저, 스마트폭스서버의 홈페이지(http://www.smartfoxserver.com/)중 다운로드 페이지에서 해당하는 플랫폼의 스마트폭스서버의 설치파일을 다운 받습니다. (http://www.smartfoxserver.com/2X/download.php) 그리고

API 페이지(http://smartfoxserver.com/labs/API/)에서 유니티용 다운로드 .Net API를 다운로드 합니다.

다운 받을 파일을 설치한 후에  .Net API 압축파일을 풀은 폴더를 살펴보면 예제폴더(SFS_CSharp_1.2.6\SFS_CSharp_1.2.6\Examples)가 있습니다 그 중 01_SimpleConnect 를 살펴보도록 하겠습니다.

 

설치가 끝났다면 스마트폭스 설치폴더(C:\Program Files\SmartFoxServerPRO_1.6.6)에 가셔서 start 라는 윈도우용 배치파일을 실행시키면 아래와 같은 화면을 보실 수 있습니다.

[그림] 11. 스마트폭스 서버의 콘솔창

스마트폭스서버가 실행이 되었습니다~!

 

자 이제 예제 폴더 SFS_CSharp_1.2.6\SFS_CSharp_1.2.6\Examples\01_SimpleConnect 를 유니티에서 열어보면 프로젝트 폴더 구성은 아래와 같습니다.

[그림] 12. SimpleConnect 예제의 프로젝트 창

 

살펴보시면 Plugins라는 폴더 안에 SmartFoxClient라는 DLL 파일이 들어 있는 것을 확인하실 수 있습니다. 유니티에서 외부 라이브러리(DLL)을 사용할 경우엔 Plugins 폴더 안에 넣어야 합니다.

이렇게 DLL 파일이 Plugins 폴더안에 들어가게 되면 유니티프로젝트에서 SmartFoxClient의 기능을 사용할 수가 있습니다.

 

실행을 해보면 아래와 같은 메시지를 확인하실 수 있으실 겁니다.

[그림] 13. 실행 화면

연결이 성공됐다는 메시지가 화면에 출력이 됩니다.

 

그럼 이제 마지막으로 연결 소스 코드를 한번 살펴 보도록 하겠습니다.

[코드] 15. ConnectionGUI.cs – C# 스크립트 버전

 

using UnityEngine;

using System;

using SmartFoxClientAPI;

 

public class ConnectionGUI : MonoBehaviour

{

             //----------------------------------------------------------

             // Setup variables

             //----------------------------------------------------------

             private string ip = "127.0.0.1";

             private int port = 9339;

             private string statusMessage = "";

 

             //----------------------------------------------------------

             // Called when program starts

             //----------------------------------------------------------

             void Start()

             {

                           SmartFoxClient smartFox = new SmartFoxClient();

                           SFSEvent.onConnection += HandleConnection;

                           smartFox.Connect(ip, port);

             }

               

             //----------------------------------------------------------

             // Draw GUI every frame

             //----------------------------------------------------------

             void OnGUI()

             {

                           GUI.Label(new Rect(10, 10, 500, 100), "Status: " + statusMessage);

             }

 

             //----------------------------------------------------------

             // Handle connection response from server

             //----------------------------------------------------------

             void HandleConnection(bool success, string error)

             {

                           if (success)

                           {

                                        statusMessage = "Connection succesfull!";

                           }

                           else

                           {

                                        statusMessage = "Can't connect!";

                           }

             }           

}

 

 

 

Start()함수를 살펴보시면 SmartFoxClient라는 클래스를 인스턴스화해서 바로 사용하는 것을 보실 수 있고 소스 상단측에 보시면 using SmartFoxClientAPI 라는 구문을 통해서 스마트폭스 클라이언트 DLL을 사용할 수 있게됩니다.

 

만약, 여러분이 스마트폭스서버를 이용하여 네트워크게임의 서버를 개발하실려고 한다면 스마트폭스서버에서 제공하는 50개가 넘는 예제들을 살펴보시고 홈페이지의 문서와 WhitePaper 페이지를 꼭 숙지하는 것이 좋다는 어드바이스를 해드리고 싶습니다. 제공되는 API가 정확히 어떤식으로 돌아가는지를 알아야 서버측에서 생기는 여러가지의 문제점들을 유용하게 대처할 수 있기 때문입니다.

 

3.   포톤(Photon) 네트워크 엔진

.

[그림] 14. 포톤 네트워크 엔진

포톤 네트워크 엔진은 MMO장르에 개발하기 적합한 유니티를 위해 최적화된 소켓 네트워크 엔진입니다. MMO에만 적합한 것은 아니지만 그만큼 대형 서버 네트워크에 어울리는 성능을 가진 엔진입니다. 포톤 역시 스마트폭스서버와 함께 온라인 게임 개발사들이 많이 사용하는 네트워크 엔진이면서 지금도 게임 개발에 쓰기 적합한 많은 기능들이 추가되고 있는 엔진입니다.

 

특징

[그림] 15. 포톤 구성도

위 그림은 포톤의 구성도입니다. C/C++로 네트워크 코어 시스템의 성능을 끌어 올린후 .Net 프레임워크를 얹어서 여러 언어로 만든 개발자의 로직용 소스코드를 강력하게 서버 사이드에서 지원하고 있습니다. 클라이언트 사이드에선 SDK(SoftwareDevelopmentKit)을 사용해서 서버 사이드와 Binary Protocol을 사용해서 통신을 하게 됩니다.

 

서버 게임 로직은 C# 언어를 사용합니다. 이는 유니티에서 C#스크립트를 사용할 경우 코드를 공유해서 쓸 수 있다는 장점이 있습니다.

 

-       간단하고 유연한 RPC호출을 사용할 수 있습니다.

-       Fiber를 통한 메시지 전송은 많은 쓰레드 관련 문제를 해결했습니다.

-       Lite는 간단한 룸지향(Room Based) 게임을 위해 제공됩니다. 이는 고스톱이나 맞고와 같은 게임등을 제작할 시에 유용합니다.

-       클라우드 시스템을 간단히 개발, 추가할 수 있습니다.

-       가벼운 Binary 프로토콜은 전송대역폭 부하와 패킷의 크기를 최적화 하는 데 좋습니다.

-       플랫폼과 상관없이 통신할 수 있도록 하는 기능이 있습니다.

 

개발 시스템 요구사항

 

서버 운영을 위한 추천 사양

Windows Server 2008 - 64 bit

Microsoft .NET Framework 4.0

 

개발에 필요한 추천 사양

Windows XP, Windows Vista or Windows 7

Microsoft .NET SDK 3.5 SP1

Microsoft Visual Studio 2008

 

기본 사용 포트 리스트들

UDP: 5055

TCP: 4530

TCP: 843 (유니티 웹플레이어와 플래쉬 크로스도메인을 위해 사용)

TCP: 943 (실버라이트 크로스도메인을 위해 사용)

 

포톤의 유니티엔진에서의 기본 사용법 소개

 

먼저 포톤의 홈페이지(http://www.exitgames.com/)에서 포톤 서버를 다운받은 후(가입해서 로그인해야 합니다.) 홈페이지 하단부에 유니티 관련 SDK를 받을 수 있습니다. 포톤을 사용할 때  주의해야할 점중 하나가 라이센스(.license)파일입니다. 라이센스 파일에 따라 동시에 접속가능한 유저수와 기능이 제한되기 때문에 free license 파일을 같이 받으셔야 합니다.

 

포톤 서버의 압축을 푼 폴더를 살펴보면 Deploy폴더가 있습니다. 그 중 현재 사용하고 계신 플랫폼에 따라 Window OS 32비트 운영체제를 쓰는지 64비트 운영체제를 쓰는지, XP OS를 사용하고 계신지에 따라 폴더를 선택합니다.

 

폴더 안을 살펴보면 PhotonControl.exe라는 파일이 있습니다. 이파일을 실행시키기 전에 다운받은 라이센스파일을 PhotonControl.exe파일이 있는 폴더 안에 같이 복사해 넣으셔서 덮어씌우셔야 합니다.

이제 PhotonControl.exe를 실행시킵니다.

실행이 되었다면 아래와 같이 작업표시줄에 나타나게 됩니다.

 

[그림] 16. PhotonControl.exe 실행

박스 모양()의 포톤의 로고를 마우스 오른쪽 클릭을 하시면 아래와 같은 메뉴를 보실수있습니다.

[그림] 17. 포톤 아이콘을 마우스 오른쪽 클릭한 메뉴 화면

 

이중 Photon Start as application을 선택하시면

[그림] 18. 툴바 메뉴중 Start as application 을 선택

 

약간의 시간이 흐른 후 포톤이 실행이 된 것을 아래와 같이 확인하실 수 있습니다.

[그림] 19. Photon Application이 정상 동작함

박스모양()의 포톤의 로고에 파란 불이 들어온 것을 확인하셨으면 포톤 서버 실행에 성공하신 겁니다.

 

자 이제 유니티 SDK를 다운받아 압축을 푼 폴더을 살펴보면 샘플 프로젝트가 들어있는 걸 확인하실 수 있습니다. 유니티로 샘플 프로젝트를 열어보겠습니다.

먼저 Project 창을 살펴보시면 아래와 같은 파일들이 프로젝트 Asset폴더안에 들어 있는 것을 확인하실 수 있는데 Photon이라는 폴더 안에 library 폴더로 PhotonUnity3D 외부 라이브러리 파일이 들어 있는 것을 확인할 수 있고 같은 파일이름의 XML파일이 들어 있는 것을 보실 수있는데 이  XML파일안에  라이브러리에 포함된 포톤 클래스의 함수들과 함수 동작에 관련한 자세한 설명을 보실 수 있습니다.

 

[그림] 20. Demo-LiteLobby-ChatRoom 샘플의 프로젝트 창

 

그럼 이제demo-litelobby-chatroom 샘플 데모 프로젝트를 실행 시켜 보겠습니다.

유니티가 실행이 되어서 Project창의 ChatScene 이라는 Scene파일을 더블 클릭하면 아래와 같은 화면을 확인하실 수 있습니다.

[그림] 21. Demo-LiteLobby-ChatRoom 샘플의 Game

 

이제 유니티의 실행 버튼을 누르면 포톤 로비&채팅 데모 프로젝트가 정상동작하시는 것을 확인 하실 수 있습니다.

[그림] 22. Demo-LiteLobby-ChatRoom 샘플의 실행화면

참고로, 포톤의 경우 서버의 설정을 PhotonServer.XML파일을 통해 IP주소라던가 서버에서 실행 할 게임목록등을 정의 할 수 있습니다. 외부파일에서 정의하므로 설정 변경으로 인한 서버의 재컴파일을 안해도 되는 장점이 있습니다.

 

포톤서버에 대한 상세 설명

 

포톤의 Operation Events

 

Operations

Operation RPC(Remote Procedure Call)호출의 포톤만의 형식입니다. 이것은 서버에 구현된 함수를 클라이언트에서 호출하기 위해 사용되며, 클라이언트 측에선 특정 파라메터(위치값등이나 자신의 아이디) Operation 함수의 인자로써 서버에 구현된 함수를 호출하게 되면 전달된 함수의 인자를 갖고 서버에서 처리된 결과값을 해당 클라이언트에 돌려주게 되며 그때 OperationResult()함수가 호출됩니다.

 

Events

Operation과 달리 event는 항상 일어나는 일은 아니고 특정 이벤트나 트리거 등에 의하여 생성되어 클라이언트가 받게되는 일조의 메세지입니다. 이벤트는 서버나 다른 클라이언트에서 전송되어 질수 있습니다.

예를들어 다른 유저가 내가 접속해 있는 방에 접속해 왔을때 나한테 Event 메세지 날라와서 EventAction()함수가 호출 됩니다.

 

포톤의 작업순서

포톤 서버에 접속하고 Room에 참가하는 간단한 클라이언트를 구현한다고 해보겠습니다.

먼저 ,포톤서버의 lite 서버를 사용해서 포톤 lite 서버에서 제공하는 Room Join이나 Exit 기능을 사용하고 클라이언트가 Room join요청시 Room이 하나 생성되고 클라이언트는 특정 ID를 부여 받게 됩니다.

위와 같은 것을 포톤서버를 이용해 구현한다고 한다면

 

-       LiteLobbyPeer 인스턴스를 생성합니다.

 

-       정기적으로 Service()함수를 불러주어서 Event들을 받아오고 Command들을 보내 줍니다. (1초에 10번 정도면 적당합니다.)

 

-       Connect()함수를 호출해서 서버에 연결합니다.

 

-       IPhotonPeerListener.PeerStatusCallBack의 호출이 있을때 까지 기다립니다.

잠시후에, Status.Connect()와 같은 status가 반환되어 옵니다.

 

-       OPJoin()함수를 호출해 게임 접속을 요청하면 잠시후에, LiteOpCode.Join라는 OpCode와 함께 OperationResult()함수가 호출됩니다.

 

-       참고로, 이벤트는 evcode라는 타입으로 IPhotonPeerListener.EventAction() 함수에서 호출됩니다.

 

-      게임에서 나갈려고 한다면, LitePeer.OpLeave()를 호출하면 게임에서 나가게 되고 잠시후에 OperationResult()함수에서 LiteOpCode.Leave 메세지가 반환되어 돌아온다면 Room을 나오는 것에 성공한 것입니다.

 

자세한 구현은 아래 코드를 참고 하시면 됩니다.

 

[코드] 16. PhotonClient.cs

using System;

using System.Collections;

using System.Text;

using ExitGames.Client.Photon;

using UnityEngine;

public class PhotonClient : MonoBehaviour, IPhotonPeerListener

{

    protected LiteLobbyPeer Peer;

    public string ServerAddress = "localhost:5055";

    protected string ServerApplication = "LiteLobby";

public int SendIntervalMs = 100;

    private int NextSendTickCount = Environment.TickCount;

    public PeerStateValue LitePeerState { get { return this.Peer.PeerState; } }

    public ClientState State = ClientState.Disconnected;

    public int ActorNumber;

private StringBuilder DebugBuffer = new StringBuilder();

public string DebugOutput { get { return DebugBuffer.ToString(); } }

  

    public bool DebugOutputToConsole = true;

    public string OfflineReason = String.Empty;

    public enum ClientState : byte

    {

        Disconnected, Connected, InRoom

    }

    public virtual void Start()

    {

        this.Peer = new LiteLobbyPeer(this);

        this.Connect();

    }

    public virtual void Update()

    {

        if (Environment.TickCount > this.NextSendTickCount)

        {

            this.Peer.Service();

            this.NextSendTickCount = Environment.TickCount + this.SendIntervalMs;

        }

    }

    public virtual void OnApplicationQuit()

    {

        this.Peer.Disconnect();

    }

    internal virtual void Connect()

    {

        this.OfflineReason = String.Empty;

        // PhotonPeer.Connect() is described in the client reference doc: Photon-DotNet-Client-Documentation_v6-1-0.pdf

        this.Peer.Connect(this.ServerAddress, this.ServerApplication);

    }

    public void DebugReturn(DebugLevel level, string message)

    {

        this.DebugReturn(message);

    }

 

    public void DebugReturn(string message)

    {

        this.DebugBuffer.AppendLine(message);

        if (this.DebugOutputToConsole)

        {

            Debug.Log(message);

        }

    }

    public virtual void OperationResult(byte opCode, int returnCode, Hashtable returnValues, short invocID)

    {

        this.DebugReturn(String.Format("OperationResult: {0}={1}", opCode, returnCode));

        switch (opCode)

        {

            case (byte)LiteOpCode.Join:

                this.State = ClientState.InRoom;

                this.ActorNumber = (int)returnValues[(byte)LiteOpKey.ActorNr];

                break;

            case (byte)LiteOpCode.Leave:

                this.State = ClientState.Connected;

                break;

        }

    }

    public virtual void PeerStatusCallback(StatusCode statusCode)

    {

        this.DebugReturn(String.Format("PeerStatusCallback: {0}", statusCode));

        switch (statusCode)

        {

            case StatusCode.Connect:

                this.State = ClientState.Connected;

                break;

            case StatusCode.Disconnect:

                this.State = ClientState.Disconnected;

                this.ActorNumber = 0;

                break;

            case StatusCode.ExceptionOnConnect:

                this.OfflineReason = "Connection failed.\nIs the server online? Firewall open?";

                break;

            case StatusCode.SecurityExceptionOnConnect:

                this.OfflineReason = "Security Exception on connect.\nMost likely, the policy request failed.\nIs Photon and the Policy App running?";

                break;

            case StatusCode.Exception:

                this.OfflineReason = "Communication terminated by Exception.\nProbably the server shutdown locally.\nOr the network connection terminated.";

                break;

            case StatusCode.TimeoutDisconnect:

                this.OfflineReason = "Disconnect due to timeout.\nProbably the server shutdown locally.\nOr the network connection terminated.";

                break;

            case StatusCode.DisconnectByServer:

                this.OfflineReason = "Timeout Disconnect by server.\nThe server did not get responses in time.";

                break;

            case StatusCode.DisconnectByServerLogic:

                this.OfflineReason = "Disconnect by server.\nThe servers logic (application) disconnected this client for some reason.";

                break;

            case StatusCode.DisconnectByServerUserLimit:

                this.OfflineReason = "Server reached it's user limit.\nThe server is currently not accepting connections.\nThe license does not allow it.";

                break;

            default:

                this.DebugReturn("StatusCode not handled: " + statusCode);

                break;

        }

    }

    public virtual void EventAction(byte eventCode, Hashtable photonEvent)

    {

        this.DebugReturn(String.Format("EventAction: {0}", eventCode));

    }

}

 

포톤은 클라이언트와 서버간의 패킷을 3가지 Level로 나눠놨습니다. 그래서 각 레벨마다 중요도와 우선순위를 두어서 작동됩니다. 3가지 Level을 살펴보면 아래와 같습니다.

 

Low Level : 서비스, 연결, 끊김등과 같은 네트워크상태에 따른 서버와의 상태에 대한 Level입니다. 이 레벨은 UDP/TCP패킷을 모두 사용해서 명령을 전송합니다. 이것은 연결유지와 RPC 호출 등이 레벨에 속합니다.

 

Logic Level : Operation과 그 결과값, Event와 그 결과값등이 이 레벨에 속합니다. 포톤의 로직 처리에 필요한 레벨이며 Operation은 결과값(리턴값)이 있는 패킷이며 Event는 클라이언트의 특정 상태 업데이트등에 쓰이는 패킷 입니다.

 

Application Level : 특정 응용프로그램에 필요한 것과 특징적인 패킷들의 레벨입니다. 주로 Room이나 Actor들에 대한 패킷등을 이 레벨로 분류합니다.

 

반드시 Low Level이 항상 중요한 것만은 아닙니다만, Low Level 패킷 , Status Command등을 사용하면 내부적으로 연결을 유지시켜주고 패킷 손실이 일어났을 경우 재전송등이 이루어지게 되므로 중요한 패킷은 Low Level에서 이루어지는 것이 좋습니다.

 

모든 Operation 패킷(RPC호출) Prefix로써 Op가 붙습니다.

다른 서버에만 있는 함수를 호출할 경우 다른 파라메터와 다른 값을 돌려 받을수 있습니다. Operation 패킷은 클라이언트 라이브러리엔 포함되어 있진 않지만 OpCustom()함수를 호출해서 구현할 수 있습니다.

 

, IPhotonPeerListener 인터페이스들은 콜백들을 위하여 반드시 따로 구현을 해야합니다.

IPhotonPeerListener 인터페이스의 종류는 아래와 같습니다.

-       PeerStatusCallback() Peer 상태가 변할때(연결,끊김,에러,상태코드 변경) 사용되는 콜백 함수입니다.

-       OperationResult() Operation을 위한 콜백 함수입니다(Join,Leave,등등)

-       EventAction()은 이벤트를 위한 콜백 함수입니다.

-       DebugReturn()함수는 디버그 출력을 위한 콜백 함수입니다.

 

4.   포톤의 실제 서버 개발 후기

[그림]23. P 프로젝트의 Screen Shot

 

위의 스크린샷은 필자가 2010년 여름에 만든 게임 프로젝트로써 아이폰용 MMORPG게임프로젝트 입니다. 유니티엔진과 포톤서버를 사용해서 온라인게임을 제작했고 그 게임을 멀티 플레이 테스트하는 화면입니다.

 

비록 프로토타입 화면이지만 클라이언트와 서버를 모두 담당해서 개발했기 때문에 개발하면서 느낀점이나 노하우등을 소개해보려 합니다.

 

포톤으로의 서버 개발

C#으로 만든 서버?

포톤으로 서버를 개발하게 된다면 C#으로 프로그래밍을 하게 됩니다. 아마 기존에 서버프로그래밍을 전문적으로 담당하셨던 분이라면 C# 서버에 대해서 부정적인 생각이 있을 수 있습니다. C/C++보단 느리고 메모리 관련 문제나 패킷 최적화등을 어떻게 해결할 것인지, 수많은 데이터와 엄청나게 빠른 로직 처리를 해야 하는 게임 서버로썬 프로그래밍 언어에서 오는 네이티브 코드(C/C++)보다 느리다라는 제약까지 업고 개발하는 것은 어떻게 보면 서버프로그래밍의 기본 철칙을 어기는 것이라고 할 수 있기 때문입니다.

하지만 실제 개발을 해보니 C# 게임서버의 성능(Performance)은 서버시스템의 하드웨어의 많은 발전으로 인해 C/C++로 서버를 만들었을 때와 크게 다르지 않은 성능을 내는 것을 확인할 수 있었습니다. (포톤의 경우엔 소켓 통신 Core System C/C++로 구축이 되어 있습니다.)

그리고 성능이 약간 느리더라도 단점을 커버할 수 있는 많은 장점이 있었습니다.

장점 중 하나는 닷넷(.Net)이 제공하는 수많은 편리한 라이브러리를 가져다 쓸 수 있습니다. 쉽고 간단하면서도 빠른 DB 관리 라이브러리들과 LINQ등을 사용할 수 있습니다.

LINQ(Language Integrated Query )는 통합언어 쿼리로 .NET Framework 3.5에 포함되어 있습니다. LINQ to Object, LINQ to SQL, LINQ to XML로 크게 나눌 수 있는데 이는 데이터베이스, XML 등등 개체화하는 데이터 소스에 대해서 전반적으로 쉽게 질의 할 수 있게 제공합니다.

LINQ는 집계 연산을 수행하는 함수를 기본적으로 제공합니다. 기본함수에는 Count, Sum, Min/Max, Average, Aggregate 등등이 있습니다.

아무튼 이 LINQ를 이용해 게임 서버의 필수불가결인 수많은 게임데이터들의 Asset 파일인 XML, EXCEL 등의 파일에 쿼리문을 날려서 원하는 데이터만 추출, 저장, 가공이 쉽게 가능해 집니다. 이는 복잡한 게임 서버일수록 사용하기에 따라 엄청난 효용성을 가지고 있습니다.물론 Asset파일뿐만 아니라 DB SQL도 마찬가지입니다.

또 장점중 하나는 Window OS에서 제공하는 기능들을 쉽게 사용할 수 있다는 것입니다.

포톤의 예를 들면 아래와 같이 Window OS에서 제공하는 성능 모니터를 사용해서 서버 시스템의 성능에 대한 것을 자세히 쉽게 확인할 수 있습니다.

[그림] 24. 포톤의 성능모니터 어플을 실행

 

포톤 서버에서 성능 모니터를 실행시킨후

 

[그림] 25. 윈도우의 성능 모니터 실행 화면

 

위와 같이 현재 서버시스템의 성능을 쉽게 확인 할 수 있습니다.

 

이는 기존의 서버 프로그래밍에서 서버시스템의 운영툴이나 서버 시스템 관리툴을 별도로 제작하지 않아도 되는 이점이 있어, 서버 개발시에 많은 개발시간 단축이나 프로파일링을 통해 성능향상을 시킬 수도 있습니다.

이뿐만 아니라 .Net에서 제공하는 SMTP (simple mail transfer protocol)를 사용해서 서버 시스템의 문제가 있거나 특정 서버 시스템에 과부하가 걸렸을 경우 운영자의 메일이나 운영팀 메일로 현재 상황을 전송하여 원격으로 떨어져 있는 서버 운영팀들이 서버의 상태등을 쉽게 받아 볼수 있는 시스템을 쉽게 구축할 수 있습니다.

 

위와 같은 내용은 사실 C/C++서버에서도 불가능 한 것은 아니고 기존에 많은 사람들이 개발해 놓은 공개되어있는 좋은 라이브러리들을 가져다 써서 개발이 가능한 부분입니다, 하지만 그런 라이브러리들을 가져다 쓸 경우 기능 하나하나를 테스트 해야 하고 검증 단계를 거쳐야 하므로 쉽게 공짜로 된다고는 말할 수 없습니다. 그래서 .Net Framework의 기능들을 사용할 경우 이미 1억이 넘는 수많은 개발자들의 검증/수정 단계를 거친 라이브러리들이므로 최소한 커다란 문제는 없을 거라는 확신을 갖고 게임 서버 개발에 사용할 수 있습니다.

 

유니티와 포톤?

앞에 말씀 드렸다시피 포톤의 경우 C#으로 코딩을 하게 됩니다. 만약 유니티에서 C#스크립트를 사용하셨다면 클라이언트와 서버의 코드 공유가 가능하게 됩니다. 저 같은 경우에도 유니티에선 C#스크립트를 사용했고 서버의 로직의 경우 같은 언어(C#)를 사용함으로 인해 클라이언트와 서버에서의 똑같은 코드 공유가 가능했습니다. 뿐만 아니라 유니티의 네트워크뷰(NetworkView)와 포톤의 서버를 같이 사용했습니다. 같은 던전안에 있는 플레이어들끼리의 위치정보는 클라이언트들끼리의 P2P로써 네트워크뷰를 사용했고 만약 P2P연결이 불가능할시엔 포톤으로 만든 게임 서버를 통해 위치 정보를 공유했습니다. 아이템 구매나 캐릭터의 성장과 같은 중요한 데이터는 포톤서버를 사용했고 , 이벤트 연출이나 애니메이션 변경등은 유니티의 네트워크뷰를 사용했습니다. DB 읽고 쓰기는 포톤서버를 통해서, 이벤트 트리거나 RPC등은 유니티 네트워크뷰를 통해서 활용했습니다.

이렇게 개발을 해보니 생각보다 많은 게임서버의 개발시간을 단축할 수 있었습니다. 맨 처음 보여드렸던 스크린샷 이미지의 프로토타입의 클라이언트+서버 개발에 들어간 시간이 총 6주 입니다. 수정/변경 시간을 뺀 순수 개발시간은 3~4주 정도밖에 걸리지 않았다면 얼마나 많은 개발시간을 단축 할 수 있었는지 가늠하실 수 있을 거라 여깁니다.

 

결론

서두에 말씀 드렸다시피 향후 게임 개발에 있어서 네트워크와 멀티플레이는 필수불가결이 될 것입니다. 이는 수많은 콘솔,패키지 개발사들이 네트워크 시스템 개발에 관심을 갖게 된 배경이기도 합니다.

비록 이곳에선 리뷰와 사용법 소개 등에 그쳤지만 인터넷에 존재하는 수많은 정보와 소스코드들을 참고하신다면 좋은 네트워크 게임을 만드실 수 있을 거라 생각됩니다.

집필을 시작할때만 해도 유니티에서의 네트워크를 소개하고자 큰 포부를 갖고 시작했으나 책의 페이지 구성상 깊은 내용을 다룰 수 없는 점이 있었습니다. 그리고 지금 시점에도 빠르게 변화하고 있는 네트워크 기술들이다 보니, 세세한 설명을 할 수 없었습니다, 그래도 유니티에서 네트워크 프로그래밍을 처음 시작하는 사람들에게 미약하나마 도움이 되었으면 좋겠습니다.

 

주세영

babochuse@nate.com

 

참고 문서나 링크

http://unity3d.com/support/documentation/ScriptReference/index.html

http://www.smartfoxserver.com/

http://www.exitgames.com/

http://www.sqler.com/390183?WT.mc_id=soc-c-kr-loc-w-dw

http://100.naver.com/100.nhn?docid=719227