인벤토리 UI에 아이템 이미지를 표시하려면, 각 아이템이 자신의 아이콘을 가지고 있어야 합니다. AItemBase에 아이콘 변수와 Getter를 추가했습니다.
왜 UTexture2D가 아닌 UMaterialInterface?
텍스처를 직접 쓰면 회전·색조 변화 같은 효과를 주기 어렵다. 머티리얼로 감싸면 파라미터 하나로 회전 애니메이션, 색 보정 등을 자유롭게 적용할 수 있다.
텍스처를 직접 쓰면 회전·색조 변화 같은 효과를 주기 어렵다. 머티리얼로 감싸면 파라미터 하나로 회전 애니메이션, 색 보정 등을 자유롭게 적용할 수 있다.
// ItemBase.h
// protected 영역에 추가
UPROPERTY(EditAnywhere, Category = "Item Info | Icon")
UMaterialInstance* Icon;
// public 영역에 Getter 선언
UMaterialInstance* GetIcon();
// ItemBase.cpp
UMaterialInstance* AItemBase::GetIcon()
{
return Icon;
}
2. 아이콘 머티리얼 세팅 (에디터)
C++ 작업 후, 에디터에서 아이콘에 사용할 머티리얼을 제작했습니다.
1.텍스처 임포트
- AK-47, 수류탄, 대검 아이콘 PNG 임포트 → Texture Group을 UI로 설정 (UI 렌더링 최적화)
2.베이스 머티리얼 M_IconBase 생성
- Material Domain → User Interface / Blend Mode → Translucent
- Custom Rotator 노드 + 텍스처 파라미터화 → 회전 효과를 인스턴스에서 조절 가능하게 구성
3.아이템별 머티리얼 인스턴스 생성
- M_IconBase를 상속 → MI_AK47, MI_Grenade, MI_Knife 각각 생성
- 각 인스턴스에 해당 아이콘 텍스처 할당 후 BP_아이템의 Icon 슬롯에 연결
3. ItemWidget C++ 클래스 구성
인벤토리 칸 하나에 아이템 아이콘을 표시하는 UItemWidget을 구현했다. BindWidget으로 C++ 변수와 블루프린트 위젯을 연결합니다.
// ItemWidget.h
class UCanvasPanel;
class USizeBox;
class UBorder;
class UImage;
UCLASS()
class UItemWidget : public UUserWidget
{
GENERATED_BODY()
protected:
// BindWidget — 블루프린트의 위젯 이름과 반드시 일치해야 함
UPROPERTY(VisibleAnywhere, meta = (BindWidget))
UCanvasPanel* Canvas;
UPROPERTY(VisibleAnywhere, meta = (BindWidget))
USizeBox* BackgroundSizeBox;
UPROPERTY(VisibleAnywhere, meta = (BindWidget))
UBorder* BackgroundBorder;
UPROPERTY(VisibleAnywhere, meta = (BindWidget))
UImage* ItemImage;
AIntentorySystemCppCharacter* CharacterReference;
FVector2D Size;
virtual void NativeConstruct() override;
void Refresh(AActor* ItemToAdd);
};
// ItemWidget.cpp
void UItemWidget::NativeConstruct()
{
Super::NativeConstruct();
CharacterReference = Cast<AIntentorySystemCppCharacter>(
UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
if (CharacterReference) {
Refresh(CharacterReference->ItemToAdd);
}
}
void UItemWidget::Refresh(AActor* ItemToAdd)
{
AItemBase* Item = Cast<AItemBase>(ItemToAdd);
if (!Item) return;
// 아이콘 머티리얼을 Image에 적용
ItemImage->SetBrushFromMaterial(Item->GetIcon());
// 아이템 크기(타일 수 × TileSize)로 위젯 사이즈 결정
Size = FVector2D(
Item->GetDimensions().X * CharacterReference->InventoryComponent->TileSize,
Item->GetDimensions().Y * CharacterReference->InventoryComponent->TileSize
);
BackgroundSizeBox->SetWidthOverride(Size.X);
BackgroundSizeBox->SetHeightOverride(Size.Y);
UCanvasPanelSlot* ImageAsCanvasSlot =
UWidgetLayoutLibrary::SlotAsCanvasSlot(ItemImage);
ImageAsCanvasSlot->SetSize(Size);
}
BindWidget 바인딩 규칙
C++에서 선언한 변수명(
C++에서 선언한 변수명(
BackgroundSizeBox, ItemImage 등)과 블루프린트 BPW_Item에서 배치한 위젯의 이름이 완전히 동일해야 컴파일 에러 없이 바인딩됩니다.4. InventoryComponent — TMap 기반 아이템 관리
단순 배열(TArray)만으로는 "이 아이템이 인벤토리의 어느 좌표에 있는가"를 빠르게 알 수 없다. 아이템 포인터를 키, 타일 좌표를 값으로 하는 TMap을 추가했습니다.
<TArray만 사용할 때>
Items[index] = ItemPtr 아이템의 위치를 구하려면 전체 배열을 순회해야 해서 비효율적
<TMap 추가 후>
AllItems[ItemPtr] = FIntPoint(X, Y) 포인터 하나로 타일 좌표를 O(1)에 바로 조회 가능
// InventoryComponent.h
bool AddedItem = false; // UI 업데이트 트리거 플래그
protected:
TMap<AItemBase*, FIntPoint> AllItems; // 아이템 → 타일 좌표
// InventoryComponent.cpp — AddItemAt()
void UInventoryComponent::AddItemAt(AItemBase* ItemToAdd, int32 TopLeftIndex)
{
FIntPoint Dimensions = ItemToAdd->GetDimensions();
FIntPoint Tile = IndexToTile(TopLeftIndex);
for (int32 i = Tile.X; i <= Tile.X + Dimensions.X - 1; i++) {
for (int32 j = Tile.Y; j <= Tile.Y + Dimensions.Y - 1; j++) {
Items[TileToIndex(FIntPoint(i, j))] = ItemToAdd;
}
}
AddedItem = true; // ← Tick에서 Refresh() 호출 트리거
}
TMap<AItemBase*, FIntPoint> UInventoryComponent::GetAllItems()
{
for (int32 i = 0; i < Items.Num(); i++) {
if (Items[i] && !AllItems.Contains(Items[i])) {
AllItems.Add(Items[i], IndexToTile(i));
}
}
return AllItems;
}
// InventoryComponent.cpp — TickComponent()
void UInventoryComponent::TickComponent(...)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (AddedItem) {
if (InventoryGridWidgetReference) {
InventoryGridWidgetReference->Refresh();
}
AddedItem = false;
}
}
왜 Tick에서 Refresh를 호출하나?
OnBeginOverlap 콜백 내부에서 위젯을 직접 갱신하면 물리/콜리전 처리 도중 UI 코드가 실행되어 의도치 않은 순서 문제가 생길 수 있다. 플래그를 세우고 Tick에서 처리하면 프레임 단위로 안전하게 UI를 갱신할 수 있습니다.5. InventoryGridWidget — Refresh() 로직
아이템이 추가될 때마다 그리드 캔버스를 전부 지우고 현재 인벤토리 상태를 다시 그립니다.
// InventoryGridWidget.cpp — Refresh()
void UInventoryGridWidget::Refresh()
{
// 1. 기존 위젯 전부 제거
GridCanvasPanel->ClearChildren();
// 2. 현재 인벤토리의 아이템 목록 가져오기
TArray<AItemBase*> Keys;
InventoryComponent->GetAllItems().GetKeys(Keys);
if (!CharacterReference->ItemWidgetClass) return;
for (AItemBase* AddedItem : Keys)
{
// 3. 아이템마다 새 위젯 생성
UUserWidget* NewItemWidget = CreateWidget(
GetWorld(), CharacterReference->ItemWidgetClass);
NewItemWidget->SetOwningPlayer(GetOwningPlayer());
// 4. 타일 좌표 → 픽셀 좌표 변환
int32 X = InventoryComponent->GetAllItems()[AddedItem].X
* InventoryComponent->TileSize;
int32 Y = InventoryComponent->GetAllItems()[AddedItem].Y
* InventoryComponent->TileSize;
// 5. 캔버스에 추가 후 위치 설정
UPanelSlot* NewSlot = GridCanvasPanel->AddChild(NewItemWidget);
Cast<UCanvasPanelSlot>(NewSlot)->SetAutoSize(true);
Cast<UCanvasPanelSlot>(NewSlot)->SetPosition(FVector2D(X, Y));
}
}
1.ClearChildren() — 중복 방지
- Refresh가 호출될 때마다 기존 위젯을 전부 제거. 없으면 아이템을 먹을 때마다 위젯이 쌓임
2.GetAllItems() — TMap에서 키 목록 추출
- GetKeys()로 현재 인벤토리에 있는 아이템 포인터 목록을 가져옴
3.CreateWidget() — 아이템마다 개별 생성
- 루프 안에서 매번 새 위젯을 생성해야 함. 루프 밖에서 1개 생성 후 재사용하면 UMG 특성상 마지막 부모에만 붙음
4.타일 좌표 → 픽셀 좌표 변환
- TileX × TileSize, TileY × TileSize → 캔버스 위의 절대 픽셀 위치
5.SlotAsCanvasSlot() → SetPosition()
- AddChild()의 반환값을 UCanvasPanelSlot으로 캐스팅해야 위치/크기 조절 함수를 쓸 수 있음
6. 에디터 연결 및 테스트
코드 빌드 후 에디터에서 두 가지를 연결해야 정상 동작합니다.
BP_ThirdPersonCharacter Details 패널 확인
- Item Widget Class → BPW_Item (아이템 1칸짜리 위젯)
- Inventory Widget Class → BPW_Inventory (전체 인벤토리 위젯)
둘 중 하나라도 None이거나 잘못 설정하면 위젯이 생성되지 않는다. 특히 Item Widget Class에 InventoryWidget을 잘못 넣는 실수를 주의할 것.
테스트 결과
- AK-47 획득 → 인벤토리 그리드에 2×1 크기로 아이콘 표시
- 대검 획득 → 1×1 크기로 아이콘 표시
- 아이템을 먹을 때마다 Refresh가 트리거되어 실시간으로 인벤토리 업데이트 확인

7. 오늘 배운 점 & 회고
- UMaterialInterface vs UTexture2D — 머티리얼로 감싸면 파라미터 기반 회전·색조 제어가 가능해진다
- BindWidget — C++ 변수명과 블루프린트 위젯 이름이 정확히 일치해야 바인딩된다
- TMap<AItemBase*, FIntPoint> — 아이템 위치를 O(1)로 조회할 수 있어 Refresh 로직에서 유용하다
- AddedItem 플래그 패턴 — Overlap 콜백 안이 아닌 Tick에서 UI를 갱신하면 실행 순서가 안전해진다
- CreateWidget은 루프 안에서 — UMG 위젯은 부모를 하나만 가질 수 있으므로 아이템마다 새로 생성해야 한다
- SlotAsCanvasSlot() 캐스팅 — AddChild() 반환값을 UCanvasPanelSlot으로 캐스팅해야 위치/크기를 지정할 수 있다
'Unreal' 카테고리의 다른 글
| C++을 사용한 언리얼엔진 인벤토리 시스템 Final (0) | 2026.03.24 |
|---|---|
| C++을 사용한 언리얼엔진 인벤토리 시스템 Part.6 (0) | 2026.03.20 |
| C++을 사용한 언리얼엔진 인벤토리 시스템 Part.4 (0) | 2026.03.13 |
| C++을 사용한 언리얼엔진 인벤토리 시스템 Part.3 (0) | 2026.03.12 |
| C++을 사용한 언리얼엔진 인벤토리 시스템 Part.2 (0) | 2026.03.10 |