UE5 Issues : 데디케이트 서버 UI에 관해

2025. 6. 12. 12:31·Unreal Engine

멀티플레이 게임에서 UI를 개발함에 있어서 고려할 만한 점이 있다.

 

Owner설정

  • UI의 Owner
  • UI에 띄울 정보를 가질 Owner 

보이는 곳

  • Owner에게만 보여줄 것인가
  • 다른 유저들에게도 보여줄 것인가

위젯에서 표시할 값이 어디에서 변하는가

  • 서버에서 변경
  • 클라이언트에서 변경

1. Owner 설정과 보여지는 곳


예를 들어, 우리가 자신의 체력 바를 만든다고 했을 때를 생각해 보자

  • UI의 Owner는 나 자신이고, 띄울 정보의 Owner도 나 자신이다.
  • 보여지는 곳은 나 자신이 될 것이다.
  • 이 경우는 크게 생각할 문제가 없다.

그러나 우리가 다른 플레이어의 체력 바를 또한 띄우고 싶다면

  • UI의 Owner는 나 자신이지만, 띄울 정보는 다른 플레이어의 정보이다.
  • 보여지는 곳은 여전히 나 자신이 된다.
  • 이 경우 해당 Widget을 초기화 함에 있어서 다른 플레이어의 정보를 통해서 초기화 해주어야 한다.

아래의 코드를 보면

  • 위젯 이름 설명
    • HP_BoardWidget은 우리 팀원의 체력을 붙여서 띄워줄 VertexBox를 가지고 있는 클래스이다.
    • HP_Widget의 경우는 각 팀원들의 체력을 나타내는 클래스이다.
  • 코드 설명
    • 각각의 HP_Widget의 정보의 Owner는 나를 제외한 다른 플레이어이다.
    • 그렇게 때문에 GameState의 PlayerArray를 통해 모든 플레이어의 정보 순환하면서 Owner를 각 플레이어로 지정해주고 부모 Widget인 HP_BoardWidget에 붙여준다.
void UGS_HPBoardWidget::InitBoardWidget()
{
	if (!IsValid(HPWidgetClass) || !IsValid(HPWidgetList))
	{
		return;
	}
	
	HPWidgetList->ClearChildren();

	AGameStateBase* GS = UGameplayStatics::GetGameState(this);
	if (IsValid(GS))
	{
		TArray<APlayerState*> PSA = GS->PlayerArray; 
		for (APlayerState* PS : PSA)
		{
			AGS_PlayerState* GSPS = Cast<AGS_PlayerState>(PS);
			if (IsValid(GSPS))
			{
				UGS_HPWidget* HPWidget = CreateWidget<UGS_HPWidget>(this, HPWidgetClass);
				if (IsValid(HPWidget))
				{
					AGS_Character* Character = Cast<AGS_Character>(GSPS->GetPawn());

					//자기 자신 제외
					if (IsValid(Character) && Character != OwningCharacter)
					{
						//가디언 제외
						if (GSPS->CurrentPlayerRole == EPlayerRole::PR_Guardian)
						{
							continue;
						}
						
						HPWidget->SetOwningActor(Character);
						HPWidget->InitializeHPWidget(Character->GetStatComp());
						HPWidgetList->AddChildToVerticalBox(HPWidget);
					}
				}
			}
		}
	}	
}
  • 부모 Widget인 HP_BoardWidget의 Owner는 나 자신이기 때문에 아래와 같이 생성자에서 OwningActor를 GetOwningPlayer()를 통해 지정해준다.
void UGS_HPBoardWidget::NativeConstruct()
{
	Super::NativeConstruct();

	if (IsValid(GetOwningPlayer()->GetPawn()))
	{
		OwningCharacter = Cast<AGS_Character>(GetOwningPlayer()->GetPawn());
	}
    ...
}

 

 

2. 어디에서 값이 변경되는가


두가지 경우가 있을 수 있다.

  • 값이 클라이언트에서 변경
  • 혹은 서버에서 변경

값이 클라이언트에서 변하고, 위젯 Owner만 값을 보는 경우는 크게 생각할 것이 없다.

그러나, 값이 서버에서 변경되었을 때 모든 클라이언트가 해당 값을 보고 싶은 경우가 많다.

  •  보스 몬스터의 체력과 피버 게이지
  • 다른 플레이어의 체력 등...

현재 진행하는 프로젝트를 예를 들면, 보스 몬스터의 체력과 피버 게이지를 생각해 볼 수 있다.

빨간색 화살표가 체력, 파란색 화살표가 피버 게이지

 

2 - 1. 보스 몬스터의 체력

  • 플레이어의 체력은 변수 자체가 replicated된 상태이다.
    • 주의할 점은 체력의 변경은 주요 로직이므로 "서버"에서 진행된다.
    • 그렇기 때문에, 변경된 순간 클라이언트 쪽에서도 값을 업데이트 해줘야한다.
    • 이 경우 ReplicatedUsing 키워드를 통해서 클라이언트로 값을 알려주었다.
  • 또한 해당 체력을 가지고 있는 Stat 컴포넌트가 변수는 delegate를 통해 연동되어서 값이 바뀔 때마다 위젯의 함수를 호출하여 체력 게이지의 값을 변경해준다.
  • 로직
    • StatComp와 HPWidget을 Delegate 바인딩 하기
    • StatComp에 존재하는 CurrentHealth 변수가 SetCurrentHealth 를 통해 변경되면 BroadCast를 통해 위젯의 업데이트 함수가 call된다.
    • 이때 OnRep 함수를 통해 클라이언트에서도 BroadCast하게 되면서 모든 클라이언트 위젯이 업데이트 된다.
  • 이때 주의할 점은 OnRep을 통해서 클라이언트에 변경된 값을 알려주지 않는다면, 위젯이 업데이트되지 않는다
//GS_StatComp.h
DECLARE_MULTICAST_DELEGATE_OneParam(FOnCurrentHPChangedDelegate, UGS_StatComp*);

UPROPERTY(ReplicatedUsing = OnRep_CurrentHealth)
float CurrentHealth;

//GS_StatComp.cpp
void UGS_StatComp::OnRep_CurrentHealth()
{
	OnCurrentHPChanged.Broadcast(this);
}

void UGS_StatComp::SetCurrentHealth(float InHealth, bool bIsHealing)
{
	...
        CurrentHealth = InHealth;
	OnCurrentHPChanged.Broadcast(this);
    
    ...
}

//GS_Character.cpp
void AGS_Character::SetHPBarWidget(UGS_HPWidget* InHPBarWidget)
{
	UGS_HPWidget* HPBarWidget = Cast<UGS_HPWidget>(InHPBarWidget);
	if (IsValid(HPBarWidget))
	{
		HPBarWidget->InitializeHPWidget(GetStatComp());
		StatComp->OnCurrentHPChanged.AddUObject(HPBarWidget, &UGS_HPWidget::OnCurrentHPBarChanged);
	}
}

//GS_HPWidget.cpp
void UGS_HPWidget::NativeConstruct()
{
	Super::NativeConstruct();

	if (!IsValid(OwningCharacter))
	{
		OwningCharacter = Cast<AGS_Character>(GetOwningPlayer()->GetPawn());
	}
	
	if (IsValid(OwningCharacter))
	{
		OwningCharacter->SetHPBarWidget(this);
	}
}
void UGS_HPWidget::OnCurrentHPBarChanged(UGS_StatComp* InStatComp)
{
 	HPBarWidget->SetPercent(InStatComp->GetCurrentHealth()/InStatComp->GetMaxHealth());
}

 

2 - 2. 보스 몬스터의 피버 게이지

  • 피버 게이지의 경우 변수를 리플리케이트하지 않았다.
  • 그렇기 때문에, 이 값들을 다른 클라이언트에게 모두 알려주기 위해서 Multicast를 사용하였다.
  • 체력과 거의 유사한 구조로 구성되었지만, 값을 변경해주는 함수를 Multicast로 만든 것이 차이점이다.
//GS_Drakhar.h
DECLARE_MULTICAST_DELEGATE_OneParam(FOnCurrentFeverGageChangedDelegate, float);

float CurrentFeverGage;

//GS_Drakhar.cpp
void AGS_Drakhar::SetFeverGageWidget(UGS_DrakharFeverGauge* InDrakharFeverGageWidget)
{
	UGS_DrakharFeverGauge* DrakharFeverGaugeWidget = Cast<UGS_DrakharFeverGauge>(InDrakharFeverGageWidget);
	if (IsValid(DrakharFeverGaugeWidget))
	{
		//client
		DrakharFeverGaugeWidget->InitializeGage(GetCurrentFeverGage());
		OnCurrentFeverGageChanged.AddUObject(DrakharFeverGaugeWidget, &UGS_DrakharFeverGauge::OnCurrentFeverGageChanged);
	}
}

void AGS_Drakhar::MulticastRPCSetFeverGauge_Implementation(float InValue)
{	
	CurrentFeverGage += InValue;
	...
	
	OnCurrentFeverGageChanged.Broadcast(CurrentFeverGage);
}

void AGS_Guardian::ApplyDamageToDetectedPlayer(const TSet<AGS_Character*>& DamagedCharacters, float PlusDamge)
{
	for (auto const& DamagedCharacter : DamagedCharacters)
	{
		...
        
			AGS_Drakhar* Drakhar = Cast<AGS_Drakhar>(this);
			if (!Drakhar->GetIsFeverMode())
			{
				Drakhar->MulticastRPCSetFeverGauge(10.f);
			}
		}
	}
}

 

2 - 3. 어떤 방식이 더 좋은가?

  • 멀티캐스트 하는 방식에는 가장 큰 문제점이 존재한다.
  • 만약 다른 플레이어가 게임 도중에 들어온 경우, 피버 게이지의 값이 동기화 되지 않는 문제점이 있다.
  • 아래 사진과 같은 문제

 

변수를 리플리케이트 되도록 수정한 버전

  • 체력과 마찬가지로 OnRep을 통해 클라이언트에게 알려주도록 구현
//Drakhar.h
UPROPERTY(ReplicatedUsing=OnRep_FeverGauge)
float CurrentFeverGauge;

UFUNCTION()
void OnRep_FeverGauge();

//Drakhar.cpp
void AGS_Drakhar::OnRep_FeverGauge()
{
	OnCurrentFeverGageChanged.Broadcast(CurrentFeverGauge);
}

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

	...
	DOREPLIFETIME(ThisClass, CurrentFeverGauge);
}

 

결과 모습

 

'Unreal Engine' 카테고리의 다른 글

UnrealEngine - HitStop  (0) 2025.06.17
UE5 Issues - 서버 & 클라이언트 로직  (0) 2025.06.15
Unreal Engine - 데디케이티드 서버 11 (콤보 공격 최적화)  (0) 2025.06.02
Unreal Engine - 데디케이티드 서버 10 (콤보 공격)  (0) 2025.05.30
UE5 Issues - Dash Skill (데디케이티드 서버)  (0) 2025.05.27
'Unreal Engine' 카테고리의 다른 글
  • UnrealEngine - HitStop
  • UE5 Issues - 서버 & 클라이언트 로직
  • Unreal Engine - 데디케이티드 서버 11 (콤보 공격 최적화)
  • Unreal Engine - 데디케이티드 서버 10 (콤보 공격)
gbleem
gbleem
gbleem 님의 블로그 입니다.
  • gbleem
    gbleem 님의 블로그
    gbleem
  • 전체
    오늘
    어제
    • 분류 전체보기 (184)
      • Unreal Engine (73)
      • C++ (19)
      • 알고리즘(코딩테스트) (27)
      • TIL (60)
      • CS (4)
      • 툴 (1)
  • 블로그 메뉴

    • 홈
    • 카테고리
  • 링크

    • 과제용 깃허브
    • 깃허브
    • velog
  • 공지사항

  • 인기 글

  • 태그

    BFS
    character animation
    blend pose
    cin함수
    map을 vector로 복사
    상속
    템플릿
    Vector
    DP
    addonscreendebugmessage
    applydamage
    싱글턴
    enhanced input system
    const
    motion matching
    actor 클래스
    매크로 지정자
    C++
    gamestate
    additive animation
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
gbleem
UE5 Issues : 데디케이트 서버 UI에 관해
상단으로

티스토리툴바