Introduction

Feature Parity
Leaderboards
Achievements
Matchmaking
Downloadables
Player Management

Epic Online Services is a games backend service by Epic and powers huge multiplayer games like Fortnite. It offers many of the same features as GameSparks as well as additional features such as Epic Store integration, analytics and multiplayer features.

In this series of topics we will be covering the Unity3D SDK for EOS. It is available for download here and comes with instructions for set up and integration into your existing project. This series starts with the Authentication Basics here. You can use the guide there to see how to set up a new EOS project and authenticate players.

You can sign up for Epic Online Services by creating a developer account here.

Authentication Basics

EOS authentication differs from other platforms as there are two steps in order to gain access to Epic Online Services. First you authenticate a user using the Authentication Interface and then you connect to the online services using the Connect Interface.

Something that is important to know is that authentication is an optional service for your game’s Epic Online Services. You need to choose to enable Epic Account Services when you create or configure your product.

There are more complete docs on login and authentication for the EOS platform available here and a number of alternative login routes including external account integration are covered there but for this topic we will cover how this compares to GameSparks basic login only.

Unity Plugin Setup
First we will need to create a new project on our Developer Dashboard. Click on the link here to create a new developer account. Once your account is set up, click on your organization and then click the Create Project button.

Next we need to generate credentials to your project so that the EOS SDK on your client can connect to your game’s backend services.

New Client
We will need to add a client credential to the project. This will be something like the GameSparks API key used by the SDK to connect to the backend.

Click on the Product Settings option in the menu on the left-hand side of the dashboard. Select the Clients tab and click on the Add New Client button.

Client Policy
Next we will need to add a client policy. Client policies are used to define what features the client has access to, so here we will need to enable the features we want to use.

Add policy and select the Game Client option from the menu.

You will then see the options you can choose. For our example and those in the following tutorials we enabled Achievements, Stats, Leaderboards, Matchmaking and Title Storage.

Remember to click on the Save & Exit button at the bottom when you are finished with your setup.

Note - You will notice that if you click on one of these features you can see additional settings which can be applied to that feature. We will not cover these in this tutorial but you can always come back and edit for client policy later if you want to finalize these settings. We will look at these settings in other topics later on.

Configure Application
Next we need to configure our application. To do this, click on the Epic Account Services option on the left-hand side menu of your dashboard.

Click on the Configure button on the right-hand side.

Now we need to set up the application’s permissions and clients. We are going to use the default settings so all you have to do is click on the tab on the top-right of the window and click on the Save button for each option.

In the Clients tab simply select the client you just set up.

Now our project is set up and ready to go from the online Developer Dashboard side. We next have to set up the Unity SDK.

You can get access to the Unity SDK here. Follow the setup and configuration steps in that guide for details on how to set up the SDK.

The important step you need to complete in order for your client to start talking to your EOS game services is to apply all the correct credentials in the Unity editor.

In your Unity project open the Epic Credentials window by clicking Tools -> EpicOnlineServiceConfigEditor from the top menu bar. Fill in the relevant fields using the information provided in the Project Settings section of the Developer Dashboard.

Encryption Key
The encryption key is used for Player Data Storage and/or Title Storage. This is not taken from your EOS application setup but is instead something generated by you as the developer. EOS will never record this key to ensure game and player’s data is always encrypted.

This must be a Hex string of 64 characters or less. You can find a generator online which will allow you to create such a string.

We will use this string to encrypt our Downloadables in a later topic here.

EOS Platform Reference
The Unity SDK comes with a EOSManager singleton Prefab. We need an active instance of this in our scene in order to access the EOS SDK.

To do this simply drag it in from the Runtime folder (under the Epic Online Services Folder) into the scene.

Step 1 - Authentication
Now that we have our EOSManager instance setup, we can start using the EOS SDK for authentication.

We are going to look at 3 different methods to authenticate the user.

Device Authentication
This form of authentication is similar to device authentication you may have used for your GameSparks frontend. As with GameSparks, this process will also create a new user if one does not exist already.

In order to authenticate the user using the device id we need to create the device id using the Connect Interface. This device id will automatically be cached within the SDK and only needs to be created once.

To create the device id we will call the CreateDeviceId method on the Connect Interface.

var connectInterface = EOSManager.Instance.GetEOSConnectInterface();
var deviceIdOptions = new Epic.OnlineServices.Connect.CreateDeviceIdOptions()
{
    DeviceModel = SystemInfo.deviceType.ToString()
};
connectInterface.CreateDeviceId(deviceIdOptions, null, (Epic.OnlineServices.Connect.CreateDeviceIdCallbackInfo createDeviceIdCallbackInfo) =>
{
    if (createDeviceIdCallbackInfo.ResultCode == Result.Success)
    {
        print("Created device Id. [" + createDeviceIdCallbackInfo.ResultCode + "]");
        //device Id is stored in the keychain for the logged in user of the local device
    }
});

You will see logs from the SDK indicating that the device id has been set up correctly.

As mentioned above, there are two steps required to allow your user to gain access to Epic Online Services. We have authenticated now but we have not yet connected this user with EOS. Skip to the next section to proceed with this flow.

Additional Authentication Flows There are other ways to authenticate with EOS which we will not cover in this topic. However, many of these flows are the same from the SDK so although we are going to show how you can authenticate using a user’s Epic account, you will be able to see how these flows can be adapted to use tokens from Steam, PSN, Google, iOS, etc.

These authentication flows start with the StartLoginWithLoginOptions() method.

There can be different flows depending on the form of authentication being used. We are going to look at Epic account login (email and password) in this topic.

Note - The player’s Epic Account is the account used to sign into Epic Games. Your player does not need to have one of these accounts in order to use EOS. EOS accounts and Epic Games accounts are different and it is not required that your players have Epic Games accounts in order to log in.

Epic Account Credentials Authentication
The Epic account authentication uses the StartLoginWithLoginOptions() method. However, there can be different flows depending on the form of authentication being used. We are going to look at Epic account login (email and password) in this topic.

First let us take a look at how the StartLoginWithLoginOptions() method is used.

// Ensure platform tick is called on an interval, or this will not callback.
EOSManager.Instance.StartLoginWithLoginOptions(loginOptions, null, (LoginCallbackInfo loginCallbackInfo) =>
{
    if (loginCallbackInfo.ResultCode == Result.Success)
    {
         Debug.Log("Login succeeded");
    }
    else if (Common.IsOperationComplete(loginCallbackInfo.ResultCode))
    {
        Debug.Log("Login failed: " + loginCallbackInfo.ResultCode);
    }
});

We’ve highlighted the loginOptions variable passed into this function as it is where we can play around with different kinds of authentication.

The loginOptions object can be defined as below...

var loginOptions = new LoginOptions()
{
    Credentials = new Credentials()
    {
            Type = m_LoginCredentialType,
            Id = m_LoginCredentialId,
            Token = m_LoginCredentialToken
    }
};
`

By changing the credential-type we can change the method we wish to login with. You can see a list of external login types you can configure here.

This form of authentication is different from GameSparks as we are logging in with the player’s Epic account email and password and not just a generic email and password. The player must have an Epic account already setup

To use this form of authentication you will need to change your credential type to “Password” as in the example below.

This will allow you to pass in an Epic account email as the Id field and your account password as the Token field.

Credentials = new Credentials()
{
    Type = m_LoginCredentialType.PASSWORD,
    Id =  <eos-account-email>
    Token = <your-eos-account-pw>    
}

Logging in with this method will generate logs in the console so that you can verify your player logged in.

Note: If your account is set with 2-factor authentication this may cause an error as it will require an additional validation step.

There are many other login options which we will not cover in this topic. You can see some examples below and you can get more information on the validation flow for these authentication types here.

public enum LoginCredentialType
    {
        Password = 0,
        ExchangeCode = 1,
        PersistentAuth = 2,
        DeviceCode = 3,
        Developer = 4,
        RefreshToken = 5,
        AccountPortal = 6,
        ExternalAuth = 7
    }

Epic Account Portal Auth
If you set the login type to AccountPortal and call the StartLoginWithLoginOptions() referenced in the above section, a popup will appear where you can login directly into the Epic portal. Once logged in using this option a token will be stored on the SDK’s cache allowing for the persistent login type to be used from then on.

Credentials = new Credentials()
{
    Type = m_LoginCredentialType.AccountPortal
}

Persistent Auth is used only after a successful login using the AccountPortal type. The SDK stores a token which can be used to automatically log the client in upon subsequent sessions.

To use this form of authentication you need to call the StartPersistantLogin() method with a callback method to catch the loging or error results.

EOSManager.Instance.StartPersistantLogin((LoginCallbackInfo loginCallbackInfo) =>
{
    if (loginCallbackInfo.ResultCode == Result.Success)
    {
        Debug.Log("Login succeeded");
        Debug.Log("ID = "+loginCallbackInfo.LocalUserId.ToString());
    }
    else if (Common.IsOperationComplete(loginCallbackInfo.ResultCode))
    {
        Debug.Log("Login failed: " + loginCallbackInfo.ResultCode);
    }
});

Step 2 - Connect To Game Services
With the GameSparks SDK, once authentication was performed, the player has access to all GameSparks features. However, with EOS we need an additional step after authentication in order to allow the player access to game service features.

The method of connecting to game services differs depending on which authentication flow was used in the previous section.

Device Authentication
To connect using the device id you must call the StartConnectLoginWithDeviceToken method.

EOSManager.Instance.StartConnectLoginWithDeviceToken("YourName", (Epic.OnlineServices.Connect.LoginCallbackInfo connectLoginCallbackInfo) =>
{
    if (connectLoginCallbackInfo.ResultCode == Result.Success)
    {
        print("Connect Login Successful. [" + connectLoginCallbackInfo.ResultCode + "]");
    }
    else
    {
        print("Connect Login Failed. [" + connectLoginCallbackInfo.ResultCode + "]");
    }
});

If your player has successfully been granted permission, you should see logs from the SDK indicating a successful connection.

Your player is now connected and verified by the EOS SDK to access game services so we can start working with Leaderboards, Achievements, etc in the following topics.

Epic Games Account
To connect using your Epic account you need to call the StartConnectLoginWithEpicAccount method as below.

Debug.Log("Starting ConnectWithGameServices...");
EOSManager.Instance.StartConnectLoginWithEpicAccount(EOSManager.Instance.GetLocalUserId(), (Epic.OnlineServices.Connect.LoginCallbackInfo connectLoginCallbackInfo) =>
{
    if (connectLoginCallbackInfo.ResultCode == Result.Success)
    {
        print("Connect Login Successful. [" + connectLoginCallbackInfo.ResultCode + "]");
    }
    else if (connectLoginCallbackInfo.ResultCode == Result.InvalidUser)
    {
        Debug.Log("No connect user, we need to create one.");
        EOSManager.Instance.CreateConnectUserWithContinuanceToken(connectLoginCallbackInfo.ContinuanceToken, (Epic.OnlineServices.Connect.CreateUserCallbackInfo createUserCallbackInfo) =>
        {
            print("Creating new connect user");
            EOSManager.Instance.StartConnectLoginWithEpicAccount(EOSManager.Instance.GetLocalUserId(), (Epic.OnlineServices.Connect.LoginCallbackInfo retryConnectLoginCallbackInfo) =>
            {
                if (retryConnectLoginCallbackInfo.ResultCode == Result.Success)
                {
                    Debug.LogError("AYYYY!");
                }
            });
        });
    }
});

If your player has successfully been granted permission, you should see this in the console logs.

Leaderboard Basics

EOS leaderboards have a slightly different setup when compared to GameSparks but for simple Leaderboards posting stats like score or level they are easy to understand.

EOS has permanent leaderboards like GameSparks, but also allows you to set a start and end date on leaderboards. These would be suitable for timed events like tournaments or seasons. EOS does not currently have functionality for daily, weekly and monthly leaderboards but these could be setup manually ahead of time using the basic timed leaderboards.

EOS leaderboards are also social leaderboards by extension. You don't specifically need to set up social leaderboards to be able to get a list of your friend’s ranks on these leaderboards. We will show an example of this later on in this topic.

Leaderboards Setup
Similarly to GameSparks, leaderboards need to be first set up through the Developer Dashboard. However, there is an extra step when it comes to EOS leaderboards as you also need to set up some stats in the Developer Dashboard first. The Leaderboard can then track those stats.

When a player updates one of their stats, any Leaderboard tracking that Stat will automatically be updated too, so we don't post scores to the Leaderboard directly in EOS, we instead update a Stat.

Note - When a new Leaderboard is created and is linked to a Stat, it takes time to register your Leaderboard with the Game Service. If you are trying to get the player’s rank later in this tutorial but receive no entries in the callback this could be due to the Leaderboard and Stat not being linked up yet.

Creating New Stats
To create a new Stat click on the Stats option of the left-hand side menu of the Developer Dashboard. Click on the New Stat button to create a new Stat.

You will see some familiar aggregation types you can sort your scores by in the Leaderboard. These are similar to GameSparks for example MIN, MAX, SUM and LATEST.

For this example, we will go with a high-score Leaderboard so it will be aggregated by the MAX value.

Creating A New Leaderboard
Now that we have a simple Stat setup we can create a new Leaderboard which will track this Stat. To do this click on the Leaderboards option of the left-hand side menu of the Developer Dashboard.

Next click on the Create Leaderboard button.

First we will need to select the Stat we want to track and the duration of the leaderboard. For this example we won't use a timed Leaderboard so make sure the start date is set to some time before the current date. New leaderboards will often be set to be created later in the day by default.

Now we are ready to start posting scores.

Posting Scores
With GameSparks you would create a custom LogEventRequest and attach this to your Leaderboard as a mechanism to update player scores.

As leaderboards are directly related to stats what we are actually doing is updating a player’s Stat rather than posting to the Leaderboard like you would be familiar with in GameSparks.

To do this the player will ingest the Stat as in the example below...

using UnityEngine;
using Epic.OnlineServices;
using Epic.OnlineServices.Stats;
using PlayEveryWare.EpicOnlineServices;

public class StatManager : IEOSSubManager
{
    private StatsInterface StatsHandle;

    public StatManager()
    {
        StatsHandle = EOSManager.Instance.GetEOSPlatformInterface().GetStatsInterface();
    }

    /// <summary>Call to ingest the stat values into EOS Stat interface.</summary>
    /// <param name="statName">Name of the stat</param>
    /// <param name="amount">The amount to ingest for specified stat</param>
    public void IngestStat(string statName, int amount)
    {
        IngestData[] stats =
        {
            new IngestData()
            {
                StatName = statName,
                IngestAmount = amount
            }
        };

        IngestStatOptions options = new IngestStatOptions()
        {
            LocalUserId = EOSManager.Instance.GetProductUserId(),
            TargetUserId = EOSManager.Instance.GetProductUserId(),
            Stats = stats
        };

        StatsHandle.IngestStat(options, null, StatsIngestCallbackFn);
    }

    private void StatsIngestCallbackFn(IngestStatCompleteCallbackInfo data)
    {
        if (data == null)
        {
            Debug.LogError("StatsIngestCallbackFn: data is null");
            return;
        }

        if (data.ResultCode != Result.Success)
        {
            Debug.LogErrorFormat("StatsIngestCallbackFn: Ingest Stats error: {0}", data.ResultCode);
            return;
        }

        Debug.Log("StatsIngestCallbackFn: Ingest Stats Complete ");
    }
}

Note: Once a stat has been ingested it may take some moments before the Leaderboard will be updated on the backend as it takes time to both submit the stat and process the Leaderboard update. This also relates to any achievements triggered by stats. These achievements may also take a while to unlock.

Now we can check if the player’s score was posted in the Developer Dashboard.

Click on the Leaderboards option in the left-hand side menu again and click on the Leaderboard you created previously.

Here will be able to see a list of all ranks on your current Leaderboard.

You can see more information on Leaderboards with EOS here.

Leaderboard Definitions
First we need to get the leaderboard definitions and save them to cache. Without this call, getting the players rank will return no entries. The leaderboards need to be cached before trying to access any data within them.

Leaderboard Interface
Before we can start working with the Leaderboard APIs in the EOS Unity SDK we need to set up the Leaderboard Interface object. All Leaderboard API calls will go through this interface.

For simplicity we created a global variable in our LeaderboardManager class which would initialize the interface when it was created.

    private LeaderboardsInterface LeaderboardsHandle;

    public LeaderboardManager()
    {
        LeaderboardsHandle = EOSManager.Instance.GetEOSPlatformInterface().GetLeaderboardsInterface();
    }

Get Leaderboard Definitions
We need to send an async request to the EOS backend and cache the returned leaderboard data. To do this we use the method QueryLeaderboardDefinitions:

/// <summary>
/// Get the leaderboard definitions and store them in local cache
/// </summary>
public void QueryDefinitions()
{
    QueryLeaderboardDefinitionsOptions options = new QueryLeaderboardDefinitionsOptions()
    {
        StartTime = DateTimeOffset.MinValue,
        EndTime = DateTimeOffset.MaxValue,
        LocalUserId = EOSManager.Instance.GetProductUserId()
    };

    LeaderboardsHandle.QueryLeaderboardDefinitions(options, null, (OnQueryLeaderboardDefinitionsCompleteCallbackInfo data)=>
    {
        if (data == null)
        {
            Debug.LogError("Leaderboard (LeaderboardDefinitionsReceivedCallbackFn): data is null");
            return;
        }

        if (data.ResultCode != Result.Success)
        {
            Debug.LogErrorFormat("Leaderboard (LeaderboardDefinitionsReceivedCallbackFn): QueryDefinitions error: {0}", data.ResultCode);
            return;
        }

        Debug.Log("Leaderboard (LeaderboardDefinitionsReceivedCallbackFn): Query Definitions Complete.");
        uint leaderboardDefinitionsCount = LeaderboardsHandle.GetLeaderboardDefinitionCount(new GetLeaderboardDefinitionCountOptions());

        for (uint definitionIndex = 0; definitionIndex < leaderboardDefinitionsCount; definitionIndex++)
        {
            CopyLeaderboardDefinitionByIndexOptions copyOptions = new CopyLeaderboardDefinitionByIndexOptions()
            {
                LeaderboardIndex = definitionIndex
            };

            Result result = LeaderboardsHandle.CopyLeaderboardDefinitionByIndex(copyOptions, out Definition leaderboardDefinition);

            if (result != Result.Success)
            {
                Debug.LogErrorFormat("Leaderboard (CacheLeaderboardDefinitions): CopyLeaderboardDefinitionByIndex failed '{0}'", result);
                break;
            }

            Debug.Log("leaderboardDefinition copied: "+ leaderboardDefinition.LeaderboardId);
        }
    });
}

This will output a log with all your leaderboard Id’s:

Returning Global Leaderboard Ranked Data
Next we are going to need to get the ranked Leaderboard data so you can display your Leaderboard in-game. The example we are going to look at here would be for something like the global top 100 entries.

With GameSparks you would usually call a LeaderboardDataRequest from the client. With EOS it is much the same flow however instead of the data being available from the response, the request will cache the data in the SDK and we can then get it from there without needing to hit the Game Service for it each time unless we want the most recent Leaderboard information.

Query Ranks
We can query a Leaderboard’s global rankings with the QueryLeaderboardRanks() method. This will give us the top scores for our Leaderboard.

/// <summary>(async) Initiate query for the Global Ranks given a LeaderboardId.</summary>
/// <param name="leaderboardId"> Leaderboard Id to query.</param>
public void QueryRanks(string leaderboardId)
    {
        QueryLeaderboardRanksOptions options = new QueryLeaderboardRanksOptions()
        {
            LeaderboardId = leaderboardId,
            LocalUserId = EOSManager.Instance.GetProductUserId()
        };
        LeaderboardsHandle.QueryLeaderboardRanks(options, null, LeaderboardRanksReceivedCallbackFn);
    }

EOS Leaderboards Cache After the query is complete, the data is stored in the EOS SDK cache and the LeaderboardRanksRecievedCallbackFn() method is called.

We use this callback method to go through the cache and get all our Leaderboard records.

/// <summary>(callback) Leaderboard Query complete. Iterate through the cached data.</summary>
/// <param name="data"> Returned leaderboard query data.</param>
private void LeaderboardRanksReceivedCallbackFn(OnQueryLeaderboardRanksCompleteCallbackInfo data)
    {
        if (data == null)
        {
            Debug.LogError("Leaderboard (LeaderboardRanksReceivedCallbackFn): data is null");
            return;
        }

        if (data.ResultCode != Result.Success)
        {
            Debug.LogErrorFormat("Leaderboard (LeaderboardRanksReceivedCallbackFn): QueryRanks error: {0}", data.ResultCode);
            return;
        }

        Debug.Log("Leaderboard (LeaderboardRanksReceivedCallbackFn): Query Ranks Complete");

        uint leaderboardRecordsCount = LeaderboardsHandle.GetLeaderboardRecordCount(new GetLeaderboardRecordCountOptions());
        Debug.Log("leaderboardRecordsCount = "+ leaderboardRecordsCount);
        List<LeaderboardRecord> lbRecords = new List<LeaderboardRecord>();

        // leaderboard records are currently in cache. CopyLeaderboardRecordByIndex is called for each entry to get the cached records.
        for (uint recordIndex = 0; recordIndex < leaderboardRecordsCount; recordIndex++)
        {
            CopyLeaderboardRecordByIndexOptions options = new CopyLeaderboardRecordByIndexOptions()
            {
                LeaderboardRecordIndex = recordIndex
            };

            Result result = LeaderboardsHandle.CopyLeaderboardRecordByIndex(options, out LeaderboardRecord leaderboardRecord);

            if (result != Result.Success)
            {
                Debug.LogErrorFormat("Leaderboard (CacheLeaderboardRecords): CopyLeaderboardRecordByIndex failed '{0}'", result);
                break;
            }

            lbRecords.Add(leaderboardRecord);
        }

        foreach (LeaderboardRecord record in lbRecords)
        {
            Debug.LogFormat("    Record: UserName={0} ({1}), Rank={2}, Score={3} ", record.UserDisplayName, record.UserId, record.Rank, record.Score);
        } 
    }

Executing this method will show your Leaderboard records printed in the console for validation.

Returning Multiple Unranked Players Leaderboard Entries
Next we are going to get individual players’ scores back from the Leaderboard. In this example we will get the current player’s score but this can be used to get multiple players scores. So, for example it could be used to get all the current player’s friends scores, sort them, and use this data to create a friends leaderboard.

This would be the equivalent of GameSparks’ GetLeaderboardEntriesRequest which allows you to get the scores and ranks for any user. As with getting ranked Leaderboard data, the request will cache the details in the SDK and we can then load the data from the cache after that.

Query Player Scores
To query user scores we use the QueryLeaderboardUserScores() method.

We need to construct a QueryLeaderboardUserScoresOptions object to specify what users we want to search for.

For our example we want our current player’s rank so we only need to add the current player’s ID to the array. However, you could use this to enter an array of users and not just a single entry, for example turning this into a social Leaderboard result by getting your friends’ player IDs and ranking those scores against your own.

/// <summary>(async) Initiate query for Leaderboard given a Leaderboard Id, User, Stat and aggregation type.</summary>
/// <param name="leaderboardId"> LeaderboardId to query.</param>
/// <param name="userId"> UserId to query.</param>
/// <param name="statName"> The stat to query.</param>
/// <param name="statAggType"> The aggregation type.</param>
public void QueryUsersScore(string leaderboardId, ProductUserId userId, string statName, LeaderboardAggregation statAggType)
    {
        UserScoresQueryStatInfo statInfo;

        _leaderboardId = leaderboardId;
        _statName = statName;
        _statAggType = statAggType;

        statInfo = new UserScoresQueryStatInfo()
        {
            StatName = statName,
            Aggregation = statAggType
        };
        ProductUserId[] userIdArr = { userId };
        UserScoresQueryStatInfo []statInfoArr = { statInfo };
        // Query User Score

        QueryLeaderboardUserScoresOptions options = new QueryLeaderboardUserScoresOptions()
        {
            UserIds = userIdArr,
            StatInfo = statInfoArr,
            LocalUserId = EOSManager.Instance.GetProductUserId()
        };

        LeaderboardsHandle.QueryLeaderboardUserScores(options, null, LeaderboardUserScoresReceivedCallbackFn);
    }

EOS Leaderboards Cache
After the Leaderboard query is returned from the example above, the data is stored in the EOS SDK cache and the LeaderboardUserScoresRecievedCallbackFn callback is called (see example below).

We use this callback function to go through the cache and get all the Leaderboard records. In this example we only have the one score as we only searched for the current player’s record.

private void LeaderboardUserScoresReceivedCallbackFn(OnQueryLeaderboardUserScoresCompleteCallbackInfo data)
{
    if (data == null)
    {
        Debug.LogError("Leaderboard (LeaderboardUserScoresReceivedCallbackFn): data is null");
        return;
    }

    if (data.ResultCode != Result.Success)
    {
        Debug.LogErrorFormat("Leaderboard (LeaderboardUserScoresReceivedCallbackFn): Query User Scores error: {0}", data.ResultCode);
        return;
    }
    Debug.Log("Leaderboard (LeaderboardUserScoresReceivedCallbackFn): Query User Scores Complete");

    if (!string.IsNullOrEmpty(_statName) && !string.IsNullOrEmpty(_leaderboardId))
    {
        Debug.Log("LeaderboardId:"+_leaderboardId+", StatName = " + _statName);
        GetLeaderboardUserScoreCountOptions options = new GetLeaderboardUserScoreCountOptions()
        {
            StatName = _statName
        };

        uint userScoresCount = LeaderboardsHandle.GetLeaderboardUserScoreCount(options);
        Debug.Log("userScoresCount = " + userScoresCount);

        CopyLeaderboardUserScoreByIndexOptions userScoreOptions = new CopyLeaderboardUserScoreByIndexOptions()
        {
            StatName = _statName
        };

        for (uint userScoreIndex = 0; userScoreIndex < userScoresCount; userScoreIndex++)
        {
            // User Scores
            userScoreOptions.LeaderboardUserScoreIndex = userScoreIndex;

            Result result = LeaderboardsHandle.CopyLeaderboardUserScoreByIndex(userScoreOptions, out LeaderboardUserScore leaderboardUserScore);

            if (result != Result.Success)
            {
                Debug.LogErrorFormat("Leaderboard (CacheLeaderboardUserScores): CopyLeaderboardUserScoreByIndex {0} failed with result {1}", userScoreIndex, result);
                break;
            }

            uint position = userScoreIndex + 1;
            Debug.Log(String.Format("UserId: {0}. Rank: {1}. Score: {2}", leaderboardUserScore.UserId, position, leaderboardUserScore.Score));        
        }
    }
}

This flow is a little different from what you are used to from GameSparks but it is handy to know that this data is cached if you need to reload the page.

You can see the output of our Leaderboard Record example below…

Leaderboard Partitions

Epic Online Service
With GameSparks, leaderboards are partitioned automatically. EOS requires leaderboards to be set up ahead of time. This does not mean that we cannot make partitioned leaderboards in EOS however. We can create these leaderboards with a little preparation.

As mentioned in the previous section on leaderboards here players don't post their scores to leaderboards directly. Instead, they update stats that are tied to leaderboards and when these stats change, their scores on the Leaderboard are updated too.

This means that we can recreate some basic partitioned leaderboards by recreating the Leaderboard we need for each partition key.

In this example we want a global, EU and US partition for our ‘HighScore’ Leaderboard so we create 2 new leaderboards with the partition keys we want, and each of these new leaderboards will use the HighScore Stat.

Back in our client, we can use the same code we created to ingest stats from the previous topic on leaderboards. When our players update the HighScore Stat, it will automatically perform the update on all leaderboards using that stat.

Achievements

Achievements work in a similar way to the leaderboards we have seen in other topics as they are automatically updated when a Stat is updated instead of being modified directly. This means that you will be able to copy any Leaderboard triggered achievements from your GameSparks instance to EOS without any problems.

To see how to create and update a Stat check out the Creating New Stats and Posting Scores section of our topic on Leaderboards here.

Setup
In order to set up an Achievement we must visit the Developer Dashboard. Open up the Game Services menu from the left-hand side of the portal and select Achievements.

Click on the Create New button.

Here is where you can associate a Stat with your new Achievement.

The value you add to the field to the right of the Stat name will become the target your player needs to reach in order to unlock this Achievement.

For our example we are reusing the HighScore Stat which we tied to our Leaderboards in previous topics. In the example above we will unlock this Achievement if the player posts more than 50 for their HighScore Stat.

After clicking on the Next button you will need to fill in all the appropriate fields including a locked and unlocked image for this Achievement.

In GameSparks we don't need to add locked and unlocked icons so you can add some placeholders for now.

Interface Setup
As we have seen in some other topics, all APIs relating to a specific service go through an interface or handler so the first thing we need to do is initialize this handler.

While we are setting this up we also want to subscribe to the notification that is fired when stats reach their target value and an Achievement is unlocked.

We can see an example of this below for a generic class we called AchievementsManager...

public class AchievementsManager : IEOSSubManager
{
    AchievementsInterface achievementHandler;
    public AchievementsManager()
    {
        achievementHandler = EOSManager.Instance.GetEOSPlatformInterface().GetAchievementsInterface();
        AddNotifyAchievementsUnlockedV2Options options = new AddNotifyAchievementsUnlockedV2Options { };
        achievementHandler.AddNotifyAchievementsUnlockedV2(options, null, AchievementUnlocked);
    }

Achievement-Unlocked Callback
When an Achievement is unlocked the Achievementunlocked callback method is invoked as in the code snippet below. Here you can put whatever update code you need to show a popup or other code to indicate to the player that the Achievement has been unlocked.

/// <summary> Achievement Unlocked.</summary>
/// <param name="data"> The unlocked achievement’s data.</param>
private void AchievementUnlocked(OnAchievementsUnlockedCallbackV2Info data)
{
    UnityEngine.Debug.Log("AchievementUnlocked. Do Something Here: " + data.AchievementId);
}

In this example we will simply log the event to our console so we can quickly test that the Achievement and Stat is working.

Verifying Achievements
We can also check that a player has unlocked an Achievement using the Developer Dashboard.

In the Achievements section of the Game Services menu you can click on the Player Lookup button at the top-right of the window.

You will then be able to enter a player ID to view all that player’s Achievements, locked or unlocked. You can also manually unlock or lock their achievements and reset them if needed.

Achievement Definitions
In order to see the full details of your player Achievements you must first send a request to get all the Achievement definitions. Without this request you can still get your player’s Achievements but they will be lacking some information so this step is optional depending on what you need from the achievement notification.

Once you have fetched the Achievement definitions the SDK will store these locally in the cache which can then be accessed directly. This saves you needing to re-fetch the definitions multiple times during a session This tutorial will not go through accessing that cache directly but more info can be found here.

This can be thought of as an alternative to the GameSparks ListAchievementsRequest only that once, cached you will not have to request the achievement list again.

These cached definitions are used when we want details on the Achievements themselves such as the display name, Stat and value.

/// <summary>(async) Initiate query for the Achievement Definitions.</summary>
/// <param name="productUserId"> The user making the query.</param>
public void QueryAchievementDefinitions(ProductUserId productUserId)
{
    UnityEngine.Debug.Log("Querying Achievement Definitions");
    var options = new QueryDefinitionsOptions
    {
        LocalUserId = productUserId
    };

    achievementHandle.QueryDefinitions(options, null, (OnQueryDefinitionsCompleteCallbackInfo data) =>
    {
        if (data.ResultCode != Result.Success)
        {
            UnityEngine.Debug.Log("unable to query achievement definitions: " + data.ResultCode.ToString());
        }
        else
        {
            UnityEngine.Debug.Log("Querying Achievement Definitions Complete: " + data.ResultCode.ToString());
        }
    });
}

Getting Player Achievements
Now that we have the definitions in cache we want to get the current status of the players achievements.

To do this we need to call the QueryPlayerAchievements() method.

/// <summary>(async) Initiate query for the Player's Achievements.</summary>
/// <param name="productUserId"> The user making the query.</param>
public void QueryPlayerAchievements(ProductUserId productUserId)
{
    UnityEngine.Debug.Log("Begin query player achievements for " + productUserId);
    QueryPlayerAchievementsOptions options = new QueryPlayerAchievementsOptions
    {
        TargetUserId = productUserId,
        LocalUserId = productUserId
    };

    achievementHandle.QueryPlayerAchievements(options, null, (OnQueryPlayerAchievementsCompleteCallbackInfo data) =>
    {
        UnityEngine.Debug.Log("QueryingPlayerAchievements");
        if (data != null)
        {
            UnityEngine.Debug.Log("data.ResultCode = " + data.ResultCode);
            if (data.ResultCode != Epic.OnlineServices.Result.Success)
            {
                UnityEngine.Debug.Log("Error after query player achievements: " + data.ResultCode);
            }
            else
            {
                IterateAllPlayerAchievements(productUserId, data);
            }
        }
    });
}

Iterate Player Achievements
Now that we have the player’s achievements in cache we need to iterate through them to get each Achievement’s current state. In this example we simply print out the Achievement ID and progress.

/// <summary> Iterate through the Player's Achievements.</summary>
/// <param name="productUserId"> The user making the query.</param>
/// <param name="data"> The achievement callback data.</param>
private void IterateAllPlayerAchievements(ProductUserId productUserId, OnQueryPlayerAchievementsCompleteCallbackInfo data)
{
    var achievementCountOptions = new GetPlayerAchievementCountOptions
    {
        UserId = productUserId
    };

    uint achievementCount = achievementHandle.GetPlayerAchievementCount(achievementCountOptions);
    var playerAchievementByIndexOptions = new CopyPlayerAchievementByIndexOptions
    {
        AchievementIndex = 0,
        TargetUserId = productUserId,
        LocalUserId = productUserId
    };

    UnityEngine.Debug.Log("Fetching achievments. achievementCount = " + achievementCount);
    var collectedAchievements = new List<PlayerAchievement>();
    for (uint i = 0; i < achievementCount; ++i)
    {
        PlayerAchievement playerAchievement;
        playerAchievementByIndexOptions.AchievementIndex = i;
        var copyResult = achievementHandle.CopyPlayerAchievementByIndex(playerAchievementByIndexOptions, out playerAchievement);
        if (copyResult != Result.Success)
        {
            UnityEngine.Debug.Log("Failed to copy player achievement : " + copyResult);
            continue; // TODO handle error
        }
        collectedAchievements.Add(playerAchievement);
    }
    foreach (PlayerAchievement achievement in collectedAchievements)
    {
        UnityEngine.Debug.Log("AchievementId = " + achievement.AchievementId);
        UnityEngine.Debug.Log("Progress = " + achievement.Progress);
    }
}

We can see from the logs that our progress is shown as a value out of 1. This is the percentage value of our current Stat towards the progress of the Achievement. For example, if the trigger value was 50 for our HighScore and our current HighScore value is 10, then the value would be 0.2 for 20%.

Note - Stats can take some time to process and update on the backend. This also means that achievements also take a while to unlock. Keep this in mind while testing.

Player Manager

EOS provides tools for inspecting and editing many of the player attributes we have seen in other topics. While GameSparks provided a dashboard where all player features could be viewed and updated in one window, with EOS you will find these features in the relevant section of the Developer Dashboard associated with that feature as we will see below.

Player Stats
To view your player’s stats from the Developer Portal select Game Services from the left-hand side menu and then click on the Stats option. Click the Reset Player Stats button, don't worry, this will not reset your player’s stats. That will be another option you need to select. Before that option is available you will be able to view your player’s stats.

This will bring up a new menu where you can enter a player’s ID. Simply enter the player's ID and click the Search button.

Player Achievements
To see all of a player’s achievements, go to the Developer Portal and under the Game Services section click on the Achievements option. Click the Player Lookup button and enter the player's ID.

Click search and you will be able to see all the players achievements. From here you can view their achievements and even reset or unlock them manually.

Downloadables

EOS has an alternative to GameSparks Downloadables called Title Storage. These are files uploaded to the Developer Portal and are made accessible to any user who is logged in through EOS with access to Game Services.

Title Storage is different to Player Data Storage as Title Storage is not specific to any player. For an alternative to GameSparks uploadables you might consider something like EOS Player Data Storage.

Note - Player Data Storage should not be used as an alternative to GameSparks player objects. It is intended to be used for storing save-files, not player configuration data or JSON as in GameSparks.

Setup
First we need to make sure our client policy has access to the Title Storage feature. We can check this from the Developer Portal. Click on the Product Settings options on the left-hand side menu and then scroll down to select your client policy. You should see the options currently selected in the Client policy details menu.

Next we need to set the encryption key for your client. This ensures that all data is encrypted between server and client. This needs to be a hex-key of less than 64 characters. Developers are responsible for the creation and storage of this encryption key. You can find a random hex generator online to create this key for you.

After generating the hex key you need to add it to the Unity project. From the Unity editor menu bar select Tools -> EpicOnlineServicesConfigEditor and add the encryption key.

Next head to the Developer Portal, select Game Services from the left-hand side menu and then Title Storage. Click the Add New File button.

You will see a popup asking you to enter your encryption key here and then proceed to select the file you wish to be accessible to the client through the SDK.

Initialize The Storage Interface
As was the case with some of the other topics we explored, in order to access specific features for the EOS Unity SDK we first need to initialize an interface for that feature.

Back in the Unity project create a new C# script and enter the following code to initialize the Title Storage Interface. We will need to add a few variables to track the file and download stream.

public class MyTitleStorageManager : IEOSSubManager
{
    TitleStorageInterface titleStorageHandle;
    public const uint MAX_CHUNK_SIZE = 4 * 4 * 4096;
    private string CurrentTransferName;
    private float CurrentTransferProgress;  
    private Dictionary<string, string> StorageData = new Dictionary<string, string>();
    private Dictionary<string, EOSTransferInProgress> TransfersInProgress = new Dictionary<string, EOSTransferInProgress>();

    public MyTitleStorageManager ()
    {
        titleStorageHandle = EOSManager.Instance.GetEOSPlatformInterface().GetTitleStorageInterface();
    }

We will also need a class to represent our file transfers as below...

public class EOSTransferInProgress
{
    public bool Download = true;
    public uint TotalSize = 0;
    public uint CurrentIndex = 0;
    public List<char> Data = new List<char>();

    public bool Done()
    {
        return TotalSize == CurrentIndex;
    }
}

Now we are all set up to start downloading some of our files.

Querying Downloadable Files
We can query the online files to get the metadata associated with the files (names, size, ect..). We will use the QueryFileList() method to do this.

/// <summary>
/// Query the available list of files available for download
/// </summary>
/// <param name="tags">tags associated with the online files</param>
public void QueryFileList(string[] tags)
{
    QueryFileListOptions queryOptions = new QueryFileListOptions();
    queryOptions.ListOfTags = tags;
    queryOptions.LocalUserId = EOSManager.Instance.GetProductUserId();
    titleStorageHandle.QueryFileList(queryOptions, null, OnQueryFileListCompleted);
}

/// <summary>
/// Callback for QueryFileList
/// </summary>
/// <param name="data">Callback data</param>
private void OnQueryFileListCompleted(QueryFileListCallbackInfo data)
{
    if (data == null)
    {
        Debug.LogError("Title storage: OnFileListRetrieved data == null!");
        return;
    }

    if (data.ResultCode != Result.Success)
    {
        Debug.LogErrorFormat("Title storage: file list retrieval error: {0}", data.ResultCode);
        return;
    }

    uint fileCount = data.FileCount;
    Debug.Log("Title storage file list is successfully retrieved. File count = "+ fileCount);

    for (uint fileIndex = 0; fileIndex < fileCount; fileIndex++)
    {
        CopyFileMetadataAtIndexOptions copyFileOptions = new CopyFileMetadataAtIndexOptions();
        copyFileOptions.Index = fileIndex;
        copyFileOptions.LocalUserId = EOSManager.Instance.GetProductUserId();

        titleStorageHandle.CopyFileMetadataAtIndex(copyFileOptions, out FileMetadata fileMetadata);

        if (fileMetadata != null)
        {
            if (!string.IsNullOrEmpty(fileMetadata.Filename))
            {
                Debug.Log("Download complete. File: "+ fileMetadata.Filename);
            }
        }
    }
}

If you test this in the Unity editor, you should see that meta-data displayed in the console.

From here we can download the file directly from EOS instead of downloading the file using our own code as in GameSparks.

Downloading A File
We can use the ReadFile() method to download a file. This method takes two callbacks as parameters (among other data) as well as a callback method in the request itself. The options callbacks are:

● ReadFileDataCallback This callback handles data as it is returned from the request. ● FileTransferProgressCallback This optional callback contains data on the progress of the transfer ● OnFileRecieved

This callback is raised when the download completes.

/// <summary>
/// Download a file from Title Storage to cache
/// </summary>
/// <param name="fileName">Name of the file</param>
public void DownloadFile(String fileName)
{
    Debug.Log("Attempting to download: " + fileName);
    // StartFileDataDownload
    ReadFileOptions fileReadOptions = new ReadFileOptions();
    fileReadOptions.LocalUserId = EOSManager.Instance.GetProductUserId();
    fileReadOptions.Filename = fileName;
    fileReadOptions.ReadChunkLengthBytes = MAX_CHUNK_SIZE;

    fileReadOptions.ReadFileDataCallback = ReceiveData;
    fileReadOptions.FileTransferProgressCallback = OnFileTransferProgressUpdated;

    TitleStorageFileTransferRequest transferReq = titleStorageHandle.ReadFile(fileReadOptions, null, OnFileReceived);

    EOSTransferInProgress newTransfer = new EOSTransferInProgress();
    newTransfer.Download = true;

    TransfersInProgress.Add(fileName, newTransfer);

    CurrentTransferProgress = 0.0f;
    CurrentTransferName = fileName;
}

On File-Received Callback
/// <summary>
/// Download callback
/// </summary>
/// <param name="data">Callback data</param>
private void OnFileReceived(ReadFileCallbackInfo data)
{
    if (data == null)
    {
        Debug.LogError("Title storage: OnReadFileComplete data == null!");
        return;
    }

    if (data.ResultCode != Result.Success)
    {
        Debug.LogErrorFormat("Title storage: OnFileReceived error: {0}", data.ResultCode);
        FinishFileDownload(data.Filename, false, data.ResultCode);
        return;
    }

    FinishFileDownload(data.Filename, true, data.ResultCode);
}
/// <summary>
/// Process the downloaded data
/// </summary>
/// <param name="fileName">Name of the file</param>
/// <param name="success">The success result</param>
/// <param name="result">The result data</param>
public void FinishFileDownload(string fileName, bool success, Result result)
{
    Debug.LogFormat("Title storage: FinishFileDownload '{0}', success = {1}", fileName, success);

    if (!TransfersInProgress.TryGetValue(fileName, out EOSTransferInProgress transfer))
    {
        Debug.LogErrorFormat("[EOS SDK] Title storage: '{0}' was not found in TransfersInProgress.", fileName);
        return;
    }

    if (!transfer.Download)
    {
        Debug.LogError("[EOS SDK] Title storage: error while file read operation: can't finish because of download/upload mismatch.");
        return;
    }

    if (!transfer.Done() || success)
    {
        if (!transfer.Done())
        {
            Debug.LogError("[EOS SDK] Title storage: error while file read operation: expecting more data. File can be corrupted.");
        }

        TransfersInProgress.Remove(fileName);
    }

    string fileData = string.Empty;
    if (transfer.TotalSize > 0)
    {
        fileData = new string(transfer.Data.ToArray());
    }

    StorageData.Add(fileName, fileData);
    Debug.Log("fileData = " + fileData);
    Debug.LogFormat("[EOS SDK] Title storage: file read finished: '{0}' Size: {1}.", fileName, fileData.Length);

    TransfersInProgress.Remove(fileName);
}

If you test this in the Unity editor, you should see that meta-data displayed in the console.

File-Transfer Callback

/// <summary>
/// Callback for transfer progress update
/// </summary>
/// <param name="data">Progress data</param>
private void OnFileTransferProgressUpdated(FileTransferProgressCallbackInfo data)
{
    if (data == null)
    {
        Debug.LogError("Title storage: OnFileTransferProgressUpdated data == null!");
        return;
    }

    if (data.TotalFileSizeBytes > 0)
    {
        if (data.Filename.Equals(CurrentTransferName, StringComparison.OrdinalIgnoreCase))
        {
            CurrentTransferProgress = data.BytesTransferred / data.TotalFileSizeBytes;
        }
        Debug.LogFormat("Title storage: transfer progress {0} / {1}", data.BytesTransferred, data.TotalFileSizeBytes);
    }
}

/// <summary>
/// Callback fired as data comes in
/// </summary>
/// <param name="ReadFileData">Data chunk received</param>
/// <returns></returns>
private ReadResult ReceiveData(ReadFileDataCallbackInfo ReadFileData)
{
    Debug.Log("Received data.");
    if (ReadFileData == null)
    {
        return ReadResult.RrFailrequest;
    }

    string fileName = ReadFileData.Filename;
    byte[] data = ReadFileData.DataChunk;
    uint totalSize = ReadFileData.TotalFileSizeBytes;

    if (data == null)
    {
        Debug.LogError("[EOS SDK] Title storage: could not receive data: Data pointer is null.");
        return ReadResult.RrFailrequest;
    }

    TransfersInProgress.TryGetValue(fileName, out EOSTransferInProgress transfer);

    if (transfer != null)
    {
        if (!transfer.Download)
        {
            Debug.LogError("[EOS SDK] Title storage: can't load file data: download/upload mismatch.");
            return ReadResult.RrFailrequest;
        }

        // First update
        if (transfer.CurrentIndex == 0 && transfer.TotalSize == 0)
        {
            transfer.TotalSize = totalSize;

            if (transfer.TotalSize == 0)
            {
                return ReadResult.RrContinuereading;
            }
        }

        // Make sure we have enough space
        if (transfer.TotalSize - transfer.CurrentIndex >= data.Length)
        {
            char[] chars = Encoding.Default.GetChars(data);  // Must match encoding of file

            transfer.Data.AddRange(chars);
            transfer.CurrentIndex += (uint)data.Length;
            Debug.Log("Amount: "+ (uint)data.Length + " bytes");

            return ReadResult.RrContinuereading;
        }
        else
        {
            Debug.LogError("[EOS SDK] Title storage: could not receive data: too much of it.");
            return ReadResult.RrFailrequest;
        }
    }
    return ReadResult.RrCancelrequest;
}

If you test this in the Unity editor, you should see that meta-data displayed in the console.

Matchmaking

EOS provides matchmaking based on single attributes but they can be any Stat your player is tracking rather than only the ‘skill’ value that GameSparks allows, and you can set these values to be within certain conditions.

While configurable thresholds surrounding these Stats are not available it is possible to create these thresholds yourself using a timer in Unity.

Matchmaking in EOS is done through their Sessions Interface. We will therefore need to create a session, add our player’s attributes and register our player as a Host. This session can then be found by other players who can search with specific criteria that satisfy the session’s configuration.

This flow is different from GameSparks matchmaking where we are familiar with a matchmaking pool or a matchmaking ticket system as other services use. With EOS, we are creating something like a lobby or session pool, where the host-player must submit the session with their desired attributes which can then be found and joined by other players.

This means that there is always a host player creating the session and other players have the choice of joining that session or creating their own new session with unique parameters.

Setup
We have a couple of things to set up in the Developer Dashboard before we can get started registering sessions and joining them.

Our current client policy (created in the Authentication topic here was set up to have all the features we needed so far but it does not come with what we need for matchmaking by default.

Select the Project Settings option from the left-hand side menu of the Developer Dashboard and select the client tab. Scroll down to the bottom and click the Add New Client button.

Name your new policy and select the Custom option from the Policy Type dropdown menu.

Selecting the Custom policy-type will allow you to select specific features. We need to turn on the Matchmaking feature and, for simplicity, select all the Allowed Actions.

Note - You may want to restrict some actions here at a later date for security reasons.

Save your new policy and attach this new policy to your client. We are now ready to start making requests from our Unity project.

Session Manager Interface
As was the case with some of the other topics we explored, in order to access specific features for the EOS Unity SDK we first need to initialize an interface for that feature.

In your Unity project create a new script that inherits from IEOSSubManager and import the session interface. You can see in the example below that we have added in a few extra variables and an enum which we will be using later in the tutorial.

public class MySessionManager : IEOSSubManager
{
    SessionsInterface sessionInterface;
    SessionSearch currentSearch;
    string sessionName; 
    public ulong SessionJoinGameNotificationHandle = 0;
    enum ValueType
    {
        String,
        Bool,
        Int,
        Double
    }

    public MySessionManager()
    {
        sessionInterface = EOSManager.Instance.GetEOSPlatformInterface().GetSessionsInterface();
        currentSearch = new SessionSearch();
    }

Creating A Session
Next we need to create a session. There are many options for creating a session but for this example we will keep things simple.

After creating the session we need to add the following parameters:

  1. The permissions for the session.
  2. If joining an in-progress session is allowed.
  3. If invites to the session are allowed.
  4. What attributes are needed to find this session.

Attributes are what we will be using to find sessions based on what thresholds we want to define.

In this example we will use a string: bucketId and an integer: score. Once these modifications are made we update the session.

This session is now live and can be searched for.

Note - Only the host (user that created the session) can register players to the session.

Step 1 - Create New Session

/// <summary>
/// Create a session which can then be searched for and can be used to register players.
/// </summary>
/// <param name="bucketId">Partition for the session. Eg "GameType:Region:Level"</param>
/// <param name="score">The players score</param>
/// <param name="maxPlayers">Max number of players allowed to join the session</param>
/// <param name="sessionName">The name of the session</param>
/// <param name="presenceEnabled">Is the session public or private</param>
/// <returns></returns>
public bool CreateSession(string bucketId, int score, int maxPlayers, string sessionName, bool presenceEnabled)
{
    CreateSessionModificationOptions createOptions = new CreateSessionModificationOptions() 
    { 
        BucketId = bucketId,
        MaxPlayers = 2,
        SessionName = sessionName,
        LocalUserId = EOSManager.Instance.GetProductUserId(),
        PresenceEnabled = presenceEnabled
    };

    Result result = sessionInterface.CreateSessionModification(createOptions, out SessionModification sessionModificationHandle);

    if (result != Result.Success)
    {
        Debug.LogErrorFormat("Session Matchmaking: could not create session modification. Error code: {0}", result);
        return false;
    }

    SessionModificationSetPermissionLevelOptions permisionOptions = new SessionModificationSetPermissionLevelOptions()
    {
        PermissionLevel = OnlineSessionPermissionLevel.PublicAdvertised
    };

    result = sessionModificationHandle.SetPermissionLevel(permisionOptions);

    if (result != Result.Success)
    {
        Debug.LogErrorFormat("Session Matchmaking: failed to set permissions. Error code: {0}", result);
        sessionModificationHandle.Release();
        return false;
    }

    SessionModificationSetJoinInProgressAllowedOptions jipOptions = new SessionModificationSetJoinInProgressAllowedOptions()
    {
        AllowJoinInProgress = false
    };

    result = sessionModificationHandle.SetJoinInProgressAllowed(jipOptions);

    if (result != Result.Success)
    {
        Debug.LogErrorFormat("Session Matchmaking: failed to set 'join in progress allowed' flag. Error code: {0}", result);
        sessionModificationHandle.Release();
        return false;
    }

    SessionModificationSetInvitesAllowedOptions iaOptions = new SessionModificationSetInvitesAllowedOptions()
    {
        InvitesAllowed = false
    };

    result = sessionModificationHandle.SetInvitesAllowed(iaOptions);

    if (result != Result.Success)
    {
        Debug.LogErrorFormat("Session Matchmaking: failed to set invites allowed. Error code: {0}", result);
        sessionModificationHandle.Release();
        return false;
    }

    // Set Bucket Id
    AttributeData aData = CreateAttribute("bucket", bucketId, ValueType.String);
    AddAttributeToSession(sessionModificationHandle, aData);

    // Set Other Attributes
    aData = CreateAttribute("Score", score+"", ValueType.Int);
    AddAttributeToSession(sessionModificationHandle, aData);

    UpdateSessionOptions updateOptions = new UpdateSessionOptions()
    {
        SessionModificationHandle = sessionModificationHandle
    };

    sessionInterface.UpdateSession(updateOptions, null, OnUpdateSessionCompleteCallback_ForCreate);

    sessionModificationHandle.Release();
    Debug.Log("Session Created.");

    return true;
}

Step 2 - Updating A Session

On creating this session we used the following methods:

/// <summary>
/// Used to create an attribute
/// </summary>
/// <param name="_key">The key</param>
/// <param name="_value">The value. Read in as a string but will be parsed using the _valueType</param>
/// <param name="_valueType">The value type. Used to parse the value</param>
/// <returns></returns>
private AttributeData CreateAttribute(string _key, string _value, ValueType _valueType)
{
    AttributeData attrData = new AttributeData();
    attrData.Key = _key;
    attrData.Value = new AttributeDataValue();

    switch (_valueType)
    {
        case ValueType.String:
            attrData.Value.AsUtf8 = _value;
            break;
        case ValueType.Int:
            attrData.Value.AsInt64 = long.Parse(_value);
            break;
        case ValueType.Double:
            attrData.Value.AsDouble = double.Parse(_value);
            break;
        case ValueType.Bool:
            attrData.Value.AsBool = bool.Parse(_value);
            break;
    }
    return attrData;
}
/// <summary>
/// Adding the attribute and calling the session modification method
/// </summary>
/// <param name="_sessionModificationHandle">The handle used to modify the session</param>
/// <param name="attrData">The attribute to add</param>
/// <returns></returns>
private bool AddAttributeToSession(SessionModification _sessionModificationHandle, AttributeData attrData)
{
    SessionModificationAddAttributeOptions attrOptions = new SessionModificationAddAttributeOptions()
    {
        SessionAttribute = attrData,
        AdvertisementType = SessionAttributeAdvertisementType.Advertise
    };

    Result result = _sessionModificationHandle.AddAttribute(attrOptions);

    if (result != Result.Success)
    {
        Debug.LogErrorFormat("Session Matchmaking: failed to set a bucket id attribute. Error code: {0}", result);
        _sessionModificationHandle.Release();
        return false;
    }
    return true;
}

When a session is updated it will raise the OnUpdateSessionCompleteCallback_ForCreate callback so that you can get notification of changes to your session.

/// <summary>
/// Callback for the the session update 
/// </summary>
/// <param name="data">callback data</param>
private void OnUpdateSessionCompleteCallback_ForCreate(UpdateSessionCallbackInfo data)
{
    if (data == null)
    {
        Debug.LogError("Session Matchmaking (OnUpdateSessionCompleteCallback_ForCreate): data is null");
        return;
    }

    bool success = (data.ResultCode == Result.Success);

    if (success)
    {
        Debug.Log("Session Name = "+ data.SessionName + "Session Id = " + data.SessionId);
        ProductUserId prodUserId = EOSManager.Instance.GetProductUserId();
        Debug.Log("prodUserId = "+ prodUserId.ToString());
        if (prodUserId != null)
        {
            // Register session owner
            sessionName = data.SessionName;
            RegisterPlayer( prodUserId.ToString());
        }
        else
        {
            Debug.LogError("Session Matchmaking (OnUpdateSessionCompleteCallback_ForCreate): player is null, can't register yourself in created session.");
        }
    }
    else
    {
        Debug.LogErrorFormat("Session Matchmaking (OnUpdateSessionCompleteCallback): error code: {0}", data.ResultCode);
    }
}

/// <summary>
/// Register a player to the session
/// </summary>
/// <param name="_prodUserId">The players producer Id as a String</param>
public void RegisterPlayer(string _prodUserId)
{
    RegisterPlayersOptions registerOptions = new RegisterPlayersOptions()
    {
        SessionName = sessionName,
        PlayersToRegister = new ProductUserId[] { ProductUserId.FromString(_prodUserId) }
    };

    sessionInterface.RegisterPlayers(registerOptions, null, (RegisterPlayersCallbackInfo registerPlayersData) =>
    {
        if (registerPlayersData == null)
        {
            Debug.LogError("Session Matchmaking (OnRegisterCompleteCallback): data is null!");
        }

        if (registerPlayersData.ResultCode != Result.Success)
        {
            Debug.LogErrorFormat("Session Matchmaking (OnRegisterCompleteCallback): error code: {0}", registerPlayersData.ResultCode);
        }
        else
        {
            Debug.Log("Player successfully registered: " + _prodUserId);
        }
    });
}

Checking Live Sessions
Now that we have a session active you can verify that it is live using the Developer Portal.

Click on the Game Services option of the left-hand side menu of the Developer Portal and matchmaking and click the deployment you want to search in. By default, this will be “Release” unless you have set up a dev or staging deployment yourself.

This will bring up a search menu as you can see below.

You can enter specific session details or you can just click on the “Search” button to get a list of all active sessions.

Notice we have one player registered. This is the session Host.

Searching For Sessions
Next we want other players to be able to search for this session in our client.

Before we can do that though, we need to be able to set attributes into the session-search object we will create. Below is an example of how we can achieve this.

/// <summary>
/// Adding the attribute and calling the session modification method on the search
/// </summary>
/// <param name="_sessionSearchHandle">The handle used to modify the sessionSearch</param>
/// <param name="_comparison">The comparison operator we want to apply to the attribute</param>
/// <param name="attrData">The attribute to query for</param>
/// <returns></returns>
private bool SetSearchAttribute(SessionSearch _sessionSearchHandle,  ComparisonOp _comparison, AttributeData attrData)
{
    SessionSearchSetParameterOptions paramOptions = new SessionSearchSetParameterOptions();
    paramOptions.ComparisonOp = _comparison;
    paramOptions.Parameter = attrData;

    Result result = _sessionSearchHandle.SetParameter(paramOptions);

    if (result != Result.Success)
    {
        Debug.LogErrorFormat("Session Matchmaking: failed to update session search with bucket id parameter. Error code: {0}", result);
        return false;
    }
    return true;
}

Now we can actually search for sessions with our given attributes. Below is an example of how we can search for a live session. We are passing in the bucketId and score attributes in this example.

/// <summary>
/// Search for a live session using set parameters
/// </summary>
/// <param name="bucketId">The session partition</param>
/// <param name="score">The score we want to search for</param>
/// <param name="range">How far away from the score we deem an acceptable distance</param>
public void SearchSession(string bucketId, int score, int range)
{
    // Clear previous search
    currentSearch.Release();

    CreateSessionSearchOptions searchOptions = new CreateSessionSearchOptions
    {
        MaxSearchResults = 10
    };

    Result result = sessionInterface.CreateSessionSearch(searchOptions, out SessionSearch sessionSearchHandle);

    if (result != Result.Success)
    {
        Debug.LogErrorFormat("Session Matchmaking: failed to create session search. Error code: {0}", result);
        return;
    }

    currentSearch = sessionSearchHandle;
    AttributeData aData = CreateAttribute("bucket", bucketId, ValueType.String);
    SetSearchAttribute(currentSearch, ComparisonOp.Equal, aData);

    int lowScore = score - range;
    int highScore = score + range;

    aData = CreateAttribute("Score", highScore+"", ValueType.Int);
    SetSearchAttribute(currentSearch, ComparisonOp.Lessthanorequal, aData);

    aData = CreateAttribute("Score", lowScore+"", ValueType.Int);
    SetSearchAttribute(currentSearch, ComparisonOp.Greaterthanorequal, aData);

    SessionSearchFindOptions findOptions = new SessionSearchFindOptions();
    findOptions.LocalUserId = EOSManager.Instance.GetProductUserId();

    sessionSearchHandle.Find(findOptions, null, (SessionSearchFindCallbackInfo data) =>
    {
        if (data == null)
        {
            Debug.LogError("Session Matchmaking (OnFindSessionsCompleteCallback): data is null!");
        }

        if (data.ResultCode != Result.Success)
        {
            Debug.LogErrorFormat("Session Matchmaking (OnFindSessionsCompleteCallback): error code: {0}", data.ResultCode);
        }
        else
        {
            OnSearchResultsReceived();
        }
    });
}

On Session Found
As with GameSparks matchmaking, finding sessions with EOS can take some time so a callback will be raised if any results have been found.

/// <summary>
/// Callback for the search
/// </summary>
private void OnSearchResultsReceived()
{
    Debug.Log("Search results Received");
    SessionSearchGetSearchResultCountOptions searchResultOptions = new SessionSearchGetSearchResultCountOptions();

    if (currentSearch == null)
    {
        Debug.LogError("Session Matchmaking (OnSearchResultsReceived): CurrentSearch is null");
        return;
    }

    uint numSearchResult = currentSearch.GetSearchResultCount(new SessionSearchGetSearchResultCountOptions());
    Debug.Log("GetSearchResultCount = " + numSearchResult);

    SessionSearchCopySearchResultByIndexOptions indexOptions = new SessionSearchCopySearchResultByIndexOptions();

    for (uint i = 0; i < numSearchResult; i++)
    {
        indexOptions.SessionIndex = i;

        Result result = currentSearch.CopySearchResultByIndex(indexOptions, out SessionDetails sessionHandle);

        if (result == Result.Success && sessionHandle != null)
        {
            result = sessionHandle.CopyInfo(new SessionDetailsCopyInfoOptions(), out SessionDetailsInfo sessionInfo);

            if (result == Result.Success)
            {
                Debug.Log("Search Success. Id = " + sessionInfo.SessionId);
                sessionName = sessionInfo.SessionId;
                JoinSession(sessionHandle);
                break; // we are just getting the first session that matches our criteria. You could get them all and filter for the best fit.
            }
        }
    }
}

You should see the results logged in your Unity console along with the session ID.

Joining A Session
Now that we have found an active session we can have players join it using the JoinSession() method of the Session Interface class as below...

/// <summary>
/// Joins the session
/// </summary>
/// <param name="sessionHandle">Handle for the session</param>
public void JoinSession(SessionDetails sessionHandle)
{
    JoinSessionOptions joinOptions = new JoinSessionOptions()
    {
        SessionHandle = sessionHandle,
        SessionName = sessionName,
        LocalUserId = EOSManager.Instance.GetProductUserId(),
        PresenceEnabled = true
    };
    sessionInterface.JoinSession(joinOptions, null, OnJoinSessionListener);
}
/// <summary>
/// Callback for JoinSession
/// </summary>
/// <param name="data"></param>
private void OnJoinSessionListener(JoinSessionCallbackInfo data) // OnJoinSessionCallback
{
    if (data == null)
    {
        Debug.LogError("Session Matchmaking (OnJoinSessionListener): data is null!");
        return;
    }

    else if (data.ResultCode != Result.Success)
    {
        Debug.LogErrorFormat("Session Matchmaking (OnJoinSessionListener): error code: {0}", data.ResultCode);
        return;
    }
    else
    {
        Debug.Log("Session Matchmaking: joined session successfully.");
    }
}

You should be able to see in the console of the Unity editor if your player joined the session.

Registering The Player
Once the player has found and joined a session they can access the session details which contain the host’s address.

Note - From this point on we deviate from GameSparks matchmaking we are instead talking about how we can start a multiplayer session so we won't go into too much detail on how this works as you would have your own multiplayer systems in place that you will need to transition.

Using the host-address you can send a message to the host through your own networking layer. This message would need to contain the productUserId of the user looking to connect.

ProductUserId productUserId = EOSManager.Instance.GetProductUserId();

The Host can then register the player using the Register method we created above in the Creating A Session section. Once this is completed you can verify the player is registered using the Developer Portal to search for the active session.