Network 통신 상태를 확인할 수 있는 언리얼 인사이트 도구의 활용 방법에 대해 알아보자. Actor Replication의 빈도 속성을 활용한 효율적인 Actor Replication의 구현해보자. Actor Replication에서 연관성의 개념과 용어 및 관련 설정의 학습하자. 거리 속성을 변경에 따른 연관성 동작의 구현해보자.

 

목차

     

     

     


     

     

     

    Unreal Insight

     

    액터 리플케이션의 연관성과 빈도에 대한 동작 방식의 확인


     

     

    학습할 내용 -  연과성 & 빈도

     


     

     

     

     

    언리얼 인사이트 Unreal Insight

     

    Network 통신 상태를 확인할 수 있는 언리얼 인사이트 도구의 활용 방법


     

    언리얼 인사이트 Unreal Insights

     

    • 언리얼 프로그램의 다양한 퍼포먼스를 체크할 수 있는 강력한 프로파일링 도구
    • 언리얼 엔진에 포함되어 있음.
    • 프로그램 프로파일링 뿐만 아니라 네트웍 상태도 확인할수 있음. (Network Insight)


     

     

    Unreal Insight 구동을 위한 환경 설정

     

    • 언리얼 엔진의 설치 폴더 확인
    • 인사이트 프로그램의 숏컷 새성
    • 언리얼 에디터 실행 파일의 Path 설정
    • 언리얼 에디터를 구동하기 위한 배치파일 제작

     

    NetworkInsightsEditor.bat

    UnrealEditor.exe%cd%\프로젝트명.uproject -NetTrace= -trace=net

     

     

     

    ※ 선행사항:  언리얼 에디터 내의  Plugin 설치

     

     

    1.  언리얼 엔진의 설치 폴더 확인

    • C:\Program Files\Epic Games\UE_5.3\Engine\Binaries\Win64

     

     

    2.  인사이트 프로그램의 Shortcut 생성

    • UnrealInsight.exe 우클릭 - 작업표시줄에 고정

     

     

    3.  언리얼 에디터 실행 파일의 Path 설정

     

     

    4.  언리얼 에디터를 구동하기 위한 배치파일 제작 

    • 프로젝트 폴더에 .bat 파일 생성
    • NetworkInsightsEditor.bat
      • UnrealEditor.exe %cd%\프로젝트명.uproject -NetTrace=1 -trace=net

    텍스트 파일을 생성하고 .bat로 저장

     

    NetworkInsightsEditor.bat 파일을 더블 클릭하여 실행

     

    실행하면 프로젝트 파일 경로를 찾고 NetTrace가 시작되고 언리얼 에디터가 열림.

    일반적인 방법으로 언리얼 에디터+Visual Studio를 실행하지 않고 .bat로 에디터를 실행시킨다.

     

    에디터가 열린다.

     

     

    그 다음으로 언리얼 인사이트(Unreal Insight) 툴을 실행시킨다.

     

     

    자신 컴퓨터의 IP를 로컬 호스트로 설정한다.

     

     

     

     

     

    서버와 클라이언트 정보를 확인할 수 있다.


     

     

     

    Actor Replication 빈도 설정

     

    Actor Replication의 빈도 속성을 활용한 효율적인 Actor Replication의 구현

    Actor Replication에서 연관성의 개념과 용어 및 관련 설정의 학습

    거리 속성을 변경에 따른 연관성 동작의 실습 


     

     

    Actor Replication의 빈도(Frequency)란? 

     

    Actor Replication의 빈도란

    Client와 Server 간에 진행되는 통신 빈도 "

    를 말한다.

     

     

    NetUpdateFrequency:  Replication 빈도의 최대치 설정

    • 1초당 몇 번 Replication을 시도할지 지정한 값
    • 기본값은 100. 즉, 이론적으로 Server는 1/100초 간격으로 Replication을 시도함

     

    Network 빈도는 최대치일 뿐 이를 보장하진 않음

    • Server의 Tick Rate에 따라 Replication이 발생하지만, Server의 성능에 따라 달라짐.
      • ex. NetUpdateFrequency = 100으로 지정하더라도 서버의 틱이 1초에 50번 밖에 수행되지 않는다면 통신은 50번의 빈도로 진행된다. 
    • Server의 성능이 Network 빈도보다 낮은 경우, Server의 성능으로 복제됨.
    • 일반적으로 그래픽 기능이 없는 Dedicated Server가 게임 기능이 탑재된 Listen Server 보다 더 좋은 성능을 발휘함.

     

    (좌) 기본 업데이트 빈도를 사용한 결과  /  (우) 업데이트 빈도를 줄인 결과

    왼쪽은 기본 빈도수인 100 설정. 오른쪽은 빈도수 1로 설정(1초에 한 번씩 전송됨)


     

     

    주요 Actor에 설정된 빈도 기본값

     

    NetUpdateFrequency 속성

    Actor의 종류 업데이트 빈도  사용
    Actor 100.0  
    Pawn 100.0  
    PlayerController 100.0  
    GameState 10.0  게임상태 정보는 플레이에 영향을 주지 않기 때문에 낮게 설정되어 있다
    PlayerState 1.0  플레이어 점수

     


     

     

    Network 데이터 줄이기

     

    규칙적으로 움직이는 Actor의 Network 통신 데이터를 줄이는 예제

     

    NetUpdateFrequency 속성 값을 1로 설정(= 1초에 한 번씩 전달).

     

     

    데이터 공백을 Client에서 부드러운 움직임으로 보완하기

    • 이전 복제된 데이터에 기반해 현재 틱에서의 회전 값을 예측.
    • Client에서 예측된 값을 보간해 회전.

     


     

     

     

    적응형 네트워크 업데이트 (Adaptive Network Update)  - 고정형 빈도가 아닌 방법

     

    적응형 네트워크 업데이트란?

    " 유의미한 업데이트가 없으면 (언리얼 엔진이 스스로 파악해서) 빈도를 줄여서 부하를 줄이는 기법 "

     

    MinNetUpdateFrequency

    • Replication 빈도의 최소치 설정을 사용함
    • 기본값은 2

     

    최소값과 최대값 사이에서 현재 Actor에 맞는 최적의 전송 타이밍을 설정함.

     

    이를 사용하기 위해서는 설정에서 직접 활성화 시켜줘야 함.

    • Config - DefaultEngine.ini를 열어 아래의 문구를 기입해야 함.
    • [SystemSettings]
      net.UseAdaptiveNetUpdateFrequelncy=1


     

     

    연관성(Relevancy)이란?

     

    서버의 관점에서 현재 Actor가 Client의 Connection에 관련된 Actor인지 확인하는 작업

     

    대형 레벨에 존재하는 모든 Actor 정보를 Client에게 보내는것은 불필요함.   

    • Server가 레벨에 있는 모든 Actor의 정보를 실시간으로 모든 Client에 보낸다면 네트워크로 전송하는 패킷 양이 감당할 수 없을 정도로 크다.
    • ex. MMORPG 몹, NPC / Battleground 플레이어 정보 

     

    Client와 연관있는 Actor만 체계적으로 모아 통신 데이터를 최소화하는 방법.

     

     

     

    참고) 언리얼 공식문서

    " 거대한 레벨에서 일정 시점에, 플레이어는 그 레벨에 있는 액터의 작은 일부분만 볼 수 있습니다. 레벨에 있는 다른 액터는 대부분 보이지도, 들리지도 않으며, 플레이어에 중대한 영향을 끼치지도 않습니다. 서버가 보이는 것으로 또는 클라이언트에 영향을 끼칠 수 있는 것으로 여기는 액터 세트는, 그 클라이언트에 대해 연관성이 있는 액터 세트라 여깁니다. 언리얼 네트워크 코드의 상당한 대역폭 최적화는, 서버가 클라이언트에게 연관성이 있는 액터 세트에 대해서만 알려주는 것으로 이루어집니다. "

     

    " 이러한 규칙은 플레이어에게 정말 영향을 끼칠 수 있는 액터 세트를 제대로 추정해 낼 수 있도록 디자인되어 있습니다. 물론, 완벽하지는 않습니다: 거리 검사는 가끔 커다란 액터에 거짓 음성 판단을 내릴 수 있으며 (물론 몇 가지 경험법칙을 사용하여 보완하고는 있습니다만), 앰비언트 사운드에 대한 사운드 오클루전같은 것은 지원하지 않습니다. 하지만 인터넷과 같은 지연시간 및 패킷 손실 특성이 있는 네트워크 환경에 내재된 오류의 범람으로 인하여 그러한 추정은 어쩔 수 없습니다. "

     

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

     

    액터 연관성 및 우선권

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

    docs.unrealengine.com


     

     

     

    연관성에 관련된 다양한 속성

     

    연관성 판별을 위한 특별한 Actor의 정의

    • 뷰어(Viewer):  Client의 Connection을 담당하는 PlayerController를 가리킴. 
    • 뷰 타겟(View Target):  PlayerController가 빙의한 폰(Possessed Pawn)
    • 가해자(Instigatro):  나에게 대미지를 가한 Actor 

     

    Owner의 정의

    • Actor를 소유하는 Actor. 최상단의 소유 Actor를 의미.


     

     

     

     

    연관성의 점검

     

    • 서버에서는 Tick 마다 모든 Connection과 Actor에 대해 연관성을 점검함
    • Client의 뷰어와 관련있고 뷰어와의 일정 거리 내에 있는 Actor를 파악
    • 해당 Actor 묶음의 정보를 Client에게 전송   
    • 연관성 검사는 Client Connection 당 진행된다.(= 각 클라이언트는 연관성 검사를 한다)
    • 연관성 검사에 실패하는 순간에는 서버로부터 데이터를 받지 못하게 된다. 


     

     

    액터 속성에 따른 연관성 판정을 위한 속성

     

       
     AlwaysRelevant -  항상 Connection에 대해 연관성을 가짐 
    -  뷰어와 무관하게 항상 복제해야 되는 Actor들에게 설정 (= 네트워크 트래픽을 증가시킨다) 
    -  GameState와 PlayerState의 Actor에 이 옵션이 설정되어 있음. (항상 게임의 상태와 플레이어 상태를 전송해야 하기 때문이다.)
     NetUseOwnerRelevancy -  자신의 연관성은 Owner의 연관성으로 판정됨 
    -  ex. 시야에 안 들어오는 장치 + 그 장치에 장착된 무기 Actor가 있다고 가정. 
    무기에 대해서 따로 연관성을 파악하는것이 아니라 무기를 소유하고 있는 장치에 대해서 연관성을 판단
     OnlyRelevantToOwner -  Owner에 대해서만 연관성을 가짐
    -  이 옵션은 뷰어나 뷰타겟을 Owner로 하지 않으면 아예 연관성에서 제외시킴  
    -  ex. 이 옵션을 체크하여 카메라 또는 내가 조종하는 캐릭터와 연관이 없으면 아예 Client 쪽에 보내지 않음. 연관성 검사에서 일찍 걸러지기 때문에 서버와 네트워크의 부하를 줄일 수 있다.
     NetCullDistance -  뷰어(Viewer)와의 거리에 따라 연관성 여부를 결정함
    -  기본값은 2억2500만.  제곱한 값이므로 루트값인 15000cm = 150m 다. 

     

    이러한 연관성 판정에 대한 로직은 게임의 종류에 따라서 다양하게 직접 만들어서 적용할 수 있도록 가상함수(Virtual function)으로 구성되어 있다. 하지만 이 작업 자체가 로직이 많아질수록 서버에 부하가 많이 걸리기 때문에 최대한 가볍게 지정하는 것이 좋다.

     

    Actor, Pawn, PlayerController의 virtual bool IsRelevantFor() 가상함수 코드 살펴보기 

     

     

     

     

    PlayerController는 Client에 하나만 존재하기 때문에 대부분의 연관성을 통과한다.

     


     

     

     

     

    예제 코드

     

    더보기
    #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;
    
    	UFUNCTION()
    	void OnRep_ServerRotationYaw();
    
    	float RotationRate = 30.0f;
    	float ClientTimeSinceUpdate = 0.0f; // Server로부터 패킷을 받은 이후에 얼마만큼 시간이 경과됐는지를 기록하는 변수
    	float ClientTimeBetweenLastUpdate = 0.0f; // Server로부터 데이터를 받고 그 다음 데이터를 받았을때 걸린 시간을 기록하는 변수
    };

     

     

    더보기
    #include "Prop/ABFountain.h"
    #include "Components/StaticMeshComponent.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; // 1초에 한 번씩 전송
    	NetCullDistanceSquared = 4000000.0f; // 20m 거리 범위
    }
    
    // Called when the game starts or when spawned
    void AABFountain::BeginPlay()
    {
    	Super::BeginPlay();
    	
    	//if (HasAuthority())
    	//{
    	//	FTimerHandle Handle;
    	//	GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
    	//		{
    	//			ServerRotationYaw += 1.0f;
    	//		}
    	//	), 1.0f, true, 0.0f);
    	//}
    }
    
    // Called every frame
    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);
    }
    
    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;
    }

    AABFountain::AABFountain()

    • bReplicates = true;
    • NetUpdateFrequency = 1.0f; 
      • 1초에 한 번씩 전송
    • NetCullDistanceSquared = 4000000.0f; 
      • 20m 거리 범위

     

    bool AABFountain::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const

    • if (!NetRelevantResult) {
      AB_LOG(LogABNetwork, Log, TEXT("Not Relevant:[%s] %s"), *RealViewer->GetName(), *SrcLocation.ToCompactString());
      }

     

    void AABFountain::OnRep_ServerRotationYaw()

    • ClientTimeBetweenLastUpdate = ClientTimeSinceUpdate;
    • ClientTimeSinceUpdate = 0.0f;