2026. 6. 3. 01:24ㆍTIL
서버와 클라이언트
멀티플레이 게임은 네트워크로 연결된 여러 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()는 클라이언트 전용 로직 보호에 사용한다.
'TIL' 카테고리의 다른 글
| <멀티플레이 기초> 채팅 구현 (RPC 실전 적용) (0) | 2026.06.07 |
|---|---|
| <Unreal> 멀티플레이 기초 - RPC(Remote Procedure Call) (0) | 2026.06.04 |
| < 언리얼 C++ > GameMode (0) | 2026.04.12 |
| < 언리얼 C++ > 회전 발판과 움직이는 장애물 만들기 (0) | 2026.04.11 |
| [TIL 46일차] <언리얼 C++> 리플렉션 시스템 (2) | 2026.04.09 |