오늘의 할일
1. 코드카타 정리
2. 세션 정리
문제 설명
머쓱이는 태어난 지 11개월 된 조카를 돌보고 있습니다. 조카는 아직 "aya", "ye", "woo", "ma" 네 가지 발음과 네 가지 발음을 조합해서 만들 수 있는 발음밖에 하지 못하고 연속해서 같은 발음을 하는 것을 어려워합니다. 문자열 배열 babbling이 매개변수로 주어질 때, 머쓱이의 조카가 발음할 수 있는 단어의 개수를 return하도록 solution 함수를 완성해주세요.
#include <string>
#include <vector>
using namespace std;
int solution(vector<string> babbling) {
int answer = 0;
for (int i = 0; i < babbling.size(); i++) {
string word = babbling[i];
int pos = 0; // 현재 읽고 있는 글자의 위치
int prev_sound = 0; // 방금 낸 소리를 기억할 변수 (0: 없음, 1: aya, 2: ye, 3: woo, 4: ma)
bool is_valid = true; // 이 단어를 발음할 수 있는지 체크
// 단어 끝까지 계속 글자를 확인
while (pos < word.length()) {
// 현재 위치에서 3글자를 잘랐을 때 aya고 방금 낸 소리가 aya(1)가 아니면
if (word.substr(pos, 3) == "aya" && prev_sound != 1) {
prev_sound = 1; // 방금 낸 소리를 "aya"로 업데이트
pos += 3; // 3글자를 읽었으니 위치를 3칸 앞으로 이동
}
// 현재 위치에서 2글자를 잘랐을 때 ye고, 방금 낸 소리가 ye(2)가 아니면
else if (word.substr(pos, 2) == "ye" && prev_sound != 2) {
prev_sound = 2;
pos += 2;
}
// 현재 위치에서 3글자를 잘랐을 때 woo고 방금 낸 소리가 woo(3)가 아니면
else if (word.substr(pos, 3) == "woo" && prev_sound != 3) {
prev_sound = 3;
pos += 3;
}
// 현재 위치에서 2글자를 잘랐을 때 ma고 방금 낸 소리가 ma(4)가 아니면
else if (word.substr(pos, 2) == "ma" && prev_sound != 4) {
prev_sound = 4;
pos += 2;
}
// 네 가지 소리 중 아무것도 아니거나, 연속된 소리라서 발음할 수 없는 경우
else {
is_valid = false; // 불합격 처리
break;
}
}
// 단어를 끝까지 읽고 불합격 판정을 받지 않았다면
if (is_valid) {
answer++;
}
}
return answer;
}
AI가 작성한 코드
#include <string>
#include <vector>
using namespace std;
int solution(vector<string> babbling) {
int answer = 0;
// 1. 하드코딩된 if-else를 없애고, 발음 사전을 정적 배열로 분리합니다.
const string sounds[4] = {"aya", "ye", "woo", "ma"};
// 2. 값 복사를 방지하기 위해 const string& (상수 참조)로 단어를 읽어옵니다.
for (const string& word : babbling) {
int pos = 0;
int prev_sound = -1;
bool is_valid = true;
while (pos < word.length()) {
bool matched = false;
// 3. 4개의 발음 사전을 돌며 현재 글자와 일치하는지 확인합니다.
for (int i = 0; i < 4; i++) {
// substr 대신 compare 함수를 사용하여 메모리 할당 없이 글자를 비교합니다.
if (word.compare(pos, sounds[i].length(), sounds[i]) == 0) {
if (prev_sound == i) { // 연속 발음 체크
is_valid = false;
break;
}
prev_sound = i;
pos += sounds[i].length();
matched = true;
break; // 일치하는 발음을 찾았으니 사전 검색을 멈춥니다.
}
}
// 4. 사전에 있는 4가지 발음 중 어떤 것과도 일치하지 않거나, 연속 발음으로 판정된 경우
if (!matched || !is_valid) {
is_valid = false;
break;
}
}
// 끝까지 무사히 파싱(Parsing)을 마쳤다면 정답 처리합니다.
if (is_valid) {
answer++;
}
}
return answer;
}
substr 소멸과 compare 도입 (Zero-Allocation):
이전 코드의 word.substr()은 호출될 때마다 힙(Heap) 메모리에 새로운 문자열 덩어리를 잘라서 계속 생성합니다. 만약 몬스터 AI나 전투 시스템이 매 프레임마다 이런 문자열 자르기를 수행한다면 심각한 메모리 파편화와 프레임 드랍이 발생합니다. 반면 word.compare(시작점, 길이, 비교할단어)는 원본 문자열을 건드리지 않고 '눈으로만' 빠르게 비교하고 넘어가므로 메모리 할당(동적 할당) 비용이 '0'입니다.
const string& 참조자 사용:
for (string word : babbling) 처럼 쓰면 배열 안의 단어를 매번 새로운 변수에 통째로 '복사'해서 가져옵니다. 뒤에 &(참조자)를 붙이면 복사하지 않고 원본 데이터의 주소만 슬쩍 들여다보기 때문에, 데이터가 클수록 속도 차이가 엄청나게 벌어집니다.
데이터와 로직의 분리 (Data-Driven Design):
이전에는 if (.. == "aya"), else if (.. == "ye") 처럼 로직 안에 데이터가 섞여 있어, 나중에 옹알이 종류가 10개로 늘어나면 코드 길이도 10배로 길어집니다. 이 코드는 sounds라는 배열로 데이터를 분리해 두었기 때문에, 옹알이 종류가 100개로 늘어나도 안쪽 for문의 조건만 i < 100으로 바뀔 뿐 전체 로직은 단 한 줄도 길어지지 않습니다.
문제 설명
두 정수 X, Y의 임의의 자리에서 공통으로 나타나는 정수 k(0 ≤ k ≤ 9)들을 이용하여 만들 수 있는 가장 큰 정수를 두 수의 짝꿍이라 합니다(단, 공통으로 나타나는 정수 중 서로 짝지을 수 있는 숫자만 사용합니다). X, Y의 짝꿍이 존재하지 않으면, 짝꿍은 -1입니다. X, Y의 짝꿍이 0으로만 구성되어 있다면, 짝꿍은 0입니다.
예를 들어, X = 3403이고 Y = 13203이라면, X와 Y의 짝꿍은 X와 Y에서 공통으로 나타나는 3, 0, 3으로 만들 수 있는 가장 큰 정수인 330입니다. 다른 예시로 X = 5525이고 Y = 1255이면 X와 Y의 짝꿍은 X와 Y에서 공통으로 나타나는 2, 5, 5로 만들 수 있는 가장 큰 정수인 552입니다(X에는 5가 3개, Y에는 5가 2개 나타나므로 남는 5 한 개는 짝 지을 수 없습니다.)
두 정수 X, Y가 주어졌을 때, X, Y의 짝꿍을 return하는 solution 함수를 완성해주세요.
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
string solution(string X, string Y) {
string answer = "";
// 0부터 9까지 숫자가 몇 번 나왔는지 countX, countY 2개(모두 0으로 초기화)
int countX[10] = {0};
int countY[10] = {0};
// X 문자열을 돌면서 각 숫자가 몇 개 있는지 체크
for (int i = 0; i < X.length(); i++) {
// '3'에서 '0'을 빼면 실제 숫자 3 (아스키코드 원리)
int num = X[i] - '0';
countX[num]++;
}
// Y 문자열도 똑같이 체크
for (int i = 0; i < Y.length(); i++) {
int num = Y[i] - '0';
countY[num]++;
}
// 가장 큰 수를 만들어야 하니까 제일 큰 숫자인 9부터 0까지 거꾸로
for (int i = 9; i >= 0; i--) {
int common_count = min(countX[i], countY[i]);
// 공통으로 가진 개수만큼 숫자를 이어 붙이기
for (int j = 0; j < common_count; j++) {
answer += to_string(i);
}
}
// 예외 처리: 짝꿍이 하나도 없어서 문자열 비면 "-1"
if (answer == "") {
return "-1";
}
// 예외 처리: 짝꿍을 만들었는데 0으로만 시작한다면 그냥 "0"으로 바꾸기
if (answer[0] == '0') {
return "0";
}
return answer;
}
AI가 작성한 코드
#include <string>
#include <vector>
#include <algorithm> // min 함수 사용
using namespace std;
string solution(string X, string Y) {
int countX[10] = {0};
int countY[10] = {0};
// 1. 인덱스(i) 변수 없이 범위 기반 for문으로 문자열을 직접 순회합니다.
for (char c : X) countX[c - '0']++;
for (char c : Y) countY[c - '0']++;
string answer = "";
// 2. 가장 큰 숫자(9)부터 0까지 확인하며 문자열을 조립합니다.
for (int i = 9; i >= 0; i--) {
int common_count = min(countX[i], countY[i]);
if (common_count > 0) {
// 3. 반복문 안의 += 연산과 to_string()을 완전히 제거하고,
// append 함수를 통해 '필요한 개수만큼 단번에' 메모리에 복사해 넣습니다.
answer.append(common_count, i + '0');
}
}
// 4. empty() 함수를 사용하여 빈 문자열인지 더 직관적으로 확인합니다.
if (answer.empty()) return "-1";
if (answer[0] == '0') return "0";
return answer;
}
to_string과 += 연산의 소멸 (Zero Overhead)
이전 코드의 answer += to_string(i)는 겉보기엔 간단하지만, 내부적으로는 끔찍한 연산 폭탄입니다. 매번 숫자를 문자로 바꾸기 위해 새로운 임시 문자열(객체)을 생성하고, 그걸 다시 정답 문자열 뒤에 이어 붙이기 위해 힙(Heap) 메모리를 헐었다가 다시 짓는 과정을 300만 번 반복할 수도 있습니다.
반면 answer.append(개수, 문자)를 사용하면, 컴퓨터가 "아! 숫자 9를 10만 개 한 번에 붙이라는 거구나!" 하고 필요한 메모리 공간을 딱 한 번만 확보한 뒤 블록 단위로 초고속 복사를 수행합니다.
char와 아스키코드를 이용한 직접 치환 (i + '0')
to_string(3) 같은 무거운 변환 함수를 쓸 필요가 없습니다. 컴퓨터에게 3 + '0'(아스키코드 48)을 계산하라고 하면 즉시 아스키코드 51번, 즉 문자 '3'이 튀어나옵니다. 숫자를 문자로 바꾸는 가장 로우레벨(Low-level)이면서 가장 빠른 방법입니다.
RPC : 호출하는 PC와 실행하는 PC가 달라도 되게끔 해주는 통신 기법
-> 게임에 큰 영향을 끼치지 않는 일시적인 효과들에 사용(사운드, 파티클 등)

함수를 실행할 때 내부적으로 동작하는 방식에 따라 Call과 Invoke로 구분
멀티플레이 프로그래밍에서 클라이언트가 서버의 함수를 실행하거나, 서버가 클라이언트의 함수를 실행하도록 명령할 때 "RPC를 Invoke 한다"고 표현
RPC는 코드를 작성하는 시점(컴파일 타임)에 바로 내 컴퓨터에서 실행될지 확정되는 것이 아니라, 게임이 돌아가는 도중(런타임)에 네트워크 상태와 NetRole, NetMode에 따라 실행될 타겟(서버 혹은 특정 클라이언트)이 동적으로 결정되어 간접적으로 실행되기 때문
PlayerController가 Local PlayerController와 같다면, Client-Owned Actor
PlayerController가 Local PlayerController와 다르다면, Owned by different client
서버에서 스폰되었지만 SetOwner() 함수 호출이 없으면, Server-Owned Actor
Unowned Actor == Server-Owned Actor 라고 생각해도 됨
UFUNCTION() 매크로와 함께 사용하는 해당 원격 PC에서 RPC를 실행시켜 달라는 요청
실제로 실행될지는 표를 통해 판단
NetMulticast : 서버를 포함한 모든 클라이언트에서 해당 RPC 실행 요청
Server : 클라이언트에서 서버쪽으로 해당 RPC 실행 요청
Client : 서버에서 클라이언트쪽으로 해당 RPC 실행 요청
NetMulticast -> 해당 폰 클래스에다가 NetMulticast RPC를 선언하고 호출했다고 가정 -> 데디케이트 서버, 본인 자신한테도 실행되고 클라이언트에서도 똑같이 실행, 클라이언트 2번도 똑같이 실행(레플리케이트 되어있기 때문)
Server RPC에서는 클라이언트 쪽에서 폰 쪽으로 실행하는 것
Client RPC에서는 서버에서 호출이 되고 클라이언트에서 실행되는 것
<자주 쓰이는 케이스>
1. RPC가 클라이언트에서 호출되고 서버에서 실행되어야 하는 경우에는 Server 키워드를 사용해야 함
WithValidation :
Unreliable VS Reliable
서버와 클라이언트 간의 통신만 가능
-> 채팅 메시지는 일단 서버로 전송 -> 서버는 전송 받은 메세지를 다시 클라이언트들에게 전송
// CXPlayerController.h
class CHATX_API ACXPlayerController : public APlayerController
{
public:
UFUNCTION(Client, Reliable)
void ClientRPCPrintChatMessageString(const FString& InChatMessageString);
UFUNCTION(Server, Reliable)
void ServerRPCPrintChatMessageString(const FString& InChatMessageString);
};
// CXPlayerController.cpp
#include "EngineUtils.h"
void ACXPlayerController::SetChatMessageString(const FString& InChatMessageString)
{
ChatMessageString = InChatMessageString;
//PrintChatMessageString(InChatMessageString);
if (IsLocalController() == true)
{
ServerRPCPrintChatMessageString(InChatMessageString);
}
}
void ACXPlayerController::ClientRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
PrintChatMessageString(InChatMessageString);
}
void ACXPlayerController::ServerRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
for (TActorIterator<ACXPlayerController> It(GetWorld()); It; ++It)
{
ACXPlayerController* CXPlayerController = *It;
if (IsValid(CXPlayerController) == true)
{
CXPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
}
}
}
GameStateBase를 부모 클래스로 하는 CXGameStateBase 만들기
// CXGameStateBase.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameStateBase.h"
#include "CXGameStateBase.generated.h"
UCLASS()
class CHATX_API ACXGameStateBase : public AGameStateBase
{
GENERATED_BODY()
public:
UFUNCTION(NetMulticast, Reliable)
void MulticastRPCBroadcastLoginMessage(const FString& InNameString = FString(TEXT("XXXXXXX")));
};
// CXGameStateBase.cpp
#include "CXGameStateBase.h"
#include "Kismet/GameplayStatics.h"
#include "Player/CXPlayerController.h"
void ACXGameStateBase::MulticastRPCBroadcastLoginMessage_Implementation(const FString& InNameString)
{
if (HasAuthority() == false)
{
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
if (IsValid(PC) == true)
{
ACXPlayerController* CXPC = Cast<ACXPlayerController>(PC);
if (IsValid(CXPC) == true)
{
FString NotificationString = InNameString + TEXT(" has joined the game.");
CXPC->PrintChatMessageString(NotificationString);
}
}
}
}
// CXGameModeBase.h
class CHATX_API ACXGameModeBase : public AGameModeBase
{
GENERATED_BODY()
public:
virtual void OnPostLogin(AController* NewPlayer) override;
};
// CXGameModeBase.cpp
#include "Game/CXGameModeBase.h"
#include "CXGameStateBase.h"
void ACXGameModeBase::OnPostLogin(AController* NewPlayer)
{
Super::OnPostLogin(NewPlayer);
ACXGameStateBase* CXGameStateBase = GetGameState<ACXGameStateBase>();
if (IsValid(CXGameStateBase) == true)
{
CXGameStateBase->MulticastRPCBroadcastLoginMessage(TEXT("XXXXXXX"));
}
}
레플리케이션(Replication) : 생성된 액터의 정보를 네트워크 내 다른 클라이언트에게 복제하는 작업
서버-클라이언트 모델에서는 서버에서 클라이언트로 복제되는 것이 규칙
레플리케이션 방법 :
1. RPC
2. Property Replication
Property Replication : 원하는 속성만을 다른 클라이언트에게 복제하는 것
프로퍼티 레플리케이션 설정
1. 액터의 bReplicates 속성을 true로 설정
2. 네트워크로 복제할 액터의 속성을 키워드로 지정
Q1. 아래와 같은 상황이라면, 프로퍼티 레플리케이션 가능할까요?

Q2. 아래와 같은 상황이라면, 프로퍼티 레플리케이션 가능할까요?

Q3. 아래와 같은 상황이라면, 프로퍼티 레플리케이션 가능할까요?

정답을 3개 생각 -> 자리랑 정답 힌트 주기
판정은 서버에서 해야함(게임모드)
// CXGameModeBase.h
class CHATX_API ACXGameModeBase : public AGameModeBase
{
public:
// 세 자리의 정답 시크릿 번호
FString GenerateSecretNumber();
// 비밀번호를 추측하는 번호(중복 숫자 안됨, 문자도 안됨)
bool IsGuessNumberString(const FString& InNumberString);
// 시크릿 번호랑 게스 번호가 얼마나 비슷한지
FString JudgeResult(const FString& InSecretNumberString, const FString& InGuessNumberString);
};
// CXGameModeBase.cpp
// 중복되지 않게 정답 번호 생성
FString ACXGameModeBase::GenerateSecretNumber()
{
TArray<int32> Numbers;
for (int32 i = 1; i <= 9; ++i)
{
Numbers.Add(i);
}
FMath::RandInit(FDateTime::Now().GetTicks());
Numbers = Numbers.FilterByPredicate([](int32 Num) { return Num > 0; });
FString Result;
for (int32 i = 0; i < 3; ++i)
{
int32 Index = FMath::RandRange(0, Numbers.Num() - 1);
Result.Append(FString::FromInt(Numbers[Index]));
Numbers.RemoveAt(Index);
}
return Result;
}
bool ACXGameModeBase::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;
}
UniqueDigits.Add(C);
}
if (bIsUnique == false)
{
break;
}
bCanPlay = true;
} while (false);
return bCanPlay;
}
FString ACXGameModeBase::JudgeResult(const FString& InSecretNumberString, const FString& InGuessNumberString)
{
int32 StrikeCount = 0, BallCount = 0;
for (int32 i = 0; i < 3; ++i)
{
if (InSecretNumberString[i] == InGuessNumberString[i])
{
StrikeCount++; //숫자랑 자리가 일치
}
else
{
FString PlayerGuessChar = FString::Printf(TEXT("%c"), InGuessNumberString[i]);
if (InSecretNumberString.Contains(PlayerGuessChar))
{
BallCount++; //자리가 이상함
}
}
}
if (StrikeCount == 0 && BallCount == 0)
{
return TEXT("OUT");
}
return FString::Printf(TEXT("%dS%dB"), StrikeCount, BallCount);
}
// CXPlayerController.cpp
#include "Kismet/GameplayStatics.h"
#include "Game/CXGameModeBase.h"
void ACXPlayerController::ServerRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
AGameModeBase* GM = UGameplayStatics::GetGameMode(this);
if (IsValid(GM) == true)
{
ACXGameModeBase* CXGM = Cast<ACXGameModeBase>(GM);
if (IsValid(CXGM) == true)
{
CXGM->PrintChatMessageString(this, InChatMessageString);
}
}
}
// CXGameModeBase.cpp
#include "Player/CXPlayerController.h"
#include "EngineUtils.h"
void ACXGameModeBase::OnPostLogin(AController* NewPlayer)
{
ACXPlayerController* CXPlayerController = Cast<ACXPlayerController>(NewPlayer);
if (IsValid(CXPlayerController) == true)
{
AllPlayerControllers.Add(CXPlayerController);
}
}
void ACXGameModeBase::BeginPlay()
{
Super::BeginPlay();
SecretNumberString = GenerateSecretNumber();
}
// 게임의 중요한 규칙들
void ACXGameModeBase::PrintChatMessageString(ACXPlayerController* InChattingPlayerController, const FString& InChatMessageString)
{
FString ChatMessageString = InChatMessageString;
int Index = InChatMessageString.Len() - 3;
FString GuessNumberString = InChatMessageString.RightChop(Index);
if (IsGuessNumberString(GuessNumberString) == true)
{
FString JudgeResultString = JudgeResult(SecretNumberString, GuessNumberString);
for (TActorIterator<ACXPlayerController> It(GetWorld()); It; ++It)
{
ACXPlayerController* CXPlayerController = *It;
if (IsValid(CXPlayerController) == true)
{
FString CombinedMessageString = InChatMessageString + TEXT(" -> ") + JudgeResultString;
CXPlayerController->ClientRPCPrintChatMessageString(CombinedMessageString);
}
}
}
else
{
for (TActorIterator<ACXPlayerController> It(GetWorld()); It; ++It)
{
ACXPlayerController* CXPlayerController = *It;
if (IsValid(CXPlayerController) == true)
{
CXPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
}
}
}
}
PlayerState를 부모로 갖는 CXPlayerState 생성
// CXGameModeBase.cpp
#include "Player/CXPlayerState.h"
void ACXGameModeBase::OnPostLogin(AController* NewPlayer)
{
Super::OnPostLogin(NewPlayer);
ACXPlayerController* CXPlayerController = Cast<ACXPlayerController>(NewPlayer);
if (IsValid(CXPlayerController) == true)
{
AllPlayerControllers.Add(CXPlayerController);
ACXPlayerState* CXPS = CXPlayerController->GetPlayerState<ACXPlayerState>();
if (IsValid(CXPS) == true)
{
CXPS->PlayerNameString = TEXT("Player") + FString::FromInt(AllPlayerControllers.Num());
}
ACXGameStateBase* CXGameStateBase = GetGameState<ACXGameStateBase>();
if (IsValid(CXGameStateBase) == true)
{
CXGameStateBase->MulticastRPCBroadcastLoginMessage(CXPS->PlayerNameString);
}
}
}
// CXPlayerController.cpp
#include "CXPlayerState.h"
void ACXPlayerController::SetChatMessageString(const FString& InChatMessageString)
{
ChatMessageString = InChatMessageString;
if (IsLocalController() == true)
{
// ServerRPCPrintChatMessageString(InChatMessageString);
ACXPlayerState* CXPS = GetPlayerState<ACXPlayerState>();
if (IsValid(CXPS) == true)
{
FString CombinedMessageString = CXPS->PlayerNameString + TEXT(": ") + InChatMessageString;
ServerRPCPrintChatMessageString(CombinedMessageString);
}
}
}
// CXPlayerState.h
class CHATX_API ACXPlayerState : public APlayerState
{
GENERATED_BODY()
public:
ACXPlayerState();
virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override;
public:
UPROPERTY(Replicated)
FString PlayerNameString;
};
// CXPlayerState.cpp
#include "CXPlayerState.h"
#include "Net/UnrealNetwork.h"
ACXPlayerState::ACXPlayerState()
: PlayerNameString(TEXT("None"))
{
bReplicates = true;
}
void ACXPlayerState::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ThisClass, PlayerNameString);
}
// CXPlayerState.h
class CHATX_API ACXPlayerState : public APlayerState
{
public:
UPROPERTY(Replicated)
int32 CurrentGuessCount;
UPROPERTY(Replicated)
int32 MaxGuessCount;
};
// CXPlayerState.cpp
#include "CXPlayerState.h"
#include "Net/UnrealNetwork.h"
ACXPlayerState::ACXPlayerState()
: PlayerNameString(TEXT("None"))
, CurrentGuessCount(0)
, MaxGuessCount(3)
{
bReplicates = true;
}
void ACXPlayerState::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ThisClass, PlayerNameString);
DOREPLIFETIME(ThisClass, CurrentGuessCount);
DOREPLIFETIME(ThisClass, MaxGuessCount);// CXPlayerState.cpp
#include "CXPlayerState.h"
#include "Net/UnrealNetwork.h"
ACXPlayerState::ACXPlayerState()
: PlayerNameString(TEXT("None"))
, CurrentGuessCount(0)
, MaxGuessCount(3)
{
bReplicates = true;
}
void ACXPlayerState::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ThisClass, PlayerNameString);
DOREPLIFETIME(ThisClass, CurrentGuessCount);
DOREPLIFETIME(ThisClass, MaxGuessCount); // Q. MaxGuessCount가 프로퍼티 레플리케이션될 필요가 있을까? -> 상황에 따라 달라진다 (필요할 때 추가해주기)
}
}
// CXGameModeBase.h
class CHATX_API ACXGameModeBase : public AGameModeBase
{
GENERATED_BODY()
public:
void IncreaseGuessCount(ACXPlayerController* InChattingPlayerController);
};
// CXGameModeBase.cpp
void ACXGameModeBase::PrintChatMessageString(ACXPlayerController* InChattingPlayerController, const FString& InChatMessageString)
{
FString ChatMessageString = InChatMessageString;
int Index = InChatMessageString.Len() - 3;
FString GuessNumberString = InChatMessageString.RightChop(Index);
if (IsGuessNumberString(GuessNumberString) == true)
{
FString JudgeResultString = JudgeResult(SecretNumberString, GuessNumberString);
IncreaseGuessCount(InChattingPlayerController);
for (TActorIterator<ACXPlayerController> It(GetWorld()); It; ++It)
{
ACXPlayerController* CXPlayerController = *It;
if (IsValid(CXPlayerController) == true)
{
FString CombinedMessageString = InChatMessageString + TEXT(" -> ") + JudgeResultString;
CXPlayerController->ClientRPCPrintChatMessageString(CombinedMessageString);
}
}
}
else
{
for (TActorIterator<ACXPlayerController> It(GetWorld()); It; ++It)
{
ACXPlayerController* CXPlayerController = *It;
if (IsValid(CXPlayerController) == true)
{
CXPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
}
}
}
}
void ACXGameModeBase::IncreaseGuessCount(ACXPlayerController* InChattingPlayerController)
{
ACXPlayerState* CXPS = InChattingPlayerController->GetPlayerState<ACXPlayerState>();
if (IsValid(CXPS) == true)
{
CXPS->CurrentGuessCount++;
}
}
// CXPlayerState.h
class CHATX_API ACXPlayerState : public APlayerState
{
GENERATED_BODY()
public:
FString GetPlayerInfoString();
};
// CXPlayerState.cpp
FString ACXPlayerState::GetPlayerInfoString()
{
FString PlayerInfoString = PlayerNameString + TEXT("(") + FString::FromInt(CurrentGuessCount) + TEXT("/") + FString::FromInt(MaxGuessCount) + TEXT(")");
return PlayerInfoString;
}
// CXPlayerController.cpp
void ACXPlayerController::SetChatMessageString(const FString& InChatMessageString)
{
if (IsLocalController() == true)
{
if (IsValid(CXPS) == true)
{
FString CombinedMessageString = CXPS->GetPlayerInfoString() + TEXT(": ") + InChatMessageString;
}
}
}
// CXPlayerController.h
class UUserWidget;
UCLASS()
class CHATX_API ACXPlayerController : public APlayerController
{
public:
ACXPlayerController();
virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override;
protected:
UPROPERTY(EditDefaultsOnly)
TSubclassOf<UUserWidget> NotificationTextWidgetClass;
UPROPERTY()
TObjectPtr<UUserWidget> NotificationTextWidgetInstance;
public:
UPROPERTY(Replicated, BlueprintReadOnly)
FText NotificationText;
};
// CXPlayerController.cpp
#include "Net/UnrealNetwork.h"
ACXPlayerController::ACXPlayerController()
{
bReplicates = true;
}
void ACXPlayerController::BeginPlay()
{
if (IsValid(NotificationTextWidgetClass) == true)
{
NotificationTextWidgetInstance = CreateWidget<UUserWidget>(this, NotificationTextWidgetClass);
if (IsValid(NotificationTextWidgetInstance) == true)
{
NotificationTextWidgetInstance->AddToViewport();
}
}
}
void ACXPlayerController::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ThisClass, NotificationText);
}
// CXGameModeBase.cpp
void ACXGameModeBase::OnPostLogin(AController* NewPlayer)
{
Super::OnPostLogin(NewPlayer);
ACXPlayerController* CXPlayerController = Cast<ACXPlayerController>(NewPlayer);
if (IsValid(CXPlayerController) == true)
{
CXPlayerController->NotificationText = FText::FromString(TEXT("Connected to the game server."));
}
}


// CXGameModeBase.h
class CHATX_API ACXGameModeBase : public AGameModeBase
{
GENERATED_BODY()
public:
void ResetGame();
void JudgeGame(ACXPlayerController* InChattingPlayerController, int InStrikeCount);
};
// CXGameModeBase.cpp
void ACXGameModeBase::PrintChatMessageString(ACXPlayerController* InChattingPlayerController, const FString& InChatMessageString)
{
if (IsGuessNumberString(GuessNumberString) == true)
{
for (TActorIterator<ACXPlayerController> It(GetWorld()); It; ++It)
{
if (IsValid(CXPlayerController) == true)
{
int32 StrikeCount = FCString::Atoi(*JudgeResultString.Left(1));
JudgeGame(InChattingPlayerController, StrikeCount);
}
}
}
}
void ACXGameModeBase::ResetGame()
{
SecretNumberString = GenerateSecretNumber();
for (const auto& CXPlayerController : AllPlayerControllers)
{
ACXPlayerState* CXPS = CXPlayerController->GetPlayerState<ACXPlayerState>();
if (IsValid(CXPS) == true)
{
CXPS->CurrentGuessCount = 0;
}
}
}
void ACXGameModeBase::JudgeGame(ACXPlayerController* InChattingPlayerController, int InStrikeCount)
{
if (3 == InStrikeCount)
{
ACXPlayerState* CXPS = InChattingPlayerController->GetPlayerState<ACXPlayerState>();
for (const auto& CXPlayerController : AllPlayerControllers)
{
if (IsValid(CXPS) == true)
{
FString CombinedMessageString = CXPS->PlayerNameString + TEXT(" has won the game.");
CXPlayerController->NotificationText = FText::FromString(CombinedMessageString);
ResetGame();
}
}
}
else
{
bool bIsDraw = true;
for (const auto& CXPlayerController : AllPlayerControllers)
{
ACXPlayerState* CXPS = CXPlayerController->GetPlayerState<ACXPlayerState>();
if (IsValid(CXPS) == true)
{
if (CXPS->CurrentGuessCount < CXPS->MaxGuessCount)
{
bIsDraw = false;
break;
}
}
}
if (true == bIsDraw)
{
for (const auto& CXPlayerController : AllPlayerControllers)
{
CXPlayerController->NotificationText = FText::FromString(TEXT("Draw..."));
ResetGame();
}
}
}
}