[UE5] 추출 슈터 개발 기록: 전체 목차

게시:     수정

카테고리:

태그: , ,

📌 이 시리즈의 표지 겸 목차입니다. 각 편으로 가는 링크는 아래 연재 목록에 있습니다. 완성된 시스템만 보려면 프로젝트 소개 페이지가 더 빠릅니다. 👾 깃허브 · 📋 기획

EmploymentProj

UE5 C++ 멀티플레이어 익스트랙션 슈터. 템플릿·블루프린트 없이 C++로 바닥부터 설계했다.


가장 어려웠던 것: 서버 사이드 리와인드

먼저 이것부터 보여주는 게 맞을 것 같다. 이 프로젝트에서 제일 오래 붙잡았고, 제일 많이 배운 부분이다.

언리얼엔진 Server Side Rewind 구축

문제: 랙 보상을 붙였는데 보이는 위치가 아니라 그보다 살짝 뒤를 쏴야 맞았다. 네트워크 상태와 무관하게 항상 정확히 한 틱만큼 빗나갔다.

원인: 스냅샷의 시각CMC::OnMovementUpdated(TickDispatch)에서 찍고, 본 Transform은 같은 틱의 TG_PostPhysics에서 읽고 있었다. 그런데 엔진의 UWorld::TickTickDispatch를 먼저 돌리고 그 다음에 월드 시간을 전진시킨다.

// 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로 한다.

DevLog 카테고리 내 다른 글 보러가기

댓글남기기