언리얼 인벤토리 시스템과 UMG 위젯 연동
TArray·TMap·TSet이 각자 맡는 자리
목록은 순서가, 아이템 정의는 즉시 조회가, 칭호는 중복 제거가 필요하다 — 인벤토리 하나에 컨테이너 셋이 다 들어가는 이유. generated.h include 순서와 GameMode 위젯 생성에서 걸린 오류까지.
인벤토리는 자료구조 선택이 그대로 기능이 되는 시스템이다. 아이템 목록은 순서가 필요하고, 아이템 정의는 이름으로 즉시 찾아야 하고, 획득한 칭호는 중복이 없어야 한다 — TArray·TMap·TSet이 각각 자리를 잡는 지점이 여기다. 이 글에서는 캐릭터에 인벤토리를 직접 얹어 만든 과정을 이야기하려 한다. 구현은 FItem 구조체 정의부터 UMG 위젯 연동까지고, 트러블슈팅은 컴파일 오류와 “위젯은 떴는데 버튼이 안 눌리는” 문제를 포함한 디버깅 기록이다.
같은 날 푼 CodeKata 19와 정렬 알고리즘 강의 내용은 이 글의 주제와 달라 덜어냈다. 정렬은 list와 sort·LeetCode 912 Sort an Array에 따로 정리돼 있다.
언리얼 마스터 과제 — 인벤토리 시스템 (필수 + 도전)
참고 챕터: 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 사용 이유: RequiredTitle과 OwnedTitles의 타입을 일치시켜 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 Controller → Cast To MyCharacter 는 항상 실패.
PlayerController ≠ Character. Get Owning Player Pawn을 써야 Pawn(Character)을 반환.
트러블슈팅 — 만난 오류들
돌이켜보면 막힌 지점 대부분이 두 갈래였다. 하나는 generated.h include 순서, 다른 하나는 “GameMode는 PlayerController가 아니다”라는 사실을 코드 곳곳에서 다시 확인한 것.
| 오류 | 원인 | 해결 |
|---|---|---|
C4430 + C2039: StaticStruct not a member of FItem | USTRUCT가 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 명시 |
정리 — 이 과제에서 남은 것
generated.h는 마지막 include — 이 규칙 하나만 지키면 UHT(Unreal Header Tool, 리플렉션 코드 생성 도구) 관련 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를 하나로 합치면 서로 다른 커뮤니티가 섞여 탐색 품질 저하
핵심 요약 —
generated.h는 반드시 마지막 include여야 한다는 것, 그리고 GameMode는 PlayerController가 아니므로 위젯 생성 시GetFirstPlayerController()를 넘기고 위젯 안에서는Get Owning Player Pawn으로 캐릭터를 찾아야 한다는 것. 오늘 만난 컴파일 오류와 Blueprint 캐스팅 실패 대부분이 이 두 지점에서 나왔다.