1. 본 방법론은 다른 자료나, 책, 강의를 가져다 디자인 것이 아니며, LiteNetLib의 기본 라이브러리 예제와 MessagePack 예제를 참고하여 제작중인 게임의 특징에 따라 여러차례 리팩토링을 거쳐 정착한 방식입니다. 특출나거나, 독자적인 방식이 아니며 상황에 다소 의존적일 수 있어 모든 상황에 알맞는 예제는 아님을 밝힙니다.
2. 강의 글이 아닌 기록 남김이라 코드는 이해를 돕기 위한 이미지만 첨부합니다.
오토 배틀 시뮬레이터는 UDP를 기반으로 한 네트워크 플레이를 기반으로 개발되었습니다. 게임은 데디케이트 서버에서 진행되며, 각 게임 진행상황과 데이터 변동을 패킷으로 묶어 처리합니다. 서버에서 클라이언트로 보내지는 모든 패킷은 헤더에 포함된 패킷 정보로 타입을 식별하고 데이터 영역을 클라이언트에서 사용할 수 있는 형태로 가공하며, 미리 배포된 스크립트에서 지정된 행동을 수행하도록 디자인 되었습니다.
패킷 디자인
예제 패킷인 PositionBroadcast는 어떤 Actor의 위치가 변경되었음을 연결된 모든 클라이언트에게 통지합니다. GAME_CLIENT 전처리기로 감싸진 Behavior 함수는 클라이언트의 패킷 핸들러에서 패킷의 최상위 클래스에서 호출되며, 패킷은 패킷 Serialization을 담당하는 MessagePack의 도우미 Attribute인 [Key]의 순서에 따라 Deserialize 되어 인스턴스 형태로 제공됩니다.
서버에서 클라이언트로 도착하는 패킷은 [Key]로 지정된 필드/프로퍼티의 값만 저장된 형태를 갖습니다. 생성자를 포함한 서버 내부적인 로직은 전처리기 GAME_SERVER로 감싸져 클라이언트 빌드에는 포함되지 않습니다.
패킷 타입 지정
패킷 타입을 서버와 클라이언트에서 각각 공유하여 확인할 수 있기 위해 선택한 방법은 Enum과 패킷 구조 하나를 대응시키는 것입니다. 패킷 정의 클래스에 [GamePacket] Attribute를 달아주면, 유니티의 Compilation Pipeline 타이밍의 콜백에서 리플렉션을 사용해 클래스에 대응하는 패킷을 생성합니다.
이 패킷 타입은 서버와 클라이언트의 스트림 핸들러에서 참조되어 패킷 클래스를 생성하는데 사용됩니다.
enum-Type, type-Enum 맵은 하나만 존재해도 무방하므로 싱글톤으로 처리되어 있습니다. 유니티 에디터에서의 원활한 작업 환경을 위해 라이프 사이클을 고려한 디자인이며, 스트림 핸들러에서 맵을 관리해도 되지만 MessagePack을 사용하지 않게 되거나 다른 패킷 뭉치를 사용하게 될 상황에 대비하여 분리되어 있습니다.
현재는 패킷 수가 늘어날 것을 대비하여 ulong 타입 으로 패킷 헤더에 쓰고있습니다. Enum을 꼭 사용하지 않아도 되고 ulong을 사용하여도 무방합니다. 프로덕션 단계에서는 옵션으로서 패킷 매칭 Enum이 클라이언트에 노출되지 않고 매직넘버로 동작하도록 설계할 수도 있습니다.
코드가 디스어셈블리 되지 않는다는 가정하에, 매 패치, 매 세션마다 간헐적으로 패킷 매직넘버 시드를 변경해 사용하여 패킷스니핑과 리버스 엔지니어링을 조금 번거롭게 조절할 수 있습니다.
패킷 자동 생성
리플렉션으로 Attribute를 받아온 다음, string으로 클래스 파일을 재작성합니다.
InitializeOnLoadMethod에 체인이 걸려있어, 코드가 수정되고 컴파일이 완료된 직후 리로드 과정에서 코드를 업데이트합니다. 이 부분은 Compilation Request 타이밍에 코드를 생성하도록 수정하여 한 번만 컴파일하도록 하면 되지 않냐는 지적이 있지만, 도메인 리로드를 마치지 않은 IL 코드는 리플렉션을 이용해 Attribute를 받아오더라도 업데이트 되지 않은 이전 어셈블리의 정보를 가져오게 되므로 주의해야 합니다.
정리
1. 패킷 구조 작성, 동작 정의
2. 컴파일타임에서 패킷에 대응하는 Enum 생성
3. 패킷 헤더에서 힌트로 사용