[UNSEEN 리팩토링] 인벤토리 고민

suyoung·2024년 7월 14일

UE5

목록 보기
1/12

이번 UNSEEN 2기 스마일게이트 인턴십을 떨어지면서, 면접 때 들은 제 코드 문제에 대해 고민해보고 수정하기 위해서 이 벨로그를 작성합니다.

첫 번째. SweepMultiByChannel 무거운 작업이다.

UNSEEN 프로그램에서 제공해주는 이득우 교수님의 강의를 들으면서 자연스럽게 사용했던 SweepMultiByChannel이 무거운 작업이라는 것을 면접 이후 알게됐습니다.
그래서, 인벤토리에서 사용하는 SweepMultiByChannel을 이벤트 함수 호출 방법인 Overlap으로 변경하면서, 트레이스 방식에 대해 찾아보고 정리할 예정입니다.

왜? 무거울까에 대해 먼저 생각해보았습니다.

  1. 오버랩은 이벤트를 중심으로 발생, 지속적인 확인이 필요하지 않음.
  2. SweepMultiByChannel 함수는 지정된 경로를 따라 객체를 이동시키면서 충돌을 검사, 여러가지 복잡한 수학적 계산과 충돌 검사를 수행
  3. 인벤토리를 열 때마다 이 함수를 호출하게 되면, 많은 객체를 한번에 처리하기 때문에 비용이 많이 든다.

즉, 이벤트 기반 업데이트이기 때문에, 인벤토리를 열 때마다 재 계산이 필요하지 않음. 그래서 더 효율적이다. SweepMuliByChannel대신에 컴포넌트 오버랩 델리게이트에 OverlapBegin/OverlapEnd 함수를 연결하여, 콜백으로 변경해본다.

변경 예정인 수도 코드를 정리해보면,
1. 인벤토리의 SweepMultiByChannel에서 Overlap 방식으로 변경한다.

2. 인벤토리 주변 아이템 삽입 시 아이템 삭제와 Server 체크 문제를 해결한다.

=> TMap<EItemType, TArray<TObjectPtr>> 자료구조를 사용해서 Begin시 삽입/End 삭제를 수행

void UQLInventoryComponent::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
	AQLItemBox* HitItem = Cast<AQLItemBox>(OtherActor);

	if (HitItem == nullptr)
	{
		return;
	}
	AQLPlayerController* PC = GetController<AQLPlayerController>();

	if (PC == nullptr)
	{
		return;
	}

	UQLItemData* ItemData = CastChecked<UQLItemData>(HitItem->Stat);
	ItemData->CurrentItemCnt = 1; //UI 추가를 위해서 CurrentItemCnt는 항상 1로 고정

	//근처에 있는 아이템 추가 
	if (NearbyItems.Find(ItemData->ItemType))
	{
		NearbyItems[ItemData->ItemType].Push(OtherActor);
	}
	else
	{
		//임시로, TArray를 생성해서, NearbyItems 자료구조에 삽입할 수 있도록 한다.
		TArray<TObjectPtr<AActor>> ActorArray;
		ActorArray.Add(OtherActor);
		NearbyItems.Add(ItemData->ItemType, ActorArray); 
	}
	PC->UpdateNearbyItemEntry(ItemData); //UI 업데이트 
}


void UQLInventoryComponent::OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	AQLItemBox* HitItem = Cast<AQLItemBox>(OtherActor);

	if (HitItem == nullptr)
	{
		return;
	}
	AQLPlayerController* PC = GetController<AQLPlayerController>();

	if (PC == nullptr)
	{
		return;
	}

	UQLItemData* ItemData = CastChecked<UQLItemData>(HitItem->Stat);
  	//근처 아이템이 타입이 있다면, OtherActor를 삭제하면서 주변 아이템을 보이지 않도록 업데이트 한다.
	if (NearbyItems.Find(ItemData->ItemType))
	{
		NearbyItems[ItemData->ItemType].Remove(OtherActor);
	}
	PC->RemoveNearbyItemEntry(ItemData);
}
  1. 아이템 파밍 시 클라이언트와 서버 연동하기
//아이템 사용 코드, 파라미터 자체도 InItemCnt를 받을 필요 없음,. 
//어떤 아이템을 추가했는지에 대한 정보만 넘겨주면 된다.
//클라이언트 부분
void UQLInventoryComponent::AddInventoryByDraggedItem(EItemType InItemId)
{
	AQLPlayerController* PC = GetController<AQLPlayerController>();

	if (PC == nullptr)
	{
		return;
	}

	if (!HasAuthority())
	{
		//현재 개수 추가
		AddItem(InItemId, NearbyItems[InItemId].Num());
	}

	//UI변경
	if (InItemId == EItemType::Bomb)
	{
		PC->UpdateEquipBombUI(true);
	}
	//실제로 아이템이 있는지 검사하기 위해서 서버에게 요청해야함;
	PC->BlinkBag();
	ServerRPCAddInventoryByDraggedItem(InItemId, InItemCnt);
	NearbyItems[InItemId].Empty(); //주변에 있는 ItemId에 대해서 개수를 모두 없앤다. -> Why? Inventory안에 넣었으니깐.
}

//서버는 실제 스탯을 증가시켜야하는 총알 부분에서, 스탯을 증가하고, 나머지는 아이템 크기만 증가시킨다.
void UQLInventoryComponent::ServerRPCAddInventoryByDraggedItem_Implementation(EItemType ItemId)
{
	AQLPlayerController* PC = GetController<AQLPlayerController>();
	AQLCharacterPlayer* Character = GetPawn<AQLCharacterPlayer>();
	if (PC == nullptr|| Character == nullptr)
	{
		return;
	}
	if (NearbyItems[ItemId].Num() == 0)
	{
		return;
	}

	for (const auto& NearbyItem : NearbyItems[ItemId])
	{
		NearbyItem->SetLifeSpan(0.3f);
	}

	if (ItemId == EItemType::Ammo)
	{
		AQLPlayerState* PS = GetPlayerState<AQLPlayerState>();
		if (PS == nullptr)
		{
			return;
		}
		UQLDataManager* DataManager = UGameInstance::GetSubsystem<UQLDataManager>(GetWorld()->GetGameInstance());
		UQLItemData* Data = DataManager->GetItem(ItemId);
		IQLGetItemStat* ItemStat = CastChecked<IQLGetItemStat>(Data);
		PS->SetAmmoStat(ItemStat->GetStat() * NearbyItems[ItemId].Num()); //총알의 개수 * 현재 주운 총알 개수 그리고 Empty로 리셋시켜준다.
	}

  	//아이템 개수 증가
	AddItem(ItemId, NearbyItems[ItemId].Num());

	NearbyItems[ItemId].Empty(); //주변에 있는 아이템을 모두 풀어줌으로써 GC회수 할 수 있도록함.
}
  1. 'F'키를 누르면 아이템 파밍/아이템 버리기 작업

코드를 변경하면서, 발생한 문제 1. MultiSweepByChannel 에서 Overlap으로 변경하면서, Preset 설정을 건드렸기 때문에, 'F'키를 사용한 파밍과 아이템 버리기 작업에서 아이템이 바닥에 닿지 않는 문제가 발생했다.

이 문제는 간단하게 Collision Preset을 설정해주면 되는 문제였기에, 간단하게 설명한다.

					*** 트레이스를 사용할 때 채널이 Block 되어있어야 작동한다. ***

저는 아이템 파밍과 바닥에 아이템을 놓기 위해서, LineTrace를 사용합니다.
수도 코드로 말하면,

  • 'F'키를 눌렀을 때 카메라가 바라보는 방향으로 SearchRange 길이 만큼 트레이스(Channel - QLITEMACTION)를 쏘고, 아이템이 있다면, 아이템을 줍기를 수행한다.

  • 아이템을 버릴 시 아이템을 생성한 위치에서 Actor의 UpVector 반대방향으로 라인트레이스(Channel - QLGROUND)를 쏘고, 바닥이 있다면, 그 위치에 ItemBox의 Z값 2분의 1만큼 올려서 위치시킨다.

  1. 코드를 변경하면서, 발생한 문제 2. 아이템을 버렸을 때, 주변 아이템의 개수가 현재 인벤토리 아이템 *2 로 등장한다.

문제점의 이유 : UI 상에서 Ground로 버릴 때 GroundItem Listview에 아이템을 추가해서 버리는데, 새로 아이템이 생성되면서, 플레이어와 Overlap이 발생하여, Listview에 추가된다.
즉, 이중으로 증가하는 문제점을 가지고 있다.

이 문제는 간단하게 Ground로 버릴 때 GroundItem의 Listview에 추가하지 않으면 된다.
과거엔 MultiSweep을 하기 전까지 아이템의 개수를 알 수 없기 때문에 이 방법을 사용했으나, 아이템이 생성되면서, 오버랩이 발생하기 때문에 해당 코드는 없어져야한다.

void UQLInventory::UpdateNearbyItemEntryByDraggedItem(UObject* InItem)
{
	//GroundItem->AddItem(InItem);  이전 코드에서는 GroundItem에 추가했지만, 충돌에 의해서
	//UpdateNearbyItemEntry 함수에 의해 Entry가 추가되었기 때문에 실제 추가하지 않는다.
	UQLItemData* InItemInfo = Cast<UQLItemData>(InItem);
	//PC호출
	AQLPlayerController* PC = CastChecked<AQLPlayerController>(GetOwningPlayer());
	PC->AddGroundByDraggedItem(InItemInfo->ItemType);
}

MultiSweepByChannel 에서 Overlap으로 변경 완료했다.
앞으로 개발을 할 때에는 얼마나 비용이 드는가에 대해 좀 더 생각해보고, 코드를 작성해보는걸 목표로 하며 이번 회고는 끝냅니다!

다음 리팩토링, 제 2편 PlayerController에 있는 모든 UI 업데이트를 HUD로 옮긴다.

profile
게임 클라이언트 프로그래머

0개의 댓글