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;
}
- NetDriver가 없으면 → 싱글플레이(StandAlone)
- NetDriver가 있고 클라이언트 전용 실행이면 → NM_Client
- 그 외에는 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 대상을 찾지 못하는 상태로 이어질 수 있다.
'학습 > Unreal' 카테고리의 다른 글
| Remote Procedure Call 기초 (RPC 기초) (0) | 2026.06.19 |
|---|---|
| NetRole (Authority, Proxy, 로컬/리모트 롤) (0) | 2026.06.19 |
| Dedicated Server, 서버-클라이언트 구조의 핵심 특징 (0) | 2026.06.17 |
| TArray, TMap, TSet 사용해서 가방에 아이템 담기 (0) | 2026.06.09 |
| TObjectPtr / TSubclassOf / TArray / TSet / TMap - 언리얼 주요 타입과 컨테이너 정리 (0) | 2026.06.04 |
