개인 강의/Unreal Engine 멀티플레이어 게임 개발

Unreal Engine 멀티플레이어 게임 개발 1-4

jh009 2026. 7. 29. 11:07

1-4. NetMode, NetConnection, NetDriver

NetMode

해당 게임 프로세스가 네트워크 상에서 어떤 역할을 하고 있는지를 의미.

  • 싱글(NM_StandAlone)
  • 서버(NM_Listen, NM_DedicatedServer)
  • 클라이언트(NM_Client)

데미지를 입어서 HP가 줄어드는 로직을 어느 컴퓨터에서 처리해야되나?

  • 클라이언트 컴퓨터에서 한다면 해킹에 취약해짐.

온라인 게임 핵을 만드는 과정

  • 서버와 클라이언트가 주고받는 패킷을 분석.

스킬을 계속 쓰면서, 이진법으로 이뤄진 패킷 속 공통부분을 찾음.

데미지일 것 같은 패킷 위치에 내가 원하는 숫자를 써서 서버로 패킷을 보내면 끝.

 

해킹을 방지하기 위해서 데미지 처리 로직은 서버 컴퓨터에서만 처리해야함.

데미지 값이 패킷에 담기지 않으므로 좀 더 안전함.

 

게임에 중대한 영향을 미치는 로직서버 컴퓨터에서 처리함.


NetMode의 필요성

작성한 로직이 서버에서 돌고 있는지 클라이언트에서 돌고 있는지 확인할 때 필요.

 

멀티플레이 개발 시에 가장 큰 걸림돌이 같은 코드가 여러 PC에서 동작한다는 것.

서버 PC, 내 PC, 다른 사람의 PC에서 동작.

 

예시:

PlayerCharacter에 BeginPlay > UE_LOG를 찍게 된다면 서버, Client01, Client02에 호출됨

PlayerController에 BeginPlay > UE_LOG를 찍게 된다면 서버, Client01에 호출됨


UWorld::InternalGetNetMode() 함수

// World.cpp

...

ENetMode UWorld::InternalGetNetMode() const
{
	if ( NetDriver != NULL )                                      // 넷드라이버가 설정되어 있으면
	{
		const bool bIsClientOnly = IsRunningClientOnly();
		return bIsClientOnly ? NM_Client : NetDriver->GetNetMode(); // 클라 아니면 서버 둘 중 하나.
	}

	...
}
// UNetDriver.cpp

ENetMode UNetDriver::GetNetMode() const
{
#if WITH_EDITOR
	if (World && World->WorldType == EWorldType::PIE && IsServer())
	{
		FWorldContext* WorldContext = GEngine->GetWorldContextFromWorld(World);
		if (WorldContext && WorldContext->RunAsDedicated) // 데디로 실행했다면
		{
			return NM_DedicatedServer;                      // 데디 서버 반환.
		}
	}
#endif

	return (IsServer() ? (GIsClient ? NM_ListenServer : NM_DedicatedServer) : NM_Client);
		// 서버인데, 클라이언트로 참여하고 있다? 리슨서버. 서버인데 참여하고 있지 않다? 데디서버.
}
// UNetDriver.cpp

bool UNetDriver::IsServer() const
{
	// Client connections ALWAYS set the server connection object in InitConnect()
	// @todo ONLINE improve this with a bool
	return ServerConnection == NULL;
		// 클라이언트는 무조건 서버 커넥션을 가지고 있다.
}

NetConnection

  • 다른 PC와의 연결이 발생하면 그에 대응하는 UNetConnection 객체도 생성.
  • 서버에 클라이언트가 접속하면 서버에는 ClientConnection 객체가 추가.
  • 반대로 클라이언트에는 ServerConnection 객체가 생성.
  • 두 PC는 UNetConnection 객체를 통해 통신.
  • UNetDriver는 생성된 UNetConnection 객체를 소유하고 관리
  • 서버 PC에 생성된 UNetDriver는 접속한 클라이언트의 수 만큼 UNetConnection을 관리
  • 클라이언트 PC에 생성된 UNetDriver는 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) - unsorted, and ordering changes depending on actor replication */
	UPROPERTY()
	TArray<TObjectPtr<UNetConnection>> ClientConnections;
	
	...

}
// AActor.cpp

UNetConnection* AActor::GetNetConnection() const
{
	return Owner ? Owner->GetNetConnection() : nullptr;
		// 여기서 오너는 액터/폰/컨트롤러 등등이 될 수 있음.
		// 코드에서 볼 수 있듯이 Owner가 지정되어 있지 않으면 통신이 안되게끔 되어 있음.
}


class UNetConnection* APawn::GetNetConnection() const
{
	// if have a controller, it has the net connection
	if ( Controller ) // 컨트롤러가 있다면
	{
		return Controller->GetNetConnection(); // 컨트롤러의 GetNetConnection()을 호출.
	}
	return Super::GetNetConnection(); // 없다면 AActor::GetNetConnection() 함수 호출.
}


UNetConnection* APlayerController::GetNetConnection() const
{
	// A controller without a player has no "owner"
	return (Player != NULL) ? NetConnection : NULL;
		// 플레이어가 존재한다면 NetConnection 객체를 반환.
}

언리얼에서의 Ownership

  • 하나의 ClientConnection은 하나의 PlayerController를 소유.
  • 즉, PlayerController의 Owning Connection은 ClientConnection인 셈.
  • PlayerController가 빙의하는 폰의 Owner 속성해당 PlayerController로 설정.
  • 폰에 무기 액터가 생성되고, 무기 액터의 Owner 속성에 해당 폰을 설정할 수도 있음.
  • ClientConnection에서부터 무기 액터에 이르는 소유 관계를 패밀리라고도 부름.
  • 소유 관계 속에 있는 액터가 본인의 Owning Connection을 얻으려면 AActor::GetNetConnection() 함수를 호출하면 됨.


NetDriver

  • 언리얼 네트워크 통신에서 로우레벨 동작들을 관리하는 클래스.
  • 싱글플레이에서는 UNetDriver 객체가 생성 X.
  • 멀티플레이에서만 UWorld::Listen() 함수를 통해 UNetDriver 객체가 생성.
  • 멀티플레이에 참여하는 각 PC마다 UNetDriver 객체가 생성.
// 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);

		...
	}

	...
}

멀티플레이 디버깅용 로그 매크로 작성

// ChatX.h

#pragma once

#include "CoreMinimal.h"

class ChatXFunctionLibrary
{
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;
	}
	
};



// ChatX.cpp

#include "ChatX.h"
#include "Modules/ModuleManager.h"

IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, ChatX, "ChatX" );
// CXPlayerController.cpp


...
#include "ChatX.h"

...

void ACXPlayerController::PrintChatMessageString(const FString& InChatMessageString)
{
	//UKismetSystemLibrary::PrintString(this, ChatMessageString, true, true, FLinearColor::Red, 5.0f);

	FString NetModeString = ChatXFunctionLibrary::GetNetModeString(this);
	FString CombinedMessageString = FString::Printf(TEXT("%s: %s"), *NetModeString, *InChatMessageString);
	ChatXFunctionLibrary::MyPrintString(this, CombinedMessageString, 10.f);
		// 문제 상황이 생기면, 위와 같은 로깅 함수로 다양한 변수의 값들과 함수이름을 확인 후
		// 문제 원인 찾기
}

디버깅 로그 제거

// CXPlayerController.cpp


...

void ACXPlayerController::PrintChatMessageString(const FString& InChatMessageString)
{
	//FString NetModeString = ChatXFunctionLibrary::GetNetModeString(this);
	//FString CombinedMessageString = FString::Printf(TEXT("%s: %s"), *NetModeString, *InChatMessageString);
	//ChatXFunctionLibrary::MyPrintString(this, CombinedMessageString, 10.f);

	ChatXFunctionLibrary::MyPrintString(this, InChatMessageString, 10.f);
}

NetMode에 대한 추가적인 설명

NetMode는 월드의 속성

 

StandAlone 

  • 게임이 원격 클라이언트의 연결을 허용하지 않는 서버로 실행 중. (커넥션이 없음)
  • 게임에 참여하는 모든 플레이어는 로컬 플레이어.
  • 싱글 플레이 및 로컬 플레이 게임에 사용.
  • 로컬 플레이어에 맞게 서버 측 로직과 클라이언트측 로직을 모두 실행.

Client

  • 게임이 네트워크 멀티플레이 세션에서 서버에 연결된 클라이언트로 실행.
  • 서버 측 로직을 실행 X.
  • 서버로부터 복제된 Proxy를 보여주는 역할.

Listen Server

  • 게임이 네트워크 멀티플레이어 세션을 호스팅 하는 서버로 실행 중.
  • 원격 클라이언트의 연결을 수락하고 로컬 플레이어를 서버에 직접 배치. (서버 자기 자신도 게임에 직접 참여)
  • 이 모드는 캐주얼 협동 및 경쟁 멀티플레이어에 자주 사용.

Dedicated Server

  • 게임이 네트워크 멀티플레이 세션을 호스팅 하는 서버로 실행.
  • 원격 클라이언트의 연결을 허용하지만 로컬 플레이어가 없음.
    따라서 그래픽, 사운드, 입력 및 기타 플레이어 중심 기능이 필요 없으니 삭제됨.
  • 이 모드는 지속적이고 안전한 대규모 멀티플레이어가 필요한 게임에 주로 사용.

NetMode에 따른 액터 위치

 

서버에만 존재하는 액터

  • 게임 모드

서버와 모든 클라이언트에 존재하는 액터

  • 배경 액터와 폰

서버와 클라이언트에만 존재하는 액터

  • 플레이어 컨트롤러

클라이언트에만 존재하는 언리얼 오브젝트

  • UI