안동민 개발노트

본문 시작

언리얼 네트워킹 기본 개념

권한을 가진 서버가 클라이언트 입력을 검증하고 확정 상태를 Replication으로 동기화하는 구조를 이해합니다.

이전 장에서 우리는 게임 데이터를 저장하고 관리하는 다양한 방법을 살펴보았습니다.

이제 게임 개발의 꽃이라 할 수 있는 멀티플레이어(Multiplayer) 게임 구현을 위한 핵심 요소인 네트워킹(Networking)에 대해 다룰 시간입니다.

언리얼 엔진은 복잡한 네트워크 게임을 효율적으로 개발할 수 있도록 강력하고 유연한 네트워킹 프레임워크를 제공합니다.

이번 절에서는 언리얼 엔진 네트워킹의 가장 기본적인 개념들을 이해하고, 클라이언트-서버 모델이 어떻게 작동하는지, 그리고 복제(Replication)의 중요성에 대해 알아보겠습니다.

핵심 흐름: 클라이언트 입력은 Server RPC로 서버에서 검증되고, 서버가 확정한 상태는 Replication으로 클라이언트에 동기화됩니다.


클라이언트-서버 모델

언리얼 엔진의 멀티플레이어 게임은 기본적으로 클라이언트-서버(Client-Server) 모델을 따릅니다.

이 모델에서는 게임의 진실된(Authoritative) 상태를 관리하는 하나의 서버(Server)와, 서버로부터 게임 상태를 받아 자신의 화면에 렌더링하고 사용자 입력을 서버로 전송하는 여러 대의 클라이언트(Client)가 존재합니다.

  • 서버 (Server)
    • 공유 게임플레이의 권위 있는 상태를 소유하고 관리합니다. (예: 캐릭터의 정확한 위치, 아이템의 재고, 총알의 궤적 등)
    • 공유 결과를 결정하는 규칙을 처리합니다. 클라이언트도 UI·효과·예측 같은 로컬 로직을 실행합니다.
    • 클라이언트로부터 입력을 받아 처리하고, 변경된 게임 상태를 클라이언트들에게 복제(Replicate)하여 동기화합니다.
    • 치트 방지에 중요한 역할을 합니다. 클라이언트가 조작된 데이터를 전송해도 서버가 검증하여 무시할 수 있습니다.
    • AGameModeBase는 서버 전용이고, AGameStateBase와 APlayerState는 클라이언트에도 복제됩니다.
  • 클라이언트 (Client)
    • 서버로부터 수신한 게임 상태를 자신의 로컬 화면에 렌더링하고, 플레이어에게 시각적/청각적 피드백을 제공합니다.
    • 플레이어의 입력(키보드, 마우스, 컨트롤러)을 감지하고, 이를 서버로 전송합니다.
    • 서버로부터의 응답을 기다리는 동안 예측(Prediction)이나 보간(Interpolation)을 통해 게임이 부드럽게 보이도록 합니다.
    • 자신이 제어하는 액터(예: 플레이어 캐릭터)에 대해서는 소유권(Ownership)을 가집니다.
전용 서버(Dedicated Server) vs. 리슨 서버(Listen Server)
  • 전용 서버: 게임 클라이언트 없이 독립적으로 실행되는 서버 프로그램입니다. 안정성과 보안이 높아 대규모 온라인 게임에서 주로 사용됩니다.
  • 리슨 서버: 한 명의 플레이어가 서버이자 동시에 클라이언트 역할을 하는 형태입니다. 친구들과의 소규모 플레이에 적합하며, 일반적으로 호스트 역할을 하는 플레이어의 컴퓨터에서 서버가 실행됩니다. 에디터에서는 플레이 인원과 함께 Net Mode를 Play As Listen Server로 설정해야 합니다. 인원 수만으로 서버 모드가 정해지지는 않습니다.

복제 (Replication)의 개념

복제(Replication)는 언리얼 엔진 네트워킹의 핵심입니다.

이는 서버에 있는 데이터나 함수 호출을 네트워크를 통해 클라이언트로 전송하여, 클라이언트의 게임 상태를 서버와 동기화하는 과정을 의미합니다.

액터의 위치, 체력, 인벤토리 아이템 등 게임의 모든 중요한 상태는 복제를 통해 클라이언트에 전달되어야 합니다.

액터 복제 (Actor Replication)

모든 AActor 클래스는 기본적으로 복제될 수 있습니다.

서버의 액터에 bReplicates를 켜면 복제 대상으로 참여합니다. 실제 연결별 전송은 관련성(Relevancy), 휴면(Dormancy), 복제 조건과 업데이트 시점 등의 영향을 받습니다. 모든 멤버가 자동으로 전송되는 것도 아닙니다.

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;

    // 루트 컴포넌트의 이동 상태 복제 활성화 (이 클래스는 루트 생성을 생략한 부분 예제)
    SetReplicateMovement(true);
}

void AMyReplicatedActor::BeginPlay()
{
    Super::BeginPlay();

    // 이 액터 인스턴스의 Authority 역할 확인
    if (HasAuthority())
    {
        UE_LOG(LogTemp, Warning, TEXT("AMyReplicatedActor: Authority role"));
    }
    else
    {
        UE_LOG(LogTemp, Warning, TEXT("AMyReplicatedActor: Non-authority role"));
    }
}

// 이 함수를 오버라이드하여 복제할 변수들을 정의합니다.
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)
{
    // 일반 함수이며 RPC가 아닙니다. 해당 액터의 Authority 역할에서만 값을 바꿉니다.
    if (HasAuthority())
    {
        ReplicatedInteger = NewInt;
        ReplicatedFloat = NewFloat; // 복제 조건·시점에 따라 전송되며 중간 값은 합쳐질 수 있습니다.
        UE_LOG(LogTemp, Warning, TEXT("Server: Replicated values set to Int: %d, Float: %f"), ReplicatedInteger, ReplicatedFloat);
    }
}

HasAuthority()는 해당 액터의 로컬 역할이 Authority인지 검사합니다. 클라이언트가 로컬에서만 생성한 액터도 true일 수 있으므로 실행 프로세스의 서버 모드 검사와 같지 않습니다. 위 Server_SetReplicatedValues는 이름과 달리 Server 지정자가 없는 일반 함수입니다. 클라이언트 호출을 서버로 보내지 않습니다.

C++에서 서버가 값을 대입하는 것만으로 OnRep가 자동 실행되지는 않습니다. 복제 수신에서도 중간 값마다 알림이 전달되는 것은 아닙니다.

함수 복제 (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)
{
    // 실행 위치는 호출 위치와 소유 연결에 따라 정해집니다.
    UE_LOG(LogTemp, Warning, TEXT("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); // 현재 관련 있는 클라이언트에 효과 재생 요청
        }
    }
}

Reliable은 연결이 유지되는 동안 확인 응답과 재전송을 사용하는 전달 방식입니다. 게임 규칙의 유효성이나 연결 종료 후 재생을 보장하지 않습니다. Unreliable은 손실·순서 변경을 허용하므로 상태를 복원할 유일한 경로로 사용하지 않습니다.

WithValidation은 선택 사항입니다. _Validate의 false는 호출한 클라이언트 연결을 끊으므로, 정상적인 재사용 대기시간 같은 게임상 거절은 구현 함수에서 처리하는 편이 적절합니다. 서버의 게임 규칙 검증 자체는 반드시 별도로 설계해야 합니다.

클라이언트에서 Client·NetMulticast RPC를 직접 호출하면 그 클라이언트에서만 실행됩니다. Server RPC의 원격 호출은 소유 연결 조건을 충족해야 합니다.


소유권 (Ownership)

소유권(Ownership)은 언리얼 엔진 네트워킹에서 매우 중요한 개념입니다.

액터의 Owner는 다른 액터이거나 nullptr일 수 있습니다. 소유 체인을 따라 PlayerController에 도달하면 해당 컨트롤러의 연결로 소유 클라이언트를 판단합니다. 월드의 서버가 자동으로 모든 액터의 Owner 포인터가 되는 것은 아닙니다.

왜 소유권이 중요한가요?
  • RPC 대상 선택: 서버에서 원격 소유 클라이언트로 Client RPC를 보내거나, 소유 클라이언트가 Server RPC를 요청할 때 소유 연결이 기준이 됩니다.
  • 변수 복제: 특정 변수는 소유한 클라이언트에게만 복제되도록 설정할 수 있습니다 (DOREPLIFETIME_COND).
  • 네트워크 업데이트 우선순위: 오너십을 가진 액터는 네트워크 업데이트 우선순위가 더 높아질 수 있습니다.

서버에서 AActor::SetOwner(AActor* NewOwner)로 소유 관계를 설정합니다. 클라이언트에서 로컬 포인터만 바꾸어 서버 RPC 권한을 얻을 수는 없습니다.


게임 상태 관리 클래스

언리얼 엔진은 멀티플레이어 게임의 다양한 상태를 관리하기 위해 특화된 클래스들을 제공합니다.

게임 상태를 두는 클래스와 존재 위치

규칙의 결정과 클라이언트에 공개할 상태를 구분합니다.

게임 상태를 두는 클래스와 존재 위치
클래스존재 위치맡기는 정보
GameMode서버에만 존재접속·스폰·점수 부여 등 게임 규칙의 결정. 클라이언트가 직접 읽을 상태 저장소로 쓰지 않습니다.
GameState서버와 클라이언트의 복제 인스턴스라운드·경기 시간 등 공유 상태. 커스텀 속성은 복제 등록과 조건을 지정합니다.
PlayerState서버와 클라이언트의 복제 인스턴스각 플레이어의 이름·점수 등 공유 상태. 일반적인 Pawn 리스폰과 독립적으로 유지합니다.
PlayerController서버와 해당 소유 클라이언트해당 플레이어의 제어·소유 연결. 다른 클라이언트의 PlayerController가 모든 클라이언트에 복제되지는 않습니다.
GameMode
존재 위치: 서버에만 존재
맡기는 정보: 접속·스폰·점수 부여 등 게임 규칙의 결정. 클라이언트가 직접 읽을 상태 저장소로 쓰지 않습니다.
GameState
존재 위치: 서버와 클라이언트의 복제 인스턴스
맡기는 정보: 라운드·경기 시간 등 공유 상태. 커스텀 속성은 복제 등록과 조건을 지정합니다.
PlayerState
존재 위치: 서버와 클라이언트의 복제 인스턴스
맡기는 정보: 각 플레이어의 이름·점수 등 공유 상태. 일반적인 Pawn 리스폰과 독립적으로 유지합니다.
PlayerController
존재 위치: 서버와 해당 소유 클라이언트
맡기는 정보: 해당 플레이어의 제어·소유 연결. 다른 클라이언트의 PlayerController가 모든 클라이언트에 복제되지는 않습니다.

복제는 연결별로 진행되므로 서버의 모든 변경이 모든 클라이언트에 같은 순간 보인다는 뜻은 아닙니다.

복제나 RPC가 보이지 않으면 액터 생성 위치, 소유 연결, 복제 등록과 수신 조건을 차례로 확인합니다. 예제의 로그 문자열은 실행 위치를 관측한 결과가 아니라 실행 시 출력하도록 작성한 코드입니다.