https://dev.epicgames.com/documentation/ko-kr/unreal-engine/optimization-guidelines-for-umg-in-unreal-engine

https://coding-hell.tistory.com/78

 

[UE4] 언리얼 ui 최적화 기법

[영어원문] https://topic.alibabacloud.com/a/ui-optimization-tips-in-unreal-engine-4_8_8_10274886.html UI optimization tips in Unreal Engine 4 At the Unreal Open Day 2017 event, Epic Games developer support engineer Mr. Guo Chunbiao introduced the UI op

coding-hell.tistory.com

 

https://topic.alibabacloud.com/a/ui-optimization-tips-in-unreal-engine-4_8_8_10274886.html

 

UI optimization tips in Unreal Engine 4

At the Unreal Open Day 2017 event, Epic Games developer support engineer Mr. Guo Chunbiao introduced the UI optimization techniques in Unreal Engine 4 to the developers present. The following is a speech record. Hello everyone, I'm Guo Chunbiao, a develope

topic.alibabacloud.com

https://blog.nullbus.net/94

 

UMG 드로우콜 분석

한줄요약 드로우 콜은 같은 리소스와 같은 레이어 ID라는 두 가지 조건이 동시에 만족되어야 하나로 합칠 수 있습니다. UMG에서의 드로우 콜 언리얼 공식 문서에는 드로우 콜과 관련하여 이런 설

blog.nullbus.net

 

Slate Render Process

일반적인 Rendring Process의 개략도

Game Thread

  • Game Thread에서 Slate Tick은 Frame당 2번 WidgetTree를 순회한다.
    • 한 번은 Paint할 Widget의 크기를 계산하기 위해 PrePass 과정에서.
    • 다른 한 번은 Paint 과정에서 Draw Elements를 계산하기 위해 OnPaint 과정에서.
  • 조금 더 풀어 설명하면 다음과 같다.
    • Common Widget의 Type과 Parameter에 해당하는 Vertex Buffer 생성
    • Widget의 Render Transform이 Vertex Buffer로 계산 되어 Layer ID 및 Material 정보에 따라 Batch Merge 수행
    • 마지막 User Widget이 하나 이상의 Draw Elements 생성
    • 각 Draw Element가 Draw call에 해당하는 Render Thread에 Draw Element를 전달

Render Thread

  • Render Thread에서의 Slate Rendering 작업은 다음 순서를 거친다.

Widget Render

  • UI의 RTT(Render to Texture) 수행
  • Retainer Box를 사용하면 Draw Elements가 Retainer Box의 Retain Target으로 Rendering 된다.

Slate Render

  • Draw Elements를 Back Buffer에 Render
  • Retainer Box를 사용하는 경우, Retainer Box의 Texture Resource가 Back Buffer에 Rendering 된다.

Invalidation

https://dev.epicgames.com/documentation/en-us/unreal-engine/invalidation-in-slate-and-umg-for-unreal-engine

  • Slate Widget을 캐싱하고 Paint, Layout, 계층 정보 등의 변경점을 관리하는 기능.
    • Widget의 위와 같은 정보가 변경되지 않으면, 매 Frame마다 Widget을 다시 그리는 대신 Cache를 출력한다.
    • 위 정보들에 유효한 변경사항이 발생하면 Slate가 이를 재 계산하여 다시 그려준다.

Invalidation Box

  • Child Widget의 Geometry를 Cache 하고 관리하는 UI
    • 해당 Widget의 Geometry가 바뀌지 않는 한 Cache 된 Geometry로 대체되어 CPU 사용량을 크게 줄여준다.
  • Invalidation Box는 감싸진 UI 뿐 아니라 그 하위 계층의 모든 UI에 대해 Geometry Cache를 진행합니다.

Global Invalidation

  • SWindow의 Invalidation을 이용해 효과적으로 모든 UI를 Invalidation Box로 Wrapping하는 기능.
    • 이 SWindow에 포함 된 모든 Invalidation Box는 무효화 되고, SWindow의 Invalidation Box만 동작하게 된다.
  • Slate.EnableGlobalInvalidation을 true로 트리거하여 활성화
void SWidget::Invalidate(EInvalidateWidgetReason InvalidateReason)
{
	SLATE_CROSS_THREAD_CHECK();

	if (InvalidateReason == EInvalidateWidgetReason::None || !IsConstructed())
	{
		return;
	}

	SCOPED_NAMED_EVENT_TEXT("SWidget::Invalidate", FColor::Orange);

	// Backwards compatibility fix:  Its no longer valid to just invalidate volatility since we need to repaint to cache elements if a widget becomes non-volatile. So after volatility changes force repaint
	if (EnumHasAnyFlags(InvalidateReason, EInvalidateWidgetReason::Volatility))
	{
		InvalidateReason |= EInvalidateWidgetReason::PaintAndVolatility;
	}

	if (EnumHasAnyFlags(InvalidateReason, EInvalidateWidgetReason::Prepass))
	{
		MarkPrepassAsDirty();
		InvalidateReason |= EInvalidateWidgetReason::Layout;
	}

	if (EnumHasAnyFlags(InvalidateReason, EInvalidateWidgetReason::ChildOrder) || !PrepassLayoutScaleMultiplier.IsSet())
	{
		MarkPrepassAsDirty();
		InvalidateReason |= EInvalidateWidgetReason::Prepass;
		InvalidateReason |= EInvalidateWidgetReason::Layout;
	}

	const bool bVolatilityChanged = EnumHasAnyFlags(InvalidateReason, EInvalidateWidgetReason::Volatility) ? Advanced_InvalidateVolatility() : false;

	if(FastPathProxyHandle.IsValid(this))
	{
		// Current thinking is that visibility and volatility should be updated right away, not during fast path invalidation processing next frame
		if (EnumHasAnyFlags(InvalidateReason, EInvalidateWidgetReason::Visibility))
		{
			SCOPED_NAMED_EVENT(SWidget_UpdateFastPathVisibility, FColor::Red);
			UpdateFastPathVisibility(FastPathProxyHandle.GetProxy().Visibility.MimicAsParent(), FastPathProxyHandle.GetInvalidationRoot_NoCheck()->GetHittestGrid());
		}

		if (bVolatilityChanged)
		{
			SCOPED_NAMED_EVENT(SWidget_UpdateFastPathVolatility, FColor::Red);

			TSharedPtr<SWidget> ParentWidget = GetParentWidget();

			UpdateFastPathVolatility(ParentWidget.IsValid() ? ParentWidget->IsVolatile() || ParentWidget->IsVolatileIndirectly() : false);

			ensure(!IsVolatile() || IsVolatileIndirectly() || EnumHasAnyFlags(UpdateFlags, EWidgetUpdateFlags::NeedsVolatilePaint));
		}

		FastPathProxyHandle.MarkWidgetDirty_NoCheck(InvalidateReason);
	}
	else
	{
#if WITH_SLATE_DEBUGGING
		FSlateDebugging::BroadcastWidgetInvalidate(this, nullptr, InvalidateReason);
#endif
		UE_TRACE_SLATE_WIDGET_INVALIDATED(this, nullptr, InvalidateReason);
	}
}

Retainer Panel

  • Child Widgets을 유저의 화면에 Render 하기 전에 하나의 Texture로 병합

  • 여기서 Phase는 Render를 시작하는 Frame, Phase Count는 Render가 되는 Frame 주기이다.
    • 예를 들어 위의 경우 0 Frame에서 시작하여 3 Frame 단위로 Retainer Panel의 Child Widgets이 Render 된다.
  • 이러한 기능들은 매 Frame마다 호출되는 UI의 Draw call을 줄여주는 기능을 제공한다.
  • 하지만 Retainer Panel은 다시 그려질 때 큰 overhead를 가지면서,
    Widget 개개인이 점유하는 Memory가 Invalidation Box보다 크다.
    • 이는 Retainer Panel이 각 Widget의 Invalidation Data 뿐 아니라 고유한 Render Target도 가지기 때문이다.
  • 때문에 UI의 CPU 점유율을 낮추려 한다면 우선 Invalidation Box부터 써야 한다.
    • 그럼에도 Draw Call을 줄이고 싶다면, Retainer Panel을 이용함으로 CPU 사용량을 더 압축할 수 있다.
  • 이는 성능 제약이 빡빡한 저성능 모바일에서 유용다.

How Invalidation works

  • Widget이 화면에 그려질 때 다음 작업들이 순차적으로 발생한다.
    • 계층 구조 - Slate가 root widget과 그들의 child를 모두 포함한 계층에 따라 widget tree를 생성한다.
    • Layout - Slate가 Render Transform을 기반으로 Widget의 크기와 스크린 상 위치를 계산한다.
    • Paint - Slate가 각 Widget의 Geometry를 계산한다.
  • 각 작업들은 수행 할 때 뒤이은 작업을 반드시 수행해야 한다.
    • 예를 들어 계층 구조 작업이 수행되면 반드시 Layout, Paint 작업이 수행되어야 한다.
  • Invalidation System은 위 과정에서 발생하는 모든 데이터를 Memory에 Cache한다.
    • Widget이 Invalidation Box를 사용하든, SWidget의 Global Invalidation을 사용하든.
  • Memory에 Cache된 Widget들은 변경사항이 발생하지 않는 한 재연산을 하지 않는다.
    • 만약 변경사항이 발생하게 되면 Dirty List에 추가되고,
      이 List의 Widget들은 다음 Frame에서 재연산이 이루어진다.
  • Invalidation에서 Cache된 Data를 갱신하게 되는 타입은 대략 다음과 같다.
/**
 * The different types of invalidation that are possible for a widget.
 */
enum class EInvalidateWidgetReason : uint8
{
	None = 0,

	/**
	 * Use Layout invalidation if your widget needs to change desired size.  This is an expensive invalidation so do not use if all you need to do is redraw a widget
	 */
	Layout = 1 << 0,

	/**
	 * Use when the painting of widget has been altered, but nothing affecting sizing.
	 */
	Paint = 1 << 1,

	/**
	 * Use if just the volatility of the widget has been adjusted.
	 */
	Volatility = 1 << 2,

	/**
	 * A child was added or removed.   (this implies prepass and layout)
	 */
	ChildOrder = 1 << 3,

	/**
	 * A Widgets render transform changed
	 */
	RenderTransform = 1 << 4,

	/**
	 * Changing visibility (this implies layout)
	 */
	Visibility = 1 << 5,

	/**
	 * Attributes got bound or unbound (it's used by the SlateAttributeMetaData)
	 */
	AttributeRegistration = 1 << 6,

	/**
	 * Re-cache desired size of all of this widget's children recursively (this implies layout)
	 */
	Prepass = 1 << 7,

	/**
	 * Use Paint invalidation if you're changing a normal property involving painting or sizing.
	 * Additionally if the property that was changed affects Volatility in anyway, it's important
	 * that you invalidate volatility so that it can be recalculated and cached.
	 */
	PaintAndVolatility = Paint | Volatility,
	/**
	 * Use Layout invalidation if you're changing a normal property involving painting or sizing.
	 * Additionally if the property that was changed affects Volatility in anyway, it's important
	 * that you invalidate volatility so that it can be recalculated and cached.
	 */
	LayoutAndVolatility = Layout | Volatility,


	/**
	 * Do not use this ever unless you know what you are doing
	 */
	All UE_DEPRECATED(4.22, "EInvalidateWidget::All has been deprecated.  You probably wanted EInvalidateWidget::Layout but if you need more than that then use bitwise or to combine them") = 0xff
};
  • Invalidation은 자주 변경되지 않은 UI에서 최적화된 기능이다.
    • Widget이 오랫동안 변경되지 않을수록 Slate가 Cache Data를 오래 들고 있고, 그 만큼 CPU 부하를 줄여준다.
  • 이는 특히 MMORPG나 라이브 서비스 게임의 Depth가 있는 메뉴와 같이 규모가 크고 복잡한 UI 작업에서 중요하다.

Volatile Widget

  • 간혹 특정 Widget은 가능한 매 Frame마다 업데이트가 되어야 하는 경우가 있다.
    • 이 경우 Widget은 변경사항이 있을 때마다 매 tick이 Invalidate 된다.
    • 하지만 CPU 부하는 Invalidation을 사용할 때와 동일하게 발생한다.
    • 더불어 Hiearchy를 Cache하기 위해 Memory도 점유하게 된다.
  • 이를 해결하기 위해 자주 사용하는 Widget에 Volatile 선언을 해주는 것이 좋다.
    • Volatile 선언이 된 Widget과 그 Child Widget들은 Paint Data가 Cache되지 않는다.
  • 비록 Geometry는 매 Frame마다 재계산되어 다시 그려지겠지만,
    Slate는 직접적인 변경사항이 없는 이상 Layout 계산은 계속해서 Skip한다.
  • 이는 UI를 전반적으로 Invalidation 하고 싶지만,
    자주 갱신되는 소수의 Widget으로 인해 이득을 보지 못할 때 유용하다.

개발자가 주의해야 할 점

OnTick/OnPaint 사용 지양

  • OnTick이나 OnPaint에서 작업을 하게 되면 그 작업이 매 Frame마다 호출하게 됨.
  • 가급적 Event Dispatch나 Delegate를 이용할 것을 권장

Attribute Bind 대신 Event-driven Update 사용

  • Unreal Engine에서 제공하는 Attribute Bind 역시 매 Frame마다 할당 됨.
  • 이 역시 Event와 Delegate를 이용해 변경사항이 있을 때에만 Widget에 적용 되도록 작업할 것을 권장 함.

Widget Construction

Reduce unused widgets

  • Widget의 모든 child는 시각화 여부와 무관하게 항상 construct 됨.
    • 이는 Render가 되지 않더라도 Loading time, Construction time, Memory를 점유한다는 의미.
  • 때문에 당장 사용되지 않는 Widget들은 Hierarchy에서 제거하는 것이 옳다.

Break complex widgets

  • 특히 Main System에서 사용되는 Widget의 경우 실제로 표시되는 것은 몇 없지만 Cild가 수 천개씩 있는 경우가 있다.
    • 이러한 Widget을 한꺼번에 Load하게 되면 불필요한 Child로 인해 Loading이 지연되고 Memory를 점유하게 된다.
  • 때문에 일정 규모 이상의 Widget은 어느정도 Child Widget을 세분화 하는 것이 좋다. 
  • 예를 들어, 다음과 같이 Widget을 구분하여 작업 방향성을 정할 수 있다. 
    • 항상 표시되는 Widget
      • BaseWidget과 같이 Load하면서 화면에 바로 출력
    • 가능한 빨리 표시되어야 하는 Widget
      • 당장 사용하지 않을 수도 있지만 반응성이 높아야 함.
      • BaseWidget과 같이 Load 하되, 화면에는 출력하지 않고 Visibility로 컨트롤.
    • 조금 늦게 표시되어도 괜찮은 Widget
      • 가끔 사용되거나 아얘 사용되지 않는 경우도 있음.
      • BaseWidget과 별개로 필요할 때마다 Async Load.
  • 이러한 방식은 Memory를 크게 절약할 뿐 아니라 Load 할 때의 CPU 영향도 줄일 수 있다.

Layout

CanvasPanel 사용 지양

  • CanvasPanel은 좌표 평면과 Widget 별 Anchor를 이용해 다른 Widget의 위치를 지정할 수 있는 강력한 Widget이다.
    • 이는 Widget을 원하는 위치에 정확하게 지정하면서,
      동시에 Screen의 외각을 기준으로 Widget의 위치를 유지할 수 있다.
  • 하지만 그와 동시에 높은 성능을 요구하는 Widget이기도 하다.
    • Slate의 Draw call은 Widget의 Layer ID별로 발생한다.
    • VerticalBox나 HorizontalBox등의 다른 Container Widget들은 Child Widget의 Layer ID를 통합한다.
    • 하지만 CanvalsPanel은 Child Widget이 필요할 때 다른 Widget 위에 Render 될 수 있도록 ID를 증가시킨다.
  • 결과적으로 CanvasPanel은 단기로 여러 개의 Draw call을 발생시켜 높은 CPU 사용량을 요구하게 된다.
    • 비록 CanvasPanel보다 용처가 제한적이지만, OverlayPanel 역시 Draw call을 증가시킨다.
    • GridPanel은 Slot마다 Layer를 직접 지정할 수 있지만, 보통은 Child를 Iterate하며 LayerID를 새로 부여한다.
    • ScrollBox는 Scroll Bar가 Layer ID를 증가시킨다.
    • Border 역시 Layer ID를 증가시킨다.
  • HUD나 Menu System의 Root Widget으로 CanvasPanel을 사용하는 것은 크게 문제가 되지 않는다.
    • 이 경우에는 상세한 위치 조정이나 복잡한 Z-Order 배치가 필요할 가능성이 높기 때문이다.
  • 다만 Template 속성이 있는 Widget은 CanvasPanel 위에서 작업하는 것을 지양해야 한다.
    • TextBox, Custom Button과 같이 다른 Widget의 구성요소로 사용되는 Custom Widget들
  • 또는 CanvasPanel을 다수의 Layer에서 과도하게 사용하면, 최종 Layer를 혼돈하기 쉽다.
  • 일반적으로 하나의 요소로 구성된 Widget은 Canvas Panel로 감쌀 필요가 전혀 없다.
  • 또한 HUD나 Menu 같은 경우에도,
    Overlay나 SizeBox를 HorizontalBox, VerticalBox, GridBox와 같이 사용하여 CanvasPanel 사용을 대체할 수 있다.

SizeBox 대신 가능한 Spacer 사용

  • SizeBox는 자신의 크기를 계산하고 Render하는데 다양한 값을 사용한다.
  • 만약 Widget이 특정 Width와 Height를 고정적으로 가진다면, Spacer가 훨씬 가볍다.

ScaleBox와 SizeBox를 같이 사용하지 않기

  • ScaleBox와 SizeBox를 같이 사용하면 매 Frame마다 각자 서로의 Size를 오가며 Update하는 Loop에 빠지게 된다.
  • 이 둘에 의존하기 보다는 Layout이 Content의 Native Size에 따라 동작하도록 만드는 것이 적절하다.

RichTextWidget 사용 지양

  • RichTextWidget은 강력한 Format 지정 기능을 제공하는 만큼 표준 TextBox보다 훨씬 무겁다.
  • 만약 RichTextWidget의 모든 기능이 필요한게 아니라 디자인적인 표현만이 필요하다면,
    원하는 외형을 표현할 수 있는 Font를 제작하여 TextWidget에서 사용하는 것이 훨씬 가볍다.

Visibility

  • 화면 출력되어야 하는 Widget이 Visible인 경우, 클릭 시 Click 반응이 동작해 오버헤드가 발생 함.
    • visible 대신 HitTestInvisible, SelfHitTestInvisible을 사용할 것을 권장
  • Hidden의 경우 화면에 보이지 않더라도 영역을 차지하기 위해 Layout Space를 사용 함.
    • Layout Space를 사용하면 Prepass 계산이 매 Frame마다 수행 됨.
    • 완전히 화면에서 숨기면서 영역을 차지할 필요가 없다면 Collapse를 사용하는 것을 권장.

Texture

Merged Texture

  • Widget에서 여러 개의 Texture를 사용하는 경우, Texture의 갯수만큼 Draw call이 발생한다.
    • 때문에 가능하면 Merge Texture 1개를 사용하는 것이 좋다.
  • Widget의 Sprite를 사용하면 Merged Texture를 사용하거나 편집할 수 있다.

Atlas Group

https://velog.io/@devkcy/ue5-textureatlas

 

[UE5] Texture Atlas

텍스처 아틀라스(Texture Atlas)란 여러 텍스처를 포함하는 텍스처로, 빈 공간에 다른 텍스처를 추가하여 메모리 낭비를 줄인다.

velog.io

 

  • Atlas를 효율적으로 사용하는 방안
    • 위 Merged Texture를 Sprite로 사용하는 것도 Atlas를 거친다.
  • 보통 Texture는 2의 지수로 저장이 된다.
    • 300 * 300인 경우 실제 크기는 512 * 512가 된다.
    • 이 때 필요한 영역 외의 Pixel 너비 만큼 메모리를 낭비하게 된다.
  • Atlas Group은 이런 Texture를 하나의 Texture로 묶는 방식이다.
    • 이는 같은 Widget에서 사용되는 Image들에게 주로 사용되며, Context Switching을 줄이는 효과가 있다.
    • 반대로 다른 Widget의 Image를 묶어주면 Context Switch가 증가한다.
  • 일반적인 Atlas Group은 2048 * 2048 크기를 가진다.

Animation Cost

Material만 있는 Animation

  • GPU로 처리되기 때문에 CPU 비용이 발생하지 않는다.
  • 발광 이펙트, 배경 Scroll, Material 변화로만 표현 가능한 Effect들이 포이에 포함된다.
  • Animation을 Material에 포함시킬 수 있다면, 가장 우선적으로 사용하는 것을 권장한다.

Blueprint로 작업 되었지만 Sequencer가 필요 없는 Animation

  • 실행 비용은 거의 같지만,
    Sequencer Animation은 실행 전 Animation Object Initialize와 담당 Property Path 해석이 요구된다.
    • 즉, Sequencer Animation이 BP Animation보다 CPU 비용이 조금 더 높다.
  • 비교적 짧고 자주 사용되는 Animation의 경우 Sequencer를 지양하고 BP Script로 작업할 것을 권장.

UMG의 Animation Editor로 만들어진 Sequencer Animation

  • UMG의 Animation Editor은 Sequencer 구현체이다.
    • Color 같이 Attribute를 변경하는 경우에는 Widget을 다시 그리게 하지만 Layout을 Invalidate 하지 않는다.
    • 반대로 Render Transform의 변화를 가져오는 모든 Animation은 Layout을 Invalidate한다.
  • 가급적 이러한 작업은 피하거나, Volatile 선언을 해줘야 한다.

Layout 변경을 유발하는 Animation

  • CPU 비용이 가장 높음

기타 최적화 방안

Switch Material

  • 성능이 낮은 Machine에서 Material Effect의 부하를 줄이거나 제거하는 방안
  • DYNAMIC_MULTICAST Framework를 사용

Manager Class

  • 모든 USerWidget과 Brush, Font등 UI Resource를 관리하는 Manager Class를 만드는 것을 권장.

Free Texture Memory

  • Texture를 미리 Setting 하는 것이 아니라 코드를 통해 수동으로 Load/Set/Destroy 하는 것을 전제
    • Editor에서는 Texture를 설정하지 않으면 CDO에서 이 Texture 객체를 참조하는 것을 피할 수 있다.
    • CDO Reference는 ShardPtr의 Reference Count를 최소 1로 만들어 앱이 종료될 때까지 제거되지 않는다.
  • Editor에서 Image Property가 설정되어 있고 Texture를 삭제하는 경우,
    CookStage에서 UImage와 UTexture 사이 Reference를 제거하여 UserWidget의 CDO가
    UTexture를 Reference 하지 않아야 한다.
void UWeakRefImage::Serialize(FArchive& Ar)
{
    Super::Serialize(Ar);
    
    if (Ar.IsCooking() && Ar.IsSaving())
    {
        // Let's suppose user always intend to set a valid resource texture here.
        UObject* ImageRes Brush.GetResourceObject();
        if (ImageRes != nullptr)
        {
            ImageWeakRef = ImageRes;
            Brush.SetResourceObject(nullptr);
            SetBrush(Brush);
        }
    }
    Ar << ImageWeakRef;
}
  • 이 외에 Texture를 Load하는 코드는 다음과 같다.
void UWeakRefImage::LoadTextureResource(bool bAsync/*= true*/)
{
    if (ImageWeakRef.ResolveObject() || (!bAsync && ImageWeakRef.TryLoad()))
    {
    	Brush.SetResourceObject(ImageWeakRef.ResolveObject()); 
        SetBrush(Brush);
    }
    else
    {
        if (StreamableMgr == nullptr)
        {
        	StreamableMgr = new FStreamableManager();
        }
        StreamableMgr->RequestAsyncLoad(ImageWeakRef,
        	FStreamableDelegate::CreateUObject(this, &UWeakRefImage:: LoadTextureDeferred));
    }
}

void UWeakRefImage::LoadTextureDeferred()
{
    if (ImageWeakRef.ResolveObject())
    {
    	Brush.SetResourceObject(ImageWeakRef.ResolveObject()); 
        SetBrush (Brush);
    }
}
  • Texture Unload하는 코드는 다음과 같다.
void UWeakRefImage::UnloadTextureResource()
{
    TSharedPtr<FSlateRenderer> Renderer = FSlateApplicationBase::Get().GetRenderer(); 
    if (Renderer.IsValid())
    {
        FSlateApplicationBase::Get().GetRenderer()->ReleaseDynamicResource(Brush);
        FSlateApplicationBase::Get().GetRenderer()->ReleaseAccessedResources(true);
    }
    Brush.SetResourceObject(nullptr);
    SetBrush (Brush);
}

3D RTT 최적화

  • SceneCaptureComponent2D는 매 Frame에서 tick이 호출된다.
    • 매 Frame마다 Image Update가 발생하는 것을 취소할 수는 있다.
  • 이 때 Animation의 Update 빈도는 30 fps 정도로 충분하므로 BP에서 Tick 간격을 설정할 수 있다.
    • 이 경우 BP에서 Capture을 수동으로 호출해야 한다.
  • 이와 별개로, SceneCapturecomponent2D는 Render Target이 크면 그 자체만으로 성능이 악화된다.

'UE5 > UI' 카테고리의 다른 글

[UI] Common UI FAQ  (0) 2024.07.10
[UI] CommonUI Technical Guide  (0) 2024.07.10
[UI] Common UI Widget  (0) 2024.07.10
[UI] Common UI Introduction  (0) 2024.07.10
[UI] UMG ViewModel  (0) 2024.07.08

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/replicated-object-execution-order-in-unreal-engine?application_version=5.3

Actor Property

  • 기본적으로 Unreliable하고 Single Bunch로 전송된다.
    • Single Bunch에 Unreliable하기 때문에 다른 RPC보다는 나중에 전송된다.
    • 하지만 ForceQueue RPC보다는 먼저 전송된다.

Replicated Using Order

  • 서로 다른 Replicated Property에 대한 RepNotify의 순서는 명확하게 정해진 것이 없다.
    • 클라이언트 상에서 Property의 Dirty 마킹 순서나 메모리상 위치와 아무런 연관이 없다.
  • 때문에 몇몇 Property가 동시에 Replicated 되어야 한다면 Struct로 묶어서 관리하는 것을 권장한다.
  • 만약 Gameplay에 중요한 Replicated Property가 있다면,
    RepNotify를 구현해 Property의 변경사항을 프레임 단위로 대응 하는 것이 좋다.
    • Replication을 통해 변경된 값을 전달받고 RepNotify가 호출된 후에,
      UObject::PostRepNotifies 함수에서 변경사항을 처리할 수 있다.
    • 이 때, 변경된 값이 사용될 준비가 될 때까지 각각의 RepNotify에 저장하는 것이 좋다.

RPC

  • RPC에 대한 대전제는 다음과 같다.
    • Reliable이 Unreliable보다 먼저 전송된다.
    • Unicast는 Reliability와 무관하게 가장 먼저 전송된다.

Order Accross Actors

  • Replicated Actor에 대한 RPC 호출에 대해서는 명확한 법칙이 없다.
  • 때문에 RPC 함수 호출 순서와 실제 RPC 전달 순서는 일치하지 않는다.

Order Inside an Actor

  • Replication System은 동일한 Actor에 대한 reliable RPC 호출의 순서는 보장해준다.
    • 이는 Actor의 Subobject에도 적용된다.
  • 이는 reliable RPC의 경우, 함수를 호출한 순서와 실제 RPC의 전달 순서가 일치한다.

Unreliable VS Reliable

  • Unreliable RPC와 Reliable RPC가 섞여 있을 때에는 순서가 보장되는 것처럼 보이지만, 실상은 보장되지 않는다.
  • Packet 손실이나 재배치가 일어나지 않는다면, Unreliable RPC와 Reliable RPC의 전달 순서는 호출 순서와 일치한다.
    • 하지만 손실/재배치가 일어나면 순서가 달라지게 된다.
    • 정확히는 Reliable RPC끼리는 호출 순서와 전달 순서가 일치한다.

Multicast VS Unicast

  • Multicast RPC와 Unicast RPC가 섞여 있을 때에는 항상 호출 순서와 전송 순서가 일치하지는 않아 더욱 복잡하다.

Reliable Multicast

  • Reliable Multicast의 경우 Reliable Unicast와 섞여 있을 경우 호출 순서와 전송 순서가 일치한다.

Unreliable Multicast

  • Unreliable Multicast의 경우는 절대로 다른 Reliable/Unreliable Multicast와의 순서를 보장해주지 않는다.

RPC Send Policy

  • ERemoteFunctionSendPolicy를 정의하여 Send Policy를 명시해 RPC의 전송 순서에 영향을 줄 수 있다.

https://redchiken.tistory.com/389

 

[Network] Remote Procedure Calls(RPCs)

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/remote-procedure-calls-in-unreal-engine?application_version=5.3RPCLocal 단위에서 호출하여 Remote로 연결된 1개 이상의 Machinge에서 실행되는 함수Return이 없는 단방향

redchiken.tistory.com

Force Send

  • Unreliable Multicast RPC의 순서를 바꾸고 이들이 Queue되는 것을 방지한다.
    • 여기서 Queue 되지 않는다는 것은 대기하지 않고 바로 전송이 된다는 의미이다.

Force Queue

  • 다른 Force Queue RPC와 Unreliable Multicast를 제외한 RPC와의 순서를 보장하지 않는다.
  • 이 말은 ForceQueue, Unreliable Multicast와는 절대적인 순서가 보장된다는 의미이다.

Order Between RPCs and Actor Properties

  • RPC와 Property Replicate 사이에는 대략 다음과 같은 규칙이 적용된다.
    • RPC가 먼저 실행된다.
    • 그리고 Property가 나중에 update된다.
      • Property Replicate는 단일의 Unreliable 데이터 단위로 전송된다.
  • Bunch Payload는 다음 규칙을 따라 생성된다.
    • Queue 되지 않은 PRC 직렬화
    • Replicated Property 직렬화
    • Queue 된 RPC 직렬화
  • 이로 인해 한가지 주의해야 할 점이 있다.
    • RPC 내부에서 변경된 Replicated Property의 값이 뒤이어 발생할 Property Replicate에 의해 손실 될 수 있다.

'UE5 > Network' 카테고리의 다른 글

[Network] Remote Procedure Calls(RPCs)  (0) 2024.06.28
[Network] Property Replication  (1) 2024.06.28
[Network] Network Property  (0) 2024.06.25
[Network] Network Driver  (0) 2024.06.18
[Network] DemoNetDriver 및 Streamer  (0) 2024.06.17

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

RPC

  • Local 단위에서 호출하여 Remote로 연결된 1개 이상의 Machinge에서 실행되는 함수
  • Return이 없는 단방향 함수 호출이 특징
  • RPC는 주로 일시적이거나, 외형적으로 드러나는 Unreliable Gameplay Event에 사용된다.
    • 사운드 재생
    • Particle 생성
    • Animation 재생
  • RPC는 Replicated/ReplicatedUsing 선언이 된 Property의 Replication을 보완하는 중요한 기능이다.
  • RPC를 호출하려면 다음 2가지 조건이 성립되어야 한다.
    • Actor나 Actor Component일 것.
    • RPC를 호출하는 Object가 Replicate되어 있을 것.
  • 마지막으로 RPC를 잘 사용하기 위해서는 Ownership을 잘 이해하는 편이 좋다.

Type

Client

  • 이 Actor에 대해 Client Connection을 소유한 Client에서 실행되는 Unicast RPC
#pragma once

#include "DerivedActor.generated.h"

UCLASS()
class ADerivedActor : public AActor
{
    GENERATED_BODY()

public:

    // Client RPC Function
    UFUNCTION(Client)
    void ClientRPC();
}
#include "DerivedActor.h"

ADerivedActor::ADerivedActor(const class FPostConstructInitializeProperties & PCIP) : Super(PCIP)
{
    bReplicates = true;
}

void ADerivedActor::ClientRPC_Implementation()
{
    // This log will print on every machine that executes this function.
    UE_LOG(LogTemp, Log, TEXT("ClientRPC executed."))
}
// Call from client to run on server
ADerivedClientActor* MyDerivedClientActor;
MyDerivedClientActor->ClientRPC();

Execution Matrix

Server

  • 해당 Actor를 소유하는 Client에서 호출하여 Server에서 실행되는 Unicast RPC
#pragma once

#include "DerivedActor.generated.h"

UCLASS()
class ADerivedActor : public AActor
{
    GENERATED_BODY()

public:
    // Server RPC Function
    UFUNCTION(Server)
    void ServerRPC();
}
#include "DerivedActor.h"
ADerivedActor::ADerivedActor(const class FPostConstructInitializeProperties & PCIP) : Super(PCIP)
{
    bReplicates = true;
}

void ADerivedActor::ServerRPC_Implementation()
{	
    // This function only executes if ServerRPC_Validate returns true.
    // This log will print on every machine that executes this function.
    UE_LOG(LogTemp, Log, TEXT("ServerRPC executed."))
}
// Call from client to run on server
ADerivedClientActor* MyDerivedClientActor;
MyDerivedClientActor->ServerRPC();

Execution Matrix

WithValidation

  • Server RPC에서만 사용할 수 있는 Specifier
    • Server RPC의 신뢰성과 Network Policy를 구현할 수 있다.
#pragma once
#include "DerivedActor.generated.h"

UCLASS()
class ADerivedActor : public AActor
{
    GENERATED_BODY()

public:

    UPROPERTY(Replicated)
    int32 Health;
    int32 MAXHEALTH = 100;

    // Server Unreliable RPC Function
    UFUNCTION(Server, Unreliable, WithValidation)
    void ServerUnreliableRPC(int32 RecoverHealth); 
}
  • Validate 함수는 내부 로직을 통해 RPC 함수를 Server에서 실행할지 여부를 판단한다.
    • 그렇기에 Validate Specifier가 선언된 Server RPC가 실행될 때 Validate 함수가 가장 먼저 호출된다.
#include "DerivedActor.h"

// RPC Validation Implementation
bool ServerUnreliableRPC_Validate(int32 RecoverHealth)
{
    if (Health + RecoverHealth > MAXHEALTH)
    {
        return false;
    }
return true;
}

// RPC Implementation
void ServerUnreliableRPC_Implementation(int32 RecoverHealth)
{
    Health += RecoverHealth;
}
  • 만약 Validate 함수에서 false를 반환하면, 해당 Server RPC를 전송한 Client는 Server로부터 연결이 끊긴다.

NetMulticast

  • Server에서 호출
  • 호출한 Actor와 Relevant한 모든 Client에서 실행되는 Multicast RPC
  • Client에서도 호출할 수 있으나 Local에서만 동작한다.
#pragma once

#include "DerivedActor.generated.h"

UCLASS()
class ADerivedActor : public AActor
{
    GENERATED_BODY()

public:
    // Multicast RPC Function
    UFUNCTION(NetMulticast)
    void MulticastRPC();
}
#include "DerivedActor.h"

ADerivedActor::ADerivedActor(const class FPostConstructInitializeProperties & PCIP) : Super(PCIP)
{
    bReplicates = true;
}

void ADerivedActor::MulticastRPC_Implementation()
{
    // This log will print on every machine that executes this function.
    UE_LOG(LogTemp, Log, TEXT("MulticastRPC executed."))	
}
// Call from server to run on server and all relevant clients
ADerviedServerActor* MyDerivedServerActor;
MyDerievedServerActor->MulticastRPC();

Execution Matrix

Reliability

  • Client/Server/NetMulticast와 같이 사용되는 Specifier

Reliable

  • RPC 수신자로부터 ACK를 받지 못하면 RPC를 재전송한다.
    • 다음 RPC 호출은 앞선 RPC의 ACK를 수신할 때 실행된다.
  • 순서대로 도착하는 것을 보장해준다.

Unreliable

  • RPC Packet이 Drop되면 실행되지 않는다.
  • 도착 순서를 보장하지 않는다.

Send Policy

  • ERemoteFunctionSendPolicy를 지정하여 RPC의 전송 순서를 명시적으로 조정할 수 있다.
enum class ERemoteFunctionSendPolicy
{		
	/** Unreliable multicast are queued. Everything else is send immediately */
	Default, 

	/** Bunch is send immediately no matter what */
	ForceSend,

	/** Bunch is queued until next actor replication, no matter what */
	ForceQueue,
};
  • Send Policy 조절은 NetDriver::ProcessRemoteFunctionForChannel을 통해 가능하다.
/** Process a remote function on given actor channel. This is called by ::ProcessRemoteFunction.*/
ENGINE_API void ProcessRemoteFunctionForChannel(
	UActorChannel* Ch,
	const class FClassNetCache* ClassCache,
	const FFieldNetCache* FieldCache,
	UObject* TargetObj,
	UNetConnection* Connection,
	UFunction* Function,
	void* Parms,
	FOutParmRec* OutParms,
	FFrame* Stack,
	const bool IsServer,
	const ERemoteFunctionSendPolicy SendPolicy = ERemoteFunctionSendPolicy::Default);

void UNetDriver::ProcessRemoteFunctionForChannel(
	UActorChannel* Ch,
	const FClassNetCache* ClassCache,
	const FFieldNetCache* FieldCache,
	UObject* TargetObj,
	UNetConnection* Connection,
	UFunction* Function,
	void* Parms,
	FOutParmRec* OutParms,
	FFrame* Stack,
	const bool bIsServer,
	const ERemoteFunctionSendPolicy SendPolicy)
{
	EProcessRemoteFunctionFlags UnusedFlags = EProcessRemoteFunctionFlags::None;
	ProcessRemoteFunctionForChannelPrivate(Ch, ClassCache, FieldCache, TargetObj, Connection, Function, Parms, OutParms, Stack, bIsServer, SendPolicy, UnusedFlags);
}

Default

  • RPC가 bunch에 직렬화 된다.
  • Bunch는 다음 Frame 마지막에 NetUpdate에서 전송된다.

ForceSend

  • RPC가 NetDriver::PostTickDispatch에서 trigger되면 bunch에 즉시 직렬화 되고 Network에 전송된다.
    • tick의 나머지 부분이 동작하는 도중에 trigger 되면, Default로 동작한다.
  • 이 특별한 RPC 최적화 기법은 아래 조건 하에서 동작한다.
    • Replication Graph나 Iris를 사용할 때에만 동작한다.
    • NetWroldTickTime에서 호출된 RPC에서 동작.
      • 수신한 패킷되고 수신한 RPC가 실행된다.

ForceQueue

  • Network Update이 마무리 될 때 Bandwidth가 남아 있다면 Bunch에 직렬화된다.

'UE5 > Network' 카테고리의 다른 글

[Network] Replication Execution Order  (0) 2024.06.28
[Network] Property Replication  (1) 2024.06.28
[Network] Network Property  (0) 2024.06.25
[Network] Network Driver  (0) 2024.06.18
[Network] DemoNetDriver 및 Streamer  (0) 2024.06.17

+ Recent posts