25년 4월

3주차

어빌리티 쿨타임 구현

쿨타임 타이머가 실행할 때 어빌리티의 실행하지 못하도록 설정

어빌리티가 Deactive될 때 쿨타임 타이머가 실행

 

드론의 미사일 발사 어빌리티를 플레이어가 아닌 드론 컨트롤러가 갖고 있도록 리팩토링

드론을 스폰하는 어빌리티와 미사일 발사 어빌리티를 플레이어가 갖고 있어 기존에는 스폰 어빌리티에서 미사일 발사 어빌리티로 교체되는 구조였음

따라서 드론 스폰 어빌리티가 Destroy되기 때문에 어빌리티의 상태를 관리하기 어려움

그래서 어빌리티 교체하는 시스템을 제거하고 드론 미사일 발사 어빌리티를 드론 컨트롤러가 갖고 있도록 수정

플레이어는 드론을 스폰하고 드론 컨트롤러에게 어빌리티를 실행하도록 요청

어빌리티 객체를 유지하여 상태를 관리할 수 있고, 각 어빌리티를 기능의 주체자가 소유하여 코드가 단순해짐

 

HUD 총기 정보 UI 구현

HUDUIController에서 MagazineCount, TotalMagazineCount Type의 StatusUserWidget을 소유하여 각 장전 탄창 수, 보유 탄창 수를 반환할 수 있도록 함

HUDUIController에서 Fire, Reload 등 델리게이트가 호출될 때 MagazineCount, TotalMagazineCount에게 탄창 수를 반환 받아 TextBlock을 수정 

 

AMFHOFireArm의 RPC가 제대로 호출되지 않는 현상 수정

AMFHOFireArm를 스폰할 때 Owner를 설정하지 않았는데, Owner를 설정하지 않아서 Connection을 알 수 없어 RPC가 제대로 호출되지 않음

스폰할 때 FActorSpawnParameters를 통해 Owner 설정

 

CurrentActivatedEquipment가 리플리케이트 되지 않는 현상 수정

CurrentActivatedEquipment가 리플리케이트 액터가 아니었기 때문에 CurrentActivatedEquipment를 리플리케이트 액터로 변경하여 수정

 

HUD Player HP UI 구현

HUDUIController에서 StatusUserWidget을 상속 받는 HP 블루프린트 Widget을 소유

HUDUIController에서 HP 변경 이벤트를 받으면 StatusUserWidget에게 반환된 HP 퍼센트에 맞게 HP Widget의 블록들의 색상을 설정

 

StatusUserWidget 구현

StatusUserWidget의 StatusType을 블루프린트 에디터에서 지정하면 해당 Enum에 맞는 스텟을 StatusUserWidget에서 반환

 

2주차

UI 프레임워크 및 룰 설계

 

MFBaseUserWidget : UI의 기본 기능 및, UserWidget의 기본 기능을 구현

MFXXUserWidget : 콘텍스트에 독립적인 UI 요소(ex. HP바, 스텟 표시, 아이템 슬롯 등)

 

MFBaseUIController : Controller의 기본 기능을 구현

MFXXUIController

  • 각 콘텐츠를 담당하는 UI 관리자(ex. HUD, Invetory 등)
  • MFUserWidget을 소유하고 관리할 수 있음

  • UserWidget은 AddToViewport 불가능 : MFBaseUserWidget::AddToViewport를 막기 위해 NameHiding 사용
    • UserWidget 포인터를 사용한다면 막지 못하는 한계는 있음
  • UI는 게임플레이 로직을 수정하지 않음
    • 수정을 요청하는 함수를 호출할 수는 있지만 Setter 함수나 직접적으로 수정해서는 안됨
  • 블루프린트가 바이너리이므로 코드가 많이 복잡하지 않는 이상 블루프린트 그래프를 사용하지 않음

GameplayAbilitySystem(GAS) 기본 이해하기

GAS는 언리얼 엔진에서 제공하는 강력한 프레임워크로, 게임플레이 능력, 효과, 속성 등을 관리하는 시스템입니다.

캐릭터의 스킬, 버프/디버프, 상태 효과 등을 구현하는 데 효과적

 

서버-클라이언트 구조에서의 초기화 문제

GAS는 Ability와 AttributeSet(속성(체력, 마나 등)을 관리)사용하기 때문에 플레이어의 경우 PlayerState에 저장해야 함

보통 게임플레이 로직을 BeginPlay에서 초기화합니다.

 

하지만 플레이어가 게임이 시작된 후에 추가되면 월드가 이미 시작되었기 때문에 BeginPlay를 빨리 호출하게 됩니다.

void AActor::PostNetInit()
{
	...
    
	if (!HasActorBegunPlay())
	{
		const UWorld* MyWorld = GetWorld();
		if (MyWorld && MyWorld->HasBegunPlay())
		{
			SCOPE_CYCLE_COUNTER(STAT_ActorBeginPlay);
			DispatchBeginPlay();
		}
	}
}

이는 GAS가 클라이언트에 리플리케이트되기 전에 BeginPlay가 호출되기 때문에 GAS를 사용하는 컴포넌트는 초기화할 때 GAS가 없는 문제가 발생

 

인터페이스를 통해 GAS가 리플리케이트되는 시점을 전달하여 해결

GAS를 사용하는 컴포넌트는 UPNAbilitySystemUserInterface을 상속받아 OnInitializeAbilitySystem 함수가 호출되는 시점에 GAS를 활용하여 초기화를 진행

void APNCharacterPlayer::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);

	InitializeAbilitySystemComponent();
}

void APNCharacterPlayer::OnRep_PlayerState()
{
	Super::OnRep_PlayerState();

	InitializeAbilitySystemComponent();
}

void APNCharacterPlayer::InitializeAbilitySystemComponent()
{
	APNPlayerState* PlayerStateCast = GetPlayerState<APNPlayerState>();
	check(PlayerStateCast);

	UAbilitySystemComponent* AbilitySystemComponent = PlayerStateCast->GetAbilitySystemComponent();
	ActorExtensionComponent->InitializeAbilitySystem(Cast<UPNAbilitySystemComponent>(AbilitySystemComponent), PlayerStateCast);
}

void UPNActorExtensionComponent::InitializeAbilitySystem(UPNAbilitySystemComponent* InAbilitySystemComponent, AActor* InOwnerActor)
{
	...

	TArray<UActorComponent*> AbilitySystemUserComponents = GetOwner()->GetComponentsByInterface(UPNAbilitySystemUserInterface::StaticClass());
	for (UActorComponent* Component : AbilitySystemUserComponents)
	{
		Cast<IPNAbilitySystemUserInterface>(Component)->OnInitializeAbilitySystem(AbilitySystemComponent);
	}
}

네트워크 멀티플레이어를 위해 고려할 최적화 요소

불필요한 액터의 리플리케이션 옵션 끄기

액터에 맞는 최적의 Frequency와 Priority 설정

Relevancy를 위한 최적의 조건 설정

프로퍼티 리플리케이션 조건 설정

필요 시 Dormancy 상태 설정

RPC 데이터 크기 양자화(FVector_NetQuantize)

 

프로퍼티에 마크를 설정하여 필요할 때에만 데이터를 전송할 수 있도록 프로퍼티 리플리케이션의 푸시 모델 설정

 

FFastArraySerializer : 변화된 데이터의 인덱스 값만 마킹해서 변경된 데이터만 보내는 기법

ReplicationGraph, IRIS(언리얼 5.1) : 빠른 리플리케이션 엔진의 교체

 

NetSerialize

네트워크로 전송할 구조체 데이터를 직접 설계

데이터 양을 최소화할 수 있음

데이터가 자주 바뀔 때 유용

플래그를 설정해 불필요한 데이터 전송을 건너뛸 수 있음

정수 데이터로 변환해 크기를 줄일 수 있음

 

언리얼 엔진은 네트워크 데이터 전송 시, 필요한 만큼의 비트만 사용하여 데이터를 효율적으로 직렬화

uint32 변수 5개가 있다고 해서 항상 5 x 32비트(= 160비트)를 사용하는 건 아님

네트워크 트래픽의 최적화를 위해 각 변수가 실제로 사용하는 최소 비트 수로 데이터를 패킹
예를 들어 5개의 uint32 변수 각각이 단순한 0~31 범위에서 값을 가진다면, 이것들은 5비트로 표현 가능
그러므로 5 x 5 = 25비트가 실제 데이터에만 사용되고, 나머지 여유 공간은 패딩 등의 이유로 추가되고, 데이터 전송 과정에서는 추가 압축이 일어나 최종적으로 64비트에 적합한 필드를 생성

 

구조체에 NetSerialize 함수 및 struct TStructOpsTypeTraits<FABCharacterStat>를 선언해야 함

struct FABCharacterStat
{
    bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
	{
		return true;
	}
};

template<>
struct TStructOpsTypeTraits<FABCharacterStat> : public TStructOpsTypeTraitsBase2<FABCharacterStat>
{
	enum
	{
		WithNetSerializer = true,
	};
};

 

변경되지 않은 데이터를 보내지 않으려면 FRepMovement::NetSerialize 함수 참고

캐릭터 Movement 리플리케이션

캐릭터 movement 리플리케이션 플로우

https://dev.epicgames.com/documentation/en-us/unreal-engine/understanding-networked-movement-in-the-character-movement-component-for-unreal-engine?application_version=5.1\

 

TickComponent에서 가속도를 계산하여 PerformMovement 함수를 호출해서 이동 계산

 

Autonomous Proxy 클라이언트의 UCharacterMovementComponent::ReplicateToServer

클라이언트 캐릭터의 움직임을 보관하는 네트워크용 클라이언트 데이터 FNetworkPredictionData_Client_Character 생성

클라이언트의 데이터에 저장된 움직임 중에 참고할 중요한 Move 기록(OldMove)

현재 틱의 Move를 기록하는 신규 Move 생성(NewMove)하여 각종 상태를 저장

필요 시 최종 Move와 현재 Move를 병합 시도

클라이언트 로컬에서의 move 진행(PerformMovement)

신규 Move에 Move 결과 상태를 저장하고 클라이언트 데이터에 추가

OldMove와 NewMove를 서버에 전송

 

ReplicateToServer -> ServerMovePacked -> ClientMoveResponsePacked(조정)

 

Movement 리플리케이션 디버깅

RPC 패킷을 로그로 확인

// DefaultEngine.ini
[Core.Log]
LogNetPlayerMovement=VeryVerbose

 

드로우 디버그를 위해 콘솔 변수 p.NetShowCorrection 1 설정

오차 발생시 클라이언트의 위치를 붉은색, 서버의 위치를 녹색으로 표시하고, 수정 후 동일하다면 노란색으로 표시

 

먼저 보낸 RPC가 이후에 보낸 RPC 혹은 리플리케이션보다 느릴 수 있음

각 채널은 독립적으로 패킷을 처리하므로 순서가 보장되지 않음

 

액터 Movement 리플리케이션 플로우

액터의 리플리케이션 및 무브먼트 리플리케이션 옵션이 설정돼야 무브먼트 리플리케이션이 실행됨

 

ReplicatedMovement

서버에서 SimulatedProxy로 보내는 움직임 정보를 기록한 멤버 변수

일반 Movement와 물리 Movement의 리플리케이션을 모두 처리하는 용도로 활용

 

GatherCurrentMovement

현재 액터의 Movement를 ReplicatedMovement로 변환해 설정하는 함수

액터의 PreReplication 함수에서 호출되어 각 클라이언트에 패킷을 보내기 전에 자신의 상태 데이터를 확정

액터의 물리 무브먼트와 일반 무브먼트를 구분해 각각 처리

일반 무브먼트는 단순히 액터의 위치, 회전, 속도 값을 ReplicatedMovement에 저장

물리 시뮬레이션의 경우 현재 월드에 설정된 물리 씬에서 해당 컴포넌트의 물리 상태를 저장하고 최종 ReplicatedMovement가 설정되어 클라이언트에 보내짐

OnRep_ReplicatedMovement

일반 무브먼트는 Simulated Proxy에 한해서 컴포넌트의 위치와 회전 정보를 갱신, 속도 처리는 별도로 진행하지 않음

물리 무브먼트는 물리 리플리케이션 씬에서 컴포넌트와 일치하는 타겟을 찾아서 업데이트

 

ApplyRigidBodyState

물리 리플리케이션의 틱에서 호출되는 함수

클라이언트의 물리 상태가 서버의 물리 상태의 오차 내에 있을 때까지 계속 호출됨

 

서버에서 받은 최종 속도와 핑을 기반으로 클라이언트의 물리 상태를 외삽(다른 변수와의 관계를 통해 추정하는 과정, Extrapolation)으로 예측

예측한 위치와 방향이 서버와 비교하고 문제가 있다면 에러 시간을 누적

누적된 에러 시간 설정값을 넘으면 강제 조정(하드 스냅)을 진행

차이가 크지 않다면 내삽(보간, Interpolation)을 수행해 현재 위치, 회전, 속도, 각속도를 조정

 

SimulatedPhysics 옵션으로 물리 무브먼트를 활성화

탈 것, 혹은 플랫폼 위에 타는 경우라면 ReplicatedBasedMovement를 활성화해야 함

 

SimulatedTick

Simulated Proxy 캐릭터가 처리하는 무브먼트

캐릭터만의 추가적인 작업으로, 시뮬레이션으로 캡슐을 이동시킨 후 메시의 무브먼트를 부드럽게 보간(SimulateMovement, SmoothClientPosition)

 

네트워크 캐릭터 무브먼트 컴포넌트

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/understanding-networked-movement-in-the-character-movement-component-for-unreal-engine?application_version=5.1

 

텔레포트 구현하는 방법

1. 클라이언트에서만 실행 : 네트워크 동기화 X

2. 서버만 RPC : 지연 시간이 눈에 띄고, 순간적인 위치 이동이 아니라 부드러운 이동이 됨

3. 서버 RPC 및 로컬 트리거 : RPC 순서가 바뀌거나 손실이 되면 동기화가 비정상적

4. 캐릭터 무브먼트 컴포넌트 능력 구현 : 캐릭터 무브먼트에 대해 이해하고 텔레포트에 대한 데이터 패킹과 언패킹을 잘 해줘야 함

 

캐릭터 무브먼트 구현을 위한 무브먼트 클래스

FNetworkPredictionData_Client_Character : 클라이언트 캐릭터 데이터

FSavedMove_Client : 캐릭터 무브먼트

 

캐릭터 무브먼트 컴포넌트는 클라이언트 캐릭터 데이터의 타입과 캐릭터 무브먼트의 타입을 확정하지 않고 런타임에 객체를 생성하므로 위 두 클래스를 상속받으면 대체 가능

 

무브먼트 관련 클래스를 바꾸는데 사용되는 가상 함수

UCharcterMovementComponent::GetPredictionData_Client

FNetworkPredictionData_Client_Character::ClientCharacter

FNetworkPredictionData_Client_Character::FreeMove

(만약 동적 할당이 없다면 FreeMove는 무시 가능)

 

FObjectInitializer를 통해 서브 오브젝트 클래스를 변경할 수 있음

따라서 새로운 캐릭터 무브먼트 컴포넌트로 교체 가능

APNCharacter::APNCharacter(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer.SetDefaultSubobjectClass<UPNCharacterMovementComponent>(ACharacter::CharacterMovementComponentName))

 

신규 무브먼트 능력을 추가하는 방법

특별한 무브먼트에 대한 플래그 정보인 CompressedFlags를 새로 할당해서 사용

 

텔레포트 능력 구현 플로우

RPC(Remote Procedure Call)

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/remote-procedure-calls-in-unreal-engine?application_version=5.4

 

원격 프로시저(함수) 호출의 약자

원격 컴퓨터에 있는 함수를 호출할 수 있도록 만든 통신 프로토콜

네트워크 멀티플레이에서 서버와 클라이언트 간에 빠르게 행동을 명령하고 정보를 주고받는 데 사용

 

Client RPC

서버에서 클라이언트로 호출하는 RPC
서버에서 명령을 보낼 클라이언트의 커넥션을 소유한 액터를 사용해야 함

엔진에서 통신 과정을 진행하도록 내부에 구현해두었으므로 서버에서 함수를 호출만 하면 됨

// Server
UFUNCTION(Client, Unreliable)
ClientRPCFunction();

// Client
ClientRPCFunction_Implementation();

 

오너십을 가지지 않은 상태에서는 서버 액터의 함수가 호출됨

 

Server RPC

클라이언트에서 서버로 호출하는 RPC

클라이언트에서 서버로 통신하는 유일한 수단

WithValidation 키워드를 설정하면 클라이언트의 명령을 검증할 수 있음

서버와의 커넥션을 소유한 액터를 사용해야 함

// Server
ServerRPCFunction_Validate();
ServerRPCFunction_Implementation();

// Client
UFUNCTION(Server, Unreliable, WithValidation)
ServerRPCFunction();

 

오너십을 설정하지 않은 액터가 ServerRPC 함수를 호출할 때 발생하는 Warning 로그

 

NetMultiCast RPC

서버를 포함한 모든 플레이어에게 명령을 보내는 RPC

커넥션을 소유하지 않아도 프로퍼티 리플리케이션과 유사하게 연관성 기반으로 동작

프로퍼티 리플리케이션과 유사하지만 다른 용도로 사용됨

네트워크 업데이트 기간(Tick 혹은 프레임) 동안 한 번만 실행되어 네트워크 부하를 줄임

// Server
UFUNCTION(NetMulticast, Unreliable)
MulticastRPCFunction();

MulticastRPCFunction_Implementation();

// Client1
MulticastRPCFunction_Implementation();

// Client2
MulticastRPCFunction_Implementation();

 

키워드

Reliable

RPC 호출을 보장, 데이터 전송을 보장하기 위해서 패킷의 송수신 여부를 확인하는 작업으로 네트워크 부하 증가

게임 플레이에 중요하지만 자주 호출되지 않는 함수(충돌 이벤트, 무기 발사, 액터 스폰 등)에 사용

과도하게 호출될 경우 대기열이 넘쳐 연결이 강제로 끊어질 수 있음

 

UnReliable

RPC 호출을 보장하지 않음, 빠르고 자주 호출되는 함수에 적합

 

WithValidation

서버에서 검증 로직을 추가로 구현할 때 추가하는 옵션

검증에 실패하면 클라이언트의 접속을 즉시 끊고 Standalone 모드로 전환됨

 

네트워크 멀티플레이 구현을 위한 4원칙

클라이언트의 명령은 Server RPC 사용

중요한 게임 플레이 판정은 서버에서 처리

게임 플레이에 영향을 주는 중요한 정보는 프로퍼티 리플리케이션을 사용

클라이언트의 시각적 효과는 Client RPC와 Multicast RPC를 사용하는데, Client RPC가 더 좋음

 

RPC 사용 시 주의할점

https://dev.epicgames.com/documentation/en-us/unreal-engine/networking-overview-for-unreal-engine?application_version=5.1

 

가능한 RPC와 리플리케이트 블루프린트 함수를 적게 사용

리플리케이션과 무관한 함수가 서버에서만 실행된다는 것을 보장할 수 있다면 RPC가 아닌 함수로 사용하여 불필요한 오버헤드를 줄이자

Reliable RPC를 플레이어 입력에 바인딩하는 것은 주의 : 입력이 워낙 빈번하게 발생하기 때문

Client와 NetMulticast는 클라이언트에서 호출할 수 있지만 서버에서만 호출돼야 함

 

클라이언트의 행동이 서버를 거친 후에 수행되도록 설계할 경우 통신 부하가 발생할 때 반응이 느려짐

서버에서 모든 것을 처리하는 방식이 데이터 관리 측면에서는 안전할 수 있지만 동작이 느려짐

따라서 서버에서 처리하는 것을 최소화하고, 네트워크 데이터 전송을 최소화하고, 검증을 강화

 

아래 표에서 빨간색으로 칠해진 경우는 RPC 의도에 맞지 않으므로 해당 경우에 사용하지 않도록 주의

 

서버에서 RPC를 호출한 경우

  NetMultiCast Server Client
클라 소유 서버와 모든 클라 서버에만 소유한 클라
서버 소유 서버와 모든 클라 소유한 서버 서버에만
소유 없음 서버와 모든 클라 서버에만 서버에만

 

클라이언트에서 RPC를 호출한 경우

  NetMulticast Server Client
호출한 클라 소유 호출한 클라에만 서버 호출한 클라에만
다른 클라 소유 호출한 클라에만 버려짐 호출한 클라에만
서버 소유 호출한 클라에만 버려짐 호출한 클라에만
소유 없음 호출한 클라에만 버려짐 호출한 클라에만

 

Property Replication vs NetMulticast RPC

유사점

서버와 모든 클라이언트의 지정한 함수를 호출할 수 있음

지정한 데이터 전송을 보장할 수 있음

액터의 오너십과 무관하게 연관성으로 동작함

 

차이점

프로퍼티 리플리케이션으로 설정한 데이터는 클라이언트에 반드시 동기화 됨(Reliable과 다른 개념)

NetMulticast RPC를 호출한 타이밍에 클라이언트가 없으면 해당 데이터를 받지 못함

 

정리

프로퍼티 리플리케이션은 게임에 영향을 미치는 데이터에 사용

NetMulticast RPC는 게임과 무관한 휘발성 데이터(시각 등)에 사용

 

NetQuantize

FVector는 24바이트를 차지하므로 높은 정밀도가 필요없을 때는 FVector_NetQuantize를 사용하여 패킷 데이터 줄이는 것이 좋음

반올림하여 정수형으로 변환하여 최대 60비트(약 8바이트)로 전송

NetQuantize10, NetQuantize100, NetQuantizeNormal도 있으므로 참고

 

특정 클라이언트들에게만 패킷을 보내는 구현 방법

for(APlayerController* PlayerController : TActorRange<APlayerController>(GetWorld()))
{
	// 특정 클라이언트 필터링
	if(PlayerController && PlayerController != GetController() && !PlayerController->IsLocalController())
	{
		// ClientRPC 호출
	}
}

 

액터 리플리케이션

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

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

프로퍼티 리플리케이션(속성), RPC(함수)를 통해 수행

 

가장 중요한 것은 데이터 전송량을 최소화하는 것

 

주의사항

리플리케이트된 액터를 스폰 또는 소멸할 때는 반드시 서버에서 수행

언리얼 엔진이 제공하는 movementcomponent는 리플리케이트용으로 빌드되어 있으므로 가급적 사용

중요한 작업은 서버를 통해서 진행되는지 확인

리플리케이션은 서버에서 호출이 되지 않기 때문에 리플리케이션 노티파이를 통해 서버와 클라이언트가 병렬로 실행되도록 재활용할 수 있음

 

기본 액터의 로딩

클라이언트가 초기화될 때 모든 액터 정보를 서버로부터 받는 것은 비효율적이므로 배경에 관련된 액터는 맵을 통해 서버와 클라이언트가 가지고 있는 파일의 유효성 검사만 진행하고 네트워크 없이 스스로 로딩

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

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

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

 

프로퍼티 리플리케이션

고정 액터 중, 변경 사항이 발생하는 액터는 네트워크 데이터를 최소화하기 위해 변경 사항을 보내는 것보다 변경을 유발한 프로퍼티를 전달

 

리플리케이션 프로퍼티 지정

1. 액터의 bReplicates를 true로 설정 / 움직여야하는 경우 bReplicatesMovement를 true로 설정(폰은 true가 기본값)

2. 네트워크로 복제할 프로퍼티의 UPROPERTY에 Replicated 키워드 설정

3. GetLifetimeReplicatedProps 함수에 DOREPLIFETIME 매크로를 사용해 프로퍼티 추가

Lifetime은 액터 채널의 Lifetime을 의미하여, 활성화된 액터 채널로 전송할 프로퍼티를 의미

 

리플리케이션 콜백 함수 호출

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

틱마다 프로퍼티를 업데이트하면 틱의 코드가 커지고, 네트워크 전송 주기가 틱보다 느리다면 불필요한 업데이트가 발생하기 때문에 콜백 함수를 사용

UPROPERTY의 Replicated 키워드 대신 ReplicatedUsing = {콜백 함수 이름} 으로 변경

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

콜백 함수 이름은 OnRep_ 접두사를 가짐

콜백 함수는 클라이언트에서만 호출됨

UFUNCTION()
void OnRep_ServerRotationYaw();

UPROPERTY(ReplicatedUsing=OnRep_ServerRotationYaw)
float ServerRotationYaw;

 

OnActorChannelOpen 함수가 호출된 후 커넥션에 대해 번치 정보를 해석해서 어떤 리플리케이션을 작업해야하는지 설정한 후 리플리케이션이 동작함

 

C++ OnRep vs 블루프린트 RepNotify

C++ OnRep

클라이언트에서만 호출됨

명시적으로 호출 가능하고, 값이 변경될 때만 호출됨

 

Blueprint RepNotify

서버, 클라이언트 모두 호출됨

서버에서도 호출되므로 명시적 호출 불가능

서버는 항시 호출되고 클라이언트는 값이 변경될 때만 호출됨

 

언리얼 인사이트

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

프로그램 프로파일링 뿐만 아니라 네트워크 상태도 확인할 수 있음(Network Insights)

언리얼 인사이트 구동

1. UnrealInsight 파일이 있는 디렉토리 Epic Games\UE_5.4\Engine\Binaries\Win64를 환경 변수로 설정

2. 배치파일 생성

NetTrace=1 Trace=Net은 네트워크 인사이트를 실행하기 위한 옵션

cd는 현재 디렉토리를 의미하는 변수

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

 

3. 배치파일 실행

4. 언리얼 인사이트 실행

5. 커넥션 설정

6. 트레이스 오픈

 

InComing : 수신되는 데이터

OutGoing : 송신되는 데이터

 

액터 리플리케이션의 빈도(Frequency)

클라이언트와 서버 간에 진행되는 통신 빈도

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

  • 1초당 리플리케이션의 횟수를 의미

네트워크 빈도는 최대치일뿐 이를 보장하지 않고, 서버의 성능이 네트워크 빈도보다 낮을 경우 서버의 성능으로 결정됨

일반적으로 그래픽 기능이 없는 데디케이티드 서버가 더 좋은 성능

 

주요 액터에 설정된 NetUpdateFrequency 값

Actor : 100.0

Pawn : 100.0

PlayerController : 100.0

GameState : 10.0

PlayerState : 1.0

 

규칙적으로 움직이는 액터의 네트워크 데이터 줄이는 예제

클라이언트에서 이전 복제된 데이터에 기반해 현재 틱에서 회전 값을 예측하고 예측된 값을 보간해 회전

 

if (ClientTimeBetweenLastUpdate > KINDA_SMALL_NUMBER)
{
    const float EstimateRotationYaw = ServerRotationYaw + RotationRate * ClientTimeBetweenLastUpdate;
    const float LerpRatio = ClientTimeSinceUpdate / ClientTimeBetweenLastUpdate;
	
    FRotator ClientRotator = RootComponent->GetComponentRotation();
    ClientRotator.Yaw = FMath::Lerp(ServerRotationYaw, EstimateRotationYaw, LerpRatio);
    RootComponent->SetWorldRotation(ClientRotator);
}

 

적응형(Adaptive) 네트워크 업데이트

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

MinNetUpdateFrequencey로 리플리케이션 빈도의 최소를 설정(기본값은 2)

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

// DefaultEngine.ini

[SystemSettings]
net.UseAdaptiveNetUpdateFrequency=1

 

액터 리플리케이션 연관성 설정

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/actor-relevancy-and-priority-in-unreal-engine?application_version=5.1

 

연관성(Relevancy)

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

대형 레벨에 존재하는 모든 액터 정보를 클라이언트에게 보내는 것은 불필요하므로 클라이언트와 연관있는 액터만 체계적으로 모아 통신 데이터를 최소화하는 방법

 

연관성 규칙으로 액터 세트를 완벽하게 추정해낼 수 없으므로 오차가 있을 수 있음

지연 시간, 패킷 손실 등으로 오차가 발생할 수 있음

 

클라이언트에게 연관 액터를 취합할 때 가장 중요한 것은 클라이언트 화면에 보이는 액터들을 묶어서 전달하는 것

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

Viewer : 클라이언트 커넥션을 담당하는 플레이어 컨트롤러

View Target : 플레이어 컨트롤러가 빙의한 폰

Instigator(가해자) : 나에게 데미지를 가한 액터

Owner : 액터를 소유하는 액터

예를 들면 View Target이 들고 있는 무기는 View Target과 함께 보여줘야 하기 때문에 연관성을 가진다고 판정하고 이러한 개념을 설명할 때 Owner를 사용

 

서버에서는 틱마다 모든 커넥션과 액터에 대해 연관성을 점검하므로 부하가 많이 걸리는 작업

클라이언트의 Viewer와 관련있는 Viewer와의 일정 거리 내에 있는 액터를 파악하고 액터 묶음 정보를 클라이언트에게 전송

 

서버의 부하를 줄이기 위해 필요한 연관된 액터를 판정하는 프로퍼티 제공

AlwaysRelevant : 항상 커넥션에 대해 연관성을 가짐, GameState, PlayerState

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

OnlyRelevantToOwner : Viewer, ViewTarget이 오너일 때 연관성을 가짐

NetCullDistance : 뷰어와의 거리에 따라 연관성을 판정, 기본값은 15000cm ^ 2

 

IsRelevantFor 가상 함수를 오버라이드하여 연관성을 재정의할 수 있음

 

우선권(Priority)

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/actor-relevancy-and-priority-in-unreal-engine?application_version=5.1

 

클라이언트에 보내는 대역폭(NetBandWidth)는 한정되어 있으므로 클라이언트에 보낼 액터 중 우선권이 높은 액터의 데이터를 우선 전달하도록 설계됨

우선권 값인 액터의 NetPriority 프로퍼티를 활용해 전송 순서를 결정함

 

Actor : 1.0

Pawn : 3.0

PlayerController : 3.0

 

네트워크 트래픽 포화(Saturation)

 

우선권 설정 로직(GetNetPriority)

마지막으로 패킷을 보낸 후의 경과 시간과 최초 우선권 값을 곱해 최종 우선권 값을 계산하여 클라이언트에 보낼 액터 목록을 정렬

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

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

보내지 못한 액터들은 경과 시간이 늘어나므로 높은 우선권을 가지게 됨

 

현재 뷰타겟인 경우 4배

잘 보이는 경우 2배

잘 안보이는 경우 상황에 따라 0.2 또는 0.4배

플레이어 컨트롤러가 뷰어인경우 4배

 

전체 대역폭 조절 설정

// DefaultEngine.ini

[/Script/Engine.GameNetworkManager]
TotalNetBandWidth=32000 // byte

 

네트워크에 대한 상세한 로그 설정

// DefaultEngine.ini

[Core.Log]
LogNetTraffic=Log

 

액터의 휴면(Dormancy) 상태

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

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

 

DORM_Never : 휴면 불가

DORM_Awake : 휴면이 아닌 상태

DORM_DormantAll : 언제나 휴면 상태, 필요시 깨울 수 있음

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

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

 

프로퍼티 리플리케이션을 사용할 때는 DORM_Initial만 고려하는 것이 좋음

// 휴면 상태 해제
FlushNetDormancy();

 

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

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/conditional-property-replication-in-unreal-engine?application_version=5.1

 

프로퍼티가 최적화 방해를 방지하기 위해 리플리케이션이 등록되면 해제시킬 수 없음

프로퍼티가 변경되지 않으면 리플리케이트하지 않음

DOREPLIFETIME_CONDITION를 통해 ELifetimeCondition enum(조건)과 함께 등록하여 조건이 만족하지 않는 경우 리플리케이트하지 않음

DOREPLIFETIME_ACTIVE_OVERRIDE는 자유롭게 조건을 설정 가능하지만 조건이 자주 바뀐다면 느려질 수 있으므로 주의

 

액터 리플리케이션 플로우

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/detailed-actor-replication-flow?application_version=4.27

 

액터 리플리케이션 대부분은 UNetDriver::ServerReplicateActors에서 발생

1. 서버가 각 클라이언트에 연관성 있는 액터를 수집

2. 접속된 각 클라이언트에 변경된 프로퍼티가 있으면 전송

 

NextUpdateTime은 NetUpdateFrequency와 관련

bPendingNetUpdate는 포화 상태에서 패킷을 보내지 못하는 상황으로 우선권과 관련

ConsiderList에는 클라이언트에 보낼 수 있는 조건을 만족한 액터를 모아둔 목록

 

액터 컴포넌트 리플리케이션

언리얼에서 리플리케이션의 주체는 액터로, 액터가 소유하는 언리얼 오브젝트(Subobject)에 대해 리플리케이션이 가능

 

SetIsReplicated(true) : 리플리케이션을 지정

ReadyForReplication : 리플리케이션이 준비되면 호출되는 이벤트 함수, PostNetInit과 유사하게 호출됨

+ Recent posts

목차