https://dev.epicgames.com/documentation/ko-kr/unreal-engine/remote-procedure-calls-in-unreal-engine?application_version=5.3

RPC

  • Local 단위에서 호출하여 Remote로 연결된 1개 이상의 Machinge에서 실행되는 함수
  • Return이 없는 단방향 함수 호출이 특징
  • RPC는 주로 일시적이거나, 외형적으로 드러나는 Unreliable Gameplay Event에 사용된다.
    • 사운드 재생
    • Particle 생성
    • Animation 재생
  • RPC는 Replicated/ReplicatedUsing 선언이 된 Property의 Replication을 보완하는 중요한 기능이다.
  • RPC를 호출하려면 다음 2가지 조건이 성립되어야 한다.
    • Actor나 Actor Component일 것.
    • RPC를 호출하는 Object가 Replicate되어 있을 것.
  • 마지막으로 RPC를 잘 사용하기 위해서는 Ownership을 잘 이해하는 편이 좋다.

Type

Client

  • 이 Actor에 대해 Client Connection을 소유한 Client에서 실행되는 Unicast RPC
#pragma once

#include "DerivedActor.generated.h"

UCLASS()
class ADerivedActor : public AActor
{
    GENERATED_BODY()

public:

    // Client RPC Function
    UFUNCTION(Client)
    void ClientRPC();
}
#include "DerivedActor.h"

ADerivedActor::ADerivedActor(const class FPostConstructInitializeProperties & PCIP) : Super(PCIP)
{
    bReplicates = true;
}

void ADerivedActor::ClientRPC_Implementation()
{
    // This log will print on every machine that executes this function.
    UE_LOG(LogTemp, Log, TEXT("ClientRPC executed."))
}
// Call from client to run on server
ADerivedClientActor* MyDerivedClientActor;
MyDerivedClientActor->ClientRPC();

Execution Matrix

Server

  • 해당 Actor를 소유하는 Client에서 호출하여 Server에서 실행되는 Unicast RPC
#pragma once

#include "DerivedActor.generated.h"

UCLASS()
class ADerivedActor : public AActor
{
    GENERATED_BODY()

public:
    // Server RPC Function
    UFUNCTION(Server)
    void ServerRPC();
}
#include "DerivedActor.h"
ADerivedActor::ADerivedActor(const class FPostConstructInitializeProperties & PCIP) : Super(PCIP)
{
    bReplicates = true;
}

void ADerivedActor::ServerRPC_Implementation()
{	
    // This function only executes if ServerRPC_Validate returns true.
    // This log will print on every machine that executes this function.
    UE_LOG(LogTemp, Log, TEXT("ServerRPC executed."))
}
// Call from client to run on server
ADerivedClientActor* MyDerivedClientActor;
MyDerivedClientActor->ServerRPC();

Execution Matrix

WithValidation

  • Server RPC에서만 사용할 수 있는 Specifier
    • Server RPC의 신뢰성과 Network Policy를 구현할 수 있다.
#pragma once
#include "DerivedActor.generated.h"

UCLASS()
class ADerivedActor : public AActor
{
    GENERATED_BODY()

public:

    UPROPERTY(Replicated)
    int32 Health;
    int32 MAXHEALTH = 100;

    // Server Unreliable RPC Function
    UFUNCTION(Server, Unreliable, WithValidation)
    void ServerUnreliableRPC(int32 RecoverHealth); 
}
  • Validate 함수는 내부 로직을 통해 RPC 함수를 Server에서 실행할지 여부를 판단한다.
    • 그렇기에 Validate Specifier가 선언된 Server RPC가 실행될 때 Validate 함수가 가장 먼저 호출된다.
#include "DerivedActor.h"

// RPC Validation Implementation
bool ServerUnreliableRPC_Validate(int32 RecoverHealth)
{
    if (Health + RecoverHealth > MAXHEALTH)
    {
        return false;
    }
return true;
}

// RPC Implementation
void ServerUnreliableRPC_Implementation(int32 RecoverHealth)
{
    Health += RecoverHealth;
}
  • 만약 Validate 함수에서 false를 반환하면, 해당 Server RPC를 전송한 Client는 Server로부터 연결이 끊긴다.

NetMulticast

  • Server에서 호출
  • 호출한 Actor와 Relevant한 모든 Client에서 실행되는 Multicast RPC
  • Client에서도 호출할 수 있으나 Local에서만 동작한다.
#pragma once

#include "DerivedActor.generated.h"

UCLASS()
class ADerivedActor : public AActor
{
    GENERATED_BODY()

public:
    // Multicast RPC Function
    UFUNCTION(NetMulticast)
    void MulticastRPC();
}
#include "DerivedActor.h"

ADerivedActor::ADerivedActor(const class FPostConstructInitializeProperties & PCIP) : Super(PCIP)
{
    bReplicates = true;
}

void ADerivedActor::MulticastRPC_Implementation()
{
    // This log will print on every machine that executes this function.
    UE_LOG(LogTemp, Log, TEXT("MulticastRPC executed."))	
}
// Call from server to run on server and all relevant clients
ADerviedServerActor* MyDerivedServerActor;
MyDerievedServerActor->MulticastRPC();

Execution Matrix

Reliability

  • Client/Server/NetMulticast와 같이 사용되는 Specifier

Reliable

  • RPC 수신자로부터 ACK를 받지 못하면 RPC를 재전송한다.
    • 다음 RPC 호출은 앞선 RPC의 ACK를 수신할 때 실행된다.
  • 순서대로 도착하는 것을 보장해준다.

Unreliable

  • RPC Packet이 Drop되면 실행되지 않는다.
  • 도착 순서를 보장하지 않는다.

Send Policy

  • ERemoteFunctionSendPolicy를 지정하여 RPC의 전송 순서를 명시적으로 조정할 수 있다.
enum class ERemoteFunctionSendPolicy
{		
	/** Unreliable multicast are queued. Everything else is send immediately */
	Default, 

	/** Bunch is send immediately no matter what */
	ForceSend,

	/** Bunch is queued until next actor replication, no matter what */
	ForceQueue,
};
  • Send Policy 조절은 NetDriver::ProcessRemoteFunctionForChannel을 통해 가능하다.
/** Process a remote function on given actor channel. This is called by ::ProcessRemoteFunction.*/
ENGINE_API void ProcessRemoteFunctionForChannel(
	UActorChannel* Ch,
	const class FClassNetCache* ClassCache,
	const FFieldNetCache* FieldCache,
	UObject* TargetObj,
	UNetConnection* Connection,
	UFunction* Function,
	void* Parms,
	FOutParmRec* OutParms,
	FFrame* Stack,
	const bool IsServer,
	const ERemoteFunctionSendPolicy SendPolicy = ERemoteFunctionSendPolicy::Default);

void UNetDriver::ProcessRemoteFunctionForChannel(
	UActorChannel* Ch,
	const FClassNetCache* ClassCache,
	const FFieldNetCache* FieldCache,
	UObject* TargetObj,
	UNetConnection* Connection,
	UFunction* Function,
	void* Parms,
	FOutParmRec* OutParms,
	FFrame* Stack,
	const bool bIsServer,
	const ERemoteFunctionSendPolicy SendPolicy)
{
	EProcessRemoteFunctionFlags UnusedFlags = EProcessRemoteFunctionFlags::None;
	ProcessRemoteFunctionForChannelPrivate(Ch, ClassCache, FieldCache, TargetObj, Connection, Function, Parms, OutParms, Stack, bIsServer, SendPolicy, UnusedFlags);
}

Default

  • RPC가 bunch에 직렬화 된다.
  • Bunch는 다음 Frame 마지막에 NetUpdate에서 전송된다.

ForceSend

  • RPC가 NetDriver::PostTickDispatch에서 trigger되면 bunch에 즉시 직렬화 되고 Network에 전송된다.
    • tick의 나머지 부분이 동작하는 도중에 trigger 되면, Default로 동작한다.
  • 이 특별한 RPC 최적화 기법은 아래 조건 하에서 동작한다.
    • Replication Graph나 Iris를 사용할 때에만 동작한다.
    • NetWroldTickTime에서 호출된 RPC에서 동작.
      • 수신한 패킷되고 수신한 RPC가 실행된다.

ForceQueue

  • Network Update이 마무리 될 때 Bandwidth가 남아 있다면 Bunch에 직렬화된다.

'UE5 > Network' 카테고리의 다른 글

[Network] Replication Execution Order  (0) 2024.06.28
[Network] Property Replication  (1) 2024.06.28
[Network] Network Property  (0) 2024.06.25
[Network] Network Driver  (0) 2024.06.18
[Network] DemoNetDriver 및 Streamer  (0) 2024.06.17

참고 링크

더보기

기본 개념

Packet

  • UDP에서 사용하는 Packet과 동한 개념
  • 여러 개의 Bunch로 구성되어 있음.

Bunch

  • 보통은 하나의 RPC나 Property Replication와 대응
    • Packet 최대 크기를 초과하면 여러 개로 쪼개진다.
    • 반대로 크기가 충분히 작으면 하나로 합쳐지기도 한다.
  • 정보를 수신할 때에는 FInBunch로 수신하고, 송신할 때에는 FOutBunch로 송신한다.

ACK/NAK

ACK

  • 일반적인 Network에서 사용하는 ACK와 동일한 개념
    • 상대방이 Packet을 성공적으로 수신 했을 때 수신 됨.
  • Ack 역시 Bunch의 형태를 띈다. 
    • 한 Packet에 여러 Bunch가 존재할 수 있듯, 한 Packet에 ACK Bunch 또한 여러 개 존재할 수 있다.

NAK

  • ACK의 반대되는 개념
    • 상대방이 Packet을 성공적으로 수신하지 못했을 때 수신 됨.
  • 실제로 구현 상 특정 Packet이 전달되지 못함을 정확하게 판단하는 것은 불가능하다.
    • 때문에 송신한 쪽에서 다른 Packet의 ACK를 통해 자체적으로 NAK 판단을 내린다.
    • 모든 Packet(ACK 포함)에는 PacketId가 입력되기 때문에 가능한 동작.
      • 예를 들어 PacketId 1, 2, 4가 수신되면 3에 해당하는 Packet은 NAK판정을 낸다.
      • 이 3번에 해당하는 Packet은 뒤늦게 수신하더라도 처리하지 않는다.
  • NAK에 대한 처리는 다음 코드를 통해 대략적으로 확인이 가능하다.
void UNetConnection::ReceivedPacket( FBitReader& Reader, bool bIsReinjectedPacket, bool bDispatchPacket )
{
/*-------Skip------*/
    // Process acks
    // Lambda to dispatch delivery notifications, 
    auto HandlePacketNotification = [&Header, &ChannelsToClose, this](FNetPacketNotify::SequenceNumberT AckedSequence, bool bDelivered)
    {
        // Increase LastNotifiedPacketId, this is a full packet Id
        ++LastNotifiedPacketId;
        ++OutTotalNotifiedPackets;
        Driver->IncreaseOutTotalNotifiedPackets();

        // Sanity check
        if (FNetPacketNotify::SequenceNumberT(LastNotifiedPacketId) != AckedSequence)
        {
            UE_LOG(LogNet, Warning, TEXT("LastNotifiedPacketId != AckedSequence"));

            Close(ENetCloseResult::AckSequenceMismatch);

            return;
        }

        if (bDelivered)
        {
            ReceivedAck(LastNotifiedPacketId, ChannelsToClose);
        }
        else
        {
            ReceivedNak(LastNotifiedPacketId);
        };
    };

    // Update incoming sequence data and deliver packet notifications
    // Packet is only accepted if both the incoming sequence number and incoming ack data are valid		
    const int32 UpdatedPacketSequenceDelta = PacketNotify.Update(Header, HandlePacketNotification);
/*-------Skip------*/
}

template<class Functor>
FNetPacketNotify::SequenceNumberT::DifferenceT FNetPacketNotify::Update(const FNotificationHeader& NotificationData, Functor&& InFunc)
{
	const SequenceNumberT::DifferenceT InSeqDelta = GetSequenceDelta(NotificationData);

	if (InSeqDelta > 0)
	{
		UE_LOG_PACKET_NOTIFY(TEXT("FNetPacketNotify::Update - Seq %u, InSeq %u"), NotificationData.Seq.Get(), InSeq.Get());
	
		ProcessReceivedAcks(NotificationData, InFunc);

		return InternalUpdate(NotificationData, InSeqDelta);
	}
	else
	{
		return 0;
	}
}

template<class Functor>
void FNetPacketNotify::ProcessReceivedAcks(const FNotificationHeader& NotificationData, Functor&& InFunc)
{
	if (NotificationData.AckedSeq > OutAckSeq)
	{
		SequenceNumberT::DifferenceT AckCount = SequenceNumberT::Diff(NotificationData.AckedSeq, OutAckSeq);

		UE_LOG_PACKET_NOTIFY(TEXT("Notification::ProcessReceivedAcks - AckedSeq: %u, OutAckSeq: %u AckCount: %u"), NotificationData.AckedSeq.Get(), OutAckSeq.Get(), AckCount);

		// Update InAckSeqAck used to track the needed number of bits to transmit our ack history
		// Note: As we might reset sequence history we need to check if we already have advanced the InAckSeqAck
		const SequenceNumberT NewInAckSeqAck = UpdateInAckSeqAck(AckCount, NotificationData.AckedSeq);
		if (NewInAckSeqAck > InAckSeqAck)
		{
			InAckSeqAck = NewInAckSeqAck;
		}
		
		// ExpectedAck = OutAckSeq + 1
		SequenceNumberT CurrentAck(OutAckSeq);
		++CurrentAck;

		// Make sure that we only look at the sequence history bit included in the notification data as the sequence history might have been reset, 
		// in which case we might not receive the max size history even though the ack-count is bigger than the history
		const SequenceNumberT::DifferenceT HistoryBits = NotificationData.HistoryWordCount * SequenceHistoryT::BitsPerWord;

		// Warn if the received sequence number is greater than our history buffer, since if that is the case we have to treat the data as lost
		// Note: This should normally not be a problem since we try to flush the sequence history before allowing an overshoot of the sequence history window on the sending side.
		// If this occurs with no hitches on server or client, there might be reason to investigate if too much data is being sent in which case the the size sequence history might have to be increased.
		if (AckCount > (SequenceNumberT::DifferenceT)(SequenceHistoryT::Size))
		{
			UE_LOG_PACKET_NOTIFY_WARNING(TEXT("FNetPacketNotify::ProcessReceivedAcks - Missed Acks: AckedSeq: %u, OutAckSeq: %u, FirstMissingSeq: %u Count: %u"), NotificationData.AckedSeq.Get(), OutAckSeq.Get(), CurrentAck.Get(), AckCount - (SequenceNumberT::DifferenceT)(SequenceHistoryT::Size));
		}

		// Everything not found in the history buffer is treated as lost
		while (AckCount > HistoryBits)
		{
			--AckCount;
			InFunc(CurrentAck, false);
			++CurrentAck;
		}

		// For sequence numbers contained in the history we lookup the delivery status from the history
		while (AckCount > 0)
		{
			--AckCount;
			UE_LOG_PACKET_NOTIFY(TEXT("Notification::ProcessReceivedAcks Seq: %u - IsAck: %u HistoryIndex: %u"), CurrentAck.Get(), NotificationData.History.IsDelivered(AckCount) ? 1u : 0u, AckCount);
			InFunc(CurrentAck, NotificationData.History.IsDelivered(AckCount));
			++CurrentAck;
		}
		OutAckSeq = NotificationData.AckedSeq;

		// Are we done waiting for an reset of the ack history?
		if (OutAckSeq > WaitingForFlushSeqAck)
		{
			WaitingForFlushSeqAck = OutAckSeq;
		}
	}
}

 

NetGUID

  • Object Pointer를 Network로 Replicate하기 위해 발급하는 ID
    • 여기서 Object는 Actor 뿐 아니라 Component도 포함된다.
    • 정확히는 UObject 단위로 구분하는 편이 적절하다.
  • Network에서 Object 식별이나 Replicate 여부를 판단하는 기준으로 사용된다.

대략적인 Unreal Network Architecture를 표현 한 그림

NetDriver

  • Game에서 Network를 총괄하는 객체
  • 크게 2가지 정보를 관리.
    • NetConnection
      • Server에서는 ClientConnection으로, Client에서는 ServerConnection으로 관리한다.
    • Connection별로 관리가 필요 없는 정보들

특징

연결 지향

  • NetConnection을 통해 연결이 되어 있어야 통신 가능.
  • 만약 비연결 프로토콜을 사용하면 Timeout 등을 처리하는 로직이 구현되어 있어야 한다.

Packet 형식

  • Stream 형식이 아닌 Packet 형식
  • 모든 데이터가 MAX_PACKET_SIZE 크기로 쪼개져서 독립된 Packet으로 저장된다.

비신뢰성

  • 신뢰성을 보장하는 패킷 송수신은 NteDriver에서 알 수 없는 고수준 계층에서 동작한다.

무결성

  • Packet의 데이터가 변조되지 않는다.

Connection별 관리가 필요 없는 정보들

FNetworkObjectInfo

  • Network에서 Replicate되는 Object에 대한 정보 및 포인터를 포함한 구조체
  • Replicated Actor의 현재 상태를 추적, 관리한다.

FReplicationChangelistMgr

  • 각각의 Replicated Object가 Replicate될 때까지의 변경사항들을 관리한다.
  • List가 가득 차게 된다면, 가장 먼저 들어온 것이 그 다음 것과 Merge 된다.
  • 이 구조체는 모든 Connection에서 사용된다.
    • 각 Connection에 어떤 정보를 전송해야 할지 결정하기 위한 비교 작업을 공유한다.
  • Connection은 마지막으로 Connection이 확인된 이후의 모든 변경사항을 전송한다.

FRepChangedPropertyTracker

  • Connection들이 공유하는 Property에 대한 메타 데이터를 저장.
    • 조건, 활성화 여부, Replay에 필요할 수 있는 외부 데이터와 무관하게 모든 메타데이터가 저장된다.
  • Object가 Replicate 될 때 어느 Property가 변경 되었는지 파악해 최적의 Network Data Packet을 구성한다.

NetConnection

Client가 Server에 접속을 요청할 때 NetConnection의 상호작용 구조

  • Game이 실질적으로 네트워크 통신을 하는 Host의 주체
  • Replicated Actor의 연결을 담당하는 (Actor)Channel을 관리한다.
    • PlayerController, GameMode 등도 이 Actor Channel을 통해 Replicate된다.
  • 때문에 하나의 Connection은 하나의 Server/Client로 취급된다.
    • Client의 경우 접속한 Server에 대응하는 NetConnection을 가진다.
    • Server의 경우 자신에게 접속한 Client에 대응하는 NetConnection들을 가진다.
      • 즉, NetConnection의 수가 Client의 수와 일치한다.

Channel

  • 실질적으로 Packet을 송/수신하는 주체
  • 각 Channel은 타겟팅하는 Object의 정보를 소유하고 있다.
  • 각 Connection들이 서로 다른 Channel을 가진다.
    • 예를 들어 4개의 Client가 연결되어 있고 Replicated Object가 200개라면,
      Client에 대응하는 NetConnection들은 각각 고유한 Actor Channel을 200개씩 가지게 된다.

ControlChannel

  • Client가 Server에 연결될 때 생성되는 Handshake용 Channel
  • 연결 설정, 인증, 상태 업데이트 등의 Control Message를 처리한다.

ActorChannel

  • Actor의 상태, Event를 Replicate하는 Channel
  • Actor 내 RepNotify, RPC이 발생하면 그 Actor의 ActorChannel을 통해 Client로 전달된다.

VoiceChannel

  • 음성 데이터를 송/수신 하는 Channel
  • Game 내 음성채팅 기능 구현에 사용된다.

Replication 판단 순서

Replicated Actor의 변경사항이 발생했을 때 Client로 Replicate되는 구조

  • Replicate 판단 여부는 UNetDriver::ServerReplicateActors에서 이루어진다.
  • Replicate가 결정된 Actor들은 UActorChannel::ReplicateActor 함수에서 세부 사항을 처리해 Replicte 된다.

NetUpdateFrequency

  • NetPriority를 통해 Replicate 여부를 판단

PreReplication

  • Replication 조건을 검사하거나 변수를 Update

bOnlyRelevantToOwner

  • NetRelevancy를 통해 Replicate 여부 판단
  • Owner에게만 Replicate 되어야 하는지 여부 결정
  • IsRelevancyOwnerFor, IsNetRelevantFor의 호출 조건을 결정

IsRelevancyOwnerFor

  • bOnlyRelevantToOwner가 True일 때에만 호출
  • bOnlyRelevantToOwner가 true일 때, 대상 Client와 Relevant한지 판단

IsNetRelevantFor

  • bOnlyRelevantToOwner가 false일 때, 어느 Client에 Relevant한지 판단

NetDormancy

  • Actor의 Dormancy(휴면) 여부를 관리
    • Dormant인 경우 Actor가 List에 등록되지 않아 Iterate 부하를 크게 줄여준다.
  • Network Traffic과 상태 변화의 빈도에 따라 조정된다.

'UE5 > Network' 카테고리의 다른 글

[Network] Property Replication  (1) 2024.06.28
[Network] Network Property  (0) 2024.06.25
[Network] DemoNetDriver 및 Streamer  (0) 2024.06.17
[Network] Beacon  (0) 2024.06.17
[Network] Replication Graph  (1) 2024.06.13

+ Recent posts