Sparta Unreal 부트캠프 79일차

정찬호·2026년 3월 25일

코딩 테스트

프로그래머스 - 소수 찾기

DFS로 숫자 조합
소수 판정 알고리즘(7자리의 수이니 최대한 시간복잡도가 작게 만들 필요가 있어 보임)
unordered_set을 만들어진 소수 담기

#include <string>
#include <vector>
#include <unordered_set>
#include <cmath>

using namespace std;

bool checkPrim(int number)
{
    if(number < 2)
    {
        return false;
    }
    if(number == 2)
    {
         return true;
    }
    
    for(int i = 2; i <= ceil(sqrt(number)); i++)
    {
        if(number % i == 0)
        {
            return false;
        }
    }
    
    return true;
}

void DFS(const string& numbers, vector<bool>& visited,
        string currentNumber, unordered_set<int>& primSet)
{
    if(currentNumber.length() == numbers.length())
    {
        return;
    }
    for(int i = 0; i< numbers.length(); i++)
    {
        if(visited[i] == true)
        {
            continue;
        }
        visited[i] = true;
        
        int number = stoi(currentNumber + numbers[i]);
        if(checkPrim(number))
        {
            primSet.insert(number);    
        }
        DFS(numbers, visited, currentNumber + numbers[i], primSet);
        visited[i] = false;
    }
}
int solution(string numbers) {
    int answer = 0;
    unordered_set<int> primSet;
    vector<bool> visited(numbers.length(), false);
    
    DFS(numbers, visited, "", primSet);
    answer = primSet.size();
    return answer;
}

인강 챕터 8 완강하기

전체적인 게임 규칙이나 진행을 담당하는 액터는 GameMode입니다.
이 GameMode는 서버에만 존재하므로 서버가 각 클라이언트에게 알려야 하는 정보는 양쪽에 존재하는 GameState에 작성해야 합니다. 속성에 Replicated 키워드만 지정하면 됩니다.
GameState는 기본값으로 각 클라이언트에 복제 생성됩니다.
그럼 각 클라이언트의 UI는 게임 스테이트에 접근, 해당 속성을 읽어오기만 하면 됩니다.

데디 서버로의 수정 접속

우선 클라이언트에서의 Play Net Mode를 Play StandAlone으로 지정해서 서버로의 자동 연결을 막습니다.
기존 실습에서는 Play Net Mode를 Play as Client로 지정했기 때문에 자동으로 서버에 연결되었던 겁니다.
Editor Preferences > Play Net Mode도 Play Standalone으로 수정.

그 후 Project Settings > Maps & Modes에서 기본 맵을 Title(접속 전 맵)으로 변경하고 ServerDefaultMap(서버가 기본적으로 여는 맵)으로 멀티 플레이가 진행될 맵(여기서는 ThirdPersonMap)을 지정합니다.

Editor Preferences > 검색창에 “Server Map Name Override” 검색 후 Server Map Name Override에 멀티 플레이용 맵의 이름을 적어줍니다.

Title Map에서 PlayButton을 클릭 시 서버 맵으로 접속 시도하도록 구현되었습니다.

void UUW_TitleLayout::NativeConstruct()
{
	PlayButton.Get()->OnClicked.AddDynamic(this, &ThisClass::OnPlayButtonClicked);
	ExitButton.Get()->OnClicked.AddDynamic(this, &ThisClass::OnExitButtonClicked);
}

void UUW_TitleLayout::OnPlayButtonClicked()
{
	ADXTitlePlayerController* PlayerController = GetOwningPlayer<ADXTitlePlayerController>();
	if (IsValid(PlayerController) == true)
	{
		FText ServerIP = ServerIPEditableText->GetText();
		PlayerController->JoinServer(ServerIP.ToString());
	}
}

GameManager와 GameState에서 게임 상태 관리 로직을 작성했고, 종료 상태에서는 모든 Client를 Title 화면으로 추방하고 서버는 맵을 다시 열어 초기화하는 식으로 구현했습니다.

void ADXGameModeBase::OnMainTimerElapsed()
{
	ADXGameStateBase* DXGameState = GetGameState<ADXGameStateBase>();
	if (IsValid(DXGameState) == false)
	{
		return;
	}
	
	switch (DXGameState->MatchState)
	{
	case EMatchState::None:
		break;
	case EMatchState::Waiting:
    ...
    
    
	case EMatchState::Playing:
    ...
    
    case EMatchState::Ending:
		{
			FString NotificationString = FString::Printf(TEXT("Waiting %d for returning to title."),RemainWaitingTimeForEnding);
			
			NotifyToAllPlayer((NotificationString));
			
			--RemainWaitingTimeForEnding;
			
			if (RemainWaitingTimeForEnding <= 0)
			{
				for (ADXPlayerController* AlivePlayerController  : AlivePlayerControllers)
				{
					AlivePlayerController->ClientRPCReturnToTitle();
				}
				for (ADXPlayerController* DeadPlayerController  : DeadPlayerControllers)
				{
					DeadPlayerController->ClientRPCReturnToTitle();
				}
				
				MainTimerHandle.Invalidate();
				
				// 서버 레벨 초기화
				FName CurrentLevelName = FName(UGameplayStatics::GetCurrentLevelName(this));
				UGameplayStatics::OpenLevel(this, CurrentLevelName, true, FString(TEXT("listen")));
				return;
			}
			break;
		}
...

현재 서버 초기화가 진행되지 않는 건지, Ending 상태로 진입되어 Player들을 Title로 내보낸 이후 서버에 재접속이 안 되는 문제가 발생 중입니다.

8 챕터 강의 완강 후 플레이 도중 발생한 오류 로그

[2026.03.25-14.20.17:055][630]LogBlueprintUserMessages: [ThirdPersonMap] TakeDamage: 10.000000
[2026.03.25-14.20.17:296][637]LogDXNet: [Server] ADXBox::IsNetRelevantFor BP_Box_C_UAID_244BFE5902688EC802_1747504687 is not relevant for(BP_PlayerController_C_1, BP_PlayerCharacter_C_1)
[2026.03.25-14.20.17:528][644]LogRep: Error: ReceivedRPC: RPC_GetLastFailedReason: ServerRPCMeleeAttack_Validate
[2026.03.25-14.20.17:580][644]LogNet: Error: UActorChannel::ProcessBunch: Replicator.ReceivedBunch failed.  Closing connection. RepObj: BP_PlayerCharacter_C /Game/ThirdPerson/Maps/ThirdPersonMap.ThirdPersonMap:PersistentLevel.BP_PlayerCharacter_C_2, Channel: 4
[2026.03.25-14.20.17:581][644]LogNet: UNetConnection::Close: [UNetConnection] RemoteAddr: 127.0.0.1:59595, Name: IpConnection_2, Driver: Name:GameNetDriver Def:GameNetDriver IpNetDriver_0, IsServer: YES, PC: BP_PlayerController_C_2, Owner: BP_PlayerController_C_2, UniqueId: NULL:DESKTOP-CPNE3S2-3E2CDCF54A52BC6DC6E11AAEEF9000B5, Channels: 17, Time: 2026.03.25-14.20.17
[2026.03.25-14.20.17:582][644]LogNet: UNetConnection::SendCloseReason:
[2026.03.25-14.20.17:582][644]LogNet:  - Result=ObjectReplicatorReceivedBunchFail, ErrorContext="ObjectReplicatorReceivedBunchFail"
[2026.03.25-14.20.17:582][644]LogNet: UChannel::Close: Sending CloseBunch. ChIndex == 0. Name: [UChannel] ChIndex: 0, Closing: 0 [UNetConnection] RemoteAddr: 127.0.0.1:59595, Name: IpConnection_2, Driver: Name:GameNetDriver Def:GameNetDriver IpNetDriver_0, IsServer: YES, PC: BP_PlayerController_C_2, Owner: BP_PlayerController_C_2, UniqueId: NULL:DESKTOP-CPNE3S2-3E2CDCF54A52BC6DC6E11AAEEF9000B5

다른 오류를 해결한 뒤 재현하는 것을 실패해서 왜 발생한 오류인지는 명확히 파악하진 못했습니다.

Data Driven 특강 - 김조은 튜터님 (복습 필요)

라이브 서비스는 주기적인 업데이트가 생명입니다.
데이터만 패치로 넣어주면 빌드 없이 변경이 가능합니다.

Const / Runtime 데이터
정해진 용어가 아닌 편의상 구분하기 쉽게 만든 이름
Const : 테이블에 고정된 값. 모든 서버/클라가 같은 데이터를 공유. 플레이 도중 변경되지 않음
Runtime : 플레이 중 변하는 값. 서버가 계산하고, 개체마다 다르다.

서버 통신에는 소수점을 쓰지 않습니다.

공통 데이터 처리 방법

테이블 분리 vs 컬럼 확장
프로그래머 입장에서는 테이블을 나눠두는 것이 좋다.
미리 나누어져 있어야 유연하게 사용할 수 있다.
문제는 기획자가 힘듧니다. 어디서 어떤 것을 쓰는 지 테이블 들을 열어보며 직접 찾아야 하는데, 이 테이블이 수십개가 되니 눈이 빙빙 돕니다.

데이터 관리부터 복습 필요
Data Table
UDeveloperSettings를 이용하면, Blueprint Subsystem을 만들 수 있다고 합니다.
블루프린트화된 서브 시스템은 상당히 유용합니다,
그냥 서브 시스템을 만드는 것과는 코드 한 두줄 차이 정도?
데이터 테이블을 게임 인스턴스에 집어넣고 가져와서 사용하는 코드를 짜면 코드보고 에디터에서 블루 프린트 보고 다시 코드를 보는 동작을 반복해야 하는데 온전히 가지고 있게 만들면 코드를 보는 걸로 다 확인이 가능하다?
뭔가 중간에 놓쳐서 내용이 이상해졌네요.

CDO에 올려놓으면 서스시스템이 된다? 블루프린트는 올려놓으면 체크가 되질 않는다? 데이터 베이스 배치
데이터 베이스를 블루프린트 서브 시스템으로 만들면 그냥 메모리에 올라가지 않는다.
C++의 경유 CDO에 바로 올라가는데 BP는 그렇지 않음.
다른 것들보다 먼저 동작하지 않기에 UDevelopSettings(게임 설정 클래스)에 블루프린트 서브시스템을 하드 레퍼런스 시키면
게임 기동과 함계 메모리에 올라가게 된다.
ShouldCreateSubsystem을 코딩해주어야 한다. 이게 있어야 C++ 생성 없이 블루프린트화된 서브 시스템만 만들어진다.
C++ CDO, BP CDO가 각각 존재하기에 양쪽에 올라가는 불살사를 막기 위해서는 필요한 과정입니다.

Data Asset 관련 PrimAseet 관련 설명 다시 듣기

언리얼의 코스메틱은 우리가 보고 듣고 느끼는 게임 로직 이외의 것들, 혹은 게임 로직이나 조직에 관여하더라도 client only이거나 플레이 도중에 수치를 조정하고 바꾸는 것들을 의미한다고 합니다.

프라이머리 데이터 에셋
언리얼 에셋 시스템이 맨 처음 로딩 가능한 것, 에셋 아이디로 찾아서 로딩한다?

CDO : 클래스 디폴트 오브젝트
객체 만들고 파괴하는 오버헤드 피할려고 사용하는 용도?
기본으로 하나 만들어져 있는 거라 오버헤드가 적다?

blueprint는 부모 클래스를 CDO에 올리고나서 올라가는 건가?

에셋은 개별 관리하는 것이 유리하다

서버와 클라이언트 데이터를 맞춰두고 로그인을 시작?

무점검 패치가 가능한 게임
웹서버처럼 서버 인스턴스가 여러 개 있거나 하나의 클라이언트 요청에 대한 인스턴스가 생기고 사라져야 한다?
무점검을 하고 싶다면 서버 V1있을 때 수정된 버전인 V2를 만들어 놓고
로드 밸런서 : 클라이언트가 접속을 시도하면 연결된 여러 서버 중 어디로 보낼 지를 결정한다.

미리 새로은 요청 인스턴스를 준비해 두고, 로드 밸런서로 특정 시간 이후로 접속하는 유저는 다른 서버로 보내는 것?
V1에 접속된 인원이 다 빠지면 V1을 끄고 V2만 유지?
이게 무점검 패치의 원리

실시간 == 주로 소켓서버
매치메이킹 == 주로 웹서버

실시간 서버는 무점검 패치를 하려면 코딩이 복잡하다
로직 자체가 다르다.

stateless
매치 메이킹은 서버에서는 응답만?

유효성 검증
수정을 여기저기서 하다 보니

float double 절대 안씀, 클라와 서버가 계산할 때 조금씩 오차가 생겨서 문제발생
서버는 노출이 안 되지만 클라이언트는 다 뜯긴다
패킷도 변조가 가능

애니메이션은 게임 로직이 들어있으면 동기, 없으면 비동기?
실시간 서버는 서버에서 캐릭터가 놀고 있는 것

profile
게임 개발 지망생입니다.

0개의 댓글