멀티플레이 디버깅용 로그 매크로 작성
// DedicatedX.h
#pragma once
#include "CoreMinimal.h"
#pragma region NetLogging
DEDICATEDX_API DECLARE_LOG_CATEGORY_EXTERN(LogDXNet, Log, All);
#define NETMODE_TCHAR ((GetNetMode() == ENetMode::NM_Client) ? *FString::Printf(TEXT("Client%02d"), UE::GetPlayInEditorID()) : ((GetNetMode() == ENetMode::NM_Standalone) ? TEXT("StandAlone") : TEXT("Server")))
#define FUNCTION_TCHAR (ANSI_TO_TCHAR(__FUNCTION__))
#define DX_LOG_NET(LogCategory, Verbosity, Format, ...) UE_LOG(LogCategory, Verbosity, TEXT("[%s] %s %s"), NETMODE_TCHAR, FUNCTION_TCHAR, *FString::Printf(Format, ##__VA_ARGS__))
#pragma endregion
// DedicatedX.cpp
#include "DedicatedX.h"
#include "Modules/ModuleManager.h"
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, DedicatedX, "DedicatedX" );
#pragma region NetLogging
DEFINE_LOG_CATEGORY(LogDXNet);
#pragma endregion
게임모드, 게임스테이트, 플레이어 컨트롤러, 플레이어 캐릭터 생성자에 DX_LOG_NET으로 로그를 출력했다.

게임모드는 서버에서만 생성된다.(맨 위)
게임모드 - 게임스테이트를 생성한 후 Client01의 플레이어 컨트롤러와 캐릭터를 서버에서 생성한다.
서버에서 생성된 Client01의 플레이어 컨트롤러와 캐릭터가 Client01에 복제되고 게임스테이트도 복제된다.
Client를 추가하면 서버에서 플레이어 컨트롤러와 캐릭터를 생성하고 기존에 존재했던 Client에 새로운 Client의 캐릭터를 복제한다.
Client02에 서버에서 생성된 플레이어 컨트롤러, 캐릭터가 복제되고 게임스테이트도 복제된다.
마지막으로 서버에 있는 Client01의 캐릭터가 Client02로 복제가 된다.
플레이어의 접속 막기
플레이어가 서버에 접속하기 전에 GameMode::PreLogin 함수가 호출된다.
그래서 플레이어의 접속을 막으려면 PreLogin에서 막으면 된다.
void ADXGameModeBase::PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage)
{
DX_LOG_NET(LogDXNet, Log, TEXT("Begin"));
Super::PreLogin(Options, Address, UniqueId, ErrorMessage);
ErrorMessage = TEXT("The server is currently full. Please try again later.");
// 에러 메세지에 문자열을 넣어주면 로그인 중인 플레이어의 연결이 거부됨.
// 연결이 거부된 플레이어는 스탠드얼론 넷모드로 동작함.
DX_LOG_NET(LogDXNet, Log, TEXT("End"));
}

플레이어가 접속이 실패하여 넷모드가 StandAlone이 되어 각 클라이언트에도 GameMode가 생성이 된다.
서버에 클라이언트 접속 후 서버에서의 NetConnection 개체 생성을 보려면 GameMode::PostLogin 함수에서 구현
PostLogin 함수는 플레이어의 로그인 성공 후 호출되는 함수다.
// DXGameModeBase.cpp
APlayerController* ADXGameModeBase::Login(UPlayer* NewPlayer, ENetRole InRemoteRole, const FString& Portal,
const FString& Options, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage)
{
DX_LOG_NET(LogDXNet, Log, TEXT("Begin"));
APlayerController* LoginPlayerController = Super::Login(NewPlayer, InRemoteRole, Portal, Options, UniqueId, ErrorMessage);
DX_LOG_NET(LogDXNet, Log, TEXT("End"));
return LoginPlayerController;
}
void ADXGameModeBase::PostLogin(APlayerController* NewPlayer)
{
DX_LOG_NET(LogDXNet, Log, TEXT("Begin"));
Super::PostLogin(NewPlayer);
UNetDriver* ServerNetDriver = GetNetDriver();
if (IsValid(ServerNetDriver) == true)
{
if (ServerNetDriver->ClientConnections.Num() == 0)
{
DX_LOG_NET(LogDXNet, Log, TEXT("There is no client connection."));
}
else
{
for (const auto& ClientConnection : ServerNetDriver->ClientConnections)
{
if (IsValid(ClientConnection) == true)
{
DX_LOG_NET(LogDXNet, Log, TEXT("Client Connection: %s"), *ClientConnection->GetName());
}
}
}
}
else
{
DX_LOG_NET(LogDXNet, Log, TEXT("ServerNetDriver is invalid."));
}
DX_LOG_NET(LogDXNet, Log, TEXT("End"));
}
서버에 클라이언트 접속 후 클라이언트에서의 NetConnection 개체 생성을 보려면 PlayerController::PostNetInit 함수에서 구현
PostNetInit 함수는 액터의 네트워크 관련 속성들이 모두 초기화 된 후 호출되는 함수다. 따라서 NetConnection 관련 속성도 초기화 되었을 것이라고 추측이 가능하다.
// DXPlayerController.cpp
void ADXPlayerController::PostNetInit()
{
DX_LOG_NET(LogDXNet, Log, TEXT("Begin"));
Super::PostNetInit();
if (IsLocalController() == true)
{
UNetDriver* ClientNetDriver = GetNetDriver();
if (IsValid(ClientNetDriver) == true)
{
UNetConnection* ServerConnection = ClientNetDriver->ServerConnection;
if (IsValid(ServerConnection) == true)
{
DX_LOG_NET(LogDXNet, Log, TEXT("Server Connection: %s"), *ClientNetDriver->ServerConnection->GetName());
}
else
{
DX_LOG_NET(LogDXNet, Log, TEXT("There is no server connection."));
}
}
else
{
DX_LOG_NET(LogDXNet, Log, TEXT("ClientNetDriver is invalid."));
}
}
DX_LOG_NET(LogDXNet, Log, TEXT("End"));
}
void ADXPlayerController::OnActorChannelOpen(FInBunch& InBunch, UNetConnection* Connection)
{
DX_LOG_NET(LogDXNet, Log, TEXT("Begin"));
Super::OnActorChannelOpen(InBunch, Connection);
DX_LOG_NET(LogDXNet, Log, TEXT("End"));
}

언리얼 네트워크에서 사용되는 데이터 단위
넷커넥션(NetConnection)
- 모든 멀티플레이 관련 데이터들이 드나드는 통로
- 서버는 접속한 클라이언트의 갯수만큼 넷커넥션을 가진다.
- 클라이언트는 서버와의 통신을 위한 단 하나의 넷커넥션만 가진다.
채널(Channel)
- 넷커넥션 개체의 Channels라는 속성이 있다. 이를 통해 넷커넥션 개체가 채널들을 관리함을 알 수 있다.
- ActorChannel, ControlChannel, VoiceChannel들을 관리한다. ActorChannel이 Actor Replication과 관련된 채널이다.
패킷(Packet)
- 네트워크에서 사용되는 통상적인 데이터 단위
번치(Bunch)
- 언리얼에서 사용하는 특별한 패킷 단위

GameMode::StartPlay 함수가 실행되지 않는다면
//DXPlayerController.cpp
void ADXPlayerController::PostInitializeComponents()
{
DX_LOG_NET(LogDXNet, Log, TEXT("Begin"));
Super::PostInitializeComponents();
DX_LOG_NET(LogDXNet, Log, TEXT("End"));
}
void ADXPlayerController::BeginPlay()
{
DX_LOG_NET(LogDXNet, Log, TEXT("Begin"));
Super::BeginPlay();
DX_LOG_NET(LogDXNet, Log, TEXT("End"));
}
// DXGameModeBase.cpp
void ADXGameModeBase::StartPlay()
{
DX_LOG_NET(LogDXNet, Log, TEXT("Begin"));
// Super::StartPlay();
// GameMode::StartPlay() 함수가 호출 되지 않으면,
// 모든 액터의 BeginPlay() 함수도 호출 안됨. 따라서 캐릭터를 움직일 수 없어짐.
DX_LOG_NET(LogDXNet, Log, TEXT("End"));
}

서버에만 있는 게임 모드의 StartPlay가 호출되지 않았는데 모든 액터의 BeginPlay가 호출되지 않는 이유는 게임 모드에서 모든 클라이언트에 복제가 되는 게임 스테이트 액터에 명령을 내려서 월드에 있는 모든 액터들에게 BeginPlay 함수를 호출시킨다.

// DXGameStateBase.cpp
void ADXGameStateBase::HandleBeginPlay()
{
DX_LOG_NET(LogDXNet, Log, TEXT("Begin"));
Super::HandleBeginPlay();
// 서버 로직. 여기서 월드의 모든 액터들에게 BeginPlay() 함수 호출 지시.
// 이를 통해 ADXGameStateBase::OnRep_ReplicatedHasBegunPlay() 함수가 호출됨.
DX_LOG_NET(LogDXNet, Log, TEXT("End"));
}
void ADXGameStateBase::OnRep_ReplicatedHasBegunPlay()
{
DX_LOG_NET(LogDXNet, Log, TEXT("Begin"));
Super::OnRep_ReplicatedHasBegunPlay();
DX_LOG_NET(LogDXNet, Log, TEXT("End"));
}

시작 관련 주요 이벤트 함수
PostNetInit()
- 서버에서 해당 액터의 속성 등 변경된 내용 세팅이 모두 완료된 뒤 클라이언트에 복제되었을 때 호출되는 함수
- 멀티플레이에서는 BeginPlay() 함수가 호출되려면 해당 액터에 대한 서버에서의 처리가 모두 완료되어야 한다. 그래서 PostNetInit() 함수가 항상 먼저 실행된 후 StartPlay() 함수에 의해 BeginPlay() 함수가 호출된다.
PostInitializeComponents()
- 해당 액터에 부착된 컴포넌트가 모두 준비된 상태에 호출된다.
StartPlay()
- 게임의 시작을 지시하는 함수
BeginPlay()
- 게임 모드의 StartPlay() 함수를 통해 게임이 시작될 때 모든 액터에서 호출 되는 함수
Possess
// DXPlayerController.cpp
void ADXPlayerController::OnPossess(APawn* InPawn)
{
DX_LOG_NET(LogDXNet, Log, TEXT("Begin"));
Super::OnPossess(InPawn);
// 클라이언트에서는 Possess() 함수가 호출되지 않음에 주의.
// 그렇다면 클라이언트에서 Owner는 어떻게 초기화 되는 걸까.
// AActor::Owner 속성은 ReplicatedUsing 키워드가 달린 속성임.
// Onwer가 초기화 되면 OnRep_Owner() 함수가 클라이언트에서 호출됨.
DX_LOG_NET(LogDXNet, Log, TEXT("End"));
}
// DXPlayerCharacter.cpp
void ADXPlayerCharacter::PossessedBy(AController* NewController)
{
DX_LOG_NET(LogDXNet, Log, TEXT("Begin"));
AActor* OwnerActor = GetOwner();
if (IsValid(OwnerActor) == true)
{
DX_LOG_NET(LogDXNet, Log, TEXT("OwnerActor Name: %s"), *OwnerActor->GetName());
}
else
{
DX_LOG_NET(LogDXNet, Log, TEXT("There is no OwnerActor."));
}
Super::PossessedBy(NewController);
OwnerActor = GetOwner();
if (IsValid(OwnerActor) == true)
{
DX_LOG_NET(LogDXNet, Log, TEXT("OwnerActor Name: %s"), *OwnerActor->GetName());
}
else
{
DX_LOG_NET(LogDXNet, Log, TEXT("There is no OwnerActor."));
}
DX_LOG_NET(LogDXNet, Log, TEXT("End"));
}
void ADXPlayerCharacter::OnRep_Owner()
{
DX_LOG_NET(LogDXNet, Log, TEXT("Begin"));
Super::OnRep_Owner();
AActor* OwnerActor = GetOwner();
if (IsValid(OwnerActor) == true)
{
DX_LOG_NET(LogDXNet, Log, TEXT("OwnerActor Name: %s"), *OwnerActor->GetName());
}
else
{
DX_LOG_NET(LogDXNet, Log, TEXT("There is no OwnerActor."));
}
DX_LOG_NET(LogDXNet, Log, TEXT("End"));
}
void ADXPlayerCharacter::PostNetInit()
{
DX_LOG_NET(LogDXNet, Log, TEXT("Begin"));
Super::PostNetInit();
DX_LOG_NET(LogDXNet, Log, TEXT("End"));
}

매크로를 통한 로컬 롤과 리모트 롤 출력
//DedicatedX.h
#define LOCAL_ROLE_TCHAR *(UEnum::GetValueAsString(TEXT("Engine.ENetRole"), GetLocalRole()))
#define REMOTE_ROLE_TCHAR *(UEnum::GetValueAsString(TEXT("Engine.ENetRole"), GetRemoteRole()))
#define DX_LOG_ROLE(LogCat, Verbosity, Format, ...) UE_LOG(LogCat, Verbosity, TEXT("[%s][%s/%s] %s %s"), NETMODE_TCHAR, LOCAL_ROLE_TCHAR, REMOTE_ROLE_TCHAR, FUNCTION_TCHAR, *FString::Printf(Format, ##__VA_ARGS__))
빙의 전 후 캐릭터의 리모트 롤 비교
void ADXPlayerCharacter::PossessedBy(AController* NewController)
{
//DX_LOG_NET(LogDXNet, Log, TEXT("Begin"));
DX_LOG_ROLE(LogDXNet, Log, TEXT("Begin"));
...
// DX_LOG_NET(LogDXNet, Log, TEXT("End"));
DX_LOG_ROLE(LogDXNet, Log, TEXT("End"));
}

'멀티플레이 공부' 카테고리의 다른 글
| 멀티플레이 공부(Property Replication) (0) | 2025.11.21 |
|---|---|
| 멀티플레이 공부(Remote Procedure Call, 액터 소유권, WithValidation, UnReliable, Reliable) (0) | 2025.11.20 |
| 멀티플레이 공부(NetRole, Authority, Proxy, Local Role, Remote Role) (0) | 2025.11.19 |
| 멀티플레이 공부(NetMode, NetConnection, NetDriver, Ownership) (0) | 2025.11.18 |
| 멀티플레이 공부(Server, Client, Dedicated Server 흐름도, 특징) (0) | 2025.11.17 |