본문 바로가기

블루프린트에서의 리플리케이션

변수 복제와 RepNotify·RPC·액터 복제를 설정하고 서버 권한 기준으로 아이템 획득을 동기화합니다.

혼자 하는 게임에서는 아이템을 줍는 그래프가 한 컴퓨터에서만 실행되므로, 아이템이 사라지고 인벤토리가 늘어나는 것을 확인하면 끝입니다. 두 사람이 같은 월드에서 플레이하면 사정이 달라집니다. 한쪽 화면에서만 아이템이 사라지거나, 두 사람이 같은 아이템을 동시에 주워 하나가 둘이 되는 일이 생길 수 있습니다. 누가 아이템을 주웠는지 정하는 곳과, 그 결과를 모든 화면에 맞추는 방법이 필요합니다.

앞의 두 글에서 게임 모드, 스테이트, 컨트롤러, 폰이 규칙과 상태, 조작을 나눠 맡는 것을 보았습니다. 이 구조가 멀티플레이어에서 제대로 동작하게 하는 장치가 리플리케이션(replication), 곧 네트워크 복제입니다. 이 글에서는 블루프린트에서 변수, 이벤트, 액터를 복제하는 세 가지 설정을 익히고, 서버가 판단하는 아이템 줍기를 만들어 봅니다.


서버가 정하고 클라이언트에 전달하는 구조

리플리케이션은 멀티플레이어 게임에서 서버와 클라이언트가 게임의 상태(변수, 오브젝트의 위치 등)와 이벤트(함수 호출)를 네트워크로 맞추는 과정입니다. 언리얼 엔진은 서버-클라이언트 모델을 쓰며, 서버가 게임의 확정된 상태를 가지고 관리합니다. 서버는 이 상태를 관련성(relevancy), 복제 조건, 갱신 일정에 따라 필요한 클라이언트에 보냅니다. 관련성은 어떤 액터가 특정 클라이언트에게 지금 의미가 있는지를 뜻하며, 예를 들어 아주 멀리 있는 액터는 그 클라이언트에 보내지 않을 수 있습니다.

리플리케이션이 필요한 이유는 세 가지입니다. 첫째, 모든 플레이어의 컴퓨터가 같은 월드와 상태를 공유해야 합니다. 다른 플레이어 캐릭터의 위치, 아이템을 누가 주웠는지, 적의 체력 같은 값이 화면마다 다르면 같은 게임을 한다고 할 수 없습니다. 둘째, 피해 계산이나 충돌 처리처럼 중요한 게임 로직을 서버가 처리하면 클라이언트가 보낸 요청을 서버에서 검증할 수 있습니다. 다만 서버를 쓴다고 부정행위가 저절로 막히는 것은 아니며, 서버의 그래프가 실제로 요청을 검사해야 합니다. 셋째, 언리얼 엔진은 필요한 데이터만 골라 복제해 네트워크 대역폭을 아낍니다.


변수 복제와 RepNotify

복제되는 액터의 변수를 서버에서 바꾸면, 그 액터를 받는 클라이언트에 새 값이 전달됩니다. 먼저 블루프린트에서 복제할 변수를 만듭니다. 플레이어의 Health나 적의 CurrentAmmo 같은 값입니다. 그다음 변수를 선택하고 디테일 패널의 Replication 섹션에서 Replication 드롭다운을 Replicated로 바꿉니다. 이제 서버에서 이 변수를 바꾸면 클라이언트에 있는 같은 액터 인스턴스의 변수도 갱신됩니다.

여기서 전달되는 것은 상태입니다. 서버에서 변수에 값을 다섯 번 대입했다고 클라이언트가 다섯 번 모두 받는 것은 아니며, 클라이언트는 복제가 일어날 때의 최신 값을 받습니다. 여러 변수를 한꺼번에 바꿨다고 클라이언트에서도 같은 순간에 모두 바뀐다는 보장도 없습니다. 그래서 "몇 번 바뀌었는가"가 중요한 정보라면 변수 복제만으로는 부족하고, 뒤에서 볼 이벤트 설계가 따로 필요합니다.

값이 복제된 뒤 특정 로직을 실행하고 싶다면 RepNotify를 씁니다. Replication 드롭다운에서 RepNotify를 고르면 OnRep_Health 같은 알림 함수가 만들어지고, 복제된 새 값이 클라이언트에 도착하면 이 함수가 호출됩니다. 알림 함수에서는 체력 UI처럼 현재 값에 맞는 표시를 갱신합니다. 블루프린트에서는 공식 문서에 따르면 복제 변수의 Set 노드가 그 변수의 RepNotify 함수를 자동으로 호출하므로, 서버에서 Set으로 대입한 경우에도 같은 알림 경로를 탑니다. 반대로 블루프린트 매크로나, 변수를 참조로 받아 값을 바꾸는 함수는 RepNotify를 호출하지 않습니다. 이 동작은 블루프린트의 것이며, C++에서 값을 대입할 때도 똑같이 자동 호출된다고 일반화하지 않습니다.

RepNotify는 현재 값에 맞춰 화면을 고치는 데 알맞습니다. 피격 한 번마다 반드시 효과음을 재생해야 하는 것처럼 횟수가 중요한 일은, 상태 복제와 별개의 이벤트로 설계합니다.


커스텀 이벤트로 보내는 RPC

특정 함수를 서버에서 실행해 달라고 클라이언트가 요청하거나, 서버가 클라이언트에 특정 동작을 실행하라고 지시할 수도 있습니다. 이를 원격 프로시저 호출(Remote Procedure Call, RPC)이라고 합니다. 블루프린트에서는 일반 함수가 아니라 커스텀 이벤트(Custom Event)의 디테일 패널에서 Replicates 항목을 설정해 RPC를 만들며, 이 글에서 "RPC를 호출한다"는 말은 그렇게 설정한 커스텀 이벤트를 호출한다는 뜻입니다. 설정할 수 있는 실행 위치는 세 가지입니다.

Run on Server는 클라이언트의 공격 요청처럼 서버의 판단이 필요한 동작을 서버로 보냅니다. 클라이언트에서 이 이벤트를 보내려면 호출하는 액터가 그 클라이언트의 연결에 소유되어 있어야 합니다. 그래서 월드에 놓인 문처럼 플레이어가 소유하지 않은 액터는 클라이언트가 직접 서버 RPC를 보낼 수 없고, 자신의 플레이어 컨트롤러나 캐릭터의 서버 RPC로 문 참조를 보낸 뒤 서버에서 거리와 권한을 검증합니다. 서버 자신이 이 이벤트를 호출할 수도 있습니다.

Multicast는 서버에서 호출하면 서버와, 그 액터가 지금 관련 있는 클라이언트들에서 실행됩니다. 클라이언트에서 호출하면 그 클라이언트에서만 로컬로 실행되고 다른 기기로는 전해지지 않습니다. 또 Multicast는 늦게 접속한 플레이어에게 지난 효과를 다시 재생해 주는 저장소가 아니므로, 계속 유지되어야 하는 상태는 복제 변수로 나타냅니다.

Run on Owning Client는 서버가 액터를 소유한 클라이언트 한 곳에만 개별 피드백을 보낼 때 씁니다. 이때는 그 액터의 소유 연결이 올바른지 확인해야 합니다. 서버가 소유했거나 소유 연결이 없는 액터에서는 서버에서 로컬로 실행될 수 있고, 클라이언트에서 호출하면 서버를 거쳐 전송되는 요청이 되지 않습니다.


액터를 복제하도록 설정하기

서버에서 스폰한 아이템, 투사체, 적 AI 같은 액터는 관련 있는 클라이언트에 복제되도록 설정할 수 있습니다. 이 설정은 서버에서 만든 액터를 클라이언트에 보내는 것이며, 클라이언트가 로컬에서 스폰한 액터를 서버로 올려 주지는 않습니다.

복제할 액터 블루프린트, 예를 들어 BP_PickupItem을 열고 Class Defaults를 누른 뒤 디테일 패널의 Replication 섹션을 찾습니다. 여기서 세 항목을 봅니다. Replicates는 액터의 복제를 허용합니다. 다만 이 옵션이 액터의 모든 컴포넌트와 그 변수까지 자동으로 복제하는 것은 아니므로, 컴포넌트와 변수마다 복제를 지원하는지와 설정을 따로 확인합니다. Replicate Movement는 루트 컴포넌트의 위치, 회전, 속도 같은 이동 상태를 복제합니다. 자식 컴포넌트의 변환이나 임의의 스케일 변경까지 이 옵션 하나로 맞춰지지는 않습니다. Net Update Frequency는 이 액터를 복제 대상으로 얼마나 자주 검토할지에 영향을 줍니다. 실제로 전송되는 횟수는 값이 바뀌었는지, 관련성이 있는지, 대역폭이 남는지에도 달려 있습니다.

더 깊이: 서버는 어떤 클라이언트에 액터를 보낼지 어떻게 정할까

관련성은 클라이언트마다, 네트워크 용어로는 연결(connection)마다 따로 판정됩니다. 액터 관련성 문서에 따르면 기본 AActor는 몇 단계로 판정합니다. 항상 관련 있음(Always Relevant)이 켜져 있거나, 그 연결의 폰이나 플레이어 컨트롤러가 소유한 액터이거나, 그 연결의 폰 자신이거나, 그 폰이 소음이나 피해 같은 행동을 일으킨 경우에는 관련 있는 것으로 봅니다. 소유자의 관련성을 따르도록 설정한 액터는 소유자의 판정을 그대로 쓰고, 소유자에게만 관련 있도록 설정한 액터는 소유자가 없거나 소유자가 관련 없으면 보내지 않습니다. 또 숨겨져 있으면서 루트 컴포넌트가 없거나 그 충돌이 꺼져 있는 액터는 관련 없는 것으로 보고, 거리 기반 관련성을 쓴다면 클라이언트의 시점에서 정해진 거리 안에 있는 액터를 관련 있는 것으로 봅니다.

여기서 놓치기 쉬운 점이 두 가지 있습니다. 첫째, 그 거리를 정하는 Net Cull Distance Squared는 이름 그대로 거리의 제곱입니다. 50m, 즉 5000cm 안에서만 복제하려면 5000² = 25,000,000을 넣어야 합니다. 둘째, 게임 중에 스폰해 복제한 액터는 어떤 클라이언트에게 관련이 없어지면 그 클라이언트에서 파괴됩니다. 그래서 클라이언트가 그 액터에 따로 들고 있던 로컬 상태는 멀리 갔다가 돌아오면 남아 있지 않을 수 있으므로, 다시 보였을 때도 맞아야 하는 상태는 서버의 복제 변수로 둡니다. 폰과 플레이어 컨트롤러는 이 판정을 자기 방식으로 바꿔 두었으므로, 여기서 본 규칙이 모든 클래스에 그대로 적용되지는 않습니다.


권한, 신뢰성, 소유권

언리얼 엔진의 멀티플레이어는 서버-클라이언트 모델이므로, 그래프를 짤 때마다 네 가지 규칙을 염두에 둡니다.

첫째는 서버의 권한(authority)입니다. 점수나 인벤토리 같은 최종 게임 상태는 서버에서 검증하고 결정합니다. 그렇다고 클라이언트의 이동 예측이나 로컬 시각 효과까지 모두 서버에서만 계산한다는 뜻은 아닙니다. 권한을 확인하는 Has Authority 노드는 그 액터에 대한 권한을 검사하므로, 클라이언트에서만 만든 비복제 액터에서도 참이 될 수 있습니다. 그래서 Has Authority가 참이라고 지금 서버에서 실행 중이라고 단정하지 않습니다.

둘째는 Reliable RPC입니다. RPC 이벤트의 Reliable을 켜면, 연결이 살아 있는 동안 상대가 받았다고 확인할 때까지 다시 보냅니다. 그러나 연결이 끊긴 뒤에도 언젠가 전달되는 것은 아니고, 서로 다른 액터의 RPC 사이에서 전체 실행 순서를 맞춰 주지도 않습니다. 자주 호출하는 RPC를 무조건 Reliable로 만들면 지연과 대기열 부담이 커집니다.

셋째는 Unreliable RPC입니다. 전달 중에 사라질 수 있는 RPC로, 이전 효과 하나쯤 빠져도 괜찮은 순간적인 피드백에 씁니다. 여러 번의 호출을 자동으로 최신 호출 하나로 합쳐 주는 기능은 아닙니다.

넷째는 소유권(ownership)입니다. 플레이어가 빙의한 폰의 RPC는 그 플레이어 컨트롤러의 소유 연결을 거쳐 전달됩니다. AI 컨트롤러는 서버에서 동작합니다. 소유권은 RPC가 어느 경로로 가는지를 정할 뿐, 요청 내용이 타당한지 검사하는 일을 대신하지 않습니다.


서버가 판단하는 아이템 줍기

이제 플레이어가 아이템에 닿으면 그 아이템이 모든 클라이언트의 월드에서 사라지고, 주운 플레이어의 인벤토리에 추가되는 로직을 만들어 보겠습니다.

서버의 획득 판단과 두 결과

서버 Overlap→검사/획득 잠금→위치 저장·인벤토리/아이템 갱신→상태 복제와 순간 효과라는 실행/자원 변화이다. 상태/RPC 도착 순서는 보장하지 않는다.

SERVER VALIDATION · TWO RESULT TYPES

획득은 서버가 확정하고 상태와 효과로 반영합니다

이 예제는 서버에서 발생한 Overlap을 출발점으로 삼습니다. 캐릭터·아이템의 유효성, 거리, 획득 여부, 인벤토리 공간을 검사한 뒤 중복 획득을 잠급니다.

서버의 획득 판단과 두 결과서버의 Overlap에서 캐릭터를 확인하고 TryPickupItem 검사를 통과하면 인벤토리와 아이템 상태를 갱신한다. 저장한 획득 위치는 별도로 순간 효과 RPC에 사용한다.서버 Overlap캐릭터 확인TryPickupItem거리·상태·공간 확인통과 → 획득 잠금인벤토리 갱신아이템 파괴저장한 획득 위치캐릭터 Multicast지속할 게임 상태순간적인 표현
서버획득검사와두결과서버Overlap에서TryPickup검사를통과하면확정인벤토리/아이템상태와별도의순간효과로갈라진다. 두갈래는네트워크도착순서나동시성을보장하지않는다.서버 Overlap캐릭터 확인TryPickupItem거리·상태·공간 검사통과 → 획득 잠금지속할 게임 상태인벤토리 갱신아이템 파괴순간적인 표현저장한 획득 위치캐릭터 Multicast

파괴 전에 위치를 보관합니다

획득 위치를 먼저 값으로 복사합니다. 인벤토리를 갱신하고 아이템을 파괴한 뒤에도 이 값으로 효과를 재생할 수 있습니다. 아이템 참조를 파괴 후 다시 읽지 않습니다.

순간 효과는 저장 이력이 아닙니다

복제되는 캐릭터에서 서버가 Multicast를 호출하면 관련 있는 현재 클라이언트에 효과를 전달합니다. 늦게 들어온 플레이어의 상태는 복제 변수로 맞춥니다.

갈라진 화살표는 확정한 획득의 두 결과를 구분합니다. 두 경로의 동시 실행이나 네트워크 도착 순서를 보장하는 표시가 아닙니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
도표 제목서버 획득 검사와 두 결과
관계·조건 설명서버 Overlap에서 TryPickupItem 검사를 통과하면 확정된 인벤토리/아이템 상태와 별도의 순간 효과로 갈라진다. 두 갈래는 네트워크 도착 순서나 동시성을 보장하지 않는다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
서버 검사

서버 Overlap에서 획득 조건과 잠금을 검사한다.

  • 권한: 서버
  • 획득: 중복 방지 잠금
  • 클라이언트 표시만으로 상태 획득을 확정하지 않는다.
상태 갱신

효과 위치를 보관하고 인벤토리와 아이템을 바꾼다.

  • 인벤토리: 획득 반영
  • 아이템: 획득 처리
  • 위치가 필요한 순간 효과를 아이템 정리 전에 준비한다.
상태·효과 전달

복제 상태와 순간 효과를 각각 전달한다.

  • 지속 결과: 상태 복제
  • 순간 결과: 피드백 효과
  • 두 전달 경로의 도착 순서를 보장하지 않는다.

서버의 획득 판단과 두 결과

서버 Overlap→검사/획득 잠금→위치 저장·인벤토리/아이템 갱신→상태 복제와 순간 효과라는 실행/자원 변화이다. 상태/RPC 도착 순서는 보장하지 않는다.

SERVER VALIDATION · TWO RESULT TYPES

획득은 서버가 확정하고 상태와 효과로 반영합니다

이 예제는 서버에서 발생한 Overlap을 출발점으로 삼습니다. 캐릭터·아이템의 유효성, 거리, 획득 여부, 인벤토리 공간을 검사한 뒤 중복 획득을 잠급니다.

서버의 획득 판단과 두 결과서버의 Overlap에서 캐릭터를 확인하고 TryPickupItem 검사를 통과하면 인벤토리와 아이템 상태를 갱신한다. 저장한 획득 위치는 별도로 순간 효과 RPC에 사용한다.서버 Overlap캐릭터 확인TryPickupItem거리·상태·공간 확인통과 → 획득 잠금인벤토리 갱신아이템 파괴저장한 획득 위치캐릭터 Multicast지속할 게임 상태순간적인 표현
서버획득검사와두결과서버Overlap에서TryPickup검사를통과하면확정인벤토리/아이템상태와별도의순간효과로갈라진다. 두갈래는네트워크도착순서나동시성을보장하지않는다.서버 Overlap캐릭터 확인TryPickupItem거리·상태·공간 검사통과 → 획득 잠금지속할 게임 상태인벤토리 갱신아이템 파괴순간적인 표현저장한 획득 위치캐릭터 Multicast

파괴 전에 위치를 보관합니다

획득 위치를 먼저 값으로 복사합니다. 인벤토리를 갱신하고 아이템을 파괴한 뒤에도 이 값으로 효과를 재생할 수 있습니다. 아이템 참조를 파괴 후 다시 읽지 않습니다.

순간 효과는 저장 이력이 아닙니다

복제되는 캐릭터에서 서버가 Multicast를 호출하면 관련 있는 현재 클라이언트에 효과를 전달합니다. 늦게 들어온 플레이어의 상태는 복제 변수로 맞춥니다.

갈라진 화살표는 확정한 획득의 두 결과를 구분합니다. 두 경로의 동시 실행이나 네트워크 도착 순서를 보장하는 표시가 아닙니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
도표 제목서버 획득 검사와 두 결과
관계·조건 설명서버 Overlap에서 TryPickupItem 검사를 통과하면 확정된 인벤토리/아이템 상태와 별도의 순간 효과로 갈라진다. 두 갈래는 네트워크 도착 순서나 동시성을 보장하지 않는다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
서버 검사

서버 Overlap에서 획득 조건과 잠금을 검사한다.

  • 권한: 서버
  • 획득: 중복 방지 잠금
  • 클라이언트 표시만으로 상태 획득을 확정하지 않는다.
상태 갱신

효과 위치를 보관하고 인벤토리와 아이템을 바꾼다.

  • 인벤토리: 획득 반영
  • 아이템: 획득 처리
  • 위치가 필요한 순간 효과를 아이템 정리 전에 준비한다.
상태·효과 전달

복제 상태와 순간 효과를 각각 전달한다.

  • 지속 결과: 상태 복제
  • 순간 결과: 피드백 효과
  • 두 전달 경로의 도착 순서를 보장하지 않는다.

위 그림은 서버가 획득을 확정하고, 그 결과를 지속할 게임 상태와 순간적인 표현이라는 두 갈래로 내보내는 흐름입니다. 출발점은 서버에서 일어난 Overlap입니다. 서버는 캐릭터를 확인하고 TryPickupItem에서 거리, 획득 여부, 인벤토리 공간을 검사하며, 통과하면 획득을 잠근 뒤 인벤토리를 갱신하고 아이템을 파괴합니다. 이와 별도로 파괴하기 전에 저장한 획득 위치는 캐릭터의 Multicast로 넘어가 순간 효과를 재생하는 데 쓰입니다.

그림의 두 주석은 각 갈래의 주의점을 짚습니다. 획득 위치는 먼저 값으로 복사해 두므로 아이템을 파괴한 뒤에도 효과를 재생할 수 있으며, 파괴한 아이템의 참조를 다시 읽지 않습니다. 또 순간 효과는 저장된 이력이 아니라서, Multicast는 지금 관련 있는 클라이언트에만 효과를 보내고 늦게 들어온 플레이어의 상태는 복제 변수로 맞춥니다. 갈라진 화살표는 확정된 획득의 두 결과를 구분할 뿐, 두 경로가 동시에 실행되거나 네트워크에 특정 순서로 도착한다는 표시가 아닙니다.

단계 그림은 이 흐름을 세 단계로 나눕니다. 서버 검사 단계에서는 서버의 Overlap에서 획득 조건과 잠금을 검사합니다. 상태 갱신 단계에서는 효과에 쓸 위치를 보관하고 인벤토리와 아이템을 바꿉니다. 마지막 상태와 효과 전달 단계에서는 복제되는 상태와 순간 효과를 각각의 경로로 클라이언트에 보냅니다.

서버의 Overlap에서 시작하기

먼저 BP_PickupItem의 Replicates를 켜고, 아이템이 움직인다면 Replicate Movement도 켭니다. 아이템과 캐릭터의 콜리전 컴포넌트가 서로 Overlap 이벤트를 내도록 콜리전도 설정합니다. On Component Begin Overlap에서는 Other Actor를 BP_PlayerCharacter로 캐스트하고, 서버가 스폰한 이 아이템의 Has Authority를 검사합니다. 권한이 있는 서버 경로에서만 캐릭터의 일반 함수 TryPickupItem에 아이템 참조를 넘깁니다. 이미 서버에서 시작한 흐름이므로 여기서 서버 RPC를 한 번 더 보낼 필요는 없습니다.

서버에서 검사하고 상태 바꾸기

서버의 TryPickupItem은 먼저 아이템 참조가 유효하고 아직 획득되지 않았는지, 캐릭터와 아이템의 거리가 허용 범위인지, 인벤토리에 공간이 있는지를 검사합니다. 검사를 통과하면 가장 먼저 아이템의 획득 중 플래그를 켜서, 같은 아이템에 대한 다음 요청이 이 아이템을 다시 가져가지 못하게 합니다. 그다음 Delay 같은 잠복 노드 없이 인벤토리 갱신과 아이템 파괴를 이어서 처리하고, 검사에 실패한 경로에서는 아무 상태도 바꾸지 않습니다.

아이템을 파괴하기 전에 아이템의 위치를 변수에 저장해 둡니다. 인벤토리는 서버의 데이터로 관리하고 필요한 소유 클라이언트에 복제하며, UI는 복제된 그 상태를 받아 갱신합니다. 마지막으로 서버에서 캐릭터의 Multicast_PlayPickupEffect를 저장한 위치와 함께 호출합니다.

모든 화면에 효과 보내기

Multicast_PlayPickupEffect는 Replicates를 Multicast로 설정한 커스텀 이벤트이며, 전달받은 위치에서 Play Sound at Location과 시각 효과를 재생합니다. 이미 파괴한 아이템의 참조에서 위치를 다시 읽지 않는 것이 핵심입니다. 화면과 스피커가 없는 데디케이티드 서버(dedicated server)는 시각, 청각 효과를 건너뛰도록 만들고, 효과를 받는 쪽은 그 캐릭터가 관련 있는 클라이언트들입니다. 획득했다는 상태 자체는 이 순간 효과에 기대지 않으므로, 효과가 일부 클라이언트에서 빠져도 인벤토리는 맞게 유지됩니다.

버튼을 눌러 줍기를 요청하는 변형도 같은 검사를 거칩니다. 소유한 캐릭터에 Server_PickupItem 커스텀 이벤트를 Run on Server로 두고, 이 이벤트가 서버에서 같은 TryPickupItem을 호출합니다. 클라이언트가 보낸 아이템 참조만 믿고 인벤토리를 바로 바꾸지 않는다는 점이 중요합니다. 이렇게 서버가 게임 로직을 판단하고, 필요한 상태와 피드백만 클라이언트에 보내면 멀티플레이어에서도 모든 플레이어가 같은 결과를 보게 됩니다.


여러 클라이언트로 시험하고 디버깅하기

네트워크 그래프는 에디터에서 여러 클라이언트를 띄워 시험합니다. 툴바의 Play 버튼 옆 드롭다운에서 Net Mode를 Play As Listen Server나 Play As Client로 바꾸고, Number of Players를 늘리면 여러 클라이언트를 동시에 실행할 수 있습니다. 플레이 중에 콘솔 키(기본값은 ~)를 눌러 콘솔을 열고 stat net을 입력하면 네트워크 대역폭 사용량, 패킷 손실률 같은 통계를 볼 수 있습니다.

그래프가 어느 인스턴스에서 실행됐는지 헷갈릴 때는, RPC의 진입점과 RepNotify 함수에서 액터 이름, 권한, 소유 여부를 함께 Print String으로 출력합니다. 서버와 클라이언트가 같은 이름의 액터를 각자 가지고 있으므로, 이렇게 구분하지 않으면 어떤 출력이 어느 쪽의 것인지 알 수 없습니다. 패킷 단위로 더 자세히 보려면 Unreal Insights의 Networking Insights로 캡처를 남겨 분석합니다.

게임플레이 능력 시스템(GAS)을 쓰더라도 이 원칙은 같습니다. 능력을 실행하는 권한, 비용과 쿨다운의 적용, 예측을 보정하는 흐름은 게임플레이 능력 시스템 구현의 멀티플레이어 실행 흐름과 함께 보면 구현 실수를 줄일 수 있습니다.


정리

  • 리플리케이션은 서버가 가진 확정 상태와 이벤트를 관련성, 복제 조건, 갱신 일정에 따라 클라이언트에 맞추는 과정입니다.
  • 변수는 Replication을 Replicated나 RepNotify로 설정하고 서버에서 바꿉니다. 클라이언트는 그때의 최신 상태를 받으며, 블루프린트의 Set 노드는 RepNotify를 자동으로 호출합니다.
  • RPC는 커스텀 이벤트의 Replicates로 만들며, Run on Server는 소유한 액터에서만 클라이언트가 보낼 수 있고, Multicast는 서버에서 호출해야 퍼지며, Run on Owning Client는 소유 연결을 확인해야 합니다.
  • 액터의 Replicates는 서버에서 스폰한 액터를 클라이언트에 보내지만 모든 컴포넌트와 변수를 자동으로 복제하지는 않습니다.
  • 아이템 줍기 같은 확정 상태는 서버에서 검사하고 잠근 뒤 바꾸고, 순간 효과는 별도의 Multicast로 보내며 상태가 효과에 기대지 않게 합니다.

연습 문제

1. 같은 아이템에 들어온 두 요청

A와 B의 인벤토리는 최대 2칸이고 각각 1칸을 쓰고 있습니다. 아직 아무도 줍지 않은 아이템 하나에 두 사람의 요청이 서버로 들어왔고, 두 요청 모두 거리 같은 검사는 통과할 수 있습니다. 서버가 A의 요청을 먼저 처리하며, 본문처럼 획득 플래그 설정, 인벤토리 갱신, 파괴 사이에 잠복 노드나 비동기 대기를 넣지 않았다고 가정합니다. B의 요청까지 처리된 뒤 A와 B가 쓰는 칸 수는 각각 몇일까요? A가 순간 획득 효과를 받지 못했다면 확정된 인벤토리도 되돌려야 할까요?

해설 보기

A는 2칸, B는 1칸입니다. A의 요청이 성공할 때 획득 중 상태를 먼저 표시하고 아이템을 파괴하므로, 뒤이은 B의 요청은 이미 획득했거나 유효하지 않은 아이템이라 거부됩니다. 이 문제는 서버가 A를 먼저 처리한다고 정해 둔 것이며, 실제로 거의 동시에 도착한 요청에서 누가 먼저 처리될지를 미리 알 수 있다는 뜻은 아닙니다.

효과를 받지 못한 것은 표현 경로의 문제일 뿐이므로 인벤토리를 되돌리지 않습니다. A의 화면은 인벤토리 2칸이라는 확정 상태를 복제로 받아 UI를 맞추고, 지난 Multicast 효과를 받았는지를 획득 성공의 기준으로 쓰지 않습니다. 중간에 비동기 처리를 넣는다면 예약, 취소, 실패 복구를 따로 설계해야 합니다.

2. 클라이언트에서 호출한 Multicast

어떤 개발자가 클라이언트의 입력 이벤트에서 바로 Multicast_PlayPickupEffect를 호출했습니다. 다른 플레이어들의 화면에도 효과가 보일까요? 효과를 모두에게 보여 주려면 어떻게 바꿔야 할까요?

해설 보기

보이지 않습니다. Multicast를 클라이언트에서 호출하면 그 클라이언트에서만 로컬로 실행되고 다른 기기로 전해지지 않으므로, 효과는 호출한 플레이어의 화면에만 나타납니다.

클라이언트는 먼저 소유한 캐릭터의 Run on Server 이벤트로 서버에 요청하고, 서버가 검사를 마친 뒤 Multicast를 호출하도록 바꿉니다. 그러면 서버와 그 캐릭터가 관련 있는 클라이언트들에서 효과가 재생됩니다.

3. 문 액터의 서버 RPC

월드에 배치된 BP_Door에 Server_Open 이벤트를 Run on Server로 만들고, 클라이언트에서 플레이어가 문을 바라보고 키를 누르면 이 이벤트를 호출했습니다. 서버에서 문이 열리지 않는다면 이유는 무엇일까요?

해설 보기

클라이언트가 서버 RPC를 보내려면 호출하는 액터가 그 클라이언트의 연결에 소유되어 있어야 하는데, 월드에 배치된 문은 플레이어가 소유하지 않기 때문입니다. 그래서 이 호출은 서버로 전해지지 않습니다.

플레이어가 소유한 플레이어 컨트롤러나 캐릭터에 서버 RPC를 두고, 그 RPC로 문 참조를 보냅니다. 서버는 받은 참조가 유효한지, 플레이어가 문과 충분히 가까운지, 열 권한이 있는지를 검사한 뒤 문을 엽니다. 문이 열린 상태는 복제 변수로 두어 늦게 들어온 플레이어에게도 맞게 보이게 합니다.

블루프린트에서 복제 변수의 Set 노드가 RepNotify를 호출하는 규칙은 언리얼 엔진 공식 문서의 액터 속성 복제에서 확인할 수 있습니다.

다음 글 체크포인트와 세이브 설계에서는 플레이어의 진행 상황을 체크포인트로 기록하고, 게임을 껐다 켜도 이어 할 수 있게 저장하는 구조를 설계합니다.