액터 리플리케이션의 내부 동작 원리를 이해하자.

  • Actor Replication의 Priority의 개념과 동작 방식 이해하기
  • Actor의 휴면 상태 설정과 활용 방법의 학습 
  • Server에서 Actor Replication을 처리하는 전반적인 플로우의 이해 

 

인프런 이득우님의 '언리얼 프로그래밍 Part3 - 네트웍 멀티플레이 프레임웍의 이해' 강의를 참고하였습니다. 
😎 [이득우의 언리얼 프로그래밍 Part3 - 네트웍 멀티플레이 프레임웍의 이해] 강의 들으러 가기!

 

 

목차

     

     


     

     

    액터 리플리케이션의 우선권 Actor Replication Priority

     

    Actor Replication의 Priority의 개념과 동작 방식 이해하기


     

     

    학습할 내용


     

     

    우선권(Priority)이란?

     

    •  Client에 보내는 대역폭(NetBandwidth)은 한정되어 있음.
    •  Client에 보낼 Actor 중, 우선권이 높은 Actor의 데이터를 우선 전달하도록 설계되어 있음.
    •  Actor에 설정된 NetPriority 우선권 값을 활용해 전송 순서를 결정함 

     

     

    NetPriority 속성 (기본값)

     

    NetPriority 속성 (기본값)

    Actor의 종류 NetPriority
    Actor 1.0
    Pawn 3.0
    PlayerController 3.0

     

    일반적인 Actor는 1의 값이, Pawn과 PlayerController와 같이 중요한 역할을 담당하는 Actor에는 3의 값이 지정되어 있다. 

    Actor의 현재 우선권은 AActor::GetNetPriority() 가상 함수를 사용해 계산한다. 

     

     

    https://docs.unrealengine.com/5.1/ko/actor-relevancy-and-priority-in-unreal-engine/

     

    액터 연관성 및 우선권

    액터 연관성 및 우선권 관련 정보입니다.

    docs.unrealengine.com


     

    우선권 설정 로직

     

    우선권 설정 로직

    •  마지막으로 패킷을 보낸후의 경과 시간과 최초 우선권 값을 곱해 최종 우선권 값을 생성
    •  최종 우선권 값을 사용해 Client에 보낼 Actor 목록을 정렬함 
    •  Network이 포화(Saturation)될 때까지 정렬된 순서대로 Replication을 수행  
    •  Network이 포화(Saturation)되면 해당 Actor는 다음 Server Tick으로 넘김 

     

    Tick1에서 Pawn, PlayerController, GameState, PlayerState을 처리하고 Fountain을 미처 처리하지 못했다. 해당 경우, Tick2에서 Fountain이 우선권이 다른 Actor보다 높아진다.

     

    Tick1에서 Pawn, PlayerController, GameState, PlayerState을 처리하고 Fountain을 미처 처리하지 못했다. 해당 경우, Tick2에서 Fountain이 우선권이 다른 Actor보다 높아진다.  

     

     

    Actor의 우선권을 최종 계산하는 GetNetPriority 함수 로직

    Actor의 상태 가중치 배수 
     현재 View Target인 경우 4배
     잘 보이는 경우 2배
     잘 안 보이는경우  상황에 따라 0.2 또는 0.4 
     PlayerController가 Viewer인 경우  4배

     

    GetNetPriority() 함수에 구현되어 있다.

     

     


     

     

    예제코드

     

    DefaultEngine.ini

    더보기
    [Core.Log]
    LogNetTraffic=Log // 에디터에서 보여주도록 설정
    
    [/Script/Engine.GameNetworkManager]
    TotalNetBandwidth=3200 // 기본값이 32000인데 1/10로 줄임.

     

    ABFountain.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "GameFramework/Actor.h"
    #include "ABFountain.generated.h"
    
    UCLASS()
    class ARENABATTLE_API AABFountain : public AActor
    {
    	GENERATED_BODY()
    	
    public:	
    	AABFountain();
    
    protected:
    	virtual void BeginPlay() override;
    
    public:	
    	virtual void Tick(float DeltaTime) override;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Mesh)
    	TObjectPtr<class UStaticMeshComponent> Body;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Mesh)
    	TObjectPtr<class UStaticMeshComponent> Water;
    
    public:
    	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
    	virtual void OnActorChannelOpen(class FInBunch& InBunch, class UNetConnection* Connection) override;
    	virtual bool IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const override;
    
    	UPROPERTY(ReplicatedUsing = OnRep_ServerRotationYaw)
    	float ServerRotationYaw;
        
    	UPROPERTY(Replicated)
    	TArray<float> BigData; // Data
    
    	UFUNCTION()
    	void OnRep_ServerRotationYaw();
    
    	float RotationRate = 30.0f;
    	float ClientTimeSinceUpdate = 0.0f;
    	float ClientTimeBetweenLastUpdate = 0.0f;
    
    	//float BigDataElement = 0.0f;
    };

     

    ABFountain.cpp

    더보기
    #include "Prop/ABFountain.h"
    #include "Components/StaticMeshComponent.h"
    #include "Components/PointLightComponent.h"
    #include "Net/UnrealNetwork.h"
    #include "ArenaBattle.h"
    
    AABFountain::AABFountain()
    {
     	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    	PrimaryActorTick.bCanEverTick = true;
    
    	Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Body"));
    	Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Water"));
    
    	RootComponent = Body;
    	Water->SetupAttachment(Body);
    	Water->SetRelativeLocation(FVector(0.0f, 0.0f, 132.0f));
    
    	static ConstructorHelpers::FObjectFinder<UStaticMesh> BodyMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Castle_Fountain_01.SM_Plains_Castle_Fountain_01'"));
    	if (BodyMeshRef.Object)
    	{
    		Body->SetStaticMesh(BodyMeshRef.Object);
    	}
    
    	static ConstructorHelpers::FObjectFinder<UStaticMesh> WaterMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Fountain_02.SM_Plains_Fountain_02'"));
    	if (WaterMeshRef.Object)
    	{
    		Water->SetStaticMesh(WaterMeshRef.Object);
    	}
    
    	bReplicates = true;
    	NetUpdateFrequency = 1.0f;
    	NetCullDistanceSquared = 4000000.0f;
    	//NetDormancy = DORM_Initial;
    }
    
    void AABFountain::BeginPlay()
    {
    	Super::BeginPlay();
    	
    	if (HasAuthority())
    	{
        	// 이 부분 작업함!
    		FTimerHandle Handle;
    		GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
    			{
    				BigData.Init(BigDataElement, 1000); // 4000byte가 되어 앞서 설정한 3200byte를 넘어서게 됨. 포화상태(Saturation)에 로그에 기록이 남게 된다.
    				BigDataElement += 1.0;
    			}
    		), 1.0f, true, 0.0f);
    	}
    }
    
    void AABFountain::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    
    	if (HasAuthority())
    	{
    		AddActorLocalRotation(FRotator(0.0f, RotationRate * DeltaTime, 0.0f));
    		ServerRotationYaw = RootComponent->GetComponentRotation().Yaw;
    	}
    	else
    	{
    		ClientTimeSinceUpdate += DeltaTime;
    		if (ClientTimeBetweenLastUpdate < KINDA_SMALL_NUMBER)
    		{
    			return;
    		}
    
    		const float EstimateRotationYaw = ServerRotationYaw + RotationRate * ClientTimeBetweenLastUpdate;
    		const float LerpRatio = ClientTimeSinceUpdate / ClientTimeBetweenLastUpdate;
    
    		FRotator ClientRotator = RootComponent->GetComponentRotation();
    		const float ClientNewYaw = FMath::Lerp(ServerRotationYaw, EstimateRotationYaw, LerpRatio);
    		ClientRotator.Yaw = ClientNewYaw;
    		RootComponent->SetWorldRotation(ClientRotator);
    	}
    }
    
    void AABFountain::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
    {
    	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    	DOREPLIFETIME(AABFountain, ServerRotationYaw);
    	DOREPLIFETIME(AABFountain, BigData);
    }
    
    void AABFountain::OnActorChannelOpen(FInBunch& InBunch, UNetConnection* Connection)
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    
    	Super::OnActorChannelOpen(InBunch, Connection);
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }
    
    bool AABFountain::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
    {
    	bool NetRelevantResult = Super::IsNetRelevantFor(RealViewer, ViewTarget, SrcLocation);
    	if (!NetRelevantResult)
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("Not Relevant:[%s] %s"), *RealViewer->GetName(), *SrcLocation.ToCompactString());
    	}
    
    	return NetRelevantResult;
    }
    
    void AABFountain::OnRep_ServerRotationYaw()
    {
    	//AB_LOG(LogABNetwork, Log, TEXT("Yaw : %f"), ServerRotationYaw);
    
    	FRotator NewRotator = RootComponent->GetComponentRotation();
    	NewRotator.Yaw = ServerRotationYaw;
    	RootComponent->SetWorldRotation(NewRotator);
    
    	ClientTimeBetweenLastUpdate = ClientTimeSinceUpdate;
    	ClientTimeSinceUpdate = 0.0f;
    }

     

     

    실행화면

     

    의도적으로 네트워크 트래픽을 낮추고 굉장히 큰 데이터를 추가함으써 네트웍이 어떻게 포화되는지 포화됐을때 어떻게 처리하는지 확인해보자.

     

    Log 분석

    -  " 현재 Network이 포화돼서 NetUpdateTime에 도달했지만 다음 Tick에서 처리된다. "는 문구가 나온다.


     

     

     

    Actor Dormancy (액터의 휴면상태)

     

    Actor의 휴면 상태 설정과 활용 방법의 학습


     

    액터의 휴면(Dormancy)이란?

     

    •  Actor의 전송을 최소화 하기위해 연관성과 더불어 제공하는 속성
    •  Actor가 휴면 상태라면 연관성이 있더라도 액터 리플리케이(RPC)을 수행하지 않음.

     

     

    언리얼 엔진에서 지정한 휴면 상태 

       
     DORM_Never   Actor는 휴면이 없음
     DORM_Awake   Actor는 깨어나 있음 
     DORM_DormantAll   Actor는 언제나 휴면 상태. 필요시에 깨울 수 있음
     DORM_DormantPartial   특정 조건을 만족할 경우에만 Replication을 수행
     DORM_Initial   Actor를 휴면상태로 시작하고 필요한 때 깨우도록 설정할 수 있음  

     

    "

    속성 Replication에 사용시에는 DORM_Initial만 고려하는 것이 좋다 

    "


     

     

    예제 코드

     

    ABFountain.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "GameFramework/Actor.h"
    #include "ABFountain.generated.h"
    
    UCLASS()
    class ARENABATTLE_API AABFountain : public AActor
    {
    	GENERATED_BODY()
    	
    public:	
    	AABFountain();
    
    protected:
    	virtual void BeginPlay() override;
    
    public:	
    	virtual void Tick(float DeltaTime) override;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Mesh)
    	TObjectPtr<class UStaticMeshComponent> Body;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Mesh)
    	TObjectPtr<class UStaticMeshComponent> Water;
    
    public:
    	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
    	virtual void OnActorChannelOpen(class FInBunch& InBunch, class UNetConnection* Connection) override;
    	virtual bool IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const override;
    
    	UPROPERTY(ReplicatedUsing = OnRep_ServerRotationYaw)
    	float ServerRotationYaw;
    
    	UPROPERTY(ReplicatedUsing = OnRep_ServerLightColor)
    	FLinearColor ServerLightColor;
    	//UPROPERTY(Replicated)
    	//TArray<float> BigData;
    
    	UFUNCTION()
    	void OnRep_ServerRotationYaw();
    
    	UFUNCTION()
    	void OnRep_ServerLightColor();
    
    	float RotationRate = 30.0f;
    	float ClientTimeSinceUpdate = 0.0f;
    	float ClientTimeBetweenLastUpdate = 0.0f;
    
    	//float BigDataElement = 0.0f;
    };

     

    ABFountain.cpp

    더보기
    #include "Prop/ABFountain.h"
    #include "Components/StaticMeshComponent.h"
    #include "Components/PointLightComponent.h"
    #include "Net/UnrealNetwork.h"
    #include "ArenaBattle.h"
    
    AABFountain::AABFountain()
    {
     	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    	PrimaryActorTick.bCanEverTick = true;
    
    	Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Body"));
    	Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Water"));
    
    	RootComponent = Body;
    	Water->SetupAttachment(Body);
    	Water->SetRelativeLocation(FVector(0.0f, 0.0f, 132.0f));
    
    	static ConstructorHelpers::FObjectFinder<UStaticMesh> BodyMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Castle_Fountain_01.SM_Plains_Castle_Fountain_01'"));
    	if (BodyMeshRef.Object)
    	{
    		Body->SetStaticMesh(BodyMeshRef.Object);
    	}
    
    	static ConstructorHelpers::FObjectFinder<UStaticMesh> WaterMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Fountain_02.SM_Plains_Fountain_02'"));
    	if (WaterMeshRef.Object)
    	{
    		Water->SetStaticMesh(WaterMeshRef.Object);
    	}
    
    	bReplicates = true;
    	NetUpdateFrequency = 1.0f;
    	NetCullDistanceSquared = 4000000.0f;
    	//NetDormancy = DORM_Initial;
    }
    
    void AABFountain::BeginPlay()
    {
    	Super::BeginPlay();
    	
    	if (HasAuthority())
    	{
    		FTimerHandle Handle;
    		GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
    			{
    				//BigData.Init(BigDataElement, 1000);
    				//BigDataElement += 1.0;
    				ServerLightColor = FLinearColor(FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), 1.0f);
    				
                    // OnRep_함수의 경우 클라이언트에서는 당연히 호출되지만, 
                    // 서버에서는 동작을 하지 않기 때문에 관련된 액션을 취할 수 있게 서버는 해당 함수를 명시적호출 
                    OnRep_ServerLightColor(); 
    			}
    		), 1.0f, true, 0.0f);
    
    		FTimerHandle Handle2;
    		GetWorld()->GetTimerManager().SetTimer(Handle2, FTimerDelegate::CreateLambda([&]
    			{
    				FlushNetDormancy(); // Server 휴면 상태에서 10초가 지난 후에 Client에서 색상 변경이 시작됨
    			}
    		), 10.0f, false, -1.0f);
    	}
    }
    
    void AABFountain::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    
    	if (HasAuthority())
    	{
    		AddActorLocalRotation(FRotator(0.0f, RotationRate * DeltaTime, 0.0f));
    		ServerRotationYaw = RootComponent->GetComponentRotation().Yaw;
    	}
    	else
    	{
    		ClientTimeSinceUpdate += DeltaTime;
    		if (ClientTimeBetweenLastUpdate < KINDA_SMALL_NUMBER)
    		{
    			return;
    		}
    
    		const float EstimateRotationYaw = ServerRotationYaw + RotationRate * ClientTimeBetweenLastUpdate;
    		const float LerpRatio = ClientTimeSinceUpdate / ClientTimeBetweenLastUpdate;
    
    		FRotator ClientRotator = RootComponent->GetComponentRotation();
    		const float ClientNewYaw = FMath::Lerp(ServerRotationYaw, EstimateRotationYaw, LerpRatio);
    		ClientRotator.Yaw = ClientNewYaw;
    		RootComponent->SetWorldRotation(ClientRotator);
    	}
    }
    
    void AABFountain::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
    {
    	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    	DOREPLIFETIME(AABFountain, ServerRotationYaw);
    	//DOREPLIFETIME(AABFountain, BigData);
    	DOREPLIFETIME(AABFountain, ServerLightColor);
    }
    
    void AABFountain::OnActorChannelOpen(FInBunch& InBunch, UNetConnection* Connection)
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    
    	Super::OnActorChannelOpen(InBunch, Connection);
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }
    
    bool AABFountain::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
    {
    	bool NetRelevantResult = Super::IsNetRelevantFor(RealViewer, ViewTarget, SrcLocation);
    	if (!NetRelevantResult)
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("Not Relevant:[%s] %s"), *RealViewer->GetName(), *SrcLocation.ToCompactString());
    	}
    
    	return NetRelevantResult;
    }
    
    void AABFountain::OnRep_ServerRotationYaw()
    {
    	//AB_LOG(LogABNetwork, Log, TEXT("Yaw : %f"), ServerRotationYaw);
    
    	FRotator NewRotator = RootComponent->GetComponentRotation();
    	NewRotator.Yaw = ServerRotationYaw;
    	RootComponent->SetWorldRotation(NewRotator);
    
    	ClientTimeBetweenLastUpdate = ClientTimeSinceUpdate;
    	ClientTimeSinceUpdate = 0.0f;
    }
    
    void AABFountain::OnRep_ServerLightColor()
    {
    	if (!HasAuthority())
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("LightColor : %s"), *ServerLightColor.ToString());
    	}
    
    	UPointLightComponent* PointLight = Cast<UPointLightComponent>(GetComponentByClass(UPointLightComponent::StaticClass()));
    	if (PointLight)
    	{
    		PointLight->SetLightColor(ServerLightColor);
    	}
    }

     

     

    실행화면

     

     

    Server로부터 휴면상태에서 10초가 지난 후 Client의 분수 색상이 변경되기 시작한다. 10초 후에 Server로부터 수신된 내용이 반영이된다. (참고: Server를 먼저 몇 초간 켜놓은 후 Client를 켜서 10초가 안 된것처럼 보이는거다. 10초후다.)


     

     

    Conditional Property Replication (조건식 프로퍼티 리플리케이션)

     

     

    COND_SimulatedOnly:  이 액터의 시뮬레이션 사본이 있는 클라이언트에만 리플리케이트한다.

     

    "

    The COND_SimulatedOnly flag that was passed into the condition macro will cause an extra check to be performed before even considering this property for replication.In this case, it will only replicate to clients that have a simulating copy of this actor.

    One of the big benefits of this is that it saves bandwidth; since we've determined that client that has the autonomous proxy version of this actor doesn't need to know about this property (this client is setting this property directly for prediction purposes for example). Another benefit is that for the client not receiving this property, the server won't step on this client's local copy.

    "

     

    https://docs.unrealengine.com/5.1/ko/conditional-property-replication-in-unreal-engine/

     

    조건식 프로퍼티 리플리케이션

    조건에 따라 액터 프로퍼티를 리플리케이트하는 법에 대한 상세 정보입니다.

    docs.unrealengine.com

     


     

     

     

     

    액터 리플리케이션 로우레벨 Actor Replication - Low Level Flow

     

    Server에서 Actor Replication을 처리하는 전반적인 플로우의 이해 


     

     

    Actor Replication Flow

     

    https://docs.unrealengine.com/4.27/ko/InteractiveExperiences/Networking/Actors/ReplicationFlow/

     

    자세한 액터 리플리케이션 흐름

    로우 레벨 액터 리플리케이션에 대한 상세 설명입니다.

    docs.unrealengine.com


     

     

    Actor Replication Flow 도식화

     

     

    FNetworkObjectInfo  구조체

    • Server는 매 Tick 마다 Client에 데이터를 전송한다. 이 때, 각 Actor의 정보는 FNetworkObjectInfo 구조체에 정의가 되어 있다.
    • NextUpdateTime 
      • 다음에 전송할 시간.
      • 빈도를 설정하는 속성인 NetUpdate Frequency 속성과 관련 있다.   
    • bPendingNetUpdate
      • 전송되어 있는지를 파악하는 변수.
      • 포화상태가 되어 패킷을 보내지 못할 때 체크되는 값. 우선권과 관련이 있다.

     

    ConsiderList

    • Actor 정보를 모아둔 것.
    • ConsiderList에 들어간 Actor들은 각각 자신의 PreReplication() 이란 함수를 호출함으로써 전송할 준비가 되었다고 알려준다. 

     

    PriorityList 

    • Server에 접속한 Client 마다 생성된 목록.
    • Actor 묶음들
    • 우선권을 계산해서 이 Actor들을 정렬. 각 Client에서 먼저 보내야 되는 Actor의 대기열이 생성됨.
    • 이 우선권을 정렬하기 위해서 내부적으로 FActorPriority 구조체를 사용.
      • FActorPriority는 ' 우선권 값 '과  ' FNetworkObjectInfo ' 을 가지고 있음.
    • PriorityList는 Actor의 정보를 하나씩 Client에 보내다가 Network 상태가 포화되면 FActorPriorityInfo의 bPendingNetUpdate의 플래그를 활성화해서 다음 Server Tick에서 이를 참고하도록 조정한다. 
    • 다른 Client는 해당 Client의 Viewer에 따라 연관성과 우선권이 달라지기 때문에 해당 Client에 맞는 새로운 우선권 리스트가 만들어지고 이에 따라서 관련된 정보가 다르게 전송된다. 

     

     

    NetDriver.cpp 내에 구현된 Actor Replication Flow

     

     

     


     

     

     

    예제 코드

     

    ABFountain.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "GameFramework/Actor.h"
    #include "ABFountain.generated.h"
    
    UCLASS()
    class ARENABATTLE_API AABFountain : public AActor
    {
    	GENERATED_BODY()
    	
    public:	
    	AABFountain();
    
    protected:
    	virtual void BeginPlay() override;
    
    public:	
    	virtual void Tick(float DeltaTime) override;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Mesh)
    	TObjectPtr<class UStaticMeshComponent> Body;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Mesh)
    	TObjectPtr<class UStaticMeshComponent> Water;
    
    public:
    	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
    	virtual void OnActorChannelOpen(class FInBunch& InBunch, class UNetConnection* Connection) override;
    	virtual bool IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const override;
    	virtual void PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker) override;
    
    	UPROPERTY(ReplicatedUsing = OnRep_ServerRotationYaw)
    	float ServerRotationYaw;
    
    	UPROPERTY(ReplicatedUsing = OnRep_ServerLightColor)
    	FLinearColor ServerLightColor;
    	//UPROPERTY(Replicated)
    	//TArray<float> BigData;
    
    	UFUNCTION()
    	void OnRep_ServerRotationYaw();
    
    	UFUNCTION()
    	void OnRep_ServerLightColor();
    
    	float RotationRate = 30.0f;
    	float ClientTimeSinceUpdate = 0.0f;
    	float ClientTimeBetweenLastUpdate = 0.0f;
    
    	//float BigDataElement = 0.0f;
    };

     

    함수 추가

    • virtual void PreReplication()

     

    ABFountain.cpp

    더보기
    #include "Prop/ABFountain.h"
    #include "Components/StaticMeshComponent.h"
    #include "Components/PointLightComponent.h"
    #include "Net/UnrealNetwork.h"
    #include "ArenaBattle.h"
    
    AABFountain::AABFountain()
    {
     	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    	PrimaryActorTick.bCanEverTick = true;
    
    	Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Body"));
    	Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Water"));
    
    	RootComponent = Body;
    	Water->SetupAttachment(Body);
    	Water->SetRelativeLocation(FVector(0.0f, 0.0f, 132.0f));
    
    	static ConstructorHelpers::FObjectFinder<UStaticMesh> BodyMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Castle_Fountain_01.SM_Plains_Castle_Fountain_01'"));
    	if (BodyMeshRef.Object)
    	{
    		Body->SetStaticMesh(BodyMeshRef.Object);
    	}
    
    	static ConstructorHelpers::FObjectFinder<UStaticMesh> WaterMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Fountain_02.SM_Plains_Fountain_02'"));
    	if (WaterMeshRef.Object)
    	{
    		Water->SetStaticMesh(WaterMeshRef.Object);
    	}
    
    	bReplicates = true;
    	NetUpdateFrequency = 1.0f;
    	NetCullDistanceSquared = 4000000.0f;
    	//NetDormancy = DORM_Initial;
    }
    
    void AABFountain::BeginPlay()
    {
    	Super::BeginPlay();
    	
    	if (HasAuthority())
    	{
    		FTimerHandle Handle;
    		GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
    			{
    				//BigData.Init(BigDataElement, 1000);
    				//BigDataElement += 1.0;
    				ServerLightColor = FLinearColor(FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), 1.0f);
    				OnRep_ServerLightColor(); // Client에는 당연히 호출되지만, Server는 명시적호출 
    			}
    		), 1.0f, true, 0.0f);
    
    		FTimerHandle Handle2;
    		GetWorld()->GetTimerManager().SetTimer(Handle2, FTimerDelegate::CreateLambda([&]
    			{
    				FlushNetDormancy(); // Server 휴면 상태에서 10초가 지난 후에 Client에서 색상 변경이 시작됨
    			}
    		), 10.0f, false, -1.0f);
    	}
    }
    
    void AABFountain::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    
    	if (HasAuthority())
    	{
    		AddActorLocalRotation(FRotator(0.0f, RotationRate * DeltaTime, 0.0f));
    		ServerRotationYaw = RootComponent->GetComponentRotation().Yaw;
    	}
    	else
    	{
    		ClientTimeSinceUpdate += DeltaTime;
    		if (ClientTimeBetweenLastUpdate < KINDA_SMALL_NUMBER)
    		{
    			return;
    		}
    
    		const float EstimateRotationYaw = ServerRotationYaw + RotationRate * ClientTimeBetweenLastUpdate;
    		const float LerpRatio = ClientTimeSinceUpdate / ClientTimeBetweenLastUpdate;
    
    		FRotator ClientRotator = RootComponent->GetComponentRotation();
    		const float ClientNewYaw = FMath::Lerp(ServerRotationYaw, EstimateRotationYaw, LerpRatio);
    		ClientRotator.Yaw = ClientNewYaw;
    		RootComponent->SetWorldRotation(ClientRotator);
    	}
    }
    
    void AABFountain::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
    {
    	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    	DOREPLIFETIME(AABFountain, ServerRotationYaw);
    	//DOREPLIFETIME(AABFountain, BigData);
    	DOREPLIFETIME(AABFountain, ServerLightColor);
    }
    
    void AABFountain::OnActorChannelOpen(FInBunch& InBunch, UNetConnection* Connection)
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    
    	Super::OnActorChannelOpen(InBunch, Connection);
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }
    
    bool AABFountain::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
    {
    	bool NetRelevantResult = Super::IsNetRelevantFor(RealViewer, ViewTarget, SrcLocation);
    	if (!NetRelevantResult)
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("Not Relevant:[%s] %s"), *RealViewer->GetName(), *SrcLocation.ToCompactString());
    	}
    
    	return NetRelevantResult;
    }
    
    void AABFountain::PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker)
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    	Super::PreReplication(ChangedPropertyTracker);
    }
    
    void AABFountain::OnRep_ServerRotationYaw()
    {
    	//AB_LOG(LogABNetwork, Log, TEXT("Yaw : %f"), ServerRotationYaw);
    
    	FRotator NewRotator = RootComponent->GetComponentRotation();
    	NewRotator.Yaw = ServerRotationYaw;
    	RootComponent->SetWorldRotation(NewRotator);
    
    	ClientTimeBetweenLastUpdate = ClientTimeSinceUpdate;
    	ClientTimeSinceUpdate = 0.0f;
    }
    
    void AABFountain::OnRep_ServerLightColor()
    {
    	if (!HasAuthority())
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("LightColor : %s"), *ServerLightColor.ToString());
    	}
    
    	UPointLightComponent* PointLight = Cast<UPointLightComponent>(GetComponentByClass(UPointLightComponent::StaticClass()));
    	if (PointLight)
    	{
    		PointLight->SetLightColor(ServerLightColor);
    	}
    }

    함수 정의

    • virtual void PreReplication()