언리얼 UI DrawWindow 최적화 - ColorMonster 프로젝트 개선 [2]

컵피자·2024년 9월 30일

UNSEEN-2ND & UNREAL

목록 보기
1/9
post-thumbnail

https://velog.io/@cuppizza/언리얼-UI-최적화
여기 이론에서 배운 내용을 실제 저의 프로젝트에서 적용해, 프로젝트를 개선해본 시도.
여러 UI 최적화 방법 중에서, DrawWindow 함수 관련 최적화 -> 그 중에서도 Visibility 관련 옵션으로 최적화하는 것을 시도했다.

Slate::DrawWindows() 최적화

대충 이러한 UMG 위젯 트리 구조다.

해상도 변경에 따라 배경 이미지는 비율을 잃지 않기 위해 size box 하위에 scale box 하위로 일괄 관리.

아래 코드에서 Window는 SelfHitTestInvisible로 하고, Img는 HitTestInvisible로 한 이유다.

  • Window 하위의 버튼 등은 상호작용 가능해야하고, 자식이 확장될 때 상호작용 해야할 가능성이 크기 때문이다. 그래서 SelfHitTestInvisible 을 선택했다.
  • Img 자식이 확장되어도 상호작용할 가능성이 작기 때문에, 자식마저도 HitTestGrid에 넣지 않는 옵션인 HitTestInvisible 을 선택했다.
void UCMUserWidget::TurnWinWindow(bool IsTurnOn)
{
	
	// 마우스 위치 해제 필요
	ACMPlayerController* PlayerController = Cast<ACMPlayerController>(UGameplayStatics::GetPlayerController(GetWorld(), 0));
	if(PlayerController)
	{
		PlayerController->SetPlayerInputMode(false);
	}
	if(IsTurnOn == true)
	{
		// 마지막 레벨이면 엔딩 띄우기
		UCMGameInstance* GameInstance = Cast<UCMGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
		if(GameInstance && GameInstance->GetGameLevel() == GameInstance->MaxLevel)
		{
        	//******************************************************************
			EndingWindow->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
			EndingImg->SetVisibility(ESlateVisibility::HitTestInvisible);
            //******************************************************************
		}
		else
		{
        	//******************************************************************
			WinWindow->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
			WinImg->SetVisibility(ESlateVisibility::HitTestInvisible);
            //******************************************************************
		}
		//******************************************************************
		BlackImg->SetVisibility(ESlateVisibility::HitTestInvisible);
        //******************************************************************
		InGameWindow->SetVisibility(ESlateVisibility::Hidden);
	}
	else
	{
		WinWindow->SetVisibility(ESlateVisibility::Hidden);
		WinImg->SetVisibility(ESlateVisibility::Hidden);
		Black_Image->SetVisibility(ESlateVisibility::Hidden);
	}
}
  • 인게임 내에서는 마우스로 UI를 클릭하거나 기타 상호작용할 가능성이 없기 때문에, 과감하게 HitTestInvisible 옵션을 선택했다. 인게임에서는 마우스로 방향 전환을 해야하기 때문이다.
void UCMUserWidget::ClickedPlayBtn()
{
	// 버튼과 기존 이미지 Hidden
	
	// 마우스 위치 고정 필요
	// .. 
	
	InGameWindow->SetVisibility(ESlateVisibility::HitTestInvisible);
  • 총 전환에 따라 왼쪽 총 UI, 오른쪽 총 UI 역시 On/Off 가 자동화되어 있는 함수 역시 마찬가지로, 상호작용할 가능성이 없기 때문에 Turn On 할 때는 HitTestInvisible
void UCMUserWidget::ConvertGunUI(uint8 InIsLeft)
{
	ESlateVisibility visibleLeft = (InIsLeft == 1) ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Hidden;
	ESlateVisibility visibleRight = (InIsLeft == 0) ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Hidden;
	LeftGunWindow->SetVisibility(visibleLeft);
	RightGunWindow->SetVisibility(visibleRight);
}

프로파일링 비교

최적화 전

최적화 후

Slate::DrawWindows()의 Inclusive Time과 Exclusive Time이 줄어들었다.

특히 Exclusive Time이 1.46%에서 0.07%로 크게 감소하였다.
(몇 번 더 테스트 해봤는데, 최대 0.58%, 1%를 더 이상 넘지 않는다.)

Inclusive Time과 Exclusive Time의 감소 원인:

  • Inclusive Time: 해당 함수와 그 함수가 호출하는 모든 하위 함수들의 실행 시간을 합한 것으로, DrawWindow() 함수의 호출 횟수가 줄어들면 Inclusive Time도 감소한다.
  • Exclusive Time: 해당 함수 자체만의 실행 시간으로, DrawWindow() 함수 내부의 작업량이 줄어들면 Exclusive Time도 감소한다.

DrawWindows() 함수 내에서 HitTestGrid 에 관련된 어떤 연산을 줄였는지 확인해보자.

Exclusive Time 감소 영향 미친 연산 확인

  • WidgetRenderer.cpp → Slate와 UMG의 WidgetComponent도 사용하는 DrawWindow 함수
void FWidgetRenderer::DrawWindow(
	FRenderTarget* RenderTarget,
	FHittestGrid& HitTestGrid,
	TSharedRef<SWindow> Window,
	FGeometry WindowGeometry,
	FSlateRect WindowClipRect,
	float DeltaTime,
	bool bDeferRenderTargetUpdate)
{
	FPaintArgs PaintArgs(nullptr, HitTestGrid, FVector2D::ZeroVector, FApp::GetCurrentTime(), DeltaTime);
	DrawWindow(PaintArgs, RenderTarget, Window, WindowGeometry, WindowClipRect, DeltaTime, bDeferRenderTargetUpdate);
}
void FWidgetRenderer::DrawWindow(
	const FPaintArgs& PaintArgs,
	FRenderTarget* RenderTarget,
	TSharedRef<SWindow> Window,
	FGeometry WindowGeometry,
	FSlateRect WindowClipRect,
	float DeltaTime,
	bool bDeferRenderTargetUpdate)
{
#if !UE_SERVER
	FSlateRenderer* MainSlateRenderer = FSlateApplication::Get().GetRenderer();
	FScopeLock ScopeLock(MainSlateRenderer->GetResourceCriticalSection());

	if (LIKELY(FApp::CanEverRender()))
	{
		if (bPrepassNeeded)
		{
			// Ticking can cause geometry changes.  Recompute
			Window->SlatePrepass(WindowGeometry.Scale);
		}

		PaintArgs.GetHittestGrid().SetHittestArea(WindowClipRect.GetTopLeft(), WindowClipRect.GetSize());
        
		//******************강조**********************
		if (bClearHitTestGrid)
		{
			// Prepare the test grid 
			PaintArgs.GetHittestGrid().Clear();
		}
        //******************강조**********************

		{
			// Get the free buffer & add our virtual window
			ISlate3DRenderer::FScopedAcquireDrawBuffer ScopedDrawBuffer{ *Renderer, bDeferRenderTargetUpdate };
			FSlateWindowElementList& WindowElementList = ScopedDrawBuffer.GetDrawBuffer().AddWindowElementList(Window);

			// Paint the window
			int32 MaxLayerId = Window->Paint(
				PaintArgs,
				WindowGeometry, WindowClipRect,
				WindowElementList,
				0,
				FWidgetStyle(),
				Window->IsEnabled());

			//MaxLayerId = WindowElementList.PaintDeferred(MaxLayerId);
			DeferredPaints = WindowElementList.GetDeferredPaintList();

			Renderer->DrawWindow_GameThread(ScopedDrawBuffer.GetDrawBuffer());

			ScopedDrawBuffer.GetDrawBuffer().ViewOffset = ViewOffset;

			FRenderThreadUpdateContext RenderThreadUpdateContext =
			{
				&(ScopedDrawBuffer.GetDrawBuffer()),
				(FApp::GetCurrentTime() - GStartTime),
				static_cast<float>(FApp::GetDeltaTime()),
				(FPlatformTime::Seconds() - GStartTime),
				static_cast<float>(FApp::GetDeltaTime()),
				RenderTarget,
				Renderer.Get(),
				bClearTarget
			};

			MainSlateRenderer->AddWidgetRendererUpdate(RenderThreadUpdateContext, bDeferRenderTargetUpdate);
		}
	}
#endif // !UE_SERVER
}

DrawWindow() 함수 내에서 HitTestGrid를 지우고 재설정하는 연산을 생략하게 된다.

	if (bClearHitTestGrid)
	{
		// Prepare the test grid 
		PaintArgs.GetHittestGrid().Clear();
	}

요약

문제 상황

UMG 위젯 트리의 깊이가 최대 6으로 깊은 편이어서, DrawWindow() 함수의 오버헤드를 줄여야 한다.

해상도와 상관없이 위젯 위치가 자동으로 정렬 되도록 의도하여 구현하다보니, 위젯의 트리가 깊어 위젯이 많고 이를 수정하기 곤란할 때.

원인

Hit Test Grid에 위젯이 많이 추가 될 수록 DrawWindow() 함수 내에서 HitTestGrid를 지우고 재설정하는 연산이 증가한다.

해결방법

다른 방법으로 DrawWindow() 함수의 오버헤드를 줄이기 위해서 상호작용이 필요 없는 위젯은 HitTestGrid에서 제거하는 옵션(HitTestInvisible, SelfHitTestInvisible)으로 수정한다.

결과

Slate::DrawWindows()의 Inclusive Time과 Exclusive Time이 줄어들었다.

특히 Exclusive Time이 1.46%에서 0.07%로 크게 감소하였다.

Inclusive Time과 Exclusive Time의 감소 원인:

  • Inclusive Time: 해당 함수와 그 함수가 호출하는 모든 하위 함수들의 실행 시간을 합한 것으로, DrawWindow() 함수의 호출 횟수가 줄어들면 Inclusive Time도 감소한다.
  • Exclusive Time: 해당 함수 자체만의 실행 시간으로, DrawWindow() 함수 내부의 작업량이 줄어들면 Exclusive Time도 감소한다.

혼자 시도해 본 것이라서..
틀린 내용이 있거나 기타 의견이 있으신 경우, 편하게 댓글 남겨주세요!

0개의 댓글