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 함수가 가장 먼저 호출된다.
만약 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에 직렬화된다.
정보를 수신할 때에는 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 부하를 크게 줄여준다.