오늘은 언리얼의 멀티 네트워크를 깊게 이해해보고자 숫자 야구 게임을 만드는 과제를 진행했다.
이렇게 채팅을 "/123" 이런 느낌으로 치면 서버가 생성한 난수에 대한 야구 게임을 시작하는거다.
일단 작업 플로우는 다음과 같이 설계했다.
1. 난수 생성 클래스 구현
- 여기서는 몇개의 난수를 생성할건지만 매개변수로 받아서 동작하게 설계했다
(나중에 난수를 또 만들게 되었을 때 재사용할 수 있으면 좋겠어서)
2. 판정 클래스 구현
- 플레이어가 입력한 Input에 대해 몇 개의 수를 맞췄는지 알려주는 부분을 구현했다.
3. GameMode 클래스 구현
- 게임모드는 난수생성클래스와 판정클래스를 가져와서 이 데이터들을 저장한다.
- 플레이어의 Input에 대한 예외처리(숫자갯수, 문자가 들어갔는지, 0이 포함되어있는지, 공백이나 특수문자가 있는지 등)을 처리했다.
- 플레이어의 승리, 패배 기능을 구현했다.
4. PlayerContoller (구현 중)
- 멀티캐스트로 RPC 함수 뿌려주는 부분
- 위젯과 연결
+코드짜면서 모르는 부분 공부
이 중에서 오늘은 서버 연결 바로 전 플로우인 3번 플로우까지 진행했다.
난수 생성 클래스 구현
난수는 0을 제외한 1 ~ 9까지의 수를 통해 생성해야한다.
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "GenerateRandomNumber.generated.h"
UCLASS(Blueprintable)
class SIMPLECHAT_API UGenerateRandomNumber : public UObject
{
GENERATED_BODY()
public:
UGenerateRandomNumber();
UFUNCTION(BlueprintCallable, Category = "BaseBallGame")
TArray<int32> GenerateSecretNumber(int32 NumDigits = 3); // 추후 3개말고 더 늘릴 수도 있어서 매개변수로 디폴트 지정해줬음
private:
UPROPERTY()
TArray<int32> InitDigits;
};
#include "GenerateRandomNumber.h"
#include "Math/UnrealMathUtility.h"
UGenerateRandomNumber::UGenerateRandomNumber()
{
}
TArray<int32> UGenerateRandomNumber::GenerateSecretNumber(int32 NumDigits)
{
TArray<int32> Digits = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
TArray<int32> SecretNumber;
for (int i = 0; i < NumDigits; i++)
{
int32 RandomIndex = FMath::RandRange(0, Digits.Num() - 1);
int32 SelectedDigit = Digits[RandomIndex];
SecretNumber.Add(SelectedDigit);
Digits.RemoveAt(RandomIndex);
}
return SecretNumber;
}
TArray<int> 형태에 원하는 숫자들을 모두 넣어주었다.
반목문을 돌면서 랜덤으로 숫자를 뽑아주고, 반환값에 추가해주었다.
그리고 이 뽑힌 랜덤 수는 중복을 없애기 위해 삭제해주었다.
판정 클래스 구현
판정은 생성된 난수와 플레이어의 입력만을 비교해서 판정하도록 구현했다.
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "JudgmentNumber.generated.h"
UCLASS(Blueprintable)
class SIMPLECHAT_API UJudgmentNumber : public UObject
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "BaseBallGame")
FString Judgment(const TArray<int32>& SecretNumber, const TArray<int32>& PlayerNumber);
};
#include "JudgmentNumber.h"
FString UJudgmentNumber::Judgment(const TArray<int32>& SecretNumber, const TArray<int32>& PlayerNumber)
{
if (SecretNumber.Num() != PlayerNumber.Num())
{
return "배열 초과";
}
int32 StrikeCount = 0;
int32 BallCount = 0;
for (int i = 0; i < SecretNumber.Num(); i++)
{
if (PlayerNumber[i] == SecretNumber[i])
StrikeCount++;
else if (SecretNumber.Contains(PlayerNumber[i]))
BallCount++;
}
if (StrikeCount == SecretNumber.Num())
return TEXT("WIN");
else if (StrikeCount == 0 && BallCount == 0)
return TEXT("OUT");
else
return FString::Printf(TEXT("%dS%dB"), StrikeCount, BallCount);
}
+트러블 슈팅
상황
게임을 실행해서 동작을 확인하던 도중, SecretNumber에서 Index Out Of Range 라는 예외가 떴었다.
알고보니 생성된 난수의 길이와 플레이어가 입력한 인풋의 길이체크를 안해줘서 예외가 나는거였다.
해결방법
생성된 난수의 길이와 플레이어가 입력한 인풋의 길이를 비교해서 같지 않으면 return 하는 방식으로 간단하게 추가해줬다.
GameMode 클래스 구현
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "SimpleChatGameMode.generated.h"
class UGenerateRandomNumber;
class UJudgmentNumber;
UCLASS()
class SIMPLECHAT_API ASimpleChatGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
ASimpleChatGameMode();
virtual void StartPlay() override;
UFUNCTION(BlueprintCallable, Category = "BaseBallGame|GameMode")
void JugmentPlayerInput(const FString& PlayerInput);
protected:
UFUNCTION(BlueprintCallable, Category = "BaseBallGame|GameMode")
void InitializeGame();
private:
UPROPERTY()
TArray<int32> SecretNumber;
UPROPERTY()
UGenerateRandomNumber* SecretNumberGenerate;
UPROPERTY()
UJudgmentNumber* Judgment;
UPROPERTY()
int32 PlayerInputCount;
static const int32 MaxInputCount = 3;
};
#include "SimpleChatGameMode.h"
#include "GenerateRandomNumber.h"
#include "JudgmentNumber.h"
#include "Engine/Engine.h"
ASimpleChatGameMode::ASimpleChatGameMode()
{
PlayerInputCount = MaxInputCount;
SecretNumberGenerate = CreateDefaultSubobject<UGenerateRandomNumber>(TEXT("SecreteNumberGenerate"));
Judgment = CreateDefaultSubobject<UJudgmentNumber>(TEXT("JudgmentPlayerInput"));
}
void ASimpleChatGameMode::StartPlay()
{
Super::StartPlay();
InitializeGame();
}
void ASimpleChatGameMode::InitializeGame()
{
// 랜덤난수 생성하고 저장
if (IsValid(SecretNumberGenerate))
{
SecretNumber = SecretNumberGenerate->GenerateSecretNumber(3);
PlayerInputCount = MaxInputCount;
FString SecretStr;
for (int32 Num : SecretNumber)
{
SecretStr += FString::FromInt(Num);
}
UE_LOG(LogTemp, Log, TEXT("[ServerGameMode] Secret Number: %s"), *SecretStr);
}
}
void ASimpleChatGameMode::JugmentPlayerInput(const FString& PlayerInput)
{
// 입력 문자열에서 /, 공백 제거
FString CleanInput = PlayerInput.TrimStartAndEnd();
if (CleanInput.StartsWith("/"))
{
CleanInput = CleanInput.RightChop(1);
}
CleanInput = CleanInput.TrimStartAndEnd();
// 입력 길이 체크
if (CleanInput.Len() != 3)
{
UE_LOG(LogTemp, Warning, TEXT("[ServerGameMode] 유효하지 않은 입력 : %s"), *PlayerInput);
if (GEngine)
{
//GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("Invalid input length."));
}
return;
}
// 문자열을 TArray<int32>로 변환
TArray<int32> PlayerGuess;
for (int i = 0; i < CleanInput.Len(); i++)
{
TCHAR ch = CleanInput[i];
if (!FChar::IsDigit(ch))
{
UE_LOG(LogTemp, Warning, TEXT("[ServerGameMode] 숫자가 아닌 문자가 포함됨: %s"), *PlayerInput);
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("Contains non-digit characters."));
}
return;
}
int32 digit = ch - '0';
if (digit == 0)
{
UE_LOG(LogTemp, Warning, TEXT("[ServerGameMode] 0은 허용되지 않음: %s"), *PlayerInput);
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("0 is not allowed."));
}
return;
}
PlayerGuess.Add(digit);
}
// 중복 숫자 체크
TSet<int32> UniqueDigits;
for (int32 num : PlayerGuess)
{
UniqueDigits.Add(num);
}
if (UniqueDigits.Num() != PlayerGuess.Num())
{
UE_LOG(LogTemp, Warning, TEXT("[ServerGameMode] 중복 숫자가 있음: %s"), *PlayerInput);
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("Duplicate digits found."));
}
return;
}
// UJudgmentNumber 클래스를 통해 판정 결과를 얻음
if (Judgment)
{
FString Result = Judgment->Judgment(SecretNumber, PlayerGuess);
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Yellow, FString::Printf(TEXT("%s"), *Result));
}
UE_LOG(LogTemp, Log, TEXT("[ServerGameMode] Judgment Result: %s"), *Result);
PlayerInputCount--;
// 결과에 따른 게임 상태 처리
if (Result.Equals("WIN"))
{
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("Player WIN!"));
}
UE_LOG(LogTemp, Log, TEXT("[ServerGameMode] Player wins! Resetting game."));
// InitializeGame();
}
else if (PlayerInputCount <= 0)
{
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Orange, TEXT("Player LOSE! : No Count!"));
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Orange, FString::Printf(TEXT("Secret Number is %s !"), *Result));
}
UE_LOG(LogTemp, Log, TEXT("[ServerGameMode] No attempts remaining. Resetting game."));
// InitializeGame();
}
else
{
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::White, FString::Printf(TEXT("Reamainning Count : %d"), PlayerInputCount));
}
UE_LOG(LogTemp, Log, TEXT("[ServerGameMode] Reamainning Count : %d"), PlayerInputCount);
}
}
}
GameMode에서는 앞에서 만들었던 난수 생성 클래스와 판정 클래스를 가져와서 처리해주는 부분을 구현했다.
그리고 위젯에서 입력을 받을 때 먼저 데이터 예외처리를 해줄텐데 안전하게 가기 위해서 한번 더 진행해주었다.
+ 이 부분은 솔직히 중복된 코드들이라 나중에 유틸리티를 만들어서 끌어다쓰면 더 유용할듯하다.
로그는 사용자가 볼 수 있게 뷰포트에 한 번 띄워주고 디버깅하기위해 LogTemp로도 출력해주었다.
+트러블슈팅
상황
뷰포트로 로그찍은 부분이 한글이 깨져서 보이는 현상.
블루프린트로 Print String 찍으면 잘 나오는데 C++ 코드로 접근하면 왜 한글이 깨질까?
해결
과제 구현 시간 때문에 따로 서치를 못해보았다.
일단 임시로 다 영어로 출력하는걸로 변경했고, 과제가 끝나고 버그를 잡는 시간에 다시 해결해보고자 한다.
마지막 말
난수생성 클래스와 판정 클래스는 블루프린트에서 쓰려고 처음에는 블루프린트 라이브러리 클래스로 구현했다.
하지만 블루프린트 이해가 너무 어려워서 결국 위젯을 제외하고 모두 C++로 구현하려고 방식이 변경되었다.
하다보니 블루프린트 라이브러리 클래스는 C++ 코드를 통해 인스턴스 생성을 못한다고 해서 UObject로 변경해주었다.
멀티캐스트로 RPC 함수를 뿌려준다고하는데 서버가 가지고 있는 PlayerController에다가 뿌려야할지 클라이언트가 가지고 있는 PlayerController에 뿌려야할지? 아직 PlayController 소유에 대한 이해가 잡히지 않아 이 부분을 더 공부해야겠다.
'Unreal Engine' 카테고리의 다른 글
[ UE ] 격투 게임 기획 및 설계 (0) | 2025.03.24 |
---|---|
[UE, C++] 트러블슈팅, 멀티환경에서 카운트 개별 처리 (0) | 2025.03.21 |
[ UE ] 데디케이트 서버 환경 만들기 (0) | 2025.03.17 |
[ UE ] 언리얼 멀티플레이어에서 NetMode가 뭘까? (0) | 2025.03.14 |
[ UE ] FIntPoint가 뭘까? (0) | 2025.03.12 |