- 개요: Steamworks API 기반의 데디케이티드 서버 탑다운 배틀로얄 게임
- 기술 스택: Unreal Engine(5.5.4), C++, Steamworks API
- 상세 업무
- Steamworks API를 연동하여 실시간 UDP 통신 기반의 WAN 멀티플레이어 환경 및 중앙 서버 아키텍처 구축(데디케이티드 서버)
- 중앙 서버 권한 기반의 실시간 데이터 동기화 구현 및 매치메이킹 세션 검증을 통한 유저 인메모리 세션 관리
- 서버 패킷 플로딩 공격에 대비한 패킷 수신 주기 제어 및 비정상 유저 강제 세션 종료 로직 개발
기술 설명
📣구현 기술에 대한 주된 메커니즘들을 설명합니다
1. 실시간 UDP WAN 네트워크 기반의 중앙(데디케이티드) 서버 아키텍처 구축
🛠️구현 목표
- 글로벌 WAN 환경에서 매치메이킹이 가능하도록 안정적인 실시간 멀티플레이 환경을 구축
ㅤ
구현 내용
- Headless 기반의 중앙 데디케이티드 서버(Dedicated Server) 구조 구축을 통해 클라이언트 의존성 제거 및 보안성 확보
void URCGameInstance::Init() { UWorld* World = GetWorld(); if (World) { // NetMode 검증을 통해 데디케이티드 서버일 경우 if (World->GetNetMode() == NM_DedicatedServer) { // 세션 생성 GetOnlineSubsystem(); CreateGameSession(); }ㅤ
결과
![]()
서버 실행 시, 17777 포트에서 게임 서버가 대기함
![]()
NetMode 확인을 통해 세션 생성
![]()
일정 인원 이상 접속한 경우, 세션 자동 시작
🛠️구현 목표
- 서버 정보 동기화: 중앙 서버 중심의 플레이어 상태 관리로 유저 간 데이터 오차 및 변조 방지
- 인메모리 세션 라이프사이클 관리: 유저 입/퇴장 및 세션 정보의 빠른 인메모리 처리
ㅤ
구현 내용
- PostLogin()을 통해 들어온 유저의 정보를 확인하고 세션 정보를 관리함
void ARCWaitingLvGameModeBase::PostLogin(APlayerController* NewPlayer) { Super::PostLogin(NewPlayer); ARCPlayerController* NewPlayerController = Cast<ARCPlayerController>(NewPlayer); if (NewPlayerController != nullptr) { // SeverTravel을 위한 컨트롤러 인스턴스 메모리 저장 LogInPlayerControllers.Add(NewPlayerController); // 게임시작을 위한 최소 인원 확인 if (LogInPlayerControllers.Num() >= MinPlayerCount) { URCGameInstance* RCGameInstance = GetWorld()->GetGameInstance<URCGameInstance>(); if (IsValid(RCGameInstance)) { // 세션 시작 RCGameInstance->ManageSession(true);
- 주기적으로 게임 정보를 업데이트하고 이를 접속한 플레이어들에게 전달하여(멀티케스트) 게임 정보 및 게임 메시지를 동기화함
// 1초 단위로 게임 정보 갱신 GetWorld()->GetTimerManager().SetTimer(MainTimerHandle, this, &ThisClass::OnMainTimerElapsed, 1.f, true); // 게임 정보 갱신 함수 일부 ARCGameStateBase* RCGameState = GetGameState<ARCGameStateBase>(); if (IsValid(RCGameState)) { RCGameState->AlivePlayerControllerCount = LogInPlayerControllers.Num(); }// 접속 인원 정보 수신 시, 델리게이트 호출 void ARCGameStateBase::OnRep_AlivePlayerControllerCount() { OnAlivePlayersChanged.Broadcast(AlivePlayerControllerCount); } // 게임 메시지 정보 수신 시, 게임 내 ui 게임 메시지 수정 void ARCGameStateBase::OnRep_ReplicatedGameModeDelay() { FString NotiMsg; switch (MatchState) { case EMatchState::None: break; case EMatchState::Waiting: NotiMsg = FString::Printf(TEXT("게임 시작까지 %d초"), ReplicatedGameModeDelay);ㅤ
결과
![]()
새로운 유저 세션 접속
![]()
최소 플레이어 수 만족으로 세션 시작
🛠️구현 목표
- 실시간 액션 동기화: Server RPC 기반의 유저 액션 처리 및 상태/애니메이션 복제(Replication)
ㅤ
구현 내용
- 유저 액션에 따른 Server RPC 실행을 통해 게임정보 및 애니메이션 동기화
// 자신의 애니메이션 실행 // 클라이언트 일 경우 if (!HasAuthority()) { // Server RPC 호출 Server_StartAttack(TargetWorldPos); return; }// Server RPC 실행 void AWeaponBase::Server_StartAttack_Implementation(const FVector_NetQuantize& TargetWorldPos) { CachedTargetWorldPos = TargetWorldPos; bWantsToAttack = true; AttackAccum = GetAttackInterval(); // 애니메이션 실행 및 주변 플레이어에게 브로드케스팅 ..ㅤ
결과
![]()
공격에 따른 서버 로그 및 애니메이션 복제 ↑ 공격자 화면 / ↑ 피해자 화면
🛠️구현 목표
- 악의적으로 과도한 패킷 송신(Packet Flooding)을 유발하는 유저를 퇴장 처리시키는 로직
ㅤ
구현 내용
- Rate Limiting (수신 주기 제어): 유저별 패킷 수신 주기 패킷 검증
// 최대 패킷 레이트를 설정할 수도 있음 [/Script/Engine.NetDriver] net.MaxPacketRate=50 bEnablePacketRPCThrottling=Truevoid ARCPlayerCharacter::Server_FloodTest_Implementation() { ServerTestPacketCount++; if (ServerTestPacketCount > MAX_PACKET_RATE) { APlayerController* PC = Cast<APlayerController>(GetController()); if (PC) { UE_LOG(LogTemp, Error, TEXT("!!! FLOOD DETECTED: KICKING CLIENT !!!")); // 1. 클라이언트 처리: Client RPC 전송 Client_ForceQuitGame(); // 1. 서버처리: 클라이언트에게 킥 사유를 보내고 메인 화면/접속 해제 처리 PC->ClientWasKicked(FText::FromString(TEXT("Packet Flooding Detected"))); // 2. 서버처리: 네트워크 소켓 세션 즉시 단절 if (PC->GetNetConnection()) { PC->GetNetConnection()->Close();// Client에서 킥처리(클라이언트 화면 종료) void ARCPlayerCharacter::Client_ForceQuitGame_Implementation() { APlayerController* PC = Cast<APlayerController>(GetController()); if (PC) { UKismetSystemLibrary::QuitGame(GetWorld(), PC, EQuitPreference::Quit, false); } }ㅤ
결과
![]()
어뷰징으로 킥 당하는 유저 (우측 유저)
트러블 슈팅
📣주요 트러블 슈팅에 관해서 설명합니다.
🛠️문제 상황
- 데디케이티드 서버(Dedicated Server) 프로세스를 정상적으로 실행 및 초기화하였으나, 클라이언트 측에서 게임 세션을 검색할 수 없는 현상 발생
ㅤ
원인 분석 및 가설 검증
- 가설 1: 세션 식별자(Session Name) 및 세션 설정 문제
- 검증: 세션 이름의 유일성(Uniqueness) 미보장 또는 설정 오류로 인해 검색이 안 되는 것으로 추정하여 세션 생성 및 검색 파라미터를 수정하였으나, 여전히 세션이 검색되지 않음
OnlineSessionInterface->OnCreateSessionCompleteDelegates.AddUObject(this, &URCGameInstance::OnCreateSessionComplete); // 세션 설정 정보 재확인 TSharedPtr<FOnlineSessionSettings> SessionSettings = MakeShareable(new FOnlineSessionSettings()); SessionSettings->bIsLANMatch = false; // Steam Server이므로 LanMatch = false SessionSettings->NumPublicConnections = 4; SessionSettings->bAllowJoinInProgress = true; SessionSettings->bAllowJoinViaPresence = true; SessionSettings->bShouldAdvertise = true; // 세션 생성 후, 세션 정보 어드버타이징 = true SessionSettings->bUsesPresence = true; SessionSettings->bIsDedicated = true; // 세션 종류이므로 dedicated = true // 세션 검색에 필요한 별도의 Key 값 (SessionName) 생성하여 검색 -> 실패.. SessionSettings->Set(FName(TEXT("SessionName")), FString(TEXT("DedicatedServer Session")), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing); SessionSettings->Set(FName(TEXT("SessionStart")), false, EOnlineDataAdvertisementType::ViaOnlineServiceAndPing); OnlineSessionInterface->CreateSession(0, FName(TEXT("DedicatedServer Session")), *SessionSettings);ㅤ
- 가설 2: 네트워크 포트 바인딩 및 통신 블로킹 문제
- 검증: 세션 검색 결과 개수를 로그로 출력하여 세션 정보 수집 여부를 확인하던 중, 서버와의 통신 자체가 차단되고 있음을 인지
void ATitlePlayerController::OnFindSessionComplete(bool bWasSuccessful) { if (!OnlineSessionInterface.IsValid() || !bWasSuccessful) { return; } // 해당 로그에서 세션 수가 0으로 출력됨 UE_LOG(LogTemp, Error, TEXT("Found Session Count : %d"), SessionSearch->SearchResults.Num());ㅤ
- 원인: 로컬(집) 환경에서 데디케이티드 서버를 호스팅하면서 서버 기본 포트(27015)가 외부로 열려있지 않아 클라이언트의 패킷 요청이 차단됨
ㅤ
해결방법
- 포트포워딩(Port Forwarding) 설정: 공유기 및 방화벽 설정에서 데디케이티드 서버가 사용하는 통신 포트(27015 / UDP)를 개방하고 서버 호스트 PC로 포워딩 처리
![]()
ㅤ
결과
- 실제 STEAM 글로벌 서버에서 생성한 게임 세션을 확인할 수있음
![]()
- 패킷을 수신할 수 없을 경우 방화벽과 포트 개방 여부를 확인해볼 수 있다는 점을 배움
🛠️문제 상황
- 어뷰저 클라이언트은 Packet Flooding판정으로 킥(Kick)되었으나 이미 서버에 먼저 도착한 패킷으로 인해 타겟 플레이어의 체력이 감소하는 문제 발생
ㅤ
롤백 시도 및 한계
- 시도
- 데미지 역산 (
ApplyDamage에 음수값 전달): 데미지 계산식(방어력 계산 등)을 재경유하면서 정확한 체력 복구가 불가능함을 확인- 이전 체력 스냅샷(Snapshot) 복원: 킥 발생 시 피격 대상의 체력을 롤백하려 했으나, 정상적인 다른 플레이어의 공격과 어뷰징 공격이 같은 프레임에 섞여 들어올 경우 정확한 '어뷰징 피격분'만 발라내어 복구하기 어려움 확인. 뿐만 아니라 체력 이외의 별도의 모든 특성에 스냅샷을 도입해야하는 어려움 확인
ㅤ
해결방법
- 엔진 기본 기능 중에 클라이언트의 요청이 정상적인 요청인지 검사 후 실행하는 방식인
_Validate함수 구현// ARCPlayerCharacter.h UFUNCTION(Server, Reliable, WithValidation) void Server_Attack()// ARCPlayerCharacter.cpp bool ARCPlayerCharacter::Server_Attack_Validate() { ServerTestPacketCount++; // 어뷰징이 확인될 경우 if (ServerTestPacketCount > MAX_PACKET_RATE) { UE_LOG(LogTemp, Error, TEXT("!!! FLOOD DETECTED IN VALIDATION: KICK CLIENT !!!")); return false; } return true; // 정상 요청 } void ARCPlayerCharacter::Server_Attack_Implementation() { // 데미지 계산 및 적용 ApplyDamage(...); }ㅤ
결과
- Server RPC 호출 시 서버 측에서
_Validate→_Implementation순으로 처리되는 엔진 라이프사이클을 학습함- 어떠한 처리이든지 트랜잭션 처리 전 “선 검증, 후 실행”의 중요성을 깨달음








