Unreal Engine Multiplayer Sessions in C++
This post shall give you a short introduction to handling your Multiplayer Sessions via your own C++ code. Most of you probably either started with the very limited native Blueprint Nodes or fell back to using Plugins like the Advanced Sessions one.
The tutorial will utilize the OnlineSubsystem NULL. OnlineSubsystems, like Steam, should theoretically work with this too as it's all interface-based, however, I haven't tested it with Steam yet.
Files / Repository
The full code for handling Sessions in C++ can be found in the following GitHub Repository.
If you encounter issues with the posted code, please post those into the Issues tab of the GitHub Repo.
If you wish to suggest changes, improvements, updates, and whatnot, please also utilize the GitHub Repo.
The Tutorial was created with Unreal Engine 4.26.2 inside an empty C++ project.
Preparing the Project
Create and Clean up new Project
One of the things I do when creating fresh C++ projects is clean up the Source code folder. Epic Games throws a GameMode at us, which is 'really' nice for an 'empty' project, so I delete that one straight away.
I'm also one of those peeps that utilize Public and Private folders in projects that are used by other people. Inside of those, I move the Game's Module .cpp and .h and then regenerate project files and recompile.
The final folder structure, before we start adding our code, will look like this. My project is called CppSessions, so I will prefix my classes with CS.
Change your "DefaultEngine.ini" file
Locate the “DefaultEngine.ini” file in your project's config folder. Then add the following lines to it:
This defines the Default OnlineSubsystem that UE4 should use. Since this tutorial only covers the usage of NULL, that's what we are going to utilize. For those of you who want to research this a bit more, you can find the variable in “OnlineSubsystemModule.h:26”.
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.
Change your "\<Your Project Name>.Build.cs"
For my project, as you can see in the image further up, this would be “CppSessions.Build.cs”. Here we need 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 in 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.
Your final Build.cs file should look something like this:
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.
Add a GameInstance Subsystem class
As previously stated, my classes will be prefixed with CS, due to the project being called CppSessions.
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.
The 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 to Creating or Joining.
Here are all the Session Methods we will implement:
- Create Session
- Update Session
- Start Session
- End Session
- Destroy Session
- Find Sessions
- Join Session
There are more. For example “Find Sessions” also exists for things like “Find a Session of a Friend”, but we won't tackle those today. They follow a similar setup and I'm sure you'll be able to set that up yourself whenever you need to.
The whole setup of each of these methods will follow the same concept:
- Function to execute your Session code
- Function to receive a callback from the OnlineSubsystem
- Delegate to bind your function to
- DelegateHandle to keep track of the Delegate and later unbind it again
Each Gist will only contain the Subsystem Class with the specific Method we are looking at. That means you'll see the CSSessionSubsystem.h/.cpp 7 times, with only the functions and variables relevant to the current Method. If you wish to see all 7 Session Methods implemented at once, head over to the public GitHub Repro that is linked at the start of this post.
You should notice the following include at the top of the header file:
This is required to be able to use both the “FOnlineSessionSettings” struct and the “FOnCreateSessionCompleteDelegate” delegate. Same goes for future Delegates of other Methods.
Let's talk about a few things here which will be repeated throughout the next Methods.
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.
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 describes the type of Session as a Game(play) one.
IOnlineSubsyconst IOnlineSessionPtr sessionInterface = Online::GetSessionInterface(GetWorld());
In the Callback Function, we will always clear the Callback (as well as when the actual function call fails).
Our custom Delegate is used to broadcast the event back to whoever called this. For example, 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 definitions into the header file of your Subsystem or some custom header just for types.
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.
Usually, you want to start the Session right after creating it. For a more Matchmaking approach you could first create the Session, then let Players find it and register them, and then start the Session and the Match.
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.
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.
Finding Sessions is a bit annoying, because to properly show the results via your UI, you either have to stay in C++ or set up some Blueprint Types and static Functions to extract all the information from the Session Result. Especially 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.
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.
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.