Featured Project
PrivateServer
Windows IOCP 기반 NetworkRuntime과 서버 권위형 World를 설계하고, C ABI를 거쳐 Godot thin client까지 연결한 실시간 게임 서버 프로젝트입니다.
Role
개인 설계·구현·검증
Network
C++ / Windows IOCP
World
Fixed-step / AOI / Channel
Client
C ABI / C# / Godot
클라이언트의 입력이 서버에 도착해 서버에서 판단할 수 있는 정규 상태로 확정되고 다시 클라이언트에게 전달되는 전체 게임 사이클을 구현했습니다.
이를 위해 연결별 I/O 상태와 수명을 소유하는 Session Actor, deterministic한 tick 경계를 만드는 fixed-step World, 관심 영역에 따라 결과를 배포하는 AOI, 그리고 네이티브 객체의 수명을 안전하게 감싸는 C ABI와 Godot thin client를 하나의 흐름으로 구성했습니다.
End-to-end Game Cycle
접속부터 라운드 결과까지 실제 클라이언트로 확인할 수 있게 구성했습니다. 아래 이미지는 구현 과정에서 사용한 게임 화면입니다.
%%{init: {"flowchart": {"useMaxWidth": false}}}%%
flowchart TB
Select["Channel 선택 · 연결"] --> Role{"참여 방식"}
Role -->|"Player"| Play["Join<br/>입력 · prediction"]
Role -->|"Observer"| Watch["Observe<br/>World overview 관측"]
Play --> Result["RoundResult<br/>결과 화면 보존"]
Watch --> Result
Result --> Cleanup["연결 종료<br/>session 정리"]
Cleanup -->|"사용자가 복귀 선택"| Select
1. Channel 선택
클라이언트는 자신이 접속할 World Host를 선택합니다.
2. Player 설정
닉네임을 입력하고 CONNECT를 요청합니다. 서버가 발급한 player identity와 함께 World에 진입합니다.
3. 서버 권위형 Gameplay
클라이언트는 입력을 전송하고 예측해 표현하지만, 이동과 충돌·라운드 결과는 서버가 판정합니다.
4. Observer 관측
시뮬레이션 대상에는 포함되지 않는 observer가 라운드를 관측할 수 있습니다.
5. Round 결과
라운드 종료 시 최종 순위와 승자를 표시하고, 로비로 돌아가기 전까지 종료 상태를 유지합니다.
System Architecture
서버는 네트워크 수신과 gameplay 판정을 분리하고, 클라이언트는 서버 결과를 소비하는 표현 계층으로 한정했습니다.
flowchart LR A["Godot input / presentation"] --> B["C# managed layer"] B --> C["C ABI boundary"] C --> D["C++ IOCP NetworkRuntime"] D --> E["Session Actor"] E --> F["World ingress"] F --> G["Fixed-step authoritative World"] G --> H["AOI replication"] H --> D D --> C C --> B B --> I["Prediction / correction / UI"]
NetworkRuntime: 연결 상태와 비동기 I/O 수명 관리
IOCP worker와 연결된 클라이언트의 상태를 변경하는 Actor Worker를 구분했습니다. 연결된 클라이언트 세션 정보는 Session Registry에서 관리되며, 각 Session에는 Actor Worker만 접근할 수 있어 클라이언트별 상태는 Actor Worker 내부에서 직렬화됩니다.
- Session Actor가 recv/send queue와 close state를 단일 소유
- completion은 session을 직접 수정하지 않고 Actor Mailbox 로 전달
- pending I/O가 없는 경우에 최종 세션 정리
%%{init: {"flowchart": {"useMaxWidth": false}}}%%
flowchart TB
Io["IOCP completion"] -->|"event 변환"| Mailbox["Session mailbox"]
Mailbox -->|"실행 예약"| Executor["Schedule gate + executor"]
Executor -->|"단일 drain 권한"| Actor["Session actor"]
Registry["Actor registry"] -.->|"객체 수명 소유"| Actor
Actor -.->|"소유"| Session["Socket · recv buffer"]
Actor -.->|"completion까지 보유"| Pending["Pending I/O context<br/>OVERLAPPED · payload"] World Server: 60Hz 고정 tick 기반의 권위 서버 모델
NetworkRuntime을 통해 전달된 WorldEvent는 World 서버의 상태에 즉시 반영하지 않습니다. ingress write slot에 이벤트를 모은 뒤, tick이 시작될 때 이를 read only buffer로 사용합니다. 시뮬레이션 단계에서는 계산을 진행한 뒤, 그 결과를 바로 상태에 반영하는 것이 아니라 typed result를 만들어 Coordinator가 정한 순서대로 commit을 하여 상태에 반영합니다.
flowchart LR A["NetworkRuntime events"] --> B["Ingress write slot"] B --> C["Sealed tick input"] C --> D["World Coordinator"] D --> E["Read-only calculation"] E --> F["Typed result"] F --> G["Canonical commit"] G --> H["AOI recipients"] H --> I["Outbound publisher"]
Game Client: 네이티브 경계와 서버 결과 표현
Godot client는 C# binding이 C ABI를 통해 C++ NetworkRuntime을 호출하고, SafeHandle이 native handle 수명을 소유하도록 구성했습니다. 플레이어 자신의 이동은 예측을 통해 즉시 화면에 반영하되 정확한 위치는 서버가 보낸 snapshot으로 보정하며, 다른 플레이어의 이동은 몇 tick 전의 데이터를 기반으로 interpolation/extrapolaion 처리를 진행합니다.
2 채널, 200개 클라이언트 작동 테스트
단일 채널에서 100개 클라이언트를 총 2개의 채널에서 작동하는 것을 확인했습니다.
- 단일 채널의 100개 클라이언트 모두 connected / joined / round completed 정상 완료
- 독립된 2개의 채널에서 총 200개 클라이언트의 테스트 정상 완료
- 테스트 진행 중 서버 라이프사이클 오류 0
%%{init: {"flowchart": {"useMaxWidth": false}}}%%
flowchart TB
Launcher["Local fleet launcher"] -->|"Channel별 실행"| Controller["Benchmark Controller"]
Controller -->|"시작 · 준비 확인 · 종료"| Host["World Host process"]
Controller --> Workload["Virtual client workload"]
Workload <-->|"TCP gameplay"| Host
Launcher -.->|"선택적 실행"| Observer["Godot Observer"]
Host -->|"WorldOverview"| Observer 현재 범위와 한계
- 클라이언트와 서버, 채널 목록 등은 로컬 구성으로 관리합니다.
- Godot client는 서버 동작을 보여주는 thin client이며 gameplay authority를 갖지 않습니다.
- 2 × 100 결과는 구성한 workload의 실행 증거이며 최대 수용량을 의미하지 않습니다.
- 테스트는 같은 디바이스에서 서버와 클라이언트를 실행해 진행했으므로, 로직이 정상적으로 작동하는지 검증하는데 더 큰 의의를 가집니다.