멀티플레이어 환경에 맞춰 서버(Authority)와 클라이언트(Proxy)의 역할을 분리한 클래스 설계입니다.
게임의 마스터 룰을 통제하며 서버에서만 존재하고 실행되는 핵심 클래스입니다.
플레이어 개인의 상태 데이터를 보관하며 서버와 클라이언트 간 변수를 동기화(Replication)합니다.
클라이언트의 입력을 서버로 전달하고, 서버의 피드백을 사용자 UI에 출력하는 RPC(Remote Procedure Call) 통신 창구입니다.
사용자가 직접 숫자를 타이핑하고 제출하는 클라이언트 측 UI 뷰입니다.
// BBGGameModeBase.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "BBGGameModeBase.generated.h"
// 숫자 야구 게임의 전체 규칙과 서버 로직을 총괄하는 클래스
UCLASS()
class BASEBALLGAME_API ABBGGameModeBase : public AGameModeBase
{
GENERATED_BODY()
public:
// 게임 시작 시 최초 1회 호출 및 정답 숫자 생성
virtual void BeginPlay() override;
// 플레이어의 입력값을 받아 정답 판정
void ProcessPlayerGuess(class ABBGPlayerController* PC, const FString& GuessStr);
// 플레이어 서버 접속 시 자동 호출 및 접속 순서에 따른 이름 부여
virtual void OnPostLogin(AController* NewPlayer) override;
private:
// 서버 전용 3자리 정답 숫자
FString SecretAnswer;
// 1~9 사이의 중복 없는 3자리 난수 생성 및 저장
void GenerateRandomNumbers();
// 자릿수, 숫자 구성, 중복 여부 등 입력값 유효성 검사
bool IsValidInput(const FString& Input);
// 정답과 비교하여 스트라이크 및 볼 개수 계산 후 문자열 반환
FString CalculateResult(const FString& Input, int32& OutStrike);
// 시도 횟수 소진 여부 확인 및 무승부 판정
void CheckGameEndCondition();
// 승리 또는 무승부 시 데이터 초기화 및 게임 리셋
void ResetGame();
// 모든 플레이어의 화면 중앙에 공지 텍스트 출력
void AnnounceToAll(const FString& Message);
};
// BBGGameModeBase.cpp
#include "Game/BBGGameModeBase.h"
#include "Player/BBGPlayerController.h"
#include "Player/BBGPlayerState.h"
void ABBGGameModeBase::OnPostLogin(AController* NewPlayer)
{
Super::OnPostLogin(NewPlayer);
// 접속 플레이어 상태 정보 획득
if (ABBGPlayerState* PS = NewPlayer->GetPlayerState<ABBGPlayerState>())
{
// 총 접속 인원 기반 이름 조립
FString NewName = FString::Printf(TEXT("Player%d"), GetNumPlayers());
// 플레이어 닉네임 설정
PS->SetPlayerName(NewName);
}
}
void ABBGGameModeBase::GenerateRandomNumbers()
{
// 1~9 배열 생성 및 무작위로 해놓기
TArray<int32> Numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Numbers.Sort([](int32 A, int32 B) { return FMath::RandBool(); });
// 앞 3자리 숫자 추출 후 문자열 조합
SecretAnswer = FString::Printf(TEXT("%d%d%d"), Numbers[0], Numbers[1], Numbers[2]);
// 서버 콘솔에 정답 로그 출력
UE_LOG(LogTemp, Warning, TEXT("[Server] Secret Answer Generated : %s"), *SecretAnswer);
}
bool ABBGGameModeBase::IsValidInput(const FString& Input)
{
// 길이 3자리 제한 검사
if (Input.Len() != 3) return false;
// 숫자로만 구성되었는지 검사
if (!Input.IsNumeric()) return false;
// 숫자 0 포함 여부 검사
if (Input.Contains(TEXT("0"))) return false;
// 각 자리 숫자 중복 여부 검사
if (Input[0] == Input[1] || Input[1] == Input[2] || Input[0] == Input[2]) return false;
return true;
}
FString ABBGGameModeBase::CalculateResult(const FString& Input, int32& OutStrike)
{
// 스트라이크 및 볼 개수 초기화
OutStrike = 0;
int32 Ball = 0;
// 3자리 숫자 순회 검사
for (int i = 0; i < 3; i++)
{
// 위치와 숫자 모두 일치 시 스트라이크 증가
if (Input[i] == SecretAnswer[i])
{
OutStrike++;
}
// 위치는 다르나 숫자가 존재할 시 볼 증가
else if (SecretAnswer.Contains(FString::Printf(TEXT("%c"), Input[i])))
{
Ball++;
}
}
// 일치 숫자 없을 시 OUT 텍스트 반환
if (OutStrike == 0 && Ball == 0) return TEXT("OUT");
// 계산된 스트라이크 및 볼 결과 문자열 반환
return FString::Printf(TEXT("%dS %dB"), OutStrike, Ball);
}
void ABBGGameModeBase::ProcessPlayerGuess(ABBGPlayerController* PC, const FString& GuessStr)
{
// 컨트롤러 유효성 검사
if (!PC) return;
// 플레이어 상태 및 잔여 기회 검사
ABBGPlayerState* PS = PC->GetPlayerState<ABBGPlayerState>();
if (!PS || !PS->HasAttemptsLeft()) return;
// 입력값 검증 실패 시 당사자에게만 에러 출력 후 로직 종료
if (!IsValidInput(GuessStr))
{
PC->ClientShowMessage(TEXT("잘못된 입력입니다. 1~9 사이의 중복 없는 3자리 숫자를 입력하세요."));
return;
}
// 시도 횟수 차감
PS->IncrementAttempt();
// 입력값 판정 및 결과 문자열 획득
int32 Strikes = 0;
FString ResultStr = CalculateResult(GuessStr, Strikes);
// 전체 출력용 결과 텍스트 포맷 조립
FString MsgToClient = FString::Printf(TEXT("[%s] [%d / %d] %s -> %s"),
*PS->GetPlayerName(), PS->GetCurrentAttempts(), PS->GetMaxAttempts(), *GuessStr, *ResultStr);
// 접속 중인 전체 클라이언트 화면에 메세지 출력
for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
ABBGPlayerController* LoopPC = Cast<ABBGPlayerController>(It->Get());
if (LoopPC)
{
LoopPC->ClientShowMessage(MsgToClient);
}
}
// 3 스트라이크 달성 시 승리 공지 및 게임 초기화
if (Strikes == 3)
{
AnnounceToAll(FString::Printf(TEXT("Winner는 %s 입니다! (정답: %s)"), *PS->GetPlayerName(), *SecretAnswer));
ResetGame();
return;
}
// 전체 인원 기회 소진 여부 검사 로직 호출
CheckGameEndCondition();
}
void ABBGGameModeBase::BeginPlay()
{
Super::BeginPlay();
// 서버 시작 직후 정답 난수 최초 생성
GenerateRandomNumbers();
}
void ABBGGameModeBase::CheckGameEndCondition()
{
// 무승부 판별 상태 변수
bool bIsDraw = true;
// 전체 플레이어 잔여 기회 순회 검사
for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
ABBGPlayerController* PC = Cast<ABBGPlayerController>(It->Get());
if (PC)
{
ABBGPlayerState* PS = PC->GetPlayerState<ABBGPlayerState>();
// 기회 남은 플레이어 발견 시 무승부 상태 해제
if (PS && PS->HasAttemptsLeft())
{
bIsDraw = false;
break;
}
}
}
// 전원 기회 소진 상태일 경우 무승부 공지 및 초기화
if (bIsDraw)
{
AnnounceToAll(TEXT("모든 기회 소진. 무승부입니다!"));
ResetGame();
}
}
void ABBGGameModeBase::ResetGame()
{
// 신규 정답 난수 생성
GenerateRandomNumbers();
// 전체 클라이언트 시도 횟수 데이터 초기화
for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
ABBGPlayerController* PC = Cast<ABBGPlayerController>(It->Get());
if (PC)
{
ABBGPlayerState* PS = PC->GetPlayerState<ABBGPlayerState>();
if (PS)
{
// 플레이어 횟수 0으로 되돌리기
PS->ResetAttempt();
}
}
}
}
void ABBGGameModeBase::AnnounceToAll(const FString& Message)
{
// 전체 클라이언트 공지용 UI 텍스트 변수 업데이트
for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
ABBGPlayerController* PC = Cast<ABBGPlayerController>(It->Get());
if (PC)
{
PC->NotificationText = FText::FromString(Message);
}
}
}
// BBGPlayerState.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "BBGPlayerState.generated.h"
// 플레이어 고유의 상태 데이터(시도 횟수 등) 보관 및 동기화 클래스
UCLASS()
class BASEBALLGAME_API ABBGPlayerState : public APlayerState
{
GENERATED_BODY()
public:
ABBGPlayerState();
// 서버와 클라이언트 간 네트워크 변수 동기화 설정
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
// 현재 시도 횟수 반환
UFUNCTION(BlueprintCallable)
int32 GetCurrentAttempts() const { return CurrentAttempts; }
// 최대 시도 가능 횟수 반환
UFUNCTION(BlueprintCallable)
int32 GetMaxAttempts() const { return MaxAttempts; }
// 숫자 입력 시 현재 시도 횟수 1 증가
void IncrementAttempt() { CurrentAttempts++; }
// 현재 시도 횟수 0으로 초기화
void ResetAttempts() { CurrentAttempts = 0; }
// 남은 입력 기회 존재 여부
bool HasAttemptsLeft() const { return CurrentAttempts < MaxAttempts; }
// 현재 시도 횟수 0으로 초기화 선언
void ResetAttempt();
protected:
// 클라이언트에게 실시간 동기화되는 현재 시도 횟수 변수
UPROPERTY(Replicated)
int32 CurrentAttempts;
// 블루프린트에서 편집 가능한 게임 최대 시도 횟수 (기본값 3)
UPROPERTY(EditDefaultsOnly)
int32 MaxAttempts = 3;
};
// BBGPlayerState.cpp
#include "Player/BBGPlayerState.h"
#include "Net/UnrealNetwork.h"
// 생성자 및 초기값 설정
ABBGPlayerState::ABBGPlayerState()
{
// 네트워크 복제 활성화
bReplicates = true;
// 초기 시도 횟수 0 설정
CurrentAttempts = 0;
}
// 서버 및 클라이언트 간 변수 동기화 설정
void ABBGPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// CurrentAttempts 변수를 동기화 대상으로 등록
DOREPLIFETIME(ABBGPlayerState, CurrentAttempts);
}
// 시도 횟수 초기화 로직
void ABBGPlayerState::ResetAttempt()
{
// 현재 시도 횟수 0 설정
CurrentAttempts = 0;
}
// BBGPlayerController.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "BBGPlayerController.generated.h"
UCLASS()
class BASEBALLGAME_API ABBGPlayerController : public APlayerController
{
GENERATED_BODY()
public:
// 게임 시작 시 최초 1회 호출 및 공지 위젯 생성
virtual void BeginPlay() override;
// 서버와 클라이언트 간 네트워크 변수 동기화 설정
virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override;
// 클라이언트가 입력한 숫자 값을 서버로 전송
UFUNCTION(Server, Reliable, WithValidation)
void ServerSubmitGuess(const FString& GuessStr);
// 서버의 텍스트 메세지를 특정 클라이언트 화면에 출력
UFUNCTION(Client, Reliable)
void ClientShowMessage(const FString& Message);
// 승리 및 무승부 공지를 클라이언트 화면에 출력
UFUNCTION(Client, Reliable)
void ClientShowAnnouncement(const FString& AnnouncementMsg);
// 서버 변경 시 클라이언트 위젯에 자동 동기화되는 공지 텍스트 변수
UPROPERTY(Replicated, BlueprintReadOnly)
FText NotificationText;
protected:
// 블루프린트에서 지정할 공지 위젯 클래스
UPROPERTY(EditDefaultsOnly)
TSubclassOf<class UUserWidget> NotificationTextWidgetClass;
// 화면에 생성된 공지 위젯 인스턴스 보관
UPROPERTY()
TObjectPtr<UUserWidget> NotificationTextWidgetInstance;
};
// BBGPlayerController.cpp
#include "Player/BBGPlayerController.h"
#include "Game/BBGGameModeBase.h"
#include "Net/UnrealNetwork.h"
#include "Blueprint/UserWidget.h"
void ABBGPlayerController::BeginPlay()
{
Super::BeginPlay();
// 로컬 컨트롤러인 경우에만 UI 생성
if (!IsLocalController()) return;
// 블루프린트에 지정된 클래스로 공지 위젯 생성 및 화면 추가
if (NotificationTextWidgetClass)
{
NotificationTextWidgetInstance = CreateWidget<UUserWidget>(this, NotificationTextWidgetClass);
if (NotificationTextWidgetInstance)
{
NotificationTextWidgetInstance->AddToViewport();
}
}
}
bool ABBGPlayerController::ServerSubmitGuess_Validate(const FString& GuessStr)
{
// 입력값 무조건 승인
return true;
}
void ABBGPlayerController::ServerSubmitGuess_Implementation(const FString& GuessStr)
{
// 게임모드 인스턴스 획득 후 입력값 판정 로직 실행
ABBGGameModeBase* GameMode = GetWorld()->GetAuthGameMode<ABBGGameModeBase>();
if (GameMode)
{
GameMode->ProcessPlayerGuess(this, GuessStr);
}
}
void ABBGPlayerController::ClientShowMessage_Implementation(const FString& Message)
{
// 5초간 메세지 출력
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Yellow, Message);
}
}
void ABBGPlayerController::ClientShowAnnouncement_Implementation(const FString& AnnouncementMsg)
{
// 10초간 공지 출력
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 10.f, FColor::Red, AnnouncementMsg);
}
}
void ABBGPlayerController::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// NotificationText 변수를 서버와 클라이언트 간 동기화 대상으로 지정
DOREPLIFETIME(ABBGPlayerController, NotificationText);
}
// BBGChatWidget.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "BBGChatWidget.generated.h"
// 채팅 입력을 처리하는 UI 위젯 클래스
UCLASS()
class BASEBALLGAME_API UBBGChatWidget : public UUserWidget
{
GENERATED_BODY()
protected:
// UI 화면 초기 생성 시 자동 호출
virtual void NativeConstruct() override;
// 블루프린트 내 동일한 이름의 텍스트 박스 컴포넌트와 자동 연결
UPROPERTY(meta = (BindWidget))
class UEditableTextBox* EditableTextBox_ChatInput;
// 텍스트 박스에서 엔터키 입력 시 실행
UFUNCTION()
void OnChatInputCommitted(const FText& Text, ETextCommit::Type CommitMethod);
};
// BBGChatWidget.cpp
#include "BBGChatWidget.h"
#include "Components/EditableTextBox.h"
#include "Player/BBGPlayerController.h"
void UBBGChatWidget::NativeConstruct()
{
Super::NativeConstruct();
// 텍스트 박스 엔터키 입력 이벤트와 처리 함수 연결
if (EditableTextBox_ChatInput)
{
EditableTextBox_ChatInput->OnTextCommitted.AddDynamic(this, &UBBGChatWidget::OnChatInputCommitted);
}
}
void UBBGChatWidget::OnChatInputCommitted(const FText& Text, ETextCommit::Type CommitMethod)
{
// 엔터키 입력 시에만 내부 로직 실행
if (CommitMethod == ETextCommit::OnEnter)
{
// 현재 UI 소유 플레이어 컨트롤러 획득
ABBGPlayerController* PC = Cast<ABBGPlayerController>(GetOwningPlayer());
if (PC)
{
// 컨트롤러를 통해 서버로 입력 텍스트 전송
PC->ServerSubmitGuess(Text.ToString());
}
// 전송 완료 후 텍스트 박스 내용 비우기
EditableTextBox_ChatInput->SetText(FText::GetEmpty());
}
}