[UE5] Longvinter 모작 - 파밍하기, NetOwner

·2023년 3월 30일

UE5

목록 보기
3/5
post-thumbnail

플레이어 Death 시 아이템 떨어뜨리기

// 모닥불과 Player의 거리가 일정 거리안에 들어오면 HP가 증가하고,
// 너무 가까우면 HP가 감소함
void ACampFire::CheckDistanceFromActor_Implementation()
{
	FCollisionQueryParams	param(NAME_None, false, this);

	TArray<FOverlapResult>	ResultMinusHPArray;
	TArray<FOverlapResult>	ResulPlusHPtArray;

	bool CollisionMinusHPEnable = GetWorld()->OverlapMultiByChannel(ResultMinusHPArray,
		GetActorLocation(), FQuat::Identity,
		ECollisionChannel::ECC_GameTraceChannel8,
		FCollisionShape::MakeSphere(150),
		param);

	bool CollisionPlusHPEnable = GetWorld()->OverlapMultiByChannel(ResulPlusHPtArray,
		GetActorLocation(), FQuat::Identity,
		ECollisionChannel::ECC_GameTraceChannel8,
		FCollisionShape::MakeSphere(250),
		param);

#if ENABLE_DRAW_DEBUG
	FColor	DrawMinusHPColor = CollisionMinusHPEnable ? FColor::Red : FColor::Green;

	DrawDebugSphere(GetWorld(), GetActorLocation(),
		150, 20,
		DrawMinusHPColor, false, 0.3f);

	FColor	DrawPlusHPColor = CollisionPlusHPEnable ? FColor::Red : FColor::Blue;

	DrawDebugSphere(GetWorld(), GetActorLocation(),
		250, 20,
		DrawPlusHPColor, false, 0.3f);

#endif

	if (CollisionMinusHPEnable)
	{
		int Range = ResultMinusHPArray.Num();
		for (int i = 0; i < Range; i++)
		{
			ALvPlayer* Player = Cast<ALvPlayer>(ResultMinusHPArray[i].GetActor());
			if (IsValid(Player))
			{
				if(Player->GetCurrentHealth() > 0)
					Player->ServerAttack(Player, 1.f);
			}
		}
	}
	else if (CollisionPlusHPEnable)
	{
		int Range = ResulPlusHPtArray.Num();
		for (int i = 0; i < Range; i++)
		{
			ALvPlayer* Player = Cast<ALvPlayer>(ResulPlusHPtArray[i].GetActor());
			if (IsValid(Player))
			{
				Player->ServerAttack(Player, -1.f);
			}
		}
	}
}
// 
void ALvPlayer::ServerAttack_Implementation(AActor* Actor, float Damage)
{
	Actor->TakeDamage(Damage, FDamageEvent(), GetController(), this);
}

// AActor::TakeDamage를 override한 함수
float ACharacterBase::TakeDamage(float DamageTaken, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	float damageApplied = CurrentHealth - DamageTaken;
	SetCurrentHealth(damageApplied);

	return damageApplied;
}

void ACharacterBase::SetCurrentHealth(float healthValue)
{
	if (GetLocalRole() == ROLE_Authority)
	{
		// Clamp는 Value가 min, max 사이에 있으면 값을 반환
		// min보다 적으면 min을, max보다 크면 max를 반환
		CurrentHealth = FMath::Clamp(healthValue, 0.f, MaxHealth);
		OnHealthUpdate();
	}
}

void ALvPlayer::OnHealthUpdate()
{
	// HP가 0보다 작고, 한번만 실행 가능
	if (GetCurrentHealth() <= 0 && !mOnceCheck)
	{	
		mOnceCheck = true;
		
		ServerSpawnPlaceholder();

		DeleteAllItems();

		Destroy();
	}

	if (GetCurrentHealth() != mPlayerHP)
	{
		mPlayerHP = GetCurrentHealth();
	}
}
// 서버/클라이언트 리플리케이션 함수에는 Net Owner (네트 소유자)가 있는 액터에만 사용 가능
void ALvPlayer::ServerSpawnPlaceholder_Implementation()
{
	TArray<int32> AllItems = mInventoryComponent->GetItems();

	FActorSpawnParameters Param;
	Param.Owner = this;

	APlaceholderActor* Items = GetWorld()->SpawnActor<APlaceholderActor>(mItemClass, GetTransform(), Param);
	Items->ServerAddAllItems(AllItems);
}

void APlaceholderActor::ServerAddAllItems_Implementation(const TArray<int32>& Items)
{
	mPlaceholderComponent->ServerAddAllItems(Items);
}

void ALvPlayer::DeleteAllItems()
{
	TArray<int32> AllItems = mInventoryComponent->GetItems();

	mInventoryComponent->ServerRemoveAllItems(AllItems);
}

void UInventoryComponent::ServerRemoveAllItems_Implementation(const TArray<int32>& ItemIDs)
{
	mItems.RemoveAll(ItemIDs);
}

파밍하기

void ALvPlayer::Click()
{
	// 중략
    if (Hit)
    {
    	APlaceholderActor* Items = Cast<APlaceholderActor>(Result.GetActor());
		if (IsValid(Items))
		{
			PlayerController->GetMainHUD()->GetPlaceholderWidget()->SetPlaceholder(Items);
			PlayerController->GetMainHUD()->GetPlaceholderWidget()->SetVisibility(ESlateVisibility::Visible);
		}
    }
}

// 아이템 클릭 시
void UPlaceholderBase::ItemClick(UObject* Object)
{
	APlayerController* Controller = GetOwningLocalPlayer()->GetPlayerController(GetWorld());

	if (IsValid(Controller))
	{
		ALvPlayer* Character = Cast<ALvPlayer>(Controller->GetCharacter());
		if (IsValid(Character))
		{
        	// 클릭한 객체의 정보를 가져오기
			UItemDataBase* pData = Cast<UItemDataBase>(Object);

			if (IsValid(pData))
			{
				int ItemID = pData->GetItemID();
				
                // ID를 얻어와서 인벤토리에는 추가하고
				Character->GetInventoryComponent()->ServerAddItem(ItemID);
                
                // Placeholder에서는 제거
				mPlaceholder->ServerRemoveItem(ItemID);
			}
		}
	}
}

문제점

mPlaceholder->ServerRemoveItem(ItemID); 함수가 실행되지 않음
-> mPlaceholder의 아이템이 변하지 않음

  • 왼쪽 : Placeholder
  • 오른쪽 : Inventory

Placeholder의 아이템 클릭 시 인벤토리로 전달됨
그러나 Placeholder에서 제거되지 않음🤔

무한대로 아이템을 얻을 수 있는 버그 발생

원인

ALvPlayer::ServerSpawnPlaceholder_Implementation() 함수에서 Placeholder Spawn 시
NetOwner를 설정하지 않음
서버/클라이언트 리플리케이션 함수는 NetOwner (네트 소유자)가 있는 액터에만 사용할 수 있으므로 함수가 호출되지 않음

(참고 문서) https://docs.unrealengine.com/4.26/ko/Resources/ContentExamples/Networking/1_5/

해결

Placeholder Spawn 시 FActorSpawnParameters의 NetOwner를 설정하여 리플리케이션 함수 호출이 가능하도록 변경

  • 왼쪽 : Placeholder
  • 오른쪽 : Inventory

Placeholder의 아이템 클릭 시 인벤토리로 전달되고, Placeholder에서는 제거됨

0개의 댓글