채팅 메시지가 서버를 거쳐야 하는 이유

데디케이티드 서버 구조는 서버-클라이언트 구조이기 때문에 클라이언트와 클라이언트 간의 직접 통신은 불가능하다.

오로지 서버와 클라이언트 간의 통신만 가능하다.

그러므로 내가 작성한 채팅 메시지는 일단 서버로 전송되어야 하고, 서버는 그 메시지를 다시 모든 클라이언트에게 전송해주는 방식으로 구현된다.

  1. 클라이언트에서 메시지 입력 → Server RPC로 서버에 전송
  2. 서버가 메시지를 받아서, 월드에 있는 모든 PlayerController를 찾아 → 각각에게 Client RPC로 재전송

 

RPC 종류와 역할 (받는 사람 기준 이름이라고 생각하면 됨)

  • Server RPC: 클라이언트 → 서버. 한 명의 클라이언트가 서버에게 요청을 보낸다.
  • Client RPC: 서버 → 특정 클라이언트 한 명. 이 RPC를 호출한 액터의 소유자(Owner) 클라이언트에게만 전달된다.
  • NetMulticast RPC: 서버 → 이 액터를 인지하고 있는 모든 클라이언트.

UFUNCTION으로 RPC를 선언할 때는 Reliable 또는 Unreliable 중 하나를 반드시 작성해야 한다.

 

// CXPlayerController.h
class CHATX_API ACXPlayerController : public APlayerController
{
public:
	UFUNCTION(Client, Reliable)
	void ClientRPCPrintChatMessageString(const FString& InChatMessageString);

	UFUNCTION(Server, Reliable)
	void ServerRPCPrintChatMessageString(const FString& InChatMessageString);
};

// CXPlayerController.cpp
#include "EngineUtils.h"

void ACXPlayerController::SetChatMessageString(const FString& InChatMessageString)
{
	if (IsLocalController() == true)
	{
		ServerRPCPrintChatMessageString(InChatMessageString);		
	}
}

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

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

 

 

IsLocalController() 체크가 필요한 이유

리슨 서버는 호스트 컴퓨터가 서버 역할과 클라이언트 역할을 동시에 수행하므로

호스트 컴퓨터 안에는 ① 호스트 자신의 로컬 PlayerController와 ② 다른 접속자들의 PlayerController 복제 사본이 함께 존재한다.

IsLocalController()는 "지금 실행되는 이 컨트롤러가 실제로 이 컴퓨터에서 직접 조종되는 로컬 컨트롤러인가"를 확인해서 다른 사람의 컨트롤러 사본에서 의도치 않게 같은 로직이 실행되어 중복으로 서버에 요청이 가는 상황을 방어한다

 

왜 NetMulticast RPC 대신 Client RPC를 써야 하는가

RPC는 액터 단위로 동작하며 핵심은 이 RPC가 어떤 액터에 붙어있는가이다.

PlayerController는 특별한 액터로, 내 PlayerController는 내 컴퓨터와 서버 컴퓨터에만 존재한다.

친구의 컴퓨터에는 내 PlayerController가 존재하지 않는다

 

NetMulticast RPC는 이 액터를 인지하는 모든 클라이언트에게 전달하는 방식인데 내 PlayerController를 인지하는 클라이언트는 나 자신뿐이다

따라서 PlayerController의 멤버 함수를 NetMulticast로 만들면 메시지는 내 컴퓨터와 서버 컴퓨터에만 보이고 친구 컴퓨터에는 전달되지 않는다

반면 Client RPC는 "이 액터의 소유자에게 전달"이라는 정확한 타겟팅이 가능하므로 서버가 모든 플레이어의 PlayerController를 직접 찾아가 각각에게 Client RPC를 걸어주는 방식으로 전체 전달을 구현할 수 있다

 

멀티플레이 컨텐츠 로직을 어느 클래스에 작성할 것인가

접속 알림 기능을 예로 들면 로직이 GameModeBase와 GameStateBase 두 클래스에 나뉘어 작성된다.

  • GameModeBase: 서버에만 존재하는 액터. 게임 규칙을 정하는 역할을 하며, "누가 접속했다"와 같이 서버만 감지할 수 있는 이벤트를 처리한다.
  • GameStateBase: 모든 클라이언트에 복제되는 액터. 모두가 알아야 하는 게임 상태 정보를 표현하며, Multicast RPC로 전체 방송을 수행한다.

GameModeBase는 접속 이벤트를 감지하기만 하고, 실제 방송은 모두에게 복제되는 GameStateBase가 담당하도록 역할이 나뉜다.

// CXGameStateBase.h
UCLASS()
class CHATX_API ACXGameStateBase : public AGameStateBase
{
	GENERATED_BODY()

public:
	UFUNCTION(NetMulticast, Reliable)
	void MulticastRPCBroadcastLoginMessage(const FString& InNameString = FString(TEXT("XXXXXXX")));
};

// CXGameStateBase.cpp
void ACXGameStateBase::MulticastRPCBroadcastLoginMessage_Implementation(const FString& InNameString)
{
	if (GetNetMode() != NM_DedicatedServer)
	{
		APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
		if (IsValid(PC) == true)
		{
			ACXPlayerController* CXPC = Cast<ACXPlayerController>(PC);
			if (IsValid(CXPC) == true)
			{
				FString NotificationString = InNameString + TEXT(" has joined the game.");
				CXPC->PrintChatMessageString(NotificationString);
			}
		}
	}
}
// CXGameModeBase.h
class CHATX_API ACXGameModeBase : public AGameModeBase
{
	GENERATED_BODY()

public:
	virtual void OnPostLogin(AController* NewPlayer) override;	
};

// CXGameModeBase.cpp
void ACXGameModeBase::OnPostLogin(AController* NewPlayer)
{
	Super::OnPostLogin(NewPlayer);

	ACXGameStateBase* CXGameStateBase = GetGameState<ACXGameStateBase>();
	if (IsValid(CXGameStateBase) == true)
	{
		CXGameStateBase->MulticastRPCBroadcastLoginMessage(TEXT("XXXXXXX"));
	}
}

 

GameStateBase는 모든 클라이언트에 복제되는 액터이므로 GameStateBase의 멤버 함수를 NetMulticast로 만들면 진짜로 전체 클라이언트에게 전달된다

GetNetMode() != NM_DedicatedServer 체크가 필요한 이유

데디케이티드 서버는 화면 출력이 아예 없는 컴퓨터이며 로컬 플레이어 자체가 존재하지 않는다.

따라서 UGameplayStatics::GetPlayerController(GetWorld(), 0)로 로컬 플레이어 컨트롤러를 가져오려 해도 의미가 없다

이 체크는 데디케이티드 서버에서는 화면에 채팅을 출력하는 로직을 건너뛰도록 미리 걸러주는 역할을 한다

+ Recent posts