언리얼 멀티플레이(3) - Remote Procedure Call

yys·2026년 6월 9일

TIL

목록 보기
56/80

RPC(Remote Procedure Call)


지금까지의 복제(Replication) 는 서버가 가진 변수의 상태를 클라이언트로 내려보내는 서버 → 클라 한 방향 동기화였다. 하지만 게임을 만들다 보면 그 반대 방향, 즉 클라이언트가 서버에게 무언가를 요청하거나 서버가 특정 클라이언트에게만 명령을 내려야 하는 순간이 온다. 이때 사용하는 것이 RPC(Remote Procedure Call, 원격 프로시저 호출) 다.

  • RPC = "내가 호출하면 다른 머신에서 실행되는 함수"
  • 복제가 상태(데이터) 를 동기화한다면, RPC는 함수 호출 자체를 네트워크 너머로 전송한다
  • 다른 말로 Replicated Function(복제되는 함수)이라고도 부른다

RPC는 다음 3가지 종류가 존재한다.

종류호출하는 곳 → 실행되는 곳용도
Client서버 → owning 클라이언트서버가 특정 클라에게만 명령
Server클라이언트 → 서버클라가 서버에게 요청
NetMulticast서버 → 서버 + 모든 클라서버가 전체에 방송

공통 작성 규칙


① UFUNCTION 으로 선언하기

// Client / Server / NetMulticast 중 하나 + Reliable / Unreliable 중 하나
UFUNCTION(Client, Reliable)
void Client_Print(const FString& Message);
  • RPC는 입력 파라미터로 데이터를 받을 수 있고, 그 데이터가 네트워크를 타고 전송
  • 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로 붙인다.

Client RPC


서버에서 함수를 호출하면 그 액터를 소유한 클라이언트 에서 실행되는 RPC다. 즉, 서버가 "특정 플레이어 한 명에게만" 피드백이나 연출을 지시할 때 사용한다.

예시

  • UI/HUD: 크로스헤어에 피격 표시, 인벤토리 부족 경고 메시지 띄우기
  • 사운드: 퀘스트 완료 알림음
// .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이기 때문에 자신 클라에서 호출된다.

Server RPC — 클라에서 호출, 서버에서 실행


클라이언트에서 함수를 호출하고 서버에서 실행되게 하는 RPC다. 클라이언트가 서버에게 권한(Authority)이 필요한 중요한 행동을 허락해 달라고 요청할 때 사용된다.
언리얼은 서버 권위 모델이기 때문에 클라가 자기 로컬에서 값을 바꿔도 서버에는 반영되지 않는다. 따라서 "클라가 원하는 행동을 서버에게 요청"하는 유일한 채널이 바로 Server RPC다.

예시

  • 전투: 무기 발사(탄약 감소 및 데미지 판정 요청), 스킬 사용, 수류탄 투척
  • 상호작용: 바닥에 떨어진 아이템 줍기, 상점 NPC와 거래하기, 문 열기
// .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
	);
}

이후 블루프린트에서 간단하게 함수 호출을 해주었다.

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

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

OnRep_Owner 로 안전한 호출 타이밍 잡기

액터에 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();
}

owner가 없으면?

액터를 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);

NetMulticast RPC — 서버에서 호출, 모두에서 실행


서버에서 함수를 호출하면 서버 + 모든 relevant 클라이언트에서 실행되는 RPC다.
서버가 접속해 있는 모든 유저에게 일회성 시각적/청각적 이벤트를 동시다발적으로 발생시킬 때 사용한다.

예시

  • VFX: 수류탄 폭발 파티클 생성, 총구 화염 표시
// .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의 두 가지 핵심 특징이 있다.

  • owning connection과 무관하다 — 오직 net relevancy(관련성) 기준으로 퍼진다. relevant한 클라 전부에서 실행된다
  • 반드시 서버에서 호출해야 한다 — 클라에서 부르면 그 클라에서만 실행되어 multicast의 의미가 없다.

Listen Server vs Dedicated Server 결과 차이

테스트 시 Net Mode에 따라 출력 개수가 달라진다.

  • Listen Server (2P): 캐릭터 2명 → 서버 2개 + 클라 2개 = 메시지 4개
  • Play as Client (Dedicated Server 시뮬, 2P): 캐릭터가 2명 → 2 x (서버 1개 + 각 클라 2개) = 메시지 6개

아래는 Listen Server 기준 Multicast를 수행한 로그이다.

RPC Validation


멀티플레이어 프로그래머가 반드시 신경 써야 할 것이 치팅(cheating) 이다. 가장 흔한 수법은 메모리 주소를 뒤져 변수 값을 조작하는 것이다. 예를 들어 화면에 체력 120 이 보이면, 메모리에서 그 값을 찾아 10000 으로 바꿔버린다고 가정했을 때 게임이 이를 처리하도록 짜여 있지 않으면 그대로 치팅이 통한다.

클라이언트가 서버로 보낸 데이터를 검증 해서, 비정상적인 값이면 플레이어를 kick 할 수 있다. 주로 Server RPC에 사용하지만 어느 RPC에든 적용 가능하다.

① UFUNCTION 에 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).

Relevancy & Priority


Relevancy (관련성)

액터가 어떤 net connection에 relevant 하다는 것은, 그 클라이언트가 해당 액터의 net update(복제)를 받는다는 뜻이다. 다수가 참가하는 게임에서 내가 지하에 있어 아무도 안 보인다면, 나머지 인원의 변수를 굳이 내 머신으로 복제할 이유가 없다. 언리얼은 relevant한 액터만 업데이트해서 대역폭을 절약한다.

관련성은 IsNetRelevantFor() virtual 함수가 아래 규칙을 순서대로 적용해 판정한다.

순서규칙결과
1bAlwaysRelevant = true 거나, owner가 Pawn/PlayerController, 또는 Pawn이 instigatorrelevant
2bNetUseOwnerRelevancy = true 이고 owner 있음owner의 relevancy 사용
3bOnlyRelevantToOwner = true 인데 1번 통과 못 함not relevant
4다른 액터의 스켈레톤에 attach됨base의 relevancy 따름
5bHidden = true 이고 root component 충돌 없음not relevant
6root component가 없음not relevant
7distance 기반 relevancy (기본값)net cull distance보다 가까우면 relevant

Priority (우선순위)

언리얼은 대역폭을 Load Balancing 으로 나눠준다. 각 액터는 NetPriority(float, 기본값 1.0) 를 가지며, 이 값이 클수록 다른 액터 대비 더 많은 대역폭을 받는다.

  • priority가 2 인 액터는 priority 1 인 액터보다 정확히 2배 자주 업데이트된다.
  • 비율(ratio)만 의미가 있기 때문에, 모든 액터의 priority를 똑같이 올리면 비율이 그대로라 아무 효과가 없다.
  • 실제 우선순위는 GetNetPriority() 에서 계산되며 priority × 마지막 복제 후 경과 시간 에 더해, 액터와 viewer 사이의 상대 위치·거리도 고려한다.

참고 자료
언리얼 RPC 문서: https://dev.epicgames.com/documentation/unreal-engine/remote-procedure-calls-in-unreal-engine?application_version=5.7

profile
게임 개발 지망생

0개의 댓글