유니티 네트워크 프로그래밍
유니티 네트워크 프로그래밍(가와다 마사토시 / 길벗)
1.1 온라인 게임 제작의 실제
고도의 기술이 필요한 것은 아니다.(이미 오래된 기술들, 통신 등)
게임 특유의 노하우와 테크닉이 필요하다.
구체적이고 세부적인 사항이 명확하지 않은점이 어려운 부분
ex) '어떤 정보를 보내면 되는가', '언제 어느 정도의 빈도로 보내야 하는가'
오프라인 모드와 온라인 모드는 다르다.(게임 디자인이 다르다 = 다른 게임)
통신 지연
지연은 반드시 발생한다.
송신한 쪽과 수신한 쪽의 정보가 같은 프레임에서 처리되지 않는다는 사실을 전제로 처리해야함.
동기 방식 VS 비동기 방식
동기방식
턴 동기 VS 키 입력 동기
고려해야 할 통신 제약
통신 지연(데이터가 도착 할 때까지 어떻게 할 것인가...)
데이터 소실(어떻게 재전송 할 것인가 재전송까지 어떻게 할 것인가...)
회선 끊김(남은 플레이어로 어떻게 할것인가...)
통신구조
게임에 필요한 기본적인 통신 지식 정도만!
데이터는 패킷을 통해 전달된다. IP주소:포트번호를 목적지로해서.
패킷은 유실되기도 한다(기기 고장, 처리속도 초과)
패킷 유실의 대처 방안. 프로토콜
프로토콜 TCP와 UDP
TCP
전송과 순서를 보증함. 혼잡을 제어함
접속 처리 -> 확인 응답(ACK) -> 데이터 전달(송신 요구) -> 확인응답
ACK
수신 버퍼의 크기가 들어있어 보낼 데이터의 크기가 버퍼보다 작으면 전송 시작
애플리케이션, 데이터스트림, 버퍼, 헤더+세그먼트
버퍼
데이터를 임시보관(작은 데이터를 모으거나 큰 데이터의 일부를 보관)
UDP
전송과 순서를 보증 안함, 빠르다.
데이터 전달(송신 요구) -> 데이터 전달(송신 요구)
RUDP
지연 시간(레이턴시)
통신을 통해 상대의 입력 정보를 반영하려먼 적어도 편도 레이턴시만큼은 반드시 느려진다.
게임 디자인과 구조를 연구하여 플레이어가 이런 레이턴시를 느끼지 못하도록 해야 한다.
통신대역(bps)을 고려하자
소켓 : TCP와 UDP를 간단하게 다루기 위한 통신 API
TCP의 소켓 통신
서버와 클라이언트
서버는 리스닝 소켓을 미리 만들어 대기 시켜 클라이언트가 언제라도 접속 요청을 할 수 있도록한다. IP:Port
Listen으로 대기
Accept로 클라이언트의 요청을 받음(블로킹), Accept는 클라이언트와 통신하기 위한 소켓을 반환함
Poll로 데이터를 수신했을때만 Asscept를 호출하게함
전용서버 서버로만 동작하는 게임 인스턴스
호스트서버 전용 서버가 없는 경우 클라이언트 중 하나가 서버 역할을 맡을 수도 있습니다. 이 클라이언트가 “호스트 서버”입니다.
클라이언트는
소켓.Connect(아이피, 포트)로 서버에 접속을 요청한다(블록킹, SocketException)
소켓.Send로 수신 단말의 버퍼로 데이터를 보낼 수 있고
소켓.Receive로 버퍼에 저장된 데이터를 가져올 수 있다.
적절히 리시브를 사용하지않으면 버퍼가 가득찬다.
Shutdown과 Close로 통신 종료
UDP의 소켓 통신
서버, 클라이언트의 구별이 없다. 대기가 없다.
소켓 생성(아이피,포트)만으로 통신 상태가 된다.
SendTo와 ReceiveForm으로 데이터를 주고 받음
바이트 오더 : 2Byte 이상의 데이터를 메모리에 배치하는 방식.
빅 엔디언(big endian) : 상위바이트 부터
리틀 엔디언(little endian) : 하위바이트 부터
엔디언은 프로세서에 따라 달라진다. 이를 고려해야한다
HostToNetworkOrder, NetworkToHostOrder 를 사용해
소스트 바이트 오더 -> 네트워크 바이트 오더 -> 호스트 바이트 오더
통신 라이브러리를 만들자
게임 프로그램 전체로 보면 Soket 클래스의 인스턴스는 게임 처리와 별도로 관리하고싶은 부분, 라이브러리로 만들어 놓으면 재사용이 가능하고 버그도 줄일 수 있다.
라이브러리를 이용해 Socket을 숨기면 게임 프로그램은 접속이나 오류를 검출 할 수 없다. 이벤트 처리(델리게이트 활용)를 통해 게임 프로그램에 알려야한다.
스레드를 사용하자
게임 애플리케이션의 일부로 통신을 처리하면 게임 자체를 처리하는데 부하가 걸립니다.(혹은 통신을 처리하는데 부하가 걸립니다.-> 버퍼 넘침, 패킷 유실)
수신한 데이터는 큐를 이용해 관리해용. 두 스레드가 공유할 큐. 패킷 큐
패킷 단위로 큐에 넣고 빼면 편합니다. 왜냐 통신에서는 패킷단위로 데이터르 송/수신합니다.
MemoryStream. MemoryStream은데이터를 끊김없이 추가할 수 있는 스트림 버퍼로 통신 데이터를 간단하게 다룰 수 있어 편합니다.
스레드는 자원을 공유해용
멀티 스레드를 사용할때는 배타적으로 자원을 이용해야합니다. 락(lock)을 통해 배타 제어(exclusive control)를 합시다. 락을 많이 사용하면 타이밍이 서로 충돌했을 때 한쪽이 정지됩니다. 최소한으로 사용합시다.
통신스레드
메인스레드의 송신 데이터 등록 -> 데이터 송신(Socket.Send) -> 데이터 수신(버퍼에서 메인 스레드로 주고받는 큐로 옮김, Socket.Receive)
이 과정을 반복합니다. 연속 실행하면 다른 스레드로 처리 제어가 돌아가지 않으므로 일정간격으로(예제는 5ms) 실행합니다.
상대가 모르는 정보를 적절한 타이밍에 서로 주고받는다.
상대가 알고 있는 정보를 보낼 필요는 없다. -> 이미 알고 있는 정보, 계산으로 구할 수 있는 정보
액션 게임에서의 결과와 표현이 어긋나는 현상
서로 근소한 차이로 입력한 경우 발생합니다.
통신 지연을 어느정도까지 허용할지 정해야합니다. RTT?
리얼타임을 통한 측정!
키입력 동기 vs 비동기
키입력 동기
핵심 : 로컬 단말과 리모트 단말의 키 입력 정보를 프레임별로 일치시키는 것(프레임 동기, 리모트 단말과의 키 입력 정보 송수신)
키입력동기는 단말의 수가 적고 통신지연이 짧을때 유용
통신 지연을 고려 : 60FPS라고 할 때 16.666...ms 내에 송신해야 처리 가능. 일본의 통신 지연의 100ms 정도 라고합니다.(2015년)
키 입력 지연으로 완화가 가능하지만 조작이 어색할 수 있다.
키입력을 받아오지 못하면 아무것도 할 수 없음. -> 대기하다 종료 ( Time.TimeScale = 0;)
UDP 키 입력 동기
비 연결형 통신이므로 갑자기 데이터를 보낼 수 있다(키 입력 정보와 몇 프레임인지).
갑자기 보낸 데이터를 처리할 수 있을지 모른다 -> 중복 데이터 재송신으로 완화
패킷과 시리얼라이저
Send의 잦은 호출은 부하를 일이키므로 시리얼 라이저를 통해 구조체 등을 바이트화해서 한 패킷에 실어보낸다.
BitConverter : 기본형을 byte[] 형으로
엔디언 판정
int i = 1; //00 00 00 01 or 10 00 00 00
byte[] check = BitConverter.GetBytes(i);
최상위 비트(check[0])가 0이면 빅 엔디언, 1이면 리틀 엔디언
접속 종료 플래그로 게임이 부주의하게 멈추는 일을 방지합시다
온라인 게임에서는 가능한 한 데이터를 빠르게 송수신하여 응답 속도를 높여야 하므로 UDP로 통신합니다. 그래서 패킷 유실 대책으로 데이터를 중복해서 송신합니다. 데이터가 중복되면 송수신할 데이터가 늘어나므로 시리얼라이저를 사용합니다. 시리얼라이저로 송수신할 데이터의 시리얼라이즈, 디시리얼라이즈, 바이트 오더 변환을 간단히 할 수 있습니다.
온라인 게임을 디버깅할 때 디버거의 중단점이 제 기능을 하지 못할 때가 있습니다. 이와 같은 온라인 게임 고유의 문제는 지연 에뮬레이터, 로그 출력, 패킷 캡처 툴 같은 도구를 이용해서 대처할 수 있습니다.
캐릭터의 이동(위치)은 완전히 일치하지 않는것을 전제로!
위치좌표를 매 프레임 송수신해도 캐릭터 위치는 어긋난다.(통신지연)
어차피 어긋나면 프레임 추출과 보간을 이용해 통신량을 줄이자.
보간
선형 보간(Linear Interpolation)
스플라인 보간(Spline Interpolation)
라그랑주 보간(Lagrange Interpolation)
에르미트 보간(Hermite Interpolation)
외관의 불일치와 타협하면서 게임성을 검토하는 것도 중요한 게임 디자인이다.
페킷 헤더
매칭 서버
호스트 게스트
방에 참가한 단말의 접속이 끊겼다
방을 만든 단말의 접속이 끊겼다
참가하려던 방이 게임을 시작해버렸다.
매칭이 완료되고 게임을 시작하기 전에 멤버와 접속이 귾기거나 연결할수없다.
네트워크 토폴로지 : 컴퓨터나 통신 기기의 접속 형태
스타형
서버/클라이언트 방식의 접속 형태. 서버에서 처리된 데이터를 클라이언트에 배포하는 경우에 이용
장점
정보가 서버에 집중되므로 단말 간의 일관성을 유지하기 쉽다.
단점
반드시 서버를 경유하므로 통신 지연이 발생한다.
서버의 통신 부하가 크다.
서버와 접속이 끊기면 모든 통신이 두절된다.
메시형
각 노드가 하나 이상의 노드와 연결되는 접속 형태. 서버가 없기 때문에 단말 간의 정보를 순차적으로 송신해야 합니다. 경로가 하나 끊어져도 다른 경로로 계속 접속할 수 있으므로 접속성을 확보하기 쉽습니다.
장점
단절에 대한 내성이 강하다.
단점
멀리 떨어진 단말의 통신 지연이 길다.
풀 메시형
모든 노드가 서로 접속하는 형태. 서버/클라이언트 방식과는 반대로 서로 경로가 확보되어 있어야 합니다. 즉시성이 필요한 통신에는 적합하지만, 통신할 상대가 많은 통신에는 적합하지 않습니다.
서버를 거치지 않고 각 노드와 직접 접속하므로 데이터를 즉시 반영하고 조정이 필요 없는 통신에 어울립니다. 캐릭터 이동에 사용되는 토폴로지입니다.
장점
단절에 대한 내성이 강하다.
모든 단말과 동시에 통신할 수 있다.
단점
같은 정보를 여러 단말에 송신하므로 통신량이 증가한다.
접속할 단말의 관리가 복잡해진다.
매칭 시간과 로딩 시간 트릭
게임을 시작하겠다 라고 결정한 뒤 기다리는 시간
매칭에 필요한 시간 + 송수신 시간
매칭 서버의 세션 관리 ID != 플레이어 ID
난수의 씨앗 seed
난수를 발생시킬때는 시드를 통일하자
캐릭터의 공격
로컬 단말에서만 공격을 연산하고 리모트 단말에서는 모션만! 안그러면 데미지가 여러차례 연산 될 수 있다.
몬스터의 남은 HP 정보가 아닌 대미지를 보내자
데미지를 받는건 로컬 단말 뿐! 그리고 통보하기!?