Connection and Hosting

On the Internet, I found various guides on how to write a connection, and almost nowhere, even in the English-speaking segment, was there a complete and (relatively) correct guide, which I was trying to find at one time.

I want to use 2 key figures: IOnlineSubsystem – to interact with the services of the future game and AGameSession is the key class that handles the connection directly.


Main part.

1. Description.

IOnlineSubsystem is a singleton object that is a set of interfaces for interacting with any service (Steam, Origin, PS store, etc.). Therefore, in order for your game to be able to work with this service, you must put in the config the heir to these interfaces, written specifically for this service. In the current guide, we will use OnlineSubsystemSteam

As I already said, IOnlineSubsytem is, first of all, a set of interfaces, and very high-level ones. So that your game does not have to be rewritten for different platforms, interaction with these services has been greatly simplified (good for developers). For a complete understanding of the picture, I ask you to go to the source yourself. OnlineSubsystemSteam

There will be no OnlineSubsystemSteam settings in the guide, because there are many guides on this topic.

Rice.  1. Location of OnlineSubsystemSteam
Rice. 1. Location of OnlineSubsystemSteam

AGameSession is an object that exists exclusively on the server. It is responsible for connecting the player to the session and acts as a wrapper over the interface.

I strongly advise you to familiarize yourself with the headder GameSession.h by the address GameFramework / GameSession.h for a complete understanding of the mechanisms of this class.

Rice.  2. Methods of the AGameSession class for attaching a player.
Rice. 2. Methods of the AGameSession class for attaching a player.

As a result, these two classes must work in synergy to get a quality product.

2.Connecting to the server

Now let’s figure out what the pipeline for connecting the player to the server is. By a server we mean both another client and a dedicated server.

  1. Session search.

  2. Joining a session.

  3. Exit the session.

Now, in detail about each item:

  1. Search. All found sessions need to be stored somewhere and searched somehow. Let’s create a field in our custom class and a search function:

#pragma once
#include "CPP_GameSession.generated.h"

UCLASS(config = Game)
class ACPP_GameSession : public AGameSession
{
	GENERATED_BODY()
public:
	/**
	 * @param UserId Юзер, инициирующий поиск.
	 * @param bIsLAN Если ищем в локальной сети
	 * @param bIsPresence Будет поиск сессий с тем же флагом.                                                   
	 * Используется с сервисами вроде стим'а,
	 * чтобы позволить игрокам подключаться друг к другу через список друзей, к примеру.  
	 */
	void FindSessions(TSharedPtr<const FUniqueNetId> UserId, bool bIsLAN, bool bIsPresence);
private:
	/**Структура, хранящая найденые сессии. */
	TSharedPtr<FOnlineSessionSearch> SearchSettings;
	
};

Do not forget to include the following modules in your project file (which ends with .Build.cs):

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

Directly the search itself:

void ACPP_GameSession::FindSessions(TSharedPtr<const FUniqueNetId> UserId, bool bIsLAN, bool bIsPresence)
{
  //Берем singleton объект
	IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld());
	if (OnlineSub)
	{		
  	//Мы очень часто будем обращаться к этому интерфейсу
    //В каждой OnlineSubsystem они работают по-своему
    //В нашем случае, мы берем интерфейс Нашей сессии
    //Пока игрок не подключился к серверу, он сам является сервером
    //И по этой причине у нас есть наша "сессия",
    //которую мы используем для подключения к сессии другого игрока.
		IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();

		if (Sessions.IsValid() && UserId.IsValid())
		{
      //Заполняем интерфейс сессии необходимыми настройками
			SearchSettings = MakeShareable(new FOnlineSessionSearch());
			SearchSettings->bIsLanQuery = bIsLAN;
			SearchSettings->MaxSearchResults = 100;

      //Некоторые настройки будут заполняться в QuerySettings
			if (bIsPresence)
			{
        //Выставляем указываем, что параметр это SEARCH_PRESENCE и передаем наш флаг с типом сравнения EOnlineComparisonOp::Equals
				SearchSettings->QuerySettings.Set(SEARCH_PRESENCE, bIsPresence, EOnlineComparisonOp::Equals);
			}

			//Когда поиск сессий окончится - будет вызвана функция
      //Привязанная к делегату OnFindSessionsCompleteDelegate (об этом ниже)
      //Кроме того нам потребуется этот делегат в будущем отвязать,
      //Поэтому создадим делегат Handler и привяжем его:
      OnFindSessionsCompleteDelegateHandle = Sessions->AddOnFindSessionsCompleteDelegate_Handle(OnFindSessionsCompleteDelegate);

			//Инициируем поиск, передавая ID искателя (игрока) и наши настройки.
			Sessions->FindSessions(*UserId, SearchSettings.ToSharedRef());
		}
	}
}

When the search for sessions is over, we need to find out about this.

Let’s create several delegates and functions for this:

UCLASS(config = Game)
class ACPP_GameSession : public AGameSession
{
	GENERATED_BODY()
...
protected:
	virtual void BeginPlay() override;
private:
	/** Делегат на создание сессии */
	FOnCreateSessionCompleteDelegate OnCreateSessionCompleteDelegate;
	/** Делегат на старт сессии */
	FOnStartSessionCompleteDelegate OnStartSessionCompleteDelegate;
	/** Делегат на уничтожение сессии (инициируется сервером) */
	FOnDestroySessionCompleteDelegate OnDestroySessionCompleteDelegate;
	/** Делегат на окончание поиска сессии */
	FOnFindSessionsCompleteDelegate OnFindSessionsCompleteDelegate;
	/** Делегат на присоединение к сессии*/
	FOnJoinSessionCompleteDelegate OnJoinSessionCompleteDelegate;
  
  //Ниже перечислим функции для каждого делегата.
  
 /**
	 * @param SessionName Имя сессии, для которой вызывается callback
	 * @param bWasSuccessful true, если ассинхронный процесс выполнен без ошибок
	 */
	virtual void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);

	/**
	 * @param SessionName Имя сессии, для которой вызывается callback 
	 * @param bWasSuccessful true, если ассинхронный процесс выполнен без ошибок
	 */
	void OnStartOnlineGameComplete(FName SessionName, bool bWasSuccessful);
	/**
	 * @param bWasSuccessful true, если ассинхронный процесс выполнен без ошибок
	 */
	void OnFindSessionsComplete(bool bWasSuccessful);

	/**
	 * @param SessionName Имя сессии, для которой вызывается callback 
	 * @param bWasSuccessful true, если ассинхронный процесс выполнен без ошибок
	 */
	void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);

	/**
	 * @param SessionName Имя сессии, для которой вызывается callback
	 * @param bWasSuccessful true, если ассинхронный процесс выполнен без ошибок
	 */
	virtual void OnDestroySessionComplete(FName SessionName, bool bWasSuccessful);	
	
  /** Handler'ы наших делегатов */
	FDelegateHandle OnStartSessionCompleteDelegateHandle;
	FDelegateHandle OnCreateSessionCompleteDelegateHandle;
	FDelegateHandle OnDestroySessionCompleteDelegateHandle;
	FDelegateHandle OnFindSessionsCompleteDelegateHandle;
	FDelegateHandle OnJoinSessionCompleteDelegateHandle;
  };

Now let’s bind them to functions:

...
void ACPP_GameSession::BeginPlay()
{
	Super::BeginPlay();

	OnCreateSessionCompleteDelegate = FOnCreateSessionCompleteDelegate::CreateUObject(this, &ACPP_GameSession::OnCreateSessionComplete);
	OnStartSessionCompleteDelegate = FOnStartSessionCompleteDelegate::CreateUObject(this, &ACPP_GameSession::OnStartOnlineGameComplete);
	OnFindSessionsCompleteDelegate = FOnFindSessionsCompleteDelegate::CreateUObject(this, &ACPP_GameSession::OnFindSessionsComplete);
	OnJoinSessionCompleteDelegate = FOnJoinSessionCompleteDelegate::CreateUObject(this, &ACPP_GameSession::OnJoinSessionComplete);
	OnDestroySessionCompleteDelegate = FOnDestroySessionCompleteDelegate::CreateUObject(this, &ACPP_GameSession::OnDestroySessionComplete);
}
...

Now, as soon as the search is over, our function will be called OnFindSessionsComplete(). Let’s process it:

void ACPP_GameSession::OnFindSessionsComplete(bool bWasSuccessful)
{
	// Опять берем наш Subsystem
	IOnlineSubsystem* const OnlineSub = IOnlineSubsystem::Get();
	if (OnlineSub)
	{
		// Снова просим интерфейс сессии.
		IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
		if (Sessions.IsValid())
		{
      //Чистим делегат
			Sessions->ClearOnFindSessionsCompleteDelegate_Handle(OnFindSessionsCompleteDelegateHandle);
			// Если кол-во найденых сессий не нуль
			if (SearchSettings->SearchResults.Num() > 0)
			{
        //В целях дебага можем вывести параметры каждой найденой сессии.
				for (int32 SearchIdx = 0; SearchIdx < SearchSettings->SearchResults.Num(); SearchIdx++)
				{
					//Как уже отмечалось, SearchSettings хранит иформацию о найденых сессиях
          //Просим вывести нам имя всех найденых сессий:
					GEngine->AddOnScreenDebugMessage(-1, 10.f, FColor::Red, FString::Printf(TEXT("Session Number: %d | Sessionname: %s "), SearchIdx + 1, *(SearchSettings->SearchResults[SearchIdx].Session.OwningUserName)));
				}
			}
		}
	}
}

As soon as the search is over, you can notify the player about this by removing the loading screen or any other logo that has been temporarily spawned. In our case, the debug output on the screen will say this

  1. Join the session.

bool ACPP_GameSession::JoinSession(TSharedPtr<const FUniqueNetId> UserId, FName InSessionName, const FOnlineSessionSearchResult& SearchResult)
{
	bool bSuccessful = false;
	
	IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
    
	if (OnlineSub)
	{
		//Интерфейс нашей сессии сессии
		IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
    
		if (Sessions.IsValid() && UserId.IsValid())
		{
			//Все тоже самое - просим интерфейс подключится к сессии и ждем,
			//пока вызовется функция, привязанная к делегату OnJoinSessionCompleteDelegate
			OnJoinSessionCompleteDelegateHandle = Sessions->AddOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegate);
			bSuccessful = Sessions->JoinSession(*UserId, InSessionName, SearchResult);
		}
	}
	return bSuccessful;
}

Let’s handle the call to the delegate:

void ACPP_GameSession::OnJoinSessionComplete(FName InSessionName, EOnJoinSessionCompleteResult::Type Result)
{
    IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
    if (OnlineSub)
    {
    	IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
    	if (Sessions.IsValid())
    	{
    		//Т.к этот вызов завершен - чистим делегат
    		Sessions->ClearOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegateHandle);
    
    		//PlayerController входа непосредственно
    		APlayerController* const PlayerController = GetGameInstance()->GetFirstLocalPlayerController();
    
    		// Поскольку у всех Online subsystem разные URL для подключения
        //Необходимо попоросить наш интерфес собрать для нас этот URL и положить его в эту стрингу
    		FString TravelURL;
    		if (PlayerController && Sessions->GetResolvedConnectString(InSessionName, TravelURL))
    		{
    			// И, наконец, перенос:
    			PlayerController->ClientTravel(TravelURL, TRAVEL_Absolute);
    		}
    	}
    }
}

After completing these steps, travel itself should occur.

If you dig into the ClientTravel function, you will find that it is nothing more than just OpenLevel with the desired URL and a few settings.

  1. Exit the session.

Since we are on the server, we do not have access to the GameSession and all actions must be performed purely using the controller and subsystem.

Somewhere in the controller or, as I did, in

UCPP_ClientTravelSubsystem : public UGameInstanceSubsystem

class UCPP_ClientTravelSubsystem  : public UGameInstanceSubsystem
{
	GENERATED_BODY()
  
  public:
 	//Уничтожение сессии
	void DestroySession();
  
  private:
  //Добавим делегат Handle на уничтожение сессии
  FDelegateHandle OnDestroySessionCompleteDelegateHandle;
  
  //Вызовется, когда сессия будет уничтожена
  void DestroySessionComplete(FName InSessionName, bool bWasSuccessful);
}

* I will not analyze what other Subsystems are, because this is far beyond the scope of this guide. You can use any other object the client owns (GameInstance or PlayerController)

Let’s add a function DestroyClientSession()

void UCPP_ClientTravelSubsystem :: DestroyClientSession()
{
	IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
	if(Subsystem)
	{
		IOnlineSessionPtr Session = Subsystem->GetSessionInterface();
		if(Session.IsValid())
		{
      //Подвязываемся на событие Destroy'я
			OnDestroySessionCompleteDelegateHandle = Session->AddOnDestroySessionCompleteDelegate_Handle(OnDestroySessionComplete);
			//Банально просим наш интерфейс уничтожить эту сессию.
      Session->DestroySession(NAME_GameSession);
		}
	}                                                                                             
}

Let’s process our Destroy:

void UCPP_ClientTravelSubsystem::DestroySessionComplete(FName InSessionName, bool bWasSuccessful)
{
	if(bWasSuccessful)
	{
  	//Если прошло успешно, откроем какую нибудь карту
		UGameplayStatics::OpenLevel(MapName)
	}
}

As a result, I would like to clarify: the session interface is created both by the server and by the client when it creates or connects to the session.

If the server produces a Destroy session, it disconnects all clients, if Destory calls the client (locally on its machine), then it is disconnected from the server, and this does not affect the server in any way.

3. Hosting.

The hosting has the following pipeline:

  1. Session creation

  2. Session start

  3. Destroy session

Now let’s look at how the server should act if it wants to allow other players to connect to itself.

  1. Session creation:

class ACPP_GameSession : public AGameSession
{
	GENERATED_BODY()
public:
...
	/**
	* Хостим новую сессию
	*
	* @param UserId ID игрока, который инициирует хостинг
	* @param SessionName Имя сессии
	* @param bIsLAN Если хостинг только в локальной сети
	* @param bIsPresence Если сессия помечена как Presence
	* @param MaxNumPlayers Максимальное число игроков
	*
	* @return bool флаг состояния
	*/
	bool HostSession(TSharedPtr<const FUniqueNetId> UserId, FName SessionName, const FString& GameType, const FString& MapName, bool bIsLAN, bool bIsPresence, int32 MaxNumPlayers);
private:

/**
	 * Вызывается делегатом, когда сессия успешно создана
	 *
	 * @param SessionName Имя сессии, для которой вызывается этот callback
	 * @param bWasSuccessful true если ассинхронный процесс выполнен успешно
	 */
	virtual void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);

	/**
	 * Вызывается делегатом когда сессия успешно начата
	 *
	 * @param SessionName Имя сессии, для которой вызывается этот callback
	 * @param bWasSuccessful true если ассинхронный процесс выполнен успешно
	 */
	void OnStartOnlineGameComplete(FName SessionName, bool bWasSuccessful);
  
  /** Настройки нашей конкретной сессии, которые мы будем заполнять */
	TSharedPtr<FOnlineSessionSettings> HostSettings;
  ...
};

Let’s move on to the implementation:

bool ACPP_GameSession::HostSession(TSharedPtr<const FUniqueNetId> UserId, FName InSessionName, const FString& GameType, const FString& MapName, bool bIsLAN, bool bIsPresence, int32 MaxNumPlayers)
{
	IOnlineSubsystem* const OnlineSub = Online::GetSubsystem(GetWorld());
	if (OnlineSub)
	{
		IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
		if (Sessions.IsValid())
		{
      //Запоняем HostSettings нашими настройками.
      //Их может быть любое кол-во на ваше усмотрение.
      //Перечислим только самые необходимые:
			HostSettings = MakeShareable(new FOnlineSessionSettings());
			HostSettings->bIsLANMatch = bIsLAN;
			HostSettings->bUsesPresence = bIsPresence;
			HostSettings->NumPublicConnections = MaxNumPlayers;
			
      //В качестве примера представлю еще один вид настройки
      //Есть возможность в HostSettings определенному флагу выставить значение
      //Как здесь: SETTING_MAPNAME выставляем имя нашей карты, чтобы дать возможность
      //Игроку вытащить эту настройку из найденой сессии и выставить фильтр по картам
			HostSettings->Set(SETTING_MAPNAME, MapName, EOnlineDataAdvertisementType::ViaOnlineService);
			
			OnCreateSessionCompleteDelegateHandle = Sessions->AddOnCreateSessionCompleteDelegate_Handle(OnCreateSessionCompleteDelegate);
			//Передаем наши настройки.
      return Sessions->CreateSession(*UserId, InSessionName, *HostSettings);
		}
	}
	return false;
}

Let’s process the delegate:

void ACPP_GameSession::OnCreateSessionComplete(FName InSessionName, bool bWasSuccessful)
{
	IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
	if (OnlineSub)
	{
		IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
		if (Sessions.IsValid())
		{
      //Чистим делегат
			Sessions->ClearOnCreateSessionCompleteDelegate_Handle(OnCreateSessionCompleteDelegateHandle);
			if (bWasSuccessful)
			{
        //Привязываем делегат на старт сессии
				OnStartSessionCompleteDelegateHandle = Sessions->AddOnStartSessionCompleteDelegate_Handle(OnStartSessionCompleteDelegate);
				Sessions->StartSession(SessionName);
			}
		}
	}
}
  1. Session start

It’s already easier here. On the last call, the delegate will simply call OnStartOnlineGameComplete();

void ACPP_GameSession::OnStartOnlineGameComplete(FName _SessionName, bool bWasSuccessful)
{
	IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
	if (OnlineSub)
	{
		IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
		if (Sessions.IsValid())
		{
			//Чистим делегат
      Sessions->ClearOnStartSessionCompleteDelegate_Handle(OnStartSessionCompleteDelegateHandle);
		}

    //Если сессия успешно создана - открываем карту
		if (bWasSuccessful)
		{
      //MapName - имя какой нибудь карты
      //И важно (!), обязательно указываем в параметрах "listen"
      //Маркируя эту карту как онлайн-открытую
			UGameplayStatics::OpenLevel(GetWorld(),MapName , true, "listen");
		}
	}
}
  1. Destroy session

Destruction of the session is performed in the same way as we showed above for the client. The only difference could be if we put the implementation in UGameSession, here at your discretion.

Conclusion

That’s probably all. The guide turned out to be very cumbersome and, probably, difficult for beginners.

I could easily admit any typos or inaccuracies, so write comments, additions and wishes, I will be happy to answer them.

Thanks for reading!

Similar Posts

Leave a Reply