포스트

TIL 2026-04-22

TIL 2026-04-22

2026-04-22 언리얼 인벤토리 시스템 완성 + 정렬 알고리즘 + Unreal C++ 지식 그래프

목차


오늘 한 일 요약

  1. CodeKata_19 — 부족한 금액 계산하기 (for문 + 등차수열 공식 두 가지 풀이)
  2. Week07 알고리즘 강의 — 정렬(Sorting): O(n²) 3종 · 병합 정렬 · sort() 커스텀 정렬 · 이진 탐색 복선
  3. 언리얼 마스터 과제 인벤토리 시스템 완성 — TArray/TMap/TSet/칭호/UMG 도전 포함
  4. Unreal C++ 강의 내용 graphify로 그래프화 (111노드/139엣지/10커뮤니티) + CLAUDE.md에 이중 그래프 참조 규칙 추가



CodeKata_19 — 부족한 금액 계산하기

문제: N번째 이용 요금 = price × N, count번까지 누적 후 money로 부족한 금액 반환 (부족하지 않으면 0)

1
2
3
4
5
6
7
8
9
10
11
12
13
// for문 풀이 — O(N)
long long solution(int price, int money, int count)
{
    long long total = 0;
    for (int i = 1; i <= count; i++)
        total += (long long)price * i;

    long long diff = total - money;
    return diff > 0 ? diff : 0;
}

// 등차수열 합 공식 — O(1)
// total = price * count * (count + 1) / 2

핵심: price * i 에서 int 범위 초과 방지를 위해 (long long)price * i 캐스팅 필수.




Week07 — 줄 세우기의 기술: 정렬(Sorting)

핵심 질문: 왜 정렬 알고리즘을 종류별로 배우는가?
→ O(n²)과 O(n log n)의 차이는 n=100,000에서 수십 초 vs 수 밀리초로 실측 가능하기 때문.


O(n²) 정렬 3종 비교

세 가지 모두 O(n²)이지만 교환/밀기 횟수는 다르다.

알고리즘핵심 동작교환 방식특징
버블 정렬인접 두 원소 비교 → 교환swap 빈번구현 가장 단순, 실전 비추
선택 정렬남은 범위에서 최솟값 위치 탐색 후 swapswap 최소 (최대 n-1번)비교는 많지만 교환은 적음
삽입 정렬정렬된 부분에 현재 원소를 끼워넣기shift (밀기)거의 정렬된 데이터에서 O(n)에 가까움
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 버블 정렬 — 라운드마다 가장 큰 값이 뒤로 확정
for (int round = 0; round < n - 1; round++)
    for (int j = 0; j < n - 1 - round; j++)
        if (data[j] > data[j + 1]) swap(data[j], data[j + 1]);

// 선택 정렬 — 남은 것 중 최솟값 인덱스 찾아 swap
for (int i = 0; i < n - 1; i++) {
    int minIdx = i;
    for (int j = i + 1; j < n; j++)
        if (data[j] < data[minIdx]) minIdx = j;
    if (minIdx != i) swap(data[i], data[minIdx]);
}

// 삽입 정렬 — key를 꺼내 정렬된 앞부분에서 자리 찾아 삽입
for (int i = 1; i < n; i++) {
    int key = data[i], j = i - 1;
    while (j >= 0 && data[j] > key) { data[j + 1] = data[j]; j--; }
    data[j + 1] = key;
}

n=5 데이터 실측 (64,25,12,22,11):

1
2
3
버블 정렬 : 비교 10번, 교환 8번
선택 정렬 : 비교 10번, 교환 3번
삽입 정렬 : 비교 7번,  밀기 5번

→ 같은 O(n²)이라도 데이터 상태·알고리즘에 따라 실제 연산 횟수가 다르다.


O(n log n) — 병합 정렬 원리

분할 정복: 쪼개서 정렬하고 합치기.

1
2
3
4
5
6
7
8
9
[5,3,8,1,4,7,2,6]
      ↓ 분할 (log n 단계)
[5,3,8,1] | [4,7,2,6]
[5,3]|[8,1] | [4,7]|[2,6]
[5]|[3]|[8]|[1] | ...
      ↓ merge (각 단계 O(n))
[3,5] | [1,8] | ...
[1,3,5,8] | [2,4,6,7]
[1,2,3,4,5,6,7,8]

merge 핵심 — 앞쪽만 비교하면 된다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 두 정렬된 배열을 O(n)으로 합치기
vector<int> merge(const vector<int>& left, const vector<int>& right) {
    vector<int> result;
    int i = 0, j = 0;
    while (i < left.size() && j < right.size()) {
        if (left[i] <= right[j]) result.push_back(left[i++]);
        else                     result.push_back(right[j++]);
    }
    // 남은 원소 붙이기
    while (i < left.size())  result.push_back(left[i++]);
    while (j < right.size()) result.push_back(right[j++]);
    return result;
}

// 병합 정렬 (재귀)
vector<int> mergeSort(vector<int> data) {
    if (data.size() <= 1) return data;
    int mid = data.size() / 2;
    auto left  = mergeSort({data.begin(), data.begin() + mid});
    auto right = mergeSort({data.begin() + mid, data.end()});
    return merge(left, right);
}

복잡도 계산:

  • 분할 단계: log₂(n) 번
  • 각 단계 merge 비용: O(n)
  • 총합: O(n log n)

실측 (n = 1000 / 10000 / 100000):

n버블 정렬sort()배수
1,000~10 ms~0.1 ms~100x
10,000~800 ms~1 ms~800x
100,000수십 초~10 ms수천x

→ n이 10배 → 버블은 ~100배 느려짐 / sort()는 ~13배 (O(n log n)).


sort() 커스텀 정렬 — 람다 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 오름차순 (기본)
sort(v.begin(), v.end());

// 내림차순
sort(v.begin(), v.end(), greater<int>());

// 구조체 — 점수 내림차순
sort(students.begin(), students.end(), [](const Student& a, const Student& b) {
    return a.score > b.score;
});

// 복합 기준 — 길이 오름차순, 같으면 사전순
sort(words.begin(), words.end(), [](const string& a, const string& b) {
    if (a.size() != b.size()) return a.size() < b.size();
    return a < b;
});

람다 비교 함수 규칙: return a < b → a가 b보다 앞에 와야 하면 true.
잘못 짜면 Undefined Behavior (엄격한 약순서 위반) → 런타임 크래시 가능.


정렬 → 이진 탐색 복선 (9회차 예고)

1
2
3
4
5
6
7
8
9
10
11
// 정렬 전: 순차 탐색 O(n) — 최악 n번 비교
for (int x : data) { if (x == target) break; }

// 정렬 후: 이진 탐색 O(log n) — 반으로 줄여가며 탐색
int lo = 0, hi = data.size() - 1;
while (lo <= hi) {
    int mid = (lo + hi) / 2;
    if      (data[mid] == target) break;          // 찾음
    else if (data[mid] < target)  lo = mid + 1;   // 오른쪽
    else                          hi = mid - 1;   // 왼쪽
}

n=7 데이터에서 순차 탐색 4번 vs 이진 탐색 2번. 정렬 비용을 투자하면 탐색이 빨라진다.

알고리즘 과제 (다음주 수요일까지):




언리얼 마스터 과제 — 인벤토리 시스템 (필수 + 도전)

프로젝트: D:\Unreal\NBC_JangSik_Inv
참고 챕터: 1-7 (리플렉션) · 3-1 (아이템 설계) · 3-3 (데이터 관리) · 4-1 (UMG)


구조 설계 — Character 직접 구현

Component 분리 없이 AMyCharacter에 인벤토리 로직을 직접 구현. UMG 도전과제를 위해 AInvGameMode(AGameModeBase 상속)를 별도로 만들어 위젯 생성 담당.

1
2
3
AInvGameMode (AGameModeBase)   ← 위젯 생성 · PlayerController 설정
AMyCharacter (ACharacter)      ← 인벤토리 로직 (Bag/ItemDB/OwnedTitles)
WBP_Inventory (UUserWidget)    ← BagList TextBlock / TitleList TextBlock / UseBtn


STEP 1 — FItem USTRUCT + 헤더 선언

include 순서 규칙: generated.h는 파일의 절대 마지막 include, 모든 USTRUCT/UCLASS는 그 에 선언.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// MyCharacter.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "MyCharacter.generated.h"   // ← 마지막 include

USTRUCT(BlueprintType)
struct FItem
{
    GENERATED_BODY()   // ← 없으면 StaticStruct() 미생성 → C2039 에러

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
    FString Name;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
    FString Desc;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
    FName RequiredTitle;   // NAME_None = 조건 없음
};

UCLASS()
class NBC_JANGSIK_INV_API AMyCharacter : public ACharacter
{
    GENERATED_BODY()

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Inventory")
    TArray<FItem> Bag;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory")
    TMap<FString, FItem> ItemDB;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Inventory")
    TSet<FName> OwnedTitles;

    UFUNCTION(BlueprintCallable, Category = "Inventory") void AddItem(FItem Item);
    UFUNCTION(BlueprintCallable, Category = "Inventory") void AcquireTitle(FName Title);
    UFUNCTION(BlueprintPure,     Category = "Inventory") bool HasTitle(FName Title) const;
    UFUNCTION(BlueprintCallable, Category = "Inventory") void UseItem(FString ItemName);
    UFUNCTION(BlueprintCallable, Category = "Inventory") void PrintBag() const;
    UFUNCTION(BlueprintCallable, Category = "Inventory") void UpdateInventoryUI();
};

FName 사용 이유: RequiredTitleOwnedTitles의 타입을 일치시켜 Contains() 직접 비교 가능. FString보다 비교 연산이 빠름.


STEP 2 — CPP 구현 + BeginPlay 통합 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// MyCharacter.cpp
#include "MyCharacter.h"
#include "InvGameMode.h"          // .h가 아닌 .cpp에서 include → 순환참조 방지
#include "Components/TextBlock.h"
#include "Blueprint/UserWidget.h"

void AMyCharacter::BeginPlay()
{
    Super::BeginPlay();

    FItem Sword  = { TEXT("검"),  TEXT("강력한 검"),  TEXT("전사") };
    FItem Potion = { TEXT("물약"), TEXT("체력 회복"), NAME_None   };
    ItemDB.Add(Sword.Name, Sword);
    ItemDB.Add(Potion.Name, Potion);

    AddItem(Sword);    // → Bag에 추가 + UpdateInventoryUI
    AddItem(Potion);
    PrintBag();

    if (FItem* Info = ItemDB.Find(TEXT("검")))
        UE_LOG(LogTemp, Warning, TEXT("[ItemDB] 검: %s"), *Info->Desc);

    UseItem(TEXT("검"));       // 칭호 없음 → 실패
    AcquireTitle(TEXT("전사"));
    UseItem(TEXT("검"));       // 칭호 있음 → 성공
    UseItem(TEXT("물약"));     // 조건 없음 → 성공
}

void AMyCharacter::AddItem(FItem Item)
{
    Bag.Add(Item);
    UpdateInventoryUI();
}

void AMyCharacter::UseItem(FString ItemName)
{
    FItem* Item = ItemDB.Find(ItemName);
    if (!Item) return;

    if (Item->RequiredTitle != NAME_None && !HasTitle(Item->RequiredTitle))
    {
        UE_LOG(LogTemp, Warning, TEXT("[UseItem] %s 불가 — 칭호 '%s' 필요"),
            *ItemName, *Item->RequiredTitle.ToString());
        return;
    }
    UE_LOG(LogTemp, Warning, TEXT("[UseItem] %s 사용 완료"), *ItemName);
}

void AMyCharacter::UpdateInventoryUI()
{
    AInvGameMode* InvGM = Cast<AInvGameMode>(GetWorld()->GetAuthGameMode());
    if (!InvGM) return;

    UUserWidget* Widget = InvGM->GetInventoryWidget();
    if (!Widget) return;

    if (UTextBlock* BagText = Cast<UTextBlock>(Widget->GetWidgetFromName(TEXT("BagList"))))
    {
        FString BagStr;
        for (const FItem& Item : Bag)
            BagStr += Item.Name + TEXT("\n");
        BagText->SetText(FText::FromString(BagStr));
    }

    if (UTextBlock* TitleText = Cast<UTextBlock>(Widget->GetWidgetFromName(TEXT("TitleList"))))
    {
        FString TitleStr;
        for (const FName& Title : OwnedTitles)
            TitleStr += Title.ToString() + TEXT("\n");
        TitleText->SetText(FText::FromString(TitleStr));
    }
}

기대 Output Log 순서:

1
2
3
4
5
6
7
8
[PrintBag] 가방 목록 (2개):
  - 검: 강력한 검
  - 물약: 체력 회복
[ItemDB] 검: 강력한 검
[UseItem] 검 불가 — 칭호 '전사' 필요
[AcquireTitle] 칭호 획득: 전사
[UseItem] 검 사용 완료
[UseItem] 물약 사용 완료


STEP 3 — UMG 도전: InvGameMode + WBP_Inventory

InvGameMode — 위젯 생성 + 마우스 입력 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// InvGameMode.h (AGameModeBase 상속)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI")
TSubclassOf<UUserWidget> InventoryWidgetClass;

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "UI")
UUserWidget* InventoryWidgetInstance;

UFUNCTION(BlueprintPure, Category = "UI")
UUserWidget* GetInventoryWidget() const;

// InvGameMode.cpp — BeginPlay
APlayerController* PC = GetWorld()->GetFirstPlayerController();
InventoryWidgetInstance = CreateWidget<UUserWidget>(PC, InventoryWidgetClass);
if (InventoryWidgetInstance)
{
    InventoryWidgetInstance->AddToViewport();
    PC->SetShowMouseCursor(true);
    PC->SetInputMode(FInputModeGameAndUI());  // 버튼 클릭 허용
}

GameMode에서 CreateWidget 시 주의: this가 PlayerController가 아니므로 GetWorld()->GetFirstPlayerController()를 먼저 가져와서 넘겨야 한다.

WBP_Inventory 구조:

1
2
3
4
Canvas Panel
├── TextBlock "BagList"    ← GetWidgetFromName으로 찾아 SetText
├── TextBlock "TitleList"
└── Button "UseBtn"        → OnClicked 이벤트

에디터 세팅:

  • World Settings → GameMode Override → InvGameMode (또는 BP_InvGameMode)
  • BP_InvGameMode → Inventory Widget Class → WBP_Inventory
  • BP_InvGameMode → Default Pawn Class → BP_MyCharacter


Blueprint 연결 — UseBtn OnClicked

1
2
3
4
5
6
7
[On Clicked (UseBtn)]
    ↓ 실행 핀
[Cast To MyCharacter]
    Object ← [Get Owning Player Pawn]  ← Target: self (Widget)
    ↓ As My Character
[Use Item]
    Item Name: "검"

핵심 실수: Get Player ControllerCast To MyCharacter 는 항상 실패.
PlayerController ≠ Character. Get Owning Player Pawn을 써야 Pawn(Character)을 반환.


디버깅 기록 — 오늘 만난 오류들

오류원인해결
C4430 + C2039: StaticStruct not a member of FItemUSTRUCT가 generated.h 앞에 선언됨include 순서 교정: generated.h → USTRUCT 순으로
함수 중첩 컴파일 오류BeginPlay 닫는 } 누락중괄호 추가
Cast To MyCharacter always fails (Blueprint 경고)Get Player Controller 반환값은 PlayerController, MyCharacter가 아님Get Owning Player Pawn으로 교체
버튼 클릭해도 반응 없음PlayerController가 DefaultPawn을 possess, MyCharacter 아님레벨 배치 MyCharacter → Auto Possess Player: Player 0
GetController()로 캐스팅 실패GameMode에서 GetController() 없음GetWorld()->GetAuthGameMode()로 교체
CreateWidget(this, ...) 오류GameMode는 PlayerController가 아님CreateWidget(PC, ...) 로 PC 명시



Unreal C++ 지식 그래프 구축

기존 CS 면접 그래프(graphify-out/)와 별도로 scrum/unrealc++/graphify-out/에 UE5 C++ 강의 전용 그래프 구축.

항목CS 면접 그래프Unreal C++ 그래프
경로graphify-out/scrum/unrealc++/graphify-out/
노드102111
엣지126139
커뮤니티1310

Unreal 그래프 god 노드 TOP 3:

  1. 강좌목차 — 9 edges (모든 챕터의 진입점)
  2. ABaseItem — 8 edges (인터페이스 ↔ 아이템 시스템 브릿지)
  3. 챕터1-3 Actor 생성삭제 / 챕터1-4 컴포넌트 / 챕터2-1 Character — 각 7 edges

CLAUDE.md에 이중 그래프 참조 규칙 추가:

  • CS 질문 → graphify-out/GRAPH_REPORT.md
  • Unreal 질문 → scrum/unrealc++/graphify-out/GRAPH_REPORT.md



오늘 배운 것 정리

  • generated.h는 마지막 include — 이 규칙 하나만 지키면 UHT 관련 C4430/C2039 대부분 예방 가능

  • GameMode ≠ PlayerController — GameMode에서 CreateWidget은 반드시 GetFirstPlayerController()를 가져와서 넘겨야 하고, 위젯 내부에서 Character를 찾을 때는 Get Owning Player Pawn 사용

  • forward declaration으로 순환참조 방지.h에는 class AInvGameMode; 전방선언, 실제 include는 .cpp에서. generated.h 이후에 다른 include 넣으면 UHT 오작동

  • SetInputMode(FInputModeGameAndUI()) 필수 — 위젯이 뜨더라도 입력모드가 Game Only면 버튼 클릭이 UI에 안 닿음. 마우스 커서 표시(SetShowMouseCursor(true))도 같이 세팅

  • Auto Possess Player — 레벨에 직접 배치한 Pawn은 기본적으로 PlayerController가 빙의하지 않음. DefaultPawnClass 설정 또는 Auto Possess Player: Player 0 중 하나 선택 필요

  • 그래프 2개 운용 — 도메인이 다른 지식은 그래프를 분리해야 god 노드가 의미 있음. CS와 UE5를 하나로 합치면 서로 다른 커뮤니티가 섞여 탐색 품질 저하


참고 링크

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.