TIL(67)

이채원·7일 전

TIL

목록 보기
69/72

오늘의 할일
1. 코드카타 정리
2. 세션 정리


코드카타 정리

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으로 바뀔 뿐 전체 로직은 단 한 줄도 길어지지 않습니다.

2. 숫자 짝꿍

문제 설명
두 정수 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)이면서 가장 빠른 방법입니다.


Unreal Engine 멀티플레이어 게임 개발 : 3회차 정리

챕터 2-1 : Remote Procedure Call 기초

RPC : 호출하는 PC와 실행하는 PC가 달라도 되게끔 해주는 통신 기법
-> 게임에 큰 영향을 끼치지 않는 일시적인 효과들에 사용(사운드, 파티클 등)

함수를 실행할 때 내부적으로 동작하는 방식에 따라 Call과 Invoke로 구분

1. Call

  • 특징: 정적인(Static) 의미
  • 결정 시점: 프로그램이 빌드되는 컴파일 타임에 어떤 함수가 실행될지, 호출하는 곳과 실행하는 곳이 이미 명확하게 정해져 있음.
  • 작동 방식: 대상 함수를 직접적으로(Direct) 호출하고 실행
  • 예시: 일반적인 전역 함수나 클래스의 멤버 함수 호출이 여기에 해당

2. Invoke

  • 특징: 동적인(Dynamic) 의미
  • 결정 시점: 프로그램이 실행 중인 런타임 상황에 따라 어떤 함수가 실행될지, 호출하는 곳과 실제로 실행되는 곳이 어디인지 결정
  • 작동 방식: 대리자나 네트워크를 거쳐 간접적으로(Indirect) 함수를 호출하고 실행
  • 예시: 함수 포인터, 동적 바인딩, 그리고 RPC가 대표적인 Invoke 방식

멀티플레이 프로그래밍에서 클라이언트가 서버의 함수를 실행하거나, 서버가 클라이언트의 함수를 실행하도록 명령할 때 "RPC를 Invoke 한다"고 표현

RPC는 코드를 작성하는 시점(컴파일 타임)에 바로 내 컴퓨터에서 실행될지 확정되는 것이 아니라, 게임이 돌아가는 도중(런타임)에 네트워크 상태와 NetRole, NetMode에 따라 실행될 타겟(서버 혹은 특정 클라이언트)이 동적으로 결정되어 간접적으로 실행되기 때문

Actor Ownership

PlayerController가 Local PlayerController와 같다면, Client-Owned Actor

  • 예: 내 플레이어 캐릭터

PlayerController가 Local PlayerController와 다르다면, Owned by different client

  • 예: 내 화면에 보이는 친구의 플레이어 캐릭터

서버에서 스폰되었지만 SetOwner() 함수 호출이 없으면, Server-Owned Actor

  • 예: 보물상자 액터

Unowned Actor == Server-Owned Actor 라고 생각해도 됨

NetMulticast, Server, Client 키워드

UFUNCTION() 매크로와 함께 사용하는 해당 원격 PC에서 RPC를 실행시켜 달라는 요청
실제로 실행될지는 표를 통해 판단

NetMulticast : 서버를 포함한 모든 클라이언트에서 해당 RPC 실행 요청
Server : 클라이언트에서 서버쪽으로 해당 RPC 실행 요청
Client : 서버에서 클라이언트쪽으로 해당 RPC 실행 요청

NetMulticast -> 해당 폰 클래스에다가 NetMulticast RPC를 선언하고 호출했다고 가정 -> 데디케이트 서버, 본인 자신한테도 실행되고 클라이언트에서도 똑같이 실행, 클라이언트 2번도 똑같이 실행(레플리케이트 되어있기 때문)

Server RPC에서는 클라이언트 쪽에서 폰 쪽으로 실행하는 것
Client RPC에서는 서버에서 호출이 되고 클라이언트에서 실행되는 것

<자주 쓰이는 케이스>
1. RPC가 클라이언트에서 호출되고 서버에서 실행되어야 하는 경우에는 Server 키워드를 사용해야 함

  • ClientConnection이 소유하고 있는 액터(Client-Owned)에서 RPC가 호출되어야함
  • _Validate() 함수에서 RPC를 실행할지 말지 결정(유효성 검사 필요해서)
  1. RPC가 서버와 모든 클라이언트에서 실행되어야 하는 경우에는 NetMulticast 키워드를 사용해야 함
  • 서버에서 호출
  • 부하가 심하므로, 빈번하게 호출되게끔 코드를 작성하는 건 권장하지 않음(모든 클라이언트랑 서버까지 다 포함하기 때문에 부하가 심함)
  1. RPC가 서버에서 호출되고 클라이언트에서 실행되어야 하는 경우에는 Client 키워드를 사용해야 함
  • 해당 ClientConnection이 소유하고 있는 액터에서 RPC가 호출되어야 함(대표적 예시 : PlayerController)

WithValidation :

  • UFUNCTION() 매크로와 함께 사용되는 키워드
  • 서버 RPC에서 사용
  • _Implementation() 함수와 _Validate() 함수로 나뉘어짐.
  • 서버 실행 로직은 무조건적으로 신뢰되기 때문에, 위변조를 막기 위한 방어막 역할

Unreliable VS Reliable

  • UFUNCTION() 매크로와 함께 사용되는 키워드
  • RPC는 기본적으로 Unreliable 키워드를 기준으로 동작(아무것도 안쓰면 Unreliable)
  • Unreliable은 원격 PC에서 무조건 실행되리라는 보장 없음
  • 원격 PC에서 무조건 실행되어야 하는 로직이라면 Reliable 키워드를 작성
    Unreliable : 꾸미기 용도
    Reliable : 충돌, 데미지 등 큰 영향 끼치는 것들

챕터 2-2 : 멀티플레이 채팅 구현

서버와 클라이언트 간의 통신만 가능
-> 채팅 메시지는 일단 서버로 전송 -> 서버는 전송 받은 메세지를 다시 클라이언트들에게 전송

멀티플레이 채팅 구현

// 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);
		}
	}
}

NetMulticast RPC를 통한 서버 접속 알리기

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"));
	}
}

챕터 3-1 : Property Replication

레플리케이션(Replication) : 생성된 액터의 정보를 네트워크 내 다른 클라이언트에게 복제하는 작업
서버-클라이언트 모델에서는 서버에서 클라이언트로 복제되는 것이 규칙

레플리케이션 방법 :
1. RPC
2. Property Replication

Property Replication : 원하는 속성만을 다른 클라이언트에게 복제하는 것

  • 액터의 모든 속성의 변경된 값을 모든 클라이언트에게 복제하는 것은 비효율적

프로퍼티 레플리케이션 설정
1. 액터의 bReplicates 속성을 true로 설정
2. 네트워크로 복제할 액터의 속성을 키워드로 지정

  • UPROPERTY()에 Replicated 키워드 추가
  1. GetLifetimeReplicatedProps() 함수에 네트워크로 복제할 속성을 추가
  • #include “Net/UnrealNetwork.h” 헤더파일 추가

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

  • 서버에 액터가 없으므로 불가능 (프로퍼티 레플리케이션은 서버로부터 클라쪽으로의 단방향이기 때문)

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

  • 복제되어 있지 않으므로 불가능 (프로퍼티 레플리케이션이 가능하려면 bReplicates가 true여서 클라이언트 쪽에도 복제가 되어있어야 함)

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

  • 불가능, 각자 따로 생성된 액터기 때문에

챕터 3-2 : 판정 로직

세 자리 난수 생성 로직과 판정 로직 작성

정답을 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);
			}
		}
	}
}

챕터 3-3 : 시도 횟수 관리

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;

		}	
	}
}

챕터 3-4 : 패배, 승리, 무승부, 게임 리셋

접속한 플레이어에게 공지사항 띄우기

// 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();
			}
		}
	}
}
profile
초보 개발자

0개의 댓글