게임 멀티플레이 기초 - 서버와 클라이언트 <NetMode, NetRole>

2026. 6. 3. 01:24TIL

서버와 클라이언트

멀티플레이 게임은 네트워크로 연결된 여러 PC가 역할을 나눠서 동작한다.

 

- 서버(Server) : 게임 상태를 관리하고 서비스를 제공하는 컴퓨터

- 클라이언트(Client) : 서버에 접속해서 서비스를 요청하는 컴퓨터

클라이언트 A ──┐
클라이언트 B ──┼──→ 서버 (게임 상태 관리)
클라이언트 C ──┘

 

서버는 모든 플레이어의 실제 위치, HP, 점수 등 게임의 진실(Truth)을 가지고 있다. 클라이언트는 서버로부터 이 정보를 받아서 화면에 보여주는 역할만 한다.

 

서버 방식은 크게 두 가지로 나뉜다.

데디케이티드 서버 (Dedicated Server)

- 게임 회사가 운영하는 별도의 서버 컴퓨터

- 서버는 게임에 직접 참여하지 않고 관리만 한다.

- 배틀그라운드, 오버워치 같은 대형 온라인 게임이 이 방식을 사용한다.

 

리슨 서버 (Listen Server)

- 플레이어 중 한 명의 PC가 서버 역할을 겸한다.

- 방장이 서버이면서 동시에 게임도 플레이한다.

- 소규모 협동 게임, 방 만들기 방식의 게임에서 자주 사용된다. ex) 마인크래프트, 어몽어스 등


서버와 클라이언트로 나누는 이유

클라이언트는 해커가 조작할 수 있는 환경이다. 메모리를 직접 건드리거나 패킷을 가로채서 값을 바꿀 수 있다.

데미지 처리를 클라이언트에서 하면, 해커가 패킷을 조작해서 데미지 값을 임의로 바꾸는 것이 가능해진다. 따라서 게임에 중대한 영향을 끼치는 로직은 반드시 서버에서 처리해야 한다.

 

멀티플레이 개발에서 가장 큰 특징은 같은 코드가 여러 PC에서 동시에 실행된다는 점이다.

예를 들어 APlayerCharacter::BeginPlay()는 서버 PC, 내 클라이언트 PC, 다른 사람의 클라이언트 PC에서 모두 실행된다.

 

따라서 지금 실행 중인 환경이 서버인지, 클라이언트인지 구분할 수 있어야 하며, 이를 위한 개념이 NetMode와 NetRole이다.


NetMode

NetMode는 해당 게임 프로세스가 네트워크에서 어떤 역할인지를 나타내는 월드 단위의 속성이다.

 

NetMode 설명
NM_Standalone 싱글플레이. 원격 연결 없음
NM_ListenServer 서버이면서 직접 플레이도 함
NM_DedicatedServer 서버 역할만. 화면/입력 없음
NM_Client 서버에 접속한 클라이언트

 

NetMode 체크하기

ENetMode UWorld::InternalGetNetMode() const
{
    if (NetDriver != NULL)  // 멀티플레이면
        return bIsClientOnly ? NM_Client : NetDriver->GetNetMode();
    // NetDriver 없으면 → Standalone
}
ENetMode UNetDriver::GetNetMode() const
{
    return (IsServer() ? (GIsClient ? NM_ListenServer : NM_DedicatedServer) : NM_Client);
}
bool UNetDriver::IsServer() const
{
    return ServerConnection == NULL;
    // 클라이언트는 항상 ServerConnection을 가지고 있다.
    // 서버는 ServerConnection이 없고 ClientConnection 배열만 있다.
}

 

핵심 포인트는 ServerConnection이 있으면 클라이언트, 없으면 서버이다.


NetDriver와 NetConnection

NetDriver

네트워크 통신을 관리하는 저수준 클래스이다.

- 싱글플레이(NM_Standalone)에서는 생성되지 않는다.

- 멀티플레이에서만 UWorld::Listen() 호출 시 생성된다.

- 게임에 참여하는 각 PC마다 하나씩 생성된다.

 

NetConnection

두 PC 사이의 연결 하나를 나타내는 객체이다.

- 서버에는 접속한 클라이언트 수만큼 ClientConnection이 존재한다.

- 클라이언트에는 ServerConnection 하나만 존재하고 ClienConnection은 존재하지 않는다.

서버 NetDriver
  └── ClientConnection (클라이언트 A)
  └── ClientConnection (클라이언트 B)

클라이언트 NetDriver
  └── ServerConnection

Ownership (소유관계)

액터가 자신의 NetConnection을 찾아가는 체계이다.

이 소유 관계는 RPC의 Property Replication의 전제가 된다.

ClientConnection
    └── PlayerController  ← Connection이 직접 소유
            └── Pawn      ← Possess 시 Owner가 PC로 자동 설정
                    └── 무기 액터  ← SetOwner(Pawn)으로 수동 설정

GetNetConnection()은 Owner를 타고 올라가면서 NetConnection을 찾아온다.

UNetConnection* AActor::GetNetConnection() const
{
    return Owner ? Owner->GetNetConnection() : nullptr;
    // Owner가 없으면 nullptr → 통신 불가
}

UNetConnection* APawn::GetNetConnection() const
{
    if (Controller)
        return Controller->GetNetConnection();
    return Super::GetNetConnection();
}

UNetConnection* APlayerController::GetNetConnection() const
{
    return (Player != NULL) ? NetConnection : NULL;
    // 여기서 실제 NetConnection 반환
}

Owner가 중간에 끊기면 NetConnection을 찾지 못해 통신이 불가능해진다.

액터 서버 내 클라이언트 다른 클라이언트
GameMode O X X
PlayerController O O (본인 것만) X
Pawn / 배경 액터 O O O
UI (UMG Widget) X O O

클라이언트에서 GetGameMode()를 호출하면 GameMode가 없으므로 nullptr이 반환된다.

GameMode에 접근하려면 HasAuthority()로 서버임을 확인하거나 ServerRPC내에서 접근해야 한다.


NetRole

NetMode가 월드 단위의 속성이라면, NetRole은 액터 단위의 속성이다.

액터가 어느 PC에 스폰되어 있고, 그 멤버 함수가 어느 PC에서 실행되는지 구분하기 위해 사용된다.

 

Authority와 Proxy

Authority : 서버에 스폰된 액터의 NetRole. 중요한 로직을 수행할 권한이 있다.

 

Proxy : Authority 액터가 클라이언트로 복제되었을 때 NetRole. 허상이므로 중요한 로직을 수행하면 안된다.

 

로컬 롤과 리모트 롤

LocalRole : 지금 이 코드가 실행 중인 PC에서 이 액터의 역할

 

RemoteRole : 반대편 PC에서의 이 액터의 역할

 

같은 액터라도 어느 PC에서 보느냐에 따라 LocalRole / RemoteRole이 다르게 보인다.

즉, 내가 컨트롤 하는 PC는 LocalRole, 서버쪽 PC는 RemoteRole이 된다.

서버 입장의 플레이어 캐릭터:
  LocalRole  = Authority
  RemoteRole = AutonomousProxy

클라이언트 입장의 플레이어 캐릭터:
  LocalRole  = AutonomousProxy
  RemoteRole = Authority

 

NetRole의 종류

NetRole 설명 예시
None 레플리케이션 안 됨 서버 전용 액터
Authority 원본. 중요한 로직 수행 가능 GameMode
AutonomousProxy 복제본이지만 송신도 가능 내가 조종하는 PlayerController, 내 캐릭터
SimulatedProxy 복제본. 수신만 가능 내 화면에 보이는 다른 플레이어 캐릭터

 

LocalRole과 RemoteRole로 나눈 이유?

로컬 롤만 있다면 아래의 케이스를 구분할 수 없다.

서버에 접속한 플레이어 캐릭터 A:
  LocalRole  = Authority
  RemoteRole = AutonomousProxy  ← 클라이언트가 조종 중

서버에서 스폰된 NPC B:
  LocalRole  = Authority
  RemoteRole = None             ← 복제만 됨, 조종 없음

LocalRole만 보면 둘 다 Authority라서 구분이 불가능하다.

RemoteRole까지 봐야 플레이어 캐릭터인지, 서버에서 스폰된 액터인지 구분할 수 있다.


NetRole 기반 핵심 함수

HasAuthority()

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

LocalRole이 Authority인지 확인한다. "서버에서 실행중인가?"와 같은 의미의 함수이다.

데미지, 스폰, 게임 규칙 판정 등 중요한 로직 앞에서 반드시 체크한다.

 

예시코드

void AMyActor::TakeDamage()
{
    if (HasAuthority())
    {
        HP -= 10;
    }
}

 

IsLocalController() / IsLocallyControlled()

bool APawn::IsLocallyControlled() const
{
    return (Controller && Controller->IsLocalController());
}
bool AController::IsLocalController() const
{
    // 싱글플레이는 항상 로컬
    if (NetMode == NM_Standalone)
        return true;

    // 클라이언트에서 내가 조종하는 컨트롤러
    if (NetMode == NM_Client && GetLocalRole() == ROLE_AutonomousProxy)
        return true;

    // 리슨서버에서 호스트 본인의 컨트롤러
    // RemoteRole != AutonomousProxy → 반대편에서 조종하는 클라이언트가 없다는 뜻
    if (GetRemoteRole() != ROLE_AutonomousProxy && GetLocalRole() == ROLE_Authority)
        return true;

    return false;
}

"지금 이 PC에서 직접 조종되는 컨트롤러 / 폰이냐"를 판별한다.

입력 처리, UI 생성 등 클라이언트 전용 로직에서 사용한다.

 

 

리슨서버에서 로컬 플레이어 컨트롤러

조건 3번째의 리슨서버에서 로컬 플레이어 컨트롤러를 찾는 부분을 잘 살펴보자.

 

리슨 서버는 서버이면서 직접 플레이도 하는 구조이다. 그래서 여러 PlayerController가 존재한다.

그래서 LocalRole = Authority이기 때문에 LocalRole만으로는 구분이 어렵기 때문에 RemoteRole의 조건을 추가한 것이다.

내 컨트롤러 (직접 조종):
  LocalRole  = Authority
  RemoteRole = None  ← 반대편이 없음, 내가 이 서버 PC에서 직접 조종하는 거니까

클라이언트 A의 컨트롤러:
  LocalRole  = Authority
  RemoteRole = AutonomousProxy  ← 반대편(클라이언트 A)이 AutonomousProxy로 조종 중

 

근데 여기서 GetRemoteRole() != ROLE_AutonomousProxy가 아닌, GetRemoteRole() == None을 쓸 수도 있지 않을까? 라는 생각을 하게 되었다.

 

이 두 조건은 비슷한 것 같지만 다르다.

우선 두 조건이 참인 경우의 수를 보겠다.

 

GetRemoteRole() != ROLE_AutonomousProxy

RemoteRole = None             → true 
RemoteRole = Authority        → true 
RemoteRole = SimulatedProxy   → true 
RemoteRole = AutonomousProxy  → false

 

GetRemoteRole() == None

RemoteRole = None             → true 
RemoteRole = 나머지 전부       → false

 

이 함수는 AController에 정의되어있다. PlayerController뿐 아니라 AIController에도 이 함수를 상속받아 사용한다.

AIController는 서버에서 NPC를 조종하는데, 이 경우에는

AIController:
  LocalRole  = Authority
  RemoteRole = SimulatedProxy  ← None이 아닐 수 있음

이렇게 될 수가 있다.

 

NPC는 클라이언트로 복제되지만, 그 컨트롤러 자체는 서버 로컬에서 실행되는 것이기 때문에 이때 ==None으로 체크해서 판별하면 AIController가 로컬 컨트롤러로 잘못 판단할 수 있다.

그래서 종합적으로 GetRemoteRole() != ROLE_AutonomousProxy로 조건을 설정하는 것이다.


핵심 포인트

 

  • NetMode는 월드 단위, NetRole은 액터 단위의 속성이다.
  • ServerConnection == NULL이면 서버, 아니면 클라이언트다.
  • Owner 체인이 끊기면 GetNetConnection()이 nullptr을 반환해 통신이 불가능하다.
  • GameMode는 서버에만 존재한다. 클라이언트에서 GetGameMode()는 nullptr을 반환한다.
  • Authority는 원본, Proxy는 복제본이다. 중요한 로직은 Authority에서만 수행한다.
  • AutonomousProxy는 양방향 통신 가능, SimulatedProxy는 수신만 가능하다.
  • LocalRole과 RemoteRole을 함께 봐야 서버에서 플레이어 캐릭터와 NPC를 구분할 수 있다.
  • HasAuthority()는 서버 로직 보호, IsLocalController()는 클라이언트 전용 로직 보호에 사용한다.