- 매치 관리 (어떻게 매치할건지 ← 규태님이랑 이야기해보기) - 플레이어 스폰(맵에 움직일 수 있는 랜덤한 위치에 생성할 예정 - 게임 시작/종료(하트 3개에 플레이어 1명 남을 때까지, 시간 타이머) - 플레이어 상태 초기화(체력, 스킬 쿨타임 등) - 이벤트 트리거(라운드시작, 종료, 플레이어 사망, 리스폰 등)
GameState
- GameMode에서 정의한 시작/종료 조건 업데이트 - 각 라운드별 결과 및 이벤트 저장 - HUD에 필요한 데이터 업데이트(체력, 콤보, 스킬 쿨타임, 남은 시간 등) - GameMode에서 이벤트 트리거 받아와서 UI에 전달 - 플레이어 사망했을 때 관전모드 - Rating 값 아웃게임에 넘겨주기? - 데미지, 준 피해량 저장하고 Rating 계산할 때 쓰기
GameInstance
- 선택한 캐릭터 저장(아웃게임에서 받아오기) - BattleManager(캐릭터랑 이야기) → 캐릭터에서 데이터 받아와서 판정하고 클라에 다시 뿌려주기 - Attack - TakeDamage - DI
인게임 UI
- 라운드 시작 UI(전투 시작 전 카운트로 표출) - 타이머 - 카메라 액션 - 피격당하거나 궁극기같은거 쓸 때 쉐이크 같은 것들(플레이어에서 하면 좋을 것 같음) - 최종 결과 UI
HUD
- 체력바(거리에 따라 UI 동기화할지 말지 추가하면 좋을듯) - 스킬 쿨타임
+PlayerController
+(후순위) 튜토리얼 진행
이런 식으로 설계해보았다.
전반적인 게임모드와 UI는 인게임, 아웃게임으로 분리해서 진행하였고 나는 인게임을 담당하기로 했다.
언리얼 프로젝트를 진행하면서 게임의 전반적인 흐름을 설계해보고 싶었는데 이번 기회에 잘 녹여내보아야겠다.
오늘은 언리얼의 멀티 네트워크를 깊게 이해해보고자 숫자 야구 게임을 만드는 과제를 진행했다.
이렇게 채팅을 "/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 "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 소유에 대한 이해가 잡히지 않아 이 부분을 더 공부해야겠다.