Basic Online System

An guide to OnlineSubsystem: NULL

Updated about 3 years ago

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.

Creating a Session

When hosting a game you first need to create a session for the online sub system.

IOnlineSubSystem* OnlineSub = IOnlineSubsystem::Get();
if (OnlineSub) 
{
   IOnlineSessionPtr Session 
       = OnlineSub->GetSessionInterface(); 
   if (Session.IsValid()) {
      Session->CreateSession(*UserId, SessionName, *HostSettings);
   }
}

This bit of code is the basic foundation for creating a new session in the subsystem. When a session has been created a delegate is fired which you can opt in to handle. This is done by assigning a delegate like so.

OnCreateSessionDelegateHandle 
    = Session->AddOnCreateSessionCompleteDelegate_Handle(FOnCreateSessionCompleteDelegate)

_Handle and no _Handle It’s worth mentioning why there are two different methods here, one that returns a FDelegateHandle and one that does not. The one without the _handle suffix is deprecated and should not be used. When you assign a Delegate you should always keep a reference to the handle so you can operate on your delegates in a proper way through out the session life cycle.

ClearOnCreateSessionCompleteDelegate_Handle(OnCreateSessionDelegateHandle)

§ Pay attention, this is subject to change as the _Handle suffix is added for backwards compatibility while the option to do it without is deprecating.

Starting a Session

You are able to and in general you should signal the subsystem that a match has started, in case you don’t want to list games in progress or maybe only list games in progress.

This can be done in the following way.

Session->StartSession(SessionName);

This will tell the subsystem that this session and gameplay has started. Take a look at how ShooterGame does this.

And as with all other calls to the subsystem you are able to assign delegates and retrieve the handle for them by methods named by convention e.g:

OnStartSessionCompleteDelegateHandle
   = Session->AddOnstartSessionCompleteDelegate_Handle(OnStartSessionDelegate);

This is normally done in a overridden void HandleMatchHasStarted() method in your GameSession Class as this is called when the match has started by the currently active game mode.

When the authority has started the session, it should RPC call to a method on all the connected clients so that they also start a online session. In this example the server is doing a RPC to a method called ClientStartOnlineGame

// tell non-local players to start online game
for (FConstPlayerControllerIterator It 
   = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
   AClientPlayerController* PC = Cast<AClientPlayerController>(*It);
   if (PC && !PC->IsLocalPlayerController())
   {
      PC->ClientStartOnlineGame();
   }
}

Updating a Session, Find and Join a Session

The online subsystem provides a way to find ongoing game sessions. This is a pretty powerful feature that allows to you query the subsystem to retrieve a set of hosts that is currently running a session of your game. The Online sub system interfaces declares methods to do this, however its up to the subsystem itself to allow querying.

Once you have a set of sessions you can either create a UI widget that allows players to pick a session and join it. Or you can choose for them by creating a matchmaking system where you put players against each other based on a set of parameters for instance, skill-level

When you wish to query the subsystem for sessions to join to need first to create an instance of FOnlineSessionSearch

Search = MakeShareable(new FOnlineSessionSearch());
 
OnFindSessionsCompleteDelegateHandle 
    = Session->AddOnFindSessionCompleteDelegate_Handle(OnFindSessionsCompleteDelegate);
TSharedRef<FOnlineSessionSearch> SearchRef = Search.ToSharedRef();
Session->FindSessions(*UserId, SearchRef);

In case we wish to query our subsystem for servers with different options or keywords we can do this by adding queries to our SessionSearch Instance.

Search->QuerySettings.Set(SEARCH_KEYWORD, "SOME_KEYWORD", EOnlineComparisonOp::Equals);

The last bit of the query is a Enum with a collection of comparison operators for instance you have Greater Than, Lesser Than, Near, Not Equals and a few others.

I will try to cover querying and matchmaking in deeper detail later on, untill then please look at ShooterGame and how it is using querying.

Once you have your results you are able to pick one and join the session.

if (SessionToJoin >= 0 && SessionToJoin < Search.SearchResults.Num())
{
   return Session->JoinSession(*UserId, SessionName, Search.SearchResults[SessionToJoin]);
}
return false;

When the OnJoinSessionCompleteDelegate is fired it passes a EOnJoinSessionCompleteResult::Type Enum as parameter to the delegate, this will either tell you that you have successfully joined the session, or that something went wrong and you should handle it accordingly, the delegate also passes a FName with the SessionName in it. If everything went well, we can then proceed to travel to the server. When we want to travel we need to resolve the URL to the server, this is done by passing a FString to contain the resolved URL along with the SessionName as a FName to GetResolvedConnectString

FString URL;
IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();
 
if ( Session->GetResolvedConnectString(SessionName, URL))
{
   PlayerController->ClientTravel(URL, TRAVEL_Absolute);
}

This might seem as a bit of magic, but the trick to this resolve is that your Session keeps a state once you have Joined a session, so when you resolve anything with the same session name that you joined with you will resolve the URL to that session.

This means that unless you explicitly decides to leave a session, you are not able to Join and travel to another. This also allows you to easily reconnect to a session if you lose the connection temporarily or your game crashed.

Be mind full of how you keep your references to the session.

End a Session

Endning a session is the the operation of notifing the subsystem that the match has ended. Its the opposite of Starting a session in this regard. When the Match has ended, GameMode calls GameSession->HandleMatchHasEnded(); This means that in your gamesession subclass you should override void HandleMatchHasEnded().

IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();
if (Session.IsValid()) 
{
   Session->EndSession(GameSessionName);
}

This only shows the subsystem call to EndSession, in reality this is very incomplete. You need to tell the connected clients that your gamesession has ended so that they can gracefully handle that your match is ending so they can end their sessions aswell. Again ShooterGame does this with elegance. When the match is ending the server calls the ClientEndOnlineGame() on connected controllers, this is a replicated RPC which in turn will call this methods implementation on each connected clients controller.

// tell the clients to end
for (FConstPlayerControllerIterator It 
   = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
   AClientPlayerController* PC = Cast<AClientPlayerController>(*It);
   if (PC && !PC->IsLocalPlayerController())
   {
      PC->ClientEndOnlineGame();
   }
}

Each client then calls EndSession to end its session!

IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();
if(Session.IsValid())
{
   Session->EndSession(PlayerState->SessionName);
}

Destroy a Session

IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();
if (Session.InValid())
{
   Session->DestroySession(SessionName);
}

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.