1. NetMode : 컴퓨터 한 대의 역할

NetMode는 한 컴퓨터가 네트워크 상에서 어떤 역할을 맡고 있는지 나타내는 값이다

같은 멀티플레이 세션에 참여해도, 서버 컴퓨터와 클라이언트 컴퓨터는 서로 다른 NetMode 값을 갖는다

NetMode  의미
NM_Standalone 싱글플레이. 네트워크 연결 자체가 없음
NM_Client 서버에 접속한 클라이언트
NM_ListenServer 서버이면서 동시에 로컬 플레이어도 참여 (호스트가 직접 플레이)
NM_DedicatedServer 서버 전용. 로컬 플레이어 없음 (그래픽/사운드/입력 로직 없음)

 

NetMode는 어떻게 결정되는가

// UWorld.cpp
ENetMode UWorld::InternalGetNetMode() const
{
	if ( NetDriver != NULL )                                       // 넷드라이버가 설정되어 있으면
	{
		const bool bIsClientOnly = IsRunningClientOnly();
		return bIsClientOnly ? NM_Client : NetDriver->GetNetMode(); // 클라이언트거나 서버 둘 중 하나
	}
	...
}
// UNetDriver.cpp
ENetMode UNetDriver::GetNetMode() const
{
	return (IsServer() ? (GIsClient ? NM_ListenServer : NM_DedicatedServer) : NM_Client);
		// 서버인데 클라이언트로도 참여 중? 리슨 서버.
		// 서버인데 참여하고 있지 않음? 데디케이터 서버.
}
// UNetDriver.cpp
bool UNetDriver::IsServer() const
{
	// 클라이언트는 항상 ServerConnection을 갖고 있다
	return ServerConnection == NULL;
}

 

  1. NetDriver가 없으면 → 싱글플레이(StandAlone)
  2. NetDriver가 있고 클라이언트 전용 실행이면 → NM_Client
  3. 그 외에는 IsServer()로 판단:
    • ServerConnection == NULL → 서버 (자신이 접속할 다른 서버가 없으므로)
    • 서버이면서 GIsClient(로컬 클라이언트 기능 활성화 여부)도 true → NM_ListenServer
    • 아니면 → NM_DedicatedServer

IsServer()는 "내가 접속하기 위한 ServerConnection을 들고 있는가"만 확인한다.
그 연결을 들고 있으면 클라이언트, 안 들고 있으면 서버라는 뜻이다.

 

주의할 점

  • StandAlone에서는 커넥션 객체가 없으며, HasAuthority()는 항상 true를 반환한다.
  • 클라이언트 컴퓨터에는 서버 측 로직이 실행되지 않는다.
  • 멀티플레이 클라이언트에서 GetGameMode()를 호출하면 게임모드 객체가 그 컴퓨터에 존재하지 않으므로 nullptr이 반환된다. 게임모드가 필요하면 HasAuthority()로 서버임을 먼저 확인하거나, Server RPC를 통해 가져와야 한다.

2. NetConnection : 컴퓨터 간 연결선 하나

UNetConnection은 두 컴퓨터 사이의 연결 하나를 표현하는 객체다

서버에 클라이언트가 접속하면 서버 쪽에 ClientConnection 객체가 추가되고, 클라이언트 쪽에는 ServerConnection 객체가 생성된다

// UNetDriver.h
UCLASS(...)
class UNetDriver : public UObject, public FExec
{
	/** Connection to the server (this net driver is a client) */
	UPROPERTY()
	TObjectPtr<class UNetConnection> ServerConnection;

	/** Array of connections to clients (this net driver is a host) */
	UPROPERTY()
	TArray<TObjectPtr<UNetConnection>> ClientConnections;
};
  • 서버의 UNetDriver → 접속한 클라이언트 수만큼의 UNetConnection을 배열로 관리
  • 클라이언트의 UNetDriver → ServerConnection 단 하나만 관리

 

Owner 체인을 통한 연결 조회

액터는 자기 자신이 연결 객체를 직접 들고 있지 않다. 대신 자신의 Owner에게 물어보는 재귀적 구조로 되어 있다.

// AActor.cpp
UNetConnection* AActor::GetNetConnection() const
{
	return Owner ? Owner->GetNetConnection() : nullptr;
	// Owner가 지정되어 있지 않으면 통신이 불가능하다는 뜻
}
class UNetConnection* APawn::GetNetConnection() const
{
	if ( Controller )                          // 컨트롤러가 있다면
		return Controller->GetNetConnection(); // 컨트롤러에게 위임
	return Super::GetNetConnection();          // 없으면 AActor 기본 로직(Owner 체인)
}
UNetConnection* APlayerController::GetNetConnection() const
{
	// 플레이어가 없는 컨트롤러는 "Owner" 개념 자체가 없음
	return (Player != NULL) ? NetConnection : NULL;
}

 

APlayerController가 이 체인의 종점이다

여기서만 실제 NetConnection 멤버 변수를 직접 보유하고, 나머지 액터들(Pawn, Weapon 등)은 결국 이 PlayerController까지 거슬러 올라가서 연결을 빌려 쓰는 구조다.

 

네트워크 연결은 사람(플레이어) 단위로만 존재하는 게 자연스럽다.

게임 안의 수많은 액터(총알, 아이템, 몬스터 등)가 각자 독립적인 연결을 가질 필요는 없다.

대신 이 액터가 누구의 것인지만 알면, 그 소유자의 연결을 타고 올라가 통신 대상을 찾을 수 있다.

 


3. NetDriver : 통신 저수준 로직을 담당하는 관리자

UNetDriver는 언리얼 네트워크 통신의 로우레벨 로직을 관리하는 클래스다.

  • 싱글플레이: UNetDriver 객체가 생성되지 않음
  • 멀티플레이: UWorld::Listen() 함수를 통해 생성됨. 참여하는 각 컴퓨터마다 하나씩 생성

와이파이를 사용하려면 Wi-Fi 드라이버를 설치해야 하는 것처럼, 네트워크 통신을 하려면 NetDriver가 있어야 한다.

// UWorld.cpp
bool UWorld::Listen( FURL& InURL )
{
#if WITH_SERVER_CODE
	// Create net driver
	if (GEngine->CreateNamedNetDriver(this, NAME_GameNetDriver, NAME_GameNetDriver))
	{
		NetDriver = GEngine->FindNamedNetDriver(this, NAME_GameNetDriver);
	}
#endif
}

 

UWorld::Listen()이 호출되는 시점이 멀티플레이 세션을 여는 시점이며, 이때 World->NetDriver가 NULL이 아니게 되면서 앞서 살펴본 InternalGetNetMode()의 분기가 의미를 갖게 된다

 


4. 멀티플레이에서의 Ownership : 소유권 체인 ("패밀리")

소유 관계는 다음과 같이 이어진다:

ClientConnection → PlayerController → Pawn → Weapon(소유 액터)
  • 하나의 ClientConnection은 하나의 PlayerController를 소유한다. 즉 PlayerController의 Owning Connection은 ClientConnection이다.
  • PlayerController가 빙의(Possess)한 Pawn의 Owner 속성은 해당 PlayerController로 설정된다.
  • Pawn이 생성한 무기 액터의 Owner를 그 Pawn으로 설정할 수 있다.

이 전체 소유 관계를 패밀리라고 부른다.

패밀리 안의 어떤 액터든 AActor::GetNetConnection()을 호출하면 Owner 체인을 거슬러 올라가 결국 ClientConnection을 찾아낸다

왜 Ownership이 중요한가

이 소유 관계는 단순히 누구 것이냐를 넘어, RPC와 Property Replication에 직접적인 영향을 미친다.

예를 들어 무기를 줍는(Pickup) 로직에서 Owner 설정을 빠뜨리면:

  • GetNetConnection()이 nullptr을 반환 → 이 액터는 어느 클라이언트에게 보고해야 할지 모르는 상태가 됨
  • Owning Client 대상 RPC나 일부 Replication 로직이 의도대로 동작하지 않을 수 있음

즉 Ownership은 "네트워크 권한과 정보 전달 범위를 결정하는 실질적 기준"이다

무기/아이템 Pickup 로직을 짤 때 SetOwner()를 빠뜨리지 않는 것이 중요한 이유가 여기에 있다

 


5. NetMode에 따른 액터 존재 위치

멀티플레이에서는 컴퓨터마다 같은 월드의 서로 다른 복사본을 들고 있고, 그 복사본에 들어있는 액터의 종류도 다르다.

존재 위치 예시 이유
서버에만 존재 GameMode 판정/규칙은 서버만 알아야 함 (치팅 방지)
서버 + 모든 클라이언트 배경 액터, Pawn 모두가 봐야 하는 "게임 세계"의 일부
서버 + 해당 클라이언트만 PlayerController 그 사람의 입력/조종은 그 사람과 서버만 관여
해당 클라이언트에만 UI 화면 표시는 순전히 그 컴퓨터의 로컬 책임

 

클라이언트에서 GetGameMode()를 호출하면 그 컴퓨터에 GameMode 객체 자체가 없으므로 nullptr이 반환된다는 점이 이 표의 실전 적용 예시

 


6. 디버깅용 로깅 함수 구현

멀티플레이 디버깅의 흔한 어려움: "이 로그가 서버에서 찍힌 건지, 클라이언트에서 찍힌 건지 구분이 안 된다."

이를 해결하기 위한 간단한 유틸리티 함수

// 프로그램명.h
#pragma once
#include "CoreMinimal.h"

class 프로그램명FunctionLibrary
{
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;
	}
};
// 프로그램명.cpp
#include "프로그램명.h"
#include "Modules/ModuleManager.h"

IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultGameModuleImpl, 프로그램명, "프로그램명");
// PlayerController.cpp
#include "프로그램명.h"

void APlayerController::PrintChatMessageString(const FString& InChatMessageString)
{
	FString NetModeString = 프로그램명FunctionLibrary::GetNetModeString(this);
	FString CombinedMessageString = FString::Printf(TEXT("%s: %s"), *NetModeString, *InChatMessageString);
	프로그램명FunctionLibrary::MyPrintString(this, CombinedMessageString, 10.f);
}

 

분기 로직 핵심

if (NetMode == NM_Client || NetMode == NM_ListenServer)
{
	GEngine->AddOnScreenDebugMessage(-1, InTimeToDisplay, InColor, InString); // 화면 출력
}
else
{
	UE_LOG(LogTemp, Log, TEXT("%s"), *InString); // 로그 파일에만 기록
}

 

화면 출력이 가능한 조건은 "이 컴퓨터에 로컬 플레이어가 있는가" (Client 또는 ListenServer)이다.

DedicatedServer는 로컬 플레이어가 없어 그래픽 자체가 존재하지 않으므로 화면 출력 대신 로그 파일로 기록한다.

 


NetMode 컴퓨터 한 대가 네트워크 상에서 맡은 역할 (StandAlone / Client / ListenServer / DedicatedServer)
NetConnection 컴퓨터 간 연결 하나하나를 표현하는 객체
NetDriver NetConnection들을 소유하고 관리하는 통신 저수준 관리자. 싱글플레이에서는 생성되지 않음
Ownership Owner 속성을 통해 이어지는 소유 체인("패밀리"). 액터가 NetConnection을 찾는 방식이며, RPC/Replication의 권한·전달 범위를 결정

 

  • IsServer()는 ServerConnection == NULL 여부만으로 판단한다 : 서버는 접속할 대상이 없으므로 이 값이 NULL이다.
  • 액터의 Owner가 설정되지 않으면 GetNetConnection()은 nullptr을 반환하며, 이는 해당 액터가 RPC/Replication 대상을 찾지 못하는 상태로 이어질 수 있다.

+ Recent posts