[UE5] 추출 슈터 개발 기록: 전체 목차
카테고리: DevLog
태그: C++, Multiplayer, UE5
📌 이 시리즈의 표지 겸 목차입니다. 각 편으로 가는 링크는 아래 연재 목록에 있습니다. 완성된 시스템만 보려면 프로젝트 소개 페이지가 더 빠릅니다. 👾 깃허브 · 📋 기획
EmploymentProj
UE5 C++ 멀티플레이어 익스트랙션 슈터. 템플릿·블루프린트 없이 C++로 바닥부터 설계했다.
가장 어려웠던 것: 서버 사이드 리와인드
먼저 이것부터 보여주는 게 맞을 것 같다. 이 프로젝트에서 제일 오래 붙잡았고, 제일 많이 배운 부분이다.
문제: 랙 보상을 붙였는데 보이는 위치가 아니라 그보다 살짝 뒤를 쏴야 맞았다. 네트워크 상태와 무관하게 항상 정확히 한 틱만큼 빗나갔다.
원인: 스냅샷의 시각은 CMC::OnMovementUpdated(TickDispatch)에서 찍고,
본 Transform은 같은 틱의 TG_PostPhysics에서 읽고 있었다.
그런데 엔진의 UWorld::Tick은 TickDispatch를 먼저 돌리고 그 다음에 월드 시간을 전진시킨다.
// LevelTick.cpp: UWorld::Tick
1545: BroadcastTickDispatch(DeltaSeconds); // ServerMove RPC 처리 (여기서 시각을 읽었다)
1581: TimeSeconds += DeltaSeconds; // 월드 시간은 '그 뒤'에 전진
1721: RunTickGroup(TG_PrePhysics); // 틱 그룹은 '그 다음'
즉 같은 틱 안에서 읽었는데도 두 값이 한 프레임 어긋나 있었다. 시각과 위치를 같은 델리게이트로 함께 넘기도록 고쳐서 해결했다.
결과: 리와인드 위치 오차 242cm → 2.3cm (Bad 네트워크 프리셋 기준).
→ 전체 과정: 3-2. 서버 사이드 리와인드
프로젝트 소개
채용공고의 필수/우대 요건을, 주장이 아니라 동작하는 시스템으로 증명하는 프로젝트이다.
각 요건마다 “무슨 클래스를 썼다”가 아니라 “어떤 판단을 내렸고 왜 그렇게 했나”를 글로 남겼다. 아래 표의 오른쪽 칸이 그 판단이다.
필수요건
| 요구사항 | 이 프로젝트에서 내린 판단 | 글 |
|---|---|---|
| UE Gameplay Framework 이해 | 매치 상태를 AGameMode 내장 상태머신에 얹고, 클라가 알아야 할 것만 GameState로 승격 |
1-1, 1-3 |
| 게임모드 및 기믹 개발 | ChoosePlayerStart 오버라이드로 중복 없는 랜덤 스폰, 매치 타이머로 EndMatch 트리거 |
1-3, 1-4 |
| Replication 기반 네트워크 개발 | Sprint를 Server RPC에서 CMC CompressedFlags로 이관, RPC와 복제 변수를 없애고 기존 플래그 바이트의 빈 비트로 대체 | 2-1 |
| 서버-클라이언트 동기화 이해 | 히트 판정 전량 서버 권한 + 본 단위 히트박스 리와인드, 시계는 GetServerWorldTimeSeconds()로 통일 |
3-1~3-3 |
| C++ 심층 이해 | CMC SavedMove 확장, FObjectInitializer로 기본 서브오브젝트 교체, FBodyInstance 직접 조작 |
2-1, 3-2 |
우대요건
| 요구사항 | 이 프로젝트에서 한 것 |
|---|---|
| 서버 권한 아키텍처 | 전 게임 로직 서버 판정. 클라는 요청과 표현만 담당 |
| GAS 활용 | AttributeSet(Health/MaxHealth/MoveSpeedMultiplier), GameplayEffect 데미지 파이프라인, 어빌리티 7종: Skill 3종(Dash/Heal/ShieldOn) + Death / Interact / Item_PrimaryUse / Item_Reload |
| Data-Driven 설계 | 3계층 구조, FEPItemData(DataTable Row) → UEPItemDefinition(DataAsset) → 런타임 상태. ItemId로 연결 |
| 버전 관리 | 단계별 feature 브랜치(feature-gas, feature-loot), 단계 완료 시 태그(step5-2) |
검증 범위를 분명히 해둔다. 설계 목표는 2~8인이지만, 실제로 확인한 것은 PIE 다중 클라이언트 2인 동시 접속까지이다. 데디케이티드 서버 타깃(
*Server.Target.cs)은 아직 분리하지 않았고, Listen Server 구성으로 서버 권한 경로를 검증했다.
어떻게 확인했는가
포트폴리오에서 “구현했다”만큼 중요한 게 “어떻게 틀린 걸 잡았나”라고 생각해서, 검증 수단도 같이 만들었다.
| 수단 | 용도 |
|---|---|
| SSR 디버그 시각화 | 파랑=현재 물리 프리미티브, 빨강=리와인드 후, 흰색=트레이스, 노랑=확정 히트. UEPCombatDeveloperSettings에서 토글 |
| 서버 로그 | [SERVER_REWIND_POS] ServerPos=… RewindPos=…: 리와인드 오차를 수치로 확인 |
| 콘솔 커맨드 | EP.Loot.RollTable, EP.Inv.Add 등, 에디터 조작 없이 서버 상태 조작 |
| PIE 다중 클라이언트 | Net Mode: Play As Client, 클라이언트 2 + p.NetShowCorrections |
| 네트워크 에뮬레이션 | PIE Network Emulation Bad 프리셋으로 지연/패킷 손실 하에서 재현 |
Shipping/Test 빌드에서는 디버그 코드가 전부 컴파일에서 빠지도록 가드를 걸어뒀다.
#if (UE_BUILD_SHIPPING || UE_BUILD_TEST)
bDebugDraw = false;
bDebugLog = false;
#endif
게임 컨셉
“취업”을 테마로 한 1인칭 익스트랙션 슈터.
매치 시작 → 랜덤 스폰 → 파밍(루팅 + 자판기) → 교전(PvP/AI) → 탈출 → 보상
- 자판기: 돈 투입 → 5초 대기(소리로 위치 노출) → 확률 아이템 배출
- 퀘스트 아이템(이력서, 자격증 등)을 모두 수집하면 “취업 성공”
- 사망 시 소지품 전부 손실, 탈출 성공 시 보존
타르코프를 참고했고, 그래서 정보 은폐가 설계의 축이다.
남은 플레이어 수, 상대 HP, 내 킬 수는 전부 공개하지 않는다.
이 결정이 복제 조건(COND_OwnerOnly) 선택으로 그대로 이어진다. → 2-5
연재 목록
1. Gameplay Framework
| 편 | 제목 | 한 줄 |
|---|---|---|
| 1-1 | 아키텍처 설계 | AGameMode vs AGameModeBase 선택, 서버 전용/복제 경계 긋기 |
| 1-2 | Enhanced Input으로 FPS 캐릭터 | 입력을 PlayerController에 두는 이유, 버니합 억제 |
| 1-3 | 서버 권한형 매치 흐름과 상태 복제 | AGameMode 내장 상태머신 → GameState로 승격 |
| 1-4 | 스폰 시스템과 DataAsset | 중복 없는 랜덤 스폰, UPrimaryDataAsset을 고른 이유 |
2. Replication & 전투 기반
| 편 | 제목 | 한 줄 |
|---|---|---|
| 2-1 | CMC 확장, 초당 60회 RPC를 0바이트로 | Server RPC를 버리고 이동 패킷의 빈 비트에 실은 이유 |
| 2-2 | MetaHuman을 ACharacter에 이식하기 | LeaderPoseComponent가 요구한 거래, 표정을 포기했다 |
| 2-3 | 아이템을 세 겹으로 나누기 | Row / Definition / 런타임 상태, ItemId로 연결 |
| 2-4 | 사격을 서버로 옮기고, 남은 구멍들 | 진입점을 하나로. 그리고 “서버 권한”이 절반이었던 이야기 |
| 2-5 | 이 게임의 복제 설계 전부 | 게임 규칙이 복제 조건을 정한다. 결정 9개와 근거 |
| 2-6 | 애니메이션, 그리고 남에게 보이는 것 | Linked Anim Layer + 조준 피치가 2바이트로 오는 이유 |
3. Net Prediction & Lag Compensation
| 편 | 제목 | 한 줄 |
|---|---|---|
| 3-1 | 본 단위 히트박스 | Physics Asset + 전용 채널. 서버는 아무것도 렌더링하지 않는다 |
| 3-2 | 서버 사이드 리와인드 | 두 함수가 다른 프레임의 시계를 보고 있었다. 이 시리즈의 핵심 |
| 3-3 | 예측 이펙트, 그리고 죽어 있던 코드 한 줄 | 체감은 클라가·판정은 서버가. 3단계 마무리 |
4. GAS 이관
| 편 | 제목 | 한 줄 |
|---|---|---|
| 4-1 | GAS 도입: ASC를 어디에 둘 것인가 | PlayerState에 둔 이유. Owner와 Avatar가 갈리는 지점 |
| 4-2 | TakeDamage를 버리다 | Health를 직접 안 깎는 이유. 사망을 어빌리티로 |
| 4-3 | 발사와 재장전을 어빌리티로 | RPC 두 개를 지웠다. 되돌릴 수 있는 것만 예측한다 |
| 4-4 | 탄이 어디에 박히는가 | 탄 분포를 커브로. 3단계 내내 죽어 있던 코드를 발견했다 |
| 4-5 | 스킬 시스템과 예측 레이스 버그 | 시전은 다 채웠는데 힐이 안 된다. 그것도 확률적으로 |
| 4-6 | HUD, 그리고 GAS 이관 결산 | UI는 판단하지 않고 구독만 한다. 지운 것과 그대로 둔 것 |
5. 진행 중
| 단계 | 상태 |
|---|---|
| 5. 루팅·인벤토리 | 진행 중, 스포너/상호작용 완료, 인벤토리(FastArraySerializer) 작업 중 |
기술 스택
| 엔진 | Unreal Engine 5.7 |
| 언어 | C++ (게임플레이 로직 100%) |
| 네트워크 | 서버 권한 모델, UCharacterMovementComponent 예측 확장 |
| 빌드 | MSVC (Visual Studio Build Tools 2022) |
| IDE | JetBrains Rider |
| 버전 관리 | Git, 단계별 feature 브랜치 + 완료 태그 |
프로젝트 구조
UE5-EmploymentProj/
├── CLAUDE.md
├── README.md
├── DOCS/
│ ├── DOCS.md # 기술 로드맵 (채용공고 요건 → 구현 매핑)
│ ├── GAME.md # 게임 디자인 문서
│ ├── Mine/ # 주제별 설계 노트 (TickGroup, Dormancy, …)
│ └── Notes/ # 단계별 구현서 + STATUS
│ ├── 01/ 02/ 03/ 04/ 05/
└── EmploymentProj/
└── Source/EmploymentProj/
├── Public/ # 기능별 분리 (Core/ Combat/ Movement/ GAS/ Data/ Loot/ …)
└── Private/ # Public 구조를 그대로 미러링
각 단계 폴더에는 구현서와 *_STATUS.md를 따로 둔다.
구현서는 예정을 적고, STATUS는 실제로 동작하는 것만 적는다.
진행 상태의 진실은 항상 STATUS 쪽이다.
빌드
# 프로젝트 파일 생성
UnrealBuildTool.exe -projectfiles \
-project="EmploymentProj/EmploymentProj.uproject" -game -engine
# Development Editor 빌드
UnrealBuildTool.exe EmploymentProj Win64 Development \
-project="EmploymentProj/EmploymentProj.uproject"
멀티플레이 확인은 에디터에서 Play → Net Mode: Play As Client, Number of Players: 2로 한다.

댓글남기기