아래 글에 이어서 진행되는 내용입니다.
https://gbleem.tistory.com/140
Unreal Engine - 데디케이티드 서버 개념 및 실습
1. 서버의 종류P2P각 컴퓨터가 서버랑 클라이언트를 모두 수행하는 방식리슨 서버HOST 역할을 하는 서버용 컴퓨터가 존재 (클라이언트 역할도 수행)GUEST 역할을 하는 클라이언트용 컴퓨터가 존재
gbleem.tistory.com
지금까지 구현된 내용
- 한 클라이언트가 친 채팅이 다른 유저에게 보이도록 하는 기능 (Server, Client RPC)
- 한 클라이언트가 접속하면, 다른 유저에게 접속되었다는 정보를 알려주는 기능 (Multicast RPC)
- 클라이언트가 채팅을 할 때 몇번째 클라이언트인지 번호를 앞에 붙여 출력해주는 기능 (Property Replication)
- + 세자리 숫자를 입력하면, 판정해주는 기능
이제 좀 더 숫자야구에 가깝게 로직을 추가해야 한다.
1. 시도 횟수 관리
- GameState
더보기
//gamestate.h
public:
FString GetPlayerInfoString();
public:
UPROPERTY(Replicated)
FString PlayerNameString;
UPROPERTY(Replicated)
int32 CurrentGuessCount;
UPROPERTY(Replicated)
int32 MaxGuessCount;
//gamestate.cpp
ACXPlayerState::ACXPlayerState()
:PlayerNameString(TEXT("None"))
,CurrentGuessCount(0)
,MaxGuessCount(3)
{
bReplicates = true;
}
void ACXPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ThisClass, PlayerNameString);
DOREPLIFETIME(ThisClass, CurrentGuessCount);
DOREPLIFETIME(ThisClass, MaxGuessCount);
}
FString ACXPlayerState::GetPlayerInfoString()
{
FString PlayerInfoString = PlayerNameString + TEXT(" (") + FString::FromInt(CurrentGuessCount) +
TEXT("/") + FString::FromInt(MaxGuessCount) + TEXT(")");
return PlayerInfoString;
}
- GameMode
더보기
//gamemode.h
void IncreaseGuessCount(ACXPlayerController* InChattingPlayerController);
//gamemode.cpp
void ACXGameModeBase::PrintChatMessageString(ACXPlayerController* InChattingPlayerController, const FString& InChatMessageString)
{
...
//number input
if (IsGuessNumberString(GuessNumberString))
{
//increase count
IncreaseGuessCount(InChattingPlayerController);
...
}
void ACXGameModeBase::IncreaseGuessCount(ACXPlayerController* InChattingPlayerController)
{
ACXPlayerState* CXPS = InChattingPlayerController->GetPlayerState<ACXPlayerState>();
if (IsValid(CXPS))
{
CXPS->CurrentGuessCount++;
}
}
- PlayerController
더보기
void ACXPlayerController::SetChatMessageString(const FString& InChatMessageString)
{
ChatMessageString = InChatMessageString;
//if client send message to server
if (IsLocalPlayerController())
{
ACXPlayerState* CXPS = GetPlayerState<ACXPlayerState>();
if (IsValid(CXPS))
{
FString CombinedMessageString = CXPS->GetPlayerInfoString() + TEXT(": ") + InChatMessageString;
ServerRPCPrintChatMessageString(CombinedMessageString);
}
}
}
2. 승패 판정
- 위젯 생성
- CanvasPanel -> Text 계층구조
- Text의 변수명을 NotificationText로 지정
- PlayerController BP에서 위젯 클래스 지정
- PlayerController
더보기
//playercontroller.h
ACXPlayerController();
virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override;
protected:
UPROPERTY(EditDefaultsOnly)
TSubclassOf<UUserWidget> NotificationTextWidgetClass;
UPROPERTY()
TObjectPtr<UUserWidget> NotificationTextWidgetInstance;
public:
UPROPERTY(Replicated, BlueprintReadOnly)
FText NotificationText;
ACXPlayerController::ACXPlayerController()
{
bReplicates = true;
}
void ACXPlayerController::BeginPlay()
{
...
if (IsValid(NotificationTextWidgetClass))
{
NotificationTextWidgetInstance = CreateWidget<UUserWidget>(this, NotificationTextWidgetClass);
if (IsValid(NotificationTextWidgetInstance))
{
NotificationTextWidgetInstance->AddToViewport();
}
}
}
void ACXPlayerController::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ThisClass, NotificationText);
}
- GameMode
더보기
void ACXGameModeBase::OnPostLogin(AController* NewPlayer)
{
Super::OnPostLogin(NewPlayer);
ACXPlayerController* CXPlayerController = Cast<ACXPlayerController>(NewPlayer);
if (IsValid(CXPlayerController))
{
CXPlayerController->NotificationText = FText::FromString(TEXT("Connected to the game server"));
...
}
}
- UI Text 바인딩
- 결과 화면
이제 승패 관련 text 연동을 해보자
- GameMode
더보기
//gamemode.h
void ResetGame();
void JudgeGame(ACXPlayerController* InChattingPlayerController, int InStrikeCount);
//gamemode.cpp
void ACXGameModeBase::PrintChatMessageString(ACXPlayerController* InChattingPlayerController, const FString& InChatMessageString)
{
//FString ChatMessageString = InChatMessageString;
int32 Index = InChatMessageString.Len() - 3;
FString GuessNumberString = InChatMessageString.RightChop(Index);
//number input
if (IsGuessNumberString(GuessNumberString))
{
...
for (TActorIterator<ACXPlayerController>It(GetWorld()); It; ++It)
{
ACXPlayerController* CXPlayerController = *It;
if (IsValid(CXPlayerController))
{
FString CombinedMessageString = InChatMessageString + TEXT(" -> ") + JudgeResultString;
CXPlayerController->ClientRPCPrintChatMessageString(CombinedMessageString);
//추가
int32 StrikeCount = FCString::Atoi(*JudgeResultString.Left(1));
JudgeGame(InChattingPlayerController, StrikeCount);
}
}
}
...
}
void ACXGameModeBase::ResetGame()
{
SecretNumberString = GenerateSecretNumber();
for (const auto& ACXPlayerController : AllPlayerControllers)
{
ACXPlayerState* CXPS = ACXPlayerController->GetPlayerState<ACXPlayerState>();
if (IsValid(CXPS))
{
CXPS->CurrentGuessCount = 0;
}
}
}
void ACXGameModeBase::JudgeGame(ACXPlayerController* InChattingPlayerController, int InStrikeCount)
{
if (InStrikeCount == 3)
{
ACXPlayerState* CXPS = InChattingPlayerController->GetPlayerState<ACXPlayerState>();
for (const auto& CXPlayerController : AllPlayerControllers)
{
if (IsValid(CXPS))
{
FString CombinedMessageString = CXPS->PlayerNameString + TEXT(" has won the game");
CXPlayerController->NotificationText = FText::FromString(CombinedMessageString);
ResetGame();
}
}
}
else
{
bool bIsDraw = true;
for (const auto& CXPlayerController : AllPlayerControllers)
{
ACXPlayerState* CXPS = CXPlayerController->GetPlayerState<ACXPlayerState>();
if (IsValid(CXPS))
{
if (CXPS->CurrentGuessCount < CXPS->MaxGuessCount)
{
bIsDraw = false;
break;
}
}
}
if (bIsDraw)
{
for (const auto& CXPlayerController : AllPlayerControllers)
{
CXPlayerController->NotificationText = FText::FromString(TEXT("Draw.."));
ResetGame();
}
}
}
}
- 결과 화면
3. 버그 수정
위의 비김 상태의 사진을 보면, count가 처음 입력시 제대로 업데이트 되지 않는 문제가 존재한다.
실제로 3번을 입력했지만, 출력되는 문구에서는 2개로 뜨는 문제가 있다.
기존의 동작 방식
- UI에서 글씨 입력 후 Enter 누르면
- PlayerController의 SetChatMessageString 에서는
- Input으로 들어온 text를 string으로 바꾼 값을 매개변수로 가짐
- SetChatMessageString
- PlayerState의 GetPlayerInfoString 에서는
- PlayerName, CurrentCount, MaxCount 값 + 매개변수 값을 ServerRPCPrintChatMessageString 함수에 넣어 실행
- ServerRPCPrintChatMessageString_Implementation
- 이 함수는 GameMode를 가져와 GameMode의 PrintChatMessageString 함수 실행
- 매개변수로는 현재 PlayerController와 받아온 매개변수값
- PrintChatMessageString (GameMode)
- 여기서는 판정 로직 수행 후, 숫자라면 해당 판정된 값을 받아온 매개변수에 더해주고 Current Count를 증가시킴
- PlayerController의 ClientRPCPrintChatMessageString 실행
- ClientRPCPrintChatMessageString_Implementation
- 여기서는 PlayerController의 PringChatMessageString 실행
- 매개변수는 받아온 값 그대로
- PringChatMessageString (PlayerController)
- 여기서는 functionlibrary에 정의된 MyPrintString 함수 실행
문제점
- PlayerState에서 CurrentCount라는 값을 가져오는 시점이 GameMode에서 CurrentCount를 증가시키는 순서보다 빠르기 때문에, 문제가 생기는 것이다.
해결책
- UI에서 input을 받았을때, PlayerController의 ServerRPC를 통해 판정하기
- 판정된 후 변경되는 값인 CurrentCount 값과 OnRep_ 함수 연동하여 SetChatMessageString 실행
- 추가로) ResetGame 함수를 통해 CurrentCount값이 바뀔때 출력을 방지하기 위해 ChatString 초기화
코드
- UUserWidget 상속받은 클래스
더보기
//uuserwidget.cpp
void UCXChatInput::OnChatInputTextCommitted(const FText& Text, ETextCommit::Type CommitMethod)
{
if (CommitMethod == ETextCommit::OnEnter)
{
APlayerController* OwningPlayerController = GetOwningPlayer();
if (IsValid(OwningPlayerController))
{
ACXPlayerController* OwningCXPlayerController = Cast<ACXPlayerController>(OwningPlayerController);
if (IsValid(OwningCXPlayerController))
{
OwningCXPlayerController->ServerRPCIncreaseCount(Text.ToString());
//clear
EditableTextBox_ChatInput->SetText(FText());
}
}
}
}
- PlayerController
더보기
//playercontroller.h
UFUNCTION(Server, Reliable)
void ServerRPCIncreaseCount(const FString& InChatMessageString);
//playercontroller.cpp
void ACXPlayerController::ServerRPCIncreaseCount_Implementation(const FString& InChatMessageString)
{
AGameModeBase* GM = UGameplayStatics::GetGameMode(this);
if (IsValid(GM))
{
ACXGameModeBase* CXGM = Cast<ACXGameModeBase>(GM);
if (IsValid(CXGM))
{
int32 Index = InChatMessageString.Len() - 3;
FString GuessNumberString = InChatMessageString.RightChop(Index);
//number input
if (CXGM->IsGuessNumberString(GuessNumberString))
{
ChatMessageString = InChatMessageString;
CXGM->IncreaseGuessCount(this);
}
}
}
}
- PlayerState
더보기
//playerstate.h
UFUNCTION()
void OnRep_IncreaseCurrentCount();
public:
UPROPERTY(Replicated)
FString PlayerNameString;
//UPROPERTY(Replicated)
UPROPERTY(ReplicatedUsing = OnRep_IncreaseCurrentCount)
int32 CurrentGuessCount;
//playerstate.cpp
void ACXPlayerState::OnRep_IncreaseCurrentCount()
{
ACXPlayerController* CXPC = Cast<ACXPlayerController>(GetPlayerController());
if (IsValid(CXPC))
{
if (!CXPC->ChatMessageString.IsEmpty())
{
CXPC->SetChatMessageString(CXPC->ChatMessageString);
}
}
}
- GameMode
더보기
void ACXGameModeBase::ResetGame()
{
SecretNumberString = GenerateSecretNumber();
for (const auto& ACXPlayerController : AllPlayerControllers)
{
//추가
ACXPlayerController->ChatMessageString.Empty();
...
}
}
실행 모습
아직 존재하는 문제점
- 이 경우 OnRep_ 함수를 써서, 만약 숫자가 아닌 경우는 SetChatMessageString이 실행되지 않아서, 숫자로 판명이 나지 않은 경우 아무것도 출력되지 않는다...
4. 턴 관리 및 타이머
타이머 로직은 게임 내에서 매우 중요한 부분이기에 GameMode에 존재해야 한다.
- GameMode
더보기
//gamemodebase.h
public:
ACXPlayerController* GetCurrentTurnPlayerController() const;
private:
UFUNCTION()
void OnMainTimerElapsed();
protected:
...
FTimerHandle MainTimerHandle;
int32 CurrentTurnIndex = 0;
//gamemode.cpp
void ACXGameModeBase::BeginPlay()
{
Super::BeginPlay();
...
GetWorldTimerManager().SetTimer(MainTimerHandle, this, &ACXGameModeBase::OnMainTimerElapsed, 10.f, true);
}
void ACXGameModeBase::OnPostLogin(AController* NewPlayer)
{
Super::OnPostLogin(NewPlayer);
ACXPlayerController* CXPlayerController = Cast<ACXPlayerController>(NewPlayer);
if (IsValid(CXPlayerController))
{
...
if (CurrentTurnIndex == AllPlayerControllers.Num() - 1)
{
CXPlayerController->NotificationText = FText::FromString(TEXT("It's your turn"));
}
else
{
CXPlayerController->NotificationText = FText::FromString(TEXT("waiting for other player.."));
}
}
}
void ACXGameModeBase::PrintChatMessageString(ACXPlayerController* InChattingPlayerController, const FString& InChatMessageString)
{
//number input
if (IsGuessNumberString(GuessNumberString) && InChattingPlayerController == GetCurrentTurnPlayerController())
{
...
}
}
void ACXGameModeBase::ResetGame()
{
...
CurrentTurnIndex = 0;
}
ACXPlayerController* ACXGameModeBase::GetCurrentTurnPlayerController() const
{
if (AllPlayerControllers.IsEmpty())
{
return nullptr;
}
if (AllPlayerControllers.IsValidIndex(CurrentTurnIndex))
{
return AllPlayerControllers[CurrentTurnIndex];
}
return nullptr;
}
void ACXGameModeBase::OnMainTimerElapsed()
{
if (AllPlayerControllers.Num() == 0)
{
return;
}
CurrentTurnIndex = (CurrentTurnIndex + 1) % AllPlayerControllers.Num();
for (int32 i = 0; i < AllPlayerControllers.Num(); ++i)
{
if (IsValid(AllPlayerControllers[i]))
{
if (CurrentTurnIndex == i)
{
AllPlayerControllers[i]->NotificationText = FText::FromString(TEXT("It's your turn"));
}
else
{
AllPlayerControllers[i]->NotificationText = FText::FromString(TEXT("Waiting for other player.."));
}
}
}
}
'Unreal Engine' 카테고리의 다른 글
Unreal Engine - 데디케이티드 서버 2 (1) | 2025.04.03 |
---|---|
Unreal Engine - Save Game (0) | 2025.04.02 |
Unreal Engine - 데디케이티드 서버 개념 및 실습 (0) | 2025.03.27 |
Unreal Engine - 플러그인 만들기 (0) | 2025.03.26 |
Unreal Engine - 야구 게임(리슨 서버) (1) | 2025.03.25 |