Sparta Unreal 부트캠프 78일차

정찬호·2026년 3월 24일

코딩 테스트

프로그래머스 - 가장 큰 수

처음 아이디어는 "맨 앞에 수를 0 이외의 수로 제한하면 만들어지는 모든 수의 길이는 같으니 모든 수를 문자열로 바꾼 뒤, 각 자리 수를 비교해서 정렬하면 비교 횟수를 많이 줄일 수 있지 않을까?" 였습니다.

a[i] > b[i] return true
a[i] > b[i] return false

둘 중 한 문자열의 끝까지 비교해도 다 같다면 더 짧은 쪽이 먼저 나오게 해보자

return !(a.length() > b.length())

그런데 짧은 걸 먼저 나오게 하면... 73 739 같은 경우는 어떻게 해야 하나?

최대 100000개이니 DFS는 안 쓰는 게 맞는 것 같은데..
음.. 모듈러 연산을 통한 자리수 비교를 해야 하나?
위의 사고 끝에 나온 코드는 실패했습니다.

실패 코드

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

bool comp(int a, int b)
{
    string strA = to_string(a);
    string strB = to_string(b);
    
    int len = max(strA.length(), strB.length());
    for(int i = 0; i < len; i++)
    {
        if(strA[i % strA.length()] >
           strB[i % strB.length()])
        {
            return true;
        }
        if(strA[i % strA.length()] <
           strB[i % strB.length()])
        {
            return false;
        }
    }
    return !(strA.length() > strB.length());
}

string solution(vector<int> numbers) {
    sort(numbers.begin(),numbers.end(), comp);
    string answer = "";
    for(int num:numbers)
    {
        answer += to_string(num);
    }
    return answer;
}

signal: segmentation fault (core dumped)에 정답률도 매우 낮았습니다.

우선 segmentation fault부터 해결하기로 하고 오류의 원인을 파악했습니다.

Segmentation Fault의 원인: 엄격한 약순서 (Strict Weak Ordering)
C++의 std::sort에 사용하는 비교 함수는 반드시 엄격한 약순서를 만족해야 합니다. 즉, comp(a, a)는 항상 false여야 하고, a와 b가 같을 때 comp(a, b)와 comp(b, a) 모두 false여야 합니다.

실패 코드 - segmentation fault 해결

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

bool comp(int a, int b)
{
    string strA = to_string(a);
    string strB = to_string(b);
    
    int len = max(strA.length(), strB.length());
    for(int i = 0; i < len; i++)
    {
        if(strA[i % strA.length()] >
           strB[i % strB.length()])
        {
            return true;
        }
        if(strA[i % strA.length()] <
           strB[i % strB.length()])
        {
            return false;
        }
    }
    
    if(strA.length() == strB.length())
    {
        return false;
    }
    return !(strA.length() > strB.length());
}

string solution(vector<int> numbers) {
    sort(numbers.begin(),numbers.end(), comp);
    string answer = "";
    for(int num:numbers)
    {
        answer += to_string(num);
    }
    return answer;
}

오류는 해결됬지만 아직 정답률이 낮습니다.
각 자리마다 비교하는 대신 문자열을 합쳐서 비교하는 방식은 어떨까요?
합쳐진 결과의 길이는 strA + strB와 strB + strA 둘 다 같고, 문자열 비교는 맨 앞 글자부터 비교해나가는 식이니 문제가 없을 것 같았습니다.

실패 코드 - 정답률 급상승

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

bool comp(int a, int b)
{
    string strA = to_string(a);
    string strB = to_string(b);
    
    return strA + strB > strB + strA;
}

string solution(vector<int> numbers) {
    sort(numbers.begin(),numbers.end(), comp);
    string answer = "";
    for(int num:numbers)
    {
        answer += to_string(num);
    }
    
    return answer;
}

한 테스트 케이스를 제외하고는 통과되었습니다. 왜 오답일까? 하면서 고민하다가 모든 수가 0이 되었을 때는 answer == "0"이 되어야 한다는 것을 뒤늦게 깨달았습니다.

정답 코드

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

bool comp(int a, int b)
{
    string strA = to_string(a);
    string strB = to_string(b);
    
    return strA + strB > strB + strA;
}

string solution(vector<int> numbers) {
    sort(numbers.begin(),numbers.end(), comp);
    string answer = "";
    for(int num:numbers)
    {
        answer += to_string(num);
    }
    
    bool bAllZero = true;
    for(char ch : answer)
    {
        if(ch != '0')
        {
            bAllZero = false;
            break;
        }
    }
    
    if(bAllZero == true)
    {
        answer = "0";
    }
    return answer;
}

처음에는 long long 으로 변환해서 비교하는 방법을 사용했습니다만, 너무 길어서 string으로 표현해야 한다는 것을 까먹고 있었던 거네요. 당연히 segmentation fault가 발생해서 수정을 진행했습니다.

인강 챕터 7 완강

HP를 관리하는 ActorComponent를 상속한 DXStatusComponent를 작성해서 PlayerCharacter에 부착했습니다.

현재 로직상 데미지 데디케이드 처리를 서버에서만 진행하고 있으니 TakeDamage도 서버에서만 호출이 됩니다.

체력 변화는 StatComponent 내부에서 되고 있지만 값이 복사되지 않아 서버에서만 값이 복제되는 문제가 있었습니다.

액터와 컴포넌트의 관계

액터가 컴포넌트를 소유하고 있는 상태라고 가정해 보면
액터 입장에서는 컴포넌트를 서브 오브젝트(Subobject), 반대로 컴포넌트 입장에서는 액터를 오너(Owner)라고 부릅니다.

SetIsRelicatedByyDefault(true); 컴포넌트도 리플리케이트되도록 설정하는 함수

컴포넌트의 이벤트 함수

InitializeComponent()

  • BeginPlay(), `ReadyForReplication() 전에 호출됩니다.
  • 본격적인 게임 로직이 수행되기 전에 준비되어야 할 로직을 작성하기에 좋습니다.

ReadyForReplication()

  • InitializeComponent() 이후 BeginPlay() 이전에 호출됩니다.
  • 레플리케이션 관련 초기화 작업을 미리 해둘 때 사용하면 좋습니다.

BeginPlay()

  • 모든 초기화가 완료되어 중요한 게임 로직이 수행될 준비가 호출됩니다.

Subobject의 네트워크 디버깅용 로깅

액터는 본인이 직접 로컬 롤과 리모트 롤을 관리하고 있지만,
서브 오브젝트는 오너에 의해 소유됩니다. 따라서 오너의 로컬 롤과 리모트 롤을 쫓아갑니다.
그래서 서브 오브젝트는 로깅할 때 아래와 같이 수정된 로깅 함수를 써야 합니다.

#define SUBOBJECT_LOCAL_ROLE_TCHAR *(UEnum::GetValueAsString(TEXT("Engine.ENetRole"), GetOwner()->GetLocalRole()))
#define SUBOBJECT_REMOTE_ROLE_TCHAR *(UEnum::GetValueAsString(TEXT("Engine.ENetRole"), GetOwner()->GetRemoteRole()))
#define DX_LOG_SUBOBJECT_ROLE(LogCat, Verbosity, Format, ...) UE_LOG(LogCat, Verbosity, TEXT("[%s][%s/%s] %s %s"), NETMODE_TCHAR, SUBOBJECT_LOCAL_ROLE_TCHAR, SUBOBJECT_REMOTE_ROLE_TCHAR, FUNCTION_TCHAR, *FString::Printf(Format, ##__VA_ARGS__))

중요한 부분은 GetOwner()-> => Owner의 것을 가져오기

레플리케이션 조건식을 활용한 특정 속성 레플리케이션 제외

아래의 코드는 OwningClient에게만 Replicate를 하도록 제한합니다.

DOREPLIFETIME_CONDITION(ThisClass, MaxHP, COND_OwnerOnly);

_CONDITION을 붙이고, 어디에만 복사할 건지 옵션을 주면 되네요.

유용한 Tip

ParticleEffect->OnSystemFinished.AddDynamic(this, &ThisClass::OnEffectFinished);
파티클 시스템에 종료 델리게이트가 존재한다는 것을 알게 되었습니다. 이전 프로젝트에서는 Timer랑 Lambda를 같이 써서 제거하는 무모한 짓을 저질렀었는데 이젠 그럴 필요가 완전히 사라진 것 같습니다.

TA 분반

나나이트

나나이트는 언리얼에서 제공하는 가상화된 지오메트리 시스템입니다.

클러스터 단위(1클러스터 = 128 트라이앵글)로 LOD(Level of Detail)계층 트리를 만들고 클러스터에서 트라이앵글을 다시 세분화하여 동작합니다.
내부 로직에서 트리를 만드는 연산을 진행합니다.
GPU 부하가 올라가지 CPU 부하는 적습니다.
Low Polygon에서는 트리 구조로 변환하는 연산 탓에 오히려 성능이 더 안 좋아 질 수 있습니다.

HeightMap으로 LandScape를 만들고 LandScape에 사용할 머티리얼을 만들어 부착하는 작업을 진행했습니다.

머티리얼 작업 도중 진도를 놓쳐 내일 중으로 복습한 뒤 TIL에 기재할 예정입니다.

Challenge 복습 & 과제

ComboManagerSubsystem을 만들고 공격 시 공격 상태, 일정시간(5초로 설정) 후 일반 상태로 돌아가는 로직을 관리하게 했습니다.
내부에 Delegate로 상태 변환 시, 시간 갱신 시 Broadcast를 하도로 만들었습니다.
이에 연동되는 UI를 만들어 공격 상태 시 화면에 빨간 테두리와 남은 시간을 표시하게 하는 과제가 있었습니다.
아래는 그 WBP 그래프와 시연 화면입니다.

보충
Subsystem은 상속받아 만들면 엔진 생명주기에 따라 인스턴스가 자동 생성되고 Initialize/Deinitialize가 자동 호출됩니다.
GameInstance / World / LocalPlayer 등 종류에 따라 수명이 다릅니다.

Subsystem에 “추가 데이터(설정/테이블/에셋 참조)”가 필요하면, 에디터에서 인스턴스에 값을 직접 넣는 방식을 기대하기보다
Config(.ini) 또는 DataAsset/PrimaryDataAsset 같은 데이터 소스(단일 진실 공급원)를 정해야합니다.
그리고 Subsystem이 Initialize()에서 해당 데이터 소스를 로드/참조해 내부 상태를 구성하게 합니다.
→ 이렇게 하면 “자동 생성된 Subsystem이 에디터 주입값을 못 받아서 비는 문제”를 크게 줄일 수 있다고 합니다.

다만 초기화 타이밍 이슈는 완전히 사라지지 않는다고 합니다.
World/LocalPlayer 준비 시점 차이, Soft Reference 로딩 완료 시점, 다른 시스템 의존성 때문에
Initialize() 시점에 데이터/월드가 아직 준비되지 않았을 가능성이 있습니다.
그래서 필요한 경우 유효성 체크(null/IsValid), 로딩 완료 이후 실행(지연 호출/비동기 콜백) 같은 방어가 필요합니다.

Manager 사용 지양
“Manager”라는 이름은 범위가 모호해질 수 있으니, 역할이 드러나도록 명명한다.
예: CombatStateSubsystem, CombatStateWorldSubsystem 등

네트워크 관점: 클라이언트가 보내는 정보는 신뢰하지 않고, 서버 권한(Authority)으로 상태를 확정한다.
매 틱 전송은 낭비이므로, 가능하면 상태 변화 시점에만 전파한다.
플러그인/모듈: 플러그인 기능은 가능한 플러그인 내부에서 닫히게 설계하고, 외부 의존은 Build.cs로 열 수 있지만 의존성 방향을 명확히 해야 꼬이지 않는다.

엔진 검증 시도 이후 오류 발생

오늫 하루 종일 빌드가 정상적으로 되지 않는 오류가 계속 발생하고 있었습니다. Gemini, Claude에게 물어보고 해결방법을 시도하길 반복한 끝에 검증 과정에서 네트워크 오류로 PlayerState.h가 지워졌을 가능성을 제시했습니다.
현재 검증을 다시 돌리는 중이며 내일 오전 중으로 문제를 해결하고 챕터 8을 끝내는 것이 목표입니다.

오류 output 창

CONSOLE: E:\ProgramFiles\EpicGames\UE_5.5\Engine\Build\BatchFiles\Build.bat  MyDedicatedXEditor Win64 Development -Project="E:\UnrealProjects\SpartaStudy\MyDedicatedX\MyDedicatedX.uproject" -WaitMutex  -FromMSBuild
0>Using bundled DotNet SDK version: 8.0.300
0>Running UnrealBuildTool: dotnet "..\..\Engine\Binaries\DotNET\UnrealBuildTool\UnrealBuildTool.dll" MyDedicatedXEditor Win64 Development -Project="E:\UnrealProjects\SpartaStudy\MyDedicatedX\MyDedicatedX.uproject" -WaitMutex  -FromMSBuild
0>Log file: C:\Users\elvpf\AppData\Local\UnrealBuildTool\Log.txt
0>Creating makefile for MyDedicatedXEditor (no existing makefile)
0>Parsing headers for MyDedicatedXEditor
0>  Running Internal UnrealHeaderTool E:\UnrealProjects\SpartaStudy\MyDedicatedX\MyDedicatedX.uproject E:\UnrealProjects\SpartaStudy\MyDedicatedX\Intermediate\Build\Win64\MyDedicatedXEditor\Development\MyDedicatedXEditor.uhtmanifest -WarningsAsErrors -installed
0>PlayerCameraManager.h(94): Error  : Unable to find 'class' with name 'APlayerState'
0>VoiceConfig.h(86): Error  : Unable to find 'class', 'delegate', 'enum', or 'struct' with name 'APlayerState'
0>VoiceConfig.h(92): Error  : Unable to find 'class', 'delegate', 'enum', or 'struct' with name 'APlayerState'
0>Controller.h(50): Error  : Unable to find 'class' with name 'APlayerState'
0>PlayerController.h(1449): Error  : Unable to find 'class' with name 'APlayerState'
0>PlayerController.h(1449): Error  : Unable to find 'class' with name 'APlayerState'
0>PlayerController.h(1542): Error  : Unable to find 'class' with name 'APlayerState'
0>Pawn.h(163): Error  : Unable to find 'class' with name 'APlayerState'
0>GameModeBase.h(100): Error  : Unable to find 'class' with name 'APlayerState'
0>GameModeBase.h(374): Error  : Unable to find 'class', 'delegate', 'enum', or 'struct' with name 'APlayerState'
0>GameMode.h(138): Error  : Unable to find 'class' with name 'APlayerState'
0>GameStateBase.h(55): Error  : Unable to find 'class' with name 'APlayerState'
0>LocalMessage.h(45): Error  : Unable to find 'class' with name 'APlayerState'
0>LocalMessage.h(48): Error  : Unable to find 'class' with name 'APlayerState'
0>WorldSettings.h(740): Error  : Unable to find 'class' with name 'APlayerState'
0>GameplayStatics.h(159): Error  : Unable to find 'class' with name 'APlayerState'
0>GameplayStatics.h(168): Error  : Unable to find 'class' with name 'APlayerState'
0>Total of 0 written
0>Total execution time: 8.90 seconds
profile
게임 개발 지망생입니다.

0개의 댓글