Unreal Engine 멀티플레이어 게임 개발 과제 9

1. WHY
- 멀티플레이어 기초: Server/Client 간의 역할 분담과 RPC 통신 이해
- PlayerState : 플레이어의 개별 데이터(시도 횟수 등)를 독립적으로 관리/동기화하기
- GameModeBase: 게임의 전체적인 규칙(판정, 승리 조건)을 서버 측에서 제어하기
- UMG 연동: C++ 로직의 결과를 위젯을 통해 플레이어에게 시각적으로 전달하기
- 유효성 검사: 잘못된 입력이 서버 로직을 해치지 않도록 방어 코드 작성하기
2. FIX
- 강의에서 기존 코드의 문제점
입력 → 정보 받음 → 서버에 플레이어 정보 문자열 전달(이름, 현재 추측 수 / 최대 수)
→ 게임 로직 → 현재 추측 수 카운트 +1 → 클라이언트에 플레이어 정보 + 결과 문자열 전달 → 출력
👉 플레이어 정보를 넘길 때 현재 추측 수를 넘겨줘서 업데이트된 수가 반영되지 않음.
- 해결
서버에 플레이어 정보 전달 시 이름만 넘겨주고
게임 로직에서 플레이어 현재 추측 수 와 최대 수를 통해 기회가 남았는지 판정하고 업데이트해서
클라이언트에게 전달할 때 같이 넘겨준다.
void ANBGameModeBase::PrintChatMessageString(ANBPlayerController* InChattingPlayerController, const FString& InChatMessageString)
{
int Index = InChatMessageString.Len() - 3;
FString GuessNumberString = InChatMessageString.RightChop(Index);
if (IsGuessNumberString(GuessNumberString) == true)
{
// 숫자 야구 입력이 들어 왔을 경우
FString JudgeResultString;
// 기회 남았는지 판단
if (!IncreaseGuessCount(InChattingPlayerController))
{
// 기회가 없다
JudgeResultString = TEXT("No Chance...");
}
else
{
JudgeResultString = JudgeResult(SecretNumberString, GuessNumberString);
int32 StrikeCount = FCString::Atoi(*JudgeResultString.Left(1));
JudgeGame(InChattingPlayerController, StrikeCount);
}
FString ChanceString = Chance(InChattingPlayerController);
for (TActorIterator<ANBPlayerController> It(GetWorld()); It; ++It)
{
ANBPlayerController* NBPC = *It;
if (IsValid(NBPC) == true)
{
FString CombineMessageString = InChatMessageString + TEXT(" -> ") + JudgeResultString + TEXT(" ") + ChanceString;
NBPC->ClientRPCPrintChatMessageString(CombineMessageString);
}
}
}
else
{
for (TActorIterator<ANBPlayerController> It(GetWorld()); It; ++It)
{
ANBPlayerController* NBPC = *It;
if (IsValid(NBPC) == true)
{
NBPC->ClientRPCPrintChatMessageString(InChatMessageString);
}
}
}
}
bool ANBGameModeBase::IncreaseGuessCount(ANBPlayerController* InChattingPlayerController)
{
ANBPlayerState* NBPS = InChattingPlayerController->GetPlayerState <ANBPlayerState>();
if (IsValid(NBPS) == true)
{
if (NBPS->CurrentGuessCount < NBPS->MaxGuessCount)
NBPS->CurrentGuessCount++;
else
return false;
}
return true;
}
FString ANBGameModeBase::Chance(ANBPlayerController* InChattingPlayerController)
{
ANBPlayerState* NBPS = InChattingPlayerController->GetPlayerState <ANBPlayerState>();
if (IsValid(NBPS) == true)
{
return TEXT("(") + FString::FromInt(NBPS->CurrentGuessCount) + TEXT(" / ") + FString::FromInt(NBPS->MaxGuessCount) + TEXT(")");
}
return TEXT("");
}
IncreaseGuessCount() 함수에서 기회가 남았는지 판정해서 bool로 반환하도록 수정했다.
Chance() 함수에서는 업데이트 된 최종 현재 추측 수와 최대 추측 수를 문자열로 반환한다.

세자리 숫자 입력 시 바로 결과와 남은 기회를 확인할 수 있다.
3. HOW
- 필수 기능
숫자 입력 검사 로직 구현: 사용자가 입력한 숫자가 규칙에 맞는지 검사
- 3자리 숫자인가?
- 문자가 포함되지는 않았는가?
- 중복되는 숫자가 있는가?
- 위 조건에 맞지 않으면 "다시 입력하세요"라는 안내를 출력하고 기회를 소진하지 않습니다.
입력 검사 로직을 위해 먼저 문자열을 받아서 입력 부분만 검사하도록 로직을 구현했다.
InChatMessageString은 "플레이어 이름" + ": " 구조이기에
InputIndex에서 플레이어 이름 +2 만큼 인덱스를 저장하여 해당 위치부터 숫자 판정 함수에 넘기도록 했다.
void ANBGameModeBase::PrintChatMessageString(ANBPlayerController* InChattingPlayerController, const FString& InChatMessageString)
{
// 메시지에서 이름 길이 +2 부터 입력 시작
int Index = InputIndex(InChattingPlayerController);
FString GuessNumberString = InChatMessageString.RightChop(Index);
if (IsGuessNumberString(GuessNumberString) == true)
{ ...}
}
...
int ANBGameModeBase::InputIndex(ANBPlayerController* InChattingPlayerController)
{
ANBPlayerState* NBPS = InChattingPlayerController->GetPlayerState <ANBPlayerState>();
if (IsValid(NBPS) == true)
{
return NBPS->PlayerNameString.Len() + 2;
}
return 0;
}

길이가 길거나 짧으면 채팅으로 인식하여 게임 결과는 출력되지 않는다. (단순 채팅 기능을 유지하고 싶었다.)
오직 중복되지 않는 3자리 숫자일 때만 게임으로 판단하도록 하려고 한다.
bool ANBGameModeBase::IsGuessNumberString(const FString& InNumberString)
{
// 옳은 입력 판단
bool bCanPlay = false;
do {
if (InNumberString.Len() != 3)
{
break;
}
bool bIsUnique = true;
TSet<TCHAR> UniqueDigits;
for (TCHAR C : InNumberString)
{
if (FChar::IsDigit(C) == false || C == '0')
{
bIsUnique = false;
break;
}
// 중복 체크
if (UniqueDigits.Contains(C))
{
bIsUnique = false;
break;
}
UniqueDigits.Add(C);
}
if (bIsUnique == false)
{
break;
}
bCanPlay = true;
} while (false);
return bCanPlay;
}
중복 체크 로직을 추가해줬다.

- 도전 기능
인데 좀 다르게?
한명이 모든 기회를 소진했을 경우 그때부터 30초 시간 카운트가 되도록 구현하고자 한다.
카운트가 끝나면 자동으로 다음판으로 넘어간다.
게임모드에서 타이머핸들러, 시간, 카운팅체크 변수를 만들어준다.
bool bIsCounting;
FTimerHandle TimeHandler;
UPROPERTY(EditDefaultsOnly)
int32 RemainingTime = 30;
void ANBGameModeBase::StartTime()
{
// 타이머 시작
GetWorldTimerManager().SetTimer(
TimeHandler,
this,
&ANBGameModeBase::CountingTime,
1.f,
true
);
}
void ANBGameModeBase::CountingTime()
{
// 시간 감소
if (RemainingTime <= 0)
{
// 타이머 정지
GetWorldTimerManager().ClearTimer(TimeHandler);
for (const auto& NBPC : AllPlayerControllers)
{
NBPC->NotificationText = FText::FromString(TEXT("To the next Game..."));
}
ResetGame(); // 시간 끝
return;
}
// 시간 감소
RemainingTime--;
//UE_LOG(LogTemp, Warning, TEXT("%d"), RemainingTime);
// 클라이언트 UI에 나타내기
ANBGameStateBase* NBGS = GetGameState<ANBGameStateBase>();
if (IsValid(NBGS) == true)
{
NBGS->MulticastRPCUpdateRemainingTime(RemainingTime);
}
}
어떤 플레이어가 기회를 소진할 경우 bIsCounting으로 중복 타이머를 방지하고
StartTime()을 실행시켜 1초마다 클라이언트에 전달하도록 했다.
void ANBGameStateBase::MulticastRPCUpdateRemainingTime_Implementation(int32 InRemainingTime)
{
if (HasAuthority() == true) return;
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
ANBPlayerController* NBPC = Cast<ANBPlayerController>(PC);
if (IsValid(NBPC) == true)
{
if (InRemainingTime == 0)
{
NBPC->TimeCountText = FText::FromString(
FString::Printf(TEXT(""))
);
return;
}
NBPC->TimeCountText = FText::FromString(
FString::Printf(TEXT("종료까지 %ds"), InRemainingTime)
);
}
}
void ANBPlayerController::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ThisClass, NotificationText);
DOREPLIFETIME(ThisClass, TimeCountText);
}



4. RESULT
https://github.com/yoonseo4343/NBC_UE_P9
GitHub - yoonseo4343/NBC_UE_P9: 내일배움캠프 언리얼7기 과제 9
내일배움캠프 언리얼7기 과제 9. Contribute to yoonseo4343/NBC_UE_P9 development by creating an account on GitHub.
github.com
📽️시연 영상
'내배캠Unreal_TIL > UE' 카테고리의 다른 글
| [TIL] 2026-03-20 | UE bool 과 uint8 사용 및 차이 (0) | 2026.03.20 |
|---|---|
| [TIL] 2026-03-17 | Replication (0) | 2026.03.17 |
| [TIL] 2026-03-16 | 멀티플레이 디버그 로깅과 이벤트 함수 (0) | 2026.03.16 |
| [TIL] 2026-03-13 | 게임 플레이 프레임워크 (2) | 2026.03.13 |
| [TIL] 2026-03-12 | Property Replication (0) | 2026.03.12 |