Skip to content

Repository files navigation

로얄 치킨 | 2025.09. ~ 2025.12.

  • 개요: Steamworks API 기반의 데디케이티드 서버 탑다운 배틀로얄 게임

  • 기술 스택: Unreal Engine(5.5.4), C++, Steamworks API

  • 상세 업무
    • Steamworks API를 연동하여 실시간 UDP 통신 기반의 WAN 멀티플레이어 환경 및 중앙 서버 아키텍처 구축(데디케이티드 서버)
    • 중앙 서버 권한 기반의 실시간 데이터 동기화 구현 및 매치메이킹 세션 검증을 통한 유저 인메모리 세션 관리
    • 서버 패킷 플로딩 공격에 대비한 패킷 수신 주기 제어 및 비정상 유저 강제 세션 종료 로직 개발


기술 설명

📣

구현 기술에 대한 주된 메커니즘들을 설명합니다

1. 실시간 UDP WAN 네트워크 기반의 중앙(데디케이티드) 서버 아키텍처 구축

2. 중앙 서버의 실시간 데이터 동기화 및 인메모리 관리

3. 실시간 상태 변화 및 애니메이션 멀티캐스트 복제

4. 디도스/패킷 플로딩 방어 및 비정상 유저 강제 세션 종료 로직 구현


1. 실시간 UDP WAN 네트워크 기반의 중앙(데디케이티드) 서버 아키텍처 구축

🛠️

구현 목표

  • 글로벌 WAN 환경에서 매치메이킹이 가능하도록 안정적인 실시간 멀티플레이 환경을 구축

구현 내용

  • Headless 기반의 중앙 데디케이티드 서버(Dedicated Server) 구조 구축을 통해 클라이언트 의존성 제거 및 보안성 확보
void URCGameInstance::Init()
{
	UWorld* World = GetWorld();

	if (World)
	{
		// NetMode 검증을 통해 데디케이티드 서버일 경우
		if (World->GetNetMode() == NM_DedicatedServer)
		{
			// 세션 생성
			GetOnlineSubsystem();
			CreateGameSession();
		}

결과

server_listen

서버 실행 시, 17777 포트에서 게임 서버가 대기함

server_init

NetMode 확인을 통해 세션 생성

session_start

일정 인원 이상 접속한 경우, 세션 자동 시작

2. 중앙 서버의 실시간 데이터 동기화 및 인메모리 관리

🛠️

구현 목표

  • 서버 정보 동기화: 중앙 서버 중심의 플레이어 상태 관리로 유저 간 데이터 오차 및 변조 방지
  • 인메모리 세션 라이프사이클 관리: 유저 입/퇴장 및 세션 정보의 빠른 인메모리 처리

구현 내용

  • 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);

결과

enter_2

새로운 유저 세션 접속

server_travel_2

최소 플레이어 수 만족으로 세션 시작

3. 실시간 상태 변화 및 애니메이션 멀티캐스트 복제

🛠️

구현 목표

  • 실시간 액션 동기화: 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();
    
    // 애니메이션 실행 및 주변 플레이어에게 브로드케스팅 ..

결과

attack_2

공격에 따른 서버 로그 및 애니메이션 복제 ↑ 공격자 화면 / ↑ 피해자 화면

4. 디도스/패킷 플로딩 방어 및 비정상 유저 강제 세션 종료 로직 구현

🛠️

구현 목표

  • 악의적으로 과도한 패킷 송신(Packet Flooding)을 유발하는 유저를 퇴장 처리시키는 로직

구현 내용

  • Rate Limiting (수신 주기 제어): 유저별 패킷 수신 주기 패킷 검증
// 최대 패킷 레이트를 설정할 수도 있음
[/Script/Engine.NetDriver]
net.MaxPacketRate=50
bEnablePacketRPCThrottling=True
void 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);
	}
}

결과

flood_kick

어뷰징으로 킥 당하는 유저 (우측 유저)




트러블 슈팅

📣

주요 트러블 슈팅에 관해서 설명합니다.

1. Steamworks api 기반의 게임 세션 생성 시, 클라이언트에서 세션 검색 불가

2. Packet Flooding으로 인한 플레이어 킥 이후, 피격된 플레이어의 상태 정보 롤백


1. Steamworks api 기반의 게임 세션 생성 시, 클라이언트에서 세션 검색 불가

🛠️

문제 상황

  • 데디케이티드 서버(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로 포워딩 처리
image

결과

  • 실제 STEAM 글로벌 서버에서 생성한 게임 세션을 확인할 수있음
image 1
  • 패킷을 수신할 수 없을 경우 방화벽과 포트 개방 여부를 확인해볼 수 있다는 점을 배움

2. Packet Flooding으로 인한 플레이어 킥 이후, 피격된 플레이어의 상태 정보 롤백

🛠️

문제 상황

  • 어뷰저 클라이언트은 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 순으로 처리되는 엔진 라이프사이클을 학습함
  • 어떠한 처리이든지 트랜잭션 처리 전 “선 검증, 후 실행”의 중요성을 깨달음




발표자료

About

Steamworks API 기반의 데디케이티드 서버 탑다운 배틀로얄 게임

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages