TIL

[Unreal Engine 멀티플레이] NetRole과 RPC 기초, Ownership, 채팅 시스템 흐름

think95592 2026. 7. 31. 19:24

오늘의 학습 목표

오늘은 언리얼 엔진 멀티플레이의 핵심 개념인 다음 내용을 학습했다.

  • Authority와 Proxy를 구분하는 NetRole
  • Server, Client, NetMulticast RPC
  • 액터의 Ownership과 Owning Connection
  • RPC와 Ownership의 관계
  • 멀티플레이 채팅 메시지의 전달 과정

1. NetRole이란?

멀티플레이에서는 하나의 액터가 서버와 여러 클라이언트에 각각 존재할 수 있다.

하지만 각 컴퓨터에 존재하는 액터가 모두 같은 권한을 갖는 것은 아니다. NetRole은 현재 실행 환경에서 해당 액터가 어떤 역할을 수행하는지 나타낸다.

ENetRole LocalRole = GetLocalRole();
ENetRole RemoteRole = GetRemoteRole();

주요 역할은 다음과 같다.

ROLE_None
ROLE_SimulatedProxy
ROLE_AutonomousProxy
ROLE_Authority

2. ROLE_Authority

ROLE_Authority는 해당 액터의 최종 상태를 결정할 권한이 있다는 뜻이다.

일반적인 서버 권한형 멀티플레이에서는 서버에 존재하는 액터가 Authority를 가진다.

if (HasAuthority())
{
    // 서버 권한이 있을 때 실행
}

예를 들어 플레이어의 체력을 변경한다면 클라이언트가 직접 체력을 확정하는 것이 아니라 서버가 처리해야 한다.

void AMyCharacter::ApplyDamage(float Damage)
{
    if (!HasAuthority())
    {
        return;
    }

    Health = FMath::Clamp(Health - Damage, 0.0f, MaxHealth);
}

HasAuthority()는 현재 프로그램이 무조건 서버인지 확인하는 함수라기보다, 현재 액터에 대한 Authority를 가지고 있는지 확인하는 함수다.


3. ROLE_AutonomousProxy

ROLE_AutonomousProxy는 일반적으로 클라이언트가 직접 조작하는 Pawn 또는 Character에 사용된다.

예를 들어 Client A의 화면에서는 Client A가 조작하는 캐릭터가 Autonomous Proxy가 될 수 있다.

Client A
├─ Client A의 캐릭터: Autonomous Proxy
└─ Client B의 캐릭터: Simulated Proxy

Autonomous Proxy는 로컬 입력을 처리하고 서버에 Server RPC를 보낼 수 있다. Character Movement처럼 반응성이 중요한 시스템에서는 클라이언트 예측에도 참여한다.


4. ROLE_SimulatedProxy

ROLE_SimulatedProxy는 다른 플레이어가 조작하는 액터를 클라이언트에서 복제해 보여주는 역할이다.

로컬 입력으로 직접 움직이지 않고 서버에서 전달받은 상태를 기반으로 움직임 등을 표현한다.

서버의 캐릭터
    ↓ Replication
클라이언트의 Simulated Proxy

5. LocalRole과 RemoteRole

LocalRole은 현재 실행 중인 컴퓨터에서 액터가 가진 역할이다.

RemoteRole은 반대편 네트워크 환경에서 액터가 어떤 역할로 존재하는지를 나타낸다.

일반적인 캐릭터를 기준으로 정리하면 다음과 같다.

실행 위치액터LocalRole

서버 모든 권한 액터 Authority
소유 클라이언트 자신이 조작하는 캐릭터 Autonomous Proxy
다른 클라이언트 원격 캐릭터 Simulated Proxy

디버깅할 때 다음처럼 역할을 출력할 수 있다.

UE_LOG(
    LogTemp,
    Log,
    TEXT("Actor: %s, LocalRole: %s, RemoteRole: %s"),
    *GetName(),
    *UEnum::GetValueAsString(GetLocalRole()),
    *UEnum::GetValueAsString(GetRemoteRole())
);

실제 게임 로직에서는 단순히 Role만 검사하기보다 목적에 따라 다음 함수를 함께 사용해야 한다.

HasAuthority();             // 이 액터의 상태를 결정할 권한이 있는가?
IsLocallyControlled();      // 이 Pawn이 로컬에서 조작되는가?
IsLocalController();        // 이 Controller가 로컬 Controller인가?
GetNetMode();               // 현재 월드의 네트워크 실행 형태는 무엇인가?

6. Authority와 Ownership은 다르다

멀티플레이를 공부하면서 가장 헷갈리기 쉬운 부분은 Authority와 Ownership의 차이다.

Authority = 누가 액터의 최종 상태를 결정하는가?
Ownership = 이 액터가 어느 플레이어의 연결에 속하는가?

일반적으로 Authority는 서버가 가진다. 그러나 Ownership은 특정 클라이언트와 연결될 수 있다.

예를 들어 플레이어가 자신의 캐릭터를 조작하더라도 캐릭터의 Authority는 서버에 있다. 대신 해당 캐릭터는 플레이어의 PlayerController와 연결된 Ownership을 갖는다.

서버
└─ PlayerController
   └─ Pawn 또는 Character
      └─ 소유된 액터

Ownership 관계는 GetOwner()로 확인할 수 있다.

AActor* OwnerActor = GetOwner();

서버에서 액터의 Owner를 지정할 수도 있다.

SomeActor->SetOwner(PlayerController);

Ownership은 단순히 액터의 소유자를 표시하는 용도가 아니다. 다음 기능에 직접 영향을 준다.

  • 클라이언트가 Server RPC를 호출할 수 있는지
  • 서버가 어느 클라이언트에 Client RPC를 보낼지
  • COND_OwnerOnly 같은 조건부 Replication
  • 액터의 네트워크 관련성
  • Owning Client 전용 데이터 전달

SetOwner()를 아무 액터에 호출한다고 해서 새로운 네트워크 연결이 생기는 것은 아니다. Ownership 체인을 따라 올라갔을 때 해당 클라이언트의 PlayerController와 연결되어야 한다.

또한 네트워크 Ownership은 서버가 관리해야 한다. 클라이언트에서 임의로 SetOwner()를 호출해도 서버의 권한 관계를 바꿀 수 없다.


7. RPC란?

RPC는 Remote Procedure Call의 약자다.

한 컴퓨터에서 함수를 호출했지만 실제 함수 실행은 서버 또는 다른 클라이언트에서 이루어지도록 만드는 기능이다.

언리얼 엔진에서는 크게 세 종류의 RPC를 사용한다.

Server RPC       : 클라이언트 → 서버
Client RPC       : 서버 → 특정 클라이언트
NetMulticast RPC : 서버 → 서버와 관련 클라이언트

RPC를 선언하려면 UFUNCTION 지정자를 사용한다.


8. Server RPC

Server RPC는 클라이언트가 서버에 작업을 요청할 때 사용한다.

UFUNCTION(Server, Reliable)
void ServerSendChatMessage(const FString& Message);

구현 함수에는 _Implementation을 붙인다.

void AMyPlayerController::ServerSendChatMessage_Implementation(
    const FString& Message)
{
    // 서버에서 실행
}

호출은 일반 함수처럼 한다.

ServerSendChatMessage(TEXT("안녕하세요!"));

하지만 아무 클라이언트나 모든 액터에서 Server RPC를 호출할 수 있는 것은 아니다.

Server RPC가 정상적으로 전달되려면 일반적으로 다음 조건이 필요하다.

  • 해당 액터가 Replication 대상이어야 한다.
  • 호출한 클라이언트가 해당 액터를 소유해야 한다.
  • 서버와 클라이언트 사이에 유효한 네트워크 연결이 있어야 한다.

PlayerController는 서버와 해당 플레이어의 클라이언트에 존재하며 Owning Connection도 명확하기 때문에 Server RPC의 진입점으로 자주 사용된다.


9. Client RPC

Client RPC는 서버가 특정 클라이언트에서 함수를 실행하고 싶을 때 사용한다.

UFUNCTION(Client, Reliable)
void ClientShowSystemMessage(const FString& Message);
void AMyPlayerController::ClientShowSystemMessage_Implementation(
    const FString& Message)
{
    // 이 PlayerController를 소유한 클라이언트에서 실행
}

서버에서 호출한다.

ClientShowSystemMessage(TEXT("서버에 접속했습니다."));

어느 클라이언트에서 실행되는지는 해당 액터의 Ownership과 Owning Connection에 의해 결정된다.

대표적인 사용 예시는 다음과 같다.

  • 특정 플레이어에게만 시스템 메시지 표시
  • 특정 플레이어의 UI 열기
  • 개인 퀘스트 결과 전달
  • 해당 플레이어 전용 연출 실행

10. NetMulticast RPC

NetMulticast RPC는 서버에서 호출하면 서버와 해당 액터를 알고 있는 관련 클라이언트에서 실행된다.

UFUNCTION(NetMulticast, Reliable)
void MulticastReceiveChatMessage(
    const FString& SenderName,
    const FString& Message);
void AMyGameState::MulticastReceiveChatMessage_Implementation(
    const FString& SenderName,
    const FString& Message)
{
    // 서버와 관련 클라이언트에서 실행
}

NetMulticast RPC는 반드시 서버에서 호출해야 전체 클라이언트로 전달된다.

클라이언트에서 직접 호출하면 다른 컴퓨터로 전송되지 않고 해당 클라이언트에서만 로컬 함수처럼 실행된다.

서버에서 Multicast 호출
→ 서버와 관련 클라이언트에서 실행

클라이언트에서 Multicast 호출
→ 호출한 클라이언트에서만 실행

11. Reliable과 Unreliable

RPC에는 Reliable 또는 Unreliable을 지정할 수 있다.

Reliable

전달을 보장해야 하는 중요한 이벤트에 사용한다.

UFUNCTION(Server, Reliable)
void ServerSendChatMessage(const FString& Message);

사용 예시는 다음과 같다.

  • 채팅 메시지
  • 아이템 구매 요청
  • 매치 참가 요청
  • 중요한 상호작용

Unreliable

일부 패킷이 누락되어도 다음 업데이트로 보완할 수 있는 빈번한 이벤트에 적합하다.

UFUNCTION(NetMulticast, Unreliable)
void MulticastPlayCosmeticEffect();

사용 예시는 다음과 같다.

  • 빈번하게 발생하는 시각 효과
  • 반복되는 임시 상태
  • 다음 업데이트로 대체 가능한 정보

Reliable이라고 해서 무제한으로 사용해도 되는 것은 아니다. 너무 많은 Reliable RPC가 쌓이면 이후 네트워크 메시지 처리까지 지연될 수 있다.


12. 멀티플레이 채팅의 전체 흐름

멀티플레이 채팅은 클라이언트가 입력한 문자열을 다른 클라이언트에 바로 전달하는 방식으로 구현하면 안 된다.

서버가 메시지를 전달받아 검증한 후 모든 플레이어에게 배포해야 한다.

클라이언트 채팅 UI
    ↓
로컬 PlayerController
    ↓ Server RPC
서버의 PlayerController
    ↓ 메시지 검증
서버의 GameState
    ↓ NetMulticast RPC
모든 관련 클라이언트의 GameState
    ↓
각 클라이언트의 채팅 UI

핵심 흐름은 다음과 같다.

  1. 플레이어가 채팅 UI에 메시지를 입력한다.
  2. 로컬 PlayerController가 Server RPC를 호출한다.
  3. 서버가 메시지 길이와 내용을 검증한다.
  4. 서버가 발신자의 이름을 PlayerState에서 가져온다.
  5. 서버의 GameState가 NetMulticast RPC를 호출한다.
  6. 각 클라이언트가 메시지를 채팅 UI에 추가한다.

13. PlayerController에서 Server RPC 호출하기

헤더 파일

UCLASS()
class AMyPlayerController : public APlayerController
{
    GENERATED_BODY()

public:
    void SendChatMessage(const FString& Message);

protected:
    UFUNCTION(Server, Reliable)
    void ServerSendChatMessage(const FString& Message);
};

CPP 파일

void AMyPlayerController::SendChatMessage(const FString& Message)
{
    if (!IsLocalController())
    {
        return;
    }

    ServerSendChatMessage(Message);
}

void AMyPlayerController::ServerSendChatMessage_Implementation(
    const FString& Message)
{
    FString SanitizedMessage = Message.TrimStartAndEnd();

    if (SanitizedMessage.IsEmpty())
    {
        return;
    }

    constexpr int32 MaxMessageLength = 100;
    SanitizedMessage = SanitizedMessage.Left(MaxMessageLength);

    const APlayerState* PS = GetPlayerState<APlayerState>();

    const FString SenderName =
        IsValid(PS) ? PS->GetPlayerName() : TEXT("Unknown");

    if (AMyGameState* GS = GetWorld()->GetGameState<AMyGameState>())
    {
        GS->MulticastReceiveChatMessage(
            SenderName,
            SanitizedMessage
        );
    }
}

클라이언트는 메시지만 서버에 전달한다. 플레이어 이름은 클라이언트가 함께 보내지 않고 서버가 PlayerState에서 직접 가져온다.

클라이언트가 이름까지 전달하면 다른 플레이어의 이름을 사칭할 수 있기 때문이다.


14. GameState에서 메시지 배포하기

헤더 파일

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
    FOnChatMessageReceived,
    const FString&, SenderName,
    const FString&, Message
);

UCLASS()
class AMyGameState : public AGameStateBase
{
    GENERATED_BODY()

public:
    UPROPERTY(BlueprintAssignable)
    FOnChatMessageReceived OnChatMessageReceived;

    UFUNCTION(NetMulticast, Reliable)
    void MulticastReceiveChatMessage(
        const FString& SenderName,
        const FString& Message
    );
};

CPP 파일

void AMyGameState::MulticastReceiveChatMessage_Implementation(
    const FString& SenderName,
    const FString& Message)
{
    if (GetNetMode() == NM_DedicatedServer)
    {
        return;
    }

    OnChatMessageReceived.Broadcast(SenderName, Message);
}

각 클라이언트의 채팅 위젯은 OnChatMessageReceived 델리게이트에 이벤트를 연결한다.

메시지가 도착하면 다음과 같이 UI에 추가한다.

[PlayerA] 안녕하세요!
[PlayerB] 반갑습니다.

15. 왜 PlayerController에서 Multicast하지 않을까?

PlayerController는 모든 클라이언트에 동일하게 복제되는 액터가 아니다.

각 클라이언트에는 기본적으로 자신의 PlayerController만 존재하며 다른 플레이어의 PlayerController는 존재하지 않는다. 따라서 PlayerController에서 Multicast RPC를 호출하면 모든 플레이어에게 전달되는 전역 채팅을 기대하기 어렵다.

반면 GameState는 서버와 모든 클라이언트에 존재하고 전체 게임 상태를 공유하기 위한 액터다.

PlayerController
→ 특정 플레이어의 요청과 개인 응답에 적합

GameState
→ 전체 플레이어가 공유하는 상태와 이벤트에 적합

따라서 기본적인 전역 채팅은 다음 구조가 자연스럽다.

PlayerController의 Server RPC
→ GameState의 NetMulticast RPC
→ 각 클라이언트 UI

16. RPC 채팅의 한계

NetMulticast RPC로 전달한 채팅은 순간적인 이벤트다.

메시지가 전달된 이후 접속한 플레이어는 이전 RPC를 받을 수 없으므로 과거 채팅 기록을 확인할 수 없다.

채팅 기록이 필요하다면 서버의 GameState에 메시지를 저장하고 Replication으로 동기화하는 구조를 사용할 수 있다.

실시간 메시지 출력
→ NetMulticast RPC

채팅 기록 보존 및 늦은 접속 지원
→ Replicated 배열 또는 Fast Array

또한 실제 서비스에서는 서버에서 다음 항목도 검사해야 한다.

  • 최대 메시지 길이
  • 빈 문자열과 공백 메시지
  • 메시지 전송 간격
  • 도배 방지
  • 금칙어
  • 채널 또는 팀 권한
  • 음소거 및 차단 상태

17. 자주 발생하는 RPC 문제

Server RPC가 실행되지 않는 경우

다음 항목을 확인해야 한다.

  • 액터의 bReplicates가 활성화되어 있는가?
  • 클라이언트가 해당 액터를 소유하고 있는가?
  • 로컬 클라이언트에서 호출했는가?
  • 실제 네트워크 플레이 환경에서 테스트하고 있는가?

Client RPC가 엉뚱한 위치에서 실행되는 경우

해당 액터의 Owner와 Owning Connection을 확인해야 한다. Client RPC는 서버가 원하는 클라이언트를 직접 지정하는 방식이 아니라 액터의 Ownership을 따라 대상 클라이언트를 결정한다.

Multicast가 다른 클라이언트에 전달되지 않는 경우

클라이언트에서 Multicast RPC를 호출했는지 확인해야 한다. 전체 클라이언트로 전달하려면 서버에서 호출해야 한다.


18. 오늘 배운 내용

오늘은 NetRole을 통해 서버와 클라이언트에서 액터가 서로 다른 역할을 가진다는 것을 배웠다.

Authority는 액터의 최종 상태를 결정할 권한이고, Ownership은 액터가 어느 플레이어의 네트워크 연결에 속하는지를 나타낸다.

두 개념은 다음처럼 구분할 수 있다.

Authority
= 누가 게임 상태를 결정하는가?

Ownership
= 어느 클라이언트가 이 액터를 통해 RPC를 주고받을 수 있는가?

LocalRole
= 현재 컴퓨터에서 이 액터가 수행하는 역할

RPC
= 다른 네트워크 인스턴스에서 함수를 실행하는 방법

RPC의 방향도 다음과 같이 정리할 수 있다.

Server RPC
클라이언트 → 서버

Client RPC
서버 → 특정 Owning Client

NetMulticast RPC
서버 → 서버와 관련 클라이언트

멀티플레이 채팅에서는 클라이언트의 PlayerController가 Server RPC로 메시지를 전달하고, 서버가 메시지를 검증한 뒤 GameState를 통해 각 클라이언트에 배포할 수 있다.

오늘 공부한 내용에서 가장 중요한 점은 Authority와 Ownership을 같은 개념으로 생각하면 안 된다는 것이다. 서버가 Authority를 가지고 있어도 특정 클라이언트는 Ownership을 통해 자신이 소유한 액터에서 Server RPC를 호출할 수 있다.