UE5 Async Load Asset - Loading Screen

Hi,

I want to make a loading screen system, but I don’t want the fake solution of adding a widget, putting a delay node, and calling “Open Level” function. I want loading screen to play some music and animation while the level loads in the background, and only after the level is fully loaded, to remove the loading screen widget.

While searching the internet, I only found the c++ solution with GetMoviePlayer() and Slate widgets. But I was wondering if I can call Async Load Asset in my Game Instance. Put my level as a soft object reference, and on completion remove the loading screen widget.

image

My question is, is this a valid way of doing this? Will the “Completed” execution pin guarantee that my level is loaded into memory? Especially because “Open Level by Object Reference” takes a soft object reference as an argument. Is there even a point in doing this?

Hi @MihailoML

The truth is, this is all more easily handled in C++, but still doable in BP.

If you’re not using World Partition, you should use Level Streaming.

The way the whole Loading Screen thing works is a bit complicated, as things get destroyed or recreated during level transition, but you can initiate the loading screen from the GameInstance, which is preserved through map transition.

Assuming you’re making a single player game, this would be a viable logic:

  1. Create and display LoadingScreen Widget

  2. After that, start level transition.

  3. (Maybe add a small delay before this, specially if you have animations)

  4. On the BeginPlay of the level, delay until next tick, then start Loading Streaming Levels (your Persistent map’s sublevels)

  5. When all are done loading, depending on how big they are, they should be fully loaded and visible or still need some time, youll see by testing.

  6. If you don’t want popups, try adjusting how long after load completion you want to wait before hidding the Loading Screen. To hide the loading screen, you’ll need to Get Game Instance and have a function or event you can call to HideLoadingScreen.

On complete only determines that all the assets are spawned on the world, doesn’t mean that its had time to render everything, so its possible to see actors or visual recalculations popup after On Complete.

If you were to try doing this in C++, you can control the hole thing from the GameInstance directly, using PreLoadMap & PostLoadMap delegates (Dispatchers)

1 Like

Hi @GRIM_Warlock

Thanks for the reply. I’m not using Level Streaming since it doesn’t allow changing of Game Mode between levels. I decided for the C++ approach at the end, with PreLoadMap and PostLoadMapWithWorld delegates as you mentioned.

Right now I’m having trouble with adding custom image(texture) to my Slate widget that I show during loading.

#include "SLoadingScreenWidget.h"



void SLoadingScreenWidget::Construct(const FArguments& InArgs)
{

	const FMargin ContentPadding = FMargin(500.0f, 300.0f);

	
	ChildSlot
	[
		SNew(SOverlay)
			+ SOverlay::Slot()
			.HAlign(HAlign_Fill)
			.VAlign(VAlign_Fill)
			[
				SNew(SImage)
					.Image(new FSlateColorBrush(FLinearColor::Black))
			]

	];
	

}

This is my widget right now. It shows simple black screen, but I don’t know how tu use SImage to show custom texture I have in my Content folder.

This one way I used in the past for that:

Custom Slate header

#pragma once

#include "SlateBasics.h"
#include "SlateExtras.h"

#include "Widgets/SCompoundWidget.h"

/** SLATE CLASS */
class WORLDPARTITIONTEST_API SSlateLoadingScreen : public SCompoundWidget
{
public:

	SLATE_BEGIN_ARGS(SSlateLoadingScreen) : _ItemImage(nullptr) {}
	SLATE_ARGUMENT(UTexture2D*, ItemImage);
	SLATE_END_ARGS()
	
	void Construct(const FArguments& InArgs);

	UTexture2D* ItemImage;

	virtual bool SupportsKeyboardFocus() const override { return false; };
};

Custom Slate cpp

#include "SlateLoadingScreen.h"

#include "Widgets/Images/SImage.h"
#include "Widgets/SCanvas.h"

void SSlateLoadingScreen::Construct(const FArguments& InArgs)
{
	if (!InArgs._ItemImage)
		return; //No Texture Loaded

	InArgs._ItemImage->UpdateResource();

	FSlateImageBrush* ImageBrush = new FSlateImageBrush(InArgs._ItemImage, FVector2D(InArgs._ItemImage->GetSurfaceWidth(), InArgs._ItemImage->GetSurfaceHeight()));

	ChildSlot
	[
		SNew(SOverlay)
		+ SOverlay::Slot()
		.HAlign(HAlign_Fill)
		.VAlign(VAlign_Fill)
		[
			SNew(SImage)
			.Image(ImageBrush)
		]
	];
}

Loading Screen Manager (Where you display it from)

header

class SWidget;
class SWeakWidget;
class SSlateLoadingScreen;

UCLASS()
class LOADINGSCREEN_API ULoadingScreenManager : public UObject
{
	GENERATED_BODY()
	
public:

	ULoadingScreenManager();
	~ULoadingScreenManager();

	UFUNCTION()
	void BeginLoadingScreen();

	UFUNCTION()
	void EndLoadingScreen();

protected:

	TSharedPtr<SSlateLoadingScreen> LoadScreenWidget = nullptr;
	TSharedPtr<SWidget> LoadScreenWidgetContainer = nullptr;
	TSharedPtr<SWeakWidget> Content = nullptr;
	UTexture2D* BackgroundImage = nullptr;
};

CPP

// Required includes
#include "Widgets/SWidget.h"
#include "Widgets/SWeakWidget.h"
#include "WorldPartitionTest/SlateLoadingScreen.h"

// Create Loading Screen 
void ULoadingScreenManager::BeginLoadingScreen()
{
	if (GEngine && GEngine->GameViewport)
	{
		EndLoadingScreen();

		if(!BackgroundImage)
			BackgroundImage = LoadObject<UTexture2D>(nullptr, TEXT("/Game/00_Main/UI/WPTest.WPTest"));

		if (!BackgroundImage)
			return; // No image loaded!

		if(LoadScreenWidget.IsValid())
			LoadScreenWidget.Reset();

		if (!LoadScreenWidget.Get() && BackgroundImage)
			LoadScreenWidget = SNew(SSlateLoadingScreen).ItemImage(BackgroundImage);

		if (!Content || !LoadScreenWidgetContainer)
		{
			Content.Reset();
			Content = nullptr;
			LoadScreenWidgetContainer.Reset();
			LoadScreenWidgetContainer = nullptr;

			Content = SAssignNew(LoadScreenWidgetContainer, SWeakWidget).PossiblyNullContent(LoadScreenWidget.ToSharedRef());
		}

		GEngine->GameViewport->AddViewportWidgetContent(Content.ToSharedRef());
	}
}

void ULoadingScreenManager::EndLoadingScreen()
{
	if (GEngine && GEngine->GameViewport)
	{
		if(Content && GEngine->GameViewport->GetGameViewportWidget()->GetContent().ToSharedRef() == Content)
			GEngine->GameViewport->RemoveViewportWidgetContent(Content.ToSharedRef());
	}
}

This whole thing works fine. But if you’re interested you can also plug in a Widget Blueprint by converting it to TSharedRef.
UUserWidget* MyWidget; / GameViewportClient->AddViewportWidgetContent( MyWidget->ToSharedRef());

1 Like

I went with this simpler solution for now. I’ll look more into your code when I decide to improve my loading screen system, but for now, this works fine. Thanks for the help!

void SLoadingScreenWidget::Construct(const FArguments& InArgs)
{

	const FMargin ContentPadding = FMargin(500.0f, 300.0f);

	FString ImagePath = FPaths::ProjectContentDir() / TEXT("Images/MyImage.png");
	FName BrushName = FName(*ImagePath);

	
	ChildSlot
	[
		SNew(SOverlay)
			+ SOverlay::Slot()
			.HAlign(HAlign_Fill)
			.VAlign(VAlign_Fill)
			[
				SNew(SImage)
					.Image(new FSlateDynamicImageBrush(BrushName, FVector2D(4096, 4096)))
			]

	];
	

}

(I’m not sure what FVector2D does in the FSlateDynamic ImageBrush constructor, whatever number I put the result looks the same)