가독성: UI 구현 시 각 UI 정의 코드를 일관된 형식에 맞춰 작성함으로써, 가독성과 유지 보수성 향상을 목표합니다.
Event-Driven: Event-Driven 기반 설계를 통해 각 UI 클래스 내부 Tick 메서드 사용을 억제합니다. 더불어, Event-Driven 기반 설계를 통해 커플링 완화, 그리고 타입 안정성을 제공합니다.
Pooling: 각 UI는 CreateWidget 호출을 통해 게임 시작 시점에 생성되어 지속적으로 재활용되며, 각 UI는 가시성 변화 기능을 통해 해당 UI를 화면에 나타낼 것인지, 숨길 것인지 결정됩니다.
2. 관련 이슈
#49, Feature: Inventory 설계 및 관련 UI 제작
#68, Feature: Inventory UI 추가 기능
3. 형식
0. 생성 시점
- 생성 시점에 호출되는 생명 주기 함수들 활용(NativeOnInitialized, ...etc) '외부 바인딩' 수행
1. 자식 생성(지연 초기화, 비동기 초기화)
- 객체 생성 이후 생성을 담당하는 객체가 발행하는 ReqeustStartInitBy~ 이벤트에 각 객체의 초기화 함수를 콜백으로 등록
- '외부 바인딩' 작업이지만, 초기화 요청 이벤트는 예외적으로 생성을 담당하는 쪽에서 해줍니다.
- 생성된 객체에 대하여 '내부 바인딩' 수행
- 각 객체의 초기화 완료 이벤트 구독
- +부가 작업
- 모든 객체 생성 이후 RequestStartInitBy~ 이벤트 호출
2. 초기화
- 각 객체의 지연 초기화 작업이 이루어지는 곳
- 0번 작업 반복
- 각 객체의 자식 위젯에 대히여 1번 작업 반복
3. 초기화 완료 체크
- 각 객체의 초기화 완료 이벤트를 구독하는 각각의 콜백 함수를 활용하여 자식 위젯들의 초기화 완료 체크
4. 구현 목표
지연 초기화(의존성 정의): 각 객체의 생성을 담당하는 쪽에서 지연 초기화를 수행합니다. 각 객체의 초기화 작업이 내부적으로 제각각 수행되면서, 서로가 서로의 이벤트를 바인딩하는 의존 관계가 존재할 경우 일관성 있는 결과를 얻어내기 어려웠습니다. 따라서, 지연 초기화를 통해 모든 객체 간 의존 관계를 정의하고, 이후 지연 초기화 작업을 일괄적으로 처리함으로써 기대한 결과를 얻어내고, 이러한 초기화 작업은 비동기적으로 수행되어 성능 향상에도 도움이 되었습니다.
로딩/세이브 시스템: UI는 대체로 여러 컴포넌트들의 내부 정보를 화면에 나타냅니다. 로딩/세이브 기능 활용을 위해 외부 컴포넌트들의 로딩 작업 이전에 생성 및 초기화 작업이 모두 완료되어야 합니다. 따라서, 위 형식을 지켜 UI의 초기화 시작/완료 시점을 관리해, 이들이 대표하는 컴포넌트들의 로딩 시작 시점을 명시적으로 확인할 수 있도록 합니다.
코드 일관성: 일관된 코드 형식을 유지하여 UI 관련 코드를 작성함으로써, 타 기능을 제작하는 개발 인원들의 코드 리뷰를 원활하게 도와줍니다.
디자이너와의 협업: 기획 단계에서 작성한 UI 관련 이미지들을 반영하여 UI들의 위치, 크기, 그리고 색상 등의 작업을 Editor 상에서 쉽게 변경할 수 있도록 열어줍니다.
#3. 초기화
1. 외부 바인딩
void UItemSlots::ExternalBindToInputComp()
{
//@World
UWorld* World = GetWorld();
if (!World)
{
UE_LOGFMT(LogItemSlots, Error, "{0}: World is null", __FUNCTION__);
return;
}
//@PC
APlayerController* PC = World->GetFirstPlayerController();
if (!PC)
{
UE_LOGFMT(LogItemSlots, Error, "{0}: PlayerController is null", __FUNCTION__);
return;
}
//@Input Comp
UBaseInputComponent* BaseInputComp = Cast<UBaseInputComponent>(PC->InputComponent);
if (!BaseInputComp)
{
UE_LOGFMT(LogItemSlots, Error, "{0}: Input Component를 찾을 수 없습니다", __FUNCTION__);
return;
}
//@TODO: Binding
BaseInputComp->UIInputTagTriggered.AddUFunction(this, "OnUIInputTagTriggered");
BaseInputComp->UIInputTagReleased.AddUFunction(this, "OnUIInputTagReleased");
}
외부 바인딩 작업은 피 의존 객체, 즉 외부 컴포넌트에서 발행하는 이벤트에 콜백 함수를 등록하고자 하는 쪽에서 수행합니다. 그리고, 이 외부 바인딩 작업은 의존 관계에 있는 두 객체가 모두 생성된 이후 가장 빠른 시점에 수행합니다. 클래스 외부의 서로 다른 유형의 클래스를 가져오는 작업이기 때문에, 코드가 조금 복잡해집니다. 따라서, 생성된 시점에 구독해야 할 모든 이벤트에 대하여 바인딩을 수행해 주어, 런 타임에 발생하는 이벤트에도 별도의 추가 작업 없이 관련 작업을 처리할 수 있도록 했습니다.
2. 호출 시점
생성 시점이 동일한 경우: 생성 이후에 호출되는 UUserWidget이 제공하는 생명 주기 함수에서 외부 바인딩 진행
생성 시점이 다를 경우: 외부 UI 객체의 자식 UI 객체를 가져오는 것이 쉽지 않습니다. 따라서, 비교적 가져오기 쉬운 부모 UI의 자식 UI들의 초기화 완료를 알리는 이벤트에 콜백을 등록하고, 콜백 함수 호출 시 자식 UI를 인자로 전달받아서 외부 바인딩을 진행합니다.
#4. 자식 생성
1. 초기화 요청 이벤트
void UItemSlots::InitializeItemSlots()
{
//@Item Slots
CreateItemSlots();
//@초기화 요청 이벤트
RequestStartInitByItemSlots.Broadcast();
}
void UItemSlots::CreateItemSlots()
{
UE_LOGFMT(LogItemSlots, Log, "아이템 슬롯 생성 시작");
//@Interactable Item Slot 블루프린트 클래스, Item Slot Box
if (!ensureMsgf(InteractableItemSlotClass && ItemSlotBox, TEXT("InteractableItemSlotClass 또는 ItemSlots가 유효하지 않습니다.")))
{
UE_LOGFMT(LogItemSlots, Error, "아이템 슬롯 생성 실패: InteractableItemSlotClass 또는 ItemSlotBox가 유효하지 않음");
return;
}
//@Clear Children
ItemSlotBox->ClearChildren();
int32 TotalSlots = DefaultRows * MaxItemSlotsPerRow;
int32 CurrentSlot = 0;
for (int32 Row = 0; Row < MaxRows; ++Row)
{
UHorizontalBox* HorizontalBox = NewObject<UHorizontalBox>(this);
UVerticalBoxSlot* VerticalBoxSlot = ItemSlotBox->AddChildToVerticalBox(HorizontalBox);
if (VerticalBoxSlot)
{
VerticalBoxSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
VerticalBoxSlot->SetHorizontalAlignment(HAlign_Fill);
VerticalBoxSlot->SetVerticalAlignment(VAlign_Fill);
VerticalBoxSlot->SetPadding(PaddingBetweenRows);
}
if (Row < DefaultRows)
{
for (int8 SlotIndex = 0; SlotIndex < MaxItemSlotsPerRow && CurrentSlot < TotalSlots; ++SlotIndex, ++CurrentSlot)
{
UInteractableItemSlot* ItemSlot = CreateWidget<UInteractableItemSlot>(this, InteractableItemSlotClass);
if (ItemSlot)
{
RequestStartInitByItemSlots.AddUFunction(ItemSlot, "InitializeItemSlot");
CancelItemSlotButton.AddUFunction(ItemSlot, "ItemSlotButtonCanceledNotified");
if (CurrentSlot == TotalSlots - 1)
{
InternalBindingToItemSlot(ItemSlot, true);
}
else
{
InternalBindingToItemSlot(ItemSlot);
}
UHorizontalBoxSlot* BoxSlot = HorizontalBox->AddChildToHorizontalBox(ItemSlot);
if (BoxSlot)
{
FSlateChildSize SlateChildSize;
SlateChildSize.SizeRule = ESlateSizeRule::Fill;
SlateChildSize.Value = 1.f;
//@Size
BoxSlot->SetSize(SlateChildSize);
//@Alignment
BoxSlot->SetHorizontalAlignment(HAlign_Fill);
BoxSlot->SetVerticalAlignment(VAlign_Fill);
//@Padding
BoxSlot->SetPadding(PaddingBetweenItemSlots);
}
}
else
{
UE_LOGFMT(LogItemSlots, Error, "InteractableItemSlot 생성 실패: 슬롯 {0}", CurrentSlot + 1);
}
}
}
else
{
// DefaultRows 이후의 행에 대해 Spacer 추가
USpacer* Spacer = NewObject<USpacer>(this);
if (!Spacer)
{
UE_LOGFMT(LogItemSlots, Error, "Spacer 생성 실패!");
return;
}
UVerticalBoxSlot* SpacerSlot = ItemSlotBox->AddChildToVerticalBox(Spacer);
if (SpacerSlot)
{
//@FSlateChildSize
FSlateChildSize SlateChildSize;
SlateChildSize.SizeRule = ESlateSizeRule::Fill;
SlateChildSize.Value = 0.5f;
//@Size
SpacerSlot->SetSize(SlateChildSize);
//@Alignment
SpacerSlot->SetHorizontalAlignment(HAlign_Fill);
SpacerSlot->SetVerticalAlignment(VAlign_Fill);
}
UE_LOGFMT(LogItemSlots, Verbose, "추가 행 {0}에 Spacer 추가 완료", Row + 1);
}
}
//@초기 상태로 설정
ResetItemSlots();
UE_LOGFMT(LogItemSlots, Log, "ItemSlots가 성공적으로 생성되었습니다. 총 슬롯 수: {0}, 기본 행 개수: {1}, 최대 행 개수: {2}, 열 개수: {3}",
TotalSlots, DefaultRows, MaxRows, MaxItemSlotsPerRow);
}
초기화 요청 이벤트는 자식 UI 생성을 담당하는 쪽에서 발행하고, 객체 생성 시 해당 이벤트에 자식 UI의 초기화 함수(Initialize~,... etc)를 콜백으로 등록합니다. 모든 자식 UI의 생성을 마치고, 초기화 요청 이벤트를 호출하여 지연 초기화 작업을 수행합니다.