Search

Unity2dMmorpg | 패킷 코드 자동 생성 스크립트 제작

Date
2025/05/30
category
Unity2dMmorpg
Tags
unity-2d-mmorpg
project
MmorpgServer
nansu0425

1. 패킷 코드 자동 생성의 필요성

현재 제 프로젝트에선 패킷 직렬화 라이브러리로 protobuf를 채택했습니다. 사실 처음엔 flatbuffers를 선택했었는데, 조금 사용해보니 작성해야 할 코드량이 많고 사용법이 조금 까다로웠습니다. 또한 visual studio의 syntax highliting 지원이 없었고, 자료도 많아 보이지 않았습니다. 단순히 속도 때문에 도입하긴 했는데 mmorpg 서버에선 protobuf를 많이 쓰는 것 같아서 기존 flatbuffers 코드를 걷어내고 protobuf로 바꿨습니다(바꾸길 정말 잘 했다는 생각이 들었습니다). 패킷의 payload 구조를 proto 파일에 정의하는 방식으로 사용하고 있습니다. proto 파일 변경 사항이 생길 때마다 패킷 관련 코드를 수정해야 하는데, 이 과정이 실수가 생길 여지가 굉장히 크다고 느꼈습니다. 일관성 유지 및 생산성을 위해 proto 파일 정의에 맞는 패킷 코드를 자동으로 생성해주는 스크립트를 만들기로 했습니다. 스크립트 제작 과정에서 다음 강의를 참고했습니다.

2. PacketGenerator

Tools 솔루션에 PacketGenerator 파이썬 프로젝트를 만들었습니다. 이 프로젝트의 스크립트와 템플릿을 이용해서 패킷 관련 코드를 자동 생성합니다. PacketGnerator.py 실행 시 핵심 동작은 다음과 같습니다.
1.
proto 파일들이 존재하는 디렉토리를 구합니다.
2.
그 디렉토리에서 패킷 코드 생성에 필요한 proto 파일들을 구분합니다.
3.
ProtoParser가 key는 proto 파일 이름, value는 패킷 데이터 리스트인 패킷 딕셔너리를 만듭니다.
4.
jinja2 템플릿 엔진과 패킷 딕셔너리를 활용해서 Templates의 패킷 코드가 필요한 모든 프로젝트에 패킷 코드를 생성합니다.

3. GenPacket.bat

PacketGenerator는 패킷 코드 템플릿을 이용해서 패킷 코드를 생성하는 역할을 하는데, 저기서 하지 않는 것이 있습니다. proto 파일을 protoc.exe로 컴파일하는 부분이 없습니다. GenPacket.bat은 proto 파일의 소스 코드 생성과 PacketGenerator 실행까지 포함된 윈도우 배치 파일 스크립트입니다. 즉, 이 GenPacket.bat만 실행하면 proto 파일에 정의된 내용을 반영해야 하는 모든 패킷 코드를 생성 합니다. 이것도 Tools에 만들었습니다. 아래는 현재 사용 중인 GenPackete.bat의 스크립트 입니다.
@echo off REM -------------------------------------------------- REM GenPacket.bat REM - 정의된 Protocol을 바탕으로 Packet 관련 코드를 자동 생성 REM -------------------------------------------------- REM 현재 스크립트 위치 set SCRIPT_DIR=%~dp0 REM repository의 루트 디렉터리 for %%I in ("%SCRIPT_DIR%..") do set ROOT_DIR=%%~fI REM protoc.exe 경로 set PROTOC=%ROOT_DIR%\Server\vcpkg_installed\x64-windows-static\tools\protobuf\protoc.exe REM .proto 파일들이 있는 디렉터리 set PROTO_DIR=%ROOT_DIR%\Shared\Protocol REM 임시 출력 디렉터리 set OUT_DIR=%SCRIPT_DIR%Build REM 최종 복사 대상 디렉터리 set COPY_DIRS="%ROOT_DIR%\Server\Protocol\Payload" REM 1) 출력 폴더 초기화 if exist "%OUT_DIR%" ( rd /s /q "%OUT_DIR%" ) mkdir "%OUT_DIR%" echo ============================================ echo Generating C++ sources from .proto files... echo Proto dir : %PROTO_DIR% echo Temp out : %OUT_DIR% echo Targets : %COPY_DIRS% echo Using protoc: %PROTOC% echo ============================================ REM 2) .proto→CPP 코드 생성 for /R "%PROTO_DIR%" %%F in (*.proto) do ( echo [PROTO] Compiling %%~fF "%PROTOC%" -I="%PROTO_DIR%" --cpp_out="%OUT_DIR%" "%%~fF" if errorlevel 1 ( echo [ERROR] Failed to compile %%~fF exit /b 1 ) ) REM 3) 생성된 파일을 각 복사 대상에 배포 for %%D in (%COPY_DIRS%) do ( echo [COPY] -> %%~D xcopy "%OUT_DIR%\*" "%%~D\" /E /Y /I if errorlevel 1 ( echo [WARNING] Copy to %%~D may have failed. ) ) REM 4) 임시 출력 폴더 정리 echo [CLEAN] Removing temp dir %OUT_DIR% rd /s /q "%OUT_DIR%" REM 5) PacketGenerator.py 실행 echo ============================================ echo Generating packet code... echo ============================================ set PACKET_GEN_DIR=%SCRIPT_DIR%PacketGenerator cd /d "%PACKET_GEN_DIR%" python PacketGenerator.py if errorlevel 1 ( echo [ERROR] Failed to run PacketGenerator.py exit /b 1 ) echo [SUCCESS] Packet code generation completed. echo ============================================ echo All packet generation tasks completed successfully. echo ============================================
PowerShell
복사

4. 자동 생성 예시

syntax = "proto3"; import "Common.proto"; package proto; message ClientToWorld_EnterRoom { int64 id = 1; string password = 2; } message ClientToWorld_Chat { int64 id = 1; string message = 2; }
Protobuf
복사
ToWorld.proto
현재 proto 파일은 세 개(Common.proto, ToClient.proto, ToWorld.proto)가 있는데, 어떤 식으로 패킷 코드가 생성되는지 설명하기 위해 ToWorld.proto를 예시로 들겠습니다. 이 proto 파일은 클라이언트에서 World 서버로 전송하는 패킷의 payload 구조를 정의합니다. 이 proto 파일과 관련된 패킷 코드를 자동 생성하기 위해 GenPacket.bat를 실행했을 때 생성된 코드의 예시는 아래와 같습니다.

4.1. proto → 소스 코드

Protocol 프로젝트의 Payload 폴더에 ToWorld.proto의 컴파일된 소스 코드가 생성됩니다.

4.2. Id.h

///////// 생략 ///////// enum class PacketId : Int16 { Invalid = 0, {%- for proto_file, packets in proto_parser.packet_dict.items() %} {%- for packet in packets %} {{ packet.payload_type }} = {{ packet.packet_id }}, {%- endfor %} {%- endfor %} }; ///////// 생략 /////////
C++
복사
Id.h 템플릿 일부
///////// 생략 ///////// enum class PacketId : Int16 { Invalid = 0, WorldToClient_EnterRoom = 1000, WorldToClient_Chat = 1001, ClientToWorld_EnterRoom = 1002, ClientToWorld_Chat = 1003, }; ///////// 생략 /////////
C++
복사
생성된 Id.h 일부
ToWorld.proto에 정의된 payload 타입들이 패킷 id가 부여된 형태로 PacketId에 추가됩니다.

4.3. Dispatcher.h

///////// 생략 ///////// #include "Protocol/Packet/Id.h" {%- for proto_file, packets in proto_parser.packet_dict.items() %} #include "Protocol/Payload/{{ proto_file }}.pb.h" {%- endfor %} ///////// 생략 /////////
C++
복사
Dispatcher.h 템플릿 일부
///////// 생략 ///////// #include "Protocol/Packet/Id.h" #include "Protocol/Payload/ToClient.pb.h" #include "Protocol/Payload/ToWorld.pb.h" ///////// 생략 /////////
C++
복사
생성된 Dispatcher.h 일부
ToWrold.proto를 컴파일하여 생성된 ToWorld.ph.h 헤더 파일을 포함하는 #include 문을 추가합니다.

4.4. Sender.h

///////// 생략 ///////// // payload 타입별로 Send 함수를 오버로딩 {%- for proto_file, packets in proto_parser.packet_dict.items() %} {%- for packet in packets %} static void Send(const SharedPtr<core::Session>& target, const {{ packet.payload_type }}& payload) { Send(target, payload, PacketId::{{ packet.payload_type }}); } {%- endfor %} {%- endfor %} ///////// 생략 /////////
C++
복사
Sender.h 템플릿 일부
///////// 생략 ///////// // payload 타입별로 Send 함수를 오버로딩 static void Send(const SharedPtr<core::Session>& target, const WorldToClient_EnterRoom& payload) { Send(target, payload, PacketId::WorldToClient_EnterRoom); } static void Send(const SharedPtr<core::Session>& target, const WorldToClient_Chat& payload) { Send(target, payload, PacketId::WorldToClient_Chat); } static void Send(const SharedPtr<core::Session>& target, const ClientToWorld_EnterRoom& payload) { Send(target, payload, PacketId::ClientToWorld_EnterRoom); } static void Send(const SharedPtr<core::Session>& target, const ClientToWorld_Chat& payload) { Send(target, payload, PacketId::ClientToWorld_Chat); } ///////// 생략 /////////
C++
복사
생성된 Sender.h 일부
ToWorld.proto에 정의된 payload 타입 별 Send 함수를 추가합니다. 각 Send 함수는 패킷 id를 인자로 받는 Send 함수 템플릿을 호출하는데, 자동화 코드를 생성하면서 패킷 id 부분을 payload 타입에 맞게 설정합니다. 따라서 패킷 전송 시 패킷 id를 수동으로 설정할 필요가 없고 payload만 전달하면 됩니다.

4.5. Handler.h

/* {{ project_name }}/Packet/Handler.h */ #pragma once #include "Protocol/Packet/Dispatcher.h" namespace {{ project_namespace }} { class {{ proto_file }}_PacketHandler : public proto::PacketDispatcher { public: static {{ proto_file }}_PacketHandler& GetInstance() { static {{ proto_file }}_PacketHandler sInstance; return sInstance; } protected: // 모든 패킷 핸들러 등록 {{ proto_file }}_PacketHandler() { RegisterAllHandlers(); } virtual void RegisterAllHandlers() override { using namespace proto; {% for packet in proto_parser.packet_dict[proto_file] %} RegisterHandler<{{ packet.payload_type }}>(&Handle_{{ packet.payload_type }}, PacketId::{{ packet.payload_type }}); {%- endfor %} } private: // 모든 페이로드 핸들러 {%- for packet in proto_parser.packet_dict[proto_file] %} static Bool Handle_{{ packet.payload_type }}(const SharedPtr<core::Session>& owner, const proto::{{ packet.payload_type }}& payload); {%- endfor %} }; } // namespace {{ project_namespace }}
C++
복사
Handler.h 템플릿
/* WorldServer/Packet/Handler.h */ #pragma once #include "Protocol/Packet/Dispatcher.h" namespace world { class ToWorld_PacketHandler : public proto::PacketDispatcher { public: static ToWorld_PacketHandler& GetInstance() { static ToWorld_PacketHandler sInstance; return sInstance; } protected: // 모든 패킷 핸들러 등록 ToWorld_PacketHandler() { RegisterAllHandlers(); } virtual void RegisterAllHandlers() override { using namespace proto; RegisterHandler<ClientToWorld_EnterRoom>(&Handle_ClientToWorld_EnterRoom, PacketId::ClientToWorld_EnterRoom); RegisterHandler<ClientToWorld_Chat>(&Handle_ClientToWorld_Chat, PacketId::ClientToWorld_Chat); } private: // 모든 페이로드 핸들러 static Bool Handle_ClientToWorld_EnterRoom(const SharedPtr<core::Session>& owner, const proto::ClientToWorld_EnterRoom& payload); static Bool Handle_ClientToWorld_Chat(const SharedPtr<core::Session>& owner, const proto::ClientToWorld_Chat& payload); }; } // namespace world
C++
복사
생성된 Handler.h
WorldServer에 ToWorld.proto에 정의된 payload를 처리하는 핸들러 클래스를 생성합니다. 핸들러 클래스가 헤더 파일에 생성되면, cpp에 각 핸들러의 로직만 직접 작성하면 됩니다.