[ 강의 1 - 6 NetRole ]
< 매크로를 통한 NetRole 출력 >
- 새 C++ 클래스 > Pawn 부모 클래스 > “CXPawn”
- Path: `Player`
- `BP_GameModeBase > Details > Default Pawn Class`에 `CXPawn` 지정
< CFM.h >
// 현재 할당돼있는 NetRole을 출력해서 확인하게 해주는 함수
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;
}
< CFMPawn.h >
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "CFMPawn.generated.h"
UCLASS()
class CFM_API ACFMPawn : public APawn
{
GENERATED_BODY()
protected:
// 게임이 시작됐을 때 바로 호출
virtual void BeginPlay() override;
//빙의됐을 때 호출
virtual void PossessedBy(AController* NewController) override;
};
< CFMPawn.cpp >
#include "CFMPawn.h"
#include "CFM.h"
void ACFMPawn::BeginPlay()
{
Super::BeginPlay();
// 시작되자마자 현재 적용돼있는 NetRole 출력
FString NetRoleString = CFMFunctionLibrary::GetRoleString(this);
FString CombinedString = FString::Printf(TEXT("CXPawn::BeginPlay() %s [%s]"), *CFMFunctionLibrary::GetNetModeString(this), *NetRoleString);
CFMFunctionLibrary::MyPrintString(this, CombinedString, 10.f);
}
void ACFMPawn::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
// 게임 시작 한 뒤 컨트롤러에 Pawn이 할당됐을 때 NetRole 출력
FString NetRoleString = CFMFunctionLibrary::GetRoleString(this);
FString CombinedString = FString::Printf(TEXT("CXPawn::PossessedBy() %s [%s]"), *CFMFunctionLibrary::GetNetModeString(this), *NetRoleString);
CFMFunctionLibrary::MyPrintString(this, CombinedString, 10.f);
}

< 실행 시 나오는 로그 확인 >

- 시작 시 Simulated Proxy : 피동기화 상태 ( SetOwner 가 아직 X )
- PlayerController할당 : Autonomous Proxy : 서버로부터 데이터를 수신 받아서 동기화도 되면서, 서버로 송신도 가능
< NetRole을 활용하여 정의된 중요 함수들 >
< AActor::HasAuthority() >
- 입력 관련 로직이나 UI는 Autonomous Proxy에서 수행
- AController::IsLocalController() 와 APawn::IsLocallyControlled() 함수가 제공



< 각 액터에 따른 특징 >
- GameMode 는 HasAuthority() 함수를 호출할 필요 X
- Pawn 은 Authority, Autonomous Proxy, Simulated Proxy가 혼재
- UI 관련 로직은 클라이언트에서만 수행
< 로컬 롤과 리모트 롤로 분류한 이유 >
- 만약 로컬 롤만 있었다면 분간하기가 어려웠을 케이스들이 존재
서버에 접속한 플레이어의 캐릭터 A와 서버에서 스폰 된 캐릭터 B가 있다고 가정한다면
A와 B의 로컬 롤은 *Authority*
- 로컬 롤만으로는 서버에 접속한 플레이어의 캐릭터인지, 서버에서 스폰된건지 구분이 불가능
여기서 만약 리모트 롤까지 알게된다면, 서버에 접속한 플레이어의 캐릭터는 **Autonomus Proxy**이고,
서버에서 스폰된 캐릭터는 `None`이므로 구분이 가능
- 구분이 가능해야 서버에서 호출되고 오너 클라이언트에서 실행될 RPC를 구현이 가능
멀티플레이에서 각 액터들을 좀 더 정확하게 분류하게 위해 둘로 분류
[ 강의 2-1. Remote Procedure Call 기초 ]
< RPC(Remote Procedure Call) >
- 호출하는 PC와 실행하는 PC가 달라도 되게끔 해주는 통신 기법
- 게임에 큰 영향을 끼치지 않는 일시적인 효과들에 사용하는 편
- 주로 코스메틱(사운드, 파티클 등)에 사용

< Call과 Invoke >
< Call >
- 정적인 의미
- 컴파일 타임에 어떤 함수인지, 호출하는 곳과 실행하는 곳이 결정
- 지금까지 정의했던 일반적인 전역 함수와 멤버 함수가 이에 해당
- 직접적으로 함수를 호출하고 실행 (Direct)
< Invoke >
- 동적인 의미
- 런타임에 어떤 함수인지, 호출하는 곳과 실행하는 곳이 결정
- 함수 포인터, 동적 바인딩, RPC 같은 개념들이 이에 해당
- 간접적으로 함수를 호출하고 실행 (Indirect)
RPC도 마찬가지로 "RPC를 Invoke"라고 표현
< Actor Ownership >
트워크 멀티플레이가 적용되려면, 액터는 서버에서 스폰되고 `bReplicates`가 `true`여야 가능
서버에서 스폰된 후 `SetOwner(PlayerController)` 함수를 호출해야 Client-Owned Actor가 할당
- Client-Owned Actor : PlayerController == Local PlayerController
ex) 내 플레이어 캐릭터 - Owned by different client : PlayerController !== Local PlayerController
ex) 내 화면에 보이는 친구의 플레이어 캐릭터 - Server-Owned Actor :서버에서 스폰되었지만 SetOwner() 함수 호출 X
ex) 보물상자 액터 - Unowned Actor == Server-Owned Actor
< NetMulticast, Server, Client 키워드 >
- `UFUNCTION()` 매크로와 함께 사용되는 키워드
- 키워드 : 해당 원격 PC에서 RPC를 실행시켜 달라는 요청
- 실제로 실행될지는 표를 통해 판단

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



<자주 쓰이는 케이스 >
< Server 키워드 >
- RPC가 클라이언트에서 호출되고 서버에서 실행되어야 하는 경우
- ClientConnection이 소유하고 있는 액터(Client-Owned)에서 RPC가 호출
- _Validate() 함수에서 RPC를 실행할지 말지 결정

< NetMulticast 키워드 >
- RPC가 서버와 모든 클라이언트에서 실행되어야 하는 경우
- 서버에서 호출
- 부하가 심하므로, 빈번하게 호출되게끔 코드를 작성하는 건 권장 X
ex) Tick() 함수 내에서 NetMulticast RPC 호출

< Client 키워드 >
- RPC가 서버에서 호출되고 클라이언트에서 실행되어야 하는 경우
- 서버에서 호출
< WithValidation >
- UFUNCTION() 매크로와 함께 사용되는 키워드
- 서버에서 실행되는 RPC의 경우에 해당 키워드를 작성하는 것이 권장
- 이 키워드가 붙은 RPC를 구현 할때는 _Implementation() 함수와 _Validate() 함수로 분류
- _Validate() 함수는 해당 RPC가 서버에서 실제로 실행을 결정
- 서버 실행 로직은 무조건적으로 신뢰되기 때문에, 위변조를 막기 위한 방어막 역할
< Unreliable VS Reliable >
- UFUNCTION() 매크로와 함께 사용되는 키워드
- RPC는 기본적으로 **Unreliable** 키워드를 기준으로 동작
아무것도 안쓰면 Unreliable - Unreliable은 원격 PC에서 무조건 실행되리라는 보장 X
- 반면, 원격 PC에서 무조건 실행되어야 하는 로직이라면 Reliable 키워드를 작성
- Unreliable 의 활용 예시 : 코스메틱(이펙트, 사운드 등)처럼 게임에 큰 영향이 없는 로직
- Reliable 의 활용 예시 : 충돌, 데미지, 스폰처럼 게임에 큰 영향을 끼치는 로직
[ 강의 2 - 2 : 멀티플레이 채팅 구현 ]
< 내가 작성한 채팅 메세지가 다른 PC의 화면에도 나오게 하는 방법 >
< 데디케이티드 서버 구조 >
- 클라이언트와 클라이언트 간의 통신은 불가능
- 서버와 클라이언트 간의 통신만 가능
- 내가 작성한 채팅 메시지는 일단 서버로 전송
- 서버는 전송 받은 메세지를 다시 클라이언트들에게 전송

< 멀티플레이 채팅 구현 >
< CFMPlayerController.h >
//서버에서 출력한 채팅을 클라이언트에서 받는 함수 (Client RPC) , 채팅메세지가 날아가지 않게 끔 Reliable 키워드 추가
UFUNCTION(Client, Reliable)
void ClientRPCPrintChatMessageString(const FString& InChatMessageString);
// 채팅을 서버로 보내기 위한 함수 (Server RPC), 채팅메세지가 날아가지 않게 끔 Reliable 키워드 추가
UFUNCTION(Server, Reliable)
void ServerRPCPrintChatMessageString(const FString& InChatMessageString);
< CFMPlayerController.cpp >
#include "EngineUtils.h" // TActorIterator 사용 용도
void ACFMPlayerController::SetChatMessageString(const FString& InChatMessageString)
{
ChatMessageString = InChatMessageString;
//PrintChatMessageString(ChatMessageString);
// 기존 PrintChatMessageString 주석 처리 후 Controller가 할당 돼있다면 Server에 채팅 보내는 함수로 변경
if (IsLocalController() == true)
{
ServerRPCPrintChatMessageString(InChatMessageString);
}
}
// RPC를 만들 때 Implementation 을 붙히는 게 국룰 문법
void ACFMPlayerController::ServerRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
// 서버에서 플레이어 컨트롤러를 확인
for (TActorIterator<ACFMPlayerController> It(GetWorld()); It; ++It)
{
// 확인한 플레이어 컨트롤러들이 유효하면 실행
ACFMPlayerController* CXPlayerController = *It;
if (IsValid(CXPlayerController) == true)
{
// 해당 플레이어컨트롤러에 ClientRPCPrintChatMessageString함수를 통해 해당 PC에서 채팅 출력
CXPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
}
}
}
void ACFMPlayerController::ClientRPCPrintChatMessageString_Implementation(const FString& InChatMessageString)
{
// 출력 하는 함수만 추가 (ServerRPC 함수 쪽에서 호출용)
PrintChatMessageString(InChatMessageString);
}
< Client RPC 대신에 NetMulticast RPC를 사용한다면? >
- 작동 X
- PlayerController는 내 PC와 서버 PC에만 존재하기 때문에 NetMulticast RPC로 구현하면 내 PC와 서버 PC에만 채팅이 출력
< NetMulticast RPC를 통한 서버 접속 알리기 >
- 새 C++ 클래스 > GameStateBase 부모 클래스 > “CXGameStateBase”
- Path: `Game`
- Content Browser > Content > ChatX > Blueprint > Game
새 Blueprint 클래스 > CXGameStateBase 부모 클래스 > “BP_GameStateBase” - BP_GameModeBase > Details > Game State Class에 BP_GameStateBase 지정

< CFMGameStateBase.h >
public:
// MulticastRPC 를 사용해서 누군가 접속(로그인) 했을 때 알려주는(TEXT호출) 함수
UFUNCTION(NetMulticast, Reliable)
void MulticastRPCBroadcastLoginMessage(const FString& InNameString = FString(TEXT("XXXXXXX")));
< CFMGameStateBase.cpp >
#include "Game/CFMGameStateBase.h"
#include "Kismet/GameplayStatics.h"
#include "Player/CFMPlayerController.h"
void ACFMGameStateBase::MulticastRPCBroadcastLoginMessage_Implementation(const FString& InNameString)
{
// HasAuthority() == false = 서버가 아니라면 = 클라이언트라면
if (HasAuthority() == false)
{
// 0번 컨트롤러 (LocalPlayerController)를 가져오고
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
if (IsValid(PC) == true)
{
// 로컬플레이어가 유요 하다면 NotificationString 으로 게임에 접속했다는 문구 출력
ACFMPlayerController* CFMPC = Cast<ACFMPlayerController>(PC);
if (IsValid(CFMPC) == true)
{
FString NotificationString = InNameString + TEXT(" has joined the game.");
CFMPC->PrintChatMessageString(NotificationString);
}
}
< CFMGameStateBase.h >
public:
// 다른 플레이어 (NewPlayer)가 로그인 할 때 실행되는 함수
virtual void OnPostLogin(AController* NewPlayer) override;
< CFMGameStateBase.h >
#include "Game/CFMGameModeBase.h"
#include "Game/CFMGameStateBase.h"
void ACFMGameModeBase::OnPostLogin(AController* NewPlayer)
{
// 누군가 접속 할 때 마다
Super::OnPostLogin(NewPlayer);
// 접속한 NewPlayer의 GameStateBase를 가져오고
ACFMGameStateBase* CFMGameStateBase = GetGameState<ACFMGameStateBase>();
// GameStateBase 가 유효하다면 MulticastRPC 로그인 메세지 출력 함수 호출
if (IsValid(CFMGameStateBase) == true)
{
CFMGameStateBase->MulticastRPCBroadcastLoginMessage(TEXT("XXXXXXX"));
}
}