Multiplayer Online Game C++, Unreal Engine

👤 Sharing: AI
```cpp
// Fill out your copyright notice in the Description page of Project Settings.

#include "MyGameInstance.h"
#include "OnlineSubsystem.h"
#include "OnlineSessionSettings.h"
#include "Kismet/GameplayStatics.h"

UMyGameInstance::UMyGameInstance()
{
	// Ensure Tick is enabled.  This is crucial for session handling.
	bTickEnabled = true;

	// Set default session name (can be overridden)
	SessionName = FName("MySessionName");
}

void UMyGameInstance::Init()
{
	// Get the Online Subsystem.  This is the primary interface to the online platform (Steam, Epic, etc.)
	IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get();

	if (Subsystem != nullptr)
	{
		UE_LOG(LogTemp, Warning, TEXT("Found subsystem %s"), *Subsystem->GetSubsystemName().ToString());

		// Get the Session Interface.  This provides functions for creating, searching, joining, and destroying sessions.
		SessionInterface = Subsystem->GetSessionInterface();
		if (SessionInterface.IsValid())
		{
			// Bind our custom delegates to the Session Interface's delegates.  These will be called when session events occur.
			SessionInterface->OnCreateSessionCompleteDelegates.AddUObject(this, &UMyGameInstance::OnCreateSessionComplete);
			SessionInterface->OnDestroySessionCompleteDelegates.AddUObject(this, &UMyGameInstance::OnDestroySessionComplete);
			SessionInterface->OnFindSessionsCompleteDelegates.AddUObject(this, &UMyGameInstance::OnFindSessionsComplete);
			SessionInterface->OnJoinSessionCompleteDelegates.AddUObject(this, &UMyGameInstance::OnJoinSessionComplete);
		}
		else
		{
			UE_LOG(LogTemp, Error, TEXT("SessionInterface is not valid."));
		}
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("Found no subsystem"));
	}
}

void UMyGameInstance::Shutdown()
{
	// Clean up any ongoing sessions on shutdown.
	if (SessionInterface.IsValid() && SessionIdForLastSession.IsValid())
	{
		SessionInterface->DestroySession(SessionIdForLastSession);
	}

	Super::Shutdown();
}

void UMyGameInstance::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	//Tick the online subsystem to perform tasks such as matchmaking, session maintenance, etc.
	IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get();
	if (Subsystem)
	{
		Subsystem->Tick(DeltaTime);
	}
}

bool UMyGameInstance::CreateServer(bool bIsLan, int32 MaxNumPlayers, FString ServerName)
{
	UE_LOG(LogTemp, Warning, TEXT("Create Server Called"));

	// Store ServerName, it's not directly used in session creation (metadata is better for that), but could be used elsewhere.
	this->ServerName = ServerName;

	// Destroy existing session if one exists
	if (SessionInterface.IsValid())
	{
		FNamedOnlineSession* ExistingSession = SessionInterface->GetNamedSession(SessionName);
		if (ExistingSession != nullptr)
		{
			// Destroy the existing session before attempting to create a new one
			SessionInterface->DestroySession(SessionName);
			return false; // Creation attempt will happen after destruction completes.
		}
	}

	// Create session settings
	FOnlineSessionSettings SessionSettings;
	SessionSettings.bIsLANMatch = bIsLan;
	SessionSettings.NumPublicConnections = MaxNumPlayers;
	SessionSettings.bShouldAdvertise = true; // Make sure server is advertised
	SessionSettings.bUsesPresence = true;
	SessionSettings.bAllowJoinInProgress = false; // Prevent joining mid-game (optional)
	SessionSettings.bUseLobbiesIfAvailable = true; // Enables use of lobby system where available

	// Platform Specific Settings (Important!)
	SessionSettings.Set(FName("MatchType"), FString("FreeForAll"), EOnlineDataAdvertisementType::ViaOnlineService); // Example, use metadata.
	SessionSettings.BuildUniqueId = 1;

#if UE_BUILD_SHIPPING
	SessionSettings.bIsDedicated = true; // Dedicated Server
#else
	SessionSettings.bIsDedicated = false; // Listen Server (For testing)
#endif

	// Create the session
	return SessionInterface->CreateSession(0, SessionName, SessionSettings);
}

void UMyGameInstance::FindServer()
{
	UE_LOG(LogTemp, Warning, TEXT("Find Server Called"));

	// Destroy existing search results
	if (SessionSearch.IsValid())
	{
		SessionSearch.Reset();
	}

	// Create a new search object
	SessionSearch = MakeShareable(new FOnlineSessionSearch());

	if (SessionSearch.IsValid())
	{
		SessionSearch->bIsLanQuery = false;
		SessionSearch->MaxSearchResults = 50; // Increase search results limit.
		SessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);

		// Find Sessions
		UE_LOG(LogTemp, Warning, TEXT("Starting Find Sessions"));
		UserIdx = 0;
		SessionInterface->FindSessions(UserIdx, SessionSearch.ToSharedRef());
	}
}

bool UMyGameInstance::JoinServer(FOnlineSessionSearchResult& SearchResult)
{
	UE_LOG(LogTemp, Warning, TEXT("Join Server Called"));

	if (!SessionInterface.IsValid())
	{
		UE_LOG(LogTemp, Error, TEXT("SessionInterface is not valid, cannot join session."));
		return false;
	}

	// Join the session
	return SessionInterface->JoinSession(0, SessionName, SearchResult);
}

void UMyGameInstance::EndSession()
{
	UE_LOG(LogTemp, Warning, TEXT("End Session Called"));

	if (SessionInterface.IsValid() && SessionIdForLastSession.IsValid())
	{
		SessionInterface->DestroySession(SessionIdForLastSession);
	}
}


// Delegate Implementations

void UMyGameInstance::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{
	UE_LOG(LogTemp, Warning, TEXT("OnCreateSessionComplete, success: %d"), bWasSuccessful);

	if (bWasSuccessful)
	{
		// Get the session ID to store it for later
		FNamedOnlineSession* Session = SessionInterface->GetNamedSession(SessionName);
		if (Session)
		{
			SessionIdForLastSession = Session->SessionId;
		}

		// Travel to the gameplay level (Listen Server uses ClientTravel).
		UWorld* World = GetWorld();
		if (World)
		{
			FString TravelURL = FString("/Game/ThirdPerson/Maps/ThirdPersonMap?listen");  // Replace with your map path.
			World->ServerTravel(TravelURL);
		}
	}
	else
	{
		// Creation failed, handle the error (e.g., notify the user).
		UE_LOG(LogTemp, Error, TEXT("Session creation failed."));
		OnCreateSessionFailed.Broadcast(); // Broadcast an event for UI or other error handling.
	}
}

void UMyGameInstance::OnDestroySessionComplete(FName SessionName, bool bWasSuccessful)
{
	UE_LOG(LogTemp, Warning, TEXT("OnDestroySessionComplete, success: %d"), bWasSuccessful);

	if (bWasSuccessful)
	{
		SessionIdForLastSession.Invalidate(); // Clear out the session ID

		// If the server destroyed the session, load the Main Menu.
		UWorld* World = GetWorld();
		if (World)
		{
			UGameplayStatics::OpenLevel(World, FName("MainMenu"), false); // Replace "MainMenu" with your main menu map.
		}
	}
	else
	{
		UE_LOG(LogTemp, Error, TEXT("Session destruction failed."));
	}
}


void UMyGameInstance::OnFindSessionsComplete(bool bWasSuccessful)
{
	UE_LOG(LogTemp, Warning, TEXT("OnFindSessionsComplete, success: %d"), bWasSuccessful);

	if (SessionInterface.IsValid() && SessionSearch.IsValid())
	{
		UE_LOG(LogTemp, Warning, TEXT("Finished Find Sessions"));

		TArray<FOnlineSessionSearchResult> SearchResults = SessionSearch->SearchResults;

		if (SearchResults.Num() > 0)
		{
			UE_LOG(LogTemp, Warning, TEXT("We found sessions!"));

			// Broadcast the results to any listeners (e.g., UI).
			OnFindSessionsSucceeded.Broadcast(SearchResults);
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("We found no sessions!"));
			OnFindSessionsFailed.Broadcast(); // Broadcast that no sessions were found.
		}
	}
}


void UMyGameInstance::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
	UE_LOG(LogTemp, Warning, TEXT("OnJoinSessionComplete, result: %d"), (int32)Result);

	if (Result == EOnJoinSessionCompleteResult::Success)
	{
		// Get Resolved Connect String
		FString Address;
		if (SessionInterface->GetResolvedConnectString(SessionName, Address))
		{
			UE_LOG(LogTemp, Warning, TEXT("ConnectString: %s"), *Address);

			// Travel to the server's address
			APlayerController* PlayerController = UGameplayStatics::GetPlayerController(GetWorld(), 0);
			if (PlayerController)
			{
				PlayerController->ClientTravel(Address, ETravelType::TRAVEL_Absolute);
			}
		}
	}
	else
	{
		// Handle the failure to join (e.g., session full, password protected).
		UE_LOG(LogTemp, Error, TEXT("Failed to join session. Result code: %d"), (int32)Result);
		OnJoinSessionFailed.Broadcast(); //Broadcast an event so the UI can display error.
	}
}

bool UMyGameInstance::GetTickEnabled() const
{
	return bTickEnabled;
}
```

**Explanation and Important Considerations:**

1.  **Header Files:**
    *   `MyGameInstance.h`: Defines the class `UMyGameInstance`.  (See the header file example below).
    *   `OnlineSubsystem.h`:  Essential for using the Online Subsystem (Steam, Epic Online Services, etc.).
    *   `OnlineSessionSettings.h`:  Defines the settings for creating and finding online sessions.
    *   `Kismet/GameplayStatics.h`:  Provides static helper functions, like loading levels.

2.  **Game Instance:**
    *   A `UGameInstance` is a singleton object that persists throughout the entire game session, unlike `UWorld` (levels) which are loaded and unloaded.  It's a good place to manage global game state, including online session information.
    *   In your Unreal Engine project:  Create a new C++ class derived from `GameInstance`. Then, in the *Project Settings* under *Maps & Modes*, set your custom Game Instance class.

3.  **Online Subsystem:**
    *   `IOnlineSubsystem`:  The core interface to the online platform (Steam, Epic, etc.).  `IOnlineSubsystem::Get()` returns a pointer to the active subsystem.  Make sure your project is configured to use the desired subsystem (see the Unreal Engine documentation about configuring Online Subsystems).
    *   `IOnlineSessionPtr`:  An interface (smart pointer) for managing online sessions.

4.  **Session Creation (`CreateServer`):**
    *   `FOnlineSessionSettings`:  A struct containing the settings for the session.  Key settings:
        *   `bIsLANMatch`:  `true` for LAN games, `false` for online games.
        *   `NumPublicConnections`:  The maximum number of players allowed in the session.
        *   `bShouldAdvertise`:  Whether to advertise the session to the online service.  Essential for others to find it.
        *   `bUsesPresence`: Enables presence information (e.g., current game mode) which can be used in search filters.
        *   `bIsDedicated`: Set to `true` for dedicated servers and `false` for listen servers. This is *critical* for performance and scalability in a live environment.
        *   `Set(FName Key, FString Value, EOnlineDataAdvertisementType Type)`:  Allows you to add custom metadata to the session.  This metadata can be used to filter search results.  The example uses a "MatchType" key.
    *   `SessionInterface->CreateSession()`:  Creates the session.

5.  **Session Finding (`FindServer`):**
    *   `FOnlineSessionSearch`:  Used to search for sessions.
    *   `bIsLanQuery`:  Set to `true` for LAN searches, `false` for online searches.
    *   `MaxSearchResults`:  The maximum number of sessions to return.  Increase if you expect many servers.
    *   `QuerySettings`:  Allows you to filter search results based on metadata.  The example uses `SEARCH_PRESENCE` and `EOnlineComparisonOp::Equals` to find sessions that have presence enabled.
    *   `SessionInterface->FindSessions()`:  Starts the search.
    *   `OnFindSessionsCompleteDelegates`: Receives the results of the search.

6.  **Session Joining (`JoinServer`):**
    *   `SessionInterface->JoinSession()`:  Joins an existing session based on the search result.

7.  **Session Destruction (`EndSession`):**
    *   `SessionInterface->DestroySession()`:  Destroys the session.  Typically called when the server is shutting down.  Call this on the server *before* closing the application.

8. **Session ID Handling (`SessionIdForLastSession`)**
    * The code stores the session ID upon successful session creation in `SessionIdForLastSession`. This allows for proper cleanup during game instance shutdown by calling `DestroySession` if necessary.

9.  **Delegates:**
    *   The `OnlineSessionInterface` uses delegates to notify you when asynchronous operations complete (e.g., session creation, search, joining).  The code binds custom functions (e.g., `OnCreateSessionComplete`) to these delegates.  Delegates are a core part of Unreal Engine's event system.
    *  `OnCreateSessionFailed`, `OnFindSessionsFailed`, `OnJoinSessionFailed`: custom delegates that allow you to broadcast that the given action has failed to be completed.

10. **Traveling (Level Loading):**
    *   `UWorld::ServerTravel()`:  (Server-side) Loads a new level and tells clients to connect to it.  Used by the *server* to load the game level.  The `?listen` parameter creates a listen server (a server running on a client machine).  On a *dedicated* server, you would *not* use `?listen`.
    *   `APlayerController::ClientTravel()`:  (Client-side) Tells the client to connect to a server.  Used by the *client* to connect to the server.

11. **Error Handling:**
    *   The code includes basic error handling (e.g., checking if pointers are valid, logging error messages).  In a real game, you would want to implement more robust error handling, such as:
        *   Displaying error messages to the user.
        *   Trying to recover from errors.
        *   Logging errors to a file.

12. **Tick:**
    *  The `Tick` function must be enabled in the `GameInstance`. The online subsystem needs `Tick` called on it in order to properly handle network events, matchmaking, etc. This is *critical*.

13. **Asynchronous Operations:**
    *   Online operations are asynchronous.  This means that the functions like `CreateSession`, `FindSessions`, and `JoinSession` return *immediately*, and the results are delivered later via delegates.  This prevents the game from blocking while waiting for the online operation to complete.

14. **Platform-Specific Considerations:**
    *   Online subsystems are platform-specific (e.g., Steam, Epic Online Services, PlayStation Network, Xbox Live).  You need to configure your project to use the appropriate subsystem for your target platform.  This involves:
        *   Adding the appropriate plugin to your project.
        *   Configuring the plugin in the `DefaultEngine.ini` file.
        *   Potentially setting up accounts and keys with the platform provider.
    *   The code includes some platform-specific settings in `FOnlineSessionSettings`.  You may need to adjust these settings based on your target platform.

15.  **Dedicated Servers:**
    *   A dedicated server is a server that runs without a client.  It's typically used for production games to provide better performance and stability.
    *   When building a dedicated server, you need to:
        *   Set `SessionSettings.bIsDedicated = true`.
        *   Build the server in "Shipping" configuration.
        *   Run the server from the command line with the `-server` option.
    *   Listen servers (servers running on client machines) are fine for testing but are not suitable for production.

16. **Threading:**
   *  Multiplayer networking is heavily reliant on multithreading.  Care must be taken to ensure that data is properly synchronized between threads.

**MyGameInstance.h (Example Header File):**

```cpp
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "OnlineSessionSettings.h"
#include "MyGameInstance.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCreateSessionFailed);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnFindSessionsFailed);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnJoinSessionFailed);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFindSessionsSucceeded, TArray<FOnlineSessionSearchResult>, SearchResults);

/**
 *
 */
UCLASS()
class MYPROJECT_API UMyGameInstance : public UGameInstance
{
	GENERATED_BODY()

public:
	UMyGameInstance();

	virtual void Init() override;
	virtual void Shutdown() override;
	virtual void Tick(float DeltaTime) override;

	// Functions to create, find and join server
	UFUNCTION(BlueprintCallable, Category = "Multiplayer")
		bool CreateServer(bool bIsLan, int32 MaxNumPlayers, FString ServerName);

	UFUNCTION(BlueprintCallable, Category = "Multiplayer")
		void FindServer();

	UFUNCTION(BlueprintCallable, Category = "Multiplayer")
		bool JoinServer(FOnlineSessionSearchResult& SearchResult);

	UFUNCTION(BlueprintCallable, Category = "Multiplayer")
		void EndSession();

	//Delegate to broadcast the status to the game
	UPROPERTY(BlueprintAssignable, Category = "Multiplayer")
		FOnCreateSessionFailed OnCreateSessionFailed;

	UPROPERTY(BlueprintAssignable, Category = "Multiplayer")
		FOnFindSessionsSucceeded OnFindSessionsSucceeded;

	UPROPERTY(BlueprintAssignable, Category = "Multiplayer")
		FOnFindSessionsFailed OnFindSessionsFailed;

	UPROPERTY(BlueprintAssignable, Category = "Multiplayer")
		FOnJoinSessionFailed OnJoinSessionFailed;

	//Get the value of TickEnabled
	UFUNCTION(BlueprintCallable, Category = "Multiplayer")
		bool GetTickEnabled() const;

private:
	//Session delegate functions
	void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);
	void OnDestroySessionComplete(FName SessionName, bool bWasSuccessful);
	void OnFindSessionsComplete(bool bWasSuccessful);
	void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);

	//Online Subsystem Variable
	IOnlineSessionPtr SessionInterface;

	//Search Result Variable
	TSharedPtr<class FOnlineSessionSearch> SessionSearch;

	//Varibles
	FName SessionName;
	FString ServerName;
	int32 UserIdx;
	bool bTickEnabled;
	FUniqueNetIdRepl SessionIdForLastSession;
};
```

**How to Use (Basic Steps):**

1.  **Create a C++ class:** Create a new C++ class in your Unreal Engine project, and make it a subclass of `GameInstance`.  Copy the code above into the `.h` and `.cpp` files.

2.  **Set the Game Instance:**  In your Unreal Engine project:  Go to *Edit* -> *Project Settings* -> *Maps & Modes*.  Set the *Game Instance Class* to your custom `UMyGameInstance` class.

3.  **Blueprint Integration:**  Make sure you include `UFUNCTION(BlueprintCallable)` for the functions you want to call from Blueprints (e.g., `CreateServer`, `FindServer`, `JoinServer`).  Also include `UPROPERTY(BlueprintAssignable)` for delegates you want to bind in Blueprints.

4.  **Call from Blueprints:** Create Blueprints to call the `CreateServer`, `FindServer`, and `JoinServer` functions on your Game Instance.

5. **UI Events:**  Create a UI to display the results of the `FindServer` function (use the `OnFindSessionsSucceeded` delegate to get the results). Create a UI to display errors broadcast by delegates.

6. **Configure Online Subsystem:** Configure your `DefaultEngine.ini` file in Config folder to select your online subsystem.
    ```ini
    [/Script/Engine.Engine]
    +NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="UNetDriver")

    [OnlineSubsystem]
    DefaultPlatformService=Steam

    [OnlineSubsystemSteam]
    bEnabled=true
    SteamDevAppId=480 // Replace with your Steam App ID (if applicable)

    ```
    Make sure the OnlineSubsystem plugin is enabled in your project.

7.  **Test:**  Test your code in the Unreal Editor and as a standalone game.

This is a basic example, and you'll likely need to extend it to fit your specific game's requirements.  Remember to thoroughly test your multiplayer code on your target platform to ensure it's working correctly.  Good luck!
👁️ Viewed: 12

Comments