지금까지의 복제(Replication) 는 서버가 가진 변수의 상태를 클라이언트로 내려보내는 서버 → 클라 한 방향 동기화였다. 하지만 게임을 만들다 보면 그 반대 방향, 즉 클라이언트가 서버에게 무언가를 요청하거나 서버가 특정 클라이언트에게만 명령을 내려야 하는 순간이 온다. 이때 사용하는 것이 RPC(Remote Procedure Call, 원격 프로시저 호출) 다.
RPC는 다음 3가지 종류가 존재한다.
| 종류 | 호출하는 곳 → 실행되는 곳 | 용도 |
|---|---|---|
| Client | 서버 → owning 클라이언트 | 서버가 특정 클라에게만 명령 |
| Server | 클라이언트 → 서버 | 클라가 서버에게 요청 |
| NetMulticast | 서버 → 서버 + 모든 클라 | 서버가 전체에 방송 |
// Client / Server / NetMulticast 중 하나 + Reliable / Unreliable 중 하나
UFUNCTION(Client, Reliable)
void Client_Print(const FString& Message);
Reliable = 대상 머신에서 실행이 보장됨 (전송 보장)Unreliable = 전송 중 유실될 수 있음 — 매 프레임 보내는 사소한 값(움직임 등)에 사용Reliable / Unreliable 중 하나는 반드시 명시해야 함 (빠뜨리면 컴파일 에러)_Implementation 을 붙여 구현// 선언은 Client_Print, 실제 본문은 _Implementation 을 붙인다
void ATestProjectCharacter::Client_Print_Implementation(const FString& Message)
{
// HasAuthority() 로 지금 실행되는 머신이 서버인지 클라인지 구분
FString MessageString = HasAuthority() ? TEXT("Server: ") : TEXT("Client: ");
MessageString += Message;
GEngine->AddOnScreenDebugMessage(-1, 30.f, FColor::Yellow, MessageString);
}
_Implementation 없이Client_PrintMessage(TEXT("Test")); // _Implementation 안 붙임
엔진이 알아서 네트워크로 라우팅한 뒤 대상 머신에서 _Implementation 을 실행한다. 거꾸로 _Implementation 을 직접 호출하면 네트워크 라우팅을 건너뛰고 로컬에서 그냥 실행되어 RPC가 동작하지 않는다.
위 3가지만 하면 RPC로 동작은 하지만 관례상 Client_Print, Server_Print, Multicast_Print 처럼 함수명 앞에 종류를 prefix로 붙인다.
서버에서 함수를 호출하면 그 액터를 소유한 클라이언트 에서 실행되는 RPC다. 즉, 서버가 "특정 플레이어 한 명에게만" 피드백이나 연출을 지시할 때 사용한다.
예시
// .h
FTimerHandle DelayTimer;
// .cpp
void ATestProjectCharacter::BeginPlay()
{
Super::BeginPlay();
GetWorldTimerManager().SetTimer(DelayTimerHandle, this, &ATestProjectCharacter::OnDelayTimerFinished, 5.f, false);
}
void ATestProjectCharacter::OnRPCDelayTimer()
{
if (HasAuthority())
{
Client_Print(TEXT("Test"));
}
}
void ATestProjectCharacter::Client_Print_Implementation(const FString& Message)
{
FString MessageString = HasAuthority() ? "Server: " : "Client: ";
MessageString += Message;
GEngine->AddOnScreenDebugMessage(
-1,
5.f,
FColor::Blue,
MessageString
);
}
위에서 지연을 하는 이유는 BeginPlay()에서 바로 RPC를 진행하게 되면 Owning Connection 세팅이 안된 채로 넘어갈 수 있기 때문이다.
지연 후 다시 테스트하면 한 쪽은 "Server", 다른 한 쪽은 "Client" 로 출력된다. 서버 측 캐릭터는 Server로, 클라 측 캐릭터는 Client로 호출되는 모습이다.
(Listen Server 기준 테스트)

이렇게 호출되는 이유는 다음 표를 보면 알 수 있다.

호스트는 Owning Connection -> None이기 때문에 서버에서 호출, 클라는 Owning Connection -> Owning Client이기 때문에 자신 클라에서 호출된다.
클라이언트에서 함수를 호출하고 서버에서 실행되게 하는 RPC다. 클라이언트가 서버에게 권한(Authority)이 필요한 중요한 행동을 허락해 달라고 요청할 때 사용된다.
언리얼은 서버 권위 모델이기 때문에 클라가 자기 로컬에서 값을 바꿔도 서버에는 반영되지 않는다. 따라서 "클라가 원하는 행동을 서버에게 요청"하는 유일한 채널이 바로 Server RPC다.
예시
// .h
UFUNCTION(BlueprintCallable)
void OnKeyPressed();
UFUNCTION(Server, Reliable)
void Server_Print(const FString& Message);
// .cpp
void ATestProjectCharacter::OnKeyPressed()
{
Server_Print(TEXT("Key Pressed"));
}
void ATestProjectCharacter::Server_Print_Implementation(const FString& Message)
{
FString MessageString = HasAuthority() ? "Server: " : "Client: ";
MessageString += Message;
GEngine->AddOnScreenDebugMessage(
-1,
5.f,
FColor::Blue,
MessageString
);
}
이후 블루프린트에서 간단하게 함수 호출을 해주었다.

결과는 어느 쪽에서 눌러도 서버에서만 실행된다. 클라가 누른 입력이 서버로 전달되어 서버에서 처리된 것이다.

이 동작 또한 표를 참고하면 된다.

액터에 Server RPC를 붙일 때는 호출 타이밍이 중요하다. owner가 복제되어 유효해진 순간에 호출해야 안전하다. 액터의 owner는 복제되는 값이고, 처음 set될 때 rep notify(OnRep_Owner) 가 호출된다. 이 함수는 virtual이라 override할 수 있다.
// .h
UFUNCTION(Server, Reliable)
void Server_PrintActorName();
virtual void OnRep_Owner() override;
// .cpp
void ATestActor::OnRep_Owner()
{
Server_PrintActorName();
}
액터를 spawn할 때 owner를 설정하면 RPC가 정상 실행되지만, owner를 nullptr 로 두면 owning connection이 없어 Server RPC가 그냥 무시(drop) 된다.
// 캐릭터에서 액터를 spawn 하며 owner 설정
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this; // ← 이게 핵심. nullptr 이면 RPC drop
GetWorld()->SpawnActor<AMP_Actor>(
ActorClass, GetActorLocation(), GetActorRotation(), SpawnParams);
서버에서 함수를 호출하면 서버 + 모든 relevant 클라이언트에서 실행되는 RPC다.
서버가 접속해 있는 모든 유저에게 일회성 시각적/청각적 이벤트를 동시다발적으로 발생시킬 때 사용한다.
예시
// .h
UFUNCTION(NetMulticast, Reliable)
void Multicast_Print(const FString& Message);
// .cpp
void ATestProjectCharacter::OnRPCDelayTimer()
{
if (!HasAuthority()) return; // 서버에서만 호출
Multicast_Print(TEXT("Test"));
}
void ATestProjectCharacter::Multicast_Print_Implementation(const FString& Message)
{
FString MessageString = HasAuthority() ? "Server: " : "Client: ";
MessageString += Message;
GEngine->AddOnScreenDebugMessage(
-1,
5.f,
FColor::Blue,
MessageString
);
}
Multicast RPC의 두 가지 핵심 특징이 있다.

테스트 시 Net Mode에 따라 출력 개수가 달라진다.
아래는 Listen Server 기준 Multicast를 수행한 로그이다.

멀티플레이어 프로그래머가 반드시 신경 써야 할 것이 치팅(cheating) 이다. 가장 흔한 수법은 메모리 주소를 뒤져 변수 값을 조작하는 것이다. 예를 들어 화면에 체력 120 이 보이면, 메모리에서 그 값을 찾아 10000 으로 바꿔버린다고 가정했을 때 게임이 이를 처리하도록 짜여 있지 않으면 그대로 치팅이 통한다.
클라이언트가 서버로 보낸 데이터를 검증 해서, 비정상적인 값이면 플레이어를 kick 할 수 있다. 주로 Server RPC에 사용하지만 어느 RPC에든 적용 가능하다.
WithValidation 추가UFUNCTION(Server, Reliable, WithValidation)
void Server_PrintMessage(const FString& Message);
_Validate 함수 구현 (bool 반환)WithValidation 을 추가하면 _Validate 함수 구현이 의무가 된다. 이 함수가 false 를 반환하면 해당 net connection의 클라이언트가 서버에서 kick 된다. 치팅 감지로 간주하기 때문이다.
// .cpp — _Validate 는 bool 반환.
bool ATestProjectCharacter::Server_Print_Validate(const FString& Message)
{
return !Message.IsEmpty(); // 빈 문자열이면 false → 클라 kick
}
입력 파라미터를 꼭 쓸 필요도 없다. 여기서 원하는 어떤 조건이든 검사하고 true / false 를 반환하면 된다.
이렇게 하면 클라에서 일부러 빈 문자열을 서버로 보내고 E 키를 누르면, validation 실패로 클라이언트가 시작 맵으로 튕겨 나간다(kick).
액터가 어떤 net connection에 relevant 하다는 것은, 그 클라이언트가 해당 액터의 net update(복제)를 받는다는 뜻이다. 다수가 참가하는 게임에서 내가 지하에 있어 아무도 안 보인다면, 나머지 인원의 변수를 굳이 내 머신으로 복제할 이유가 없다. 언리얼은 relevant한 액터만 업데이트해서 대역폭을 절약한다.
관련성은 IsNetRelevantFor() virtual 함수가 아래 규칙을 순서대로 적용해 판정한다.
| 순서 | 규칙 | 결과 |
|---|---|---|
| 1 | bAlwaysRelevant = true 거나, owner가 Pawn/PlayerController, 또는 Pawn이 instigator | relevant |
| 2 | bNetUseOwnerRelevancy = true 이고 owner 있음 | owner의 relevancy 사용 |
| 3 | bOnlyRelevantToOwner = true 인데 1번 통과 못 함 | not relevant |
| 4 | 다른 액터의 스켈레톤에 attach됨 | base의 relevancy 따름 |
| 5 | bHidden = true 이고 root component 충돌 없음 | not relevant |
| 6 | root component가 없음 | not relevant |
| 7 | distance 기반 relevancy (기본값) | net cull distance보다 가까우면 relevant |
언리얼은 대역폭을 Load Balancing 으로 나눠준다. 각 액터는 NetPriority(float, 기본값 1.0) 를 가지며, 이 값이 클수록 다른 액터 대비 더 많은 대역폭을 받는다.
GetNetPriority() 에서 계산되며 priority × 마지막 복제 후 경과 시간 에 더해, 액터와 viewer 사이의 상대 위치·거리도 고려한다.참고 자료
언리얼 RPC 문서: https://dev.epicgames.com/documentation/unreal-engine/remote-procedure-calls-in-unreal-engine?application_version=5.7