1. NetMode vs NetRole

  • NetMode: 월드(컴퓨터) 단위 정보. 이 컴퓨터가 서버인지 클라이언트인지를 나타낸다. (NM_Standalone, NM_Client, NM_ListenServer, NM_DedicatedServer)
  • NetRole: 액터 단위 정보. 이 액터가 서버에서 권한을 가진 원본인지, 클라이언트에 복제된 대리인인지를 나타낸다.

같은 서버 컴퓨터(NetMode 기준 동일) 안에서도, 액터마다 NetRole이 다른 의미를 가질 수 있다. 예를 들어 "서버에만 존재하고 복제 안 되는 액터"와 "서버에서 만들어져 클라이언트로 복제된 액터"는 NetMode로는 구분이 안 되지만, NetRole(정확히는 아래에서 다룰 리모트 롤)로는 구분된다.

NetMode는 "건물 자체가 본사인지 지점인지"를, NetRole은 "건물 안의 특정 직원이 결정권자인지 대리인인지"를 알려주는 정보라고 이해하면 쉽다.


2. Authority와 Proxy

  • Authority: 서버 컴퓨터에 스폰된 액터의 NetRole 값. "권한을 가지고 있다"는 뜻으로, 게임에 중대한 영향을 끼치는 로직은 반드시 NetRole이 Authority일 때 수행해야 한다.
  • Proxy: Authority를 가진 액터가 클라이언트로 복제되었을 때, 그 복제본의 NetRole 값. "대리"라는 뜻이며, 여기서는 중요한 로직을 수행해서는 안 된다.

클라이언트(Proxy)에서 직접 중요 로직(예: 데미지 처리)을 수행하면 어떤 문제가 생길까?

  1. 동기화 깨짐: 서버는 그 변경을 모르기 때문에, 다음 동기화 시점에 서버의 값으로 덮어씌워져 버린다.
  2. 다른 클라이언트와 불일치: 나만 처리한 결과이므로, 다른 플레이어 화면에서는 반영되지 않는다.
  3. 보안 취약점: 클라이언트는 사용자가 조작 가능한 환경이므로, 중요한 판정을 클라이언트에 맡기면 핵/치트에 취약해진다.

3. 로컬 롤(Local Role)과 리모트 롤(Remote Role)

NetRole은 사실 하나의 값이 아니라, 로컬 롤리모트 롤 두 가지로 나뉜다.

  • 로컬 롤: 지금 코드가 실행되고 있는 쪽 컴퓨터에서의 역할
  • 리모트 롤: 커넥션 반대편 컴퓨터에서의 역할

둘로 나눈 이유는 로컬 롤만으로는 구분이 안 되는 케이스가 있기 때문이다.

예시: 서버에 접속한 플레이어의 캐릭터 A, 그냥 서버에만 스폰된 캐릭터 B가 있다고 하면

  로컬 롤 (서버 기준) 리모트 롤
캐릭터 A (플레이어 접속) Authority Proxy
캐릭터 B (서버 전용) Authority None

 

둘 다 로컬 롤은 Authority라서 구분이 안 되지만, 리모트 롤을 보면 A는 클라이언트로 복제되므로 Proxy, B는 복제되지 않으므로 None이다. 이 구분이 있어야 "서버에서 호출하고 클라이언트에서 실행되는 RPC" 같은 기능을 구현할 수 있다.

같은 액터를 클라이언트 컴퓨터 입장에서 보면 로컬/리모트가 반대로 뒤집힌다 (로컬=Proxy, 리모트=Authority).


4. NetRole의 4가지 값

의미 예시
None 네트워크 관련 역할이 없음 서버에서만 스폰되고 레플리케이션 안 되는 액터, 클라이언트에만 스폰된 액터
Authority 서버 컴퓨터에 위치, 중요 로직 작성 가능 GameMode 액터
Autonomous Proxy 서버→클라 복제 + 클라→서버 입력 전송 가능 (양방향) PlayerController, 내가 직접 조종하는 PlayerCharacter
Simulated Proxy 서버→클라 복제만 받음 (단방향) 내 화면에 보이는 다른 플레이어의 캐릭터

 

주의: NetRole 값은 절대적이지 않다.

로컬 롤이 Authority, 리모트 롤이 None인데도 네트워크 동기화가 일어나는 경우가 있을 수 있으므로 참고사항으로 받아들여야 한다.


5. 로깅 함수로 NetRole 출력해보기

새 C++ 클래스를 만들어 BeginPlay와 PossessedBy 시점의 NetMode/NetRole을 출력해본다.

// ChatX.h
class ChatXFunctionLibrary
{
public:
    static FString GetRoleString(const AActor* InActor)
    {
        FString RoleString = TEXT("None");

        if (IsValid(InActor) == true)
        {
            FString LocalRoleString = UEnum::GetValueAsString(TEXT("Engine.ENetRole"), InActor->GetLocalRole());
            FString RemoteRoleString = UEnum::GetValueAsString(TEXT("Engine.ENetRole"), InActor->GetRemoteRole());

            RoleString = FString::Printf(TEXT("%s / %s"), *LocalRoleString, *RemoteRoleString);
        }

        return RoleString;
    }
};
// CXPawn.h
UCLASS()
class CHATX_API ACXPawn : public APawn
{
    GENERATED_BODY()

protected:
    virtual void BeginPlay() override;
    virtual void PossessedBy(AController* NewController) override;
};
// CXPawn.cpp
void ACXPawn::BeginPlay()
{
    Super::BeginPlay();

    FString NetRoleString = ChatXFunctionLibrary::GetRoleString(this);
    FString CombinedString = FString::Printf(TEXT("CXPawn::BeginPlay() %s [%s]"), *ChatXFunctionLibrary::GetNetModeString(this), *NetRoleString);
    ChatXFunctionLibrary::MyPrintString(this, CombinedString, 10.f);
}

void ACXPawn::PossessedBy(AController* NewController)
{
    Super::PossessedBy(NewController);

    FString NetRoleString = ChatXFunctionLibrary::GetRoleString(this);
    FString CombinedString = FString::Printf(TEXT("CXPawn::PossessedBy() %s [%s]"), *ChatXFunctionLibrary::GetNetModeString(this), *NetRoleString);
    ChatXFunctionLibrary::MyPrintString(this, CombinedString, 10.f);
}

 

BP_GameModeBase의 Default Pawn Class에 CXPawn을 지정하면, 폰이 BeginPlay/PossessedBy될 때마다 NetMode와 로컬/리모트 롤이 화면에 출력된다.


6. NetRole을 활용하는 핵심 함수들

AActor::HasAuthority()

FORCEINLINE_DEBUGGABLE bool AActor::HasAuthority() const
{
    return (GetLocalRole() == ROLE_Authority);
}

 

 

지금 코드가 서버에서 실행 중인지 한 줄로 확인한다.

데미지 처리, 스폰 등 중요 로직 앞에서 가드(guard)로 사용한다.

 

APawn::IsLocallyControlled()

bool APawn::IsLocallyControlled() const
{
    return ( Controller && Controller->IsLocalController() );
}

 

이 폰이 현재 컴퓨터에서 직접 조종되고 있는지 확인한다.

카메라 부착, 입력 처리 대상 결정 등에 사용한다.

 

AController::IsLocalController()

bool AController::IsLocalController() const
{
    const ENetMode NetMode = GetNetMode();

    if (NetMode == NM_Standalone)
    {
        // Not networked.
        return true;
    }

    if (NetMode == NM_Client && GetLocalRole() == ROLE_AutonomousProxy)
    {
        // Networked client in control.
        return true;
    }

    if (GetRemoteRole() != ROLE_AutonomousProxy && GetLocalRole() == ROLE_Authority)
    {
        // Local authority in control.
        return true;
    }

    return false;
}

 

조건을 하나씩 풀어보면:

  1. NetMode == Standalone: 네트워크 자체가 없는 싱글플레이이므로 NetRole을 따질 필요 없이 무조건 로컬이다. 네트워크가 없는 상황에서는 "서버냐 클라냐"라는 질문 자체가 성립하지 않기 때문에 가장 먼저 걸러내는 조기 종료(early return) 분기다.
  2. NetMode == Client이고 로컬 롤이 Autonomous Proxy: 클라이언트 컴퓨터에서 내가 직접 조종하는 컨트롤러의 복제본을 보고 있는 경우 → 로컬.
  3. 리모트 롤이 Autonomous Proxy가 아니고 로컬 롤이 Authority: 서버 컴퓨터에서 클라이언트로 양방향 복제되지 않은 컨트롤러 → 서버 자신이 직접 조종하는 경우(리슨 서버의 호스트 플레이어) → 로컬.

NetMode(컴퓨터 단위)와 NetRole(액터 단위)을 함께 검사해야만 이 컨트롤러가 지금 나와 직접 연결된 것인지를 정확히 판단할 수 있다.


7. 멀티플레이에서 각 액터의 특징

  • GameMode 클래스는 HasAuthority()를 호출할 필요가 없다. GameMode는 서버에만 존재하고 클라이언트로 복제조차 되지 않는 액터이므로, NetRole이 항상 Authority로 고정되어 있다.
  • Pawn 액터의 롤은 Authority, Autonomous Proxy, Simulated Proxy 중 하나다. Pawn은 서버와 클라이언트 양쪽에 존재하며 복제 대상이기 때문이다.
  • UserWidget(UI) 클래스의 로직은 로컬 클라이언트에서만 수행된다. UI는 게임 전체에 영향을 주는 로직이 아니라 화면 표시용이므로, 따로 네트워크를 타지 않는다.

8. PlayerController의 UI 생성 가드

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

    if (IsLocalController() == false)
    {
        // UI 생성 로직은 로컬 Controller 객체일 때만 처리하면 됨.
        return;
    }

    // ... UI 생성 로직
}

 

서버 컴퓨터에는 모든 플레이어의 PlayerController가 존재한다. IsLocalController() 체크가 없다면, 서버에서 BeginPlay가 호출될 때 다른 플레이어의 PlayerController에 대해서까지 UI 생성 로직이 실행되어 불필요한 UI 인스턴스가 생성될 수 있다. IsLocalController()로 "지금 이 컴퓨터가 실질적으로 소유/조종하는 컨트롤러인지"를 먼저 검사하여, 그렇지 않으면 즉시 return함으로써 이런 낭비를 막는다.

 

중요한 로직은 HasAuthority()로 서버인지 확인하고, UI/입력 같은 로컬 전용 로직은 IsLocalController() / IsLocallyControlled()로 "내가 조종하는 것인지"를 확인한 뒤 실행하자.

 

+ Recent posts