Pause Game On Steam Overlay Active

Introduction Original Author: (Talk) This guide will help you to trigger a Blueprint Function that can pause your game when the Steam Overlay becomes active. Keep in mind that this guide requires c...

Updated 14 days ago Edit Page Revisions

Introduction

Original Author: Motanum, Updated: Bubble Butt

This guide will show you how to trigger a Blueprints event that can pause your game when the Steam overlay becomes active. It also serves as an introduction to Steam callbacks, so you can get started doing other functions with the help of Steamworks. Keep in mind that this guide requires creating cpp classes. A basic understanding of programming is suggested, though the guide is made to be able to follow step-by-step. I will provide the full necessary code and will explain how it works. We do NOT need to modify the engine code to achieve our goals.

IMPORTANT! The guide assumes that you already have a STEAM APP ID for your game and a build of it on Steamworks, which you can launch via Steam as if you were a normal Steam user. It also assumes that you have the functionality to pause your game via Blueprints. If you are using CPP, then you are likely more knowledgeable than me and should be able to achieve your goals after reading this guide.

The game which this guide is based on is Musical Range VR, so you will see multiple references to it. In the case of my game, I want the game to pause a song if the Steam overlay becomes active. I already have a function in Blueprints to pause the song in my game from a menu, so all I need is the game to automatically trigger it.

Image1.png

Your game will need a Game Instance class. This class can handle a bunch of variables regardless of the map you are in or if you are transitioning to another map. The Game Instance will contain a variable to a SteamManager class. The Steam Manager will have a Steam callback, which just means that Steam will trigger a function inside of SteamManager when an event happens. Then, SteamManager will trigger a Blueprints node event from the Game Instance. We will then create a Blueprint Class based on our custom Game Instance, and there we can code the functionality for the pause in Blueprints.

It's very simple in theory, but the code to get it to work with Steam can be a bit tricky if you are new to SteamAPI or are just starting to include more code in your project. I spent a lot of time fighting countless compilation errors and crashes. I hope this will help you to get this simple functionality quickly.

Steamworks SDK

For the Steam callbacks to work, you must set up the Steamworks SDK for your project. Official documentation can be found here. Also enable the Online Subsystem Steam plugin.

Also, stop by this guide, and take a look at the Requirements section.

Class Setup

Create Game Instance Base Class

It's time to create a new Game Instance base class. Create a new C++ class (UE4: Edit > New C++ Class; UE5: Tools > New C++ Class). On the window tick Show All Class. Search for GameInstance, and select it as our base class.

MotanumTutorial1-Image2.png

Give your class a name. A good idea would be to call it YourGameNameInstance. Mine is Called MusicalRangeInstance. It doesn't need to be placed in the Public or Private locations. Create the class, and wait for the game to compile the new class.

Create Steam Manager Class

Next, we will create our new class, SteamManager. Again, create a new C++ class. This time, search for Object, and select it as our parent class. You will know it's the correct base class if it says it will include "NoExportTypes.h". Name it SteamManager, and create the class. Wait for the game to compile. Once done, it's time to go to Visual Studio.

Set Up Game Instance

Before we can start coding the customized Game Instance base class, it's a good idea to create a Game Instance from our newly-created (but empty) class and set it up in your project. So as we add more functionality and compile the C++ code, it will be reflected in the game. Else, you might think your Game Instance doesn't work.

If you already have a Game Instance in your game, you can simply reparent it to the new base class. In your Game Instance, go to the Class Settings. Change Parent Class to the new C++ base class you just created. Since the base class has Game Instance as its parent class, you will still retain all normal Game Instance functionality that you may already have in your existing Game Instance.

If you do not already have a Game Instance, right click the YourGameInstance class in the content browser (should be in the C++ Classes folder), and select Create Blueprint class based on YourGameInstance. I recommend the name BP_YourGameInstance to differentiate between the C++ base class and the Blueprint child class. Place the newly-created Blueprint in your place of preference.

MotanumTutorial1Image3.png

Next, go to Project Settings. Under Projects, select Maps & Modes. Then, for Game Instance Class, select your BP_YourGameInstance. Because BP_YourGameInstance is based on YourGameInstance, any changes we do in code to YourGameInstance will be reflected in the Blueprint version.

Game Instance Code

YourGameInstance.h

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "SteamManager.h"
#include "YourGameInstance.generated.h"

/**
 * 
 */
UCLASS()
class MUSICALRANGE_API UYourGameInstance : public UGameInstance
{
    GENERATED_BODY()

private:

protected:

public:
    UYourGameInstance();
    ~UYourGameInstance();

    UPROPERTY(BlueprintReadOnly, Category = "Steamworks")
    USteamManager* SteamManager;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Steamworks")
    bool IsSteamOverlayActive;

    const bool EnableUSteamManagerFeatures = true;
    
    /*
    * Fire this from Blueprints to tell the actor when to initialize CPP elements
    */
    UFUNCTION(BlueprintCallable, Category = "Steamworks")
    bool InitializeCPPElements();
    
    /*A function pair that can be called externally executes OnSteamOverlayIsActive()*/
    void PublicOnSteamOverlayIsON();
    void PublicOnSteamOverlayIsOFF();

    UFUNCTION(BlueprintImplementableEvent, Category = "Steamworks")
    void OnSteamOverlayIsActive(bool isOverlayActive);
};

First, we have the constructor and destructor which you may or may not need. Next, we are creating a USteamManager variable inside of YourGameInstance class which will contain the class that receives the information from Steam.

Following that, we have a bool variable. This can be read from Blueprints at any time from the Game Instance if you want to know if the overlay is active. Notice it is BlueprintReadOnly as it will only be modified by one function to make sure the value is always correct.

Then, we also have a constant bool value in case you want to disable the SteamManager features globally for testing purposes. Below that, we have a function that we are going to call on Blueprints to initialize the C++ elements of the Game Instance. We do this because there may be some maps that you don't want the Game Instance to start enabling SteamManager, for example, if you have an introduction or credits level at game launch.

Next, we have a pair of functions, PublicOnSteamOverlayIsON() and PublicOnSteamOverlayIsOFF(). These are two functions that can be called by outside actors to trigger the OnSteamOverlayIsActive Blueprint event. The reason we have two functions instead of one while passing a value will be explained later on.

Finally, the function OnSteamOverlayIsActive(bool isOverlayActive) is an event that will trigger in Blueprints. Think of it like a Custom Event or a Begin Play. The UFUNCTION is what gives it this functionality and makes it special as we won't have to define that function in the C++ file. It must be fired internally by the owner class, thus we needed the other two public functions.

Note: If later you are receiving symbol linking errors when compiling, try directly linking the API library with a pragma directive:

#pragma comment(lib, "ThirdParty/Steamworks/Steamv162/sdk/redistributable_bin/win64/steam_api64")

YourGameInstance.cpp

#include "YourGameInstance.h"
#include "YourGame.h"

//Musical Range Game Instance Elements
UYourGameInstance::UYourGameInstance() {
}

UYourGameInstance::~UYourGameInstance() {
}

bool UYourGameInstance::InitializeCPPElements() {
    if (EnableUSteamManagerFeatures)
    {
        if (SteamUser() != nullptr) //Is the Steam API initialized?
        {
            UE_LOG(LogSteamworks, Log, TEXT("CPP InitializeCPPElements() through USteamManager"));
            SteamManager = NewObject<USteamManager>(this); //Use NewObject if outside of the constructor.
            UE_LOG(LogSteamworks, Log, TEXT("New Object USteamManager created!"));
            SteamManager->InitializeSteamManager();
            UE_LOG(LogSteamworks, Log, TEXT("SteamManager Initialization Finished"));
            SteamManager->AssignGameInstance(this);
            UE_LOG(LogSteamworks, Log, TEXT("CPP InitializeCPPElements() SUCCESS!"));
            return true;
        }
        else
        {
            UE_LOG(LogSteamworks, Warning, TEXT("CPP InitializeCPPElements() FAILED! NO STEAM LOGON"));
            return false;
        }
    }
    else
    {
        UE_LOG(LogSteamworks, Log, TEXT("Custom Steamworks Features NOT Initialized."));
        UE_LOG(LogSteamworks, Log, TEXT("Change EnableUSteamManagerFeatures to True."));
    }

    return false;
}

void UYourGameInstance::PublicOnSteamOverlayIsON() {
    UE_LOG(LogSteamworks, Log, TEXT("PublicOnSteamOverlayIsON Called. Setting isSteamOverlayActive"));
    IsSteamOverlayActive = true;
    this->OnSteamOverlayIsActive(IsSteamOverlayActive);
    UE_LOG(LogSteamworks, Log, TEXT("OnSteamOverlayIsActive Has been triggered"));
}

void UYourGameInstance::PublicOnSteamOverlayIsOFF() {
    UE_LOG(LogSteamworks, Log, TEXT("PublicOnSteamOverlayIsON Called. Setting isSteamOverlayActive"));
    IsSteamOverlayActive = false;
    this->OnSteamOverlayIsActive(IsSteamOverlayActive);
    UE_LOG(LogSteamworks, Log, TEXT("OnSteamOverlayIsActive Has been triggered"));
}

Notice the UE_LOG in the code. This serves as a debug function to know what the game is doing. You will need to define the keyword “LogSteamworks” in YourGame.h and YourGame.cpp. You can learn more about logging here.

First, we have our constructor and destructor. We don't need them, but if you do, then you may use them here. Next, we have InitializeCPPElements(). We have an if statement to execute the code only if we decided to enable it previously with the variable EnableUSteamManagerFeatures.

Next, we do another if statement to check that Steamworks was properly set up. I use (SteamUser() != nullptr) to check for this. If false, I return an error. This has to be used as if it's false while being on the editor or other game dev environments, the game may crash or create bugs.

Then, we initialize the variable of SteamManager by creating a new object and storing it in the SteamManager variable.

SteamManager = NewObject<USteamManager>(this);

The next function is going to initialize SteamManager with the elements it needs in order to function properly. We haven't written this function yet in SteamManager, so we will get Visual Studio telling us about it. Just ignore it for now.

SteamManager->InitializeSteamManager();

Afterwards, we have to store a reference of YourGameInstance inside of SteamManager so that SteamManager knows which class has to fire the events for when Steam tells SteamManager it's time to do so.

SteamManager->AssignGameInstance(this);

Again, we haven't actually created the function for AssignGameInstance() yet in SteamManager, so VS will throw an error. For now we'll ignore it.

Next we have the function PublicOnSteamOverlayIsON(). This function will be called by SteamManager when we get a Steam callback saying that the Steam overlay was activated with a true value. In the function, we first write a UE_LOG() to know that the function was called properly by SteamManager.

Then we set the bool variable of YourGameInstance to true. Remember, we are in the ON version of the function. Then we launch the Blueprint event node OnSteamOverlayIsActive() with IsSteamOverlayActive (which we just set).

this->OnSteamOverlayIsActive(true);

We use the this keyword as explained by Rama in his guide to know that we are not calling a C++ function but a Blueprint function.

Finally, another UE_LOG() to know that the function has finished. By the time this log is printed, the game should be paused if you follow the rest of this guide properly.

For the OFF version of the function, we copy the contents of the ON version and just change the bool from true to false.

And that is all we need for YourGameInstance.

SteamManager Code

Before we start coding SteamManager, I recommend you read the Steamworks API Overview to get familiar with it. Once you read that link and are more comfortable with what a callback is, it's time to code the SteamManager class.

SteamManager.h

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "ThirdParty/Steamworks/Steamv162/sdk/public/steam/steam_api.h"
#include "SteamManager.generated.h"

/*~~~ Forward Declarations ~~~*/
class UYourGameInstance;

/**
 * 
 */
UCLASS()
class MUSICALRANGE_API USteamManager : public UObject
{
    GENERATED_BODY()

private:
    //Steam Callback Setups
    //Using STEAM_CALLBACK_MANUAL()
    STEAM_CALLBACK_MANUAL(USteamManager, OnSteamOverlayActive, GameOverlayActivated_t, OnSteamOverlayActiveCallback);

public:
    USteamManager();
    ~USteamManager();

    void InitializeSteamManager();
    void AssignGameInstance(UYourGameInstance *NewYourGameInstance);

    UPROPERTY(BlueprintReadOnly, Category = "Musical Range")
    UYourGameInstance *YourGameInstance;
};

Make sure to include the steam_api.h functions! They will be our way into accessing the Steamworks information. Remember to keep the .generated.h file at the end of the include list, else your compilation will fail.

Next, we need to do a forward declaration. Again, you may learn more about them from another guide written by Rama. The basics of it is that YourGameInstance has a variable of class SteamManager, but SteamManager also has a class of YourGameInstance. You can see how we have this full-circle reference going on. So, a forward declaration will "weakly" define UYourGameInstance so that the compilation doesn't fail.

Next, we set up our Steam callback using STEAM_CALLBACK_MANUAL(). Using STEAM_CALLBACK will fail to compile with UCLASS, so do NOT use those for a UCLASS. The only difference, as I can tell, is that by using STEAM_CALLBACK_MANUAL(), we need to register the callbacks in the C++ file before they can work which is no hassle at all.

STEAM_CALLBACK_MANUAL(USteamManager, OnSteamOverlayActive, GameOverlayActivated_t, OnSteamOverlayActiveCallback);

USteamManager is the class where the Steam callback is handled.

Next, we have the function that will be triggered inside SteamManager.cpp when the Steam callback is fired. Don't confuse it with the function OnSteamOverlayIsActive() in YourGameInstance. I bet you can come up with better names than me. You can think of GameOverlayActivated_t as a variable/structure that is responsible for the GameOverlay. Its name is set by Steam. You can read the Steam API documentation for more callbacks that suit your needs.

OnSteamOverlayActiveCallback is an element we will use to register the callback on the C++ file.

In the public section, we have our constructor and destructor. Then we create those two functions that we called in YourGameInstance.cpp, InitializeSteamManager() and AssignGameInstance().

Finally, we have a variable to store YourGameInstance with its UPROPERTY() macro.

SteamManager.cpp

#include "SteamManager.h"
#include "YourGame.h"
#include "YourGameInstance.h"
#include "Async/Async.h"

USteamManager::USteamManager() {
    UE_LOG(LogSteamworks, Log, TEXT("USteamManager Constructor Called"));
}

USteamManager::~USteamManager() {
}

void USteamManager::InitializeSteamManager() {
    UE_LOG(LogSteamworks, Log, TEXT("Initializing USteamManager"));
    OnSteamOverlayActiveCallback.Register(this, &USteamManager::OnSteamOverlayActive);
    UE_LOG(LogSteamworks, Log, TEXT("OnSteamOverlayActiveCallback.Register called"));
}

void USteamManager::AssignGameInstance(UMusicalRangeInstance *NewYourGameInstance) {
    YourGameInstance = NewYourGameInstance;
    UE_LOG(LogSteamworks, Log, TEXT("New Game Instance Assigned to USteamManager"));
}

void USteamManager::OnSteamOverlayActive(GameOverlayActivated_t *pCallbackData)
{
    UE_LOG(LogSteamworks, Log, TEXT("Intercepted the Steam Overlay callback"));
    const bool isCurrentOverlayActive = pCallbackData->m_bActive != 0;
    UE_LOG(LogSteamworks, Log, TEXT("isCurrentOverlayActive = %d"), isCurrentOverlayActive);
    YourGameInstance; //So that the call list reference on the Lambda works

    if (isCurrentOverlayActive)
    {
        AsyncTask(ENamedThreads::GameThread, [&]() {
            UE_LOG(LogSteamworks, Log, TEXT("Running inside AsyncTask() TRUE"));
            YourGameInstance->PublicOnSteamOverlayIsON();
            UE_LOG(LogSteamworks, Log, TEXT("Exiting AsyncTask()"));
        });
    }
    else {
        AsyncTask(ENamedThreads::GameThread, [&]() {
            UE_LOG(LogSteamworks, Log, TEXT("Running inside AsyncTask() FALSE"));
            YourGameInstance->PublicOnSteamOverlayIsOFF();
            UE_LOG(LogSteamworks, Log, TEXT("Exiting AsyncTask()"));
        });
    }

    UE_LOG(LogSteamworks, Log, TEXT("OnSteamOverlayActive() at SteamManager Finished"));
}

Notice the extensive use of UE_LOG(). You may remove them or keep them to your liking.

Be sure to include YourGameInstance; we need access to the function PublicOnSteamOverlayIsActive() later on. Because the header file gets compiled first, by the time the compiler gets to the cpp file, it will already know that PublicOnSteamOverlayIsActive() exists and will work. This is thanks to the forward declaration we added in the header file. Also, don't forget to include Async.h! This is the key to avoiding crashes.

First we have our constructor and destructor.

Then we have the function to initialize the SteamManager elements. Because we are using STEAM_CALLBACK_MANUAL(), we need to register the callback at some time before the callback will work. I decided to place OnSteamOverlayActiveCallback.Register(this, &USteamManager::OnSteamOverlayActive); inside of the function InitializeSteamManager() to have better control of when the callbacks are registered. You may put that line in the constructor of SteamManager. Be careful of having it multiple times as it may produce multiple callback calls. Next, we have the AssignGameInstance() function. In there, we simply copy the new Game Instance into our variable. This will ensure that when we call the function YourGameInstance->PublicOnSteamOverlayIsActive(), it will not be a null pointer and cause errors or a crash.

Finally, we have OnSteamOverlayActive(). I recommend you leave the UE_LOG() calls that are in the code as they can help you a lot when you debug. It's better to add many at first and then remove some when it all works rather than put a few and have to recompile everything because you need more information.

Notice that we did not define that function OnSteamOverlayActive() as we regularly would in the header file. The macro STEAM_CALLBACK_MANUAL did that for us already at compile time. Note that all functions made with the macro must be void. The parameter for the function is “GameOverlayActivated_t *pCallbackData”. The pointer name can be defined by you. Some documentation names it as pResult.

Inside the function, we create a constant bool variable where we copy the callback result. Is the overlay active or not? Something weird is that pCallbackData->m_bActive actually returns a uint8, so it can't be just placed in a boolean variable with the equal operator. Instead, I make a not equal comparison to zero which yields an equivalent result. If bActive is true, then it's not equal to zero, so a true is stored for isCurrentOverlayActive. If bActive is false, then it is equal to zero, so a false is stored for isCurrentOverlayActive. A weird extra step that is no problem.

For debugging purposes, I decided to have a UE_LOG output the state of the Steam overlay. I recommend you keep these logs for callback results as they can be helpful when debugging. Following that, we write in the name of the variable holding YourGameInstance.

Next, we have an if statement to split the function we are going to call based on the value of isCurrentOverlayActive. If it is true, then we are going to execute PublicOnSteamOverlayIsON(). Inside it is a function called AsyncTask(). AsyncTask() is the key to avoiding a game crash when you try to run something from the callback. Basically, when we get the callback, it is not in the game thread, so if we tried to do YourGameInstance->PublicOnSteamOverlayON(), the game will crash. AsyncTask() takes two parameters. The first is what thread we want the code to run in, and the second is a lambda which is the code you want to run.

For the thread we do ENamedThreads::GameThread. The lambda is a bit more complicated if you haven't worked with them. You can read more about them here. The basics of it so we can get our code to work is the following:

[&]( ) {
// Your code here.
}

The square brackets ask us what variables we want the lambda to capture to be used. The & symbol tells it that we want to make use of references of the variables used on the function. Thus, we wrote previously "YourGameInstance;" so that the lambda would get access to your Game Instance. Then, the parenthesis asks for any intput parameters. We don't need any of them.

Then this code...

    UE_LOG(LogSteamworks, Log, TEXT("Running inside AsyncTask() TRUE"));
    YourGameInstance->PublicOnSteamOverlayIsON();
    UE_LOG(LogSteamworks, Log, TEXT("Exiting AsyncTask()"));

...will tell us when the callback has entered the AsyncTask() function and when it is about to leave the function. And inside the AsyncTask() function, we call PublicOnSteamOverlayIsON(). This will trigger OnSteamOverlayIsActive() which will trigger our Blueprint node to pause the game.

We add similar code for the else part of the if statement. Be sure to change the called function to the OFF version. And that is all we need for SteamManager.cpp.

Blueprint

Now it's up to you how to code your new Blueprint node so that it will pause your game. For example, I will show the Blueprint code I used to pause the song if it is playing in Musical Range VR. My setup is that the game is running a Blueprint based on MusicalRangeInstance (my YourGameInstance). I have a floor monitor actor which has a reference to pretty much every other actor in the scene and handles starting a song, setting options, et cetera.

So, my BP_MusicalRangeInstance fires the OnSteamIsActive() Blueprint event which in turn tells the FloorMonitorUI to pause the song, if possible. The Floor Monitor then tells the actual actor playing the audio to pause the song.

MotanumTutorial1Image4.png

MotanumTutorial1Image5.png

Image1.png

This helps keep the lights on