icon
8장 : 네트워킹과 멀티플레이어

언리얼 네트워킹 기본 개념


이전 장에서 우리는 게임 데이터를 저장하고 관리하는 다양한 방법을 살펴보았습니다. 이제 게임 개발의 꽃이라 할 수 있는 멀티플레이어(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로 설정하면, 서버에서 생성되거나 변경된 이 액터의 인스턴스가 네트워크를 통해 모든 연결된 클라이언트에 생성되고 동기화됩니다.

MyReplicatedActor.h
#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;
};
MyReplicatedActor.cpp
#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() 매크로를 사용해야 합니다.

MyReplicatedActor.h (변수 추가)
#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);
};
MyReplicatedActor.cpp (변수 복제 로직 및 함수 구현)
#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: 서버가 모든 클라이언트에게 특정 함수를 실행하도록 명령할 때 사용합니다.
MyReplicatedActor.h (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);
};
MyReplicatedActor.cpp (RPC 함수 구현)
#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와 같은 핵심 네트워킹 클래스들이 어떤 역할을 하는지 아는 것은 게임의 전반적인 상태를 효율적으로 관리하는 데 도움이 됩니다.