1. 프로퍼티 레플리케이션 (Property Replication)

액터의 모든 정보를 네트워크 너머로 복제하는 것은 비효율적이다. 따라서 우리가 원하는 특정 속성(Property)만 선택하여 다른 클라이언트에게 동기화하는 기법을 '프로퍼티 레플리케이션'이라고 한다.

  • 방향성: 프로퍼티 레플리케이션은 항상 서버(Authority)에서 클라이언트(Proxy)로만 이루어지는 단방향성을 가진다.
  • 전제 조건: 서버와 클라이언트 양쪽 모두에 해당 액터가 존재해야 복제가 가능하다.

프로퍼티 레플리케이션 3단계 설정 방법

  1. 액터 설정: 생성자에서 bReplicates = true;로 설정하여 복제 가능한 액터로 만든다.
  2. 프로퍼티 지정: 헤더 파일에서 동기화할 변수 위에 UPROPERTY(Replicated) 매크로를 추가한다.
  3. 수명 주기 등록: .cpp 파일에 #include "Net/UnrealNetwork.h"를 추가하고, GetLifetimeReplicatedProps 함수를 오버라이드하여 DOREPLIFETIME 매크로로 복제할 변수를 명시한다.

2. 게임 판정 로직의 위치: GameMode

네트워크 게임에서 규칙이나 정답을 판정하는 로직은 클라이언트가 변조할 수 없도록 반드시 서버에서 처리해야 한다. 언리얼 엔진에서 GameMode는 서버에만 존재하므로, 숫자 야구의 정답을 생성하고 판정하는 핵심 로직은 GameMode에 구현하는 것이 가장 적합하다.


3. 핵심 코드 분석: 숫자 야구 구현

3.1 세 자리 난수 생성 로직 (GameMode)

중복되지 않는 1~9 사이의 난수 3자리를 생성하여 문자열로 반환하는 핵심 로직이다.

C++
 
// CXGameModeBase.cpp

FString ACXGameModeBase::GenerateSecretNumber()
{
	TArray<int32> Numbers;
	// 1부터 9까지의 숫자를 배열에 초기화한다.
	for (int32 i = 1; i <= 9; ++i)
	{
		Numbers.Add(i);
	}

	// 매번 다른 난수가 나오도록 현재 시간을 시드로 설정하여 초기화한다.
	FMath::RandInit(FDateTime::Now().GetTicks());
	
	FString Result;
	// 3자리의 숫자를 뽑기 위해 3번 반복한다.
	for (int32 i = 0; i < 3; ++i)
	{
		// 남은 숫자들 중 무작위 인덱스를 하나 선택한다.
		int32 Index = FMath::RandRange(0, Numbers.Num() - 1);
		
		// 선택된 숫자를 문자열로 변환하여 결과값에 이어 붙인다.
		Result.Append(FString::FromInt(Numbers[Index]));
		
		// 중복된 숫자가 뽑히지 않도록 사용한 숫자는 배열에서 제거한다.
		Numbers.RemoveAt(Index);
	}

	// 완성된 3자리 난수 문자열을 반환한다. (예: "472")
	return Result;
}

3.2 정답 판정 로직 (GameMode)

클라이언트가 입력한 추측값(Guess)과 서버의 정답(Secret)을 비교하여 스트라이크(S)와 볼(B)을 판정한다.

C++
 
// CXGameModeBase.cpp

FString ACXGameModeBase::JudgeResult(const FString& InSecretNumberString, const FString& InGuessNumberString)
{
	int32 StrikeCount = 0, BallCount = 0;

	// 3자리 숫자를 하나씩 순회하며 비교한다.
	for (int32 i = 0; i < 3; ++i)
	{
		// 자리수와 숫자가 모두 일치하면 스트라이크로 판정함.
		if (InSecretNumberString[i] == InGuessNumberString[i])
		{
			StrikeCount++;
		}
		// 자리수는 다르지만, 해당 숫자가 정답 문자열 어딘가에 포함되어 있다면 볼로 판정함.
		else 
		{
			// 현재 추측한 문자를 FString으로 변환한다.
			FString PlayerGuessChar = FString::Printf(TEXT("%c"), InGuessNumberString[i]);
			
			// 정답 문자열이 해당 문자를 포함하고 있는지 확인한다.
			if (InSecretNumberString.Contains(PlayerGuessChar))
			{
				BallCount++;				
			}
		}
	}

	// 하나도 맞추지 못했다면 "OUT"을 반환한다.
	if (StrikeCount == 0 && BallCount == 0)
	{
		return TEXT("OUT");
	}

	// 결과 포맷에 맞춰 문자열을 반환한다. (예: "1S2B")
	return FString::Printf(TEXT("%dS%dB"), StrikeCount, BallCount);
}

3.3 클라이언트의 채팅 입력을 서버 로직으로 전달 (PlayerController)

클라이언트가 채팅을 쳤을 때, PlayerController에서 Server RPC를 호출하여 서버에 있는 GameMode로 처리를 위임하는 부분이다.

C++
 
// CXPlayerController.cpp

// 클라이언트가 호출하지만 서버에서 실행되는 Server RPC 함수이다.
void ACXPlayerController::ServerRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
	// 서버에서 현재 실행 중인 GameMode를 가져온다.
	AGameModeBase* GM = UGameplayStatics::GetGameMode(this);
	
	if (IsValid(GM) == true)
	{
		// 직접 구현한 커스텀 게임모드(CXGameModeBase)로 캐스팅한다.
		ACXGameModeBase* CXGM = Cast<ACXGameModeBase>(GM);
		
		if (IsValid(CXGM) == true)
		{
			// GameMode의 판정 및 브로드캐스팅 함수를 호출하여 로직을 위임한다.
			// GameMode 내부에서 정답을 판정한 뒤, 다시 각 플레이어의 Client RPC를 통해 결과를 뿌려주게 됨.
			CXGM->PrintChatMessageString(this, InChatMessageString);
		}
	}
}

+ Recent posts