언리얼엔진 학습 및 과제

네트워크 채팅을 이용한 숫자야구 게임 (2)

park94128 2025. 12. 1. 23:58

● 플레이어 스테이트에 각 플레이어의 정보를 저장하기

플레이어 스테이트(Player State)에는 플레이어별로 다른 정보를 저장한다. 예를 들어 이름, 무기, 상태이상 같은 것이다. 여기서는 이름과 시도 횟수를 저장하는 데에 쓰일 것이다.

 

UCLASS()
class BASEBALLGAME_API ABaseballPlayerState : public APlayerState
{
	GENERATED_BODY()
	
public:
	ABaseballPlayerState();
	
	virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override;

	//시도 횟수 현황
	FString GuessCountInfo();
	
	//플레이어 이름
	UPROPERTY(Replicated)
	FString PlayerNameString;

	//현재 시도 횟수
	UPROPERTY(Replicated)
	int32 CurrentGuessCount;

	//최대 시도 횟수
	static constexpr int32 MAX_GUESS_COUNT = 3;
};

플레이어 스테이트를 만들고 여기에 플레이어 이름과 시도 횟수를 저장하는 변수를 만든다. 참고로 플레이어 스테이트는 서버에서 관리하지만 각 클라이언트에게 복사본이 있다. 따라서 이 둘은 Replicated 키워드가 있는 UPROPERTY로 선언한다.

 

최대 시도 횟수는 3회로 고정이다. 이는 불변이며 이 클래스와 관련된 내용이니 static constexpr로 선언한 변수로 만들었다.

 

레플리케이션을 할테니 GetLifttimeReplicatedProps 함수는 필수다. 이와 함께 시도 횟수를 FString으로 반환하는 함수도 선언한다.

 

ABaseballPlayerState::ABaseballPlayerState()
{
	bReplicates = true;
	PlayerNameString = TEXT("None");
	CurrentGuessCount = 0;
}

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

	DOREPLIFETIME(ThisClass, PlayerNameString);
	DOREPLIFETIME(ThisClass, CurrentGuessCount);
}

FString ABaseballPlayerState::GuessCountInfo()
{
	//형식 : "플레이어 이름 (현재 시도 횟수 / 최대 시도 횟수)";
	FString ReturnValue = PlayerNameString + TEXT(" (") + FString::FromInt(CurrentGuessCount) + TEXT(" / ") + FString::FromInt(MAX_GUESS_COUNT) + TEXT(")");
	return ReturnValue;
}

생성자에서 bReplicates를 true로 만드는건 필수다. 그외 나머지 변수도 초기화한다.

 

GetLifttimeReplicatedProps에서 레플리케이션할 모든 변수에 대하여 DOREPLIFETIME 매크로를 사용한다. 변수 하나당 매크로 하나씩 사용한다. 이때 같이 적는 클래스 이름은 그 대신 ThisClass를 써도 무방하다.

 

시도 횟수를 반환하는 함수의 경우, "이름 (N / 3)"형식으로 반환한다.

게임 모드에서 플레이어 스테이트 지정을 깜빡했다. 생각난 김에 지정하고 계속 진행한다.

 

● 플레이어 스테이트 활용하기

if (ABaseballGameStateBase* BaseballGameState = GetGameState<ABaseballGameStateBase>(); IsValid(BaseballGameState))
{
    //접속한 플레이이어의 이름을 "PlayerN"형식으로 정한다.
    FString NewName = TEXT("Player") + FString::FromInt(PlayerCount++);
    if (ABaseballPlayerState* BaseballPlayerState = BaseballPlayerController->GetPlayerState<ABaseballPlayerState>(); IsValid(BaseballPlayerState))
    {
        BaseballPlayerState->PlayerNameString = NewName;
    }

    //접속한 플레이어 알림
    BaseballGameState->MulticastRPCBroadcastLoginOrLogoutMessage(NewName, true);
}

이름을 저장할 곳이 생겼다. 그래서 게임 모드의 OnPostLogin에서 접속한 클라이언트의 플레이어 스테이트를 찾아 그 이름을 저장하도록 한다.

 

//지정한 플레이어가 시도 가능한지 확인
bool CanGuess(const ABaseballPlayerController* BaseballPlayerController);

//지정한 플레이어의 시도 횟수 증가
void IncreaseGuessCount(const ABaseballPlayerController* BaseballPlayerController);

//지정한 플레이어의 시도 횟수 정보
FString GuessCountInfo(const ABaseballPlayerController* BaseballPlayerController);

게임 모드에 함수를 더 선언한다. 각 함수의 용도는 주석에 적었다. 우선 시도 횟수와 관련된 함수를 먼저 정의한다.

 

bool ABaseballGameMode::CanGuess(const ABaseballPlayerController* BaseballPlayerController)
{
	const ABaseballPlayerState* BaseballPlayerState = BaseballPlayerController->GetPlayerState<ABaseballPlayerState>();
	if (!IsValid(BaseballPlayerState))
		return false;

	return BaseballPlayerState->CurrentGuessCount < ABaseballPlayerState::MAX_GUESS_COUNT;
}

각 플레이어는 기회가 3번 주어진다. 따라서 답을 3번 미만 제출했다면 아직 기회가 남은 상태로 본다.

 

void ABaseballGameMode::IncreaseGuessCount(const ABaseballPlayerController* BaseballPlayerController)
{
	//시도 횟수 증가
	if (ABaseballPlayerState* BaseballPlayerState = BaseballPlayerController->GetPlayerState<ABaseballPlayerState>(); IsValid(BaseballPlayerState))
	{
		BaseballPlayerState->CurrentGuessCount += 1;
	}
}

답을 제출하면 이 함수를 실행하여 시도 횟수를 1 증가한다.

 

FString ABaseballGameMode::GuessCountInfo(const ABaseballPlayerController* BaseballPlayerController)
{
	ABaseballPlayerState* BaseballPlayerState = BaseballPlayerController->GetPlayerState<ABaseballPlayerState>();
	if (!IsValid(BaseballPlayerState))
		return TEXT("");

	return BaseballPlayerState->GuessCountInfo();
}

시도 횟수에 대한 정보는 플레이어 스테이트의 함수의 반환값을 그대로 반환한다.

 

void ABaseballGameMode::PrintChatMessageString(ABaseballPlayerController* BaseballPlayerController, const FString& ChatMessage)
{
	int Index = ChatMessage.Len() - 3;

	//채팅 내용 중 마지막 3개의 문자가 0이 아닌 숫자 3개라면 답 입력으로 취급하고 정답 체크
	//단, 이미 최대 시도 횟수를 채웠다면 더이상 답을 맞울 수 없다.
	const FString SubmittedString = ChatMessage.RightChop(Index);
	if (CanGuess(BaseballPlayerController) && IsGuessNumberString(SubmittedString))
	{
		//이 플레이어의 시도 횟수 증가
		IncreaseGuessCount(BaseballPlayerController);

		//정답 체크
		FJudgeInfo JudgeInfo = JudgeResult(SubmittedString);
		const FString JudgeResultString = JudgeInfo.ToString();

		//채팅과 결과를 각 플레이어에게 전달
		for (TActorIterator<ABaseballPlayerController> Iter(GetWorld()); Iter; ++Iter)
		{
			if (ABaseballPlayerController* PlayerController = *Iter; IsValid(PlayerController))
			{
				FString CombinedMessage = GuessCountInfo(BaseballPlayerController) + TEXT(" : ")  + ChatMessage + TEXT(" -> ") + JudgeResultString;
				PlayerController->ClientRPCPrintChatMessageString(CombinedMessage);
			}
		}
	}
	else//아니면 그냥 채팅으로 인식하고 전달
	{
		for (TActorIterator<ABaseballPlayerController> Iter(GetWorld()); Iter; ++Iter)
		{
			if (ABaseballPlayerController* PlayerController = *Iter; IsValid(PlayerController))
			{
				FString CombinedMessage = GuessCountInfo(BaseballPlayerController) + TEXT(" : ")  + ChatMessage;
				PlayerController->ClientRPCPrintChatMessageString(CombinedMessage);
			}
		}
	}
}

채팅 전달 및 정답 체크 함수에 이 3개의 함수를 활용한다. 이제 단순히 채팅 형식만 따지는 것이 아니라 기회가 남은 플레이어에 대해서만 정답 확인을 할 것이다. 정답 확인마다 해당 플레이어의 시도 횟수가 증가한다.

 

각 플레이어의 채팅시 채팅 내용 앞에 플레이어 이름과 시도 횟수가 출력되도록 변경했다.