TIL
07.30 TIL
2026. 7. 30. 19:23

일정표

시간 할 일 비고
08:30~10:00 코드카타, 멀티플레이 강의  
10:00~13:00 TA 강의, 멀티플레이 강의  
14:00~18:00 멀티플레이 강의  
19:00~21:00 멀티플레이 강의  
21:00~22:00 운동  
22:00~23:30 개인 프로젝트  

*오늘의 코드카타*

 

문제. 달리기 경주

더보기

얀에서는 매년 달리기 경주가 열립니다. 해설진들은 선수들이 자기 바로 앞의 선수를 추월할 때 추월한 선수의 이름을 부릅니다. 예를 들어 1등부터 3등까지 "mumu", "soe", "poe" 선수들이 순서대로 달리고 있을 때, 해설진이 "soe"선수를 불렀다면 2등인 "soe" 선수가 1등인 "mumu" 선수를 추월했다는 것입니다. 즉 "soe" 선수가 1등, "mumu" 선수가 2등으로 바뀝니다.

선수들의 이름이 1등부터 현재 등수 순서대로 담긴 문자열 배열 players와 해설진이 부른 이름을 담은 문자열 배열 callings가 매개변수로 주어질 때, 경주가 끝났을 때 선수들의 이름을 1등부터 등수 순서대로 배열에 담아 return 하는 solution 함수를 완성해주세요

#include <string>
#include <vector>
#include <unordered_map>
#include <algorithm>

using namespace std;

vector<string> solution(vector<string> players, vector<string> callings) {
    unordered_map<string, int> player_indices;
    
    for (int i = 0; i < players.size(); ++i) {
        player_indices[players[i]] = i;
    }
    
    for (const string& name : callings) {

        int cur_idx = player_indices[name];
        int prev_idx = cur_idx - 1;
        
        string prev_player = players[prev_idx];
        swap(players[cur_idx], players[prev_idx]);
        
        player_indices[name] = prev_idx;
        player_indices[prev_player] = cur_idx;
    }
    
    return players;
}

 

해시 맵(Hash Map)을 통한 O(1) 위치 탐색 문제.

 

"선수 이름으로 현재 위치(인덱스)를 즉시 조회하는 장부"를 만든다.

  1. 위치 기억 장부 만들기 (unordered_map)
    • unordered_map<string, int> player_indices를 선언하고, 초기 등수 정보를 기록한다.
    • 예: player_indices["mumu"] = 0, player_indices["soe"] = 1, player_indices["poe"] = 2
  2. 추월 시 연산 (4단계)
    1. 현재 위치 찾기: 해시 맵에서 "soe"의 위치 idx (1)를 O(1)로 가져온다.
    2. 앞 선수 확인: players[idx - 1]을 통해 바로 앞선 선수 "mumu"를 가져온다.
    3. 배열 위치 교환: players 배열에서 두 선수의 위치를 맞바꾼다(swap).
    4. 해시 맵 갱신: 해시 맵의 두 선수 인덱스 정보도 교환한다.
      • player_indices["soe"] = idx - 1 (0)
      • player_indices["mumu"] = idx (1)

이 방식을 사용하면 추월 한 번당 연산이 O(1)이 되므로, 전체 시간 복잡도가 O(N + M)으로 줄어들게 된다.

 


*TA 강의*

주제 : Unreal Material 사용

 

1. 디졸브(Dissolve)의 개념 및 원리

  • 디졸브(Dissolve)란?
    • 오브젝트가 재, 불꽃, 마법 입자 등으로 타들어가거나 분해되면서 나타나거나 사라지는(Spawn / Death / Warp) 머티리얼 연출 기법.
  • 핵심 원리
    • 마스크 기반 절단(Cutout): 0.0(검은색)부터 1.0(흰색) 사이의 스칼라(Scalar) 값을 가지는 흑백 노이즈/텍스처를 준비.
    • 이 텍스처 값에 특정 기준값(Dissolve Progress 파라미터)을 비교하여, 기준값보다 낮은 영역은 화면에서 투명하게 잘라내고(Opacity Mask), 경계선 부분에는 발광(Emissive Color) 효과를 준다.

2. 노이즈 노드 & If / Smoothstep 기반 에지(Edge) 추출

 

① Noise 노드 + If 노드 방식

  • Noise 노드: 절차적(Procedural) 흑백 패턴을 즉석에서 계산해 출력.
  • If 노드 연결:
    • A: Noise 노드 출력값
    • B: Dissolve Amount 파라미터 (0.0 ~ 1.0)
    • A > B: 1 (표시됨)
    • A == B & A < B: 0 (사라짐 / Opacity Mask 처리)

② Smoothstep 노드를 이용한 스마트 에지(Edge) 추출

 

독립된 테두리(Glow Edge)를 만들기 위해 두 개의 진행도 차이를 이용.

  • 원리:
    • 현재 디졸브 진행도값(A)과, 아주 약간의 두께(Edge Width)를 더한 진행도값(B = A + Width)을 준비.
    • B 마스크 영역에서 A 마스크 영역을 빼면(B - A), 두 영역 사이의 띠(Boundary Band)만 남게 된다.
  • Smoothstep 노드 활용:
    • Smoothstep(Min, Max, Value) 노드는 입력값이 Min ~ Max 사이일 때 부드러운 S자 곡선 비율(0~1)로 값을 보간한다.
    • Min에 Dissolve Amount, Max에 Dissolve Amount + Edge Width를 넣으면 경계면의 그라데이션 마스크를 깔끔하게 추출할 수 있다.

3. 구현: 텍스처(Texture Sample) 기반 가공 연산

 

성능상(GPU Instruction 절감) Noise 노드 대신 타일링 가능한 흑백 텍스처(Seamless Noise Texture)의 R, G, B 채널 중 하나를 사용한다.

 

텍스처 데이터 가공 3단계 노드 체인

  1. 일정 값 빼기 (Subtract):
    • Texture - Dissolve Param: 텍스처 전체의 어두운 영역부터 차례대로 음수(-)(완전 검은색)로 떨어뜨린다.
  2. 대비(Contrast) 조절 (CheapContrast 노드):
    • 빼기 연산을 거친 텍스처의 경계선이 밋밋하므로 CheapContrast 노드를 붙여 0과 1 사이의 경계면을 가파르고 또렷하게 만든다.
  3. 반전 연산 (OneMinus / 1 - x 노드):
    • 마스크의 흑백을 반전시켜 타들어가는 방향(안쪽 ➔ 바깥쪽 vs 바깥쪽 ➔ 안쪽)을 원하는 대로 전환한다.

4. 바운딩 박스 기반 방향성 디졸브 (Directional Variation)

 

단순히 전체가 일제히 타들어가는 것이 아니라, "아래에서 위로", "왼쪽에서 오른쪽으로" 순차적으로 타들어가는 연출 기법.

 

① BoundingBoxBased_0-1-_UVW 노드

 

개념: 3D 메쉬의 최고/최저 지점 경계를 기준으로 X, Y, Z 축에 대해 0.0 ~ 1.0 정규화된 3차원 UVW 좌표를 생성하는 머티리얼 함수.

 

활용: ComponentMask 노드를 달아 R(X축), G(Y축), B(Z축) 중 원하는 축을 뽑아낸다.

 

예: B(Z축) 채널을 쓰면 모델의 발바닥은 0.0, 머리 끝은 1.0인 직관적인 높이 값이 된다.

 

② 내적(Dot Product)을 이용한 방향 정의

 

임의의 3D 방향 벡터(예: 위쪽 (0, 0, 1) 또는 대각선 벡터)와 UVW 좌표(또는 버텍스 노멀)를 Dot (내적) 연산하면, 개발자가 원하는 기울어진 벡터 방향의 평면 그라데이션을 쉽게 얻을 수 있다.


5. 바운드(Bound / Bounding Box)의 명확한 정의

  • 바운딩 박스(Bounding Box, AABB)란?
    • 3D 오브젝트/메쉬를 완전히 감싸는 최소 크기의 직육면체 공간.
  • TA 관점에서의 중요성:
    • 캐릭터가 거대하든 작든, 메쉬의 크기와 형태에 상관없이 시작점(0.0)과 끝점(1.0)을 보장받을 수 있어 어떤 3D 에셋에 얹어도 디졸브 효과가 메쉬 범위에 맞춰 균일하게 동작하게 해준다.

6. 트랜지션 및 경계빛(Emissive Edge) 합성

  1. Blend Mode 설정: 머티리얼 디테일 패널에서 Blend Mode를 Masked로 전환.
  2. Opacity Mask 연결: 디졸브 최종 마스크 노드를 Opacity Mask 핀에 연결.
  3. Emissive Color 연동:
    • 2단계에서 추출한 Edge 마스크에 Emissive Color(RGB)와 Emissive Multiplier(빛 강도, 예: 50.0)를 곱해 Emissive Color 핀에 연결.
    • 이렇게 하면 물체 몸통은 투명하게 사라지면서 타들어가는 가장자리 표면만 불꽃처럼 강렬하게 빛나게 된다.

7. Blend_Darken 및 다단계 마스크 체인

 

① Blend_Darken (어둡게 하기)

  • 수식 원리: Result = min(A, B)
  • 두 개의 입력 마스크 중 더 어두운(낮은) 값을 채택하는 블렌딩 방식.

② 디졸브 응용 (연속 전환 트랜지션)

  • BoundingBox (방향성 그라데이션)과 Texture (노이즈 패턴) 두 가지를 Blend_Darken으로 합성하면, "위에서 아래로 타내려가면서 동시에 가장자리는 노이즈 패턴으로 찌그러지는" 복합 디졸브 마스크가 완성된다.
  • 이 마스크 연산을 한 번 더 거쳐 Opacity Mask 노드에 연결하면 훨씬 밀도 높고 자연스러운 시각 효과를 낼 수 있다.

8. 경계면 3색 믹스 연출 (3ColorBlending)

 

디졸브 경계선의 빛이 단색이 아니라 [검은 탄자국 ➔ 붉은 불꽃 ➔ 하얀 고열 코어] 형태로 다채롭게 연출되도록 만든다.

  • 언리얼 이미지 조절 함수 활용 (3ColorBlending):
    • 3ColorBlending 함수 노드는 Dark Color, Mid Color, Bright Color 3개의 컬러 핀과 컨트롤 마스크를 입력받는다.
    • Smoothstep으로 분치해 둔 3단계 에지 마스크 수치를 이 함수에 전달하면, 타들어가는 경계선 내부에서 다채로운 열기 그래디언트를 표현할 수 있다.

9. 경계면 특수 효과 및 오프셋 보강

  1. 텍스처 오프셋 추가:
    • 경계선 에지 마스크 계산부에 미세한 타일링 텍스처를 곱하거나 연산하여, 경계선이 일직선으로 떨어지지 않고 요동치거나 흩날리는 효과를 만든다.
  2. VertexNormalWS (World Space Vertex Normal) 활용:
    • 월드 공간상의 표면 법선(Normal) 방향을 참조한다.
    • 예: VertexNormalWS의 B(Z축) 채널을 추출하면 "하늘을 바라보는 윗면"부터 디졸브가 먼저 진행되거나, 표면 방향에 따라 차별화된 불꽃 튀김 효과를 구현할 수 있다.

 

<TA 요약 체크리스트>

단계 핵심 노드 및 개념 비고 / 주요 팁
1. 마스크 준비 Texture Sample (R채널) Noise 노드보다 최적화에 우수함
2. 진행도 제어 Subtract ➔ CheapContrast 진행도 파라미터로 절단면 제어
3. 방향 부여 BoundingBoxBased_0-1-_UVW Z축(Blue)을 사용해 아래➔위 타들어감 연출
4. 에지 추출 Smoothstep 또는 마스크 차감 (B-A) 경계선 띠(Band) 영역 보간
5. 마스크 합성 Blend_Darken (min(A,B)) 방향성 그라데이션 + 노이즈 조합
6. 경계 발광 3ColorBlending ➔ Emissive Color [탄자국-불꽃-고열] 3단 발광 합성

*멀티플레이 게임 만들기 강의*

2-1 Remote Procedure Call 기초

 

1) RPC(Remote Procedure Call)

RPC는 간단히 말해 "호출하는 PC와 실행하는 PC가 달라도 되게끔 해주는 통신 기법"이다.

 

2) 언리얼에서 RPC의 용도

게임에 큰 영향을 끼치지 않는 일시적인 효과들에 사용하는 편이다. 주로 코스메틱(사운드, 파티클 등)에 사용한다.

게임에 큰 영향을 끼치는 것들은 이후에 배울 프로퍼티 레플리케이션을 사용하는 것이 좋다.

 

3) RPC 표 : Actor ownership, Not replicated, NetMulticast, Server, Client를 기준으로 구분하고 설명할 수 있어야 한다.

1. RPC invoked from the server

2. RPC invoked from a client

 

4) Call과 Invoke 구분하기

Call : 컴파일 타임에 어떤 함수인지, 호출하는 곳과 실행하는 곳이 정해저야 한다. 지금까지 정의했던 일반적인 전역 함수와 멤버 함수가 이에 해당한다. 즉, 직접적으로(Direct) 함수를 호출하고 실행해야 한다.

Invoke : 런타임에 어떤 함수인지, 호출하는 곳과 실행하는 곳이 어딘지 정해진다. 예를 들어, 함수 포인터, 동적 바인딩, RPC 같은 개념들이 이에 해당한다. 즉, 간접적으로(Indirect) 함수를 호출하고 실행해야 한다.

 

5) ★Actor Ownership : 

네트워크 멀티플레이가 적용되려면, 액터는 서버에서 스폰되고 bReplicated 가 true 여야 한다. 서버에서 스폰된 후 SetOwner(PlayerController) 함수를 호출해야 Client-Owned Actor가 된다.

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

-> PlayerController 가 Local PlayerController 와 다르다면, Owned by different client이다.

-> 서버에서 스폰되었지만 SetOwner() 함수 호출이 없으면, ServerOwned Actor이다.

 

6) NetMulticast, Server, Client 키워드

UFUNCTION() 매크로와 함께 사용되는 키워드. 키워드는 “해당 원격 PC에서 RPC를 실행시켜 달라는 요청”을 뜻한다. 실제로 실행될지는 표를 통해 판단해야 한다.

 

- NetMulticast : 서버를 포함한 모든 클라이언트에서 해당 RPC를 실행시키라는 요청.

- Server : 서버에서 해당 RPC를 실행시키라는 요청

- Client : 클라이언트에서 해당 RPC를 실행시키라는 요청

 

[RPC invoked from the server]

Actor Ownership Not Replicated NetMulticast Server Client
Client-owned actor Runs on server Runs on server and all client Runs on Server Runs on actor's owning client
Server-owned actor Runs on server Runs on server and all client Runs on Server Runs on Server
Unowned actor Runs on server Runs on server and all client Runs on Server Runs on Server

 

[RPC invoked from a client]

Actor Ownership Not Replicated NetMulticast Server Client
Owned by invoking client Runs on invoking client Runs on invoking client Runs on Server Runs on invoking client
Owned by a different client Runs on invoking client Runs on invoking client Dropped Runs on invoking client
Server-owned actor Runs on invoking client Runs on invoking client Dropped Runs on invoking client
Unowned actor Runs on invoking client Runs on invoking client Dropped Runs on invoking client

 

7) 자주 쓰이는 케이스

(1) RPC가 클라이언트에서 호출되고 서버에서 실행되어야 하는 경우에는 Server 키워드를 사용해야 한다.

-> ClientConnection 이 소유하고 있는 액터(Client-Owned)에서 RPC가 호출되어야 한다. _Validate() 함수에서 RPC를 실행할지 말지 결정한다.

 

(2) RPC가 서버와 모든 클라이언트에서 실행되어야 하는 경우에는 NetMulticast 키워드를 사용해야 한다.

-> 서버에서 호출해야 합니다. 부하가 심하므로, 빈번하게 호출되게끔 코드를 작성하는 건 권장되지 않습니 다. ex) Tick() 함수 내에서 NetMulticast RPC 호출

 

(3) RPC가 서버에서 호출되고 클라이언트에서 실행되어야 하는 경우에는 Client 키워드를 사용해야 합니다.

-> 서버에서 호출해야 합니다

 

8) With Validation

 

UFUNCTION() 매크로와 함께 사용되는 키워드. 서버에서 실행되는 RPC의 경우에 해당 키워드를 작성하는 것이 권장된다.

 

이 키워드가 붙은 RPC를 구현 할때는 _Implementation() 함수와 _Validate() 함수로 나뉘어 진다.

_Validate() 함수는 해당 RPC가 서버에서 실제로 실행할지 말지 결정한다. 서버 실행 로직은 무조건적으로 신뢰되기 때문에, 위변조를 막기 위한 방어막 역할을 합니다

 

9) Unreliable vs. Reliable

 

UFUNCTION() 매크로와 함께 사용되는 키워드입니다. RPC는 기본적으로 Unreliable 키워드를 기준으로 동작한다.

 

아무것도 안쓰면 Unreliable. Unreliable은 원격 PC에서 무조건 실행되리라는 보장이 없다.

-> 예시 : 코스메틱(이펙트, 사운드 등)처럼 게임에 큰 영향이 없는 로직.

 

반면, 원격 PC에서 무조건 실행되어야 하는 로직이라면 Reliable 키워드를 작성한다.

-> 예시 : 충돌, 데미지, 스폰처럼 게임에 큰 영향을 끼치는 로직.

 

10) ★언리얼 엔진에서 채팅 기능을 구현할 때 클라이언트가 NetMulticast 함수를 직접 호출하면 안 되는 핵심 이유

 

-> 엔진 구조상 브로드캐스트가 작동하지 않음

 

언리얼 엔진의 RPC(Remote Procedure Call)에는 엄격한 호출 및 실행 규칙이 있다.

서버(Server)가 NetMulticast를 호출할 때: 서버 자신을 포함하여 현재 접속한 모든 클라이언트에게 메시지가 Broadcast된다.

클라이언트(Client)가 NetMulticast를 호출할 때: 다른 사람에게 전혀 전파되지 않고, 함수를 호출한 해당 클라이언트 본인의 로컬 PC에서만 단일 함수처럼 실행된다.

즉, 클라이언트 A가 NetMulticast 채팅 함수를 실행해도 서버나 클라이언트 B, C에게는 아무런 메시지도 전달되지 않으며, 본인 화면에만 잠깐 뜨고 끝나게 된다.

 

-> 보안 및 권한 검증 문제 (Server Authority)

 

멀티플레이 게임 아키텍처에서는 "클라이언트는 절대 신뢰하지 않는다"는 원칙이 적용된다.

클라이언트가 서버를 거치지 않고 다른 플레이어들에게 직접 메시지를 보낼 수 있다면, 악의적인 유저가 핵/변조 클라이언트를 이용해 도배, 패킷 스크립트 공격, 거짓 게임 결과 전송 등을 자행할 수 있다.

반드시 서버를 거쳐야만 욕설/비속어 필터링, 채팅 길이 제한, 턴 상태 검증(내 턴에만 채팅 가능 등)을 안전하게 처리할 수 있다.

 


2-2 멀티플레이 채팅 구현

 

1) 데디케이티드 서버 구조에서, 내가 작성한 채팅 메시지는 일단 서버로 전송해야 한다. 그리고 나서 서버는 전송 받은 메세지를 다시 클라이언트들에게 전송해줘야 하는 구조이다.

더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "SMYPlayerController.generated.h"

class USMYChatInput;

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYPlayerController : public APlayerController
{
	GENERATED_BODY()
	
public:
	virtual void BeginPlay() override;
	
	void SetChatMessageString(const FString& InChatMessageString);
	
	void PrintChatMessageString(const FString& InChatMessageString);
	
	UFUNCTION(Client, Reliable)
	void ClientRPCPrintChatMessageString(const FString& InChatMessageString);
	
	UFUNCTION(Server, Reliable)
	void ServerRPCPrintChatMessageString(const FString& InChatMessageString);
	
protected:

	UPROPERTY(EditDefaultsOnly)
	TSubclassOf<USMYChatInput> ChatInputWidgetClass;
	
	UPROPERTY()
	TObjectPtr<USMYChatInput> ChatInputWidgetInstance;
	
	FString ChatMessageString;
};
#include "Player/SMYPlayerController.h"
#include "UI/SMYChatInput.h"
#include "Kismet/KismetSystemLibrary.h"
#include "SMYProject.h"
#include "EngineUtils.h"

void ASMYPlayerController::BeginPlay()
{
	Super::BeginPlay();
	
	FInputModeUIOnly InputModeUIOnly;
	SetInputMode(InputModeUIOnly);
	
	if (IsValid(ChatInputWidgetClass) == true)
	{
		ChatInputWidgetInstance = CreateWidget<USMYChatInput>(this, ChatInputWidgetClass);
		if (IsValid(ChatInputWidgetInstance) == true)
		{
			ChatInputWidgetInstance->AddToViewport();
		}
	}
}

void ASMYPlayerController::SetChatMessageString(const FString& InChatMessageString)
{
	ChatMessageString = InChatMessageString;
	//PrintChatMessageString(InChatMessageString);
	if (IsLocalController() == true)
	{
		ServerRPCPrintChatMessageString(InChatMessageString);
	}
}

void ASMYPlayerController::PrintChatMessageString(const FString& InChatMessageString)
{
	// UKismetSystemLibrary::PrintString(this, ChatMessageString, true, true, FLinearColor::Red, 5.0f);
	FString NetModeString = SMYProjectFunctionLibrary::GetNetModeString(this);
	FString CombinedMessageString = FString::Printf(TEXT("%s: %s"), *NetModeString, *InChatMessageString); 
	SMYProjectFunctionLibrary::MyPrintString(this, CombinedMessageString, 10.f);
}

void ASMYPlayerController::ClientRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
	PrintChatMessageString(InChatMessageString);
}

void ASMYPlayerController::ServerRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
	for (TActorIterator<ASMYPlayerController> It(GetWorld()); It; ++It)
	{
		ASMYPlayerController* SMYPlayerController = *It;
		if (IsValid(SMYPlayerController) == true)
		{
			SMYPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
		}
	}
}

 

2) 누군가 접속했을 때 메시지를 띄우려면 Multicast RPC를 사용해야 한다. 이때에는 GameState를 사용해야 한다.

더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameStateBase.h"
#include "SMYGameStateBase.generated.h"

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYGameStateBase : public AGameStateBase
{
	GENERATED_BODY()
	
public:
	UFUNCTION(NetMulticast, Reliable)
	void MulticastRPCBroadcastLoginMessage(const FString& InNameString = FString(TEXT("XXXXXXX")));
};
#include "Game/SMYGameStateBase.h"
#include "Kismet/GameplayStatics.h"
#include "Player/SMYPlayerController.h"

void ASMYGameStateBase::MulticastRPCBroadcastLoginMessage_Implementation(const FString& InNameString)
{
	if (HasAuthority() == false)
	{
		APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
		if (IsValid(PC) == true)
		{
			ASMYPlayerController* SMYPC = Cast<ASMYPlayerController>(PC);
			
			if (IsValid(SMYPC) == true)
			{
				FString NotificationString = InNameString + TEXT(" has joined the game.");
				SMYPC->PrintChatMessageString(NotificationString);
			}
		}
	}
}
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "SMYGameModeBase.generated.h"

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYGameModeBase : public AGameModeBase
{
	GENERATED_BODY()
	
public:
	virtual void OnPostLogin(AController* NewPlayer) override;

};
#include "Game/SMYGameModeBase.h"
#include "Game/SMYGameStateBase.h"

void ASMYGameModeBase::OnPostLogin(AController* NewPlayer)
{
	Super::OnPostLogin(NewPlayer);
	
	ASMYGameStateBase* SMYGameStateBase = GetGameState<ASMYGameStateBase>();
	if (IsValid(SMYGameStateBase) == true)
	{
		SMYGameStateBase->MulticastRPCBroadcastLoginMessage(TEXT("XXXXXXX"));
	}
}

3-1 Property Replication

 

1) 레플리케이션

 

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

 

레플리케이션 방법에는 두 가지 방법이 있는데, RPC(Remote Procedure Call), Property Replication 이 있다.

 

2) Property Replication

액터의 모든 속성의 변경된 값을 모든 클라이언트에게 복제하는 것은 비효율적이다. 우리가 원하는 속성만을 다른 클라이언트에게 복제하는 것을 프로퍼티 레플리케이션이라고 한다.

 

3) 프로퍼티 레플리케이션 설정 방법

액터의 Replicates 속성을 true 로 설정 >> 네트워크로 복제할 액터의 속성을 키워드로 지정. UPROPERTY()에 Replicated 키워드를 추가한다. >> GetLifetimeReplicatedProps() 함수에 네트워크로 복제할 속성을 추가. #include “Net/UnrealNetwork.h” 헤더파일 추가. DOREPLIFETIME 매크로를 사용해 복제할 속성을 명시한다.

 

*프로퍼티 레플리케이션은 항상 Authority에서 Proxy로만 가능하다.

 


3-2 판정 로직

 

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

더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "SMYGameModeBase.generated.h"

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYGameModeBase : public AGameModeBase
{
	GENERATED_BODY()
	
public:
	virtual void OnPostLogin(AController* NewPlayer) override;
	
	FString GenerateSecretNumber();
	
	bool IsGuessNumberString(const FString& InNumberString);
	
	FString JudgeResult(const FString& InSecretNumberString, const FString& InGuessNumberString);
};
#include "Game/SMYGameModeBase.h"
#include "Game/SMYGameStateBase.h"

void ASMYGameModeBase::OnPostLogin(AController* NewPlayer)
{
	Super::OnPostLogin(NewPlayer);
	
	ASMYGameStateBase* SMYGameStateBase = GetGameState<ASMYGameStateBase>();
	if (IsValid(SMYGameStateBase) == true)
	{
		SMYGameStateBase->MulticastRPCBroadcastLoginMessage(TEXT("XXXXXXX"));
	}
}

FString ASMYGameModeBase::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 ASMYGameModeBase::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 ASMYGameModeBase::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);
}

2) GameMode에 로직 적용하기

더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "SMYGameModeBase.generated.h"

class ASMYPlayerController;

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYGameModeBase : public AGameModeBase
{
	GENERATED_BODY()
	
public:
	
	virtual void BeginPlay() override;
	
	virtual void OnPostLogin(AController* NewPlayer) override;
	
	FString GenerateSecretNumber();
	
	bool IsGuessNumberString(const FString& InNumberString);
	
	FString JudgeResult(const FString& InSecretNumberString, const FString& InGuessNumberString);
	
	void PrintChatMessageString(ASMYPlayerController* InChattingPlayerController, const FString& InChatMessageString);
	
protected:
	FString SecretNumberString;
	
	TArray<TObjectPtr<ASMYPlayerController>> AllPlayerControllers;
};
#include "Game/SMYGameModeBase.h"
#include "Game/SMYGameStateBase.h"
#include "Player/SMYPlayerController.h"
#include "EngineUtils.h"

void ASMYGameModeBase::OnPostLogin(AController* NewPlayer)
{
	Super::OnPostLogin(NewPlayer);
	
	ASMYGameStateBase* SMYGameStateBase = GetGameState<ASMYGameStateBase>();
	if (IsValid(SMYGameStateBase) == true)
	{
		SMYGameStateBase->MulticastRPCBroadcastLoginMessage(TEXT("XXXXXXX"));
	}
	
	ASMYPlayerController* SMYPlayerController = Cast<ASMYPlayerController>
(NewPlayer);
	if (IsValid(SMYPlayerController) == true)
	{
		AllPlayerControllers.Add(SMYPlayerController);
	}
}

void ASMYGameModeBase::BeginPlay()
{
	Super::BeginPlay();
	SecretNumberString = GenerateSecretNumber();
}

FString ASMYGameModeBase::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 ASMYGameModeBase::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 ASMYGameModeBase::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);
}

void ASMYGameModeBase::PrintChatMessageString(ASMYPlayerController* 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<ASMYPlayerController> It(GetWorld()); It; ++It)
		{
			ASMYPlayerController* SMYPlayerController = *It;
			
			if (IsValid(SMYPlayerController) == true)
			{
				FString CombinedMessageString = InChatMessageString + TEXT(" -> ") + JudgeResultString;
				SMYPlayerController->ClientRPCPrintChatMessageString(CombinedMessageString);
			}
		}
	}
	
	else
	
	{
		for (TActorIterator<ASMYPlayerController> It(GetWorld()); It; ++It)
		{
			ASMYPlayerController* SMYPlayerController = *It;
			
			if (IsValid(SMYPlayerController) == true)
			{
				SMYPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
			}
		}
	}
}
#include "Player/SMYPlayerController.h"
#include "UI/SMYChatInput.h"
#include "Kismet/KismetSystemLibrary.h"
#include "SMYProject.h"
#include "EngineUtils.h"
#include "Kismet/GameplayStatics.h"
#include "Game/SMYGameModeBase.h"

void ASMYPlayerController::BeginPlay()
{
	Super::BeginPlay();
	
	FInputModeUIOnly InputModeUIOnly;
	SetInputMode(InputModeUIOnly);
	
	if (IsValid(ChatInputWidgetClass) == true)
	{
		ChatInputWidgetInstance = CreateWidget<USMYChatInput>(this, ChatInputWidgetClass);
		if (IsValid(ChatInputWidgetInstance) == true)
		{
			ChatInputWidgetInstance->AddToViewport();
		}
	}
}

void ASMYPlayerController::SetChatMessageString(const FString& InChatMessageString)
{
	ChatMessageString = InChatMessageString;
	//PrintChatMessageString(InChatMessageString);
	if (IsLocalController() == true)
	{
		ServerRPCPrintChatMessageString(InChatMessageString);
	}
}

void ASMYPlayerController::PrintChatMessageString(const FString& InChatMessageString)
{
	// UKismetSystemLibrary::PrintString(this, ChatMessageString, true, true, FLinearColor::Red, 5.0f);
	FString NetModeString = SMYProjectFunctionLibrary::GetNetModeString(this);
	FString CombinedMessageString = FString::Printf(TEXT("%s: %s"), *NetModeString, *InChatMessageString); 
	SMYProjectFunctionLibrary::MyPrintString(this, CombinedMessageString, 10.f);
}

void ASMYPlayerController::ClientRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
	PrintChatMessageString(InChatMessageString);
}

void ASMYPlayerController::ServerRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
	// for (TActorIterator<ASMYPlayerController> It(GetWorld()); It; ++It)
	// {
	// 	ASMYPlayerController* SMYPlayerController = *It;
	// 	if (IsValid(SMYPlayerController) == true)
	// 	{
	// 		SMYPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
	// 	}
	// }
	
	AGameModeBase* GM = UGameplayStatics::GetGameMode(this);
	if (IsValid(GM) == true)
	{
		ASMYGameModeBase* CXGM = Cast<ASMYGameModeBase>(GM);
		if (IsValid(CXGM) == true)
		{
			CXGM->PrintChatMessageString(this, InChatMessageString);
		}
	}
}

3-3 시도 횟수 관리

 

1) 플레이어 번호 지정

더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "SMYPlayerState.generated.h"

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYPlayerState : public APlayerState
{
	GENERATED_BODY()
	
public:
	FString PlayerNameString;
};
// CXPlayerState.cpp
#include "CXPlayerState.h"
#include "Game/SMYGameModeBase.h"
#include "Game/SMYGameStateBase.h"
#include "Player/SMYPlayerController.h"
#include "EngineUtils.h"
#include "Player/SMYPlayerState.h"

void ASMYGameModeBase::OnPostLogin(AController* NewPlayer)
{
	Super::OnPostLogin(NewPlayer);
	
// 	ASMYGameStateBase* SMYGameStateBase = GetGameState<ASMYGameStateBase>();
// 	if (IsValid(SMYGameStateBase) == true)
// 	{
// 		SMYGameStateBase->MulticastRPCBroadcastLoginMessage(TEXT("XXXXXXX"));
// 	}
// 	
// 	ASMYPlayerController* SMYPlayerController = Cast<ASMYPlayerController>
// (NewPlayer);
// 	if (IsValid(SMYPlayerController) == true)
// 	{
// 		AllPlayerControllers.Add(SMYPlayerController);
// 	}
	
	ASMYPlayerController* SMYPlayerController = Cast<ASMYPlayerController>(NewPlayer);
	
	if (IsValid(SMYPlayerController) == true)
	{
		AllPlayerControllers.Add(SMYPlayerController);
	
		ASMYPlayerState* SMYPS = SMYPlayerController->GetPlayerState<ASMYPlayerState>();
		if (IsValid(SMYPS) == true)
		{
			SMYPS->PlayerNameString = TEXT("Player") + FString::FromInt(AllPlayerControllers.Num());
		}
		
		ASMYGameStateBase* SMYGameStateBase = GetGameState<ASMYGameStateBase>();
		if (IsValid(SMYGameStateBase) == true)
		{
			SMYGameStateBase->MulticastRPCBroadcastLoginMessage(SMYPS->PlayerNameString);
		}
	}
}

void ASMYGameModeBase::BeginPlay()
{
	Super::BeginPlay();
	SecretNumberString = GenerateSecretNumber();
}

FString ASMYGameModeBase::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 ASMYGameModeBase::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 ASMYGameModeBase::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);
}

void ASMYGameModeBase::PrintChatMessageString(ASMYPlayerController* 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<ASMYPlayerController> It(GetWorld()); It; ++It)
		{
			ASMYPlayerController* SMYPlayerController = *It;
			
			if (IsValid(SMYPlayerController) == true)
			{
				FString CombinedMessageString = InChatMessageString + TEXT(" -> ") + JudgeResultString;
				SMYPlayerController->ClientRPCPrintChatMessageString(CombinedMessageString);
			}
		}
	}
	
	else
	
	{
		for (TActorIterator<ASMYPlayerController> It(GetWorld()); It; ++It)
		{
			ASMYPlayerController* SMYPlayerController = *It;
			
			if (IsValid(SMYPlayerController) == true)
			{
				SMYPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
			}
		}
	}
}
#include "Player/SMYPlayerController.h"
#include "UI/SMYChatInput.h"
#include "Kismet/KismetSystemLibrary.h"
#include "SMYProject.h"
#include "EngineUtils.h"
#include "Kismet/GameplayStatics.h"
#include "Game/SMYGameModeBase.h"
#include "Player/SMYPlayerState.h"

void ASMYPlayerController::BeginPlay()
{
	Super::BeginPlay();
	
	FInputModeUIOnly InputModeUIOnly;
	SetInputMode(InputModeUIOnly);
	
	if (IsValid(ChatInputWidgetClass) == true)
	{
		ChatInputWidgetInstance = CreateWidget<USMYChatInput>(this, ChatInputWidgetClass);
		if (IsValid(ChatInputWidgetInstance) == true)
		{
			ChatInputWidgetInstance->AddToViewport();
		}
	}
}

void ASMYPlayerController::SetChatMessageString(const FString& InChatMessageString)
{
	ChatMessageString = InChatMessageString;
	//PrintChatMessageString(InChatMessageString);
	if (IsLocalController() == true)
	{
		// ServerRPCPrintChatMessageString(InChatMessageString);
		ASMYPlayerState* SMYPS = GetPlayerState<ASMYPlayerState>();
		if (IsValid(SMYPS) == true)
		{
			FString CombinedMessageString = SMYPS->PlayerNameString + TEXT(": ") + InChatMessageString;
			ServerRPCPrintChatMessageString(CombinedMessageString);
		}
	}
}

void ASMYPlayerController::PrintChatMessageString(const FString& InChatMessageString)
{
	// UKismetSystemLibrary::PrintString(this, ChatMessageString, true, true, FLinearColor::Red, 5.0f);
	FString NetModeString = SMYProjectFunctionLibrary::GetNetModeString(this);
	FString CombinedMessageString = FString::Printf(TEXT("%s: %s"), *NetModeString, *InChatMessageString); 
	SMYProjectFunctionLibrary::MyPrintString(this, CombinedMessageString, 10.f);
}

void ASMYPlayerController::ClientRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
	PrintChatMessageString(InChatMessageString);
}

void ASMYPlayerController::ServerRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
	// for (TActorIterator<ASMYPlayerController> It(GetWorld()); It; ++It)
	// {
	// 	ASMYPlayerController* SMYPlayerController = *It;
	// 	if (IsValid(SMYPlayerController) == true)
	// 	{
	// 		SMYPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
	// 	}
	// }
	
	AGameModeBase* GM = UGameplayStatics::GetGameMode(this);
	if (IsValid(GM) == true)
	{
		ASMYGameModeBase* SMYGM = Cast<ASMYGameModeBase>(GM);
		if (IsValid(SMYGM) == true)
		{
			SMYGM->PrintChatMessageString(this, InChatMessageString);
		}
	}
}

2) 프로퍼티 레플리케이션

더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "SMYPlayerState.generated.h"

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYPlayerState : public APlayerState
{
	GENERATED_BODY()
	
public:
	ASMYPlayerState();
	virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override;
	
	UPROPERTY(Replicated)
	FString PlayerNameString;
};
#include "Player/SMYPlayerState.h"
#include "Net/UnrealNetwork.h"

ASMYPlayerState::ASMYPlayerState()
: PlayerNameString(TEXT("None"))
{
	bReplicates = true;
}

void ASMYPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME(ThisClass, PlayerNameString);
}

3) 시도 횟수 관리

더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "SMYPlayerState.generated.h"

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYPlayerState : public APlayerState
{
	GENERATED_BODY()
	
public:
	ASMYPlayerState();
	virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override;
	
	UPROPERTY(Replicated)
	FString PlayerNameString;
	
	UPROPERTY(Replicated)
	int32 CurrentGuessCount;
	
	UPROPERTY(Replicated)
	int32 MaxGuessCount;

};
#include "Player/SMYPlayerState.h"
#include "Net/UnrealNetwork.h"

ASMYPlayerState::ASMYPlayerState()
	: PlayerNameString(TEXT("None"))
	, CurrentGuessCount(0)
	, MaxGuessCount(3)

{
	bReplicates = true;
}

void ASMYPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME(ThisClass, PlayerNameString);
	DOREPLIFETIME(ThisClass, CurrentGuessCount);
	DOREPLIFETIME(ThisClass, MaxGuessCount);
}
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "SMYGameModeBase.generated.h"

class ASMYPlayerController;

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYGameModeBase : public AGameModeBase
{
	GENERATED_BODY()
	
public:
	
	virtual void BeginPlay() override;
	
	virtual void OnPostLogin(AController* NewPlayer) override;
	
	FString GenerateSecretNumber();
	
	bool IsGuessNumberString(const FString& InNumberString);
	
	FString JudgeResult(const FString& InSecretNumberString, const FString& InGuessNumberString);
	
	void PrintChatMessageString(ASMYPlayerController* InChattingPlayerController, const FString& InChatMessageString);
	
	void IncreaseGuessCount(ASMYPlayerController* InChattingPlayerController);
	
protected:
	FString SecretNumberString;
	
	TArray<TObjectPtr<ASMYPlayerController>> AllPlayerControllers;
};
#include "Game/SMYGameModeBase.h"
#include "Game/SMYGameStateBase.h"
#include "Player/SMYPlayerController.h"
#include "EngineUtils.h"
#include "Player/SMYPlayerState.h"

void ASMYGameModeBase::OnPostLogin(AController* NewPlayer)
{
	Super::OnPostLogin(NewPlayer);
	
// 	ASMYGameStateBase* SMYGameStateBase = GetGameState<ASMYGameStateBase>();
// 	if (IsValid(SMYGameStateBase) == true)
// 	{
// 		SMYGameStateBase->MulticastRPCBroadcastLoginMessage(TEXT("XXXXXXX"));
// 	}
// 	
// 	ASMYPlayerController* SMYPlayerController = Cast<ASMYPlayerController>
// (NewPlayer);
// 	if (IsValid(SMYPlayerController) == true)
// 	{
// 		AllPlayerControllers.Add(SMYPlayerController);
// 	}
	
	ASMYPlayerController* SMYPlayerController = Cast<ASMYPlayerController>(NewPlayer);
	
	if (IsValid(SMYPlayerController) == true)
	{
		AllPlayerControllers.Add(SMYPlayerController);
	
		ASMYPlayerState* SMYPS = SMYPlayerController->GetPlayerState<ASMYPlayerState>();
		if (IsValid(SMYPS) == true)
		{
			SMYPS->PlayerNameString = TEXT("Player") + FString::FromInt(AllPlayerControllers.Num());
		}
		
		ASMYGameStateBase* SMYGameStateBase = GetGameState<ASMYGameStateBase>();
		if (IsValid(SMYGameStateBase) == true)
		{
			SMYGameStateBase->MulticastRPCBroadcastLoginMessage(SMYPS->PlayerNameString);
		}
	}
}

void ASMYGameModeBase::BeginPlay()
{
	Super::BeginPlay();
	SecretNumberString = GenerateSecretNumber();
}

FString ASMYGameModeBase::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 ASMYGameModeBase::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 ASMYGameModeBase::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);
}

void ASMYGameModeBase::PrintChatMessageString(ASMYPlayerController* 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<ASMYPlayerController> It(GetWorld()); It; ++It)
		{
			ASMYPlayerController* SMYPlayerController = *It;
			
			if (IsValid(SMYPlayerController) == true)
			{
				FString CombinedMessageString = InChatMessageString + TEXT(" -> ") + JudgeResultString;
				SMYPlayerController->ClientRPCPrintChatMessageString(CombinedMessageString);
			}
		}
	}
	
	else
	
	{
		for (TActorIterator<ASMYPlayerController> It(GetWorld()); It; ++It)
		{
			ASMYPlayerController* SMYPlayerController = *It;
			
			if (IsValid(SMYPlayerController) == true)
			{
				SMYPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
			}
		}
	}
}

void ASMYGameModeBase::IncreaseGuessCount(ASMYPlayerController* InChattingPlayerController)
{
	ASMYPlayerState* SMYPS = InChattingPlayerController->GetPlayerState<ASMYPlayerState>();
	if (IsValid(SMYPS) == true)
	{
		SMYPS->CurrentGuessCount++;
	}
}

4) 시도 횟수 출력

더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "SMYPlayerState.generated.h"

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYPlayerState : public APlayerState
{
	GENERATED_BODY()
	
public:
	ASMYPlayerState();
	virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override;
	
	UPROPERTY(Replicated)
	FString PlayerNameString;
	
	FString GetPlayerInfoString();
	
	UPROPERTY(Replicated)
	int32 CurrentGuessCount;
	
	UPROPERTY(Replicated)
	int32 MaxGuessCount;

};
#include "Player/SMYPlayerState.h"
#include "Net/UnrealNetwork.h"

ASMYPlayerState::ASMYPlayerState()
	: PlayerNameString(TEXT("None"))
	, CurrentGuessCount(0)
	, MaxGuessCount(3)

{
	bReplicates = true;
}

void ASMYPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME(ThisClass, PlayerNameString);
	DOREPLIFETIME(ThisClass, CurrentGuessCount);
	DOREPLIFETIME(ThisClass, MaxGuessCount);
}

FString ASMYPlayerState::GetPlayerInfoString()
{
	FString PlayerInfoString = PlayerNameString + TEXT("(") + FString::FromInt(CurrentGuessCount) + TEXT("/") +
		FString::FromInt(MaxGuessCount) + TEXT(")");
	return PlayerInfoString;
}
#include "Player/SMYPlayerController.h"
#include "UI/SMYChatInput.h"
#include "Kismet/KismetSystemLibrary.h"
#include "SMYProject.h"
#include "EngineUtils.h"
#include "Kismet/GameplayStatics.h"
#include "Game/SMYGameModeBase.h"
#include "Player/SMYPlayerState.h"

void ASMYPlayerController::BeginPlay()
{
	Super::BeginPlay();
	
	FInputModeUIOnly InputModeUIOnly;
	SetInputMode(InputModeUIOnly);
	
	if (IsValid(ChatInputWidgetClass) == true)
	{
		ChatInputWidgetInstance = CreateWidget<USMYChatInput>(this, ChatInputWidgetClass);
		if (IsValid(ChatInputWidgetInstance) == true)
		{
			ChatInputWidgetInstance->AddToViewport();
		}
	}
}

void ASMYPlayerController::SetChatMessageString(const FString& InChatMessageString)
{
	ChatMessageString = InChatMessageString;
	//PrintChatMessageString(InChatMessageString);
	if (IsLocalController() == true)
	{
		// ServerRPCPrintChatMessageString(InChatMessageString);
		ASMYPlayerState* SMYPS = GetPlayerState<ASMYPlayerState>();
		if (IsValid(SMYPS) == true)
		{
			// FString CombinedMessageString = SMYPS->PlayerNameString + TEXT(": ") + InChatMessageString;
			FString CombinedMessageString = SMYPS->GetPlayerInfoString() + TEXT(": ") + InChatMessageString;
			ServerRPCPrintChatMessageString(CombinedMessageString);
		}
	}
}

void ASMYPlayerController::PrintChatMessageString(const FString& InChatMessageString)
{
	// UKismetSystemLibrary::PrintString(this, ChatMessageString, true, true, FLinearColor::Red, 5.0f);
	FString NetModeString = SMYProjectFunctionLibrary::GetNetModeString(this);
	FString CombinedMessageString = FString::Printf(TEXT("%s: %s"), *NetModeString, *InChatMessageString); 
	SMYProjectFunctionLibrary::MyPrintString(this, CombinedMessageString, 10.f);
}

void ASMYPlayerController::ClientRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
	PrintChatMessageString(InChatMessageString);
}

void ASMYPlayerController::ServerRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
	// for (TActorIterator<ASMYPlayerController> It(GetWorld()); It; ++It)
	// {
	// 	ASMYPlayerController* SMYPlayerController = *It;
	// 	if (IsValid(SMYPlayerController) == true)
	// 	{
	// 		SMYPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
	// 	}
	// }
	
	AGameModeBase* GM = UGameplayStatics::GetGameMode(this);
	if (IsValid(GM) == true)
	{
		ASMYGameModeBase* SMYGM = Cast<ASMYGameModeBase>(GM);
		if (IsValid(SMYGM) == true)
		{
			SMYGM->PrintChatMessageString(this, InChatMessageString);
		}
	}
}

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

 

1. 공지사항 위젯 생성

WBP_NotificationText 구현

 

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

플레이어에게 WBP_NotificationText를 지정한다.

더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "SMYPlayerController.generated.h"

class USMYChatInput;

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYPlayerController : public APlayerController
{
	GENERATED_BODY()
	
public:
	ASMYPlayerController();
    
	virtual void BeginPlay() override;
	
	void SetChatMessageString(const FString& InChatMessageString);
	
	void PrintChatMessageString(const FString& InChatMessageString);
	
	UFUNCTION(Client, Reliable)
	void ClientRPCPrintChatMessageString(const FString& InChatMessageString);
	
	UFUNCTION(Server, Reliable)
	void ServerRPCPrintChatMessageString(const FString& InChatMessageString);
	
	virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override;
	
protected:

	UPROPERTY(EditDefaultsOnly)
	TSubclassOf<USMYChatInput> ChatInputWidgetClass;
	
	UPROPERTY()
	TObjectPtr<USMYChatInput> ChatInputWidgetInstance;
	
	UPROPERTY(EditDefaultsOnly)
	TSubclassOf<UUserWidget> NotificationTextWidgetClass;
	
	UPROPERTY()
	TObjectPtr<UUserWidget> NotificationTextWidgetInstance;
	
	FString ChatMessageString;
	
public:
	UPROPERTY(Replicated, BlueprintReadOnly)
	FText NotificationText;
};
#include "Player/SMYPlayerController.h"
#include "UI/SMYChatInput.h"
#include "Kismet/KismetSystemLibrary.h"
#include "SMYProject.h"
#include "EngineUtils.h"
#include "Kismet/GameplayStatics.h"
#include "Game/SMYGameModeBase.h"
#include "Player/SMYPlayerState.h"
#include "Net/UnrealNetwork.h"

ASMYPlayerController::ASMYPlayerController()
{
	bReplicates = true;
}

void ASMYPlayerController::BeginPlay()
{
	Super::BeginPlay();
	
	FInputModeUIOnly InputModeUIOnly;
	SetInputMode(InputModeUIOnly);
	
	if (IsValid(ChatInputWidgetClass) == true)
	{
		ChatInputWidgetInstance = CreateWidget<USMYChatInput>(this, ChatInputWidgetClass);
		if (IsValid(ChatInputWidgetInstance) == true)
		{
			ChatInputWidgetInstance->AddToViewport();
		}
	}
	
	if (IsValid(NotificationTextWidgetClass) == true)
	{
		NotificationTextWidgetInstance = CreateWidget<UUserWidget>(this, NotificationTextWidgetClass);
		if (IsValid(NotificationTextWidgetInstance) == true)
		{
			NotificationTextWidgetInstance->AddToViewport();
		}
	}
}

void ASMYPlayerController::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME(ThisClass, NotificationText);
}

void ASMYPlayerController::SetChatMessageString(const FString& InChatMessageString)
{
	ChatMessageString = InChatMessageString;
	//PrintChatMessageString(InChatMessageString);
	if (IsLocalController() == true)
	{
		// ServerRPCPrintChatMessageString(InChatMessageString);
		ASMYPlayerState* SMYPS = GetPlayerState<ASMYPlayerState>();
		if (IsValid(SMYPS) == true)
		{
			// FString CombinedMessageString = SMYPS->PlayerNameString + TEXT(": ") + InChatMessageString;
			FString CombinedMessageString = SMYPS->GetPlayerInfoString() + TEXT(": ") + InChatMessageString;
			ServerRPCPrintChatMessageString(CombinedMessageString);
		}
	}
}

void ASMYPlayerController::PrintChatMessageString(const FString& InChatMessageString)
{
	// UKismetSystemLibrary::PrintString(this, ChatMessageString, true, true, FLinearColor::Red, 5.0f);
	FString NetModeString = SMYProjectFunctionLibrary::GetNetModeString(this);
	FString CombinedMessageString = FString::Printf(TEXT("%s: %s"), *NetModeString, *InChatMessageString); 
	SMYProjectFunctionLibrary::MyPrintString(this, CombinedMessageString, 10.f);
}

void ASMYPlayerController::ClientRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
	PrintChatMessageString(InChatMessageString);
}

void ASMYPlayerController::ServerRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
	// for (TActorIterator<ASMYPlayerController> It(GetWorld()); It; ++It)
	// {
	// 	ASMYPlayerController* SMYPlayerController = *It;
	// 	if (IsValid(SMYPlayerController) == true)
	// 	{
	// 		SMYPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
	// 	}
	// }
	
	AGameModeBase* GM = UGameplayStatics::GetGameMode(this);
	if (IsValid(GM) == true)
	{
		ASMYGameModeBase* SMYGM = Cast<ASMYGameModeBase>(GM);
		if (IsValid(SMYGM) == true)
		{
			SMYGM->PrintChatMessageString(this, InChatMessageString);
		}
	}
}
#include "Game/SMYGameModeBase.h"
#include "Game/SMYGameStateBase.h"
#include "Player/SMYPlayerController.h"
#include "EngineUtils.h"
#include "Player/SMYPlayerState.h"

void ASMYGameModeBase::OnPostLogin(AController* NewPlayer)
{
	Super::OnPostLogin(NewPlayer);
	
// 	ASMYGameStateBase* SMYGameStateBase = GetGameState<ASMYGameStateBase>();
// 	if (IsValid(SMYGameStateBase) == true)
// 	{
// 		SMYGameStateBase->MulticastRPCBroadcastLoginMessage(TEXT("XXXXXXX"));
// 	}
// 	
// 	ASMYPlayerController* SMYPlayerController = Cast<ASMYPlayerController>
// (NewPlayer);
// 	if (IsValid(SMYPlayerController) == true)
// 	{
// 		AllPlayerControllers.Add(SMYPlayerController);
// 	}
	
	ASMYPlayerController* SMYPlayerController = Cast<ASMYPlayerController>(NewPlayer);
	
	if (IsValid(SMYPlayerController) == true)
	{
		SMYPlayerController->NotificationText = FText::FromString(TEXT("Connected to the game server."));
		AllPlayerControllers.Add(SMYPlayerController);
	
		ASMYPlayerState* SMYPS = SMYPlayerController->GetPlayerState<ASMYPlayerState>();
		if (IsValid(SMYPS) == true)
		{
			SMYPS->PlayerNameString = TEXT("Player") + FString::FromInt(AllPlayerControllers.Num());
		}
		
		ASMYGameStateBase* SMYGameStateBase = GetGameState<ASMYGameStateBase>();
		if (IsValid(SMYGameStateBase) == true)
		{
			SMYGameStateBase->MulticastRPCBroadcastLoginMessage(SMYPS->PlayerNameString);
		}
	}
}

void ASMYGameModeBase::BeginPlay()
{
	Super::BeginPlay();
	SecretNumberString = GenerateSecretNumber();
}

FString ASMYGameModeBase::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 ASMYGameModeBase::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 ASMYGameModeBase::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);
}

void ASMYGameModeBase::PrintChatMessageString(ASMYPlayerController* 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<ASMYPlayerController> It(GetWorld()); It; ++It)
		{
			ASMYPlayerController* SMYPlayerController = *It;
			
			if (IsValid(SMYPlayerController) == true)
			{
				FString CombinedMessageString = InChatMessageString + TEXT(" -> ") + JudgeResultString;
				SMYPlayerController->ClientRPCPrintChatMessageString(CombinedMessageString);
			}
		}
	}
	
	else
	
	{
		for (TActorIterator<ASMYPlayerController> It(GetWorld()); It; ++It)
		{
			ASMYPlayerController* SMYPlayerController = *It;
			
			if (IsValid(SMYPlayerController) == true)
			{
				SMYPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
			}
		}
	}
}

void ASMYGameModeBase::IncreaseGuessCount(ASMYPlayerController* InChattingPlayerController)
{
	ASMYPlayerState* SMYPS = InChattingPlayerController->GetPlayerState<ASMYPlayerState>();
	if (IsValid(SMYPS) == true)
	{
		SMYPS->CurrentGuessCount++;
	}
}

 

3. 승리, 무승부, 게임 리셋

더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "SMYGameModeBase.generated.h"

class ASMYPlayerController;

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYGameModeBase : public AGameModeBase
{
	GENERATED_BODY()
	
public:
	
	virtual void BeginPlay() override;
	
	virtual void OnPostLogin(AController* NewPlayer) override;
	
	FString GenerateSecretNumber();
	
	bool IsGuessNumberString(const FString& InNumberString);
	
	FString JudgeResult(const FString& InSecretNumberString, const FString& InGuessNumberString);
	
	void PrintChatMessageString(ASMYPlayerController* InChattingPlayerController, const FString& InChatMessageString);
	
	void IncreaseGuessCount(ASMYPlayerController* InChattingPlayerController);
	
	void ResetGame();
	
	void JudgeGame(ASMYPlayerController* InChattingPlayerController, int InStrikeCount);
	
protected:
	FString SecretNumberString;
	
	TArray<TObjectPtr<ASMYPlayerController>> AllPlayerControllers;
};
#include "Game/SMYGameModeBase.h"
#include "Game/SMYGameStateBase.h"
#include "Player/SMYPlayerController.h"
#include "EngineUtils.h"
#include "Player/SMYPlayerState.h"

void ASMYGameModeBase::OnPostLogin(AController* NewPlayer)
{
	Super::OnPostLogin(NewPlayer);
	
// 	ASMYGameStateBase* SMYGameStateBase = GetGameState<ASMYGameStateBase>();
// 	if (IsValid(SMYGameStateBase) == true)
// 	{
// 		SMYGameStateBase->MulticastRPCBroadcastLoginMessage(TEXT("XXXXXXX"));
// 	}
// 	
// 	ASMYPlayerController* SMYPlayerController = Cast<ASMYPlayerController>
// (NewPlayer);
// 	if (IsValid(SMYPlayerController) == true)
// 	{
// 		AllPlayerControllers.Add(SMYPlayerController);
// 	}
	
	ASMYPlayerController* SMYPlayerController = Cast<ASMYPlayerController>(NewPlayer);
	
	if (IsValid(SMYPlayerController) == true)
	{
		SMYPlayerController->NotificationText = FText::FromString(TEXT("Connected to the game server."));
		AllPlayerControllers.Add(SMYPlayerController);
	
		ASMYPlayerState* SMYPS = SMYPlayerController->GetPlayerState<ASMYPlayerState>();
		if (IsValid(SMYPS) == true)
		{
			SMYPS->PlayerNameString = TEXT("Player") + FString::FromInt(AllPlayerControllers.Num());
		}
		
		ASMYGameStateBase* SMYGameStateBase = GetGameState<ASMYGameStateBase>();
		if (IsValid(SMYGameStateBase) == true)
		{
			SMYGameStateBase->MulticastRPCBroadcastLoginMessage(SMYPS->PlayerNameString);
		}
	}
}

void ASMYGameModeBase::BeginPlay()
{
	Super::BeginPlay();
	SecretNumberString = GenerateSecretNumber();
}

FString ASMYGameModeBase::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 ASMYGameModeBase::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 ASMYGameModeBase::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);
}

void ASMYGameModeBase::PrintChatMessageString(ASMYPlayerController* 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<ASMYPlayerController> It(GetWorld()); It; ++It)
		{
			ASMYPlayerController* SMYPlayerController = *It;
			
			if (IsValid(SMYPlayerController) == true)
			{
				FString CombinedMessageString = InChatMessageString + TEXT(" -> ") + JudgeResultString;
				SMYPlayerController->ClientRPCPrintChatMessageString(CombinedMessageString);
				
				int32 StrikeCount = FCString::Atoi(*JudgeResultString.Left(1));
				JudgeGame(InChattingPlayerController, StrikeCount);
			}
		}
	}
	
	else
	
	{
		for (TActorIterator<ASMYPlayerController> It(GetWorld()); It; ++It)
		{
			ASMYPlayerController* SMYPlayerController = *It;
			
			if (IsValid(SMYPlayerController) == true)
			{
				SMYPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
			}
		}
	}
}

void ASMYGameModeBase::IncreaseGuessCount(ASMYPlayerController* InChattingPlayerController)
{
	ASMYPlayerState* SMYPS = InChattingPlayerController->GetPlayerState<ASMYPlayerState>();
	if (IsValid(SMYPS) == true)
	{
		SMYPS->CurrentGuessCount++;
	}
}

void ASMYGameModeBase::ResetGame()
{
	SecretNumberString = GenerateSecretNumber();
	for (const auto& SMYPlayerController : AllPlayerControllers)
	{
		ASMYPlayerState* SMYPS = SMYPlayerController->GetPlayerState<ASMYPlayerState>();
		if (IsValid(SMYPS) == true)
		{
			SMYPS->CurrentGuessCount = 0;
		}
	}
}

void ASMYGameModeBase::JudgeGame(ASMYPlayerController* InChattingPlayerController, int InStrikeCount)
{
	if (3 == InStrikeCount)
	{
		ASMYPlayerState* SMYPS = InChattingPlayerController->GetPlayerState<ASMYPlayerState>();
		for (const auto& SMYPlayerController : AllPlayerControllers)
		{
			if (IsValid(SMYPS) == true)
			{
				FString CombinedMessageString = SMYPS->PlayerNameString + TEXT(" has won the game.");
				SMYPlayerController->NotificationText = FText::FromString(CombinedMessageString);
				ResetGame();
			}
		}
	}
	else
	{
		bool bIsDraw = true;
		for (const auto& SMYPlayerController : AllPlayerControllers)
		{
			ASMYPlayerState* SMYPS = SMYPlayerController->GetPlayerState<ASMYPlayerState>();
			if (IsValid(SMYPS) == true)
			{
				if (SMYPS->CurrentGuessCount < SMYPS->MaxGuessCount)
				{
					bIsDraw = false;
					break;
				}
			}
		}
		if (true == bIsDraw)
		{
			for (const auto& SMYPlayerController : AllPlayerControllers)
			{
				SMYPlayerController->NotificationText = FText::FromString(TEXT("Draw..."));
				ResetGame();
			}
		}
	}
}

 

<방어코드를 삽입한 최종 코드>

더보기
#include "SMYProject.h"
#include "Modules/ModuleManager.h"

IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, SMYProject, "SMYProject" );

#include "Game/SMYGameModeBase.h"
#include "Game/SMYGameStateBase.h"
#include "Player/SMYPlayerController.h"
#include "EngineUtils.h"
#include "Player/SMYPlayerState.h"

void ASMYGameModeBase::OnPostLogin(AController* NewPlayer)
{
	Super::OnPostLogin(NewPlayer);
	
// 	ASMYGameStateBase* SMYGameStateBase = GetGameState<ASMYGameStateBase>();
// 	if (IsValid(SMYGameStateBase) == true)
// 	{
// 		SMYGameStateBase->MulticastRPCBroadcastLoginMessage(TEXT("XXXXXXX"));
// 	}
// 	
// 	ASMYPlayerController* SMYPlayerController = Cast<ASMYPlayerController>
// (NewPlayer);
// 	if (IsValid(SMYPlayerController) == true)
// 	{
// 		AllPlayerControllers.Add(SMYPlayerController);
// 	}
	
	ASMYPlayerController* SMYPlayerController = Cast<ASMYPlayerController>(NewPlayer);
	
	if (IsValid(SMYPlayerController) == true)
	{
		SMYPlayerController->NotificationText = FText::FromString(TEXT("Connected to the game server."));
		AllPlayerControllers.Add(SMYPlayerController);
	
		ASMYPlayerState* SMYPS = SMYPlayerController->GetPlayerState<ASMYPlayerState>();
		if (IsValid(SMYPS) == true)
		{
			SMYPS->PlayerNameString = TEXT("Player") + FString::FromInt(AllPlayerControllers.Num());
		}
		
		ASMYGameStateBase* SMYGameStateBase = GetGameState<ASMYGameStateBase>();
		if (IsValid(SMYGameStateBase) == true)
		{
			SMYGameStateBase->MulticastRPCBroadcastLoginMessage(SMYPS->PlayerNameString);
		}
	}
}

void ASMYGameModeBase::BeginPlay()
{
	Super::BeginPlay();
	SecretNumberString = GenerateSecretNumber();
	UE_LOG(LogTemp, Error, TEXT("%s"), *SecretNumberString);
}

FString ASMYGameModeBase::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 ASMYGameModeBase::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 ASMYGameModeBase::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);
}

void ASMYGameModeBase::PrintChatMessageString(ASMYPlayerController* 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<ASMYPlayerController> It(GetWorld()); It; ++It)
		{
			ASMYPlayerController* SMYPlayerController = *It;
			
			if (IsValid(SMYPlayerController) == true)
			{
				FString CombinedMessageString = InChatMessageString + TEXT(" -> ") + JudgeResultString;
				SMYPlayerController->ClientRPCPrintChatMessageString(CombinedMessageString);
				
				int32 StrikeCount = FCString::Atoi(*JudgeResultString.Left(1));
				JudgeGame(InChattingPlayerController, StrikeCount);
			}
		}
	}
	
	else
	
	{
		for (TActorIterator<ASMYPlayerController> It(GetWorld()); It; ++It)
		{
			ASMYPlayerController* SMYPlayerController = *It;
			
			if (IsValid(SMYPlayerController) == true)
			{
				SMYPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
			}
		}
	}
}

void ASMYGameModeBase::IncreaseGuessCount(ASMYPlayerController* InChattingPlayerController)
{
	ASMYPlayerState* SMYPS = InChattingPlayerController->GetPlayerState<ASMYPlayerState>();
	if (IsValid(SMYPS) == true)
	{
		SMYPS->CurrentGuessCount++;
	}
}

void ASMYGameModeBase::ResetGame()
{
	SecretNumberString = GenerateSecretNumber();
	for (const auto& SMYPlayerController : AllPlayerControllers)
	{
		ASMYPlayerState* SMYPS = SMYPlayerController->GetPlayerState<ASMYPlayerState>();
		if (IsValid(SMYPS) == true)
		{
			SMYPS->CurrentGuessCount = 0;
		}
	}
}

void ASMYGameModeBase::JudgeGame(ASMYPlayerController* InChattingPlayerController, int InStrikeCount)
{
	if (3 == InStrikeCount)
	{
		ASMYPlayerState* SMYPS = InChattingPlayerController->GetPlayerState<ASMYPlayerState>();
		for (const auto& SMYPlayerController : AllPlayerControllers)
		{
			if (IsValid(SMYPS) == true)
			{
				FString CombinedMessageString = SMYPS->PlayerNameString + TEXT(" has won the game.");
				SMYPlayerController->NotificationText = FText::FromString(CombinedMessageString);
				ResetGame();
			}
		}
	}
	else
	{
		bool bIsDraw = true;
		for (const auto& SMYPlayerController : AllPlayerControllers)
		{
			ASMYPlayerState* SMYPS = SMYPlayerController->GetPlayerState<ASMYPlayerState>();
			if (IsValid(SMYPS) == true)
			{
				if (SMYPS->CurrentGuessCount < SMYPS->MaxGuessCount)
				{
					bIsDraw = false;
					break;
				}
			}
		}
		if (true == bIsDraw)
		{
			for (const auto& SMYPlayerController : AllPlayerControllers)
			{
				SMYPlayerController->NotificationText = FText::FromString(TEXT("Draw..."));
				ResetGame();
			}
		}
	}
}

#include "Game/SMYGameStateBase.h"
#include "Kismet/GameplayStatics.h"
#include "Player/SMYPlayerController.h"

void ASMYGameStateBase::MulticastRPCBroadcastLoginMessage_Implementation(const FString& InNameString)
{
	if (HasAuthority() == false)
	{
		APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
		if (IsValid(PC) == true)
		{
			ASMYPlayerController* SMYPC = Cast<ASMYPlayerController>(PC);
			
			if (IsValid(SMYPC) == true)
			{
				FString NotificationString = InNameString + TEXT(" has joined the game.");
				SMYPC->PrintChatMessageString(NotificationString);
			}
		}
	}
}

#include "Player/SMYPawn.h"
#include "SMYProject.h"

void ASMYPawn::BeginPlay()
{
	Super::BeginPlay();
	
	FString NetRoleString = SMYProjectFunctionLibrary::GetRoleString(this);
	FString CombinedString = FString::Printf(TEXT("SMYPawn::BeginPlay() %s [%s]"), *SMYProjectFunctionLibrary::GetNetModeString(this), *NetRoleString);
	SMYProjectFunctionLibrary::MyPrintString(this, CombinedString, 10.f);
}

void ASMYPawn::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);
	FString NetRoleString = SMYProjectFunctionLibrary::GetRoleString(this);
	FString CombinedString = FString::Printf(TEXT("SMYPawn::PossessedBy() %s [%s]"), *SMYProjectFunctionLibrary::GetNetModeString
	(this), *NetRoleString);
	SMYProjectFunctionLibrary::MyPrintString(this, CombinedString, 10.f);
	}

#include "Player/SMYPlayerController.h"
#include "UI/SMYChatInput.h"
#include "Kismet/KismetSystemLibrary.h"
#include "SMYProject.h"
#include "EngineUtils.h"
#include "Kismet/GameplayStatics.h"
#include "Game/SMYGameModeBase.h"
#include "Player/SMYPlayerState.h"
#include "Net/UnrealNetwork.h"

ASMYPlayerController::ASMYPlayerController()
{
	bReplicates = true;
}

void ASMYPlayerController::BeginPlay()
{
	Super::BeginPlay();
	
	if (IsLocalController() == false)
	{
		return;
	}
	
	FInputModeUIOnly InputModeUIOnly;
	SetInputMode(InputModeUIOnly);
        
	if (IsValid(ChatInputWidgetClass) == true)
	{
		ChatInputWidgetInstance = CreateWidget<USMYChatInput>(this, ChatInputWidgetClass);
		if (IsValid(ChatInputWidgetInstance) == true)
		{
			ChatInputWidgetInstance->AddToViewport();
		}
	}
        
	if (IsValid(NotificationTextWidgetClass) == true)
	{
		NotificationTextWidgetInstance = CreateWidget<UUserWidget>(this, NotificationTextWidgetClass);
		if (IsValid(NotificationTextWidgetInstance) == true)
		{
			NotificationTextWidgetInstance->AddToViewport();
		}
	}
}

void ASMYPlayerController::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME(ThisClass, NotificationText);
}

void ASMYPlayerController::SetChatMessageString(const FString& InChatMessageString)
{
	ChatMessageString = InChatMessageString;
	//PrintChatMessageString(InChatMessageString);
	if (IsLocalController() == true)
	{
		// ServerRPCPrintChatMessageString(InChatMessageString);
		ASMYPlayerState* SMYPS = GetPlayerState<ASMYPlayerState>();
		if (IsValid(SMYPS) == true)
		{
			// FString CombinedMessageString = SMYPS->PlayerNameString + TEXT(": ") + InChatMessageString;
			FString CombinedMessageString = SMYPS->GetPlayerInfoString() + TEXT(": ") + InChatMessageString;
			ServerRPCPrintChatMessageString(CombinedMessageString);
		}
	}
}

void ASMYPlayerController::PrintChatMessageString(const FString& InChatMessageString)
{
	// UKismetSystemLibrary::PrintString(this, ChatMessageString, true, true, FLinearColor::Red, 5.0f);
	FString NetModeString = SMYProjectFunctionLibrary::GetNetModeString(this);
	FString CombinedMessageString = FString::Printf(TEXT("%s: %s"), *NetModeString, *InChatMessageString); 
	SMYProjectFunctionLibrary::MyPrintString(this, CombinedMessageString, 10.f);
}

void ASMYPlayerController::ClientRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
	PrintChatMessageString(InChatMessageString);
}

void ASMYPlayerController::ServerRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
	// for (TActorIterator<ASMYPlayerController> It(GetWorld()); It; ++It)
	// {
	// 	ASMYPlayerController* SMYPlayerController = *It;
	// 	if (IsValid(SMYPlayerController) == true)
	// 	{
	// 		SMYPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
	// 	}
	// }
	
	AGameModeBase* GM = UGameplayStatics::GetGameMode(this);
	if (IsValid(GM) == true)
	{
		ASMYGameModeBase* SMYGM = Cast<ASMYGameModeBase>(GM);
		if (IsValid(SMYGM) == true)
		{
			SMYGM->PrintChatMessageString(this, InChatMessageString);
		}
	}
}
#include "Player/SMYPlayerState.h"
#include "Net/UnrealNetwork.h"

ASMYPlayerState::ASMYPlayerState()
	: PlayerNameString(TEXT("None"))
	, CurrentGuessCount(0)
	, MaxGuessCount(3)

{
	bReplicates = true;
}

void ASMYPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME(ThisClass, PlayerNameString);
	DOREPLIFETIME(ThisClass, CurrentGuessCount);
	DOREPLIFETIME(ThisClass, MaxGuessCount);
}

FString ASMYPlayerState::GetPlayerInfoString()
{
	FString PlayerInfoString = PlayerNameString + TEXT("(") + FString::FromInt(CurrentGuessCount) + TEXT("/") +
		FString::FromInt(MaxGuessCount) + TEXT(")");
	return PlayerInfoString;
}
// Fill out your copyright notice in the Description page of Project Settings.


#include "UI/SMYChatInput.h"
#include "Components/EditableTextBox.h"
#include "Player/SMYPlayerController.h"

void USMYChatInput::NativeConstruct()
{
	Super::NativeConstruct();
	
	if (IsValid(EditableTextBox_ChatInput) == true)
	{
		if (EditableTextBox_ChatInput->OnTextCommitted.IsAlreadyBound(this, &ThisClass::OnChatInputTextCommitted) == false)
		{
			EditableTextBox_ChatInput->OnTextCommitted.AddDynamic(this, &ThisClass::OnChatInputTextCommitted);
		}
	}
}
void USMYChatInput::NativeDestruct()
{
	Super::NativeDestruct();
	
	if (IsValid(EditableTextBox_ChatInput) == true)
	{
		if (EditableTextBox_ChatInput->OnTextCommitted.IsAlreadyBound(this, &ThisClass::OnChatInputTextCommitted) == true)
		{
			EditableTextBox_ChatInput->OnTextCommitted.RemoveDynamic(this, &ThisClass::OnChatInputTextCommitted);
		}
	}
}

void USMYChatInput::OnChatInputTextCommitted(const FText& Text, ETextCommit::Type CommitMethod)
{
	if (CommitMethod == ETextCommit::OnEnter)
	{
		APlayerController* OwningPlayerController = GetOwningPlayer();
		if (IsValid(OwningPlayerController) == true)
		{
			ASMYPlayerController* OwningSMYPlayerController = Cast<ASMYPlayerController>(OwningPlayerController);
			if (IsValid(OwningSMYPlayerController) == true)
			{
				OwningSMYPlayerController->SetChatMessageString(Text.ToString());
				EditableTextBox_ChatInput->SetText(FText());
			}
		}
	}
}
#pragma once

#include "CoreMinimal.h"

class SMYProjectFunctionLibrary
{
public:
	static void MyPrintString(const AActor* InWorldContextActor, const FString& InString, float InTimeToDisplay = 1.f, FColor InColor = FColor::Cyan)
	{
		if (IsValid(GEngine) == true && IsValid(InWorldContextActor) == true)
		{
			if (InWorldContextActor->GetNetMode() == NM_Client || InWorldContextActor->GetNetMode() == NM_ListenServer)
			{
				GEngine->AddOnScreenDebugMessage(-1, InTimeToDisplay, InColor, InString);
			}
			else
			{
				UE_LOG(LogTemp, Log, TEXT("%s"), *InString);
			}
		}
	}
	static FString GetNetModeString(const AActor* InWorldContextActor)
	{
		FString NetModeString = TEXT("None");
		if (IsValid(InWorldContextActor) == true)
		{
			ENetMode NetMode = InWorldContextActor->GetNetMode();
			if (NetMode == NM_Client)
			{
				NetModeString = TEXT("Client");
			}
			else
			{
				if (NetMode == NM_Standalone)
				{
					NetModeString = TEXT("StandAlone");
				}
				else
				{
					NetModeString = TEXT("Server");
				}
			}
		}
		return NetModeString;
	}
	static FString GetRoleString(const AActor* InActor)
	{
		FString RoleString = TEXT("None");
		if (IsValid(InActor) == true)
		{
			FString LocalRoleString = UEnum::GetValueAsString(TEXT("Engine.ENetRole"), InActor->GetLocalRole());
			FString RemoteRoleString = UEnum::GetValueAsString(TEXT("Engine.ENetRole"), InActor->GetRemoteRole());
			RoleString = FString::Printf(TEXT("%s / %s"), *LocalRoleString, *RemoteRoleString);
		}
		return RoleString;
	}
};
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "SMYGameModeBase.generated.h"

class ASMYPlayerController;

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYGameModeBase : public AGameModeBase
{
	GENERATED_BODY()
	
public:
	
	virtual void BeginPlay() override;
	
	virtual void OnPostLogin(AController* NewPlayer) override;
	
	FString GenerateSecretNumber();
	
	bool IsGuessNumberString(const FString& InNumberString);
	
	FString JudgeResult(const FString& InSecretNumberString, const FString& InGuessNumberString);
	
	void PrintChatMessageString(ASMYPlayerController* InChattingPlayerController, const FString& InChatMessageString);
	
	void IncreaseGuessCount(ASMYPlayerController* InChattingPlayerController);
	
	void ResetGame();
	
	void JudgeGame(ASMYPlayerController* InChattingPlayerController, int InStrikeCount);
	
protected:
	FString SecretNumberString;
	
	TArray<TObjectPtr<ASMYPlayerController>> AllPlayerControllers;
};
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameStateBase.h"
#include "SMYGameStateBase.generated.h"

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYGameStateBase : public AGameStateBase
{
	GENERATED_BODY()
	
public:
	UFUNCTION(NetMulticast, Reliable)
	void MulticastRPCBroadcastLoginMessage(const FString& InNameString = FString(TEXT("XXXXXXX")));
};
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "SMYPawn.generated.h"

UCLASS()
class SMYPROJECT_API ASMYPawn : public APawn
{
	GENERATED_BODY()

protected:
	virtual void BeginPlay() override;
	virtual void PossessedBy(AController* NewController) override;
	
};
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "SMYPlayerController.generated.h"

class USMYChatInput;
class UUserWidget;

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYPlayerController : public APlayerController
{
	GENERATED_BODY()
	
public:
	ASMYPlayerController();
	
	virtual void BeginPlay() override;
	
	void SetChatMessageString(const FString& InChatMessageString);
	
	void PrintChatMessageString(const FString& InChatMessageString);
	
	UFUNCTION(Client, Reliable)
	void ClientRPCPrintChatMessageString(const FString& InChatMessageString);
	
	UFUNCTION(Server, Reliable)
	void ServerRPCPrintChatMessageString(const FString& InChatMessageString);
	
	virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override;
	
protected:

	UPROPERTY(EditDefaultsOnly)
	TSubclassOf<USMYChatInput> ChatInputWidgetClass;
	
	UPROPERTY()
	TObjectPtr<USMYChatInput> ChatInputWidgetInstance;
	
	UPROPERTY(EditDefaultsOnly)
	TSubclassOf<UUserWidget> NotificationTextWidgetClass;
	
	UPROPERTY()
	TObjectPtr<UUserWidget> NotificationTextWidgetInstance;
	
	FString ChatMessageString;
	
public:
	UPROPERTY(Replicated, BlueprintReadOnly)
	FText NotificationText;
};
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "SMYPlayerState.generated.h"

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYPlayerState : public APlayerState
{
	GENERATED_BODY()
	
public:
	ASMYPlayerState();
	virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override;
	
	UPROPERTY(Replicated)
	FString PlayerNameString;
	
	FString GetPlayerInfoString();
	
	UPROPERTY(Replicated)
	int32 CurrentGuessCount;
	
	UPROPERTY(Replicated)
	int32 MaxGuessCount;

};
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Types/SlateEnums.h"
#include "SMYChatInput.generated.h"

class UEditableTextBox;

/**
 * 
 */
UCLASS()
class SMYPROJECT_API USMYChatInput : public UUserWidget
{
	GENERATED_BODY()

public:
	virtual void NativeConstruct() override;
	virtual void NativeDestruct() override;
	
protected:
	UFUNCTION()
	void OnChatInputTextCommitted(const FText& Text, ETextCommit::Type CommitMethod);

public:
	UPROPERTY(meta = (BindWidget))
	TObjectPtr<UEditableTextBox> EditableTextBox_ChatInput;
};

<추가 기능>

 

1. 턴 제어 기능

  • 실시간 타이머 위젯: 화면에 남은 시간이 줄어드는 UI를 띄웁니다.
  • 시간 동기화
    • 서버에서 남은 시간을 관리합니다.
    • 위젯에 그 값을 바인딩하여 모든 플레이어가 동일한 시간을 보게 합니다.
  • 입력 차단
    • 남은 시간이 0초가 되면 숫자 야구 플레이를 할 수 없습니다.
    • 입력창을 막거나, 숫자를 입력해도 서버에서 받아주지 않도록 구현해보세요.

2. 턴 내에 숫자 야구(채팅을 입력)하지 않을 시 기회 소진 기능

  • [ ] 활동 체크: 해당 턴에 숫자를 입력했는지 여부를 판별하는 체크 변수를 활용합니다.
  • [ ] 턴 전환 시스템 : 플레이어가 숫자를 입력하거나 시간이 종료될 경우
    • [ ] 다음 플레이어에게 플레이 권한을 넘깁니다.
    • [ ] 시도 횟수를 1회 차감합니다.

 

SMYGameStateBase (서버-클라이언트 데이터 동기화) : RemainingTime (남은 시간) 및 CurrentTurnPlayerState (현재 턴인 플레이어) 변수를 복제(Replicated)하여 모든 클라이언트에 실시간으로 공유함.

더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameStateBase.h"
#include "SMYGameStateBase.generated.h"

class ASMYPlayerState;

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYGameStateBase : public AGameStateBase
{
	GENERATED_BODY()
	
public:
	ASMYGameStateBase();
	
	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
	
	UFUNCTION(NetMulticast, Reliable)
	void MulticastRPCBroadcastLoginMessage(const FString& InNameString = FString(TEXT("XXXXXXX")));
	
	UPROPERTY(Replicated, BlueprintReadOnly, Category = "TurnSystem")
	int32 RemainingTime;

	UPROPERTY(Replicated, BlueprintReadOnly, Category = "TurnSystem")
	TObjectPtr<ASMYPlayerState> CurrentTurnPlayerState;
};
#include "Game/SMYGameStateBase.h"
#include "Kismet/GameplayStatics.h"
#include "Player/SMYPlayerController.h"
#include "Player/SMYPlayerState.h"
#include "Net/UnrealNetwork.h"

ASMYGameStateBase::ASMYGameStateBase()
{
	bReplicates = true;
	RemainingTime = 0;
	CurrentTurnPlayerState = nullptr;
}

void ASMYGameStateBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME(ThisClass, RemainingTime);
	DOREPLIFETIME(ThisClass, CurrentTurnPlayerState);
}

void ASMYGameStateBase::MulticastRPCBroadcastLoginMessage_Implementation(const FString& InNameString)
{
	if (HasAuthority() == false)
	{
		APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
		if (IsValid(PC) == true)
		{
			ASMYPlayerController* SMYPC = Cast<ASMYPlayerController>(PC);
			if (IsValid(SMYPC) == true)
			{
				FString NotificationString = InNameString + TEXT(" has joined the game.");
				SMYPC->PrintChatMessageString(NotificationString);
			}
		}
	}
}

 

SMYGameModeBase (서버 전용 턴 및 타이머 관리) : 2명 이상의 플레이어가 접속하면 턴 시스템을 시작. FTimerHandle을 사용해 1초마다 RemainingTime을 차감한다. 시간이 0초가 되거나 플레이어가 제출을 완료하면 다음 턴(StartNextTurn)으로 전환한다. 타임아웃 발생 시에는 숫자를 입력하지 않았으므로 시도 횟수(CurrentGuessCount)를 1회 차감(증가)하고 턴을 넘긴다.

 

더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "SMYGameModeBase.generated.h"

class ASMYPlayerController;

/**
 * 
 */
UCLASS()
class SMYPROJECT_API ASMYGameModeBase : public AGameModeBase
{
	GENERATED_BODY()
	
public:
	
	virtual void BeginPlay() override;
	
	virtual void OnPostLogin(AController* NewPlayer) override;
	
	FString GenerateSecretNumber();
	
	bool IsGuessNumberString(const FString& InNumberString);
	
	FString JudgeResult(const FString& InSecretNumberString, const FString& InGuessNumberString);
	
	void PrintChatMessageString(ASMYPlayerController* InChattingPlayerController, const FString& InChatMessageString);
	
	void IncreaseGuessCount(ASMYPlayerController* InChattingPlayerController);
	
	void ResetGame();
	void JudgeGame(ASMYPlayerController* InChattingPlayerController, int InStrikeCount);
	
	void StartGame();
	void StartNextTurn();
	void UpdateTurnTimer();
	void OnTurnTimeout();
	
protected:
	FString SecretNumberString;
	
	TArray<TObjectPtr<ASMYPlayerController>> AllPlayerControllers;
	
	int32 CurrentTurnIndex = -1;
	FTimerHandle TurnTimerHandle;
	const int32 MaxTurnTime = 10;
	bool bGameStarted = false;
};
#include "Game/SMYGameModeBase.h"
#include "Game/SMYGameStateBase.h"
#include "Player/SMYPlayerController.h"
#include "EngineUtils.h"
#include "Player/SMYPlayerState.h"

void ASMYGameModeBase::OnPostLogin(AController* NewPlayer)
{
	Super::OnPostLogin(NewPlayer);
	
// 	ASMYGameStateBase* SMYGameStateBase = GetGameState<ASMYGameStateBase>();
// 	if (IsValid(SMYGameStateBase) == true)
// 	{
// 		SMYGameStateBase->MulticastRPCBroadcastLoginMessage(TEXT("XXXXXXX"));
// 	}
// 	
// 	ASMYPlayerController* SMYPlayerController = Cast<ASMYPlayerController>
// (NewPlayer);
// 	if (IsValid(SMYPlayerController) == true)
// 	{
// 		AllPlayerControllers.Add(SMYPlayerController);
// 	}
	
	ASMYPlayerController* SMYPlayerController = Cast<ASMYPlayerController>(NewPlayer);
	
	if (IsValid(SMYPlayerController) == true)
	{
		SMYPlayerController->NotificationText = FText::FromString(TEXT("Connected to the game server."));
		AllPlayerControllers.Add(SMYPlayerController);
	
		ASMYPlayerState* SMYPS = SMYPlayerController->GetPlayerState<ASMYPlayerState>();
		if (IsValid(SMYPS) == true)
		{
			SMYPS->PlayerNameString = TEXT("Player") + FString::FromInt(AllPlayerControllers.Num());
		}
		
		ASMYGameStateBase* SMYGameStateBase = GetGameState<ASMYGameStateBase>();
		if (IsValid(SMYGameStateBase) == true)
		{
			SMYGameStateBase->MulticastRPCBroadcastLoginMessage(SMYPS->PlayerNameString);
		}
		
		if (AllPlayerControllers.Num() >= 2 && !bGameStarted)
		{
			StartGame();
		}
	}
}

void ASMYGameModeBase::BeginPlay()
{
	Super::BeginPlay();
}

void ASMYGameModeBase::StartGame()
{
	bGameStarted = true;
	SecretNumberString = GenerateSecretNumber();
	UE_LOG(LogTemp, Error, TEXT("Secret Number: %s"), *SecretNumberString);
	
	CurrentTurnIndex = -1;
	StartNextTurn();
}

void ASMYGameModeBase::StartNextTurn()
{
	if (AllPlayerControllers.Num() == 0) return;

	CurrentTurnIndex = (CurrentTurnIndex + 1) % AllPlayerControllers.Num();
	ASMYPlayerController* CurrentPC = AllPlayerControllers[CurrentTurnIndex];

	ASMYGameStateBase* GS = GetGameState<ASMYGameStateBase>();
	if (IsValid(GS) && IsValid(CurrentPC))
	{
		GS->CurrentTurnPlayerState = CurrentPC->GetPlayerState<ASMYPlayerState>();
		GS->RemainingTime = MaxTurnTime;

		GetWorldTimerManager().ClearTimer(TurnTimerHandle);
		GetWorldTimerManager().SetTimer(TurnTimerHandle, this, &ASMYGameModeBase::UpdateTurnTimer, 1.0f, true);
	}
}

void ASMYGameModeBase::UpdateTurnTimer()
{
	ASMYGameStateBase* GS = GetGameState<ASMYGameStateBase>();
	if (IsValid(GS))
	{
		GS->RemainingTime--;
		
		if (GS->RemainingTime <= 0)
		{
			OnTurnTimeout();
		}
	}
}

void ASMYGameModeBase::OnTurnTimeout()
{
	GetWorldTimerManager().ClearTimer(TurnTimerHandle);

	if (AllPlayerControllers.IsValidIndex(CurrentTurnIndex))
	{
		ASMYPlayerController* CurrentPC = AllPlayerControllers[CurrentTurnIndex];
		if (IsValid(CurrentPC))
		{
			IncreaseGuessCount(CurrentPC);

			FString TimeoutMsg = FString::Printf(TEXT("[Time Out] Turn skipped."));
			CurrentPC->ClientRPCPrintChatMessageString(TimeoutMsg);
			
			ASMYPlayerState* SMYPS = CurrentPC->GetPlayerState<ASMYPlayerState>();
			JudgeGame(CurrentPC, 0);
		}
	}

	if (bGameStarted)
	{
		StartNextTurn();
	}
}

FString ASMYGameModeBase::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 ASMYGameModeBase::IsGuessNumberString(const FString& InNumberString)
{
	if (InNumberString.Len() != 3) return false;

	TSet<TCHAR> UniqueDigits;
	for (TCHAR C : InNumberString)
	{
		if (!FChar::IsDigit(C) || C == '0') return false;
		UniqueDigits.Add(C);
	}

	return UniqueDigits.Num() == 3;
}

FString ASMYGameModeBase::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);
}

void ASMYGameModeBase::PrintChatMessageString(ASMYPlayerController* InChattingPlayerController, const FString& InChatMessageString)
{
	if (!AllPlayerControllers.IsValidIndex(CurrentTurnIndex) || AllPlayerControllers[CurrentTurnIndex] != InChattingPlayerController)
	{
		InChattingPlayerController->ClientRPCPrintChatMessageString(TEXT("[System] Not your turn."));
		return;
	}
	
	FString GuessNumberString = InChatMessageString.Right(3);
	
	if (IsGuessNumberString(GuessNumberString) == true)
	{
		GetWorldTimerManager().ClearTimer(TurnTimerHandle);
		
		FString JudgeResultString = JudgeResult(SecretNumberString, GuessNumberString);
		
		IncreaseGuessCount(InChattingPlayerController);
		
		FString CombinedMessageString = InChatMessageString + TEXT(" -> ") + JudgeResultString;
		
		for (auto& PC : AllPlayerControllers)
		{
			if (IsValid(PC))
			{
				PC->ClientRPCPrintChatMessageString(CombinedMessageString);
			}
		}

		int32 StrikeCount = FCString::Atoi(*JudgeResultString.Left(1));
		
		JudgeGame(InChattingPlayerController, StrikeCount);

		if (bGameStarted)
		{
			StartNextTurn();
		}
	}
	
	else
	
	{
		for (auto& PC : AllPlayerControllers)
		{
			if (IsValid(PC))
			{
				PC->ClientRPCPrintChatMessageString(InChatMessageString);
			}
		}
	}
}

void ASMYGameModeBase::IncreaseGuessCount(ASMYPlayerController* InChattingPlayerController)
{
	if (!IsValid(InChattingPlayerController)) return;
	
	ASMYPlayerState* SMYPS = InChattingPlayerController->GetPlayerState<ASMYPlayerState>();
	if (IsValid(SMYPS) == true)
	{
		SMYPS->CurrentGuessCount++;
	}
}

void ASMYGameModeBase::ResetGame()
{
	GetWorldTimerManager().ClearTimer(TurnTimerHandle);
	SecretNumberString = GenerateSecretNumber();
	for (const auto& SMYPlayerController : AllPlayerControllers)
	{
		ASMYPlayerState* SMYPS = SMYPlayerController->GetPlayerState<ASMYPlayerState>();
		if (IsValid(SMYPS) == true)
		{
			SMYPS->CurrentGuessCount = 0;
		}
	}
	CurrentTurnIndex = -1;
	StartNextTurn();
}

void ASMYGameModeBase::JudgeGame(ASMYPlayerController* InChattingPlayerController, int InStrikeCount)
{
	if (3 == InStrikeCount)
	{
		ASMYPlayerState* SMYPS = InChattingPlayerController->GetPlayerState<ASMYPlayerState>();
		FString CombinedMessageString = (IsValid(SMYPS) ? SMYPS->PlayerNameString : TEXT("Someone")) + TEXT(" has won the game.");
		
		for (const auto& SMYPlayerController : AllPlayerControllers)
		{
			if (IsValid(SMYPlayerController))
			{
				SMYPlayerController->NotificationText = FText::FromString(CombinedMessageString);
			}
		}
		ResetGame();
	}
	else
	{
		bool bIsDraw = true;
		for (const auto& SMYPlayerController : AllPlayerControllers)
		{
			ASMYPlayerState* SMYPS = SMYPlayerController->GetPlayerState<ASMYPlayerState>();
			if (IsValid(SMYPS) == true)
			{
				if (SMYPS->CurrentGuessCount < SMYPS->MaxGuessCount)
				{
					bIsDraw = false;
					break;
				}
			}
		}
		if (true == bIsDraw)
		{
			for (const auto& SMYPlayerController : AllPlayerControllers)
			{
				SMYPlayerController->NotificationText = FText::FromString(TEXT("Draw..."));
				ResetGame();
			}
		}
	}
}

 

SMYPlayerController & USMYChatInput (UI 차단 및 동기화) : 입력창에서 현재 턴인 플레이어만 텍스트 박스를 활성화하고, 자기 턴이 아니거나 시간이 0초면 입력(또는 제출)을 차단한다.

더보기
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Types/SlateEnums.h"
#include "SMYChatInput.generated.h"

class UEditableTextBox;
class UTextBlock;

/**
 * 
 */
UCLASS()
class SMYPROJECT_API USMYChatInput : public UUserWidget
{
	GENERATED_BODY()

public:
	virtual void NativeConstruct() override;
	virtual void NativeDestruct() override;
	virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
	
protected:
	UFUNCTION()
	void OnChatInputTextCommitted(const FText& Text, ETextCommit::Type CommitMethod);

public:
	UPROPERTY(meta = (BindWidget))
	TObjectPtr<UEditableTextBox> EditableTextBox_ChatInput;
	
	UPROPERTY(meta = (BindWidgetOptional))
	TObjectPtr<UTextBlock> TextBlock_Timer;
};
#include "UI/SMYChatInput.h"
#include "Components/EditableTextBox.h"
#include "Components/TextBlock.h"
#include "Player/SMYPlayerController.h"
#include "Game/SMYGameStateBase.h"
#include "Player/SMYPlayerState.h"
#include "Kismet/GameplayStatics.h"

void USMYChatInput::NativeConstruct()
{
	Super::NativeConstruct();
	
	if (IsValid(EditableTextBox_ChatInput) == true)
	{
		if (EditableTextBox_ChatInput->OnTextCommitted.IsAlreadyBound(this, &ThisClass::OnChatInputTextCommitted) == false)
		{
			EditableTextBox_ChatInput->OnTextCommitted.AddDynamic(this, &ThisClass::OnChatInputTextCommitted);
		}
	}
}
void USMYChatInput::NativeDestruct()
{
	Super::NativeDestruct();
	
	if (IsValid(EditableTextBox_ChatInput) == true)
	{
		if (EditableTextBox_ChatInput->OnTextCommitted.IsAlreadyBound(this, &ThisClass::OnChatInputTextCommitted) == true)
		{
			EditableTextBox_ChatInput->OnTextCommitted.RemoveDynamic(this, &ThisClass::OnChatInputTextCommitted);
		}
	}
}

void USMYChatInput::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
	Super::NativeTick(MyGeometry, InDeltaTime);
	
	ASMYGameStateBase* GS = Cast<ASMYGameStateBase>(UGameplayStatics::GetGameState(this));
	APlayerController* PC = GetOwningPlayer();

	if (IsValid(GS) && IsValid(PC))
	{
		ASMYPlayerState* MyPS = PC->GetPlayerState<ASMYPlayerState>();
		bool bIsMyTurn = (GS->CurrentTurnPlayerState != nullptr && GS->CurrentTurnPlayerState == MyPS);

		if (IsValid(EditableTextBox_ChatInput))
		{
			bool bEnableInput = bIsMyTurn && (GS->RemainingTime > 0);
			EditableTextBox_ChatInput->SetIsEnabled(bEnableInput);
		}

		if (IsValid(TextBlock_Timer))
		{
			FString TurnInfoStr = FString::Printf(TEXT("Turn: %s | Time: %ds"), 
				GS->CurrentTurnPlayerState ? *GS->CurrentTurnPlayerState->PlayerNameString : TEXT("Waiting..."), 
				GS->RemainingTime);
			TextBlock_Timer->SetText(FText::FromString(TurnInfoStr));
		}
	}
}

void USMYChatInput::OnChatInputTextCommitted(const FText& Text, ETextCommit::Type CommitMethod)
{
	if (CommitMethod == ETextCommit::OnEnter)
	{
		APlayerController* OwningPlayerController = GetOwningPlayer();
		if (IsValid(OwningPlayerController) == true)
		{
			ASMYPlayerController* OwningSMYPlayerController = Cast<ASMYPlayerController>(OwningPlayerController);
			if (IsValid(OwningSMYPlayerController) == true)
			{
				OwningSMYPlayerController->SetChatMessageString(Text.ToString());
				EditableTextBox_ChatInput->SetText(FText());
			}
		}
	}
}

*개인 프로젝트 및 개인 공부*

1. Server / Client 및 서버 주권 (Authority)

 

언리얼 엔진은 서버 주권 모델(Server-Authoritative Model)을 채택하고 있다.

 

서버 (Server) : 

게임 세계의 Single Source of Truth.

체력, 점수, 위치, 턴 상태 등 중요한 게임의 '진짜 데이터'는 모두 서버에만 존재한다.

데디케이티드 서버(Dedicated Server)와 리슨 서버(Listen Server) 두 종류가 있다.

 

클라이언트 (Client) :

서버가 보내주는 정보를 받아 화면에 그리는 '렌더링 및 입력 대리자'.

클라이언트 단독으로 "나 체력 100 채웠어!"라고 결정할 수 없으며, 서버에 요청(ServerRPC)하여 허락받아야 한다.

 

주요 역할(Role) 구분 :

ROLE_Authority: 진짜 주인(서버).

ROLE_AutonomousProxy: 클라이언트 본인이 직접 조종 중인 캐릭터(예: 내 플레이어).

ROLE_SimulatedProxy: 다른 사람이 조종하고 있어서 위치만 동기화받아 복사해 보여주는 캐릭터(예: 동료 플레이어).

 

2. Property Replication (속성/변수 복제)

 

"상태(State)의 지속적인 동기화"를 담당.

 

방향: Server ➔ Client (일방향만 가능)

 

특징: 서버에서 변수 값이 변경되면, 엔진이 알아서 접속한 모든 클라이언트에게 변경된 값을 전송한다.

 

A. 일반 Replication (UPROPERTY(Replicated))

변수 값이 바뀔 때마다 서버가 클라이언트에 새 값을 덮어씌운다.

 

B. RepNotify (UPROPERTY(ReplicatedUsing = OnRep_FunctionName))

값이 동기화될 때 클라이언트에서 특정 함수(OnRep_)를 자동으로 실행하게 만든다.

사용 예시: 플레이어 체력이 변했을 때, 값만 바꿀 뿐 아니라 UI 체력바 애니메이션도 함께 갱신하고 싶을 때 사용한다.

 

3. RPC (Remote Procedure Call - 원격 프로시저 호출)

 

"이벤트나 행위(Action)를 실행하라는 순간적인 명령".

RPC는 방향과 목적에 따라 3가지로 나뉩니다.

구분 호출하는 곳 실행되는 곳 주요 용도
Server RPC Client Server 클라이언트의 요청 (총 쏘기, 채팅 보내기, 아이템 사용)
Client RPC Server 특정 Client 특정 유저 개인 알림 (UI 팝업, 개인 보상 획득)
NetMulticast Server Server + 모든 Clients 전체 시각/시각적 연출 (폭발 이펙트, 사운드, 전체 공지)

 

1) Server RPC (UFUNCTION(Server, Reliable))

클라이언트가 서버에게 "나 이 행동 했어, 검증해 줘!" 하고 요청할 때 쓴다.

소유권(Ownership) 필수: 클라이언트가 소유한 액터(PlayerController, 또는 소유 중인 Pawn)에서만 호출할 수 있다.

 

2) Client RPC (UFUNCTION(Client, Reliable))

서버가 접속한 플레이어 중 특정 한 명의 클라이언트에게만 명령을 내릴 때 쓴다.

 

3) NetMulticast RPC (UFUNCTION(NetMulticast, Unreliable))

서버에서 호출하면, 서버 자신과 연결된 모든 클라이언트에서 동시에 해당 함수가 실행된다.

주의: 클라이언트가 호출하면 다른 플레이어에게 전파되지 않고 본인 컴퓨터에서만 로컬 함수처럼 작동한다.

 

4. Property Replication vs RPC 한눈에 비교

 

개발할 때 가장 많이 고민되는 것이 "변수 복제(Replication)를 쓸까, RPC를 쓸까?" 이다.

[지속적인 '상태'인가?] ─── YES ───> Property Replication (예: 체력, 턴 시간, 이름)
         │
        NO (순간적인 '이벤트'인가?)
         │
         └───────────── YES ───> RPC (예: 총격 이펙트, 스킬 발사 버튼 클릭, 피격 사운드)
비교 항목 Property Replication RPC (Remote Procedure Call)
목적 상태(State) 유지 및 동기화 행위/이벤트(Event) 전달
지속성 O (늦게 들어온 유저도 최신 상태를 전달받음) X (호출되는 그 순간에만 실행되고 휘발됨)
방향 Server ➔ Client 전용 Client ➔ Server / Server ➔ Client 자유로움
네트워크 효율 값이 변경될 때만 패킷 전송 (효율적) 호출될 때마다 패킷 전송

 

5. OnRep(RepNotify)

 

OnRep은 변수가 복제(Replicate)되어 값이 변경되었을 때, 클라이언트에서 자동으로 호출되는 콜백 함수이다.

 

*핵심 동작 원리 및 주의점

 

① 서버에서는 자동으로 실행되지 않는다. (중요)

 

서버가 변수 값을 바꾸면, 클라이언트에게 패킷이 전달되어 값이 바뀌는 순간 클라이언트에서만 OnRep_ 함수가 자동 실행된다.

서버 자신은 변수 값만 바뀔 뿐 OnRep_ 함수가 자동으로 실행되지 않는다.

 

해결책: 서버에서도 동일한 시각 효과나 UI 업데이트가 필요하다면(예: 리슨 서버 환경), 서버 코드에서 변수를 바꾼 뒤 OnRep_ 함수를 직접 수동 호출해 주어야 한다.

 

② 이전 값(Old Value)을 파라미터로 넘겨받을 수 있다.

 

OnRep 함수는 매개변수를 비워둘 수도 있지만, 복제되기 전의 '옛날 값'을 매개변수로 받을 수 있다.

활용: 체력이 차감될 때 데미지 텍스트 띄우기(OldHealth - NewHealth), 이전 턴 유저의 강조 효과 끄기 등.

 

6. 네트워크 소유권 관계 (Network Ownership)

 

언리얼 엔진에서 "이 액터의 주인(Owner)이 누구인가?"는 단순한 부모-자식 관계가 아니라, 네트워크 통신 권한을 결정하는 핵심 기준이다.

 

*소유권(Ownership)이 필요한 3가지 이유

 

① ServerRPC 호출 권한 (가장 결정적)

 

클라이언트가 서버로 ServerRPC를 보낼 때, 엔진은 "이 클라이언트가 해당 액터의 주인인가?"를 검사한다.

클라이언트가 소유하지 않은 액터(예: 다른 플레이어의 캐릭터, 필드에 떨어진 무기, GameState)에서 ServerRPC를 호출하면 서버는 그 요청을 무시(Drop)한다.

 

② ClientRPC의 전송 대상 지정

 

서버에서 ClientRPC를 호출하면, 해당 액터를 소유하고 있는 단 한 명의 클라이언트(Network Connection)에게만 메시지가 전파된다.

 

③ 조건부 복제 (Conditional Replication)

 

COND_OwnerOnly 조건 등을 사용하여 "이 변수는 액터의 주인에게만 복제하고, 다른 사람에게는 숨긴다"(예: 인벤토리 목록, 개인 탄약 수)는 설정이 가능해진다.

 

*소유권 체인 (Ownership Chain)

소유권은 최상위 UNetConnection에서부터 아래로 계층 구조를 이룬다.

[NetConnection (네트워크 연결)]
       │
       ▼
[APlayerController] (클라이언트 접속 시 자동 소유)
       │
       ▼ (Possess 호출 시)
[APawn / ACharacter] (PlayerController가 소유)
       │
       ▼ (SetOwner 호출 시)
[AWeapon / AInventoryItem] (Pawn이 소유)

 

Pawn 조종: PlayerController->Possess(Pawn)을 실행하면 Pawn의 Owner가 PlayerController로 자동 설정된다.

 

무기/아이템 장착: 무기를 플레이어 손에 붙일 때 반드시 Weapon->SetOwner(Pawn) 또는 SetOwner(PlayerController)를 명시적으로 호출해 주어야 클라이언트가 무기 클래스 내부에서 ServerRPC(예: 무기 총격/스킬 발사)를 호출할 수 있다.

 

<소유권(Ownership) 관련 주의해야 되는 패턴>

 

상황 문제점 해결 방법
GameState에서
ServerRPC 호출
GameState는 서버가 관리하며 소유자가 없음(No Owner). PlayerController나 소유 중인 Pawn을 통해 ServerRPC를 호출한 뒤, 서버가 GameState를 수정하게 함.
필드에 떨어진 무기를 줍기 전 입력 소유자가 없는 필드 아이템에서 바로 ServerRPC 호출 시 무시됨. 플레이어가 무기를 주웠을 때 서버에서 **Weapon->SetOwner(Player)**를 호출하여 소유권을 먼저 넘겨줌.
UI 위젯에서
ServerRPC 호출
UMG 위젯은 UObject 기반이라 RPC를 가질 수 없음. GetOwningPlayer()로 얻어온 PlayerController에 작성된 ServerRPC를 호출함.

 


 


*오늘의 총평*

제법 많은 양의 공부를 한 것 같고, 복습하고 정리하는 시간을 마련하기로 했다.

'TIL' 카테고리의 다른 글

07.31 TIL  (0) 2026.07.31
07.29 TIL  (0) 2026.07.29
07.28 TIL  (0) 2026.07.28
07.27 TIL  (0) 2026.07.27
07.24 TIL  (0) 2026.07.24