Usage

This guide explains the steps required to set up a basic lobby, from initial configuration to in-editor testing. Make sure to check out the available example projects for a fully featured lobby implementation that includes UMG UI, a server browser, chat system, player kicking and more.

Getting started

Let’s go through a couple of preliminary steps.

1. Beacons configuration

All network communication in UE is handled by NetDrivers. Since Lobbyist uses beacons to communicate with a server, we’ll need to configure the BeaconNetDriver in DefaultEngine.ini:

DefaultEngine.ini
[/Script/UnrealEd.EditorEngine]
!NetDriverDefinitions=ClearArray
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemUtils.IpNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
+NetDriverDefinitions=(DefName="BeaconNetDriver",DriverClassName="OnlineSubsystemUtils.IpNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
 
[/Script/Engine.GameEngine]
!NetDriverDefinitions=ClearArray
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.IpNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
+NetDriverDefinitions=(DefName="BeaconNetDriver",DriverClassName="OnlineSubsystemSteam.IpNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")

Beacons will now use the built-in IP NetDriver (which is suitable for local testing) in both editor and game. If you plan to use Steam, you can use the following configuration instead:

DefaultEngine.ini
[/Script/Engine.GameEngine]
!NetDriverDefinitions=ClearArray
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
+NetDriverDefinitions=(DefName="BeaconNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")

2. Enabling the plugin

Make sure Lobbyist is enabled in the Edit ➜ Plugins window.

A screenshot of the plugin panel in UE, with Lobbyist enabled

If you plan to use the plugin from C++ don’t forget to list it in your build dependencies.

using UnrealBuildTool;
 
public class MyGame : ModuleRules
{
    public MyGame(ReadOnlyTargetRules Target)
        : base(Target)
    {
        // [...]
        PrivateDependencyModuleNames.Add("Lobbyist");
        // [...]
    }
}

Configuring the classes

Lobbyist provides a set of base classes that you can extend to provide custom functionality, following the same approach of UE classes such as GameMode, PlayerState, etc. At a minimum, you’ll want to provide your own implementation of LobbyistHost and LobbyistClient.

The first class is equivalent to UE GameMode and is responsible for managing players and lobby rules. A LobbyistClient is used to establish a connection to the host and exposes a set of useful events related to the connection lifetime and status.

An in-depth description of all the available classes and their intended usage can be found in the reference section.

1. Creating the classes

Create a subclass of LobbyistHost in either C++ or Blueprint:

A screenshot of the Pick Parent Class dialog in UE, with LobbyistHost selected
#pragma once
#include "CoreMinimal.h"
#include "LobbyistHost.h"
#include "MyLobbyHost.generated.h"
 
UCLASS()
class AMyLobbyHost : public ALobbyistHost
{
    GENERATED_BODY()
};

Do the same for LobbyistClient:

A screenshot of the Pick Parent Class dialog in UE, with LobbyistClient selected
#pragma once
#include "CoreMinimal.h"
#include "LobbyistClient.h"
#include "MyLobbyClient.generated.h"
 
UCLASS()
class AMyLobbyClient : public ALobbyistClient
{
    GENERATED_BODY()
};

2. Configure Lobbyist to use the new classes

Lobbyist can be configured to use the new classes by navigating to the Plugins / Lobbyist section of the Edit ➜ Project Settings menu:

A screenshot of the Lobbyist settings dialog in UE, configured to use the just created classes

Hosting a session

Hosting a session with Lobbyist involves two steps: creating an online session for players to join, and starting up the host that will handle players as they join the session.

1. Creating an online session

Unreal provides a powerful way to manage online sessions by using the Online Subsystem, an interface that provides a common way to access online services such as Steam, Xbox Live, PSN and so on.

In this example we’ll use the Null subsystem which is suitable for local testing, but any other subsystem should work out-of-the-box. To configure the subsystem add the following to DefaultEngine.ini:

DefaultEngine.ini
[OnlineSubsystem]
DefaultPlatformService=Null
 
[OnlineSubsystemNull]
bEnabled=True

If you plan to use Steam you can enable the Steam subsystem as well. This provides a flexible way to test things: the engine will default to the Null subsystem when running the editor (or the game if Steam is closed), but will prefer the Steam subsystem when running the game.

DefaultEngine.ini
[OnlineSubsystem]
DefaultPlatformService=Steam
 
[OnlineSubsystemNull]
bEnabled=True
 
[OnlineSubsystemSteam]
bEnabled=True
SteamDevAppId=480
bInitServerOnClient=true

You can use either Blueprint or C++ to create a game session. Feel free to do this wherever it works best for you: inside an actor, in the game instance, etc.

void ASomeActor::BeginPlay()
{
    Super::BeginPlay();
 
    const FUniqueNetIdRepl PrimaryPlayerId = GetGameInstance()->GetPrimaryPlayerUniqueId();
    const IOnlineSessionPtr SessionInterface = Online::GetSessionInterface(GetWorld());
 
    if (!PrimaryPlayerId.IsValid() || !SessionInterface.IsValid())
    {
        return;
    }
 
    FOnlineSessionSettings SessionSettings;
    SessionSettings.NumPublicConnections = 8;
    SessionSettings.bShouldAdvertise = true;
    SessionSettings.bAllowJoinInProgress = true;
    SessionSettings.bIsLANMatch = false;
    SessionSettings.bUsesPresence = true;
    SessionSettings.bAllowJoinViaPresence = true;
 
    SessionInterface->CreateSession(*PrimaryPlayerId, NAME_GameSession, SessionSettings);
}

2. Starting the host

We’re now ready to initialize the host and wait for clients to join. This is done by calling the CreateLobbyHost function of the Lobbyist subsystem.

void ASomeActor::BeginPlay()
{
    Super::BeginPlay();
 
    const FUniqueNetIdRepl PrimaryPlayerId = GetGameInstance()->GetPrimaryPlayerUniqueId();
    const IOnlineSessionPtr SessionInterface = Online::GetSessionInterface(GetWorld());
 
    if (!PrimaryPlayerId.IsValid() || !SessionInterface.IsValid())
    {
        return;
    }
 
    FOnlineSessionSettings SessionSettings;
    SessionSettings.NumPublicConnections = 8;
    SessionSettings.bShouldAdvertise = true;
    SessionSettings.bAllowJoinInProgress = true;
    SessionSettings.bIsLANMatch = false;
    SessionSettings.bUsesPresence = true;
    SessionSettings.bAllowJoinViaPresence = true;
 
    DelegateHandle_OnCreateSessionComplete = SessionInterface->AddOnCreateSessionCompleteDelegate_Handle(
        FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete));
    if (!SessionInterface->CreateSession(*PrimaryPlayerId, NAME_GameSession, SessionSettings))
    {
        SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(DelegateHandle_OnCreateSessionComplete);
    }
}
 
void ASomeActor::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{
    const UWorld* World = GetWorld();
    if (const IOnlineSessionPtr SessionInterface = Online::GetSessionInterface(World))
    {
        SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(DelegateHandle_OnCreateSessionComplete);
    }
 
    if (bWasSuccessful)
    {
        if (ULobbyistSubsystem* LobbyistSubsystem = ULobbyistSubsystem::Get(World))
        {
            LobbyistSubsystem->CreateLobbyHost();
        }
    }
}

If you want the host to act as a listen server and participate in the session, make sure to create a client and connect to the local host. We’ll go over clients in detail in the next section.

// [...]
 
void ASomeActor::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{
    const UWorld* World = GetWorld();
    if (const IOnlineSessionPtr SessionInterface = Online::GetSessionInterface(World))
    {
        SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(DelegateHandle_OnCreateSessionComplete);
    }
 
    if (bWasSuccessful)
    {
        if (ULobbyistSubsystem* LobbyistSubsystem = ULobbyistSubsystem::Get(World))
        {
            LobbyistSubsystem->CreateLobbyHost();
            if (ALobbyistClient* Client = LobbyistSubsystem->CreateLobbyClient())
            {
                Client->ConnectToSession();
            }
        }
    }
}

Participating in a session

For a client to be able to participate in a session we’ll need to join the session using the Online Subsystem and connect to the host.

1. Joining the session

First retrieve a list of all available sessions and select one to join. Feel free to do this wherever it works best for you: inside an actor, in the game instance, etc.

For the sake of this tutorial, we’ll assume that only one session is available (the one created by the host), but in a real world scenario there might be multiple sessions to choose from. These can be filtered to select the best one based on player preferences, available player slots or any game-specific criteria.

void ASomeActor::BeginPlay()
{
    Super::BeginPlay();
 
    const IOnlineSessionPtr SessionInterface = Online::GetSessionInterface(GetWorld());
    if (!SessionInterface.IsValid())
    {
        return;
    }
 
    SessionSearch = MakeShared<FOnlineSessionSearch>();
    SessionSearch->MaxSearchResults = 20;
    SessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);
 
    DelegateHandle_OnFindSessionsComplete = SessionInterface->AddOnFindSessionsCompleteDelegate_Handle(
        FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionsComplete));
    if (!SessionInterface->FindSessions(0, SessionSearch.ToSharedRef()))
    {
        SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(DelegateHandle_OnFindSessionsComplete);
    }
}
 
void ASomeActor::OnFindSessionsComplete(bool bWasSuccessful)
{
    const IOnlineSessionPtr SessionInterface = Online::GetSessionInterface(GetWorld());
    if (!SessionInterface.IsValid())
    {
        return;
    }
 
    SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(DelegateHandle_OnFindSessionsComplete);
 
    if (bWasSuccessful
        && SessionSearch.IsValid()
        && SessionSearch->SearchResults.Num() > 0)
    {
        SessionInterface->JoinSession(0, NAME_GameSession, SessionSearch->SearchResults[0]);
    }
}

2. Connecting to the host

To connect to the host, create a client using the CreateLobbyClient function of the Lobbyist subsystem and call ConnectToSession:

// ...
 
void ASomeActor::OnFindSessionsComplete(bool bWasSuccessful)
{
    const IOnlineSessionPtr SessionInterface = Online::GetSessionInterface(GetWorld());
    if (!SessionInterface.IsValid())
    {
        return;
    }
 
    SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(DelegateHandle_OnFindSessionsComplete);
 
    if (bWasSuccessful
        && SessionSearch.IsValid()
        && SessionSearch->SearchResults.Num() > 0)
    {
        DelegateHandle_OnJoinSessionComplete = SessionInterface->AddOnJoinSessionCompleteDelegate_Handle(
            FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplete));
        if (!SessionInterface->JoinSession(0, NAME_GameSession, SessionSearch->SearchResults[0]))
        {
            SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(DelegateHandle_OnJoinSessionComplete);
        }
    }
}
 
void ASomeActor::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
    if (Result != EOnJoinSessionCompleteResult::Success)
    {
        return;
    }
 
    if (ULobbyistSubsystem* LobbyistSubsystem = ULobbyistSubsystem::Get(GetWorld()))
    {
        if (ALobbyistClient* Client = LobbyistSubsystem->CreateLobbyClient())
        {
            Client->ConnectToSession();
        }
    }
}
 

3. Next steps

The LobbyistClient class exposes several events that are triggered during the connection lifetime. You can use these events, for example, to update your UI accordingly.

  • ConnectedToLobby: called when a connection is first established.
  • OnJoinedLobby: called when the lobby has been joined successfully. All local players are logged-in and the LobbyistState is ready to use.
  • DisconnectedFromLobby: called when the client is gracefully disconnected from the server (i.e. because the server has gracefully stopped, the client has been kicked, etc.).
  • NetworkFailure: called when the client is disconnected from the server because of a network error.
// ...
 
void ASomeActor::OnFindSessionsComplete(bool bWasSuccessful)
{
    const IOnlineSessionPtr SessionInterface = Online::GetSessionInterface(GetWorld());
    if (!SessionInterface.IsValid())
    {
        return;
    }
 
    SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(DelegateHandle_OnFindSessionsComplete);
 
    if (bWasSuccessful
        && SessionSearch.IsValid()
        && SessionSearch->SearchResults.Num() > 0)
    {
            DelegateHandle_OnJoinSessionComplete = SessionInterface->AddOnJoinSessionCompleteDelegate_Handle(
                FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplete));
            if (!SessionInterface->JoinSession(0, NAME_GameSession, SessionSearch->SearchResults[0]))
            {
                SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(DelegateHandle_OnJoinSessionComplete);
            }
    }
}
 
void ASomeActor::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
    if (Result != EOnJoinSessionCompleteResult::Success)
    {
        return;
    }
 
    if (ULobbyistSubsystem* LobbyistSubsystem = ULobbyistSubsystem::Get(GetWorld()))
    {
        if (ALobbyistClient* Client = LobbyistSubsystem->CreateLobbyClient())
        {
            Client->OnJoinedLobby.AddDynamic(this, &ThisClass::OnLobbyJoined);
            Client->ConnectToSession();
        }
    }
}
 
void ASomeActor::OnLobbyJoined()
{
    // Lobby has been joined successfully, show UI...
}

Please refer to the example projects for how to implement a lobby UI.


Local testing

A useful feature of Lobbyist that allows for fast iteration is the ability to test your lobby locally, either directly from within the editor or by running multiple instances of the game.

Testing inside the editor

To test from within the editor simply launch two standalone clients:

A screenshot of the Play In Editor options dropdown, with the two standalone clients set up

You can create and join sessions in the editor just like you would when running a standalone or packaged version of the game.

Testing standalone

To test standalone, simply launch multiple instances of the game. The easiest way to do this is by right-clicking your .uproject file and clicking on Launch game.

A screenshot of Windows Explorer, showing the context menu after clicking on the project file

Managing lobby state and players

Lobbyist provides a flexible and intuitive way to replicate both general lobby data and player-specific data.

Lobby state

All the global lobby data that needs to be replicated to remote clients should live inside the LobbyistState actor. This is equivalent to UE GameState and it’s spawned automatically by the lobby host when the lobby is started. It’s replicated to clients as soon as they join.

The default implementation provided by Lobbyist simply replicates a list of players. In order to provide custom functionality, you need to create your own LobbyistState subclass.

A screenshot of the Pick Parent Class dialog in UE, with LobbyistState selected
#pragma once
#include "CoreMinimal.h"
#include "LobbyistState.h"
#include "MyLobbyState.generated.h"
 
UCLASS()
class AMyLobbyState : public ALobbyistState
{
    GENERATED_BODY()
}

Finally, configure your LobbyistHost to use the new class.

A screenshot of the lobby host actor properties panel, configured to use the new lobby state class
#include "MyLobbyHost.h"
#include "MyLobbyState.h"
 
AMyLobbyHost::AMyLobbyHost(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
    LobbyStateClass = AMyLobbyState::StaticClass()
}

Lobby players

Each player in the lobby is represented by a LobbyistPlayer actor. This is replicated to all remote clients and is the perfect spot to store player information. You can think of it as Lobbyist’s PlayerState counterpart.

The default implementation simply stores and replicates the player’s name, so you’ll likely want to create your own LobbyistPlayer subclass.

A screenshot of the Pick Parent Class dialog in UE, with LobbyistPlayer selected
#pragma once
#include "CoreMinimal.h"
#include "LobbyistPlayer.h"
#include "MyLobbyPlayer.generated.h"
 
UCLASS()
class AMyLobbyPlayer : public ALobbyistPlayer
{
    GENERATED_BODY()
}

Finally, configure your LobbyistHost to use the new class.

A screenshot of the lobby host actor properties panel, configured to use the new lobby player class
#include "MyLobbyHost.h"
#include "MyLobbyState.h"
#include "MyLobbyPlayer.h"
 
AMyLobbyHost::AMyLobbyHost(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
    LobbyStateClass = AMyLobbyState::StaticClass()
    LobbyPlayerClass = AMyLobbyPlayer::StaticClass()
}

Handling player login/logout

LobbyistHost provides some useful extension points that you can use to customize the login flow in both C++ and Blueprint.

  • PreLogin: called before processing the login request for a given player. Only available in C++.
  • ApproveLogin: approves or rejects the login request for a given player. Override this function if you want to prevent a login, i.e. because the lobby is full.
  • PostLogin: called after a player has been successfully logged in. This is the right spot to initialize the new player.
  • PreLogout: called when a player leaves the lobby. The player login process involves multiple RPCs between the client and the host. A high level overview of the login RPC/event flow is available below.
A high-level diagram of the login flow
High-level overview of the login flow.

Starting the game

When you’re ready to start the game, call the StartGame function on the host.

if (ULobbyistSubsystem* LobbyistSubsystem = ULobbyistSubsystem::Get(GetWorld()))
{
    if (ALobbyistHost* Host = LobbyistSubsystem->GetLobbyHost())
    {
        Host->StartGame();
    }
}

This will notify all the clients that it’s time to travel to the actual game and will fire the TravelToGame event on the host as soon as all clients confirm they’re ready to travel. You should use this event to travel to the level you want to play.

void AMyLobbyHost::TravelToGame()
{
    Super::TravelToGame();
 
    const bool bAbsolute = true;
    GetWorld()->ServerTravel(TEXT("/Game/Maps/MyGameLevel?listen"), bAbsolute);
}

Traveling to the game level will tear down the active world, destroying the lobby host and client actors. When the game is over and you want to get back to lobby, simply travel back to the main menu level, recreate the lobby host and reconnect the clients as appropriate.

Online sessions created/joined with the Online Subsystem persist across map changes, so re-creating or re-joining the active session isn’t necessary after traveling to a different level.