OnlineSubsystemNull

Unreals standard integrated basic online system intented for simple local prototype testing

Updated about 2 years ago Edit Page Revisions

Introduction

Intended for simple local prototype testing

I've been working on getting Possession up and running for multiplayer testing, so I thought I should report what I've learned along the way. Basic actor replication and game mechanics is one thing, actually getting a game to work online is a big challenge in itself, and perhaps this post will help others understand how Unreal Engine 4 works with online gameplay.

This article is written as a brain dump to memorialize my research on the online subsystem, so it may be flawed. Leave a comment and I will try to edit this article if I made any mistakes to keep it relevant.

The Unreal engine provides all the basics you need to host and join a game, but at some point you will have to get your hands dirty with the online subsystem. During development, you can try replication and see how your game runs in a multiplayer environment from the editor. Just add another player when you start previewing your game, and you can test your game with multiplayer features. However, once you exit the editor, the simple action of joining a server is no longer so trivial. Within the editor, the Unreal engine provides an online sub-system that is no longer present once you are in a standalone game instance. What you need to do is implement this on your own, or use one of the provided, for development purposes the OnlineSubSystemNull is suggested to work for LAN gameplay only.

First step from here would be to take a look at how ShooterGame works, specifically how it creates an online session, how to find and join a session, and how to destroy a session when you are done playing. The second step would be to take ShooterGame apart, apply some basic session operations to your game, and test them with the null system. When you're ready to ship your game or test playing it on the Internet, you can create your own subsystem that allows online multiplayer, which is a pretty big task. Or you have the option to publish your game on Steam. Then there is not much to do, from the engines point of view, you activate the OnlineSubSystemSteam , download the Steamworks SDK and reference it. But if your game is not for PC or you don't want to use Steam, there are other options! UE4 supports subsystems for: XboxLive, PSN, Google Play, Game Center and more!

Let's start by taking a look at how you can use the OnlineSubSystem in your game to enable online/LAN multiplayer. The first thing you need to do is enable the OnlineSubSystem module in your project. Open your .Build.cs and comment out the line for the OnlineSubsystemNull module. Mine looks like this: DynamicallyLoadedModuleNames.Add("OnlineSubsystemNull"); There are two ways your game can handle game sessions in the next step. One is more elaborate and complicated, the other is very simple and straightforward. One in C++ and one in Blueprint (from UE 4.9)! Let's take the difficult, complicated and fun one first!

Again, this is a good time to go through the ShooterGame example. The whole process is described with elegance in its source code.

Before we get deeper into an implementation, we need to cover some concepts of how sessions are handled by the subsystem. This will make it easier to understand how to build it in C++ and the complex code of ShooterGame will be much clearer and easier to follow.

Life Cycle of a Game Session

There are two major Interfaces involved IOnlineSubsystem and IOnlineSession each handles crucial parts of integrating your game with the subsystem. Everything is basically driven by sessions, you notify a service about your servers presence, the service is then queried by clients who wish to find active game sessions. A client then requests to join a given session, and then if all is well, the client is allowed to travel to that server.

Sessions

DefaultEngine.ini

Go to your DefaultEngine.ini and add the following lines, this defines the default OnlineSubsystem that Unreal Engine should use, since this tutorial will only cover the use of NULL. As far as I am aware this is not 100% required, as I have working Session code in Projects that don’t have this set, but you’ll ultimately need it if you want to move away from the NULL OnlineSubsystem.

[OnlineSubsystem]
DefaultPlatformService=Null

For those of you who want to research this a bit more, you can find the variable in “OnlineSubsystemModule.h:26”.

Build.cs

Go to your Build.cs to add two Modules as dependencies since we later want to access their exported functions and variables.

  • OnlineSubsystem

    will offer us all the Session related code. An OnlineSubsystem has a lot of other functionalities, such as User Data, Stats, Achievements, and much more. We are only really interested into the “SessionInterface” of it.

  • OnlineSubsystemUtils

    contains some helper functions, such as compacter ways of accessing specific Interfaces. There is an important note to make here: The function that we will use from the “OnlineSubsystemUtils”, especially the ones from its header, also exists similarly in the “Online.h” file. The problem with those however is, that they aren’t taking the current World into account. That can lead to issues when trying Session Code in PIE (Play In Editor) because the Editor can have multiple different worlds. You may not find any Sessions for example.

public class CppSessions : ModuleRules
{
    public CppSessions(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[]
        {
            "Core",
            "CoreUObject",
            "Engine",
            "InputCore",
            "OnlineSubsystem",
            "OnlineSubsystemUtils",
        });
    }
}

And that’s almost it. Now you are ready to add new classes and files to your project, which we will utilize to place our session related code into. My original writeup is quite old and I was using the GameInstance class to hold the Session code. The reason behind that was and more or less still is, that you can easily access your Session related code from everywhere at any time. That includes executing Session related functions as well as accessing and modifying cached Session Settings.

We will however not place the code directly into the GameInstance anymore. Since 4.24 (I think), UE4 offers new Subsystems which are Framework related. They are not OnlineSubsystems, so try to not mix those up. These Subsystems share the lifetime of their “owner”. You can read up on them in the UE4 Documentation. One of those exists for the GameInstance, which allows us to neatly pack away our code but still utilize the persistent nature of the GameInstance class. Subsystems have static getter functions for Blueprints, so you should be able to easily access them from within your Blueprint Code.

GameInstance

Go into your Unreal Engine project, add new C++ Class, choose Parent Class: GameInstanceSubsystem

image-1.png

Use whatever method you want to create a new Class based on the “UGameInstanceSubsystem” Class. The final name of mine will be “CSSessionSubsystem”. I placed the file into a “Subsystems” subfolder to stay organized. I will add the constructor declaration and definition to it, so we can later use it to bind our callback functions.

#pragma once

#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "CSSessionSubsystem.generated.h"

UCLASS()
class UCSSessionSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:
	UCSSessionSubsystem();
};
#include "Subsystems/CSSessionSubsystem.h"

UCSSessionSubsystem::UCSSessionSubsystem()
{
}

Session Code

I won’t go into detail on every posted code snippet, but rather point out specific parts of the code if needed, there is a high chance that you want to modify your code a bit later on. It might for example be a good idea to check if a Session is already existing before Creating or Joining a new one, you could then save what you were supposed to do and Destroy the Session first, before returning back to Creating or Joining.

Whole setup of each of these Methods will follow the same concept:

  1. Function to execute your Session code
  2. Function to receive a callback from the OnlineSubsystem
  3. Delegate to bind your function to
  4. DelegateHandle to keep track of the Delegate and later unbind it again
Create Session
#pragma once

#include "CoreMinimal.h"

#include "Interfaces/OnlineSessionInterface.h"
#include "Subsystems/GameInstanceSubsystem.h"

#include "CSSessionSubsystem.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCSOnCreateSessionComplete, bool, Successful);

UCLASS()
class UCSSessionSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:
	UCSSessionSubsystem();

	void CreateSession(int32 NumPublicConnections, bool IsLANMatch);
	
	FCSOnCreateSessionComplete OnCreateSessionCompleteEvent;

protected:
	void OnCreateSessionCompleted(FName SessionName, bool Successful);

private:
	FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate;
	FDelegateHandle CreateSessionCompleteDelegateHandle;
	TSharedPtr<FOnlineSessionSettings> LastSessionSettings;
};

You should notice the following include at the top of the header file:

#include "Interfaces/OnlineSessionInterface.h"

This is required to be able to use both the “FOnlineSessionSettings” struct and the “FOnCreateSessionCompleteDelegate” delegate. Same goes for future Delegates of other Methods.

#include "Subsystems/CSSessionSubsystem.h"

#include "OnlineSubsystemUtils.h"

UCSSessionSubsystem::UCSSessionSubsystem()
	: CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionCompleted))
{
}

void UCSSessionSubsystem::CreateSession(int32 NumPublicConnections, bool IsLANMatch)
{
	const IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld());
	if (!sessionInterface.IsValid())
	{
		OnCreateSessionCompleteEvent.Broadcast(false);
		return;
	}

	LastSessionSettings = MakeShareable(new FOnlineSessionSettings());
	LastSessionSettings->NumPrivateConnections = 0;
	LastSessionSettings->NumPublicConnections = NumPublicConnections;
	LastSessionSettings->bAllowInvites = true;
	LastSessionSettings->bAllowJoinInProgress = true;
	LastSessionSettings->bAllowJoinViaPresence = true;
	LastSessionSettings->bAllowJoinViaPresenceFriendsOnly = true;
	LastSessionSettings->bIsDedicated = false;
	LastSessionSettings->bUsesPresence = true;
	LastSessionSettings->bIsLANMatch = IsLANMatch;
	LastSessionSettings->bShouldAdvertise = true;

	LastSessionSettings->Set(SETTING_MAPNAME, FString("Your Level Name"), EOnlineDataAdvertisementType::ViaOnlineService);

	CreateSessionCompleteDelegateHandle = sessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate);

	const ULocalPlayer* localPlayer = GetWorld()->GetFirstLocalPlayerFromController();
	if (!sessionInterface->CreateSession(*localPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *LastSessionSettings))
	{
		sessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegateHandle);

		OnCreateSessionCompleteEvent.Broadcast(false);
	}
}

void UCSSessionSubsystem::OnCreateSessionCompleted(FName SessionName, bool Successful)
{
	const IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld());
	if (sessionInterface)
	{
		sessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegateHandle);
	}

	OnCreateSessionCompleteEvent.Broadcast(Successful);
}

Let’s talk about a few things here which will be repeated throughout the next Methods.

CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionCompleted))

This line will bind the Subsystems function for the CreateSessionComplete Callback to our Delegate. We will have to do that for each of the Callbacks.

const IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld());

When using “GetSessionInterface(..)”, always make sure to pass in the current UWorld. There are two versions of these helper functions. One is declared and defined in the Online.h file, not needing a UWorld, and one is declared and defined in the OnlineSubsysbtemUtils.h file, requiring the current UWorld. The difference is that the UWorld version takes into account that the Editor has multiple UWorlds at the same time.

Not providing the UWorld will not handle PIE (Play In Editor) properly, resulting in issues with not finding sessions and similar.

CreateSessionCompleteDelegateHandle = sessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate);

Here we are simply adding our Delegate to the SessionInterfaces Delegate List, as well as saving the Handle that it returns, so we can later remove the Delegate from the List again.

NAME_GameSession

You’ll see this a lot. Some Subsystems might support other Sessions, like PartySessions, but we won’t look at those in this post. It basically describes the type of Session as a Game(play) one.

IOnlineSubsyconst IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld()); if (sessionInterface) { sessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegateHandle); }

In the Callback Function, we will always clear the Callback (as well as when the actual function call fails).

OnCreateSessionCompleteEvent.Broadcast(Successful);

Our own custom Delegate is used to broadcast the event back to whoever called this. E.g. a button press in your UMG Widget would bind to it, call CreateSession and then get the callback to react to it. You can also tie this into a Latent node, similar to how the Native Nodes work if you want to keep it compact. I won’t show any Latent node code here though as the Engine has enough examples. None of the code is exposed to Blueprints of course, so you’ll have to take care of that yourself.

LastSessionSettings = MakeShareable(new FOnlineSessionSettings());

Now, you will notice a lot of LastSessionSettings stuff in the CreateSession function. What I used here isn’t 100% needed, or the one thing you always have to use. Please read through the available Settings and decide on your own if you want them true/false or any other value.

The custom settings that are defined as followed can of course be passed in via the CreateSession function, but I didn’t want to make the function signature that huge. A good idea is to wrap this stuff into a Struct, so you can easier pass it along.

LastSessionSettings->Set(SETTING_MAPNAME, FString("Your Level Name"), EOnlineDataAdvertisementType::ViaOnlineService);

The “SETTING_MAPNAME” is defined in “OnlineSessionSettings.h:15”. There are a few more, but you aren’t limited to those. You can simply add some defines into the header file of your Subsystem or into some custom header just for types.

Update Session

Small note here: While I originally said, that we will look at each Method on its own, the UpdateSession Method works a lot better if you at least keep the original settings saved somewhere. So you’ll see the “LastSessionSettings” pop up again here, but I will only modify them, not create them. This expects you to have the CreateSession part already added.

I also won’t pass any actual changes as Inputs. You can do that of course. For simplicity I will only adjust the MapName in this UpdateSession call.

#pragma once

#include "CoreMinimal.h"

#include "Interfaces/OnlineSessionInterface.h"
#include "Subsystems/GameInstanceSubsystem.h"

#include "CSSessionSubsystem.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCSOnUpdateSessionComplete, bool, Successful);

UCLASS()
class UCSSessionSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:
	UCSSessionSubsystem();

	void UpdateSession();
	
	FCSOnUpdateSessionComplete OnUpdateSessionCompleteEvent;

protected:
	void OnUpdateSessionCompleted(FName SessionName, bool Successful);

private:
	TSharedPtr<FOnlineSessionSettings> LastSessionSettings;

	FOnUpdateSessionCompleteDelegate UpdateSessionCompleteDelegate;
  	FDelegateHandle UpdateSessionCompleteDelegateHandle;
};
#include "Subsystems/CSSessionSubsystem.h"

#include "OnlineSubsystemUtils.h"

UCSSessionSubsystem::UCSSessionSubsystem()
	: UpdateSessionCompleteDelegate(FOnUpdateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnUpdateSessionCompleted))
{
}

void UCSSessionSubsystem::UpdateSession()
{
	const IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld());
	if (!sessionInterface.IsValid())
	{
		OnUpdateSessionCompleteEvent.Broadcast(false);
		return;
	}

	TSharedPtr<FOnlineSessionSettings> updatedSessionSettings = MakeShareable(new FOnlineSessionSettings(*LastSessionSettings));
	updatedSessionSettings->Set(SETTING_MAPNAME, FString("Updated Level Name"), EOnlineDataAdvertisementType::ViaOnlineService);

	UpdateSessionCompleteDelegateHandle =
		sessionInterface->AddOnUpdateSessionCompleteDelegate_Handle(UpdateSessionCompleteDelegate);

	if (!sessionInterface->UpdateSession(NAME_GameSession, *updatedSessionSettings))
	{
		sessionInterface->ClearOnUpdateSessionCompleteDelegate_Handle(UpdateSessionCompleteDelegateHandle);

		OnUpdateSessionCompleteEvent.Broadcast(false);
	}
	else
	{
		LastSessionSettings = updatedSessionSettings;
	}
}

void UCSSessionSubsystem::OnUpdateSessionCompleted(FName SessionName, bool Successful)
{
	const IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld());
	if (sessionInterface)
	{
		sessionInterface->ClearOnUpdateSessionCompleteDelegate_Handle(UpdateSessionCompleteDelegateHandle);
	}

	OnUpdateSessionCompleteEvent.Broadcast(Successful);
}
Start Session

Usually you want to start the Session right after creating it. For a more Matchmaking like approach you could first create the Session, then let Players find it and register them, and then start the Session and the Match. #pragma once

#include "CoreMinimal.h"

#include "Interfaces/OnlineSessionInterface.h"
#include "Subsystems/GameInstanceSubsystem.h"

#include "CSSessionSubsystem.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCSOnStartSessionComplete, bool, Successful);

UCLASS()
class UCSSessionSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:
	UCSSessionSubsystem();

	void StartSession();
	
	FCSOnStartSessionComplete OnStartSessionCompleteEvent;

protected:
	void OnStartSessionCompleted(FName SessionName, bool Successful);

private:
	FOnStartSessionCompleteDelegate StartSessionCompleteDelegate;
	FDelegateHandle StartSessionCompleteDelegateHandle;
};
#include "Subsystems/CSSessionSubsystem.h"

#include "OnlineSubsystemUtils.h"

UCSSessionSubsystem::UCSSessionSubsystem()
	: StartSessionCompleteDelegate(FOnStartSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnStartSessionCompleted))
{
}

void UCSSessionSubsystem::StartSession()
{
	const IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld());
	if (!sessionInterface.IsValid())
	{
		OnStartSessionCompleteEvent.Broadcast(false);
		return;
	}

	StartSessionCompleteDelegateHandle =
		sessionInterface->AddOnStartSessionCompleteDelegate_Handle(StartSessionCompleteDelegate);

	if (!sessionInterface->StartSession(NAME_GameSession))
	{
		sessionInterface->ClearOnStartSessionCompleteDelegate_Handle(StartSessionCompleteDelegateHandle);

		OnStartSessionCompleteEvent.Broadcast(false);
	}
}

void UCSSessionSubsystem::OnStartSessionCompleted(FName SessionName, bool Successful)
{
	const IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld());
	if (sessionInterface)
	{
		sessionInterface->ClearOnStartSessionCompleteDelegate_Handle(StartSessionCompleteDelegateHandle);
	}

	OnStartSessionCompleteEvent.Broadcast(Successful);
}
End Session

Same as starting, you can End a Session by hand. I don’t think a lot of users ever do this, they mostly outright Destroy the Session, but since the Method exists I wanted to show this one too.

#pragma once

#include "CoreMinimal.h"

#include "Interfaces/OnlineSessionInterface.h"
#include "Subsystems/GameInstanceSubsystem.h"

#include "CSSessionSubsystem.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCSOnEndSessionComplete, bool, Successful);

UCLASS()
class UCSSessionSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:
	UCSSessionSubsystem();
	
	void EndSession();
	
	FCSOnEndSessionComplete OnEndSessionCompleteEvent;

protected:
	void OnEndSessionCompleted(FName SessionName, bool Successful);

private:
	FOnEndSessionCompleteDelegate EndSessionCompleteDelegate;
	FDelegateHandle EndSessionCompleteDelegateHandle;
};
#include "Subsystems/CSSessionSubsystem.h"

#include "OnlineSubsystemUtils.h"

UCSSessionSubsystem::UCSSessionSubsystem()
	: EndSessionCompleteDelegate(FOnEndSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnEndSessionCompleted))
{
}

void UCSSessionSubsystem::EndSession()
{
	const IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld());
	if (!sessionInterface.IsValid())
	{
		OnEndSessionCompleteEvent.Broadcast(false);
		return;
	}

	EndSessionCompleteDelegateHandle =
		sessionInterface->AddOnEndSessionCompleteDelegate_Handle(EndSessionCompleteDelegate);

	if (!sessionInterface->EndSession(NAME_GameSession))
	{
		sessionInterface->ClearOnEndSessionCompleteDelegate_Handle(EndSessionCompleteDelegateHandle);

		OnEndSessionCompleteEvent.Broadcast(false);
	}
}

void UCSSessionSubsystem::OnEndSessionCompleted(FName SessionName, bool Successful)
{
	const IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld());;
	if (sessionInterface)
	{
		sessionInterface->ClearOnEndSessionCompleteDelegate_Handle(EndSessionCompleteDelegateHandle);
	}

	OnEndSessionCompleteEvent.Broadcast(Successful);
}
Destroy Session

Destroying a Session has to happen on both Server and Clients when they leave. You might find yourself in a situation where you left a Server, but you forgot to clean up the Session and trying to Create or Join a new one won’t work anymore until restarting the Game/Editor.

At that point you should think about Destroying the Session (if it exists already) before Creating or Joining. That way you can ensure that your Players aren’t getting stuck.

Or you come up with a secure way of Destroying the Session whenever the Player is not supposed to be in one. For example when entering the Main Menu.

#pragma once

#include "CoreMinimal.h"

#include "Interfaces/OnlineSessionInterface.h"
#include "Subsystems/GameInstanceSubsystem.h"

#include "CSSessionSubsystem.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCSOnDestroySessionComplete, bool, Successful);

UCLASS()
class UCSSessionSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:
	UCSSessionSubsystem();

	void DestroySession();
	
	FCSOnDestroySessionComplete OnDestroySessionCompleteEvent;

protected:
	void OnDestroySessionCompleted(FName SessionName, bool Successful);

private:
	FOnDestroySessionCompleteDelegate DestroySessionCompleteDelegate;
	FDelegateHandle DestroySessionCompleteDelegateHandle;
};
#include "Subsystems/CSSessionSubsystem.h"

#include "OnlineSubsystemUtils.h"

UCSSessionSubsystem::UCSSessionSubsystem()
	: DestroySessionCompleteDelegate(FOnDestroySessionCompleteDelegate::CreateUObject(this, &ThisClass::OnDestroySessionCompleted))
{
}

void UCSSessionSubsystem::DestroySession()
{
	const IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld());
	if (!sessionInterface.IsValid())
	{
		OnDestroySessionCompleteEvent.Broadcast(false);
		return;
	}

	DestroySessionCompleteDelegateHandle =
		sessionInterface->AddOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteDelegate);

	if (!sessionInterface->DestroySession(NAME_GameSession))
	{
		sessionInterface->ClearOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteDelegateHandle);

		OnDestroySessionCompleteEvent.Broadcast(false);
	}
}

void UCSSessionSubsystem::OnDestroySessionCompleted(FName SessionName, bool Successful)
{
	const IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld());
	if (sessionInterface)
	{
		sessionInterface->ClearOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteDelegateHandle);
	}

	OnDestroySessionCompleteEvent.Broadcast(Successful);
}
Find Sessions

Finding Sessions is a bit annoying, because to properly show the results via your UI, you either have to stay in C++ or setup some Blueprint Types and static Functions to extract all the information from the Session Result. Specially your custom Settings, like the MapName we added in the Create Session part.

Of course, I won’t show how to do that here, because we are in C++ land here.

Usually you would also want to pass in some Search Settings into your FindSession node, but again, I want to keep it simple, so you aren’t bombarded by code you might not need.

#pragma once

#include "CoreMinimal.h"

#include "Interfaces/OnlineSessionInterface.h"
#include "Subsystems/GameInstanceSubsystem.h"

#include "CSSessionSubsystem.generated.h"

DECLARE_MULTICAST_DELEGATE_TwoParams(FCSOnFindSessionsComplete, const TArray<FOnlineSessionSearchResult>& SessionResults, bool Successful);

UCLASS()
class UCSSessionSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:
	UCSSessionSubsystem();

	void FindSessions(int32 MaxSearchResults, bool IsLANQuery);
	
	FCSOnFindSessionsComplete OnFindSessionsCompleteEvent;

protected:
	void OnFindSessionsCompleted(bool Successful);

private:
	FOnFindSessionsCompleteDelegate FindSessionsCompleteDelegate;
	FDelegateHandle FindSessionsCompleteDelegateHandle;
	TSharedPtr<FOnlineSessionSearch> LastSessionSearch;
};
#include "Subsystems/CSSessionSubsystem.h"

#include "OnlineSubsystemUtils.h"

UCSSessionSubsystem::UCSSessionSubsystem()
	: FindSessionsCompleteDelegate(FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionsCompleted))
{
}

void UCSSessionSubsystem::FindSessions(int32 MaxSearchResults, bool IsLANQuery)
{
	const IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld());
	if (!sessionInterface.IsValid())
	{
		OnFindSessionsCompleteEvent.Broadcast(TArray<FOnlineSessionSearchResult>(), false);
		return;
	}

	FindSessionsCompleteDelegateHandle =
		sessionInterface->AddOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegate);

	LastSessionSearch = MakeShareable(new FOnlineSessionSearch());
	LastSessionSearch->MaxSearchResults = MaxSearchResults;
	LastSessionSearch->bIsLanQuery = IsLANQuery;

	LastSessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);

	const ULocalPlayer* localPlayer = GetWorld()->GetFirstLocalPlayerFromController();
	if (!sessionInterface->FindSessions(*localPlayer->GetPreferredUniqueNetId(), LastSessionSearch.ToSharedRef()))
	{
		sessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegateHandle);

		OnFindSessionsCompleteEvent.Broadcast(TArray<FOnlineSessionSearchResult>(), false);
	}
}

void UCSSessionSubsystem::OnFindSessionsCompleted(bool Successful)
{
	const IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld());
	if (sessionInterface)
	{
		sessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegateHandle);
	}

	if (LastSessionSearch->SearchResults.Num() <= 0)
	{
		OnFindSessionsCompleteEvent.Broadcast(TArray<FOnlineSessionSearchResult>(), Successful);
		return;
	}

	OnFindSessionsCompleteEvent.Broadcast(LastSessionSearch->SearchResults, Successful);
}

The only important line here is:

LastSessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);

This will make sure that we are searching for Presence Sessions, so Player hosted Sessions instead of DedicatedServer Sessions.

Please also be aware that due to “FOnlineSessionSearchResult” not being a USTRUCT, we can use a DYNAMIC Multicast Delegate, as those need to have types that can be exposed to Blueprints. So if you want to expose this Delegate to Blueprints, you need to wrap the SearchResult struct into your own USTRUCT and utilize some Function Library to communicate with the inner SearchResult struct.

Join Session
#pragma once

#include "CoreMinimal.h"

#include "Interfaces/OnlineSessionInterface.h"
#include "Subsystems/GameInstanceSubsystem.h"

#include "CSSessionSubsystem.generated.h"

DECLARE_MULTICAST_DELEGATE_OneParam(FCSOnJoinSessionComplete, EOnJoinSessionCompleteResult::Type Result);

UCLASS()
class UCSSessionSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:
	UCSSessionSubsystem();

	void JoinGameSession(const FOnlineSessionSearchResult& SessionResult);
	
	FCSOnJoinSessionComplete OnJoinGameSessionCompleteEvent;

protected:
	void OnJoinSessionCompleted(FName SessionName, EOnJoinSessionCompleteResult::Type Result);
	bool TryTravelToCurrentSession();

private:
	FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate;
	FDelegateHandle JoinSessionCompleteDelegateHandle;
};
#include "Subsystems/CSSessionSubsystem.h"

#include "OnlineSubsystemUtils.h"

UCSSessionSubsystem::UCSSessionSubsystem()
	: JoinSessionCompleteDelegate(FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionCompleted))
{
}

void UCSSessionSubsystem::JoinGameSession(const FOnlineSessionSearchResult& SessionResult)
{
		
	const IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld());
	if (!sessionInterface.IsValid())
	{
		OnJoinGameSessionCompleteEvent.Broadcast(EOnJoinSessionCompleteResult::UnknownError);
		return;
	}

	JoinSessionCompleteDelegateHandle =
		sessionInterface->AddOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegate);

	const ULocalPlayer* localPlayer = GetWorld()->GetFirstLocalPlayerFromController();
	if (!sessionInterface->JoinSession(*localPlayer->GetPreferredUniqueNetId(), NAME_GameSession, SessionResult))
	{
		sessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegateHandle);

		OnJoinGameSessionCompleteEvent.Broadcast(EOnJoinSessionCompleteResult::UnknownError);
	}
}

void UCSSessionSubsystem::OnJoinSessionCompleted(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
	const IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld());
	if (sessionInterface)
	{
		sessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegateHandle);
	}

	OnJoinGameSessionCompleteEvent.Broadcast(Result);
}

bool UCSSessionSubsystem::TryTravelToCurrentSession()
{
	const IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld());
	if (!sessionInterface.IsValid())
	{
		return false;
	}

	FString connectString;
	if (!sessionInterface->GetResolvedConnectString(NAME_GameSession, connectString))
	{
		return false;
	}

	APlayerController* playerController = GetWorld()->GetFirstPlayerController();
	playerController->ClientTravel(connectString, TRAVEL_Absolute);
	return true;
}

Again, be aware that “EOnJoinSessionCompleteResult” can not be exposed to Blueprints, so you can’t make your own Callback Delegate BlueprintAssignable (or Dynamic to begin with) unless you make your own UENUM and convert back and forth between UE4s type and yours.

The same goes for the JoinSession function itself, as the “FOnlineSessionSearchResult” struct can’t be exposed. If you want to make the function BlueprintCallable, you’ll have to wrap the struct into your own USTRUCT as stated before.

In addition to the default Session code, you’ll also find a new function called “TryTravelToCurrentSession” in the above gist, which is for actually joining the Server behind that Session.

GitHub

Repository this tutorial uses, by Cedric Neukirchen.

And as with all other other operations a delegate will be fired, where you can clean up after the session has been destroyed.

Class and interface overview.

  • GameInstance

  • GameSession

  • IOnlineSession

  • IOnlineSessionSettings

  • IOnlineSubSystem

  • EOnlineAsyncTaskState

    The OnlineSubSystem performs some asynchronous task for example searching and each task can be polled for a state. These are the states in which a async task can be in.

  • Done

  • Failed

  • InProgress

  • NotStarted

  • FDelegateHandle

  • FOnlineSessionSearch

    This class contains your search results, your search query and properties to set Timeout of your search, and the current search state if you need to poll for it. To query the subsystem for specific fields use the QuerySettings.Set(Field, Value, EOnlineComparisonOp);

  • FOnlineSessionSearchResult

    Searching for servers is a asynchronous task which starts to run when you call FindSessions. When this task has finished you and if it was successfull in finding any sessions they will be stored as a TArray of FOnlineSessionSearchResult on your FOnlineSessionSearch reference. Here is an example of a method that polls for the async task state for the search. And while we are at it we take a snapshot of the current search progress.

EOnlineAsyncTaskState::Type APossessionGameSession::GetSearchResultStatus(
    int32& SearchResultIdx, 
    int32& NumSearchResults)
{
    SearchResultIdx = 0;
    NumSearchResults = 0;
    if (Search.IsValid())
    {
        if (Search->SearchState == EOnlineAsyncTaskState::Done)
        {
           NumSearchResults = Search->SearchResults.Num();
        }
        return Search->SearchState;
    }
    return EOnlineAsyncTaskState::NotStarted;
}

An Implementation

With this knowledge it will be easier to follow and understand the code for the online part of ShooterGame. Its quite complex but now you have some basic knowledge of the online subsystem to fall back on when doing further research.

When studying the ShooterGame example code you find that working with the subsystem is not a straight procedural line of operations, most things are handled by delegates that has fired when certain events has happened in the subsystem.

Things has to work this way by the nature of networks, they are unreliable and, this means that our program cannot halt the frame and wait for the subsystem to respond over the network. The game have to ask to find sessions, and when the subsystem has some sessions to give to us, react then, do some other operations in the meantime.

In short, when implementing multiplayer in your game you need to think event-driven-programming where you tell the system to do one thing, and eventually the system will tell you when it has finished performing its task so you can react to the outcome.

I want the easy way out

As promised, we will take a look at a easier way to integrate the OnlineSubsystemNULL into your game. In Unreal Engine you have access to a set of new Blueprint nodes by enabling the OnlineSubsystemUtils plugin, CreateSession, FindSession, JoinSession & DestroySession are some of them among others. If you wish take a look at this example on how to integration sessions into your game with blueprints, it does only provide the basics of the subsystem functionality but you will get up and running in a second with this approach.