본문으로 건너뛰기

Featured Project

PrivateServer

Windows IOCP 기반 NetworkRuntime과 서버 권위형 World를 설계하고, C ABI를 거쳐 Godot thin client까지 연결한 실시간 게임 서버 프로젝트입니다.

C++Windows IOCPServer AuthorityFixed-step WorldGodot

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
Player와 Observer가 각자의 참여 경로를 거쳐 결과를 확인하고, 연결 정리 뒤 Channel 선택으로 복귀하는 흐름
PrivateServer channel selection screen

1. Channel 선택

클라이언트는 자신이 접속할 World Host를 선택합니다.

PrivateServer player nickname setup screen

2. Player 설정

닉네임을 입력하고 CONNECT를 요청합니다. 서버가 발급한 player identity와 함께 World에 진입합니다.

PrivateServer server-authoritative gameplay screen

3. 서버 권위형 Gameplay

클라이언트는 입력을 전송하고 예측해 표현하지만, 이동과 충돌·라운드 결과는 서버가 판정합니다.

PrivateServer observer view of a running round

4. Observer 관측

시뮬레이션 대상에는 포함되지 않는 observer가 라운드를 관측할 수 있습니다.

PrivateServer round result screen

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"]
입력 전송부터 서버 판정, AOI 복제와 클라이언트 표현까지의 전체 경로

NetworkRuntime: 연결 상태와 비동기 I/O 수명 관리

IOCP worker와 연결된 클라이언트의 상태를 변경하는 Actor Worker를 구분했습니다. 연결된 클라이언트 세션 정보는 Session Registry에서 관리되며, 각 Session에는 Actor Worker만 접근할 수 있어 클라이언트별 상태는 Actor Worker 내부에서 직렬화됩니다.

%%{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"]
실선은 completion 처리 흐름, 점선은 actor 객체와 I/O 자원의 수명 소유 관계

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"]
비동기 입력을 고정 tick 경계에서 소비하고, 계산과 commit을 분리한 World pipeline

Game Client: 네이티브 경계와 서버 결과 표현

Godot client는 C# binding이 C ABI를 통해 C++ NetworkRuntime을 호출하고, SafeHandle이 native handle 수명을 소유하도록 구성했습니다. 플레이어 자신의 이동은 예측을 통해 즉시 화면에 반영하되 정확한 위치는 서버가 보낸 snapshot으로 보정하며, 다른 플레이어의 이동은 몇 tick 전의 데이터를 기반으로 interpolation/extrapolaion 처리를 진행합니다.

2 채널, 200개 클라이언트 작동 테스트

단일 채널에서 100개 클라이언트를 총 2개의 채널에서 작동하는 것을 확인했습니다.

%%{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
Channel별 Controller가 Host와 workload를 관리하고, 선택적으로 실행한 Observer가 서버 상태를 관측하는 구성
두 Channel observer 화면을 함께 녹화한 full-round fleet 실행 영상

현재 범위와 한계