본문 바로가기

개발/내일배움캠프

언리얼 네트워크 기초 정리

1. 서버와 클라이언트의 연결 구조

언리얼에서 네트워크 통신은 기본적으로 서버와 클라이언트 간의 연결(Connection) 을 기반으로 동작한다.

클라이언트가 서버에 접속하면 서버는 접속한 클라이언트마다 ClientConnection 을 생성하여 관리한다.

반대로 클라이언트는 자신이 접속한 서버와 통신하기 위한 ServerConnection 을 하나 가진다.

즉, 구조를 단순화하면 다음과 같다.

Server
 └─ UNetDriver
     ├─ ClientConnection 1
     ├─ ClientConnection 2
     └─ ClientConnection 3

Client
 └─ UNetDriver
     └─ ServerConnection

서버는 여러 클라이언트와 연결될 수 있으므로 여러 개의 ClientConnection 을 가진다.

클라이언트는 일반적으로 하나의 서버에 접속하므로 하나의 ServerConnection 을 가진다.

이 연결들은 모두 UNetConnection 을 기반으로 통신한다.


2. UNetDriver

UNetDriver 는 언리얼 네트워크 통신의 핵심 관리자 역할을 하는 클래스다.

주요 역할은 다음과 같다.

  • 네트워크 연결 생성 및 관리
  • 서버와 클라이언트 간 패킷 송수신 처리
  • 액터 복제 관리
  • RPC 호출 전달
  • 클라이언트별 Connection 관리

싱글플레이 환경에서는 일반적으로 네트워크 통신이 필요하지 않기 때문에 NetDriver가 생성되지 않는다.

정리하면 다음과 같다.

서버:
UNetDriver를 통해 연결된 클라이언트들의 ClientConnection을 관리한다.

클라이언트:
UNetDriver를 통해 서버와 연결된 하나의 ServerConnection을 관리한다.

3. NetMode

NetMode 는 현재 실행 중인 프로그램이 네트워크 상에서 어떤 역할을 하는지 나타낸다.

대표적인 NetMode는 다음과 같다.

NM_Standalone

서버 없이 독립적으로 실행되는 상태다. 일반적인 싱글플레이 모드다.

NM_DedicatedServer

전용 서버다. 화면을 렌더링하지 않고 서버 로직만 실행한다.

NM_ListenServer

서버이면서 동시에 클라이언트 역할도 하는 상태다. 흔히 말하는 호스트 플레이어가 여기에 해당한다.

NM_Client

서버에 접속한 클라이언트 상태다.

예시:

ENetMode NetMode = GetNetMode();

if (NetMode == NM_DedicatedServer)
{
    // 전용 서버에서만 실행
}
else if (NetMode == NM_Client)
{
    // 클라이언트에서만 실행
}

4. GameMode와 네트워크

GameMode 는 오직 서버에만 존재한다.

따라서 클라이언트에서 다음과 같이 호출하면 nullptr 이 반환될 수 있다.

AGameModeBase* GM = GetWorld()->GetAuthGameMode();

또는 블루프린트에서 GetGameMode 를 호출해도 클라이언트에서는 유효하지 않다.

이유는 GameMode 가 게임 규칙, 로그인 처리, 플레이어 스폰 등 서버 권한이 필요한 로직을 담당하기 때문이다.

클라이언트에서도 접근해야 하는 정보는 보통 다음 클래스에 둔다.

  • GameState
  • PlayerState
  • PlayerController
  • 복제되는 Actor 또는 Component

예를 들어 모든 클라이언트가 알아야 하는 게임 점수, 제한 시간, 매치 상태 등은 GameMode 가 아니라 GameState 에 두는 것이 일반적이다.

AGameStateBase* GS = GetWorld()->GetGameState();

GameMode 는 서버에만 있으므로, GameMode 내부에서 HasAuthority() 를 호출할 필요는 거의 없다. 이미 서버에서만 존재하기 때문이다.


5. Authority와 서버 중심 설계

멀티플레이 게임에서 중요한 핵심 로직은 반드시 서버에서 실행되도록 설계해야 한다.

예를 들어 다음과 같은 로직은 서버 권한으로 처리하는 것이 원칙이다.

  • 데미지 적용
  • 체력 감소
  • 아이템 획득
  • 경험치 증가
  • 스킬 판정
  • 게임 승패 결정
  • 인벤토리 변경
  • 중요한 상태 변경

클라이언트는 입력, 요청, UI 표시, 예측 처리 등을 담당하고, 최종 판정은 서버가 담당한다.

예시:

if (HasAuthority())
{
    // 서버에서만 실행할 핵심 로직
}

또는 클라이언트가 서버에 요청해야 하는 경우 Server RPC 를 사용한다.

UFUNCTION(Server, Reliable)
void Server_Attack();

6. NetRole

NetRole 은 액터가 네트워크 상에서 어떤 권한을 가지고 있는지를 나타내는 개념이다.

언리얼에서는 액터의 권한 관계를 확인하기 위해 주로 LocalRole 을 사용한다.

GetLocalRole()

대표적인 Role은 다음과 같다.


ROLE_Authority

Authority 는 해당 액터에 대한 최종 권한을 가진 상태다.

일반적으로 서버에 존재하는 액터가 Authority 를 가진다.

if (GetLocalRole() == ROLE_Authority)
{
    // 서버 권한을 가진 액터
}

서버에서 생성되고 관리되는 액터는 보통 서버에서 ROLE_Authority 이다.


ROLE_AutonomousProxy

AutonomousProxy 는 클라이언트가 직접 조작하는 액터에 사용된다.

대표적인 예시는 내가 조종하는 플레이어 캐릭터다.

이 액터는 서버로부터 복제 데이터를 받으면서도, 클라이언트 입력을 서버로 전송할 수 있다.

예시:

내 클라이언트의 내 캐릭터:
LocalRole == ROLE_AutonomousProxy

즉, AutonomousProxy 는 클라이언트 입장에서 “내가 조작하는 복제 액터”라고 이해하면 된다.


ROLE_SimulatedProxy

SimulatedProxy 는 서버로부터 데이터를 받아 시뮬레이션만 하는 액터다.

대표적인 예시는 다른 플레이어의 캐릭터다.

내 클라이언트에서 보이는 다른 플레이어 캐릭터:
LocalRole == ROLE_SimulatedProxy

즉, SimulatedProxy 는 클라이언트 입장에서 “서버가 보내준 정보를 받아 표현하는 액터”라고 이해하면 된다.


ROLE_None

ROLE_None 은 네트워크 복제 대상이 아니거나, 해당 방향으로 복제되지 않는 상태를 의미한다.

예를 들어 서버에는 있지만 클라이언트에 복제되지 않는 액터는 클라이언트 기준으로 존재하지 않으며, 복제 관계상 RemoteRole이 None 이 될 수 있다.


7. LocalRole과 RemoteRole

언리얼의 액터는 네트워크에서 자신의 역할을 구분하기 위해 LocalRole 과 RemoteRole 개념을 가진다.

LocalRole

현재 실행 중인 머신에서 해당 액터가 어떤 권한을 가지는지를 의미한다.

예를 들어 서버에서 액터를 보면 보통 다음과 같다.

LocalRole == ROLE_Authority

클라이언트에서 내가 조종하는 캐릭터를 보면 다음과 같다.

LocalRole == ROLE_AutonomousProxy

클라이언트에서 다른 플레이어 캐릭터를 보면 다음과 같다.

LocalRole == ROLE_SimulatedProxy

RemoteRole

상대 머신에서 이 액터가 어떤 역할로 존재하는지를 의미한다.

다만 최신 언리얼에서는 RemoteRole 을 직접 다루는 경우가 줄어들었고, 일반적인 게임 로직에서는 HasAuthority(), IsLocallyControlled(), GetLocalRole() 을 더 자주 사용한다.


8. Ownership

Ownership 은 네트워크에서 매우 중요한 개념이다.

언리얼에서 어떤 액터가 특정 클라이언트에게 소유되어 있는지를 나타내며, RPC 호출 가능 여부와도 관련이 깊다.

일반적으로 하나의 ClientConnection 은 하나의 PlayerController 와 연결된다.

그리고 그 PlayerController 가 소유하는 Pawn 또는 Character가 있다.

예를 들면 다음과 같은 구조가 만들어진다.

ClientConnection
 └─ PlayerController
     └─ PlayerCharacter
         └─ Weapon
             └─ SkillComponent

이처럼 하나의 Connection 아래에 연결된 소유 관계를 하나의 “소유 패밀리”처럼 이해할 수 있다.

이 소유 관계 안에 있는 액터들은 GetNetConnection() 또는 소유 관계를 통해 자신의 Owning Connection을 찾을 수 있다.


9. Ownership이 중요한 이유

Ownership은 특히 RPC에서 중요하다.

클라이언트가 서버 RPC를 호출하려면, 해당 RPC를 호출하는 액터가 그 클라이언트에게 소유되어 있어야 한다.

예를 들어 내 캐릭터에서 서버 RPC를 호출하는 것은 가능하다.

MyCharacter->Server_Attack();

하지만 내가 소유하지 않은 다른 플레이어의 캐릭터에서 서버 RPC를 호출하는 것은 정상적으로 동작하지 않는다.

즉, 다음과 같이 이해하면 된다.

내가 소유한 Actor:
클라이언트 → 서버 RPC 호출 가능

내가 소유하지 않은 Actor:
클라이언트 → 서버 RPC 호출 불가 또는 무시됨

따라서 멀티플레이 구조를 설계할 때는 SetOwner() 를 적절히 설정하는 것이 중요하다.


10. PlayerController와 Ownership

하나의 ClientConnection은 보통 하나의 PlayerController와 연결된다.

PlayerController 는 해당 클라이언트를 대표하는 서버 측 객체라고 볼 수 있다.

서버에는 모든 플레이어의 PlayerController가 존재한다.

하지만 각 클라이언트에는 일반적으로 자기 자신의 PlayerController만 존재한다.

즉, 클라이언트 A에서는 클라이언트 A의 PlayerController만 접근 가능하고, 클라이언트 B의 PlayerController는 존재하지 않는다.

다른 플레이어의 공개 정보가 필요하다면 PlayerController 가 아니라 PlayerState 를 사용하는 것이 일반적이다.


11. Possess와 Role 변화

Character가 처음 생성되었을 때 클라이언트에서는 SimulatedProxy 로 보일 수 있다.

이후 해당 Character가 특정 PlayerController에 의해 Possess 되면, 그 캐릭터는 소유 관계에 포함된다.

내 클라이언트에서 내가 조종하는 캐릭터라면 이후 LocalRole 이 다음과 같이 바뀐다.

ROLE_SimulatedProxy
→ ROLE_AutonomousProxy

이 변화는 해당 캐릭터가 Owning Connection을 가지게 되었기 때문이다.

즉, Possess를 통해 캐릭터가 특정 클라이언트의 소유 패밀리에 들어가고, 그 결과 클라이언트 입력을 서버로 보낼 수 있는 AutonomousProxy 가 된다.


12. 자주 사용하는 네트워크 판별 함수

멀티플레이 코드에서는 다음 함수들을 자주 사용한다.

HasAuthority

현재 실행 중인 액터가 서버 권한을 가지고 있는지 확인한다.

if (HasAuthority())
{
    // 서버 권한 로직
}

IsLocallyControlled

Pawn 또는 Character가 현재 로컬 플레이어에 의해 조종되는지 확인한다.

if (IsLocallyControlled())
{
    // 내 캐릭터에서만 실행할 로직
}

입력, 카메라, UI, 로컬 이펙트 처리 등에 자주 사용된다.


GetNetMode

현재 실행 환경이 서버인지, 클라이언트인지, 스탠드얼론인지 확인한다.

ENetMode Mode = GetNetMode();

GetLocalRole

현재 머신에서 이 액터가 어떤 네트워크 Role을 가지는지 확인한다.

ENetRole Role = GetLocalRole();

13. 정리

언리얼 네트워크 구조를 요약하면 다음과 같다.

서버는 UNetDriver 를 통해 여러 클라이언트의 ClientConnection 을 관리한다.

클라이언트는 UNetDriver 를 통해 서버와 연결된 하나의 ServerConnection 을 관리한다.

각 Connection은 UNetConnection 을 기반으로 통신한다.

 

GameMode 는 서버에만 존재하므로 클라이언트에서 직접 접근할 수 없다.

클라이언트에서도 알아야 하는 게임 정보는 GameState 또는 PlayerState 에 둔다.

중요한 게임 로직은 반드시 서버에서 실행해야 하며, 클라이언트는 서버 RPC를 통해 요청을 보내는 구조로 설계한다.

 

Authority 는 서버 권한을 의미한다.

AutonomousProxy 는 클라이언트가 직접 조작하는 액터다.

SimulatedProxy 는 서버 데이터를 받아 표현만 하는 액터다.

 

Ownership 은 RPC 호출 가능 여부와 깊게 연결되어 있으므로, 멀티플레이 구조에서는 소유 관계 설정이 매우 중요하다.

특히 클라이언트가 서버 RPC를 호출하려면 해당 액터가 그 클라이언트에게 소유되어 있어야 한다.

따라서 언리얼 멀티플레이 구조를 이해할 때는 다음 흐름을 함께 이해하는 것이 중요하다.

UNetDriver
→ UNetConnection
→ PlayerController
→ Possessed Pawn / Character
→ Owned Actor / Component
→ RPC / Replication