Sometimes games have long waiting periods where the player might be doing something else. For example in Teal this is the case when the player is waiting for someone to join their lobby. We send a desktop notification if a player has joined so the host can minimize Teal and do something else while waiting. This post will cover how you can send such notifications with the Unreal Engine and show the game window if the notification is clicked. For example this is what the notification for Teal looks like.
We won’t be using any platform specific notifcation system, such as Windows’s toast notifcations. Instead we will be using the same notification system the Unreal Engine uses in the editor. This is basically just a separate window that fades in at the bottom right corner of the screen. This approach is a lot easier than setting up a platform specific notification system.
Let’s dive right in. Here is how we can display a simple notification with some text.
#include "Framework/Notifications/NotificationManager.h"
#include "Widgets/Notifications/SNotificationList.h"
static void ShowNotification()
{
const FText notificationText = NSLOCTEXT("Notifications", "TestNotifications", "This is a test notification");
FNotificationInfo info(notificationText);
info.bFireAndForget = true;
info.FadeInDuration = 0.5f;
info.FadeOutDuration = 1.0f;
info.ExpireDuration = 4.0f;
FSlateNotificationManager::Get().AddNotification(info);
}
This shows a simple notification window with some text. It fades in over 0.5 seconds and stays for 4 seconds before fading out. But the notification window is always on the same layer as the game window. Meaning that it is covered by other windows if the game window is not the top window. But we want the notification to always be on top when it spawns. So, we retrieve the SWindow of the notification and force it to the foreground.
static void ShowNotification()
{
... // show the notification
// we force the notification windows to the foreground
{
TArray<TSharedRef<SWindow>> windows;
FSlateNotificationManager::Get().GetWindows(windows);
int32 num = windows.Num();
for (int32 i = 0; i < num; ++i)
{
FSlateNotificationManager::Get().ForceNotificationsInFront(windows[i]);
SWindow& win = windows[i].Get();
win.Restore();
win.ShowWindow();
win.BringToFront(true);
win.HACK_ForceToFront();
}
}
}
Now our notification window is always on top. But the notification is always shown, even if the player has the game window focused. We only want to show the notification if the game window is in the background.
#include "Framework/Notifications/NotificationManager.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "Engine/GameEngine.h"
static void ShowNotification()
{
UGameEngine* engine = Cast<UGameEngine>(GEngine);
if (engine != nullptr)
{
if( !engine->GameViewport->Viewport->IsForegroundWindow())
{
... // show the notification
... // force the notification windows to the foreground
}
}
}
It is time customize the notification window a bit. You can either customize the default notification window or create a new slate widget that is displayed. You can read about creating slate widgets here, and the new widget has to be a INotificationWidget. We took the easier route for Teal and just added a custom image to the default notification widget. It is important that any resources for the notification exist as long as the game is running, since the player can do anything in the game while the notification is displayed. We use a custom image that is only used by the notification and the engine crashes if the image is not always available. Therefore, we put the reference in our custom UGameInstance since that persists as long as the game is running. We need to provide the image as an FSlateBrush, but it is easy to create one.
UCLASS()
class YOUR_GAME_API UYourCustomGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
UYourCustomGameInstance();
UPROPERTY()
UObject* notificationImage;
UPROPERTY()
FSlateBrush notificationBrush;
};
UTealGameInstance::UTealGameInstance()
: notificationImage(nullptr)
{
// prepare the brush for our notification
ConstructorHelpers::FObjectFinder<UTexture2D> image(TEXT("Texture2D'/Game/WIdgets/Images/Icons/img_teallogo.img_teallogo'"));
checkf(image.Succeeded(), TEXT("Could not find the image for the notification"));
notificationImage = image.Object;
notificationBrush.SetResourceObject(notificationImage);
}
With this we have a safe brush we can set for our notification.
The notification right now is purely cosmetic, but we want to add some interactivity. Our game window should pop up if the notification is clicked, so the player does not have to dig through his windows. We bind a function for the OnMouseButtonDown event of the slate widget. We bind a lambda in this case.
#include "Framework/Notifications/NotificationManager.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "Engine/GameEngine.h"
static void ShowNotification(const FText& text, UYourCustomGameInstance* instance)
{
UGameEngine* engine = Cast<UGameEngine>(GEngine);
if (engine != nullptr)
{
if( !engine->GameViewport->Viewport->IsForegroundWindow())
{
const FText notificationText = NSLOCTEXT("Notifications", "TestNotifications", "This is a test notification");
FNotificationInfo info(notificationText);
info.bFireAndForget = true;
info.FadeInDuration = 0.5f;
info.FadeOutDuration = 1.0f;
info.ExpireDuration = 4.0f;
info.Image = &(instance->notificationBrush);
// we define our callback lambda
auto playerJoinedClicked = [](const FGeometry&, const FPointerEvent&) -> FReply
{
// we restore the game window no matter where it is
UGameEngine* engine = Cast<UGameEngine>(GEngine);
if (engine != nullptr)
{
TSharedPtr<SWindow> windowRef = engine->GameViewportWindow.Pin();
if (windowRef.IsValid())
{
SWindow* window = windowRef.Get();
if (window != nullptr)
{
if (window->IsWindowMinimized())
{
window->Restore();
window->ShowWindow();
}
window->BringToFront(true);
window->HACK_ForceToFront();
}
}
}
return FReply::Handled();
};
// we show the notification and set our callback
TSharedPtr<SNotificationItem> item = FSlateNotificationManager::Get().AddNotification(info);
if (item.IsValid())
{
item.Get()->SetOnMouseButtonDown(FPointerEventHandler::CreateLambda(playerJoinedClicked));
}
// we force the notification windows to the foreground
{
TArray<TSharedRef<SWindow>> windows;
FSlateNotificationManager::Get().GetWindows(windows);
int32 num = windows.Num();
for (int32 i = 0; i < num; ++i)
{
FSlateNotificationManager::Get().ForceNotificationsInFront(windows[i]);
SWindow& win = windows[i].Get();
win.Restore();
win.ShowWindow();
win.BringToFront(true);
// what a bunch of hacks
win.HACK_ForceToFront();
}
}
}
}
}
That concludes our notification function. The next part is a bit of a bonus. We also want a sound cue when the notification pops up. But you have probably noticed that the audio is muted when the game window loses focus. Therefore, even if we play a sound it would be muted. So, we have to enable unfocused audio first. There is a simple option for that in the Engine.ini.
[Audio]
UnfocusedVolumeMultiplier=1.0
You can also set this at runtime.
void UTealGameInstance::SetBackgroundVolumeAudio(float volume)
{
// we set the value in the config file so it is saved
GConfig->SetFloat(TEXT("Audio"), TEXT("UnfocusedVolumeMultiplier"), volume, GEngineIni);
// we also have to set it in the app directly, because it is only polled form the config file once during startup
FApp::SetUnfocusedVolumeMultiplier(volume);
}
float UTealGameInstance::GetPlayAudioInBackground()
{
return FApp::GetUnfocusedVolumeMultiplier();
}
Now you can simply play an audio cue when you trigger the notification. In Teal we also have an option for the player to enable audio playback in the background.
comments powered by Disqus