Unreal Engine - 야구게임(데디케이티드 서버)

2025. 3. 29. 21:43·Unreal Engine

아래 글에 이어서 진행되는 내용입니다.

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
'Unreal Engine' 카테고리의 다른 글
  • Unreal Engine - 데디케이티드 서버 2
  • Unreal Engine - Save Game
  • Unreal Engine - 데디케이티드 서버 개념 및 실습
  • Unreal Engine - 플러그인 만들기
gbleem
gbleem
gbleem 님의 블로그 입니다.
  • gbleem
    gbleem 님의 블로그
    gbleem
  • 전체
    오늘
    어제
    • 분류 전체보기 (189)
      • Unreal Engine (73)
      • C++ (19)
      • 알고리즘(코딩테스트) (32)
      • TIL (60)
      • CS (4)
      • 툴 (1)
  • 블로그 메뉴

    • 홈
    • 카테고리
  • 링크

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

  • 인기 글

  • 태그

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

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
gbleem
Unreal Engine - 야구게임(데디케이티드 서버)
상단으로

티스토리툴바