Replicated

[Drag Down] PlayerState에서 이름과 Ready 관리, GameState 전체 상태 관리 본문

언리얼 엔진/Drag Down

[Drag Down] PlayerState에서 이름과 Ready 관리, GameState 전체 상태 관리

라구넹 2025. 5. 21. 21:05
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "AbilitySystemInterface.h"
#include "DDPlayerState.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FPlayerInfoChanged);

/**
 * 
 */
UCLASS()
class DRAGDOWN_API ADDPlayerState : public APlayerState, public IAbilitySystemInterface
{
	GENERATED_BODY()
	
public:
	ADDPlayerState();

	virtual class UAbilitySystemComponent* GetAbilitySystemComponent() const override;

	FPlayerInfoChanged OnPlayerInfoChanged;

protected:
	UPROPERTY(EditAnywhere, Category = GAS)
	TObjectPtr<class UAbilitySystemComponent> ASC;

	UPROPERTY()
	TObjectPtr<class UDDAttributeSet> AttributeSet;

public:
	FORCEINLINE const FString& GetUserName() { return UserName; }

protected:
	virtual void BeginPlay() override;

	UFUNCTION(Server, Reliable)
	void ServerSetUserName(const FString& InUserName);

protected:
	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

	UPROPERTY(ReplicatedUsing = OnRep_UserName)
	FString UserName;

	UFUNCTION()
	void OnRep_UserName();



};

// Fill out your copyright notice in the Description page of Project Settings.


#include "Player/DDPlayerState.h"
#include "AbilitySystemComponent.h"
#include "Attribute/DDAttributeSet.h"
#include "Net/UnrealNetwork.h"
#include "Subsystem/DDUserAuthSubsystem.h"

ADDPlayerState::ADDPlayerState()
{
	ASC = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("ASC"));
	ASC->SetIsReplicated(true);
	AttributeSet = CreateDefaultSubobject<UDDAttributeSet>(TEXT("AttributeSet"));
}

void ADDPlayerState::ServerSetUserName_Implementation(const FString& InUserName)
{
	if ( HasAuthority() )
	{
		UserName = InUserName;
		OnPlayerInfoChanged.Broadcast();
	}
}

UAbilitySystemComponent* ADDPlayerState::GetAbilitySystemComponent() const
{
	return ASC;
}

void ADDPlayerState::BeginPlay()
{
	Super::BeginPlay();

	UDDUserAuthSubsystem* UserAuthSubsystem = GetGameInstance()->GetSubsystem<UDDUserAuthSubsystem>();
	if (UserAuthSubsystem == nullptr) return;

	if (HasAuthority())
	{
		UserName = UserAuthSubsystem->GetUserName(); 
		OnPlayerInfoChanged.Broadcast(); 
	}
	else
	{
		ServerSetUserName(UserAuthSubsystem->GetUserName());
	}
}

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

	DOREPLIFETIME(ADDPlayerState, UserName);
}

void ADDPlayerState::OnRep_UserName()
{
	OnPlayerInfoChanged.Broadcast();
}

플레이어 스테이트

UserName을 서버에서 등록하고, 클라이언트는 리플리케이션 받아 사용

 

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Player/DDPlayerState.h"
#include "DDWaitingPlayerState.generated.h"

/**
 * 
 */
UCLASS()
class DRAGDOWN_API ADDWaitingPlayerState : public ADDPlayerState
{
	GENERATED_BODY()
	
public:
	ADDWaitingPlayerState();

	FORCEINLINE bool IsUserReady() { return bIsUserReady; }

	void SetUserReady(bool bIsReady);

protected:
	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

	UFUNCTION(Server, Reliable)
	void ServerSetUserReady(bool bIsReady);

	UPROPERTY(ReplicatedUsing = OnRep_bIsUserReady)
	bool bIsUserReady;

	UFUNCTION()
	void OnRep_bIsUserReady();
};


// Fill out your copyright notice in the Description page of Project Settings.


#include "Player/DDWaitingPlayerState.h"
#include "Net/UnrealNetwork.h"

ADDWaitingPlayerState::ADDWaitingPlayerState()
{
	bIsUserReady = false;
}

void ADDWaitingPlayerState::SetUserReady(bool bIsReady)
{
	if ( HasAuthority() )
	{
		bIsUserReady = bIsReady;
		OnPlayerInfoChanged.Broadcast();
	}
	else
	{
		ServerSetUserReady(bIsReady);
	}
}

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

	DOREPLIFETIME(ADDWaitingPlayerState, bIsUserReady);
}

void ADDWaitingPlayerState::ServerSetUserReady_Implementation(bool bIsReady)
{
	if ( HasAuthority() )
	{
		bIsUserReady = bIsReady;
		OnPlayerInfoChanged.Broadcast(); 
	}
}

void ADDWaitingPlayerState::OnRep_bIsUserReady()
{
	OnPlayerInfoChanged.Broadcast();
}

대기방에서 사용하는 PlayerState

Ready 관리, 변경은 RPC 처리

 

각각의 PlayerState의 OnPlayerInfoChanged를 GameState에서 구독하고, UI들은 GameState의 OnGameInfoChanged를 구독하여 변경 시 업데이트

 

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameState.h"
#include "DDGameState.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGameInfoChanged);

/**
 * 
 */
UCLASS()
class DRAGDOWN_API ADDGameState : public AGameState
{
	GENERATED_BODY()
	
public:
	FGameInfoChanged OnGameInfoChanged;

protected:
	virtual void AddPlayerState(APlayerState* PlayerState) override;

	UFUNCTION()
	void OnPlayerInfoChangedCallback();
};


// Fill out your copyright notice in the Description page of Project Settings.


#include "Game/DDGameState.h"
#include "Net/UnrealNetwork.h"
#include "Player/DDPlayerState.h"
#include "DragDown.h"

void ADDGameState::AddPlayerState(APlayerState* PlayerState)
{
	Super::AddPlayerState(PlayerState);
	UE_LOG(LogDD, Log, TEXT("[NetMode: %d] ADDGameState::AddPlayerState, %d"), GetWorld()->GetNetMode(), PlayerArray.Num());

	for ( const auto& PS : PlayerArray )
	{
		if ( ADDPlayerState* DDPS = Cast<ADDPlayerState>(PS) )
		{
			DDPS->OnPlayerInfoChanged.Clear();
			DDPS->OnPlayerInfoChanged.AddDynamic(this, &ADDGameState::OnPlayerInfoChangedCallback);
		}
	}
}

void ADDGameState::OnPlayerInfoChangedCallback()
{
	UE_LOG(LogDD, Log, TEXT("ADDGameState::OnPlayerInfoChangedCallback"));
	OnGameInfoChanged.Broadcast();
}

PlayerArray를 통해 각각의 PlayerState에 접근하고 구독함

리플리케이션 받으면 브로드캐스트

 

// Fill out your copyright notice in the Description page of Project Settings.


#include "Game/DDWaitingGameState.h"
#include "Player/DDWaitingPlayerState.h"

bool ADDWaitingGameState::AreAllPlayerReady()
{
	if (!HasAuthority()) return false;

	for (const auto& PS : PlayerArray)
	{
		ADDWaitingPlayerState* WaitingPS = Cast<ADDWaitingPlayerState>(PS);
		if (!WaitingPS->IsUserReady())
		{
			return false;
		}
	}

	return true;
}

WaitingGameState에서는 모든 유저가 Ready인지 체크해주는 함수 보유

 

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "DDPlayerReadyEntryWidget.generated.h"

/**
 * 
 */
UCLASS()
class DRAGDOWN_API UDDPlayerReadyEntryWidget : public UUserWidget
{
	GENERATED_BODY()
	
public:
	void InitReadyEntry(const FString& UserName);
	void UpdateReadyEntry(bool bIsReady);

protected:
	UPROPERTY(meta = (BindWidget))
	TObjectPtr<class UTextBlock> TxtUserName;

	UPROPERTY(meta = (BindWidget))
	TObjectPtr<class UTextBlock> TxtReady;

	const FString ReadyStatement = "Ready";
	const FString NotReadyStatement = "Not Ready"; 
};


// Fill out your copyright notice in the Description page of Project Settings.


#include "UI/DDPlayerReadyEntryWidget.h"
#include "Components/TextBlock.h"

void UDDPlayerReadyEntryWidget::InitReadyEntry(const FString& UserName)
{
	if ( TxtUserName )
	{
		TxtUserName->SetText( FText::FromString( UserName ) );
	}

	if ( TxtReady )
	{
		TxtReady->SetText(FText::FromString(NotReadyStatement));

		FSlateColor NewColor = FSlateColor(FLinearColor::Red);
		TxtReady->SetColorAndOpacity(NewColor);
	}
}

void UDDPlayerReadyEntryWidget::UpdateReadyEntry(bool bIsReady)
{
	if ( TxtReady )
	{
		if (bIsReady)
		{
			TxtReady->SetText(FText::FromString(ReadyStatement));

			FSlateColor NewColor = FSlateColor(FLinearColor::Green);
			TxtReady->SetColorAndOpacity(NewColor);
		}
		else
		{
			TxtReady->SetText(FText::FromString(NotReadyStatement));

			FSlateColor NewColor = FSlateColor(FLinearColor::Red);
			TxtReady->SetColorAndOpacity(NewColor);
		}
	}
}

하나의 유저 정보를 담는 엔트리

 

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "DDPlayerReadyListWidget.generated.h"

/**
 * 
 */
UCLASS()
class DRAGDOWN_API UDDPlayerReadyListWidget : public UUserWidget
{
	GENERATED_BODY()
	
public:
	virtual void NativeConstruct() override;

	UFUNCTION(BlueprintCallable)
	void UpdateReadyList();

protected:
	UPROPERTY(meta = (BindWidget))
	TObjectPtr<class UVerticalBox> VerticalBox;

	UPROPERTY(EditAnywhere, BlueprintReadOnly)
	TSubclassOf<class UDDPlayerReadyEntryWidget> EntryWidgetToList;

	TMap<FString, TObjectPtr<class UDDPlayerReadyEntryWidget>> EntryMap;

	TObjectPtr<class ADDGameState> GameState;
};


// Fill out your copyright notice in the Description page of Project Settings.


#include "UI/DDPlayerReadyListWidget.h"
#include "Game/DDGameState.h"
#include "Player/DDWaitingPlayerState.h"
#include "UI/DDPlayerReadyEntryWidget.h"
#include "Components/VerticalBox.h"
#include "DragDown.h"

void UDDPlayerReadyListWidget::NativeConstruct()
{
	UE_LOG(LogDD, Log, TEXT("[NetMode: %d] NativeConstruct"), GetWorld()->GetNetMode());
	GameState = Cast<ADDGameState>(GetWorld()->GetGameState());

	if ( GameState )
	{
		GameState->OnGameInfoChanged.AddDynamic(this, &UDDPlayerReadyListWidget::UpdateReadyList);
	}

	if ( !GetWorld()->GetFirstPlayerController()->HasAuthority() )
	{
		UpdateReadyList();
	}
}

void UDDPlayerReadyListWidget::UpdateReadyList()
{
	if ( VerticalBox == nullptr ) return;
	if ( GameState == nullptr ) return;
	
	for ( const auto& PS : GameState->PlayerArray )
	{
		ADDWaitingPlayerState* WaitingDDPS = Cast<ADDWaitingPlayerState>(PS);
		if (WaitingDDPS == nullptr) return;

		if ( EntryMap.Contains( WaitingDDPS->GetUserName() ) )
		{
			bool bIsReady = WaitingDDPS->IsUserReady();
			EntryMap[WaitingDDPS->GetUserName()]->UpdateReadyEntry(bIsReady);
		}
		else
		{
			if (EntryWidgetToList == nullptr)
			{
				UE_LOG(LogDD, Log, TEXT("UDDPlayerReadyListWidget - EntryWidgetToList Is Null"));
				return;
			}

			UDDPlayerReadyEntryWidget* EntryWidget = NewObject<UDDPlayerReadyEntryWidget>(this, EntryWidgetToList);
			if (EntryWidget == nullptr) return;

			VerticalBox->AddChild(EntryWidget);
			EntryWidget->InitReadyEntry(WaitingDDPS->GetUserName());

			EntryMap.Emplace(WaitingDDPS->GetUserName(), EntryWidget);
		}
	}
}

전체 유저 정보 관리하는 위젯

GameState에서 변경에 대한 이벤트가 발생 시 업데이트

* NativeConstruct 시점 문제(클라이언트는 NativeConstruct 시점이 많이 느려서 PlayerState추가된 다음 바인딩됨 -> 알아서 브로드캐스트가 안됨)로 인해 로컬에서는 NativeConstruct  후 UpdateReadyList 호출

 

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "DDReadyButtonWidget.generated.h"

/**
 * 
 */
UCLASS()
class DRAGDOWN_API UDDReadyButtonWidget : public UUserWidget
{
	GENERATED_BODY()
	
public:
	virtual void NativeConstruct() override;

	UFUNCTION()
	void OnReadyButtonClick();

protected:
	UPROPERTY(meta = (BindWidget))
	TObjectPtr<class UButton> BtnReady;

	UPROPERTY(meta = (BindWidget))
	TObjectPtr<class UTextBlock> TxtReady;

	UPROPERTY()
	TObjectPtr<class ADDWaitingPlayerState> WatingPS;

	bool bIsReady;

	const FString ReadyStatement = "Ready";
	const FString NotReadyStatement = "Not Ready";
};


// Fill out your copyright notice in the Description page of Project Settings.


#include "UI/DDReadyButtonWidget.h"
#include "Components/Button.h"
#include "Components/TextBlock.h"
#include "Player/DDWaitingPlayerState.h"

void UDDReadyButtonWidget::NativeConstruct()
{
	Super::NativeConstruct();

	bIsReady = false;

	APlayerController* PC = GetWorld()->GetFirstPlayerController();

	if ( PC )
	{
		WatingPS = PC->GetPlayerState<ADDWaitingPlayerState>();
	}

	if ( BtnReady )
	{
		BtnReady->OnClicked.AddDynamic(this, &UDDReadyButtonWidget::OnReadyButtonClick);
	}

	if ( TxtReady )
	{
		TxtReady->SetText(FText::FromString(NotReadyStatement)); 
	}
}

void UDDReadyButtonWidget::OnReadyButtonClick()
{
	if ( WatingPS )
	{
		bIsReady = !bIsReady;

		WatingPS->SetUserReady(bIsReady);
	}

	if ( TxtReady )
	{
		if ( bIsReady )
		{
			TxtReady->SetText( FText::FromString( ReadyStatement ) );
		}
		else
		{
			TxtReady->SetText(FText::FromString( NotReadyStatement ));
		}
	}
}

레디 버튼

 

이런식으로 구성됨

 

GameState에서 Ready 다 관리하는 걸로 짰다가 고치느라 시간이 꽤 소모된 거 같다

시작하기 전 좀 더 고민해서 작업하자