개발 이야기 안하는 개발자

언리얼5_3_2 : 액터 리플리케이 본문

Unreal/이득우의 언리얼 프로그래밍

언리얼5_3_2 : 액터 리플리케이

07e 2024. 3. 29. 20:15
반응형

 

액터 리플리케이션

 

특정 플레이어에 속한 액터의 정보를 네트워크 내 다른 플레이어에게 복제하는 작업

클라이언트-서버 모델에서는 대부분 서버에서 클라이언트로 전달함.

리플리케이션의 방법에는 크게 2가지가 있음.

- 프로퍼티 리플리케이션

- PRC (Remote Procedure Call)

 

 

기본 액터의 로딩

클라이언트가 초기화 될 때 모든 액터 정보를 서버로부터 받는 것은 비효율적이다.

따라서 기본 배경에 관련된 액터는 맵을 통해 스스로 로딩하도록 설계되어 음.

고정으로 제공하는 액터와 동적으로 생성하는 액터

- 고정으로 제공하는 액터 : 레벨을 구성하는 배경 액터

- 동적으로 생성하는 액터 : 플레이어 컨트롤러와 폰

고정 액터에 대해 NetLoadOnClient 속성을 체크해야 함(기본 값)

NetLoadOnClient는 보통 활성화 되어 있는데,

클라이언트에 맵이 로드되면서 동시에 이 항목이 체크된 해당 액터도 맵과 함께 클라이언트에게 보여지게 된다.

이를 비활성화 한다면 클라이언트는 해당 액터가 보이지 않게 된다.

이를 활용해서 서버에서 계속 받아야 하는 데이터를 눈속임을 통해 효과적으로 이동하는 것 처럼 보이게 할 수 있다. 

 

액터 리플리케이션 설정

고정으로 보여지는 액터 중, 게임 중 변경 사항이 발생하는 액터는 그 값을 전달해야 한다.

네트워크 데이터를 최소화하기 위해 변경 사항을 보내기 보단, 변경을 유발한 속성값을 전달한다.

이를 위해 액터의 Replicates 옵션을 체크해야 한다.

 

리플리케이션 프로퍼티(속성)의 지정

액터의 리플리케이션 속성을 참으로 지정한다.

- bReplicates속성을 true로 설정한다.

네트워크로 복제할 액터의 속성을 키워드로 지정

- UPROPERTY에 Replicated 키워드 설정

GetLifetimeReplicatedProps 함수에 네트워크로 복제할 속성을 추가.

- #include "Net/UnrealNetwork.h" 헤더 파일 지정

- DOREPLIFETIME 매크로를 사용해 복제할 속성을 명시한다.

 

LifeTime은 액터 채널의 LifeTime을 의미한다.

활성화된 액터 채널로 전송할 복제될 속성을 의미한다.

 

리플리에키션 콜백 함수

클라이언트에 속성이 복제될 때 콜백 함수가 호출되도록 구현

- UPROERPTY의 Replicated 키워드를 ReplicatedUsing 키워드로 변경.

- ReplicatedUsing에 호출할 콜백 함수 지정

- 호출될 콜백 함수는 UFUNCTION 으로 선언해야함.

콜백 함수 구현

일반적으로 OnRep_ 의 접두사를 가지는 이름 규칙을 가짐.

콜백함수는 서버가 아닌 클라이언트에서만 호출된다.

 

맵에 있는 분수대를 돌릴것이다.

class ARENABATTLE_API AABFountain : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AABFountain();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	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;

	UPROPERTY(ReplicatedUsing = OnRep_ServerRotationYaw)
	float ServerRotationYaw;

	UFUNCTION()
	void OnRep_ServerRotationYaw();

	float RotationRate = 30.0f;
};

헤더에서 다음과 같이 정의한다.

 

생성자에선 꼭 bReplicates를 true로 바꿔야 한다.

bReplicates = true;

매 틱마다 변경된 값을 가져온다.

void AABFountain::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (HasAuthority())
	{
		AddActorLocalRotation(FRotator(0.0f, RotationRate * DeltaTime, 0.0f));
		ServerRotationYaw = RootComponent->GetComponentRotation().Yaw;
	}
	else
	{

	}
}

HasAutority인지만 체크해서 서버만 진행하도록 제한을 둔다.

 

void AABFountain::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME(AABFountain, ServerRotationYaw);
}

void AABFountain::OnRep_ServerRotationYaw()
{
	AB_LOG(LogABNetwork, Log, TEXT("Yaw : %f"), ServerRotationYaw);

	FRotator NewRotator = RootComponent->GetComponentRotation();
	NewRotator.Yaw = ServerRotationYaw;
	RootComponent->SetWorldRotation(NewRotator);
}

DOREPLIFETIEM 매크로를 이용해서 사용할 ServerRotationYaw를 서버가 보낸다.

이때 ServerRotationYaw값은 리플리케이션 콜백 함수로 OnRep_ServerRotationYaw()로 지정해 두었다.

즉 값이 변경되는 것을 느끼면 자동으로 메소드를 호출하게 한다.

 

액터에는 OnActorChannelOpen이라는 메소드가 있는데 이 메소드를 기준으로 활성화되면 그 다음부터 데이터를 읽어온다.

즉, 이로 인해 리플리케이션이 시작된다고 볼 수 있다.

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"));
}

 

 

C++ Vs Blueprint

두 방법은 유사하게 동작하지만,

동작 방식에 있어 미세한 차이가 있다.

 

 

액터 리플리케이션 빈도와 연관성

 

언리얼 인사이트

언리얼 프로그램의 다양한 퍼포먼스를 체크할 수 있는 강력한 프로파일링 도구

프로파일링 뿐만 아니라 네트워크 상태도 체크가된다 (Network Insights)

 

순서 :

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

- (설치경로)\Epic Games\UE_5.1\Engine\Binaries\Win64

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

- 바로가기 제작

3. 언리얼 에디터 실행파일의 사용자 변수 PATH 설정

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

UnrealEditor.exe %cd%\ArenaBattle.uproject -NetTrace=1 -Trace=Net

를 작성하고 bat파일 저장한다

이렇게 실행하면 언리얼이 켜진다.

이상태로 제작했던 숏컷을 실행하면 insight 창이 켜진다.

Connection에서 Connect를 눌러 주소 IP 주소를 연결하고 OpenTrace를 누르면 활성화 된다.

 

TimingInsights - 프로파일링

NetworkingInsights - 네트워크 관련 데이터

서버와 클라이언트를 실행하면 띄워져 있는 2개의 창으로 데이터를 볼 수 있다.

이때 서버는 OutGoing으로 하고 Client는 Incoming으로 해서 데이터의 순환을 선택하면 데이터 정보를 볼 수 있다.

그중 한 프레임을 누르면 그때 어떤걸 했는지 볼 수 있다.

 

액터 리플리케이션의 빈도

클라이언트와 서버간의 통신빈도를 뜻한다.

NetUpdateFrequency : 리플리케이션 빈도의 최대치 설정

1초당 몇 번 리플리케이션을 시도할지 지정한 값

기본값은 100. 서버는 1/100초간격으로 리플리케이션을 시도한다.

네트워크 빈도는 최대치일 뿐 이를 보장하진 않는다.

 

데이터 고액을 클라이언트에서 부드러운 움직임으로 보완하기

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

클라이언트에서 예측된 값을 보간해 회전한다.

 

회전하는 분수대의 NetUpdateFrequency를 1로 둔다.

그러면 1초에 한번 업데이트가 될 것이다.

 

분수 생성자

NetUpdateFrequency = 1.0f;

 

그럼 서버는 부드럽게 이동하지만 클라이언트에선 툭툭 끊기는 것을 볼 수 있다.

분수대에 대한 정보가 들어올땐 패킷양이 늘어나서 크기가 커지는 것을 볼수 있다.

툭툭 끊기는 문제를 해결하기 위해서 보간을 이용해 그 사이의 값을 채울것이다.

 

	float ClientTimeSinceUpdate = 0.0f;
	float ClientTimeBetweenLastUpdate = 0.0f;
void AABFountain::OnRep_ServerRotationYaw()
{
	...
    
	ClientTimeBetweenLastUpdate = ClientTimeSinceUpdate;
	ClientTimeSinceUpdate = 0.0f;
}

서버로부터 데이터를 받으면 시간을 리셋할 것이다.

 

void AABFountain::Tick(float DeltaTime)
{
	...
    
	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);
	}
}

else문 안으로 보간값을 통해 회전하도록 로직을 구현한다.

 

적응형 네트워크 업데이트

유의미한 업데이트가 없으면 빈도를 줄여서 부하를 줄이는 기법

MinNetUpdateFrequeny : 리플리케이션 빈도의 최소치 설정을 사용함 (기본은 2)

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

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

 

DefaultEngine.ini 에서 다음을 추가하면 된다.

[SystemSettings]
net.UseAdaptiveNetUpdateFrequency=1

 

 

연관성(Relevancy)

서버의 관점에서 현재 액터가 클라이언트의 커넥션에 관련된 액터인지 확인하는 작업

대형 레벨에 존재하는 모든 액터 정보를 클라이언트에게 보내는 것은 불필요함.

클라이언트와 연관있는 액터만 체계적으로 모아 통신 데이터를 최소화 하는 방

 

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

연관성 판별을 위한 특별한 액터의 정의

- 뷰어 : 클라이언트의 커넥션을 담당하는 플레이어 컨트롤러를 가르킴

- 뷰 타겟 : 플레이어 컨트롤러가 빙의한 폰

- 가해자 : 나에게 대미지를 가한 액터

오너의 정의

- 액터를 소유한 액터. 최상단의 소유 액터를 의미.

 

연관성 점검

서버에서는 틱마다 커넥션과 액터에 대해 연관성을 점검함.

클라이언트의 뷰어와 관련있고 뷰어와의 일정거리 내에 있는 액터를 파악

해당 액터 묶음의 정보를 클라이언트에게 전송

 

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

AlwaysRelevant : 항상 커넥션에 대한 연관성을 가짐

NetUseOwnerRelevancy : 자신의 연관성은 오너의 연관성으로 판정함.

OnlyRelevantToOwner : 오너에 대해서만 연관성을 가짐.

Net Cull Distance : 뷰어와의 거리에 따라 연관성 여부를 결정함.

 

액터 , 폰 , 플레이어 컨트롤러의 IsRelevantFor() 코드 살펴보기

... 생성자
	NetCullDistanceSquared = 4000000.0f;

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;
}

IsNetRelevantFor 을 상속받아 정의해본다.

분수대는 연관성이 있는가를 확인해본다.

NetCullDistanceSquared로 뷰어와의 거리를 측정할때, 4000000 는 넓이 이기때문에 제곱값으로 들어가있다.

2000센티미터를 뜻하고, 즉 20미터를 뜻한다. 20미터 내로 들어왔을때만 데이터를 서버가 보내주게 된다.

거리가 멀어지게 된다면 서버가 연관성 검사에 실패하기 때문에 로그를 띄우면서 데이터를 보내지 않는다.

서버쪽에서 데이터를 보내지 않기 때문에 클라이언트의 분수는 움직이지 않게 된다.

 

우선권

클라이언트에 보내는 대역폭(NetBandWidth)은 한정되어 있다.

클라이언트에 보낼 액터중, 우선권이 높은 액터의 데이터를 우선 전달하도록 설계되어 있다.

액터에 설정된 NetPriority 우선권 값을 활용해 전송 순서를 결정한다.

 

네트워크 트래픽 포화

위 그림처럼 대역폭에서 분수대의 전송 데이터가 너무 크다면 오른쪽 그림처럼 진행하게 된다.

이렇게 분수대의 우선권을 낮춰서 다음 틱에 보내도록 하면 된다.

 

우선권 설정 로직

마지막으로 패킷을 보낸 후의 경과 시간과 최초 우선권 값을 곱해 최종 우선권 값을 생성

최종 우선권 값을 사용해 클라이언트에 보낼 액터 목록을 정렬함

네트워크가 포화(Saturation)될 때까지 정렬된 순서대로 리플리케이션을 수행

네트워크가 포하되면 해당 액터는 다음 서버 틱으로 넘김

 

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

 

포화상태를 강제로 만든다.

TArray<float> BigData를 GetLifeTimeReplicatedProps에 등록한다.

이때 BigData도 UPROPERTY(Replicated)이다.

		FTimerHandle Handle;
		GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
			{
				BigData.Init(BigDataElement, 1000);
				BigDataElement += 1.0;
			}
		), 1.0f, true, 0.0f);

이후 서버에서 BeginPlay할때 해당 값을 초기화해주고 계속 데이터를 변경해본다.

 

이후 DefaultEngine.ini 에서 사용할 로그와 데이터 제한을 정의한다.

[Core.Log]
LogNetTraffic=Log

[/Script/Engine.GameNetworkManager]
TotalNetBandwidth=3200

TotalNetBandWidth를 줄임으로써 사용가능한 대역폭을 줄인다.

 

그렇게 되면 로그가 찍힌다.

데이터가 포화(Saturated)가 되었기 때문에 다음 틱으로 보낸다고 뜨게 된다.

 

액터의 휴면(Dormancy)

액터의 전송을 최소화 하기 위해 연고나성과 더불어 제공하는 속성

액터가 휴면 상태라면 연관성이 있더라도 액터 리플리케이션(RPC)을 수행하지 않음.

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

- DORM_NEVER : 액터는 휴면이 없음

- DORM_Awake : 액터는 깨어나 있음

- DORM_DormantAll : 액터는 언제나 휴면 상태. 필요시에 깨울 수 있음.

- DORM_DormantPartial : 특정 조건을 만족할 경우에만 리플리케이션을 수행.

- DORM_Initial : 액터를 휴면 상태로 시작하고 필요한 때 깨우도록 설정할 수 있음.

 

색상을 바꿔보자.

	UPROPERTY(ReplicatedUsing = OnRep_ServerLightColor)
	FLinearColor ServerLightColor;
    
   	UFUNCTION()
	void OnRep_ServerLightColor();
void AABFountain::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	...
	DOREPLIFETIME(AABFountain, ServerLightColor);
}

 

컬러를 사용할 것이라고 등록한다.

 

void AABFountain::BeginPlay()
{
	Super::BeginPlay();
	
	if (HasAuthority())
	{
		FTimerHandle Handle;
		GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
			{
				ServerLightColor = FLinearColor(FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), 1.0f);
				OnRep_ServerLightColor();
			}
		), 1.0f, true, 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);
	}
}

색상을 계속 바꿔주도록 한다.

 

이제 휴면을 등록해본다.

생성자에 추가한다.

NetDormancy = DORM_Initial;

그럼 서버는 색상이 변하지만 클라이언트가 보기엔 색이 바뀌지 않는다.

즉, 액터의 값 변환이 들어오지 않는 것이다.

		FTimerHandle Handle2;
		GetWorld()->GetTimerManager().SetTimer(Handle2, FTimerDelegate::CreateLambda([&]
			{
				FlushNetDormancy();
			}
		), 10.0f, false, -1.0f);

FlushNetDormancy()를 호출함으로써 휴면상태에서 깨어나게 할수 있다.

서버 기준으로 10초뒤에 다시 휴면상태에서 깨어나게 된다.

 

속성값을 리플리케이션할때 DOREPLIFETIME말고 DOREPLIFETIME_CONDITION을 사용해서 조건을 추가할 수 있다.

그중 속성중에 COND_InitialOnly라는 값은 최초 한번만 전송하게 해주는 옵션이다.

 

 

액터 리플리케이션 흐름 도식화

네트워크로 업데이트할 액터의 목록들이 액터 리플리케이션에 제일 먼저 업로드 된다.

각 액터의 정보는 NetworkObjects라고 불리는 구조체에 정리가 된다.

이때 데이터에 대해 주요 변수로는 NextUpdateTime(빈도를 설정하는 속성과 관련있음), bPendingNetUpdate(포화 상태가 되어서 못보낼때 체크되는 값) 등이 있다.

각 상태에 맞춰 보낼 액터와 보내지 않을 액터로 나뉘게 된다.

 

그렇게 보내게 될 액터들만 모아두는 리스트를 ConsiderList라고 한다.

이들은 각각 PreReplication()을 호출해서 전송할 준비가 되었다고 알려준다.

 

ConsiderList가 완성되면 각 클라이언트마다 별도의 목록을 제작해준다.

클라이언트 뷰어에 맞춰 필요한 액터가 서로 다를수 있기 때문에 이 뷰어 정보를 참고(연관성 정보)해서 컨시더 리스트로부터 각각 별도의 액터의 묶음들을 만들어준다.

이 묶음은 우선순위에 맞춰 재정렬된다.

ActorPriority라는 별도의 구조체를 사용해서 정렬하게 되는데, 우선권값과 NetworkObjectInfo를 담을 수 있는 필드를 가지고 있다. 이것들을 모아둔 목록이 우선권 리스트이다.

 

서버가 클라이언트에게 이를 맞춰서 데이터를 하나씩 보내다가 포화상태가 되면 팬딩 넷 업데이트 필드의 플래그를 활성화해서 다음서버틱에서 이를 참고하도록 조정한다. 

반응형