https://dev.epicgames.com/documentation/ko-kr/unreal-engine/replicated-object-execution-order-in-unreal-engine?application_version=5.3

Actor Property

  • 기본적으로 Unreliable하고 Single Bunch로 전송된다.
    • Single Bunch에 Unreliable하기 때문에 다른 RPC보다는 나중에 전송된다.
    • 하지만 ForceQueue RPC보다는 먼저 전송된다.

Replicated Using Order

  • 서로 다른 Replicated Property에 대한 RepNotify의 순서는 명확하게 정해진 것이 없다.
    • 클라이언트 상에서 Property의 Dirty 마킹 순서나 메모리상 위치와 아무런 연관이 없다.
  • 때문에 몇몇 Property가 동시에 Replicated 되어야 한다면 Struct로 묶어서 관리하는 것을 권장한다.
  • 만약 Gameplay에 중요한 Replicated Property가 있다면,
    RepNotify를 구현해 Property의 변경사항을 프레임 단위로 대응 하는 것이 좋다.
    • Replication을 통해 변경된 값을 전달받고 RepNotify가 호출된 후에,
      UObject::PostRepNotifies 함수에서 변경사항을 처리할 수 있다.
    • 이 때, 변경된 값이 사용될 준비가 될 때까지 각각의 RepNotify에 저장하는 것이 좋다.

RPC

  • RPC에 대한 대전제는 다음과 같다.
    • Reliable이 Unreliable보다 먼저 전송된다.
    • Unicast는 Reliability와 무관하게 가장 먼저 전송된다.

Order Accross Actors

  • Replicated Actor에 대한 RPC 호출에 대해서는 명확한 법칙이 없다.
  • 때문에 RPC 함수 호출 순서와 실제 RPC 전달 순서는 일치하지 않는다.

Order Inside an Actor

  • Replication System은 동일한 Actor에 대한 reliable RPC 호출의 순서는 보장해준다.
    • 이는 Actor의 Subobject에도 적용된다.
  • 이는 reliable RPC의 경우, 함수를 호출한 순서와 실제 RPC의 전달 순서가 일치한다.

Unreliable VS Reliable

  • Unreliable RPC와 Reliable RPC가 섞여 있을 때에는 순서가 보장되는 것처럼 보이지만, 실상은 보장되지 않는다.
  • Packet 손실이나 재배치가 일어나지 않는다면, Unreliable RPC와 Reliable RPC의 전달 순서는 호출 순서와 일치한다.
    • 하지만 손실/재배치가 일어나면 순서가 달라지게 된다.
    • 정확히는 Reliable RPC끼리는 호출 순서와 전달 순서가 일치한다.

Multicast VS Unicast

  • Multicast RPC와 Unicast RPC가 섞여 있을 때에는 항상 호출 순서와 전송 순서가 일치하지는 않아 더욱 복잡하다.

Reliable Multicast

  • Reliable Multicast의 경우 Reliable Unicast와 섞여 있을 경우 호출 순서와 전송 순서가 일치한다.

Unreliable Multicast

  • Unreliable Multicast의 경우는 절대로 다른 Reliable/Unreliable Multicast와의 순서를 보장해주지 않는다.

RPC Send Policy

  • ERemoteFunctionSendPolicy를 정의하여 Send Policy를 명시해 RPC의 전송 순서에 영향을 줄 수 있다.

https://redchiken.tistory.com/389

 

[Network] Remote Procedure Calls(RPCs)

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/remote-procedure-calls-in-unreal-engine?application_version=5.3RPCLocal 단위에서 호출하여 Remote로 연결된 1개 이상의 Machinge에서 실행되는 함수Return이 없는 단방향

redchiken.tistory.com

Force Send

  • Unreliable Multicast RPC의 순서를 바꾸고 이들이 Queue되는 것을 방지한다.
    • 여기서 Queue 되지 않는다는 것은 대기하지 않고 바로 전송이 된다는 의미이다.

Force Queue

  • 다른 Force Queue RPC와 Unreliable Multicast를 제외한 RPC와의 순서를 보장하지 않는다.
  • 이 말은 ForceQueue, Unreliable Multicast와는 절대적인 순서가 보장된다는 의미이다.

Order Between RPCs and Actor Properties

  • RPC와 Property Replicate 사이에는 대략 다음과 같은 규칙이 적용된다.
    • RPC가 먼저 실행된다.
    • 그리고 Property가 나중에 update된다.
      • Property Replicate는 단일의 Unreliable 데이터 단위로 전송된다.
  • Bunch Payload는 다음 규칙을 따라 생성된다.
    • Queue 되지 않은 PRC 직렬화
    • Replicated Property 직렬화
    • Queue 된 RPC 직렬화
  • 이로 인해 한가지 주의해야 할 점이 있다.
    • RPC 내부에서 변경된 Replicated Property의 값이 뒤이어 발생할 Property Replicate에 의해 손실 될 수 있다.

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

[Network] Remote Procedure Calls(RPCs)  (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

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

참고 링크

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/replicate-actor-properties-in-unreal-engine?application_version=5.3

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/replicated-subobjects-in-unreal-engine?application_version=5.3

 

Actor

Replicated

  • Actor 내에서 Replicate 할 Property에 UPROPERTY() 선언
  • UPROPERTY() 내에 'Replicated' Specifier를 입력
#pragma once 
 
#include "DerivedActor.generated.h"
 
UCLASS()
class ADerivedActor : public AActor
{
    GENERATED_BODY()
 
public:
    // Property to replicate
    UPROPERTY(Replicated)
    uint32 Health;
 
    // Derived Actor constructor
    ADerivedActor(const class FPostConstructInitializeProperties & PCIP);
 
    // Override Replicate Properties function
    virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};
  • 해당 Actor의 bReplicates 옵션을 활성화
    • 생성자나 BP 옵션에서 제어
  • GetLifetimeReplicatedProps() 함수를 override하여 DOREPLIFETIME 매크로를 선언
#include "DerivedActor.h"
#include "Net/UnrealNetwork.h"
 
ADerivedActor::ADerivedActor(const class FPostConstructInitializeProperties & PCIP) : Super(PCIP)
{
    bReplicates = true;
}
 
void ADerivedActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    // Call the Super
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
 
    // Add properties to replicated for the derived class
    DOREPLIFETIME(ADerivedActor, Health);
}
  • 위와 같은 방식으로 Object 또한 Replicate를 할 수 있다.
  • Network를 거쳐서 참조되는 Object는 반드시 Network 기능이 지원되어야 한다.
    • 이를 확인하려면 UObject::IsSupportedForNetworking() 함수를 이용한다.

Network Replicated Reference

  • Replicated Actor
  • Replicated Component
  • Stably-named 하지만 Replicated 하지 않은 Actor나 Component
  • Package로부터 Load 된 UObject(Actor, Component도 아닌)

Stably-named Object

  •  Server와 Client 양쪽에 같은 이름으로 존재하는 Object를 지칭한다.
    • Actor가 Gameplay 중 Spawn되지 않고 Package로부터 직접 Load 되면 Stably-named하다.
  • Actor Component는 다음 케이스일 경우 Stably-named 하다.
    • Pacakge로부터 직접 load 된 경우
    • 간단한 생성자 호출로 추가 된 경우
    • UActorComponent::SetNetAddressable로 마킹 된 경우
      • Component의 이름을 Server와 Client가 모두 명확하게 알고 있는 경우
      • AActor C++ 생성자에서 추가되는 Component들이 그 대표적인 예시이다.

ReplicatedUsing

  • "OnRep_"으로 시작하는 함수를 지정해 Replicate 될 때마다 특정 행동을 부여할 수 있는 Specifier
#pragma once 
 
#include "DerivedActor.generated.h"
 
UCLASS()
class ADerivedActor : public AActor
{
	GENERATED_BODY()
 
public:
 
	// Replicated Property using OnRep_Value
	UPROPERTY(ReplicatedUsing=OnRep_Value)
	int32 HealthValue1;
 
	// Replicated Property using OnRep_ConstRef
	UPROPERTY(ReplicatedUsing=OnRep_ConstRef)
	int32 HealthValue2;
 
	// Replicated Property using OnRep_NoParam
	UPROPERTY(ReplicatedUsing=OnRep_NoParam)
	int32 HealthValue3;
 
	// Signature to pass copy of the last value
	UFUNCTION()
	void OnRep_Value(int32 LastHealthValue);
 
	// Signature to pass const reference
	UFUNCTION()
	void OnRep_ConstRef(const int32& LastHealthValue);
 
	// Signature to pass no parameter
	UFUNCTION()
	void OnRep_NoParam();
 
	// Derived Actor constructor
	ADerivedActor(const class FPostConstructInitializeProperties & PCIP);
};
  • ReplicatedUsing에 연결 될 함수들은 UFUNCTION()으로 선언되어 있어야 한다.
  • 또한 경우에 따라서 Parameter를 받을 수도 있다.
    • Parameter에 전달되는 값은 Replicated Property가 변경되기 이전의 값이다.
    • 변경된 이후의 값은 Property가 직접 들고 있다.
#include "DerivedActor.h"
#include "Net/UnrealNetwork.h"
 
ADerivedActor::ADerivedActor(const class FPostConstructInitializeProperties & PCIP) : Super(PCIP)
{
	bReplicates = true;
}
 
void ADerivedActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	// Call the Super
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
 
	// Add properties to replicated for the derived class
	DOREPLIFETIME(ADerivedActor, HealthValue1);
	DOREPLIFETIME(ADerivedActor, HealthValue2);
	DOREPLIFETIME(ADerivedActor, HealthValue3);
}
 
void ADerivedActor::OnRep_Value(int32 LastHealthValue)
{
	UE_LOG(LogTemp, Log, TEXT("OnRep_Value with value. Last value: %d"), LastHealthValue)
	// Add custom OnRep logic
}
 
void ADerivedActor::OnRep_ConstRef(const int32& LastHealthValue)
{
	UE_LOG(LogTemp, Log, TEXT("OnRep_ConstRef with const ref. Last value: %d"), *LastHealthValue)
	// Add custom OnRep logic
}
 
void ADerivedActor::OnRep_NoParam()
{
	UE_LOG(LogTemp, Log, TEXT("OnRep_NoParam with no parameter."))
	// Add custom OnRep logic
}
  • RepNotify는 C++과 BP에서 조금 다르게 동작한다.
    • BP로 RepNotify가 선언되어 있다면, Replicated Property에 대해 Set 함수가 호출될 때 RepNotify도 호출된다.
    • 하지만 기본적으로 Replicated Property의 Reference를 갖는 Function, Macro에서는
      값이 변경되더라도 RepNotify가 호출되지 않는다.

NotReplicated

  • Replicated 되는 Actor나 Struct 내에서 특정 Property를 Replicate 되지 않도록 한다.
#pragma once 
 
#include "DerivedActor.generated.h"
 
USTRUCT()
struct FMyStruct
{
	GENERATED_BODY()
 
	UPROPERTY()
	int32 ReplicatedProperty;
 
	// Not Replicated even though encompassing struct is Replicated
	UPROPERTY(NotReplicated)
	int32 NotReplicatedProperty;
};
 
UCLASS()
class ADerivedActor : public AActor
{
	GENERATED_BODY()
 
public:
	UPROPERTY(Replicated)
	FMyStruct ReplicatedStruct;
 
	// Derived Actor constructor
	ADerivedActor(const class FPostConstructInitializeProperties & PCIP);
};
  • 5.4버전 엔진 코드 기준으로는 NotReplicated Property는 GetLifetimeReplicatedProps() 내에서 DISABLE_REPLICATED_PROPERTY 선언을 해주어야 한다.
  • 다만 이는 Warning이 발생할 뿐이고 Error가 발생하지는 않는다.
#include "DerivedActor.h"
#include "Net/UnrealNetwork.h"
 
ADerivedActor::ADerivedActor(const class FPostConstructInitializeProperties & PCIP) : Super(PCIP)
{
	bReplicates = true;
}
 
void ADerivedActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
 
	DOREPLIFETIME(ADerivedActor, ReplicatedStruct);
}

Conditional Replication

  • Replicated Property는 한번 등록되면 Lifetime 동안 해제할 수 없다.
  • 기본적으로 Replicated Property는 값이 바뀔 때에 Replicate 된다.
    • 그 즉슨, 값이 바뀌지 않으면 Replicate 되지 않고 Bandwidth를 점유하지 않는다.
    • 때문에 Replicate 조건을 부여하여 불필요한 Replicate를 줄이면 이는 성능 향상으로 이어진다.

Replication Condition

  • GetLifetimeReplicatedProps() 함수에서 DOREPLIFETIME() 매크로 대신
    DOREPTIME_CONDITION() 매크로를 사용한다.
#define DOREPLIFETIME_CONDITION(c,v,cond) \
{ \
	static_assert(cond != COND_NetGroup, "COND_NetGroup cannot be used on replicated properties. Only when registering subobjects"); \
	FDoRepLifetimeParams LocalDoRepParams; \
	LocalDoRepParams.Condition = cond; \
	DOREPLIFETIME_WITH_PARAMS(c,v,LocalDoRepParams); \
}
  • 이는 Replicated 뿐 아니라 ReplicatedUsing Specifier도 적용된다.
#include "DerivedActor.h"
#include "Net/UnrealNetwork.h"
 
void ADerivedActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	// Call the Super
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
 
	// Add property replication with a condition
	DOREPLIFETIME_CONDITION(ADerivedActor, Health, COND_OwnerOnly);
}

RepNotify Condition

  • Replication System은 Replicate 뿐 아니라 RepNotify 호출에 대해서도 조건을 부여할 수 있다.
    • 이를 위해서는 DOREPLIFETIME_CONDITION_NOTIFY가 필요하다.
/** Allows gamecode to specify RepNotify condition: REPNOTIFY_OnChanged (default) or REPNOTIFY_Always for when repnotify function is called  */
#define DOREPLIFETIME_CONDITION_NOTIFY(c,v,cond,rncond) \
{ \
	static_assert(cond != COND_NetGroup, "COND_NetGroup cannot be used on replicated properties. Only when registering subobjects"); \
	FDoRepLifetimeParams LocalDoRepParams; \
	LocalDoRepParams.Condition = cond; \
	LocalDoRepParams.RepNotifyCondition = rncond; \
	DOREPLIFETIME_WITH_PARAMS(c,v,LocalDoRepParams); \
}
  • 코드를 보면 알 수 있듯이,
    DOREPLIFETIME_CONDITION_NOTIFY에서는 RepNotify 조건 외에 Replicate 조건도 지정할 수 있다.
#include "DerivedActor.h"
#include "Net/UnrealNetwork.h"
 
void ADerivedActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	// Call the Super
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
 
	// Add property replication with a condition
	/** 	Use this to always execute RepNotify
	*	Associated OnRep called on client every time property replicates
	*/
	DOREPLIFETIME_CONDITION_NOTIFY(ADerivedActor, Health, COND_OwnerOnly, REPNOTIFY_Always);
 
	/** 	Use this to only execute RepNotify when property changes
	*	Associated OnRep called on client only when property changes
	*/
	DOREPLIFETIME_CONDITION_NOTIFY(ADerivedActor, Health, COND_OwnerOnly, REPNOTIFY_OnChanged);
}

Custom Condition

  • 다음 작업을 통해 엔진에서 제공하는 조건 외의 조건으로 Replicate를 조절할 수 있다.
    • Replication Condition을 COND_Custom으로 지정
    • bool 타입 혹은 이를 반환하는 함수를 PreReplication에 등록
#pragma once 
 
#include "DerivedActor.generated.h"
 
UCLASS()
class ADerivedActor : public AActor
{
    GENERATED_BODY()
 
public:
    UPROPERTY(Replicated)
    int32 Health;
 
    virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
 
    // Derived Actor constructor
    ADerivedActor(const class FPostConstructInitializeProperties & PCIP);
 
    virtual void PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker) override;
 
    // Custom Replication Condition override function
    bool IsInvincible();
};
  • PreReplication에 Custom Condition을 지정할 때에는 DOREPLIFETIME_ACTIVE_OVERRIDE 매크로를 사용한다.
// Built on Compile time and Disable to work with Array
#define DOREPLIFETIME_ACTIVE_OVERRIDE_FAST(c,v,active) \
{ \
	static_assert(ValidateReplicatedClassInheritance<c, ThisClass>(), #c "." #v " is not accessible from this class."); \
	UE::Net::Private::FNetPropertyConditionManager::SetPropertyActiveOverride(ChangedPropertyTracker, this, (int32)c::ENetFields_Private::v, active); \
}

// Built on Compile time and Enable to Work with Array
#define DOREPLIFETIME_ACTIVE_OVERRIDE_FAST_STATIC_ARRAY(c,v,active) \
{ \
	static_assert(ValidateReplicatedClassInheritance<c, ThisClass>(), #c "." #v " is not accessible from this class."); \
	for (int32 i = 0; i < (int32)c::EArrayDims_Private::v; ++i) \
	{ \
		UE::Net::Private::FNetPropertyConditionManager::SetPropertyActiveOverride(ChangedPropertyTracker, this, (int32)c::ENetFields_Private::v##_STATIC_ARRAY + i, active); \
	} \
}

// Built on Runtime and Enable to Work with Array
#define DOREPLIFETIME_ACTIVE_OVERRIDE(c,v,active) \
{ \
	static_assert(ValidateReplicatedClassInheritance<c, ThisClass>(), #c "." #v " is not accessible from this class."); \
	static FProperty* sp##v = GetReplicatedProperty(StaticClass(), c::StaticClass(),GET_MEMBER_NAME_CHECKED(c,v)); \
	for (int32 i = 0; i < sp##v->ArrayDim; i++) \
	{ \
		UE::Net::Private::FNetPropertyConditionManager::SetPropertyActiveOverride(ChangedPropertyTracker, this, sp##v->RepIndex + i, active); \
	} \
}
  • 이제 Health는 IsInvincible()이 false일 때에만 Replicate된다.
#include "DerivedActor.h"
#include "Net/UnrealNetwork.h"

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

void ADerivedActor::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const 
{
	// Call the Super 
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	// Add properties to replicated for the derived class
    DOREPLIFETIME_CONDITION(ADerivedActor, Health, COND_Custom);
}

/* Function where the Custom condition is registered. */ 
void ADerivedActor::PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker)
{
	// Call the Super
    Super::PreReplication(ChangedPropertyTracker);

	/* Use a custom property replication condition In this case, a function IsInvincible() If the actor is invincible, don't replicate Health */
    DOREPLIFETIME_ACTIVE_OVERRIDE(ADerivedActor, Health, !IsInvincible());
}

bool IsInvincible()
{
	bool bIsInvincible = false;

	// Custom logic to determine invincibility...
	return bIsInvincible;
}
  • Custom Condition은 매우 편리해 보이지만 2가지 큰 이유로 자주 사용하지 않는다.
    • 첫째, 작업 시간과 소모 리소스가 크다
    • 둘째, Connection 기준을 변경할 수 없다.

ELifetimeCondition

  • DOREPLIFETIME_CONDITION(_NOTIFY) 매크로의 3번째 parameter로 전달하는 값들.
/** Secondary condition to check before considering the replication of a lifetime property. */
UENUM(BlueprintType)
enum ELifetimeCondition : int
{
	COND_None = 0							UMETA(DisplayName = "None"),							// This property has no condition, and will send anytime it changes
	COND_InitialOnly = 1					UMETA(DisplayName = "Initial Only"),					// This property will only attempt to send on the initial bunch
	COND_OwnerOnly = 2						UMETA(DisplayName = "Owner Only"),						// This property will only send to the actor's owner
	COND_SkipOwner = 3						UMETA(DisplayName = "Skip Owner"),						// This property send to every connection EXCEPT the owner
	COND_SimulatedOnly = 4					UMETA(DisplayName = "Simulated Only"),					// This property will only send to simulated actors
	COND_AutonomousOnly = 5					UMETA(DisplayName = "Autonomous Only"),					// This property will only send to autonomous actors
	COND_SimulatedOrPhysics = 6				UMETA(DisplayName = "Simulated Or Physics"),			// This property will send to simulated OR bRepPhysics actors
	COND_InitialOrOwner = 7					UMETA(DisplayName = "Initial Or Owner"),				// This property will send on the initial packet, or to the actors owner
	COND_Custom = 8							UMETA(DisplayName = "Custom"),							// This property has no particular condition, but wants the ability to toggle on/off via SetCustomIsActiveOverride
	COND_ReplayOrOwner = 9					UMETA(DisplayName = "Replay Or Owner"),					// This property will only send to the replay connection, or to the actors owner
	COND_ReplayOnly = 10					UMETA(DisplayName = "Replay Only"),						// This property will only send to the replay connection
	COND_SimulatedOnlyNoReplay = 11			UMETA(DisplayName = "Simulated Only No Replay"),		// This property will send to actors only, but not to replay connections
	COND_SimulatedOrPhysicsNoReplay = 12	UMETA(DisplayName = "Simulated Or Physics No Replay"),	// This property will send to simulated Or bRepPhysics actors, but not to replay connections
	COND_SkipReplay = 13					UMETA(DisplayName = "Skip Replay"),						// This property will not send to the replay connection
	COND_Dynamic = 14						UMETA(Hidden),											// This property wants to override the condition at runtime. Defaults to always replicate until you override it to a new condition.
	COND_Never = 15							UMETA(Hidden),											// This property will never be replicated
	COND_NetGroup = 16						UMETA(Hidden),											// This subobject will replicate to connections that are part of the same group the subobject is registered to. Not usable on properties.
	COND_Max = 17							UMETA(Hidden)
};

ELifetimeRepnotifyCondition

  • DOREPLIFETIME_CONDITION_NOTIFY 매크로의 4번째 parameter로 전달되는 값들
enum ELifetimeRepNotifyCondition
{
	REPNOTIFY_OnChanged = 0,		// Only call the property's RepNotify function if it changes from the local value
	REPNOTIFY_Always = 1,		// Always Call the property's RepNotify function when it is received from the server
};

SubObject

ReplicateSubobjects

class AMyActor : public AActor
{
    UPROPERTY(Replicated)
    UMySubObjectClass* MySubObject;
}

class UMySubObjectClass : public UObject
{
    UPROPERTY(Replicated)
    int32 Counter = 0;
}

void AMyActor::CreateMyClass()
{
    MySubObject = NewObject<UMySubObjectClass>();
    MySubObject->Counter = 10;
}

void AMyActor::ReplicateSubobjects(...)
{
    Super::ReplicateSubobjects(...);
    Channel->ReplicateSubobject(MySubObject); // Becomes a subobject here
}
  • Channel에 Subobject를 등록하지 않는다면, Client에서 해당 Subobject는 항상 null일 것이다.
  • Subobject를 Replicate 하더라도 그 안의 Property를 Replicate하려면 Specifier 선언을 따로 해줘야 한다.

Registered Subobject

  • Subobject를 Owning Actor/ActorComponent List에 등록
    • List에 등록 된 Object들은 Actor Channel에 의해 자동으로 replicate 된다.
    • 등록하 때 ELifetimeCondition을 통해 Replicate condition을 부여할 수 있다.
  • 이 방식은 ReplicateSubObjects 함수 사용에 대한 업무 부담을 줄여줍니다.
AMyActor::AMyActor()
{
    bReplicateUsingRegisteredSubObjectList = true;
}

void AMyActor::CleanupSubobject()
{
    if (MySubobject)
    {
        RemoveReplicatedSubobject(MySubObject);
    }
}

void AMyActor::CreateMyClass()
{
    CleanupSubobject();

    MySubObject= NewObject<UMySubObjectClass>();
    MySubObject->Counter = 10;
    AddReplicatedSubObject(MySubObject);
}

void AMyActor::CreateMyDerivedClass()
{
    CleanupSubobject();

    MySubObject = NewObject<UMyDerivedSubObjectClass>();
    AddReplicatedSubObject(MySubObject);
}

void AMyActor::ReplicateSubobjects(...)
{
    //deprecated and not called anymore
}
  • AddReplicatedSubObject 함수는 ReadyForReplication이나 BeginPlay, 또는 Subobject를 생성할 때 호출한다.
  • 이 중 ReadyForReplication에서 함수 호출 시 주의할 점이 있따.
    • ActorComponent에서 ReadyForReplication 함수는 InitComponent와 BeginPlay 사이에 호출된다.
    • 즉슨, Component를 이 함수 안에서 등록하게 되면 BeginPlay에서는 RPC를 수행할 수 있다는 의미이다.
  • Subobject를 수정하거나 삭제 할 때에는 반드시 RemoveReplicateSubObject 함수를 호출해야 한다.
    • 해당 함수를 호출하지 않고 수정/삭제 시, Object의 Destruction 과정에서 한번 더 Destroy가 Mark된다.
    • 이는 GC가 동작할 때 Crash를 유발할 가능성이 있다.
  • 이 부분의 코드를 수정 할 때, net.SubObjects.CompareWithLegacy를 Console Command로 설정하면
    Runtime에서 Registered SubObjectList와 이전 함수를 비교할 수 있다.
    • 차이점이 감지되면 ensure가 발생한다.

Components

  • Replicated Component도 기본적으로 Subobject와 동일하다.
    • Component의 경우에는 AllowActorComponentToReplicate 함수를 override 한다.
    • 이 때 각 Component의 Replicate Condition은 내부 조건문에 맞춰 판정, 반환해야 한다.
  • 만약 BeginPlay가 호출 된 이후에 Component의 ReplicateCondition을 바꾸고 싶다면,
    SetReplicatedComponentNetCondition 함수를 이용한다.
  • Owning Component List는 Condition이 확인되기 전에 각 Connection에 Replicate 되어야 한다.
    • 예를 들어 Componenet가 SkipOwner인 경우,
      SubObject가 OwnerOnly이더라도 Owner에게 Replicate 되지 않는다.
ELifetimeCondition AMyWeaponClass::AllowActorComponentToReplicate(const UActorComponent* ComponentToReplicate) const
{
    // Do not replicate some components while the object is on the ground.
    if (!bIsInInventory)
    {
        if (IsA<UDamageComponent>(ComponentToReplicate))
        {
            return COND_Never;
        }
    }
    Super::AllowActorComponentToReplicate(ComponentToReplicate);
}

void AMyWeaponClass::OnPickup()
{
    // Now replicate the component to all
    SetReplicatedComponentNetCondition(UDamageComponent, COND_None);
    bIsInInventory = true;
}

Complex Replication Condition

  • NetConditionGroupManager와 COND_NetGroup을 이용해 Replicate Condition을 새로 만들 수 있다.
  • 이는 Subobject와 PlayerController가 여러 Group에 동시에 속해 있을 때,
    이 중 하나의 Group에만 속해 있어도 Replicate된다.
FName NetGroupAlpha(TEXT("NetGroup_Alpha"))
  • 원하는 Subobject를 위에서 추가한 NetGroupAlpha Group에 추가한다.
FNetConditionGroupManager::RegisterSubObjectInGroup(MyAlphaSubObject, NetGroupAlpha)
  • Subobject를 Replicate 할 Client의 PlayerController를 이용해 같은 Group에 추가한다.
PlayerControllerAlphaOwner->IncludeInNetConditionGroup(NetGroupAlpha)
  • 이제 PlayerControllerAlphaOwner의 Client는 Owner Actor가 해당 Client의 Connection에 Replicate 될 때마다
    등록된 MyAlphaSubobject를 Replicate 받는다.

Client Subobject List

  • Server가 Replicated Subobject List를 관리하는 것처럼, Client도 자체적으로 Subobject List를 관리해야 한다.
    • 이는 특히 Client에서 Replay를 녹화할 때 중요하다.
    • 이 경우, Client의 Actor는 Replay에 기록할 때 일시적으로 Local Authority Role로 전환된다.
    • 그렇기에 Replay에 기록된 Actor은 Local Role에 상관 없이
      Client에서 자체적으로 Subobject List를 유지해야 한다.
  • 만약 Subobject가 Replicated Property라면, RepNotify를 사용함으로 더 쉽게 관리할 수 있따.
    • Client는 RepNotify를 통해 SubObject가 변경되었음을 확인하여,
      이전 포인터를 제거하고 새로운 것을 추가할 수 있다.
  • Server의 Subobject List에서 Replicated Subobject를 제거하면
    해당 Object의 Replicated Property가 Client에 Replicate 되지는 않는다.
    • 하지만 SubObject의 포인터는 UObject가 스스로를 Grabage라고 마크 하기 전까지 Net-Referencable하다.
    • Server가 UObject가 Invalid하다 탐지하게 되면,
      다음 Reflection 업데이트에서 Client로 하여금 자체적으로 해당 Subobject를 지우도록 알린다.

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

[Network] Replication Execution Order  (0) 2024.06.28
[Network] Remote Procedure Calls(RPCs)  (0) 2024.06.28
[Network] Network Property  (0) 2024.06.25
[Network] Network Driver  (0) 2024.06.18
[Network] DemoNetDriver 및 Streamer  (0) 2024.06.17

참고 자료

Actor Owner and Owning Connection

  • Unreal Engine은 Server가 모든 권한을 가지는(server-authoritative) client-server model을 채용하고 있다.
    • 이 model에서 client는 중앙화 된 Server에 접속하게 된다.
  • Client가 Server에 접속을 하게 되면 그에 대응하는 NetConnection이 생성한다.
    • 이 때 NetConnection은 PlayerController의 Owner가 된다.
  • Client가 Server에서 Play를 시작하면, Player Controller은 Client가 Control 할 pawn을 possess한다.
    • 이 때 PlayerController는 Pawn의 Owner가 된다.
  • Actor의 Owning Connection은 Actor를 소유한 PlayerController의 Owning Connection과 연관되어 있다.
  • Owner와 Owning Connection은 어느 Client에서 Replicate와 RPC를 전달해야하는지 판단한다.

용처

Actor Replication

  • Actor의 변경사항을 어느 Connection에 Replicate하는지 판단이 필요.
  • PlayerController와 같이 Actor를 소유한 Connection에서만 Replicate를 받는 경우에는 
    bOnlyRelevantToOwner 트리거가 True로 설정되어 있다.
    • PlayerController의 경우에는 이 플래그가 기본적으로 설정되어 있다.

Property Replication

  • Owner에 따른 Property Replication 조건이 붙은 경우에 사용된다.

RPCs

  • Multicast RPC가 아니라면 Client RPC가 어느 Client에 전달되어야 하는지 판단이 필요

NetRole이 ROLE_AutonomousProxy인 Actor

  • 해당 Actor의 Owner가 아닌 NetConnection에 대한 Property Replication이 발생하면
    Role이 ROLE_SimulatedProxy로 변경된다.

Role

/** The network role of an actor on a local/remote network context */
UENUM(BlueprintType)
enum ENetRole : int
{
	/** No role at all. */
	ROLE_None UMETA(DisplayName = "None"),
	/** Locally simulated proxy of this actor. */
	ROLE_SimulatedProxy UMETA(DisplayName = "Simulated Proxy"),
	/** Locally autonomous proxy of this actor. */
	ROLE_AutonomousProxy UMETA(DisplayName = "Autonomous Proxy"),
	/** Authoritative control over the actor. */
	ROLE_Authority UMETA(DisplayName = "Authority"),
	ROLE_MAX UMETA(Hidden),
};

NetRole

ROLE_SimulatedProxy

  • 수동적인(Approximate) 프록시라는 의미
  • 클라이언트에서 모조(Simulated) 프록시로 동작한다.
    • 기본적인 물리 동작만 Simulation하고 자체적인 판단을 하지 않는다.

ROLE_AutonomousProxy

  • 능동적인(Autonomous) 프록시라는 의미
  • 클라이언트에서 Simulation이 아닌 Prediction 로직을 갖는다.

ROLE_Authority

  • 해당 Connection에서 Actor에 대한 완전하고 절대적인(Authoritative) 제어권을 갖는다는 뜻

Actor Role

Local Role

  • 현재 자신 PC에서의 NetRole을 지칭

Remote Role

  • 자신이 원격으로 연결되어 있는 PC에서의 NetRole을 지칭

Server/Client에서의 Role 값

Server

  • Unreal Engine은 Replicate Actor에 대해 모든 Authority를 Server가 갖는다.
    • 때문에 Server의 모든 Actor는 LocalRole이 ROLE_Authority가 된다.
  • 특정 PlayerController가 Actor에 대한 Ownership을 가진 경우,
    RemoteRole은 ROLE_AutonomousProxy가 된다.
  • 그 외의 Actor들에 대해서는 ROLE_SimulatedProxy가 된다.

Client

  • 해당 Client의 Connection이 Ownership을 갖는 Actor의 경우,
    Prediction이 가능하기 때문에 LocalRole이 ROLE_AutonomousProxy가 된다.
  • 반대로 자신이 OwnerShipe을 갖지 않는 Actor의 경우,
    Simulation만이 가능하기 때문에 LocalRole이 ROLE_SimulatedProxy가 된다.
  • 어느 Actor든, Replicate가 되면 Authority는 Server가 가지기 때문에 RemoteRole이 ROLE_Authority가 된다.
  • 단, Client에서만 존재하는 Local Actor의 경우에는 LocalRole이 ROLE_Authority,
    RemoteRole이 ROLE_None이다.

Dormancy

  • 가장 영향력이 강한 Network Optimize 중 하나
    • Project에서 자주 Replicate 되지 않는 Actor가 많을수록 효과적이다.
  • NetDriver는 Connection의 모든 Replicated Actor를 수집하고, 이를 Iterate하여 Replicate 대상을 결정한다.
    • Dormancy는 특정 Actor를 Dormant 상태로 두어 NetDriver로부터 Actor가 수집되지 않도록 한다.
    • Actor가 많을수록 Iterate 비용이 부담되기에 Actor 수집을 필터링하는 것은 매우 효과적이다.

사용법

  1. Actor의 Constructor에서 NetDormancy를 초기화
    • 보통은 DORM_DormantAll로 초기화
    • Actor가 Map에 배치된 경우, DORM_Initial로 초기화
  2. NetDormancy가 DORM_DormantAll/DORM_Initial일 때에는 Dormant 상태가 되어 Replicate가 되지 않는다.
    • Dormant 상태의 Actor의 값을 변경하더라도 Awake/Flush를 하면 변경사항이 보존되지 않는다.
  3. Replicated Property를 변경하기 전 FlushNetDormancy/ForceNetUpdate/SetNetDormancy 함수를 호출.
    • 일반적으로는 값을 변경 후 함수 호출을 해도 Replicate 될 수 있다.
    • 하지만 정식 스펙 상으로는 올바른 사용법도 아니니 이를 지양해야 한다.
    • 대표적으로 Fast Array의 값을 변경한 후 함수를 호출하면 변경사항이 Replicate되지 않는다.
  4. Replicated Property 값을 변경
  • 다시 한번 강조하지만 이는 값이 자주 변경되지 않는 Replicated Actor에 적합하다.
    • 너무 자주 값이 바뀌면 Dormant<->Awake/Flush 오버헤드가 발생.
  • Dormancy는 Relavancy Handling과 달리 Dormant 상태가 되더라도 Server/Client 양쪽에서 Actor가 존재한다.
    • Dormant Actor은 Relavancy 검증에서 제외된다.

ENetDormancy

/** Describes if an actor can enter a low network bandwidth dormant mode */
UENUM(BlueprintType)
enum ENetDormancy : int
{
	/** This actor can never go network dormant. */
	DORM_Never UMETA(DisplayName = "Never"),
	/** This actor can go dormant, but is not currently dormant. Game code will tell it when it go dormant. */
	DORM_Awake UMETA(DisplayName = "Awake"),
	/** This actor wants to go fully dormant for all connections. */
	DORM_DormantAll UMETA(DisplayName = "Dormant All"),
	/** This actor may want to go dormant for some connections, GetNetDormancy() will be called to find out which. */
	DORM_DormantPartial UMETA(DisplayName = "Dormant Partial"),
	/** This actor is initially dormant for all connection if it was placed in map. */
	DORM_Initial UMETA(DisplayName = "Initial"),

	DORM_MAX UMETA(Hidden),
};

Awake Method

  • NetDormancy 변수는 Public 접근자로 선언되어 있지만, 가급적 Awake Method 사용을 권장한다.
  • 변경사항을 NetDriver에 알리는 기능이 Awake Methond에 내장되어 있다.

SetNetDormancy

/** Puts actor in dormant networking state */
UFUNCTION(BlueprintAuthorityOnly, BlueprintCallable, Category = "Networking")
ENGINE_API void SetNetDormancy(ENetDormancy NewDormancy);

void AActor::SetNetDormancy(ENetDormancy NewDormancy)
{
	if (IsNetMode(NM_Client))
	{
		return;
	}

	if (IsPendingKillPending())
	{
		return;
	}

	ENetDormancy OldDormancy = NetDormancy;
	NetDormancy = NewDormancy;
	const bool bDormancyChanged = (OldDormancy != NewDormancy);

	if (UWorld* MyWorld = GetWorld())
	{
		if (FWorldContext* const Context = GEngine->GetWorldContextFromWorld(MyWorld))
		{
			// Tell driver about change
			if (bDormancyChanged)
			{
				for (FNamedNetDriver& Driver : Context->ActiveNetDrivers)
				{
					if (Driver.NetDriver != nullptr && Driver.NetDriver->ShouldReplicateActor(this))
					{
						Driver.NetDriver->NotifyActorDormancyChange(this, OldDormancy);
					}
				}
			}

			// If not dormant, flush actor from NetDriver's dormant list
			if (NewDormancy <= DORM_Awake)
			{
				// Since we are coming out of dormancy, make sure we are on the network actor list
				MyWorld->AddNetworkActor(this);

				for (FNamedNetDriver& Driver : Context->ActiveNetDrivers)
				{
					if (Driver.NetDriver != nullptr && Driver.NetDriver->ShouldReplicateActor(this))
					{
						Driver.NetDriver->FlushActorDormancy(this);
					}
				}
			}
		}
	}
}
  • Dormant Actor의 NetDormancy 값을 수정해 Awake/Dormant 상태를 변경할 수 있다.
    • Map에 배치된 Dormant Actor의 경우, Awake 이후 DORM_Initial 대신 DORM_DormantAll로 설정되어야 한다.
  • Dormant Actor가 매 Frame마다 움직일 때 사용하면 적합하다.

FlushNetDormancy

/** Forces dormant actor to replicate but doesn't change NetDormancy state (i.e., they will go dormant again if left dormant) */
UFUNCTION(BlueprintAuthorityOnly, BlueprintCallable, Category="Networking")
ENGINE_API void FlushNetDormancy();

/** Removes the actor from the NetDriver's dormancy list: forcing at least one more update. */
void AActor::FlushNetDormancy()
{
	if (IsNetMode(NM_Client) || NetDormancy <= DORM_Awake || IsPendingKillPending())
	{
		return;
	}

	QUICK_SCOPE_CYCLE_COUNTER(NET_AActor_FlushNetDormancy);

	bool bWasDormInitial = false;
	if (NetDormancy == DORM_Initial)
	{
		// No longer initially dormant
		NetDormancy = DORM_DormantAll;
		bWasDormInitial = true;
	}

	// Don't proceed with network operations if not actually set to replicate
	if (!bReplicates)
	{
		return;
	}

	if (UWorld* const MyWorld = GetWorld())
	{
		// Add to network actors list if needed
		MyWorld->AddNetworkActor(this);
	
		if (FWorldContext* const Context = GEngine->GetWorldContextFromWorld(MyWorld))
		{
			for (FNamedNetDriver& Driver : Context->ActiveNetDrivers)
			{
				if (Driver.NetDriver != nullptr && Driver.NetDriver->ShouldReplicateActor(this))
				{
					Driver.NetDriver->FlushActorDormancy(this, bWasDormInitial);
				}
			}
		}
	}
}
  • Dormant Actor의 변경사항을 Replicate
    • Actor의 NetDormancy를 변경하지 않고도 연결된 Updates를 강제로 Replicate한다.
    • 단, NetDormancy가 DORM_Initial인 Actor는 NetDormancy가 DORM_DormantAll로 변경된다.
  • BP에서는 Dormant Actor의 Replicated Property를 수정하면 자동으로 FlushNetDormancy가 호출된다.
    • 5.4 버전 기준, ActorComponent 한정으로 Replicated Property를 수정해도 FlushNetDormancy가 호출되지 않는다.

ForceNetUpdate

/** Force actor to be updated to clients/demo net drivers */
UFUNCTION( BlueprintCallable, Category="Networking")
ENGINE_API virtual void ForceNetUpdate();

void AActor::ForceNetUpdate()
{
	UNetDriver* NetDriver = GetNetDriver();

	if (GetLocalRole() == ROLE_Authority)
	{
		// ForceNetUpdate on the game net driver only if we are the authority...
		if (NetDriver && NetDriver->GetNetMode() < ENetMode::NM_Client) // ... and not a client
		{
			NetDriver->ForceNetUpdate(this);

			if (NetDormancy > DORM_Awake)
			{
				FlushNetDormancy();
			}
		}
	}

	// Even if not authority, other drivers (like the demo net driver) may need to ForceNetUpdate
	if (UWorld* MyWorld = GetWorld())
	{
		if (FWorldContext* const Context = GEngine->GetWorldContextFromWorld(MyWorld))
		{
			for (FNamedNetDriver& Driver : Context->ActiveNetDrivers)
			{
				if (Driver.NetDriver != nullptr && Driver.NetDriver != NetDriver && Driver.NetDriver->ShouldReplicateActor(this))
				{
					Driver.NetDriver->ForceNetUpdate(this);
				}
			}
		}
	}
}
  • FlushNetDormancy가 호출되고, 다음 NetUpdate에서 해당 Actor가 Replication 대상으로 고려된다.
    • Actor가 단일 Frame에서 간헐적으로 1회성 Update를 발생하는 경우에 유용
  • Actor가 Flush/Awake 된 후에 다시 Dormant 설정을 하더라도 즉시 Dormant 상태가 되지 않는다.
    • 때문에 여러 Update를 전송할 수 있다.
    • 해당 Actor와 SubObject에서 Replicate 되어야 할 Updates가 없을 때까지 Replicate 한다.
  • 또한 DormancyHysteresis가 활성화 된 경우에도 Dormant 상태가 즉각 적용되지 않는다.
  • UActorHannel::ReadyForDormancy
    • 더보기
      bool UActorChannel::ReadyForDormancy(bool suppressLogs)
      {
      	// We need to keep replicating the Actor and its subobjects until none of them have
      	// changes, and would otherwise go Dormant normally.
      	if (!bIsInDormancyHysteresis)
      	{
      		for (auto MapIt = ReplicationMap.CreateIterator(); MapIt; ++MapIt)
      		{
      			if (!MapIt.Value()->ReadyForDormancy(suppressLogs))
      			{
      				return false;
      			}
      		}
      	}
      
      	if (DormancyHysteresis > 0 && Connection && Connection->Driver)
      	{
      		bIsInDormancyHysteresis = true;
      		const double TimePassed = Connection->Driver->GetElapsedTime() - LastUpdateTime;
      		if (TimePassed < DormancyHysteresis)
      		{
      			return false;
      		}
      	}
      
      	return true;
      }
  • FObjectReplicator::ReadyForDormancy
    • 더보기
      bool FObjectReplicator::ReadyForDormancy(bool bSuppressLogs)
      {
      	if (GetObject() == nullptr)
      	{
      		UE_LOG(LogRep, Verbose, TEXT("ReadyForDormancy: Object == nullptr"));
      		return true;		// Technically, we don't want to hold up dormancy, but the owner needs to clean us up, so we warn
      	}
      
      	// Can't go dormant until last update produced no new property updates
      	if (!bLastUpdateEmpty)
      	{
      		if (!bSuppressLogs)
      		{
      			UE_LOG(LogRepTraffic, Verbose, TEXT("    [%d] Not ready for dormancy. bLastUpdateEmpty = false"), OwningChannel->ChIndex);
      		}
      
      		return false;
      	}
      
      	if (FSendingRepState* SendingRepState = RepState.IsValid() ? RepState->GetSendingRepState() : nullptr)
      	{
      		if (SendingRepState->HistoryStart != SendingRepState->HistoryEnd)
      		{
      			// We have change lists that haven't been acked
      			return false;
      		}
      
      		if (SendingRepState->NumNaks > 0)
      		{
      			return false;
      		}
      
      		if (!SendingRepState->bOpenAckedCalled)
      		{
      			return false;
      		}
      
      		if (SendingRepState->PreOpenAckHistory.Num() > 0)
      		{
      			return false;
      		}
      
      		// Can't go dormant if there are unAckd property updates
      		for (FPropertyRetirement& Retirement : SendingRepState->Retirement)
      		{
      			if (Retirement.Next != nullptr)
      			{
      				if (!bSuppressLogs)
      				{
      					UE_LOG(LogRepTraffic, Verbose, TEXT("    [%d] OutAckPacketId: %d First: %d Last: %d "), OwningChannel->ChIndex, OwningChannel->Connection->OutAckPacketId, Retirement.OutPacketIdRange.First, Retirement.OutPacketIdRange.Last);
      				}
      				return false;
      			}
      		}
      	}
      
      	return true;
      }

Awake Method를 사용해야 하는 경우

  • Actor가 Awake 되면 Replicated Property의 값을 Shadow State를 Reinitialize한다.
    • Shadow State는 변경된 Property와 Replicated Property를 비교하는데 사용한다.
    • 때문에 Domant Actor의 Replicated Property 값을 변경 하더라도 Awake 과정에서 변경사항이 탐지되지 않는다.

Dormancy with Replication Graph

  • ReplicationGraph를 사용하더라도 Dormancy는 Default NetDriver와 동일하게 동작해야 하기 때문에,
    Project에서는 Actor의 Dormant/Awake 세팅과 FlushNetDormancy 호출이 동일하게 이루어진다.
  • ReplicationGraphNode에서 Actor List를 수집할 때 Dormant Actor가 반환되더라도 아래 함수에서 건너뛴다.
void UReplicationGraph::ReplicateActorListsForConnections_Default(UNetReplicationGraphConnection* ConnectionManager, FGatheredReplicationActorLists& GatheredReplicationListsForConnection, FNetViewerArray& Viewers)
{				
//-------Skip-------//
    // Skip if dormant on this connection. We want this to always be the first/quickest check.
    if (ConnectionData.bDormantOnConnection)
    {
        DO_REPGRAPH_DETAILS(PrioritizedReplicationList.GetNextSkippedDebugDetails(Actor)->bWasDormant = true);
        if (bDoCulledOnConnectionCount)
        {
            DormancyClassAccumulator.Increment(Actor->GetClass());
        }
        continue;
    }
//-------Skip-------//
}
  • ReplicationGraphNode는 Dormant Actor에 대한 특별한 Handling이 포함될 수 있다.
    • 이를 통해 Node의 Dormant Actor 처리 시간, 메모리 뿐 아니라 Actor List의 크기도 줄일 수 있다.
    • 예를 들어 ReplicationGraphNode_GridSpatialization2D의 경우,
      Dormant Actor은 Static으로 취급하고 Awake Actor은 Dynamic으로 취급하는 Handling이 포함되어 있다.
    • 이러한 Handling은 보통은 정지 및 Dormant이지만 가끔 Grid를 통과하는 Actor에 유용하다.

Debug

Log

  • LogNetDormancy 로그 카테고리를 활성화하여 Dormant 정보를 가져올 수 있다.
    • 상세도를 높이면 Actor의 NetDormancy가 Flush될 때처럼 더 자세한 정보가 기록된다.

Console Command

NetPriority

  • Unreal Engine의 Network Update는 Bandwidth 제한으로 모든 Actor의 Replicate를 보장하지 않는다.
    • 만약 Update 용량이 Bandwidth를 초과하면 자체적인 Load Balancing을 통해 NetPriority를 할당한다.
  • NetPriority가 높을수록 더 중요한 Actor이므로 더 많은 Bandwidth가 할당된다.

Actor의 NetPriority 구하기

/** Priority for this actor when checking for replication in a low bandwidth or saturated situation, higher priority means it is more likely to replicate */
UPROPERTY(Category=Replication, EditDefaultsOnly, BlueprintReadWrite)
float NetPriority;
  • Actor의 Replicate 빈도 차이는 NetPriority의 비율과 일치하다.
    • NetPriority가 3.0인 Actor는 1,0인 Actor보다 3배의 빈도로 Update 된다.
  • 일반적으로 Actor는 1.0, Pawn과 PlayerController는 3.0의 초기값을 가진다.

Current NetPriority 구하기

/**
 * Function used to prioritize actors when deciding which to replicate
 * @param ViewPos		Position of the viewer
 * @param ViewDir		Vector direction of viewer
 * @param Viewer		"net object" owned by the client for whom net priority is being determined (typically player controller)
 * @param ViewTarget	The actor that is currently being viewed/controlled by Viewer, usually a pawn
 * @param InChannel		Channel on which this actor is being replicated.
 * @param Time			Time since actor was last replicated
 * @param bLowBandwidth True if low bandwidth of viewer
 * @return				Priority of this actor for replication, higher is more important
 */
ENGINE_API virtual float GetNetPriority(const FVector& ViewPos, const FVector& ViewDir, class AActor* Viewer, AActor* ViewTarget, UActorChannel* InChannel, float Time, bool bLowBandwidth);

/**
 * Defines in NetworkDistanceConstants.h
 * CLOSEPROXIMITY: 500
 * NEARSIGHTTHRESHOLD: 2000
 * MEDSIGHTTHREHOLD: 3162
 * FARSIGHTTHRESHOLD: 8000
 */
float AActor::GetNetPriority(const FVector& ViewPos, const FVector& ViewDir, AActor* Viewer, AActor* ViewTarget, UActorChannel* InChannel, float Time, bool bLowBandwidth)
{
	if (bNetUseOwnerRelevancy && Owner)
	{
		// If we should use our owner's priority, pass it through
		return Owner->GetNetPriority(ViewPos, ViewDir, Viewer, ViewTarget, InChannel, Time, bLowBandwidth);
	}

	if (ViewTarget && (this == ViewTarget || GetInstigator() == ViewTarget))
	{
		// If we're the view target or owned by the view target, use a high priority
		Time *= 4.f;
	}
	else if (!IsHidden() && GetRootComponent() != NULL)
	{
		// If this actor has a location, adjust priority based on location
		FVector Dir = GetActorLocation() - ViewPos;
		float DistSq = Dir.SizeSquared();

		// Adjust priority based on distance and whether actor is in front of viewer
		if ((ViewDir | Dir) < 0.f)
		{
			if (DistSq > NEARSIGHTTHRESHOLDSQUARED)
			{
				Time *= 0.2f;
			}
			else if (DistSq > CLOSEPROXIMITYSQUARED)
			{
				Time *= 0.4f;
			}
		}
		else if ((DistSq < FARSIGHTTHRESHOLDSQUARED) && (FMath::Square(ViewDir | Dir) > 0.5f * DistSq))
		{
			// Compute the amount of distance along the ViewDir vector. Dir is not normalized
			// Increase priority if we're being looked directly at
			Time *= 2.f;
		}
		else if (DistSq > MEDSIGHTTHRESHOLDSQUARED)
		{
			Time *= 0.4f;
		}
	}

	return NetPriority * Time;
}
  • Base NetPriority에 Viewer와의 거리, 마지막 Replicate 이후 시간 등을 복합적으로 판단해 결정된다.
  • 만약 NetPriority를 Customize하고 싶다면 이 함수를 Override해야 한다.
    • 단, 이는 매우 높은 숙련도와 이해도를 요구한다.

Reference

NetRelevancy

  • Level 상의 Actor들 중 Server상에서 시야 안에 들어오는 Actor들이나 Client에 영향을 Actor들만 Replicate하는 방식
    • Runtime 중에 Spawn/Replicate 되는 Actor들은 Relevant 하지 않으면 Client에서 제거된다.
    • 제거된 Actor의 경우에는 Client에서 더이상 보이지 않는다.

Actor의 현재 Relevancy

/** 
 * Checks to see if this actor is relevant for a specific network connection
 *
 * @param RealViewer - is the "controlling net object" associated with the client for which network relevancy is being checked (typically player controller)
 * @param ViewTarget - is the Actor being used as the point of view for the RealViewer
 * @param SrcLocation - is the viewing location
 *
 * @return bool - true if this actor is network relevant to the client associated with RealViewer 
 */
ENGINE_API virtual bool IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const;

bool AActor::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
{
	if (bAlwaysRelevant || IsOwnedBy(ViewTarget) || IsOwnedBy(RealViewer) || this == ViewTarget || ViewTarget == GetInstigator())
	{
		return true;
	}
	else if (bNetUseOwnerRelevancy && Owner)
	{
		return Owner->IsNetRelevantFor(RealViewer, ViewTarget, SrcLocation);
	}
	else if (bOnlyRelevantToOwner)
	{
		return false;
	}
	else if (RootComponent && RootComponent->GetAttachParent() && RootComponent->GetAttachParent()->GetOwner() && (Cast<USkeletalMeshComponent>(RootComponent->GetAttachParent()) || (RootComponent->GetAttachParent()->GetOwner() == Owner)))
	{
		return RootComponent->GetAttachParent()->GetOwner()->IsNetRelevantFor(RealViewer, ViewTarget, SrcLocation);
	}
	else if(IsHidden() && (!RootComponent || !RootComponent->IsCollisionEnabled()))
	{
		return false;
	}

	if (!RootComponent)
	{
		UE_LOG(LogNet, Warning, TEXT("Actor %s / %s has no root component in AActor::IsNetRelevantFor. (Make bAlwaysRelevant=true?)"), *GetClass()->GetName(), *GetName() );
		return false;
	}

	return !GetDefault<AGameNetworkManager>()->bUseDistanceBasedRelevancy ||
			IsWithinNetRelevancyDistance(SrcLocation);
}
  • Network Driver는 IsNetRelevantFor를 통해 Actor가 각 Connection과 Relevant한지를 판단한다.
    • Relevancy를 Customize하고 싶다면 이 함수를 Override해야 한다.
    • 다만 Override 작업은 높은 이해도와 난이도를 요구한다.
  • 참고로 Actor를 상속받은 Class 중 Pawn과 PlayerController는 위 함수를 Override하여 다른 로직으로 동작한다.

Actor Relevant 생성

/** Forces this actor to be net relevant if it is not already by default	 */
ENGINE_API virtual void ForceNetRelevant();

void AActor::ForceNetRelevant()
{
	if ( !NeedsLoadForClient() )
	{
		UE_LOG(LogSpawn, Warning, TEXT("ForceNetRelevant called for actor that doesn't load on client: %s" ), *GetFullName() );
		return;
	}

	if (RemoteRole == ROLE_None)
	{
		SetReplicates(true);
		bAlwaysRelevant = true;
		if (NetUpdateFrequency == 0.f)
		{
			NetUpdateFrequency = 0.1f;
		}
	}
	ForceNetUpdate();
}
  • Actor에서 ForceNetRelevant를 호출해 강제로 해당 Actor에 Relevancy를 부여할 수 있다.

Customize Relevancy Settings

  • Actor를 상속받은 Class는 사진의 옵션 또는 C++에서 Relevancy Setting을 Customize 할 수 있다.

bAlwaysRelevant

  • 모든 Client에서 조건 없이 Replicate 된다.

bNetUseOwnerRelevancy

  • 해당 Actor의 Owner에게 Relevant 할 때에 Replicate 된다.
  • 모든 Client에게 Replicate 되지 않고, Owner와 Relevant한 Client들에게만 Replicate 된다.

bOnlyRelevantToOwner

  • 해당 Actor의 Owner에게 Relevant 할 때에 Owner에게만 Replicate 된다.

Reference

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

[Network] Remote Procedure Calls(RPCs)  (0) 2024.06.28
[Network] Property Replication  (1) 2024.06.28
[Network] Network Driver  (0) 2024.06.18
[Network] DemoNetDriver 및 Streamer  (0) 2024.06.17
[Network] Beacon  (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

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/demonetdriver-and-streamers-in-unreal-engine?application_version=5.3

DemoNetDeriver

  • Streamer를 사용해 Replay 재생에 필요한 정보를 추출/기록
  • 시청 방식에 따라 Engine에 포함된 다양한 Streamer를 DemoNetDriver에 붙일 수 있음.

Streamer 변경

  • ReplayStreamerOverride 옵션에서 다른 Streamer Factory Module을 변경해 덮어쓸 수 있다.
    • InMemoryNetworkReplayStreaming
    • HttpNetworkReplayStreaming
  • 기본값인 LocalFileNetworkReplayStreaming은
    Default.ini의 NetworkReplayStreaming 섹션의 DefaultFactoryName 변수를 설정해 변경 가능
  • InitBase를 호출하고 적당한 URL 인수를 파라미터로 제공하는 것으로도 변경 가능
  • GameMode는 Replay를 보기 위해 ReplaySpectatorPlayerControllerClass에 지정된 Class를 사용한다.

시간 비용 분산

  • 콘솔 변수 demo.CheckpointSaveMaxMSPerFrame 값을 양수로 설정해
    리플레이 데이터 녹화에 필요한 시간 비용 분산할 수 있다.
  • Frame당 시간 제한 만료 전 Replay에 녹화되지 않은 Actor는 대기열에 등록 되었다가 다음 Frame에 녹화된다.
  • Checkpoint 녹화 및 Take에 드는 시간에 상한선을 두어 게임이 멈추는 현상을 방지하는데 도움이 된다.
  • Actor에 대한 Data에 들어있는 Checkpoint를 여러 Frame걸쳐 찍기 때문에
    재생 도중 약간의 시각적 오류가 발생할 수 있다.
  • 이 기능은 주로 게임에서 Checkpoint 녹화를 위해 지정된 시간 제한보다 오래 걸렸을 때에만 발동한다.
    • 저사양 머신 또는 퍼포먼스를 많이 요하는 게임에서 주로 적용된다는 의미이다.

녹화 우선순위 지정

  • 다음 조건을 만족하면 Replay에 저장되는 Actor의 녹화 순서를 지정 할 수 있다.
    • bPriotizeActors 옵션을 활성화
    • GetReplayPriority 함수를 Implement
  • MaxDesiredRecordTimeMS를 통한 분산 녹화와 함께 사용하면 더 효과적이다.

Checkpoint 녹화 빈도

  • 콘솔 변수 demo.CVarCheckpointUploadDelayInSeconds로 조절
    • 기본값: 30초
  • Checkpoint 사이 기간을 늘리면 Replay를 앞뒤로 문질러 이동하는 작업이 느려진다.
  • 대신 Replay 크기는 줄어든다.

녹화 중 임시 정지

  • bPauseRecording 변수를 true로 설정해 Demo 녹화 중 녹화를 임시 정지할 수 있다.
    • false로 설정하면 녹화가 재게된다.

대체 PlayerController 생성

  • SerViewerOverride를 사용하면 DemoNetDriver가 녹화 목적으로 사용되는 대체 PlayerController를 생성한다.
    • 이 Playercontroller는 Actor의 Network Relevancy, Curling, Priority 결정 방식을 변경할 수 있다.
  • 이 기능은 GamePlay에서 멀리서 일어나는 일을 알 수 없지만, Replay에서는 모든 것을 볼 수 있도록 할 때 좋다.

Slate와의 병렬 처리

  • Slate와 병렬 처리를 위해 아래 콘솔 변수가 0이 아닌 값으로 적용되어야 한다.
    • tick.DoAsyncEndOfFrameTasks
    • demo.ClientRecordAsyncEndOfFrame

주의점

  • Replay 생성 Actor는 Live Gameplay Actor와 마찬가지로 함수 호출을 한다.
  • 이는 Live Actor처럼 행동해 Replay Data를 최소화할 수도 있다.
  • 하지만 공유 Object에 영향을 주는 함수 호출이 Replay Actor에서도
    사용 가능해 원하지 않는 방식으로 Game의 상태에 영향을 줄 수 있다.
    • GameInstance, GameState, GameMode
  • 특히 Memory Streamer가 그러한데, 현재 Play 중인 Live GamePlay를 바로 시청하는 경우이다.
  • Actor에 영향을 주지 않도록 하는 보호 장치로,
    Actor가 Live인지 Replay Level의 일부인지 확인한 후 작업을 처리하는 것을 권장한다.
  • 이러한 문제는 거의 Game에서만 발생할 것이라서 각 Project에서 건 by 건으로 처리해줘야 한다.
  • 이러한 문제의 대표적인 예시로 Replay 도중 Player의 상태가 변하지만 점수가 변하지 않는 것이다.

Replay Data Format

  • Data 측면에서 Replay에는 3가지 게임 상태 정보와 더불어 약간의 Text Data가 추가로 저장된다.

기준 Data

  • Game World의 시작 상태를 설명하는 시작 부분

Checkpoint

  • World에 가해지는 전체 변경 사항의 Snapshot 역할을 한다.
  • 정기적인 사용지 지정 간격으로 나타난다.

점증적 변화량

  • Checkpoint 사이를 채워주는 요소
    • World의 개별 Object들이 각자의 점증적 변화량을 가진다.
  • 이를 통해 Game 내 어느 순간도 Engine에서 빠르고 정확하게 재구성 할 수 있다.

Text Data

  • 다음 2가지로 구성되어 있다.
    • Player가 보는 목록을 만드는데 사용할 수 있는 표시명
    • Game 목록의 검색 또는 필터링에 사용할 수 있는 Custom Text Tag
      • HTTP Streamer 전용

Replay 방식

  • World를 기준 Data로 초기화
  • 선택한 시간 이전의 Checkpoint가 구현된 상태로 변경
  • 가장 최근 Checkpoint 이후 지정된 시간 직전까지의 점증적 변화량 적용

Local File Streamer

  • DemoNetDriver가 가진 기본 Streamer 타입
  • Host Machine의 Replay Data를 로컬 저장장치의 단일 파일에 비동기 기록한다.
    • 때문에 Single Player Game 및 Replay를 로컬에서 재생하는 게임에 적합하다.
  • 저장된 Replay 배포와 관리가 쉽다.
  • 비동기 기록 기능이 있어 Console처럼 HDD 속도가 낮은 System의 Performance가 향상된다.
  • Replay Data File은 Project/Saved/Demos/ 폴더에 저장된다.
  • 확장자는 ".replay"이다.

SaveGame Replay Streamer

  • Replay를 SaveGame Slot을 선택해 저장하는 기능이 추가된 특수한 Local File Streamer
  • Client에 Replay를 저장한 뒤, 다음 Platform의 SaveGame 로드 용 Interface를 통해 읽을 수 있다. 
    •  SaveGame System 없이 Replay를 저장/보관/로드가 가능하다.
  • 주 목적은 SaveGame Slot에 복사되지 않은 Replay 식별/복사/재생/삭제 등의 작업을 Local File과 SaveGame Slot 양쪽에서 할 수 있는 보조 API 활용이다.
  • Console 게임에 적합함.

Null Streamer

  • Replay Data를 Local 디스크에 직접 작성한다.
    • Local 녹화 제작, Single Player Game에 좋다.
  • GamePlay Trailer나 Cut-Scene 영상 제작, Tutorial, TimeAttack Video 시청 및 공유를 할 수 있게 해준다.
  • 4.20 버전 이후로 Deprecated 되었지만 예전 Replay를 재생할 수 있도록 하위 호환성을 유지하고 있다.

Memory Streamer

  • 사용자가 지정할 수 있는 실행 길이의 Replay Data를 Client Machine의 Memory에 저장
  • 최근의 극적인 순간에 대한 즉석 Replay에 최적화 되어 있다. 
    • Sports Game의 즉석 Replay
    • Shooting Game의 Kill Cam
    • Action Game에서의 최종 보스 처치 장면

사용법 세부사항

  • Memory Streamer는 하나의 Session 도중 녹화와 재생, Gameplay가 재개되도록 되어 있다는 점에서 특별하다.
    • Player가 Replay를 시청하는 도중 LiveGame은 모습도 소리도 업싱 계속 진행시키고,
      Replay가 끝나면 매끄럽게 Game이 이어지도록 할 수 있다.
  • 로드 시간에 Engine은 Level을 Static, Dynamic Source, Dynamic Duplicate으로 모은다.
    • 이를 그룹을 통해 Live Gameplay 및 Replya System과의 Level 상호작용 방식이 다음과 같이 정해진다.

Static Level

  • Persistant Level이 아니면서 IsStatic이 마킹 된 Level
  • 작동 시 Gameplay 영향을 받지 않으며, Live Play와 Replay 양쪽에 모두 표시된다.

Dynamic Source Level

  • Persistant Level이면서 IsStatic이 마킹되지 않은 Sublevel
  • Live Gameplay의 영향을 받는다.
  • Replay 도중에는 숨겨지지만, Gameplay에는 여전히 정상 실행된다.

Dynamic Duplicate Level

  • 로드 시간에 Dynamic Source Level에서 복제한 사본
    • Dedicated Server나 Editor Mode에는 존재하지 않음
  • Live Gameplay 도중에는 숨겨진다.
  • Replay는 이 Level에서 발생한 뒤 비워진다.

효율적인 사용법

  • Dynamic Source Level에 대해 DemoNetDriver를 하나, Dynamic Duplicate Level에 대해 또 하나 만들 수 있다.
    • 이 경우 Dynamic Source Level의 Live Gameplay를 녹화한 뒤, Dynamic Duplicate Level에서 재생할 수 있다.
    • Replay 도중 Dynamic Source Level은 숨기고 Dynamic Duplicate Level을 표시하면
      Gameplay는 계속하면서 REplay에 영향받지 않는 Network Update를 받을 수 있다.
  • Static Level 그룹은 언제든 활성화시켜 보이도록 할 수 있다.
  • Live Gameplay에 영향을 받지 않은 것들이 저장 되므로 Replay Process에는 포함시킬 필요가 없다.
    • Static World Geometry
    • BGM
    • Particle
    • Animation
  • Dynamic Source Level은 소멸이나 정지된 적 없이 숨기기만 했을 뿐이다.
    • 그렇기에 Gameplay Replay 시청 도중에도 자연스럽게 진행되고,
      Dynamic Source Level을 다시 시청할 수 있도록 만들 수 있다.
  • 더불어 이 System에는 개발자가 Level을 Static 마킹하여 REplay 녹화 및 재생에서 제외도도록 하는 기능이 있다.
    • 결과적으로 Memory와 시간을 절약할 수 있다.

HTTP Streamer

  • Replay Data를 LAN이나 인터넷을 통해 다른 Machine에 전송할 때 사용
  • 다음 경우에 유용함
    • Dedicated Server Game
      • Server가 항상 Game 내 모든 곳에서 벌어지는 일을 알고 있음.
      • Replay Data 처리 작업에 들어가는 작업을 분산시켜
        단일 서버에서 동시 Hosting 할 수 있는 Game 수를 늘리려는 경우에 좋음.
    • 많은 수의 시청자에게 Live Streaming 방송을 하면서도 반응 속도는 좋게 유지해야 하는 경우
    • Live Streaming 경기나 경기 녹화 기능을 유지하여 언제든지 볼 수 있도록 할 때
  • Game을 실행하는 쪽이 완벽히 제어하는 Computer에서 Data를 캡쳐할 수 있어 치트 감지 툴 역할을 하기 좋다.

사용법 세부사항

  • HTTP Streamer REST API를 통해 Custom Replay Server와 통신한다.
    • GET 및 POST Method를 사용해 Binary나 JSON Format String으로 Data 전송
    • Server를 구성하기 위해서는 URL을 구성해야 한다.

Custom Replay Server URL 구성법

  • Project의 DefaultEngine.ini 파일의 [HttpNetworkReplayStreaming] 섹션 아래 ServerURL 변수
  • http://replay/yourgame.com/ << 이와같은 포맷
  • 마지막 /는 있으나 없으나 문제 없다

HTTP Streamer REST API

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/http-streamer-rest-api-for-unreal-engine?application_version=5.3

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

[Network] Property Replication  (1) 2024.06.28
[Network] Network Property  (0) 2024.06.25
[Network] Network Driver  (0) 2024.06.18
[Network] Beacon  (0) 2024.06.17
[Network] Replication Graph  (1) 2024.06.13

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/using-online-beacons-in-unreal-engine?application_version=5.3

  • 일반 게임 접속을 통하지 않고 RPC를 통해 서버와 접촉해 가벼운 상호작용을 제공하는 Actor
  • Project에 맞는 상호작용, 로직, 정보 요청을 위한 Custom Class 확장을 권장
    • 특수한 경우에 내장 Class를 그대로 사용 하기도 함.

 

OnlineBeacon

  • 대표적인 사용 예시는 다음과 같다.
    • 서비스 품질 정보 요청
    • Client가 참여하고자 하는 Game의 Slot 예약
    • Player 이름 목록
    • 진행 중인 Game의 진행 시간 및 점수

AOnlineBeacon

  • AOnlineBeaconHost, AOnlineBeaconClient의 BaseClass

AOnlineBeaconHost

  • 별도의 NetDriver를 사용해 Remote Client에서 들어오는 OnlineBeacon Access를 Listen.
  • Access를 받으면 등록된 OnlineBeaconHostObject 인스턴스를 대상으로 일치하는 Client를 탐색,
    해당 Object에 Access를 넘겨준다.
  • 이 Class는 파생 Class를 만들 필요가 없다.
    • Client와 등록된 OnlineBeaconHostObject 사이의 초기 접속만 관리하기 때문이다.

AOnlineBeaconClient

  • Host에 접속하여 실제 RPC를 생성
    • 하나는 Client에서 생성
    • 다른 하나는 Server의 OnlineBeaconHost에 등록된 적합한 OnlineBeaconHostObject에 의해 생성
  • GetBeaconType 함수를 사용해 적합한 Host Object Class의 Register Instance에 일치시킨다.
    • 이 방식은 일반적으로 Server가 Spawn하고 Client에 Replicate하는 Actor Spawning 방식과 다르다.
    • 하지만 Client Object와 Server Object 사이에 접속이 이루어진 후,
      어느 한 쪽이 다른 쪽에 RPC를 할 수 있게 되면서 Object Replicate가 정상적으로 이루어지게 되며,
      Server Actor는 Property Replicate와 관련해 Authority를 가지게 된다.
  • OnConnected와 OnFailure 함수를 통해 접속 시 RPC를 호출하거나, 접속 실패 처리를 할 수 있다.
  • Beacon에서 요구하는 Client쪽 작업이 필요할 시 이 Class에서 구현되어야 한다.

AOnlineBeaconHostObject

  • OnlineBeaconClient Class와 짝을 이루도록 만들어져야 하는 Class
    •  Client의 GetBeaconType() 함수의 반환값과, BeaconTypeName에 저장된 값을 일치 시켜 짝을 이룬다.
  • OnlineBeaconHost에 Access를 요구하는 OnlineBeaconClient에 대응하는 OnlineBeaconHostObject가 탐지 되면,
    OnlineBeaconHostObject::SpawnBeaconActor를 통해 OnlineBeaconClient의 사본을 Spawn
    • SpawnBeaconActor로 ClientBeaconActorClass 변수를 사용해 Sapwn 할 Actor Class를 결정
    • 이 CleintBeaconActorClass가 짝을 이룬 OnlineBeaconClient Class로 설정 되어야 함.
  • Spawn 된 OnlineBeaconClient에서 SetBeaconOwner도 호출해야 Client와 통신을 할 수 있다.
  • 위의 기능들은 대부분 BaseClass에서 이루어지기 때문에 Override 할 필요가 없다.

PartyBeacon

  • Party 기반 매치메이킹을 위해 특별히 설계된 클래스
  • Multiplay Game에서 파티 형성 및 GameSession 접속을 관리

PartyBeaconClient

  • Server에 있는 PartyBeaconHost와 통신해 Party 매치메이킹을 요청
    • Server에 Party 정보 전송
    • 성공 여부를 받아 처리하는 Client Interface

PartyBeaconHost

  • Server에서 각 Party의 요청에 따라 GameSession 할당

PartyBeaconState

  • 현재 Party의 상태와 Session의 예약 정보를 관리
    • Party 크기
    • Session에 접속된 Party 수
    • 가능한 최대 Party 수
  • Server로부터 예약 요청 처리에 필요한 정보들을 제공

SpectatorBeacon

  • 관전자 모드를 위한 특수 구성 요소
  • Multiplayer Game에서 관전자가 게임을 관찰할수 있도록 한다.
    • 관전자가 Server에 효율적으로 접속하고, 게임 진행을 관찰할 수 있게하는 과정을 관리

SpectatorBeaconClient

  • Client에서 관전자가 GameServer에 접속 요청을 할 때 사용
  • Server의 SpectatorBeaconHost와 통신해 관전 가능한 세션을 요청, 입장 승인 여부를 확인

SpectatorBeaconHost

  • Server에서 실질적인 관전자의 접속 요청을 수신, 관리
  • 관전자에게 제공할 GameSession을 관리
  • 관전자의 접속을 허가하거나 거부할 수 있는 권한이 있음

SpectatorBeaconState

  • 관전자 접속 상태와 관려 정보를 관리
    • 관전 가능한 GameSession의 상태
    • 현재 관전자 수
    • 최대 관전자 수
  • SpectatorBeaconHost가 이 정보를 기반으로 판단을 내린다.

VoiceSynthSystem

  • 음성 통신 기능을 담당하는 구성 요소
  • Game 내에서 Player간의 실시간 음성 커뮤니케이션이 가능하도록 한다.

VoiceEnvineImpl

  • Unreal Engine의 음성 엔진 구현 Class
    • Hardware와 OS에 특화된 음성 처리를 담당
  • 음성 데이터의 Capture, 처리, 재생을 관리

VoiceInterfaceImpl

  • 음성 데이터 인터페이스의 구현을 제공
  • 다양한 Network 환경에서 음성 데이터의 Reliable 한 전송을 보장
  • 음성 데이터 Pakcet의 생성과 송수신 Interface를 정의

VoicePacketBuffer

  • 음성 데이터 Packet을 임시로 저장하는 버퍼 역할
    • Network 지연, Data Packet 손실을 관리하는데 유용

VoicePacketImpl

  • 실제 음성 데이터 Packet의 구현체
  • 음성 데이터를 압축해 Packet 형태로 만드는 역할

VoiceListenerSynthComponent

  • Unreal Engine의 합성 음성 Component
  • 게임 내 음성 데이터를 재생하는 기능 제공

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

[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
[Network] Replication Graph  (1) 2024.06.13

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/replication-graph-in-unreal-engine?application_version=5.3

https://www.unrealengine.com/ko/tech-blog/replication-graph-overview-and-proper-replication-methods

 

  • 일반적으로 Replicate되는 Actor들은 매 Tick마다 해당 Actor가 Replicate될지 말지 판단을 한다.
    • 게임이 클수록, 동시접속자가 많을수록 CPU 병목현상이 발생하기 쉬워짐
  • Replication Graph는 Actor 단위의 Replicate 판단 여부를 Node 단위로 묶어서 비용을 감소시킨다.

동작 방식

  • 기본적인 동작 방식은 이전의 Actor Replicate와 크게 다르지 않다.
    • 특정 공간 안의 Actor를 별도로 관리(Spatial Partitioning)
    • 대상 클라이언트로부터 먼 거리의 Actor를 제외 (Relevancy Culling)
    • Replicate 빈도 관리 (Frequency Control)

ReplicationGraphNode

  • 동일한 조건으로 Replicate 되어야 할 Actor를 하나로 묶어주는 단위
  • BaseClass인 UReplicationGraphNode는 Pure Virtual Function이 있어서 반드시 이를 상속 받은 Class를 써야 한다.
    • 최소 아래 함수들은 반드시 Implement 되어야 한다.
/** Override this function to initialize the per-class data for replication */
//Initialize UReplicationGraph::GlobalActorReplicationInfoMap.
virtual void InitGlobalActorClassSettings();

/** Override this function to init/configure your project's Global Graph */
//Instantiate new UGraphNodes via ::CreateNewNode. Use ::AddGlobalGraphNode if they are global (for all connections).
virtual void InitGlobalGraphNodes();

//Route actor spawning/despawning to the right node. (Or your nodes can gather the actors themselves)
virtual void RouteAddNetworkActorToNodes(const FNewReplicatedActorInfo& ActorInfo, FGlobalActorReplicationInfo& GlobalInfo);
virtual void RouteRemoveNetworkActorToNodes(const FNewReplicatedActorInfo& ActorInfo);

/** Override this function to init/configure graph for a specific connection. Note they do not all have to be unique: connections can share nodes (e.g, 2 nodes for 2 teams) */
//Initialize per-connection nodes (or associate shared nodes with them via ::AddConnectionGraphNode)
virtual void InitConnectionGraphNodes(UNetReplicationGraphConnection* ConnectionManager);
  • Node 안에 별도의 Child Node를 가질 수 있다.

ReplicationGraphNode_ActorList

  • 간단한 Actor 목록을 관리
  • 복잡한 Frequency나 Relevancy 로직 없이 관리 된다.
  • 소규모 목록에서 사용할 때 유용

ReplicationGraphNode_ActorListFrequencyBuckets

  • Replicate 빈도에 따라 Actor들을 Bucket 단위로 관리
  • 다양한 간격으로 업데이트가 필요한 Actor를 관리할 때 유용

ReplicationGraphNode_AlwaysRelevant

  • 위치, 상태와 관계 없이 모든 Client에게 항상 Replicate 되어야 하는 경우
  • GameMode, Controller, GameState와 같은 GamePlay에 주요한 Actor에게 적합

ReplicationGraphNode_AlwaysRelevant_ForConnection

  • AlwaysRelevant와 유사하지만 그 대상을 특정 Client에게만 제한할 수 있다.
  • 특정 Player에게 필수적인 Actor를 Replicate할 때 적합

ReplicationGraphNode_ConnectionDormancyNode

  • 비활성화 될 여지가 있는 Actor들에 적합
  • 또는 배경의 Actor와 같이 업데이트를 제한하여 자원을 최적화 하는데 적합

ReplicationGraphNode_DormancyNode

  • Actor가 활성화 될 때 모든 Client에 Replicate되어야 하는 경우
  • 간헐적으로 활성화 되는 Actor들에게 유용

ReplicationGraphNode_DynamicSpatialFrequency

  • 공간적 조건에 따라 Replicate 빈도를 동적으로 조절
  • Player의 이동이나 게임 내 Event로 인해 Relevancy가 자주 변경되는 환경에 이상적

ReplicationGraphNode_GridSpatialization2D

  • World를 Grid로 나누고, Grid 위치에 따라 Replicate를 관리
  • 넓은 오픈월드 게임에서 플레이어 주변 Actor만 업데이트하여 네트워크 트래픽 최적화

ReplicationGraphNode_TearOff_ForConnnection

  • Tear Off(서버에서 어느정도 분리된 상태)된 Actor들을 관리
  • 해당 Actor들을 새로운 Client에 적절하게 Replicate 되도록 보장
  • Actor가 급격하게 변경되는 빠른 페이스의 액션이나 파괴 시나리오에서 사용됨.

ReplicationGraph

  • Replication Driver의 기능을 확장한 Class
  • 다수의 ReplicationGraphNode를 관리한다.

작업 방식

Node 추가

  • 기존에 만들었던 ReplicationGraphNode를 InitGlobalGraphNodes에서 생성, 추가한다.
void UBasicReplicationGraph::InitGlobalGraphNodes()
{
	// -----------------------------------------------
	//	Spatial Actors
	// -----------------------------------------------

	GridNode = CreateNewNode<UReplicationGraphNode_GridSpatialization2D>();
	GridNode->CellSize = 10000.f;
	GridNode->SpatialBias = FVector2D(-UE_OLD_WORLD_MAX, -UE_OLD_WORLD_MAX);

	AddGlobalGraphNode(GridNode);

	// -----------------------------------------------
	//	Always Relevant (to everyone) Actors
	// -----------------------------------------------
	AlwaysRelevantNode = CreateNewNode<UReplicationGraphNode_ActorList>();
	AddGlobalGraphNode(AlwaysRelevantNode);
}

Actor 추가

  • AddNetworkActor에서 RouteAddNetworkActorToNodes를 호출
  • RouteAddNetworkActorToNodes에서 등록된 Node를 통해 NotifyAddNetworkActor로 Actor 등록
void UReplicationGraph::AddNetworkActor(AActor* Actor)
{
	LLM_SCOPE_BYTAG(NetRepGraph);
	QUICK_SCOPE_CYCLE_COUNTER(UReplicationGraph_AddNetworkActor);

	if (IsActorValidForReplicationGather(Actor) == false)
	{
		return;
	}

	if (NetDriver && !NetDriver->ShouldReplicateActor(Actor))
	{
		return;
	}

	bool bWasAlreadyThere = false;
	ActiveNetworkActors.Add(Actor, &bWasAlreadyThere);
	if (bWasAlreadyThere)
	{
		// Guarding against double adds
		return;
	}

	ensureMsgf(!Actor->bNetTemporary, TEXT("ReplicationGraph does not support bNetTemporary. Actor: %s has bNetTemporary set."), *Actor->GetPathName());

	// Create global rep info	
	FGlobalActorReplicationInfo& GlobalInfo = GlobalActorReplicationInfoMap.Get(Actor);
	GlobalInfo.bWantsToBeDormant = Actor->NetDormancy > DORM_Awake;

	RouteAddNetworkActorToNodes(FNewReplicatedActorInfo(Actor), GlobalInfo);
}

 

void UReplicationGraph::RouteAddNetworkActorToNodes(const FNewReplicatedActorInfo& ActorInfo, FGlobalActorReplicationInfo& GlobalInfo)
{
	// The base implementation just routes to every global node. Subclasses will want a more direct routing function where possible.
	for (UReplicationGraphNode* Node : GlobalGraphNodes)
	{
		Node->NotifyAddNetworkActor(ActorInfo);
	}
}

Node 탐색

  • 매 Tick마다 ServerReplicateActors를 호출
  • 함수 내부에서 조건에 맞는 Node를  FNewReplicatedActorInfo로 검색
/** This is the struct we use to push new replication actors into the graph. "New" doesn't mean "newly spawned" it means "new to the graph". FIXME: Please suggest a better name! */
struct FNewReplicatedActorInfo
{
	explicit FNewReplicatedActorInfo(const FActorRepListType& InActor) : Actor(InActor), Class(InActor->GetClass())
	{
		StreamingLevelName = GetStreamingLevelNameOfActor(Actor);
	}

	explicit FNewReplicatedActorInfo(const FActorRepListType& InActor, FName OverrideLevelName)
		: Actor(InActor)
		, StreamingLevelName(OverrideLevelName)
		, Class(InActor->GetClass())
	{
	}

	AActor* GetActor() const { return Actor; }

	REPLICATIONGRAPH_API static FName GetStreamingLevelNameOfActor(const AActor* Actor);

	FActorRepListType Actor;
	FName StreamingLevelName;
	UClass* Class;
};

설정 방법

ini 파일 설정

  • Project의 Default.ini에서 아래와 같이 ReplicationDriverClassName 추가
[/Script/OnlineSubsystemUtils.IpNetDriver]
ReplicationDriverClassName="/Script/ProjectName.ClassName"

Instance 반환 함수 Bind

UReplicationDriver::CreateReplicationDriverDelegate().BindLambda([](UNetDriver* ForNetDriver, const FURL& URL, UWorld* World) -> UReplicationDriver*
{
	return NewObject<UMyReplicationDriverClass>(GetTransientPackage());
});

NetReplicationGraphConnection

  • 각 Client 연결에 대한 Replicate Data를 관리
    • ReplicationGraph가 ReplicationDriver를 상속받아 NetDriver의 역할을 한다면, 
      NetReplicationGraphConnection은 ReplicationConnectionDriver를 상속받아 NetConnection의 역할을 한다.
  • ReplcationGraph에서 Replication이 결정 된다면,
    NetReplcationgraphConnection은 해당 Client에게 최적화 된 데이터를 전달해준다.

일반적인 사용 기준

  • Actor의 위치에 따라 Group을 나눈다.
  • 비활성화된 Actor를 식별하여 별도의 목록으로 관리한다.
  • Character가 주워서 들고 다닐 수 있다면, 소유자와 같이 업데이트 한다.
  • 모든 Client가 Replicate 받는 특수 목록을 만든다.
  • 특정 Client에 Relevancy를 갖는 특수 목록을 만든다.

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

[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
[Network] Beacon  (0) 2024.06.17

+ Recent posts