언리얼 네트워킹 기본 개념
이전 장에서 우리는 게임 데이터를 저장하고 관리하는 다양한 방법을 살펴보았습니다. 이제 게임 개발의 꽃이라 할 수 있는 멀티플레이어(Multiplayer) 게임 구현을 위한 핵심 요소인 네트워킹(Networking) 에 대해 다룰 시간입니다. 언리얼 엔진은 복잡한 네트워크 게임을 효율적으로 개발할 수 있도록 강력하고 유연한 네트워킹 프레임워크를 제공합니다.
이번 절에서는 언리얼 엔진 네트워킹의 가장 기본적인 개념들을 이해하고, 클라이언트-서버 모델이 어떻게 작동하는지, 그리고 복제(Replication)의 중요성에 대해 알아보겠습니다.
클라이언트-서버 모델
언리얼 엔진의 멀티플레이어 게임은 기본적으로 클라이언트-서버(Client-Server) 모델을 따릅니다. 이 모델에서는 게임의 "진실된(Authoritative)" 상태를 관리하는 하나의 서버(Server) 와, 서버로부터 게임 상태를 받아 자신의 화면에 렌더링하고 사용자 입력을 서버로 전송하는 여러 대의 클라이언트(Client) 가 존재합니다.
-
서버 (Server)
-
게임 월드의 모든 액터와 오브젝트의 "진실된" 상태를 소유하고 관리합니다. (예: 캐릭터의 정확한 위치, 아이템의 재고, 총알의 궤적 등)
-
모든 게임 로직과 규칙(물리, 충돌, 데미지 계산 등)을 처리합니다.
-
클라이언트로부터 입력을 받아 처리하고, 변경된 게임 상태를 클라이언트들에게 복제(Replicate) 하여 동기화합니다.
-
치트 방지에 중요한 역할을 합니다. 클라이언트가 조작된 데이터를 전송해도 서버가 검증하여 무시할 수 있습니다.
-
일반적으로
AGameStateBase
,APlayerState
,AGameModeBase
등이 서버에서만 존재하는 핵심적인 네트워킹 클래스입니다. -
클라이언트 (Client)
-
서버로부터 수신한 게임 상태를 자신의 로컬 화면에 렌더링하고, 플레이어에게 시각적/청각적 피드백을 제공합니다.
-
플레이어의 입력(키보드, 마우스, 컨트롤러)을 감지하고, 이를 서버로 전송합니다.
-
서버로부터의 응답을 기다리는 동안 예측(Prediction) 이나 보간(Interpolation) 을 통해 게임이 부드럽게 보이도록 합니다.
-
자신이 제어하는 액터(예: 플레이어 캐릭터)에 대해서는 소유권(Ownership) 을 가집니다.
전용 서버(Dedicated Server) vs. 리슨 서버(Listen Server)
- 전용 서버: 게임 클라이언트 없이 독립적으로 실행되는 서버 프로그램입니다. 안정성과 보안이 높아 대규모 온라인 게임에서 주로 사용됩니다.
- 리슨 서버: 한 명의 플레이어가 서버이자 동시에 클라이언트 역할을 하는 형태입니다. 친구들과의 소규모 플레이에 적합하며, 일반적으로 "호스트" 역할을 하는 플레이어의 컴퓨터에서 서버가 실행됩니다. 언리얼 엔진 에디터에서
Play
버튼을 누르고Number of Players
를 2 이상으로 설정하면 리슨 서버 모드로 실행됩니다.
복제 (Replication)의 개념
복제(Replication) 는 언리얼 엔진 네트워킹의 핵심입니다. 이는 서버에 있는 데이터나 함수 호출을 네트워크를 통해 클라이언트로 전송하여, 클라이언트의 게임 상태를 서버와 동기화하는 과정을 의미합니다. 액터의 위치, 체력, 인벤토리 아이템 등 게임의 모든 중요한 상태는 복제를 통해 클라이언트에 전달되어야 합니다.
액터 복제 (Actor Replication)
모든 AActor
클래스는 기본적으로 복제될 수 있습니다. 액터의 bReplicates
속성을 true
로 설정하면, 서버에서 생성되거나 변경된 이 액터의 인스턴스가 네트워크를 통해 모든 연결된 클라이언트에 생성되고 동기화됩니다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyReplicatedActor.generated.h"
UCLASS()
class MYPROJECT_API AMyReplicatedActor : public AActor
{
GENERATED_BODY()
public:
AMyReplicatedActor();
protected:
virtual void BeginPlay() override;
// 액터가 네트워크를 통해 복제될 수 있도록 설정
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};
#include "MyReplicatedActor.h"
#include "Net/UnrealNetwork.h" // DOREPLIFETIME 매크로를 위해 포함
AMyReplicatedActor::AMyReplicatedActor()
{
// 이 액터가 네트워크를 통해 복제될 것임을 명시
bReplicates = true;
// 액터의 움직임도 복제될 것임을 명시 (Actor Movement Component가 있어야 효과 있음)
SetReplicateMovement(true);
}
void AMyReplicatedActor::BeginPlay()
{
Super::BeginPlay();
// 서버에서만 특정 로직 실행
if (HasAuthority())
{
UE_LOG(LogTemp, Warning, TEXT("AMyReplicatedActor: This is the Server!"));
}
else
{
UE_LOG(LogTemp, Warning, TEXT("AMyReplicatedActor: This is a Client!"));
}
}
// 이 함수를 오버라이드하여 복제할 변수들을 정의합니다.
void AMyReplicatedActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// DOREPLIFETIME(클래스명, 변수명): 해당 변수가 네트워크를 통해 복제되도록 선언
// DOREPLIFETIME(AMyReplicatedActor, MyReplicatedVariable); // 아래에서 예시 변수 추가 예정
}
변수 복제 (Variable Replication)
액터 내의 특정 변수만 복제하고 싶다면, 해당 변수를 UPROPERTY()
매크로에 Replicated
또는 ReplicatedUsing
지정자를 추가하고, GetLifetimeReplicatedProps()
함수 내에서 DOREPLIFETIME()
매크로를 사용해야 합니다.
#pragma once
// ...
#include "GameFramework/Actor.h"
#include "MyReplicatedActor.generated.h"
UCLASS()
class MYPROJECT_API AMyReplicatedActor : public AActor
{
GENERATED_BODY()
public:
// ...
protected:
// 서버에서만 값을 변경하고 클라이언트로 복제할 변수
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Replication")
int32 ReplicatedInteger;
// 값이 변경될 때마다 클라이언트에서 특정 함수를 호출할 변수
UPROPERTY(ReplicatedUsing = OnRep_ReplicatedFloat, BlueprintReadOnly, Category = "Replication")
float ReplicatedFloat;
// ReplicatedUsing에 지정된 함수 (OnRep_ 접두사 권장)
UFUNCTION()
void OnRep_ReplicatedFloat();
public:
// 복제된 변수 값을 변경하는 서버 전용 함수 (블루프린트 호출 가능)
UFUNCTION(BlueprintCallable, Category = "Replication")
void Server_SetReplicatedValues(int32 NewInt, float NewFloat);
};
#include "MyReplicatedActor.h"
#include "Net/UnrealNetwork.h"
// ... 생성자 및 BeginPlay ...
void AMyReplicatedActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// Replicated 지정된 변수 복제
DOREPLIFETIME(AMyReplicatedActor, ReplicatedInteger);
// ReplicatedUsing 지정된 변수 복제 (OnRep_ 함수와 연결)
DOREPLIFETIME(AMyReplicatedActor, ReplicatedFloat);
}
void AMyReplicatedActor::OnRep_ReplicatedFloat()
{
// 이 함수는 ReplicatedFloat 변수의 값이 클라이언트로 복제될 때 클라이언트에서 호출됩니다.
// 서버에서는 호출되지 않습니다.
UE_LOG(LogTemp, Warning, TEXT("Client: ReplicatedFloat changed to: %f"), ReplicatedFloat);
// 여기에 클라이언트에서만 필요한 시각적 효과나 사운드 재생 등을 구현할 수 있습니다.
}
void AMyReplicatedActor::Server_SetReplicatedValues(int32 NewInt, float NewFloat)
{
// 이 함수는 서버에서만 실행되어야 합니다.
// HasAuthority()는 현재 코드가 서버에서 실행되고 있는지 확인하는 데 사용됩니다.
if (HasAuthority())
{
ReplicatedInteger = NewInt;
ReplicatedFloat = NewFloat; // 이 값을 변경하면 클라이언트로 복제되고 OnRep_ReplicatedFloat가 호출됩니다.
UE_LOG(LogTemp, Warning, TEXT("Server: Replicated values set to Int: %d, Float: %f"), ReplicatedInteger, ReplicatedFloat);
}
}
HasAuthority()
: 현재 코드가 서버 권한(Authority)을 가지고 있는지 확인하는 매우 중요한 함수입니다. 대부분의 게임 로직은 서버에서만 실행되어야 합니다. 클라이언트에서 HasAuthority()
는 false
를 반환합니다.
함수 복제 (Remote Procedure Call)
데이터 복제 외에도, 특정 함수 호출을 네트워크를 통해 전송하여 서버 또는 클라이언트에서 실행되도록 할 수 있습니다. 이를 RPC (Remote Procedure Call) 라고 합니다.
Server
RPC: 클라이언트가 서버에게 특정 함수를 실행해달라고 요청할 때 사용합니다. (예: 플레이어가 총을 쐈을 때 서버에게 총 발사 로직을 실행해달라고 요청)Client
RPC: 서버가 특정 클라이언트 또는 모든 클라이언트에게 특정 함수를 실행하도록 명령할 때 사용합니다. (예: 서버가 총알이 명중했음을 클라이언트에게 알려 시각 효과를 재생하도록 명령)NetMulticast
RPC: 서버가 모든 클라이언트에게 특정 함수를 실행하도록 명령할 때 사용합니다.
#pragma once
// ...
UCLASS()
class MYPROJECT_API AMyReplicatedActor : public AActor
{
GENERATED_BODY()
public:
// ...
protected:
// 서버에서만 실행될 RPC (클라이언트 -> 서버)
// Validate: Optional, 클라이언트에서 보낸 인자가 유효한지 서버에서 검증하는 함수
// Reliable: Guaranteed delivery, 신뢰할 수 있는 전송 (패킷 손실 시 재전송)
UFUNCTION(Server, Reliable, WithValidation)
void Server_DoSomething(int32 Value);
bool Server_DoSomething_Validate(int32 Value); // Validate 함수는 _Validate 접미사 필수
void Server_DoSomething_Implementation(int32 Value); // Implementation 함수는 _Implementation 접미사 필수
// 클라이언트에서 실행될 RPC (서버 -> 클라이언트)
// Client: 해당 액터를 소유한 클라이언트에서만 실행
UFUNCTION(Client, Reliable)
void Client_NotifySomethingHappened(const FString& Message);
// 모든 클라이언트에서 실행될 RPC (서버 -> 모든 클라이언트)
// NetMulticast: 모든 클라이언트 (호출한 서버 포함)에서 실행
UFUNCTION(NetMulticast, Unreliable) // Unreliable: Non-guaranteed delivery (패킷 손실 무시, 빠름)
void Multicast_PlayEffectAtLocation(FVector Location);
public:
// 클라이언트에서 호출하여 서버 RPC를 트리거하는 함수 (블루프린트 호출 가능)
UFUNCTION(BlueprintCallable, Category = "Replication")
void TriggerServerFunction(int32 MyValue);
// 서버에서 호출하여 클라이언트 RPC를 트리거하는 함수 (블루프린트 호출 가능)
UFUNCTION(BlueprintCallable, Category = "Replication")
void TriggerClientFunction(const FString& NotificationMessage, bool bTriggerMulticast, FVector EffectLoc);
};
#include "MyReplicatedActor.h"
// ...
// Server RPC 구현
bool AMyReplicatedActor::Server_DoSomething_Validate(int32 Value)
{
// 입력 값 유효성 검사 (예: Value가 허용 범위 내에 있는지 확인)
if (Value < 0 || Value > 100)
{
UE_LOG(LogTemp, Warning, TEXT("Server_DoSomething_Validate: Invalid Value %d from client!"), Value);
return false; // 유효하지 않으면 함수 실행 중단
}
return true; // 유효하면 실행 허용
}
void AMyReplicatedActor::Server_DoSomething_Implementation(int32 Value)
{
// 이 로직은 서버에서만 실행됩니다.
UE_LOG(LogTemp, Warning, TEXT("Server: Received RPC Server_DoSomething with Value: %d"), Value);
// 여기서 게임 로직(예: 데미지 계산, 아이템 사용 등)을 처리합니다.
// 서버가 다시 클라이언트에게 알림을 보낼 수도 있습니다.
Client_NotifySomethingHappened(FString::Printf(TEXT("Server processed your request with value: %d"), Value));
}
// Client RPC 구현
void AMyReplicatedActor::Client_NotifySomethingHappened_Implementation(const FString& Message)
{
// 이 로직은 해당 액터를 소유한 클라이언트에서만 실행됩니다.
// 서버에서는 이 함수가 호출되지 않습니다.
if (!HasAuthority()) // 클라이언트임을 다시 확인
{
UE_LOG(LogTemp, Warning, TEXT("Client: Received RPC Client_NotifySomethingHappened: %s"), *Message);
// 여기에 UI 업데이트, 로컬 이펙트 재생 등을 구현합니다.
}
}
// NetMulticast RPC 구현
void AMyReplicatedActor::Multicast_PlayEffectAtLocation_Implementation(FVector Location)
{
// 이 로직은 서버를 포함하여 모든 클라이언트에서 실행됩니다.
UE_LOG(LogTemp, Warning, TEXT("All: Received RPC Multicast_PlayEffectAtLocation at %s"), *Location.ToString());
// 여기에 시각/청각 효과(파티클, 사운드 등) 재생을 구현하여 모든 플레이어가 볼 수 있도록 합니다.
}
// 클라이언트에서 호출할 함수 (RPC 트리거)
void AMyReplicatedActor::TriggerServerFunction(int32 MyValue)
{
// 이 함수는 클라이언트에서 호출될 수 있습니다.
// 클라이언트에서 호출되면 Server_DoSomething_Implementation을 서버에서 실행하도록 요청합니다.
Server_DoSomething(MyValue);
}
// 서버에서 호출할 함수 (RPC 트리거)
void AMyReplicatedActor::TriggerClientFunction(const FString& NotificationMessage, bool bTriggerMulticast, FVector EffectLoc)
{
// 이 함수는 서버에서 호출되어야 합니다.
if (HasAuthority())
{
Client_NotifySomethingHappened(NotificationMessage); // 특정 클라이언트에게만 알림
if (bTriggerMulticast)
{
Multicast_PlayEffectAtLocation(EffectLoc); // 모든 클라이언트에게 효과 재생 명령
}
}
}
RPC 지정자 설명
Server
/Client
/NetMulticast
: 함수의 실행 권한을 지정합니다.Server
: 클라이언트에서 호출 가능하며 서버에서만 실행.Client
: 서버에서 호출 가능하며 액터를 소유한 클라이언트에서만 실행.NetMulticast
: 서버에서 호출 가능하며 서버를 포함한 모든 클라이언트에서 실행.Reliable
/Unreliable
: 패킷 전송의 신뢰성을 지정합니다.Reliable
: 패킷 손실 시 재전송을 보장합니다. 중요한 데이터(예: 데미지 적용, 아이템 획득)에 사용됩니다. 네트워크 오버헤드가 더 큽니다.Unreliable
: 패킷 손실을 무시하고 가능한 한 빨리 전송합니다. 실시간으로 자주 업데이트되는 데이터(예: 캐릭터 위치, 애니메이션 상태)에 사용됩니다. 네트워크 오버헤드가 적습니다.WithValidation
:Server
RPC에만 사용되며, 클라이언트가 보낸 데이터가 유효한지 서버에서 검증하는_Validate
함수를 강제합니다. 치트 방지에 필수적입니다.
소유권 (Ownership)
소유권(Ownership) 은 언리얼 엔진 네트워킹에서 매우 중요한 개념입니다. 액터는 항상 하나의 오너(Owner) 를 가집니다. 오너는 특정 APlayerController
이거나, APlayerController
가 없을 경우 액터가 놓여 있는 월드의 서버 자체가 됩니다.
- 서버: 모든 액터의 오너십을 가질 수 있습니다.
- 클라이언트: 자신이 제어하는
APawn
(캐릭터)와 그APawn
이 소유하는 액터(예: 플레이어의 총)에 대해서만 오너십을 가집니다.
왜 소유권이 중요한가요?
- RPC 실행 권한:
Client
RPC는 해당 액터를 소유한 클라이언트에서만 실행됩니다.Server
RPC는 해당 액터의 오너가 클라이언트일 경우에만 해당 클라이언트에서 서버로 호출할 수 있습니다. - 변수 복제: 특정 변수는 소유한 클라이언트에게만 복제되도록 설정할 수 있습니다 (
DOREPLIFETIME_COND
). - 네트워크 업데이트 우선순위: 오너십을 가진 액터는 네트워크 업데이트 우선순위가 더 높아질 수 있습니다.
AActor::SetOwner(AActor* NewOwner)
함수를 사용하여 액터의 소유권을 설정할 수 있습니다.
게임 상태 관리 클래스
언리얼 엔진은 멀티플레이어 게임의 다양한 상태를 관리하기 위해 특화된 클래스들을 제공합니다.
AGameModeBase
(서버 전용)- 서버에서만 존재하고 실행되는 클래스입니다.
- 게임의 규칙, 플레이어 접속/연결 해제 처리, 스폰 로직, 점수 부여 등 게임의 핵심 로직을 담당합니다.
- 클라이언트에는 존재하지 않습니다.
AGameStateBase
(서버 및 클라이언트 복제)- 게임의 전역적인 상태를 관리하고, 이 상태를 모든 클라이언트에 복제합니다.
- 예: 현재 라운드 번호, 게임 시간, 모든 플레이어의 점수, 팀 정보 등.
- 서버에서 값을 변경하면 모든 클라이언트의
AGameStateBase
인스턴스에 복제됩니다. APlayerState
(서버 및 클라이언트 복제)- 각 플레이어의 상태를 관리하고, 이 상태를 모든 클라이언트에 복제합니다.
- 예: 플레이어 이름, 현재 점수, 킬/데스 수, 팀 ID 등.
APawn
과 연결되어 있지만, 플레이어가 리스폰되더라도APlayerState
는 유지됩니다.- 서버에서 변경되면 모든 클라이언트의
APlayerState
인스턴스에 복제됩니다.
마치며
언리얼 엔진의 네트워킹은 클라이언트-서버 모델을 기반으로 하며, 복제(Replication) 를 통해 서버와 클라이언트 간의 게임 상태를 동기화합니다. 액터, 변수, 함수(RPC)의 복제 메커니즘을 이해하고, 소유권(Ownership) 개념을 명확히 파악하는 것은 안정적인 멀티플레이어 게임을 개발하는 데 필수적입니다. 또한, AGameMode
, AGameState
, APlayerState
와 같은 핵심 네트워킹 클래스들이 어떤 역할을 하는지 아는 것은 게임의 전반적인 상태를 효율적으로 관리하는 데 도움이 됩니다.