[UE5] 아이템, 체력 UI 개선 및 구현

연하·2024년 7월 1일
0

Trapper

목록 보기
15/32
post-thumbnail

아이템 UI 델리게이트 설정

기존에는 위젯에 텍스트를 설정하는 일을 플레이어가 하고있었는데, 델리게이트를 사용해 플레이어 HUD에서 처리하도록 해주기로 했다.

DECLARE_MULTICAST_DELEGATE_OneParam(FOnItemCountChangedDelegate, int32/*ItemCount*/);

델리게이트를 선언해주고, 플레이어에 멤버변수 FOnItemCountChangedDelegate OnItemChanged; 를 선언해주었다.

// PlayerHUD.h

UCLASS()
class TRAPPERPROJECT_API UPlayerHUD : public UUserWidget
{
	GENERATED_BODY()
	
public:
	UPROPERTY(meta=(BindWidget), VisibleAnywhere, BlueprintReadOnly, Category = "Item")
	TObjectPtr<class UTextBlock> ItemText;

public:
	void GetItemCountChanged(int32 Count);
};

플레이어 HUD에 델리게이트를 수신받을 함수를 만들어주고,

void ATrapperPlayer::CreatePlayerHUD()
{
	if (!IsLocallyControlled())
	{
		return;
	}

	if (IsValid(PlayerHudClass))
	{
		UUserWidget* Hud = CreateWidget<UUserWidget>(this->GetGameInstance(), PlayerHudClass);
		PlayerHud = Cast<UPlayerHUD>(Hud);

		if (PlayerHud)
		{
			PlayerHud->AddToViewport();
			PlayerHud->SetVisibility(ESlateVisibility::Visible);

			OnItemChanged.AddUObject(PlayerHud, &UPlayerHUD::GetItemCountChanged);
            OnItemChanged.Broadcast(0);
		}
	}
}

델리게이트에 바인딩하고, 초기화의 의미로 0을 송신해주었다. 그리고 기존에 SetText() 함수를 호출해주는 곳을 전부 브로드캐스트로 바꿔주면 끝!

HP UI 만들기

더미 UI 위젯을 만들어주고, 바뀌어야 하는 부분들을 변수로 승격시켜주었다.

HP UI 연동하기

public:
	UPROPERTY(ReplicatedUsing = "OnRep_CurrentHP")
	float CurrentHP;

	UFUNCTION()
	void OnRep_CurrentHP();
    
// ------------------------------------------------------------

void ATrapperPlayer::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME(ATrapperPlayer, BoneItemBox);
	DOREPLIFETIME(ATrapperPlayer, CurrentHP);
}

서버에서 체력을 바꿔줄 것이므로, 리플리케이트를 설정해주고 OnRep 함수를 구현해주었다.

void ATrapperPlayer::OnRep_CurrentHP()
{
	TArray<AActor*> OutActors;
	UGameplayStatics::GetAllActorsOfClass(GetWorld(), ATrapperPlayer::StaticClass(), OutActors);

	for (const auto& Pawn : OutActors)
	{
		ATrapperPlayer* TrapperPlayer = Cast<ATrapperPlayer>(Pawn);

		if (TrapperPlayer && TrapperPlayer->IsLocallyControlled())
		{
			TrapperPlayer->OnHPChanged.Broadcast();
		}
	}
}

만약 팀원이 데미지를 입었을 경우, 변수의 값이 바뀌었을 때 호출되는 OnRep_CurrentHP() 함수는 로컬 플레이어가 아닌 팀원 캐릭터에 호출되게 된다. 하지만 우리는 로컬 플레이어의 위젯에 접근하여 값을 바꿔주어야 하므로, 현재 실행되고 있는 게임의 모든 플레이어를 가져와 로컬 플레이어를 찾고, 그 플레이어의 델리게이트를 실행시켜주어야 한다.

DECLARE_MULTICAST_DELEGATE(FOnHPChangedDelegate);

마찬가지로 델리게이트를 등록해주고, 플레이어에 FOnHPChangedDelegate OnHPChanged; 변수를 선언해주었다. 이 델리게이트에 PlayerHUDGetHPChanged() 함수를 바인딩해주었는데, 이 함수는 가지고 있는 HealthBar 위젯의 SetHealthUI() 함수를 호출해준다.

void UPlayerHealthBar::FindPlayer()
{
	if(Player && TeamPlayer) return;

	TArray<AActor*> OutActors;
	UGameplayStatics::GetAllActorsOfClass(GetWorld(), ATrapperPlayer::StaticClass(), OutActors);

	for (const auto& Pawn : OutActors)
	{
		ATrapperPlayer* TrapperPlayer = Cast<ATrapperPlayer>(Pawn);
		if(!TrapperPlayer) continue;

		if (TrapperPlayer->IsLocallyControlled())
		{
			Player = TrapperPlayer;
		}
		else
		{
			TeamPlayer = TrapperPlayer;
		}
	}
}

HealthBar 위젯은 FindPlayer() 함수를 통해 가지고있는 첫번째 플레이어와 두번째 플레이어의 포인터를 갖고있는다. 모든 액터를 가져와 auto문을 돌리고, 로컬 플레이어일 경우 Player 변수에, 타 플레이어일 경우 TeamPlayer 변수에 넣는다.

void UPlayerHealthBar::SetHealthUI()
{
	FindPlayer();

	float Health = Player->CurrentHP;
	float TeamHealth = TeamPlayer->CurrentHP;

	HealthText->SetText(FText::FromString(FString::FromInt(Health) + TEXT(" / 200")));
	TeamHealthText->SetText(FText::FromString(FString::FromInt(TeamHealth) + TEXT(" / 200")));

	float HealthPercent = Health / 200.f;
	if (HealthPercent <= 0.5)
	{
		HealthText->SetColorAndOpacity(FSlateColor(FColor::Red));
	}
	else
	{
		HealthText->SetColorAndOpacity(FSlateColor(FColor::White));
	}

	float TeamHealthPercent = TeamHealth / 200.f;
	if (TeamHealthPercent <= 0.5)
	{
		TeamHealthText->SetColorAndOpacity(FSlateColor(FColor::Red));
	}
	else
	{
		TeamHealthText->SetColorAndOpacity(FSlateColor(FColor::White));
	}

	HealthBar->SetPercent(HealthPercent - 0.04);
	TeamHealthBar->SetPercent(TeamHealthPercent - 0.04);
}

가져온 플레이어 정보를 토대로, 로컬 플레이어의 체력과 팀원의 체력을 가져와 UI를 세팅해주는 함수이다.

float ATrapperPlayer::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	float Damage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
	if (IsDead) return Damage;

	Damage = FMath::Min(CurrentHP, Damage);
	
	if (HasAuthority())
	{
		CurrentHP -= Damage;
		MulticastCharacterDamaged();
	}
	else if (IsLocallyControlled())
	{
		ServerRPCCharacterDamaged(Damage);
	}

	return Damage;
}

void ATrapperPlayer::ServerRPCCharacterDamaged_Implementation(float Damage)
{
	CurrentHP -= Damage;
	MulticastCharacterDamaged();
}

void ATrapperPlayer::MulticastCharacterDamaged_Implementation()
{
	OnRep_CurrentHP();

	if (HasAuthority() && CurrentHP <= 0)
	{
		MulticastCharacterDeath();
		return;
	}

	Alive();
}

이제 TakeDamage() 함수를 보자. 권한을 가질 경우, 체력에서 데미지만큼 감소시켜주고, 멀티캐스트 RPC를 통해 다른 클라이언트에서도 체력이 바뀌도록 해주었다. 서버가 아닌 로컬 플레이어일 경우, ServerRPC를 통해 서버에서 데미지가 바뀌도록 해주고, 마찬가지로 멀티캐스트 RPC를 통해 위젯을 변경해준다.

Alive 함수는 클라이언트에서, Death 함수는 멀티캐스트 RPC를 통해 실행한다. 나는 원래 둘 다 클라이언트에서 실행해도 된다고 생각했었다. 근데 서버에서 체력을 깎은 후 OnRep 함수보다 이게 더 먼저 호출되는건지.. Death 처리가 되지 않고 Alive 처리가 되어 서버에서만 죽는 현상이 자꾸 발생했다. 그래서 그냥 서버가 죽을 때 다 죽게(?) 만들어버렸다. 추후에 원인을 제대로 파악해보고 개선해야겠다.

서버 플레이어가 데미지를 입어도, 클라이언트 플레이어가 데미지를 입어도 정상적으로 UI가 동기화되는 것을 확인할 수 있다.

0개의 댓글