참고 링크

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

+ Recent posts