Introduction

Feature Parity
Leaderboards
Teams
Currency
Virtual Goods
Authentication
Achievements Configurable
Push Notifications
Chat
Matchmaking
Realtime Servers
Downloadables
Uploadables
Segments
Email
Permissions & Groups
Cloud-Code
Database Access
Analytics
Snapshots
Data Explorer
Player Management

Beamble is a very interesting alternative to other platforms when it comes to transitioning your game and future plans for your backend.

Traditionally you might need a member of your team to specialize in backend development or have knowledge of databases and scaling concerns associated with writing backend code. With Beamable, your entire backend is created, managed and deployed from the Unity3D editor. All code is written in C# meaning your frontend developers can contribute to the backend without the need for specialized team members. The SDK is easy to integrate and deployments can be made by the click of a button.

While this makes for a very nice workflow for games which are already developed with Unity, it does mean that if your game is using Cloud-Code will need to rewrite your backend code in C#. While Beamable provides support for this process here and we will cover specifics on how to achieve this throughout these topics it is still something to consider before making your migration plan.

This series starts with the Authentication Basics here. You can use the guide there to see how to set up a new Beamable project and authenticate players. You can start by registering for Beamble here.

Authentication Basics

There are various different authentication requests available with GameSparks but some of the most commonly used requests are the basic DeviceAuthenticationRequest, RegistrationRequest and AuthenticationRequest (username/password). We will be focusing on these basic authentication requests in this topic.

This topic doesn't cover 3rd party and social authentication. There is another topic here which covers that.

Device Authentication
Device-auth is most often used as anonymous authentication where the player is not required to submit any credentials before gaining access to the game. With GameSparks, this request still uses device and OS settings for authentication so the device can be identified, so there is some identification needed but they are provided programmatically.

You can think of that while migrating device-auth calls. If the platform does not have an alternative, but does have username & password, you can use some hashed device details like the appId or mac address as a unique Id instead.

Registration
Registration allows a player to be created with a username and password for their account so they can log in on multiple devices.

Authentication Request
The basic auth-request just uses username & password. It requires registration before the details are valid, unlike device authentication which automatically signs you in and creates the player’s account.

Note - Not all destination platforms will have the same kinds of authentication requests as GameSparks has so we will try to cover these 3 forms of auth as best we can for each platform.

For frontend examples and code-snippets, we will also be focusing on Unity C# examples as it is the most popular engine used with GameSparks.

Beamable handles authentication a bit differently to other platforms. When a new session is created, there is automatically an auth-token issued to the user and the same auth-token can be used in subsequent sessions for frictionless authentication.

In relation to a GameSparks login alternative we will be focusing on the login APIs in C# here. However, Beamable also has prefab elements that can be imported into your project to have auth setup automatically. You can read more about that feature here.

Authentication & Registration
Since every new session starts when the Beamable SDK initializes, we don't really authenticate or register player accounts directly, your players will be automatically authenticated upon new sessions.

Instead you can attach emails or other credentials to the access token granted to you upon the first session. This is how you would authenticate using another device or retrieve the token after it expired on your device.

/// <summary>
/// Adds a username and password context to the current player's account for later authentication
/// <param name="userName">Player's User name</param>
/// <param name="password">Password</param>
/// </summary>
public async void ConvertAccount(string userName, string password)
{
    var beamable = await Beamable.API.Instance; // We need a reference back to the Beamable instance
    Debug.Log("Registering user [" + beamable.User.id + "]...");
    try
    {
        var user = await beamable.AuthService.RegisterDBCredentials(userName, password);
        Debug.Log("Username & Password Set!...");
    }
    catch (PlatformRequesterException e) when (e.Status == 400 && e.Error.service == "accounts")
    {
        // the email has either already been taken by another account, or the current account already has an email.
        Debug.LogException(e);
    }
}

Now that you have a username and password attached to the account, you will need to switch accounts when you go to another device because, remember, Beamable will automatically log you into a new session on the new device.

/// <summary>
/// Logs the player with the given username and password in
/// <param name="userName">Player's User name</param>
/// <param name="password">Password</param>
/// <returns>User</returns>
/// </summary>
async UserLogin(string userName, string password)
{
    var beamable = await Beamable.API.Instance; // We need a reference back to the Beamable instance
    Debug.Log("Logging player in | userName: [" + userName + "], password: [" + password + "]...");

    var token = await beamable.AuthService.Login(userName, password, false);
    await beamable.ApplyToken(token);
    return beamable.User;
}

Device Authentication
Beamable also allows for device authentication. Something to note is that, as Beamable does a frictionless authentication before any other API calls are allowed, the device authentication route does not override frictionless authentication, you will need to call this authentication request after Beamable initializes as with the regular username and password authentication request.

First we have to register this player as a device-Id user.

async Task RegisterDeviceId()
{
   var beamable = await Beamable.API.Instance;
   User updatedUser = await beamable.AuthService.RegisterDeviceId(); //Registering Device Id to Beamable Account
   beamable.UpdateUserData(updatedUser); //This updates the cached User
}

Once the player has registered using device-Id you can log them in using the LoginDeviceId request.

async Task<User> DeviceIdLogin()
{
   var beamable = await Beamable.API.Instance;

   var token = await beamable.AuthService.LoginDeviceId(); //Login using device id
   await beamable.ApplyToken(token);
   Debug.Log("Logging player in with Device Id...");
   return beamable.User;
}

You can also send a custom device-Id into this request if you want to keep the same device-Id which GameSparks uses (SystemInfo.deviceUniqueIdentifier), however this requires more setup which we will cover below. Create Override Class First we need to create a new class which will override the default device-Id used by Beamable. In the GetDeviceId function, you can provide any alternative to SystemInfo.deviceUniqueIdentifier() you like.

public class CustomBeamableDeviceIdResolver : IDeviceIdResolver
{
    public Promise<string> GetDeviceId()
    {
        return Promise<string>.Successful(“Custom_Id_To_Use”);
    }
}

Register Override Dependency
Next we need to register this override with Beamable so that it replaces the default value when it initializes.

[RegisterBeamableDependencies]
public static void Register(IDependencyBuilder builder)
{
   builder.RemoveIfExists<IDeviceIdResolver>();
   builder.AddSingleton<IDeviceIdResolver>(() => new CustomBeamableDeviceIdResolver());
   Debug.Log("Registered custom device id resolver");
}

With this override registered, the CustomBeamableDeviceIdResolver will be used in the the default Device Id login flow.

Authentication (3rd Party)

In this topic we are going to cover some of the more popular forms of 3rd party authentication. We will be focusing on how to migrate or replicate GameSparks requests specifically, SignInWithAppleConnectRequest, GooglePlayConnectRequest and FacebookConnectRequest.

We assume that developers using these forms of authentication already have developer accounts and apps setup for each of these platforms, but we have added links on how to set those up where we can.

There are of course quite a number of other GameSparks authentication requests but we will focus on these 3 for this topic.

Some of these alternative platforms have their own versions of the above authentication requests and some of them have other options available out-of-the-box. For this topic we will also touch on those different options, but our focus will be on replicating the three requests mentioned above.

For GameSparks, the flow for authentication is as follows:

  1. We get the player/client token for the platform using the client SDK Like the Facebook Unity plugin which can get you a Facebook access token.
  2. We send that token or code to the correct GameSparks request
  3. Under-the-hood, GameSparks sends that token, along with other details configured in the Integrations section of the portal, to a validation endpoint associated with the platform.
  4. The platform API validates the token and returns any relevant information. The important information we need is some way to identify the player by Id from that token.
  5. When we have that identity, we can look for that player and authenticate them locally.

This is the flow we will try to replicate where possible. None of this is too complicated, we really just need to implement the right server-to-server calls to validate the token once we get it.

Note - You could do all this client-side, but this would expose your game’s apiKey, secret or other hidden credentials. This is why we always make this request server-to-server.

Beamable provides support for all three authentication methods listed above along with Steam and GameCenter authentication.

Beamable allows you to automatically integrate your game with these authentication mechanisms through their built-in Account Management Flow UI. However, anyone migrating from GameSparks to Beamable will already have their own UI fully developed and will likely have integrated this with their own 3rd party SDKs, so we won't cover this here.

Instead, we will more generally cover the set up needed to start logging your players in with these 3 different mechanisms in Beamable.

3rd Party Auth Flow
In all cases, the flow for authentication with a 3rd party in Beamable is simple. Once you have the access token/code/id or whatever it is referred to for the specific platform, you need to first register the player and then you can login using that code from that point on.

This is slightly different from what we discussed in the section on Authentication where we mentioned that the Beamable SDK is always logged in as it does an automatic re-login if there are cached user details..

In this case we need to call the register function first...

beamableAPI.AuthService.RegisterThirdPartyCredentials(AuthThirdParty.Apple, authToken);

This will log the player in at the same time, but after this is initially called, we can call the login…

beamableAPI.AuthService.LoginThirdParty(AuthThirdParty.Apple, appleAuthToken, true);

In both cases the AuthThirdParty enum allows us to select which platform we are authenticating with; Apple, Google or Facebook.

SignInWithAppleConnectRequest
Beamable has a tutorial here on how to set things up to work with their Account Management Flow prefabs if you would like to take a look at that route.

In this tutorial we will look at what this process looks like using the Apple-Auth Unity plugin, which is commonly used by Unity developers for the Apple Sign-In authentication route. This package is available here.

Set Up Steps
There is a lot of setup required in order to get your app ready for distribution with Apple, so we aren't going to go through this here and we will assume all of this is setup for anyone wanting to migrate already. However, of anyone new to the process there is a guide to Apple app-distribution with Unity here.

Once you have everything for your app set up all you need to do is build your app for iOS and open up the Xcode project that is generated. In Xcode, follow the steps shown in the Manual entitlements setup section of the plugin guide here. These are important to getting the auth popup to work in your app.

If everything is set up correctly you should see the Apple Sign-In popup when you click on your login button.

Registration & Login
Once signed in, you will be able to get your Identity token from the callback. Something to note here is that this is the IdentityToken and not the AuthorizationCode. It is the token that is needed to authenticate with Beamable.

We won't show all our example code for the Apple-Auth plugin because it is mostly available from the plugin guide, but here is what we are using…

We can now pass it into our registration or login code for Beamable.

public async void BeamableRegistration()
{
   Debug.Log("Starting Beamable Registration...");
   User newUser = await beamableAPI.AuthService.RegisterThirdPartyCredentials(AuthThirdParty.Apple, appleAuthToken);
   Debug.Log($"Email: {newUser.email}");
   Debug.Log($"PID: {newUser.id}");
}

public async void BeamableLogin()
{
   Debug.Log("Starting Beamable Login...");
   TokenResponse resp = await beamableAPI.AuthService.LoginThirdParty(AuthThirdParty.Apple, appleAuthToken, true);
   Debug.Log("access token: {resp.access_token}");
   Debug.Log($"Type: {resp.token_type}");
   Debug.Log($"Expires: {resp.expires_in}");
}

GooglePlayConnectRequest
GooglePlay authentication is very simple. Assuming that you already have the SDK setup and you have been using it up to this point, registration and login can be solved in a couple of lines of code. However, you can check out a guide here on how to set it up from scratch.

With your existing GameSparks integration you would be using the AuthCode in order to authenticate with GameSparks. For Beamable we use the RequestIdToken. Therefore you will have to change the permissions when you configure the client.

void Start()
{
  PlayGamesClientConfiguration config = new PlayGamesClientConfiguration.Builder()
    .RequestServerAuthCode(false)
    .RequestIdToken()
    .Build();
  PlayGamesPlatform.InitializeInstance(config);
  PlayGamesPlatform.DebugLogEnabled = true;
  PlayGamesPlatform.Activate();
}

Registration & Login
Beamable has two calls that can take this token as we already mentioned, LoginThirdParty() and RegisterThirdPartyCredentials(). The only thing to note here is that you can only register this player once. So you could create a check for your registered user or store the access token for future login checks.

public async void GooglePlayReg()
{
    User newUser = await beamableAPI.AuthService.RegisterThirdPartyCredentials(AuthThirdParty.Google, PlayGamesPlatform.Instance.GetIdToken());
    Debug.Log("Player Id: "+newUser.id);
}
public async void GooglePlayLogin()
{
    TokenResponse tokenResp = await beamableAPI.AuthService.LoginThirdParty(AuthThirdParty.Google, PlayGamesPlatform.Instance.GetIdToken());
    Debug.Log("Access Token: "+tokenResp.access_token);
}``

FacebookConnectRequest
Facebook authentication is very easy to migrate. Assuming that you already have the SDK setup and you have been using it up to this point, registration and login can be solved in a couple of lines of code. However, you can check out a guide here on how to set it up from scratch.

We are using the AccessToken.CurrentAccessToken.TokenString value, as you would have used with your GameSparks implementation.

We won't cover all of that setup here but all you need to do is make sure the Facebook SDK has initialized and you have logged in your Facebook user. Once that is complete you can get the access token.

Beamable has two calls that can take this token as we already mentioned, LoginThirdParty() and RegisterThirdPartyCredentials(). The only thing to note here is that you can only register this player once. So you could create a check for your registered user or store the access token for future login checks.

User regUser = await beamableAPI.AuthService.RegisterThirdPartyCredentials(AuthThirdParty.Facebook, AccessToken.CurrentAccessToken.TokenString);
Debug.Log("Player Id: "+regUser.id);

TokenResponse tokenResp = await beamableAPI.AuthService.LoginThirdParty(AuthThirdParty.Facebook, AccessToken.CurrentAccessToken.TokenString);
Debug.Log("Access Token: "+tokenResp.access_token);

You will be able to see your playerId and Beamable’s access token printed in the console.

Leaderboard Basics

There are a variety of different Leaderboard types and configurations available from GameSparks. However, not all these options will be available on other platforms. This topic will cover basic Leaderboard migrations involving setting up a leaderboard, posting scores, returning Leaderboard data and player specific Leaderboard data.

This topic will not cover partitioned Leaderboards. As partitioned Leaderboards are widely used by GameSparks developers, these are covered in their own topic here. It will also not cover all of the functionality of GameSparks leaderboards like social notifications, high scores, top X, etc., as these features are not available to all other platforms. Where available, we will cover some of these topics.

With GameSparks, new Leaderboards are set up through the portal, however, when using Beamable, new Leaderboards are created using the Unity editor plugin. Once you have logged into Beamable through the Unity plugin you will need to go to the Beamable Toolbox which can be opened by going to the menu bar -> Windows -> Beamable -> Open Toolbox.

You can see a guide here on how to get setup with the Beamable plugin.

From here you need to click on the Content option to open up the Content Window.

Leaderboards Setup
From the content window you can select Leaderboard from the dropdown menu and then Create Leaderboards. You will see a new Leaderboard created in the content list.

You can rename this Leaderboard by right clicking or double-clicking on it.

Important
There is one more step to allow us to write to the Leaderboard from C#. You will need to check on the content object for your new Leaderboard and click the checkbox “Write-Self”. This will allow you to post to the Leaderboard using C# from the client.

To publish your changes to your backend you can click on the Publish button on the Content Manager tab. Once the content is published you will get a message saying the content has been successfully published.

If the Leaderboard is published successfully you will be able to see it by logging into the portal. To open your portal from the editor, click on the Toolbox tab and you’ll see the Open Portal button on the right-hand side.

In your portal you should see your new Leaderboard under the Leaderboards tab. You are now ready to start posting scores.

Posting Scores
Next, we will cover how to post to the Leaderboard we just created using the C# API. As we are working in Unity, the first thing you need to do is create a new script and attach it to a new GameObject.

In order to access the Arena Leaderboard you will need to add a LeaderboardRef variable to the top of the script. This will be a private serialized field.

using UnityEngine;
using Beamable.Common.Leaderboards;

public class LBManager : MonoBehaviour
{
   [SerializeField]
   private LeaderboardRef _leaderboardRef = null;

When you let the script compile and go back into the editor you can see this field now appears as a dropdown on your GameObject.

Select your Leaderboard from this list and head back into your script so we can start posting to your Leaderboard.

Note - You technically do not need this LeaderboardRef object as you already know the name of your Leaderboard. You could just hard-code that into the API or add another parameter for the Id you can pass into the PostScore function we are about to create.

We are going to need another new variable which will be our reference back to the Beamable API. We will set this in the Start() function and create a new function for posting scores to the Leaderboard. All this function needs is the score we want to post because we can get the Id of the Leaderboard from that LeaderboardRef object we just set up.

public class LBManager : MonoBehaviour
{
   [SerializeField]
   private LeaderboardRef _leaderboardRef = null;
   private IBeamableAPI beamableAPI = null;

   // Start is called before the first frame update
   async void Start()
   {
       beamableAPI = await Beamable.API.Instance;
   }

   /// <summary>
   /// Posts the given score to the leaderboard
   /// </summary>
   /// <param name="score">The player's current score</param>
   async void PostScore(double score, Dictionary<string, object> stats)
   {
       // Post the score using the LeaderboardService //
       await beamableAPI.LeaderboardService.SetScore(_leaderboardRef.Id, score, stats);
       Debug.Log($"Score Posted {score}");
   }
  }

It is a pretty simple API as you can see. You could add your own logging here but to confirm your score was posted you can always open your portal and click on the Leaderboards tab. You should see one score on your Arena Leaderboard.

The SetScore function updates the player’s score as you can see in this example, however there is also a IncrementScore() function which will increment the score by the set amount instead of updating it. This may be useful in some cases.

Supplemental Data
Now that we have seen how we can post scores to our new Leaderboard, let us cover another GameSparks feature, Supplemental Data.

This feature is also available for Beamable and allows you to post additional information along with your score. An example of this might be your AvatarId, or the ID of the car you were using in a racing game, or other stats for your player which would be useful to have when pulling down the Leaderboard for other players to view.

This can also be added to the SetScore() function as a C# Dictionary object. What we are doing here is essentially creating JSON data which we can append to the score.

This is also very simple to add to our PostScore() function.

/// <summary>
/// Posts the given score to the leaderboard
/// </summary>
/// <param name="score">The player's current score</param>
/// <param name="stats">Supplemental data</param>
async void PostScore(double score, Dictionary<string, object> stats)
{
   // Post the score using the LeaderboardService //
   await beamableAPI.LeaderboardService.SetScore(_leaderboardRef.Id, score, stats);
   Debug.Log($"Score Posted {score}");
}

And you can create objects for this API using the existing C# Dictionary API.

Dictionary<string, object> stats = new Dictionary<string, object>();
stats.Add("avatarId", 12);
stats.Add("displayName", "DifficultyDr");
stats.Add("level", 2);
stats.Add("kills", 212);
stats.Add("elo", 2107);
PostScore(3.0d, stats);

After you post your test to the leaderboard, you will see your stats appear as a JSON string.

Returning Leaderboard Data
The next thing to cover in this topic is the ability to fetch the Leaderboard data we have posted. This is also a very simple API which lets us get a range of Leaderboard data between two ranks, for example the Top10 Leaderboard data would be between 0 and 9.

/// <summary>
/// Returns leaderboard entry data between the given ranks
/// </summary>
/// <param name="fromRank">To first rank to include in the response.</param>
/// <param name="toRank"> The last rank to include in the response</param>
async void GetLeaderboardData(int fromRank, int toRank)
{
   LeaderBoardView leaderboadView = await beamableAPI.LeaderboardService.GetBoard(_leaderboardRef.Id, fromRank, toRank);
   foreach (RankEntry entry in leaderboadView.rankings)
   {
       Debug.Log("PlayerId: "+entry.gt);
       Debug.Log("Rank: "+entry.rank);
       Debug.Log("Score: "+entry.score);
       foreach (RankEntryStat stat in entry.stats)
       {
           Debug.Log("Stat: " + stat.name);
       }
   }
}

So that you can see what parameters you are dealing with in the response we have added logs which you should be able to see in your own console.

Note - Since we are using asynchronous functions here, our GetLeaderboardData function would ideally return this data and not just print it to the console as you want to do something useful with that data like draw your Leaderboard UI. We will show another example of this flow in the next section.

Returning Player Leaderboard Entries
There are also common cases where you do not want to know the full list of Leaderboard data, you just need to know your own player’s Leaderboard rank or entry data. We can use the LeaderboardService API for this as well.

/// <summary>
/// Returns a single entry for the given playerId
/// </summary>
/// <param name="playerId">The player's Id beamableAPI.User.id</param>
/// <returns>RankEntry object containing the player's leaderboard entry details
async Task<RankEntry> GetPlayerLeaderboardEntry(long playerId)
{
   Debug.Log($"Fetching LB Entry for {playerId}");
   RankEntry entry = await beamableAPI.LeaderboardService.GetUser(_leaderboardRef.Id, playerId);
   return entry;
}

You can see from this function it is actually set up to get any player’s Leaderboard entry, not just the current player, so we need to pass that in.

long playerId = beamableAPI.User.id;
var entry = await GetPlayerLeaderboardEntry(playerId);

And that's it! You can check if the entry is returning valid data using the same debug logs as we showed for getting leaderboard data.

Around-Me Leaderboard Request
Beamable does not have an out-of-the-box alternative to GameSparks AroundMeLeaderboardRequest. However, given the two examples we showed here for getting the player’s current rank, and a range of entries you can see it is a simple case of using a range +/- the player’s current rank in order to replicate the AroundMeLeaderboardRequest.

Resetting Leaderboars
REST API calls with beamable are straightforward. The only thing you will need is a bearer token and a scope header to be included with your request.

Admin Authentication
First we are going to need to get the bearer token from Beamable. We can use the basic-auth API here for this. You will also need the ‘X-DE-SCOPE’ header value which is your CID plus your PID joined with a dot in between them as in the example below…

curl --request POST \
     --url https://api.beamable.com/basic/auth/token \
     --header 'Accept: application/json' \
     --header 'Content-Type: application/json' \
     --header 'X-DE-SCOPE: <cid>.<pid> \
     --data '
{
     "username": "<your-admin-email-login>",
     "password": "<your-admin-password>",
     "grant_type": "password"
}
https://docs.beamable.com/reference/basicauthtoken

We can see an example of this using a http client. If your details are correct you should see your access token being returned.

Now, to drop our Leaderboard entries we are going to use the following API. This call is going to need the ‘X-DE-SCOPE’ header and the token, along with the full ID of the Leaderboard, as in the example below.

curl --request DELETE \
     --url https://api.beamable.com/object/leaderboards/leaderboards.Arena_us/entries \
     --header 'Accept: application/json' \
     --header 'Authorization: Bearer bdb26b32-a6c4-4a45-976a-60d8d3ad6448' \
     --header 'X-DE-SCOPE: <CID>.<PID>

Once again, we can check the result in your http client and you should get a valid response. You should be able to double check your Leaderboard has been dropped by checking the entries from the portal.

Leaderboard Partitions

A widely used feature of GameSparks is partitioned Leaderboards. These types of Leaderboards use the same APIs for posting and listing Leaderboard scores as regular Leaderboards, but they can be subdivided into smaller Leaderboards using a partition key.

For example, a world-wide Leaderboard on GameSparks with the short code “Arena” could be subdivided into countries by posting the partition key as the country code. In this case, all entries will go into the world-wide “Arena” Leaderboard, but the scores are also automatically posted to separate Leaderboards for the player’s country, for example “Arena.us”.

For alternative platforms this is not always the case, so in this topic we will cover how to use the native APIs to achieve partitioned Leaderboards, and if the platform does not have an alternative to partitioned Leaderboards, how you can replicate the same functionality so you can migrate your existing GameSparks partitioned Leaderboards.

Creating, posting and listing leaderboard data is not covered in this for each case as these points are covered in the topic on Leaderboard Basics available here.

Beamable does not currently support Partitioned Leaderboards in the same way as GameSparks does. You can only post to one leaderboard at a time. Therefore, posting to a partition required making a new instance of the leaderboard as we did in the previous tutorial here.

GameSparks Partitioned Leaderboards are essentially just separate leaderboards with separate names anyway, so although it does require more setup, it is much the same process.

Following on from the previous tutorial on Leaderboards, we have created some partitioned Leaderboards and synchronized them with the backend.

All we need to do is post to both Leaderboards, one after another. However, we should also check if the partitioned Leaderboard exists before posting, otherwise we’ll get an error when trying to post. To do this we are going to check if the Id can be found in the content manifest.

/// <summary>
/// Checks if the leaderboard with the given Id exists
/// </summary>
/// <param name="lbId">The leaderboard Id</param>
private async Task<bool> CheckLeaderboardExists(string lbId)
{
   // Loads the manifest for all content and checks if the content with the given id exists //
   // manifest is cached to speed up this request //
   return await beamableAPI.ContentService.GetManifest().Map(manifest => manifest.entries.Exists(content => content.contentId == lbId));

   // example 2 //
   // Check if the content is null by trying to load it and catching the exception if it didnt load //
   // try
   // {
   //     var contentFromRef = await beamableAPI.ContentService.GetContent<LeaderboardContent>(lbId);
   //     return true;
   // }
   // catch (Exception e)
   // {
   //     return false;
   // }
}

To check this, we can create a function which can validate that Content exists with that Leaderboard Id.

/// <summary>
/// Posts the given score to the leaderboard and the partition
/// </summary>
/// <param name="paritionId">PartitionId of the leaderboard</param>
/// <param name="score">The player's current score</param>
async void PartitionedLeaderboardPost(string paritionId, double score)
{
   // post to main leaderboard //
   await beamableAPI.LeaderboardService.SetScore(_leaderboardRef.Id, score);
   Debug.Log($"Score [{score}] Posted to {_leaderboardRef.Id}");
   // create full lb name for partition //
   string partitionLBName = _leaderboardRef.Id + "_" + partitionId;
   // check lb exists before posting //
   if (await CheckLeaderboardExists(partitionLBName))
   {
       await beamableAPI.LeaderboardService.SetScore(partitionLBName, score);
       Debug.Log($"Score [{score}] Posted to {partitionLBName}");
   }
   else
   {
       Debug.Log($"No LB exists with Id {partitionLBName}");
   }
}

Note - for this example we will not show posting supplemental data just to make things easier. This is covered in the previous Leaderboards topic.

You can check if your Leaderboard got updated from the portal.

Microservices Example
The above example shows how you can post to a partitioned Leaderboard from the client using two calls. This is a valid alternative to GameSparks partitioned Leaderboards, however, with GameSparks you are always only sending a single request to post to the main Leaderboard and the partition.

Therefore, let us take a look at how we might model the same request using Beamable microservices.

We will not be covering a full tutorial on how to create microservices in this topic as they are already covered in this topic so consult that guide if you are new to Beamable microservices.

For this example, we need a microservice for posting scores, but you might want to turn this into a service for all server-side Leaderboard logic if that suits your needs.

Below is the same example we saw earlier but modified for microservices.

[ClientCallable]
public async void PostScore(string leaderbordId, string paritionId, double score)
{
  // post to main leaderboard //
  await Services.Leaderboards.SetScore(leaderbordId, score);
  Debug.Log($"Score [{score}] Posted to {leaderbordId}");
  // create full lb name for partition //
  string partitionLBName = leaderbordId + "_" + paritionId;
  // check lb exists before posting //
  if (await CheckLeaderboardExists(partitionLBName))
  {
     await Services.Leaderboards.SetScore(partitionLBName, score);
     Debug.Log($"Score [{score}] Posted to {partitionLBName}");
  }
  else
  {
     Debug.Log($"No LB exists with Id {partitionLBName}");
  }
}

You will also need to modify the calls to check that content exists for use in the microservice. This is also simple.

return await Services.Content.GetManifest().Map(manifest => manifest.entries.Exists(content => content.contentId == lbId));

Note - Remember to make sure your server has been built with those new changes and is running.

We then invoke the microservice with the following code and see our debug logs in the console.

_postScoreMS = new PostScoreMSClient();
await _postScoreMS.PostScore(_leaderboardRef.Id, "us", 5d);

There are a few differences between client-side and server side, mainly that we do not have the LeaderboardRef object so we can get the Id of the main Leaderboard. It is therefore passed in as a separate field to the microservice.

The calls to the LeaderboardService are also slightly different because we are using the server API which uses Services.Leaderboards to access the LeaderboardService.

Virtual Currency

Virtual Currency is a basic feature that most alternative platforms provide. The minimum requirement is the ability to create a key/value pair, tied to the player’s account which can be credited and debited to change the player’s balance.

Virtual Currency is usually associated with other features like Achievements and Virtual Goods. Those features should allow Virtual Currency to be granted or consumed on behalf of the player. They should also be accessible through some sort of API so that the developer can choose how they are credited and debited, such as GameSparks’s SparkVirtualCurrency API.

Some of the platforms discussed below include these features, but there are always differences between their implementation and GameSparks’. Keep in mind that crediting/debiting the player directly from the client is not recommended for any platform, so, in some places you may need to use the platform's Cloud-Code alternative to credit and debit the player.

GameSparks has two versions of Virtual Currency. One is the old version of currency which is the numbered currency values that come with every new player account.

The others are the currencies you can set up and name yourself from the configurator.

The only other feature related to Virtual Currency which GameSparks has, is the ability to automatically set a sign-up bonus for each currency. This isn't available for all alternative platforms, but we will show how that can be achieved where possible.

As with most features in Beamable, you create Virtual Currencies as Content through the Content Manager tab.

Once you have given this content a name there are a few other parameters we have to set up. If you click on one of these Virtual Currencies, you can see some options in the Inspector.

An important field here is “Write Self”. This should be un-selected for the normal operation of your game. In general, you do not want users to be able to modify their balance from the client as this would leave your game open for anyone to hack the API and grant themselves Virtual Currency.

Instead, we set this as false, and we’ll show you how you can use microservices to credit/debit player currency.

Sign-Up Bonus
Another option you can see is the equivalent of the GameSparks sign-up bonus. You can set a starting amount for each of your currencies for the player.

Note - Remember to push these changes to the backend before testing to make sure they are available when it comes to calling API requests.

As with other Beamable Content objects, we have multiple ways to reference CurrencyContent through the API. We will show some sample code below with a few options, however, remember that if you have the “Write Self” unclicked, these will not work for you, this is only an example.

private IBeamableAPI beamableAPI = null;

[SerializeField]
public CurrencyRef coinsRef = null;

async void Start()
{
   beamableAPI = await Beamable.API.Instance;

   // get the content reference manually if you know the Id //
   var xpRef = new ContentRef(typeof(CurrencyContent), "currency.XP");

   await beamableAPI.InventoryService.AddCurrency(coinsRef.GetId(), 1);
   await beamableAPI.InventoryService.SetCurrency(xpRef.GetId(), 100);
   // below, for example, you dont need the ContentRef object //
   long gemBal = await beamableAPI.InventoryService.GetCurrency("currency.GEMS");
   Debug.Log("GEMS: "+gemBal);
   // we'll use this to find the player in the portal //
   Debug.Log("PlayerId: "+beamableAPI.User.id);
}

The example above shows how to get the CurrencyContent object for use with the APIs. The coinsRef variable would be set from the editor.

The playerId is included here so that you can grab it and search for that player in the Beamable portal. You can see your updated player balances there.

Example: Leveling System
As already mentioned, we should not be crediting the player directly from the client API, so we are going to cover an example which shows crediting and debiting from the server, along with some server-side logic to control crediting and debiting.

For this example, we will cover a few different points. We are going to create some custom content and use that to define a set of rewards for each level, along with the XP required for each level.

Since this is all going to be using Microservices, we are not going to cover every step required. To see examples of how to create Microservices and custom content, check out the topic on Cloud-Code here.

The JSON content we are reproducing looks something like this, a common example of level-up rules for a GameSparks title.

{
  "levels": [
    {
      "level": 2,
      "xpReq": 1000,
      "rewards": [
        {
          "currType": "COINS",
          "amount": 100
        },
        {
          "currType": "GEMS",
          "amount": 5
        }
      ]
    },
....
  ]
}

Converting this JSON to a ContentObject would look something like below.

We already covered configuring XP, COINS and GEMS as Virtual Currency. We are going to use LEVEL as a Stat on the player’s account. This will come in handy as Stats can be used to target groups of players for all sorts of use cases. We will not cover those examples here, but LEVEL is a good example of where to use a Stat instead of Content Data.

Remember to add the custom content assembly reference to your Microservice (example here).

Another component you might want is a response data-structure. For this example, this structure will contain the player's current level and XP, along with any rewards given and a bool to indicate if the player leveled up. This is a pretty common response for GameSparks developers modeling a similar level-up event with their Cloud-Code events.

We will stick this class into our common GS content folder so we can get it into the Microservice as an assembly reference. Remember to add the custom-content reference to the Microservice before proceeding.

[System.Serializable]
public class GSAddXPResponse
{
   public GSAddXPResponse()
   {
       rewards = new List<LevelUpReward>();
   }
   public int currentLevel;
   public long currentXP;
   public bool hasLeveledUp;
   public List<LevelUpReward> rewards;
   public string error;
}

Now we can start on the Microservice code. As you will have found from other tutorials to this point, you will first need to modify the function to return this GSAddXPResponse. The function will also need to take an integer or a long to add XP. We are also going to add the playerId to this function so we can get the player’s level as a Stat.

[ClientCallable]
public async Task<GSAddXPResponse> GrantXP(long xp, long playerId)
{
   return null;
}

Our code will perform the following steps:

  1. We are going to load the level definitions content file. If we fail to load that definition, we cannot proceed with this example, so we can return an error here if needed.
  2. We will get the player’s current level from the Stats service.
  3. Get the player’s current XP from the inventory service.
  4. Run through the level definitions to see if the player should be leveled up.
  5. Deliver rewards if the player has leveled up.
  6. Add the XP.
  7. Return the response.

[ClientCallable]
public async Task<GSAddXPResponse> GrantXP(long xp)
{
   GSAddXPResponse response = new GSAddXPResponse();
   // declare some params we need for this example //
   string levelStatKey = "LEVEL";
   string xpCurrKey = "currency.XP";
   // we need to following for setting and getting the right stats //
   string domain = "game";
   string access = "public";
   string type = "player";
   // load the level Defs //
   GSLevelsDef levelDefDetails;
   try
   {
       levelDefDetails = (GSLevelsDef) await Services.Content.GetContent("level_defs.levelDefs");
       Debug.Log(levelDefDetails.Id);
   }
   catch (Exception e)
   {
       Debug.LogError(e.Message);
       response.error = e.Message;
       return response;
   }
   Debug.Log($"Fetching Player Level for player {Context.UserId}...");
   int currLevel;
   Dictionary<string, string> stats = await Services.Stats.GetStats(domain, access, type, Context.UserId, new []{ levelStatKey });
   // We might not have the Stat implemented yet, this is a common case when working from JS //
   // So lets check that here //
   if (stats.ContainsKey(levelStatKey))
   {
       currLevel = Int32.Parse(stats[levelStatKey]);
   }
   else
   {
       currLevel = 1;
   }
   Debug.Log($"Current Level: {currLevel}");
   // Lets grab the player's current Xp now //
   long currXp = await Services.Inventory.GetCurrency(xpCurrKey);
   Debug.Log($"Current XP: {currXp}");
   // Now we will go through the level definitions and decide if we have leveled up //
   foreach (LevelDef levelDef in levelDefDetails.levelDefs)
   {
       if (((currXp+xp) >= levelDef.xpReq) && (currLevel < levelDef.nextLevel))
       {
           Debug.Log("Player Leveled Up!");
           response.hasLeveledUp = true; // << add to the response
           // a little validation to make sure we have rewards for this level //
           if (levelDef.rewards.Length > 0)
           {
               // Deliver rewards to player //
               foreach (LevelUpReward reward in levelDef.rewards)
               {
                   string currencyId = "currency." + reward.currencyType;
                   await Services.Inventory.AddCurrency(currencyId, reward.amount);
                   // add the reward to the response //
                   response.rewards.Add(reward);
                   Debug.Log($"Granted: {reward.amount} {reward.currencyType}");
               }
           }
           else
           {
               Debug.LogWarning("No rewards to deliver...");
           }
           // Level Up the player //
           Dictionary<string, string> newStats = new Dictionary<string, string>();
           int newLevel = (currLevel + 1);
           newStats.Add(levelStatKey, newLevel.ToString());
           await Services.Stats.SetStats(domain, access, type, Context.UserId, newStats);
           currLevel = newLevel;
           break;
       }
       else if(levelDef.nextLevel > currLevel)
       {
           Debug.Log($"XP req: {currXp/levelDef.xpReq}");
           break;
       }
   }
   Debug.Log($"Granting XP {xp}");
   // add the Xp anyway //
   await Services.Inventory.AddCurrency(xpCurrKey, xp);
   currXp = await Services.Inventory.GetCurrency(xpCurrKey);
   Debug.Log($"New XP: {currXp}");
   Debug.Log($"Curr Level: {currLevel}");
   // Now we'll construct the response //
   response.currentLevel = currLevel;
   response.currentXP = currXp;
   return response;
}

And we can trigger this server function using this example code…

int grantedXP = 400;
LevelManagerMSClient _levelManagerMSClient = new LevelManagerMSClient();
GSAddXPResponse response = await _levelManagerMSClient.GrantXP(grantedXP);
if (String.IsNullOrEmpty(response.error))
{
   // Log out the details to verify //
   Debug.Log($"Current Level: {response.currentLevel}");
   Debug.Log($"Current XP: {response.currentXP}");
   Debug.Log($"Has leveled Up: {response.hasLeveledUp}");
   Debug.Log($"Delivered {response.rewards.Count} Rewards...");
   // OnLevelUp(response) //
}
else
{
   // we have an error //
   // OnResponseError(response) //
   Debug.LogError(response.error);
}

Virtual Goods

GameSparks Virtual Goods are a pretty simple feature. In essence they are just some configuration data which defines the code, name, description and the cost of the Virtual Good in some preconfigured Virtual Currency related to the platform.

GameSparks does have a number of other features such as bundles and tags. These additional features are available on some platforms but not all of them, so we won't discuss how to reproduce those. Instead we are going to focus on the following requirements for migration.

  1. We need to be able to create a flexible definition that contains the code, name and description.
  2. We need to be able to define a cost for the item.
  3. We need an API which can grant this item to the player. Ideally the API should be able to debit the cost of the item when granting it.
  4. If the API cannot debit the cost of the item, we need to be able to debit the cost manually ourselves.

GameSparks Virtual Goods also include 3rd party integrations like Google or Apple Products which we can define with the Virtual Good and let GameSparks validate purchases and grant these items to the player from the backend. We aren't going to cover that in this basic example, but there is another example here where we discuss reproducing those features.

Beamable takes a slightly different approach to GameSparks with its Virtual Goods, but their solution is not very complicated.

As you would expect, you can create item configurations and grant them to the player using built-in APIs. These APIs are also available using Microservices so you can let the granting of these items be server-authoritative.

For this topic, we will only cover some of the basic GameSparks Virtual Goods features, taking an existing GameSparks example and porting it to Beamable. There is another topic here which covers 3rd party transactions if that is what you are looking for.

For this example, we will try to migrate some basic GameSparks Virtual Goods examples. Each item has a cost in one of the currencies we defined already. We can add a description field and a tag later, but other than that, we will keep these items very simple.

Items are already content-types in Beamable so we are going to use that predefined type and modify it for this use-case. There is an example of how to create new Content from scratch here if you want to do it that way.

The first step is to create a new C# script which is going to be a customized version of the out-of-the-box ItemContent class Beamable already has. By inheriting from the ItemContent class it allows our new GSVirtualGoodContent objects to use the same APIs regular Beamable items use for granting items to the player.

Beamable doesn't credit/debit the Virtual Currency cost at the same time as delivering Items, so this is something we are going to have to add ourselves.

Create a new C# script called GSVirtualGoodContent. I would advise you stick this into a folder so that all custom content you create that relates to GameSparks features are kept together. Another reason we should use a folder here is that we are going to need to create an assembly reference for our Microservice so that they can reference this GSVirtualGoodContent class. We’ll cover that later.

Next, we are going to have this class inherit from ItemContent and give it a content-type name. This is the name it will appear as in the Content Manager menu.

using Beamable.Common.Content;
using Beamable.Common.Inventory;

[ContentType("virtual_goods")]
[System.Serializable]
public class GSVirtualGoodContent : ItemContent
{

}

Now we need to define the Virtual Good content. To keep things simple we will just add a description and an array of costs. This should give you enough to get started if you need to create more complex replicas of GameSparks Virtual Good configs.

using Beamable.Common.Content;
using Beamable.Common.Inventory;

[ContentType("virtual_goods")]
[System.Serializable]
public class GSVirtualGoodContent : ItemContent
{
   public string description;
   public VirtualGoodCost[] costs;
}

[System.Serializable]
public class VirtualGoodCost
{
   public enum CurrencyType
   {
       GEMS,
       COINS
   }

   public CurrencyType currencyType;
   public int amount;
}

If you save your script and head back into the editor you will see this new content-type appear in the Content Manager and we can add a few new items.

To create a new Item, open the Content Manager and select Items from the Create down-down menu.

You can change the item’s Id to match the shortCode or name of your item in GameSparks. Selecting the item will show you the item’s attributes in the Inspector tab.

Here you can see the Client Permission attribute which you may have seen in other topics already. This allows you to control if this item can be granted through the client or only from the server or portal. You would generally want items to be granted by the server through a Microservice for example, but we will show some examples of how to grant these items using the client API, so for that test you would need to check this attribute.

You can also change the tag of your item here. Tags are maintained across all content in your game, so you could make this tag “vg” as I have done there, or you can subdivide the tag into “vg_weapons” if you want. Using the content service API you can get content by tag which is helpful, but we won't cover that in this topic.

Before we can start working with these new items, we need to publish the changes to the backend. To do this, click on the Publish button in the Content Manager.

You can also check if your content has been published by checking the Content section of the portal.

Inventory Service API
Now that our Virtual Goods are set up we’ll show how you can grant these items using the InventoryService API.

This example will also show how to credit/debit the cost of the item and grant the item.

Note - This example will show off the client API. As we already mentioned, you generally don't want to give the client control over granting items so we will also show how this is done with Cloud-Code later.

First thing we need to do is load our content reference so we can get the costs we set on these objects. There are two conventions for doing this in Beamable. The first thing you can do is declare a ItemRef variable in your script and load the item from the editor. The other way is to just set the Virtual Good content Id directly as a string.

// content Id as a string //
string vgIdString = "items.virtual_goods.diamond_sword";
// content Id as content Ref //
[SerializeField]
private ItemRef vgRef;

Next we will actually write our checks and credit/debit the cost of the Virtual Good. There is nothing complicated here, we will just use the inventory-service APIs to check the player balance and grant the item.

To keep the code shorter I have used an exception to detect if the player has enough currency for the transaction, but there are other ways you could do it in your project which might suit better.

/// <summary>
/// Delivers the VG item to the player and debits the cost of the VG.
/// Raises an exception if the player does not have the required balance.
/// </summary>
/// <param name="vgId">Id of the GSVirtualGoodContent content </param>
/// <exception cref="Exception">Could not grant VG - Invalid VGId</exception>
public async void GrantVirtualGood(string vgId)
{
   // get the Beamable API //
   var beamableAPI = await Beamable.API.Instance;
   // First we need a description of our Virtual Good so we can check the costs //
   var itemDetails = (GSVirtualGoodContent) await beamableAPI.ContentService.GetContent(vgId);
   try
   {
       // Now we can check if the player has enough of this currency type //
       foreach (VirtualGoodCost cost in itemDetails.costs)
       {
           Debug.Log($"Type: {cost.currencyType}, amount: {cost.amount}");
           string currencyId = "currency." + cost.currencyType;
           long currBalance = await beamableAPI.InventoryService.GetCurrency(currencyId);
           Debug.Log($"Player Balance [{cost.currencyType}] = {currBalance}");
           if (currBalance < cost.amount)
           {
               throw new Exception($"Insufficient balance [{cost.currencyType}]");
           }
       }
       // We know the player has enough balance for the item, so we will debit the player the cost and grant the item //
       Debug.Log("Granting item to player...");
       // debit the player the cost //
       foreach (VirtualGoodCost cost in itemDetails.costs)
       {
           // debit the currency costs //
           string currencyId = "currency." + cost.currencyType;
           await beamableAPI.InventoryService.AddCurrency(currencyId, -cost.amount);
       }
       // grant the item //
       await beamableAPI.InventoryService.AddItem(itemDetails.Id);
   }
   catch (Exception e)
   {
       // Log Warning or Error //
       Debug.LogWarning(e.Message);
   }
}

Remember that this function has to be async in order for the beamable-API calls to work. You can then run the function by passing in the content id. Below are two examples of how to do this with the string or the ItemRef variable.

GrantVirtualGood(vgIdString);
GrantVirtualGood(vgRef.GetId());

If you run this example, you should be able to see the player's balance change from the portal.

Custom Item Data
If you check the item granted to the player in the portal you can see there appears to be a JSON object associated with the item.

This is custom data you can apply to the item if you need something like a unique instance. The API takes a Dictionary<string, object> so you can use that to reproduce the JSON you want to apply to the item.

Fetching Player Inventory
We are also going to use the Inventory Service for fetching data. This is pretty simple, there is a generic all for getting all items and we can specify what type we want to cast to, or filter from a generic type.

Something that is handy with this API is that we can also get the ItemContent info from the item itself, allowing us to take different actions based on the content-type or other attributes like tag, or custom attributes.

// get all the VG items //
List<InventoryObject<GSVirtualGoodContent>> inventoryList = await beamableAPI.InventoryService.GetItems<GSVirtualGoodContent>();
foreach (InventoryObject<GSVirtualGoodContent> vg in inventoryList)
{
   // get the properties (we didnt set any in this example) //
   // vg.Properties
   // get the vg type if we need it //
   Debug.Log(vg.ItemContent.Id);
   Debug.Log(vg.ItemContent.description);
}

Microservices Example
In most cases you will want Virtual Goods to be processed by the server and not the client. So for the next example we will go through how to create a Microservice to handle this. Much of the code and setup is the same as the example in the previous section so we won't go into too much detail around the code needed. We also wont go into much detail about creating and starting Microservices as that is already covered in this topic here

We will start by creating a new Microservice, in this example we are calling it VirtualGoodsService.

As mentioned in other topics, Microservices are intended to group together common functionality into one service. They aren't like GameSparks events where we create one for each task, so you might want to group the function we will create for granting items into a larger service used for custom transactions or inventory management.

Before we start working with that Microservice, we need to make sure we can reference the GSVirtualGoodContent class. Although publishing the content to the server allows us to reference the content in the Microservice from the backend it won't be able to recognise that content until we add an assembly definition for that content.

To do this, go to your GSContent folder and add a new Assembly Definition. We can call it CustomGSContentDef. You are going to need to add some assembly references to this definition. Make sure to hit the “apply” button before proceeding.

Note - It's common for this step to mess with your reference definitions for some of the files referencing the Beamable namespace. If this happens, first try to re-import the files from the folder. You can also restart Unity, which can help solve the problem. If that doesn't work, try removing the using statements in the scripts causing the problem and re-import them again.

Now we are going to add this assembly definition to our Microservice. You can find your Microservice folder at Beamable -> Microservices -> VirtualGoodsService. You will find two files in there, click on the assembly reference file and add a reference for the CustomGSContentDef file we just created.

Remember to hit the “Apply” button to update the file.

Now we can proceed to editing the VirtualGoodsService script so open that script. We can basically copy-paste our previous code into this Microservice script but there will be a few lines we need to update. We will need to import some references for our debug logs and change any references to the beamableAPI object to Sevices.Content or Services.Inventory. Remember also to rename the function and make it async.

One small change we’ll have to add is some kind of response code. This would be something you might want from a GameSparks response as you may want to show the player a popup if something went wrong or they did not have sufficient balance for the item. So we will also make this Microservice function return Task that will let us call await on the function and return a string which will be our response code.

[ClientCallable]
public async Task<string> GrantVirtualGood(string vgId)
{
  // First we need a description of our Virtual Good so we can check the costs //
  var itemDetails = (GSVirtualGoodContent) await Services.Content.GetContent(vgId);
  try
  {
     // Now we can check if the player has enough of this currency type //
     foreach (VirtualGoodCost cost in itemDetails.costs)
     {
        Debug.Log($"Type: {cost.currencyType}, amount: {cost.amount}");
        string currencyId = "currency." + cost.currencyType;
        long currBalance = await Services.Inventory.GetCurrency(currencyId);
        Debug.Log($"Player Balance [{cost.currencyType}] = {currBalance}");
        if (currBalance < cost.amount)
        {
           throw new Exception($"Insufficient balance [{cost.currencyType}]");
        }
     }
     // We know the player has enough balance for the item, so we will debit the player the cost and grant the item //
     Debug.Log("Granting item to player...");
     // debit the player the cost //
     foreach (VirtualGoodCost cost in itemDetails.costs)
     {
        // debit the currency costs //
        string currencyId = "currency." + cost.currencyType;
        await Services.Inventory.AddCurrency(currencyId, -cost.amount);
     }
     // grant the item //
     await Services.Inventory.AddItem(itemDetails.Id);
     return "item-granted";
  }
  catch (Exception e)
  {
     // Log Warning or Error //
     Debug.LogWarning(e.Message);
     return "insufficient-balance";
  }
}

And now you can call this Microservice function using the following code.

VirtualGoodsServiceClient _vgServiceClient = new VirtualGoodsServiceClient();
string respCode = await _vgServiceClient.GrantVirtualGood(vgIdString);
Debug.Log($"Response Code: {respCode}");

Achievements

In GameSparks, Achievements are a pretty simple feature, however there are a few things we need if we want to migrate them from GameSparks to another platform.

GameSparks Achievements have the usual short-code, name and description of other features of GameSparks, but they also have the ability to deliver Virtual Goods and Virtual Currency as part of their own delivery. This is important because some of these platforms don't have this feature built in. Where possible, we will show how you can add this feature yourself with existing tools or APIs. Therefore, the next important thing these platforms need is some sort of API for Achievements, or if not, some API for creating static objects which we can add Virtual Goods and Virtual Currency to in order to create our own Achievements.

Most of these platforms do not have Leaderboard Triggers and without an API or callback from the Leaderboard, it would be quite difficult to make an automatic version of these features. However, we will mention where possible an alternative mechanism for Leaderboard Triggers that you could investigate.

Beamable does not have an alternative to GameSparks Achievements out-of-the-box. However, they do have a lot of existing features and APIs which we can use to put together our own version of GameSparks Achievements so that is what we are going to cover in this topic.

If we break down what we need into its simplest form, we need to replicate a JSON similar to below. GameSparks config features are just JSON anyway and look something like this.

{
  "shortCode": "completeAllQuests",
  "name": "completeAllQuests",
  "description": "completeAllQuests",
  "vgAward": "diamond_sword",
  "currencyAwards": [
    {
      "type": "XP",
      "amount": 1205
    },
    {
      "type": "GEMS",
      "amount": 15
    }
  ]
}

With Beamable, we can replicate something like this using some custom Content. This process is already described in the Cloud-Code topic here, so we won't repeat all the steps here but we will cover the setup.

Item Content
We need to be able to deliver this item to a player using existing APIs, so we are going to make this an Item and then let our custom item-type inherit from the regular Beamble Items. That will let us use the Inventory Service to deliver these items.

Note - We’ve already gone through how to deliver the Virtual Currencies <link> and Virtual Goods <link> in their own topics, so we won't go through how to set those up again here. Check out their topics for more details.

First we create a new script called GSAchievementContent. We should stick this script in the common GSContent folder mentioned in the Cloud-Code topic so that we can add it to our Microservice as an assembly reference.


[ContentType("achievement")]
[System.Serializable]
public class GSAchievementContent : ItemContent
{
   public string name;
   public string description;
   public string vgAward;
   public GSCurrencyReward[] currencyRewards;
}

[System.Serializable]
public class GSCurrencyReward
{
   public enum CurrencyType
   {
       GEMS,
       COINS,
       XP
   }

   public CurrencyType currencyType;
   public int amount;
}

You’ll notice we got rid of the shortCode field for the achievement. We don't need this because we can use the Id of the content object from the Content Manager. If you go to the Content Manager in the Unity editor you will see this new item-type appear. You can now replicate the details you need for your own achievements.

Before you can proceed, remember to hit the Publish button in order to sync the Content with the backend.

Now that we have the Achievement structure replicated in Beamable, we will take a look at how to deliver this achievement through a Microservice.

If you already have some Microservies created for Virtual Currency, Virtual Goods or custom player functionality, you should add a new function to those existing services, otherwise you can create a new one.

When you create a new Microservice, remember to add the assembly reference for your custom content as described in the section on Referencing Custom Content in Microservices in the Cloud-Code topic here.

Our Microservice function is going to be pretty simple. It needs to deliver the Virtual Good and the Virtual Currency rewards, but it also needs to deliver something to the player’s account to represent itself, i.e. to show that the player has earned the Achievement or owns the Achievement. We can do this by granting the Achievement as an Item using the Inventory Service API.

However, there is another way we could do this using Stats which we will discuss briefly.

Achievements as Stats
Using stats is much the same process as if the Achievement was an Item, though slightly different APIs, but there is a huge advantage to using Stats for Achievements as you can filter and target players using Stats while you cannot do this with a player’s inventory.

What this means is that if you want to target players for campaigns or announcements, based on their Achievements, using Stats is a perfect option, so we will give an example of both options here.

We need to pass the achievement Id and the player Id into the server method. Technically we only need the achievement Id as the player Id is used for saving stats, so if you don't want to do it that way, you can leave this out.

[ClientCallable]
public async void GrantAchievement(string achId, long playerId)
{
  Debug.Log($"Granting Achievement [{achId}]...");
  // First we are going to load the achievement definition //
  GSAchievementContent achDef;
  try
  {
     achDef = (GSAchievementContent) await Services.Content.GetContent(achId);
     Debug.Log("Description: "+achDef.description); // << just to test it loaded correctly
  }
  catch (Exception e)
  {
     Debug.LogError(e.Message);
     return; // you could return an error code here //
  }
  // Now we can iterate through the currency rewards and deliver those //
  foreach (GSCurrencyReward currencyReward in achDef.currencyRewards)
  {
     await Services.Inventory.AddCurrency("currency."+currencyReward.currencyType, currencyReward.amount);
     Debug.Log($"Awarded {currencyReward.amount}, {currencyReward.currencyType}");
  }
  // Now we can add the virtual good //
  await Services.Inventory.AddItem("items.virtual_goods."+achDef.vgAward);
  Debug.Log($"Awarded {achDef.vgAward}");
  // And lastly we can add this achievement. we'll show an example as a stat and as an item //
  // Item example //
  await Services.Inventory.AddItem(achDef.Id);
  Debug.Log($"Added Item {achDef.Id}");
  // Stat example //
  Dictionary<string, string> stats = new Dictionary<string, string>();
  stats.Add(achDef.Id, "1");
  await Services.Stats.SetStats("game", "public", "player", playerId, stats);
  Debug.Log($"Added Stat {achDef.Id}");
}

And then we can call this server-method using a simple call to the Microservice client object as we’ve shown in other examples.

string achId = "items.achievement.completeAllQuests";
AchievementsServiceClient _achServiceClient = new AchievementsServiceClient();
_achServiceClient.GrantAchievement(achId, beamableAPI.User.id);

To confirm your player received their achievement you can check the Inventory section of their player account through the portal.

We can also see the Stat set from the stats section.

Leaderboard Triggers
Beamable does not have an equivalent to Leaderboard Triggers, however, because the Leaderboard API is exposed, you could create a server function for posting to your Leaderboards. From there you could add conditions to your GSAchievementContent objects which define what Leaderboard conditions would qualify for the Achievement and then deliver them.

The same process is also possible on the client, though less secure.

Virtual Goods (3rd Party)

3rd Party Virtual Goods in GameSparks relate specifically to the ability of a player to purchase an item with a given product Id or SKU from a 3rd party store like Apple or Google Play.

The transaction is validated by the backend in order for the Virtual Good to be delivered to the player safely and without interference from the client.

For GameSparks, the flow for delivery of Virtual Goods is as follows:

  1. First we need our player to be authenticated with the platform’s client SDK, for example GooglePlay or Apple.
  2. The player will initiate a transaction with that platform using the client SDK
  3. The SDK will return some form of receipt data. This can be some form of meta-data about the transaction, it can also be a hash or a token of some kind. This is what we need to validate the transaction.
  4. The client sends this data to the appropriate GameSparks request.
  5. Under-the-hood, GameSparks will send this data to some validation API specific to the platform. It will also use some of the Integration settings set through the portal like any appIds or secrets.
  6. The platform will return validation details, along with some unique identifier for the player.
  7. Using that identifier, we can deliver the Virtual Good or Item to the player.

This is the flow we will be trying to replicate in this topic.

In some cases, these platforms use a different feature for Virtual Goods and for IAPs so keep this in mind.

Anywhere we show an example of custom validation with another platform, you may need to first make sure that player has been authenticated with that platform, so check out how to do that in this topic here.

Beamable has an out-of-the-box IAP feature which is linked to the UnityIAP service. Processing purchases this way is very simple and can be done with a single line of code. However, it does have quite a bit of setup before you can get to that point.

Note - If you need to stay with your own UnityIAP setup you can avoid using Beamable’s integrated IAP service. You would do this by creating custom Microservices and calling out to your 3rd party IAP validation endpoints for processing and delivering IAPs. However, we will only focus on the Beamable IAP service in this topic.

In this topic we will go through the setup for Beamable but there is also a lot of setup to be done with your IAPs and the store you are using (Apple or Google) and also the Unity IAP service. We won't cover setting these components up but we can assume your IAPs are already working from your GameSparks implementation but if you have not been using the Unity IAP service, there is a guide on that here.

It is important to note that there are a lot of steps to the app-store and Unity IAP setup so there is a lot that you can miss or can go wrong which could lead you to getting errors when trying to test your purchases.

This guide will be the same for both Android and Apple IAPs, as Beamable and Unity IAP service have the same setup for both.

Note - Currently (July 2021) this flow only works for Unity IAP 2.2.2, if you are using a later version of the Unity IAP package you will need to revert to 2.2.2 in order for this to work.

Step 1 - Create an Item
In a previous topic here we discussed how you might replicate a GameSparks Virtual Good using a custom item content object. Check that tutorial out if you need some custom items. In this example we are going to use a regular Beamable item.

Go to the Content Manager and then Create -> Item.

Once you have created your item, remember to publish your changes.

Step 2 - Create SKU
The next thing we need to create is a new SKU. These content objects are going to be used to create a store listing. Even if you don't use a store in your game, you will still need these so your SKU code can be checked against the Beamable manifest when processing the purchase.

You will need to set the productId for your IAP as it appears in your Google-Dev or App-Store connect portal.

Once you have created your SKU, remember to publish your changes.

Step 3 - Create a Listing
Again, we will go to the Content Manager to create a new listing. There are some important parts here that you will need to get correct for your IAP to work.

In the Price section you need to set the Type to “sku” and the Symbol should be exactly the same as the Id of the SKU content you created in the last step.

You can then choose what you want to deliver when this IAP is validated. In this example we are going to deliver the item we created. The item Id needs to be complete in this field. In other words it would be “items.stabbysword” and not just “stabbysword”.

You can see some other options there for setting a purchase limit or requirements to purchase this item. We won't cover them here but you should take note in case they are useful to your game.

Once you have created your listing, remember to publish your changes.

Step 4 - Setup a Store
The next thing we need to do is set up a store and add the listing for our item. Again this is done through the Content Manager and there isn't too much to add here, you just need to add the listing to the store list.

Once you have created your store, remember to publish your changes.

There is one more step needed in order to set up the store. You will need to add this to the config of your Beamable project. To do this you will need to go to the Beamble Toolbox window and then click on “Config”.

We then need to add the store to the project config.

BeamableIAP API
Now that you have everything set up you should be able to start with a test-purchase in Unity to validate your IAP is working.

As Beamable is using the Unity IAP service there isn't much code needed to process a purchase, we only need the Id of the listing content and the Id of the SDK content.

/// <summary>
/// Initiates and processes a purchase based on the listing and sku code given
/// </summary>
/// <param name="listingCode"></param>
/// <param name="skuCode"></param>
private async void StartPurchase(string listingId, string skuId)
{
   Debug.Log($"Listing-ID: {listingId}, Sku-ID: {skuId}...");
   Debug.Log("starting purchase...");
   // get the beamable IAP instance //
   var iap = await beamableAPI.BeamableIAP;
   CompletedTransaction transDetails = await iap.StartPurchase("listings."+listingId, "skus."+skuId);
   Debug.Log("TID: "+transDetails.Txid.ToString());
   Debug.Log("Recpt: "+transDetails.Receipt.ToString());
}

You will also need to include the following namespaces.

using Beamable;
using Beamable.Api.Payments;
using Beamable.Common.Api.Auth;
using UnityEngine.Purchasing;

Known Issues
If you don't have Unity IAP enabled you may get errors relating to some of the beamable purchasing namespaces not being found. This is due to the UNITY_PURCHASING script definition symbol not being set on your project when you import the Unity IAP package. This is rare but does sometimes happen. To solve this you can try re-import the plugin or you can add the symbol to your project settings directly.

Testing Purchases
If you have UnityIAP set up correctly you should see some details printed out in the console when you start the scene from within the Unity editor.

You can now test your purchase validation code. However, since you will be making test-purchases you will need to make sure you are working from the dev or staging environment. Test-purchases will not work from the prod environment. You can make this switch from the Beamable Toolbox window.

Now, if you process the purchase you should get a popup which would allow you to test the flow.

You will be able to see your transactionId returned if the process was successful.

Now we can check if this item actually was delivered by looking for that player’s account through the portal.

Note - Since we are testing through Unity, these purchases will not go through the Apple or Google store. You will need to build and deploy your app through those stores in order to test the full purchase flow. Bear in mind that there are a number of steps to set up Unity IAP and the items on your stores correctly to get these purchases working in through the stores.

Teams

When we talk about Teams, we must keep in mind that other platforms might call this feature something else. Teams can encompass a lot of different multiplayer features of the same name such as Clans, Guilds, Groups, Orgs, Tribes, Factions, etc., so keep this in mind. These other platforms may not call their feature Teams and that might not be what the feature is called for your game.

GameSparks Teams are one of the more complicated features of GameSparks but in essence they provide functionality to group players together into a single data structure. When driven by the SparkTeams API this feature can be used to create a variety of custom features, but out-of-the-box GameSparks provides the following functionality:

  1. Players can create or delete Teams.
  2. Players can view a list of Teams they wish to join.
  3. Players can view Teams they belong to.
  4. Players can join a Team.
  5. Players can leave a Team.
  6. At an API level there is also the ability to remove players from a Team.

These features are what we need in order to migrate Teams from GameSparks to another platform.

While not all GameSparks developers use all features of Teams, the basic needs are usually the requests above, and the same functionality through an API.

However, there are other features with GameSparks Teams that we will discuss.

Team Chat & Notifications
Team Chat is a very widely used GameSparks feature. It can be adapted for other features like lobbies or tournaments or even global announcements.

Something important to keep in mind when reviewing these other platforms is that they do not all use websockets like GameSparks does. That means that it is not as simple for that system to send notifications to other team members when something has changed, or a chat message has been sent.

We will look at how these notifications can be reproduced in these platforms where possible.

Team Chat may not exist at all as an option in these platforms, but we do cover messaging and chat in another topic here.

Team Leaderboards
In GameSparks, Team Leaderboards use the team ID instead of the player ID in order to post scores. This allows any member of the Team to post scores to a Leaderboard.

Some of these alternative platforms will not provide this feature, and without access to the underlying API controlling Teams, there is no way to reproduce this feature.

Team Data Team data is not a feature of GameSparks, but it is a common feature for GameSparks developers to create themselves when using Teams and Cloud-Code.

Team data is created by using the team id as a field to search for Teams by in a new database collection. By referencing the team id, you can add any information you want to the Team. Common examples might be a “team-chest” where you can add all your Team’s earned currency or shared items. Extra fields for icons, descriptions or rules are also common.

Where possible we will also show how to create this feature if it is not offered by the platform out-of-the-box.

The Teams feature for Beamable is called Groups. It has many of the same features that you would expect from GameSparks Teams, plus a few extra features to manage members, Team data and a team wallet which can be used to deliver Virtual Currency to members.

Beamable does not have a chat system built into their Groups feature, however they do have their own chat service which is covered in the Messaging topic here.

Beamable also does not have Team Leaderboards as a feature. Since GameSparks uses the team Id for their Team-Leaderboards it is not really possible to recreate the Team-Leaderboard API as it would be in GameSparks.

No configuration or setup is needed to create a Team or a template of a Team like in GameSparks. Instead, all this setup is done through API calls directly from the client.

In Beamable you can:

Where these features overlap with GameSparks we will cover some simple examples using the Group Service API.

Creating A New Group
As mentioned above, creating a new Group/Team is done using the Group Service APIs. To create a new group we first need a GroupCreateRequest which requires a few settings.

Name, tag and maxSize are all options you should be familiar with, but there are two other options that you will not have seen from GameSparks.

A couple more things to quickly note are that the “tag” field must be unique across the game and is also optional, and the maxSize field has a max value of 50.

The API call for this is very simple; you can see an example of this below.

/// <summary>
/// Creates a new group with the given details
/// </summary>
/// <param name="groupName">Group Name</param>
/// <param name="type"> open, restricted or closed</param>
/// <param name="req"> set to zero to ignore this value</param>
/// <param name="maxSize"> max member size - max 50</param>
async void CreateGroup(string groupName, EnrollmentType type, long req, int maxSize)
{
   Debug.Log($"name: {groupName}, type: {type.ToString()}, req: {req}, maxSize: {maxSize}");
   Debug.Log($"Creating new group - {groupName}");
   // create a new group-request object //
   GroupCreateRequest groupCreateReq = new GroupCreateRequest(groupName, null, type.ToString(), req, maxSize);
   GroupCreateResponse groupCreateResp = await beamableAPI.GroupsService.CreateGroup(groupCreateReq);
   // log details //
   Debug.Log($"New Group ID: {groupCreateResp.@group.id}");
   Debug.Log($"New Group Name: {groupCreateResp.@group.name}");
   Debug.Log($"New Group Tag: {groupCreateResp.@group.tag}");
}

You can see in the response that a new ID will be given to your group indicating a new instance has been created.

Creating & Finding Groups
Now that we have created a group, let us take a look at how we can search for our groups or other groups.

There are a few options you can pass into the search method. These are things that have already been covered while creating a group such as the enrollment-type, the requirement, etc. You can see more details on these options here.

A few interesting points on these fields are listed below:

Something else to note is that the Search() method uses mongoDB text-search functionality. This means that you can do partial string matches if you search the group name, but it is not the same as a regex query.

Let us take a look at what happens with a partial search for the group we just created.

/// <summary>
/// Search for a group with the given name
/// </summary>
/// <param name="nameSearchTxt">full or partial match for the group name</param>
async void SearchGroup(string nameSearchTxt)
{
   Debug.Log($"Searching Groups - {nameSearchTxt}");
   GroupSearchResponse searchResp = await beamableAPI.GroupsService.Search(nameSearchTxt);
   // we can go through the groups to get their details //
   foreach (Group group in searchResp.groups)
   {
       Debug.Log($"Group ID: {group.id}");
       Debug.Log($"Group Name: {group.name}");
       foreach (Member member in group.members)
       {
           Debug.Log($"Member ID: {member.gamerTag}");
           Debug.Log($"Member Role: {member.role}");
       }
   }
}

We can see from the Unity console how we can get the data for the groups.

There is a lot more data available for the group from this response, but we will cover that later.

Group Management
You can see in the example above that you can get the group member’s role. In the case of this example, it was our player who created the group, so they are automatically given the “leader” role. However, you can modify and change these roles in any way you want using the SetRole() method.

This allows you to set up something like a “captain”, “lieutenant”, “private” structure where the creator of the group does not need to be the only one that has permissions to invite members or vet applications to the Group. However, this is all up to you to design, role-privileges in Beamable are not provided out-of-the-box. This sort of vetting could easily be created through the client or using microservices (for better security).

Joining A Group
When we created the group in this example we made it an “open” group. This means that any player can join without a request or invitation and so, the process for joining is very simple. We have an example below.

/// <summary>
/// Allows the player to join the group or send a request to join if the group is not open
/// </summary>
/// <param name="groupId">The id of the group</param>
async void JoinGroup(long groupId)
{
   Debug.Log($"Joining Group: {groupId}");
   GroupMembershipResponse joinResp = await beamableAPI.GroupsService.JoinGroup(groupId);
   Debug.Log($"Group Join Successful: {joinResp.member}");
}

Using the search-teams example from before we can validate this player has joined. Note that they have not been given a role in this case.

What about a case where the Group is “restricted” or “closed”?

This request still applies where the Group is restricted. Restricted groups can still be applied to using the JoinGroup() request but your players will not be granted immediate access. This will instead send a message to the leader of the Group asking for permission to join. There are more details on this flow in the section on Group Notifications.

If your Group is “closed” then no one can apply to join. Instead, you can invite a player to join your Group using the Petition() method. There are more details on this here.

Example: Team Data
Although GameSparks does not have this out-of-the-box, it is a very common feature that is custom built by GameSparks developers on top of the existing Teams API.

This is usually just a “TeamData” collection which uses the team ID as the query field. Using Runtime-collections or GDS you can append any data to a Team that you like.

Since this is a very common use case in GameSparks, we will cover how to reproduce this here.

There are a limited number of fields which you can modify and they are described here. Below is an example of how to perform this kind of update in code.

GroupUpdateProperties newProps = new GroupUpdateProperties();
newProps.name = "Not an awesome group for lame people";
newProps.slogan = "we suck!";
newProps.clientData = "some custom data";
newProps.tag = "meh";

Then, updating the Group is straigh-forward…

/// <summary>
/// Updates the group properties with new data
/// </summary>
/// <param name="groupId"> the id of the group</param>
/// <param name="properties">a GroupUpdateProperties object containing your update data</param>
async void SetGroupData(long groupId, GroupUpdateProperties properties)
{
   // update these properties //
   await beamableAPI.GroupsService.SetGroupProps(groupId, properties);
   // just for this example we will get the group and confirm the update //
   Group groupDetailsAfter = await beamableAPI.GroupsService.GetGroup(groupId);
   Debug.Log($"Updated Client Data...");
   Debug.Log($"ClientData {groupDetailsAfter.clientData}");
   Debug.Log($"Slogan {groupDetailsAfter.slogan}");
   Debug.Log($"Name {groupDetailsAfter.name}");
   Debug.Log($"Tag {groupDetailsAfter.tag}");
}

We created an example using the code above showing the data before and after the update for verification.

Group Notifications
For the example we have provided above, our Group was set to “open”. This means that joining was automatic. So what happens if you have a restricted or closed group? How can the leader or administrators know that there is a new request to join?

This process goes through Beamable’s Mail Service. This differs from GameSparks, as notifications and updates about Teams are automatically relayed through the web-socket. Since Beamable does not have a web-socket with Groups, the Mail Service is a replacement that allows leaders players to get updates about their Groups.

We will not cover the Mail Service here as it is covered in the topic on Chat & Messaging here.

Other Features
There are other features of Beamable Groups that do not overlap with GameSparks so we will not cover them in this topic. However, it is handy to know about these features for future development.

Currency Donations
The Groups Service also has functionality for delivering currency to a member of the Team. This works by the member first requesting a donation using the MakeDonationRequest() function. Admins in the Team can then approve this by sending that member the currency using the Donate() function.

Player Manager

In this topic we are going to cover how player management works with these different platforms. What we mean by player management is any kind of form or tool which allows you to search for a player by user-name, email or id and then view or change some attributes from the back-office.

These tools can be quite complex, especially as GameSparks allows developers to create custom manage-screens where they can add any functionality they want. However, in alternative platforms we don't always have this flexibility, so we have to go with what they offer us. There is an exception to this where we could create our own manage-screen outside of the platform using custom-build tools. There is a topic here which covers that.

There technically isn't a GameSparks-official player manager anymore, but in some old versions of the portal you would get the default “Player” manage-screen where you could search for player and update certain out-of-the-box features so this is what we are going to be looking for.

Through the Beamable portal, you have access to an extensive player management dashboard. You can access player management in the left hand sidebar under Engage - Players.

Here you can search for players using their account Id, device Id, Gamertag or email address.

You can also search for paying or non-paying players as well as for players on a specific platform or with a specific installation date.

To explore a specific player, click on their gamer tag in the player list. This will open the player’s profile page.

The sidebar allows you to look at various aspects of the player including their Profile, Stats, Inventory and others. We will highlight a select few categories below.

Inventory
Clicking on the Inventory tab we can see where Items and Currency are displayed in the Beamable player’s account.

The Player’s currencies are displayed at the top of the page. Here, you can search for specific currencies and filter by currency permissions. It is also possible to edit the player’s Virtual Currencies. This includes the balance and properties attached to each currency the player holds.

Click the three small dots to open the editing window and hit ‘Save’ to save your changes.

Similarly, you can add, edit and remove items from the player’s inventory. It is important to note that items are listed in categories based on item id. If your player owns 3 diamond swords, the instances will be grouped under the “diamond swords” heading.

Use the add item button to add an item to the player’s inventory. Select the item from the content dropdown and add any properties that you want. Click “Save” to grant this item to the player. These properties can also be edited from the portal at any time on any item that the player owns.

Stats
Stats are another important section that has come up in a lot of topics. As Stats are a list of strings it is sometimes easier to manage them from the portal and so they don't have a lot of options.

Stats are split up into domains (client or game) and access (public or private). You can see the player stats listed on this page. You can filter stats by name, write access and domain.

To see the stats value, hover over the stat with your cursor or expand the dropdown to see the key and value. You can also add, edit or delete stats here. To do so, use the action buttons on the right side of the stat.

Cloud Saving
We covered Cloud-Data in the Cloud-Code topic here.

In this section, you can see the names of all the Cloud-Data stored on your player. You can upload or download this data but you cannot delete or add data through this list. There is also an option to queue a replacement file upload.

Chat Messaging

In this topic we are going to cover how to migrate Chat and Messaging from GameSparks to these destination platforms. In GameSparks these two features are often the same thing and are commonly grouped together into GameSparks’ Team Chat feature. However, this feature won't exist on destination platforms and may instead be grouped into another feature or separate features (Teams and Chat).

For migration of general Chat functionality we need the following features to be available on the destination platform:

Chat
This can be any kind of asynchronous messaging between players connected to a common group. This group could be a team as is the case in GameSparks Team Chat, but it could also be a lobby or chat group unrelated to a team. There might also be global chat features.

Chat is not always handled like it is in GameSparks. In GameSparks Chat and Chat Messages are the same. You can get a list of all your Chat Messages at any point in time and see your Chat history. In some alternative platforms Chat is ephemeral, so you might be able to see your history for the session or lobby you are in at that moment in time. After that session is over you will not be able to see that chat history again.

Therefore we need to investigate if there is an alternative to this feature available for the destination platform too.

Messaging & Inbox
This is different from Chat as it involves how to get a list of messages for the player. Like Chat, this feature is often used to send messages between players, but also for the game-team to send messages to players. This feature is often called the player inbox because of this. It is another way to communicate with the player where they can also see a list of past messages.

The main difference with this feature and Chat is it is not designed to get messages in real-time. This feature would use a call to get all the player’s messages and then the frontend would need to sort those messages in the UI to show which are read and unread.

Messaging API
Along with Team Chat, GameSparks exposed a lot of the underlying Chat APIs in Cloud-Code using the SparkMessage API. This allows GameSparks developers to create their own custom Chat and inbox features to fit their requirements.

Examples of this might include profanity filtering, automatic banning from chat groups, inbox systems for player-to-player mail, news and announcements.

This feature may not be available with all alternative platforms, but we will cover any workarounds where possible.

Note - We will not be covering Push Notifications in this topic, though we will point out where that feature is available on the destination platform and provide links to documentation if available.

Beamable do not have the same features to GameSparks in terms of chat and messaging, but they do have some features that can be used to replicate much of the same functionality as GameSparks Team Chat and messaging APIs.

Note - This feature is still in development by Beamable and is likely to change and improve over time. Check with Beamable to see what updates have been made to this feature since we completed this topic.

Rooms Before we can start setting Chat up in Beamable we first have to introduce a Beamble feature called Rooms.

You can think of these as something like a lobby or a chat-group. They are different from Teams in GameSparks all thought in essence, Rooms are a way to group players together. Beamable has a dedicated feature for this called Groups which we cover in another topic here.

A key feature of Rooms is the ability to send messages to other players in a Room and get automatic notifications of all messages sent within a Room. We will cover a simple example of this here.

Rooms have 5 functions:

These are all accessed through the Rooms Service API and in addition to the functionality above there is a “Subscribe” function which allows you to receive notifications from these Rooms. We will go through this functionality now with some simple Unity examples.

Creating New Rooms
Unlike GameSparks, you don't create a config or template for Rooms. Rooms are created on-the-fly by players using the client SDK.

These calls are simple, you just need to provide an ID for your Room and a list of players. You can ignore the “keepSubscribed” parameter and just set it to ‘true’.

/// <summary>
/// Creates a new room with the given name and players
/// </summary>
/// <param name="roomName">Room Name</param>
/// <param name="roomName">Room Name</param>
async void CreateRoom(string roomName, List<long> playerList)
{
   Debug.Log($"Creating New Room {roomName}...");
   RoomInfo newRoomInfo =  await beamableAPI.Experimental.ChatService.CreateRoom(roomName, true, playerList);
   Debug.Log($"New Room ID: {newRoomInfo.id}...");
}

Note - The player who created the group should also be added to the player list.

This API allows you to add any list of players to your new Room. This is therefore a good way to recreate GameSparks’ Team-Chat functionality. Rooms persist, so once a Room is created out of a Group, you can re-subscribe to it at any time to start getting messages from other members.

Searching Rooms
Searching for rooms is a bit more limited than with the Groups features.

You can view all the rooms you are a part of. These are either your rooms or rooms you have been added to.

The API here is simple. Below is a simple example including some of the details you can get from the RoomInfo class.

/// <summary>
/// Gets a list of Rooms available to the player
/// </summary>
/// <returns>roomList</returns>
public async List<RoomInfo> GetRooms()
{
   List<RoomInfo> roomList = await beamableAPI.Experimental.ChatService.GetMyRooms();
   foreach (RoomInfo room in roomList)
   {
       Debug.Log($"ID: {room.id}, name: {room.name}, players: {room.players.Count}");
   }
   return roomList;
}

Using this example you will be able to see your new room in the console, but you will also see another room that you haven't created.

This general.chat room is a global room which can be used for global chat.

Subscribing to a Room Next we need to subscribe to the room, so that we can get notifications when anything changes about the room. Specifically, we want to get notifications for messages or for when new members subscribe or leave.

/// <summary>
/// Subscribe to a room
/// </summary>
/// <param name="roomId">Id of the room</param>
private void Subscribe(string roomId)
{
   Debug.Log($"Subscribing to room {roomId}...");
   // We can do this by assigning a message on player-leave callback, along with a subscriber to listen for changes //
   beamableAPI.Experimental.ChatService.Subscribe((chatView) =>
   {
       foreach (var room in chatView.roomHandles)
       {
           if (room.Id == roomId)
           {
               Debug.Log($"Subscribed To Room {room.Id}...");
               room.OnRemoved += OnPlayerLeave;
               room.OnMessageReceived += OnMessage;
               room.Subscribe().Then(_ =>
               {
                   Debug.Log("Room Updated...");
                   foreach (var pid in room.Players)
                   {
                       Debug.Log(pid);
                   }

               });
               return;
           }
       }
   });
}

You can see here we have callbacks assigned for receiving messages or when a player leaves the room (the OnRemoved callback).

These methods are straightforward. For the OnMessage callback, you can get some basic information about the message including the player’s Id which is shown as gamerTag field below.

private void OnMessage(Message newMess)
{
   Debug.Log($"Received Message {newMess.content} to RoomId: {newMess.gamerTag}...");
}

private void OnPlayerLeave()
{
   Debug.Log($"Player Left...");
}

You can also create an “Unsubscribe” method using those options in the RoomHandle class, for example…

foreach (var room in chatView.roomHandles)
{
   if (room.Id == roomId)
   {
       room.LeaveRoom();
       room.Unsubscribe();
       return;
   }
}

Now you can test this player-to-player to see if the messages are being passed through. Remember, if you create a room, you need both players to be added in order to pass messages between them both. The global chat room can be subscribed to by anyone.

There are more features available for Chat. For example there is a profanity filter available through the Chat Service API. We won't cover that in this topic but there is more information available on that here.

Mail
Although it is possible to receive and list messages using Beamable’s Chat Service, it is not intended for long term storage of messages for example in the case of an inbox feature. However, Beamable’s Mail Service does allow you to create a system where you can send and receive messages and list them for long term storage. Unlike GameSparks, players will not receive notifications for these messages. Instead they are meant to be refreshed infrequently.

Beamable’s Mail Service has the following functionality:

It is important to note that sending mail is restricted to admin users only.

This means that any player can get or update their own mail but they cannot send mail from the client.

This is because Beamable’s Mail Service is intended to be used to send mail to players directly from the developers and live-ops managers. It can be used for announcements, news or messaging campaigns.

There is another feature of Beamable called announcement which differs from mail. You can check that out here.

Sending Mail
As already mentioned, mail is only supposed to be sent by an Admin user. This is because the Mail Service is designed to send bulk messages to players. You can see an example of this here.

However, Microservices also act in an Admin capacity for the Mail Service and therefore you can create a custom Microservice that will allow players to send messages P2P.

We will show an example below of how to send these messages. It is very similar to the client example on Beamable’s documentation site here so you can refer to that example for some of the other functionality of the Mail Service that we won't cover here. We also wont show how to create new Microservices here, there is a guide on that in our Cloud-Code topic here

For this example we will send a subject and body into our Microservice function, along with the player Id of the player we want to send the message to.

public async void SendMail(long recipientId, string subject, string body)
{
  Debug.Log("Sending Mail...");
  // Construct our mail message and send it //
  var mailSendRequest = new MailSendRequest();
  var mailSendEntry = new MailSendEntry();
  mailSendEntry.category = "player";
  mailSendEntry.senderGamerTag = Context.UserId;
  mailSendEntry.receiverGamerTag = recipientId;
  mailSendEntry.subject = subject;
  mailSendEntry.body = body;
  mailSendRequest.Add(mailSendEntry);

  // Call may fail if sender lacks permissions
  bool isSuccess = true;
  try
  {
     var emptyResponse = await Services.Mail.SendMail(mailSendRequest);
  }
  catch (Exception e)
  {
     Debug.LogError(e.Message);
     isSuccess = false;
  }

  if (isSuccess)
  {
     Debug.Log("Mail Sent...");
  }
}

Fetching Mail
After executing the call above, we need to be able to see if the recipient got the message. The easiest way to do this is with the GetMail() method of the Mail Service API. There are other methods available such as GetCurrent() which gets you the count of unread messages and SearchMail() which lets you filter messages.

We will show a simple example using the GetMail() method here. Note that this is done through the client, not the Microservice.

/// <summary>
/// Fetches the player's messages
/// </summary>
async void GetMail()
{
   Debug.Log("Fetching Mail...");
   // get the top 100 messages //
   ListMailResponse messageList = await beamableAPI.MailService.GetMail("player");
   // go through the message list //
   foreach (var message in messageList.result)
   {
       Debug.Log($"Message Id: {message.id}");
       Debug.Log($"Sender: {message.senderGamerTag}");
       Debug.Log($"Message Subject: {message.subject}");
       Debug.Log($"Message Body: {message.body}");
   }
}

There are other attributes of these mail messages you can get from this response but we only need to show a few here.

Additional Mail Features
We have only covered the basics of Beamable’s Mail Service here as it relates to GameSparks feature parity. However, there are other features that are not included in GameSparks.

An important one is the ability to send bulk messages using this service. This is not done through a Microservice but instead uses the REST API linked in the section above.

Another feature that might be useful is the ability to deliver Items and Currency through mail. This is a parameter object set on the mailSendEntry we created in the Microservice. There is more information on that option here.

Push Notifications
Push notifications are only available through Firebase Cloud Messaging. The process works similarly to GameSparks where the client needs to register before notifications can be sent.

beamableAPI.PushService.Register(PushProvider.Apple, token);

You can use the PushProvider enum to choose iOS or Android notifications. These notifications can then be sent from a Microservice.

Downloadables

Downloadables are a pretty straight-forward feature of GameSparks. It allows developers to host static files through the platform which can be downloaded with out-of-the box requests or APIs from the client. Downloadables in GameSparks also come with a few limitations such as the size of the files which can be hosted, other platforms might have similar limitations.

In essence, Downloadables are a CDN (Content Distribution Network) which is served using AWS S3. You upload files through the portal and give them a unique code. When a player requests the file, GameSparks will request a short-lived URL where the player can download the file and return that to the player. It is then up to the client to download that file.

In this topic we will discuss how other platforms deal with their CDNs and hosting static files. Because of the simplicity of this system, it is not necessary that these alternative platforms provide a CDN or alternative feature for Downloadables. There are other options available and creating your own system is not difficult and does not require much maintenance. You can see one approach here using AWS S3 in a similar way to GameSparks.

Beamable does not currently offer any CDN support outside of their Content feature which can be used for a wide array of static content. However, you cannot serve whole files (like Unity packages for example) using Beamable Content at the moment.

However, it is possible to create your own CDN and hook it up with Beamable using Beamable’s Microservices alternative to GameSparks Cloud-Code. There is a topic here on how to use Beamable Microservices, and another topic here on how to replicate GameSpark’s downloadables feature using AWS services.

  1. [Post] - Create a new slot. You can now get the URL and upload the binary into that slot
  2. [Get] Get the URL
  3. [Update] Allows you to upload/overwrite the data
  4. [Delete] Removes the data from the slot

While AccelByte does not currently have an alternative to GameSparks Downloadables it is possible to create your own CDN using AWS services. We have a guide on this here. This would require either a custom microservice or your own backend with the AccelByte Goland or JS SDK integrated. There are more details on this process in our topic on Cloud-Code here.

Cloud Code

GameSparks Cloud-Code is actually a very broad topic to discuss from the point of view of migration. Alternative platforms to GameSparks have not been built with portability in mind, they focus on their own interpretation of Cloud-Code, so migrating Cloud-Code will not be a case of copy-pasting your existing from one platform to another. In some cases these platforms are also even using a different programming language to GameSparks.

In this topic we will look at what the differences between GameSparks Cloud-Code and what these alternative platforms have to offer, and how to adapt your existing code to fit how these platforms approach Cloud-Code.

In order to assess the complexity of migrating your own Cloud-Code we have split this topic into a list of the required features needed to replicate most GameSparks Cloud-Code use cases:

  1. Create custom scripts This means the platform allows developers to create totally new APIs for their game and not just use the out-of-the-box or modify existing APIs. You will need to create something totally from scratch.
  2. Send and receive custom data to and from these scripts GameSparks developers do use Cloud-Code to get specific player information or static data from MetaCollections, but more often they need the server to perform some actions or validations before returning a result. We therefore need to send and receive custom data and not just execute some code on the server.
  3. Get/Set Player Data Script APIs should be able to load currently stored player data for validation, update this data and save it back to the player. Ideally, we should be able to do this for any playerId through the Script API, not just the player that called the script. We need this for things likeTeams or Friends features.
  4. Get Static Game Data - MetaCollections We need a replacement for GameSparks MetaCollections as these are widely used by developers to load static game data. This could include item tables or game-configuration data like live-ops events, localization, etc. We also want these alternative data stores to be cached so they are optimized for performance.
  5. Send HTTP Requests A very common use-case for GameSparks Cloud-Code is to connect to another service like a 3rd party payment, auth or other game service. The ability to send HTTP requests is very important for extending the current API offering of the platform.
  6. Receive HTTP Requests Similar to sending requests, if we can receive data from 3rd party services we can extend the platform ourselves. This is also very common for payment validation, advertisement campaigns and email validation.

Key Differences
As already mentioned, none of these alternative platforms have a way to directly port GameSparks code into their Cloud-Code system, so we have to work with what they offer. This section covers the main differences you need to be aware of for these platforms. We go into more details on the specific differences in each platform's own section.

Player Data
With GameSparks there are several methods for storing player data we need to consider. Most developers will also use a mix between these two methods which might not be adaptable to the new platform so this is something to consider.

Player ScriptData
This would be where you are storing your player data on the SparkPlayer object in either scriptData or privateData. This uses the player system-collection and is also cached for better performance. If you are storing your current data using the SparkPlayer API you will need something that can store custom JSON. Remember that you can also store this as a JSON string if the platform doesn't allow for JSON.

Make sure to check what the limits of this storage is. There is usually a max size for both JSON or strings depending on what the platform uses.

Database Storage
Not all of these platforms offer database APIs out-of-the-box like GameSparks does. This will make migrating RuntimeCollections or Game-Data collections more difficult.

In these cases you may need to resort to mapping your player data for those collections to JSON objects which can be stored using the platform's conventional player storage. This will mean revising how your existing GameSparks database queries are fetching data. If they are mostly querying by the playerId, this should be no problem, however, more complicated queries like playerId and itemId could be solved by storing the data as an array or times or, to make it easier to access data by Id, you can store them as an object containing multiple objects, as in the example below.

GameSparks Doc Example
The example below might be used in a “PlayerInventory” collection in GameSparks where you query using { “playerId” : “5c9208b4efe6a104f1c67e24”, “itemId” : 79 }

{
  "_id": {
   "$oid": "5c9208b4efe6a104f1c67e57"
  },
  "playerID": "5c9208b4efe6a104f1c67e24",
  "itemId": 79,
  "unlocked": false,
  "unique": true,
  "dateAdded": {
   "$date": {
   "$numberLong": "1553074356610"
   }
  },
  "lastUpdated": {
   "$date": {
   "$numberLong": "1553074356610"
   }
  }
}

To convert these docs to a single object you might use the following data-model.

"playerInventory" : {
  "8": {
   "itemId": 8,
   "unlocked": false,
   "unique": true
   ...
  },
  "79": {
   "itemId": 79,
   "unlocked": false,
   "unique": true
   ...
  },
  ...
}

This would allow you to replicate the query used in GameSparks to get the item by playerId and itemId by referencing the items by their Ids as playerInventory[itemId].

MetaCollections
As with player data, GameSparks MetaCollections make use of the database and return information using noSQL queries. This may not be an option for many alternative platforms as they have their own way of working with static game-data.

These platforms often call their equivalent to MetaCollections something else like title-data, content, game-data, etc, but they are work fundamentally the same; There is some way to add content, usually through the portal or its equivalent (REST API for complex data maybe), and an API client-side or in Cloud-Code to return the data so we can use it. It is also important that this content is cached in some way in order to boost performance.

You will encounter the same issues as you might expect with player-data. If the platform does not allow you to store simple JSON, you will need to create some custom objects so that your data can be serialized by the client.

In cases where the alternative platform offers some kind of SQL database you might have to think about modelling the data to a more strict format than your noSQL collections. This shouldn't be a problem, as MetaCollections are generally static, but you will have to consider how you are going to index fields when converting the data to a SQL table.

Other Features to Consider
What we listed in the section above were only the features we need in order to get similar functionality in the new platform as you are familiar with in GameSparks Cloud-Code. However, as you already know, there are a lot of GameSparks APIs that we didn't cover that may be important to your game.

In some cases, these features will just not be available on the destination platform. This might be a blocker to migration altogether, or there may be some workaround. Some examples of these cases are outlined below.

System-Scripts
System scripts cover a few different use-cases in GameSparks. Most GameSparks developers are familiar with the every-minute, every-hour and every-day scripts as they are commonly used. Other scripts like on-player connect/disconnect and on-publish are also used, however, we will not cover those here as they are specific to GameSparks and will not have a migration route, though they may be reproduced as custom-events or scripts.

For timed scripts (every-minute, etc) there are a few options. Some platforms come with these features themselves so it is only a case of porting the code (given the same complexities as porting any other Cloud-Code, it won't be as simple as copy-paste). In other cases we might have to use external services to run these scripts for us. We cover an example of this in the topic here.

Bulk-Jobs
Bulk-jobs are a tricky feature, even for GameSparks. With Bulk-jobs in GameSparks, the server will safely spread out the workload across all players involved and execute the job over time so as not to impact server performance. Bulk-jobs on other platforms therefore should not be just considered as a way just to execute code for every player in the game or a large sub-set of players, you should also consider server performance.

For GameSparks it is not advised to use Bulk-Jobs for every-day events, they are more intended for admin tasks, so if your existing Cloud-Code relies on Bulk-Jobs for every-day operations you should consider optimizing your code rather than trying to port over an inefficient feature.

Oftentimes, an alternative to Bulk-jobs is to trigger the update from a player-action. For example, if you need to inform players of an upcoming event, you can check for this upon login instead of changing something on every player’s account in order to flag their account for that event.

Something you can use in conjunction with the above suggestion is to return a list of active and inactive events when the player logs in. The inactive events come with a timestamp for when they become active and the active events come with a timestamp for when they end. This allows the client to track when updates are needed and double-check with the server when events should take place, therefore reducing the need for bulk-jobs to modify player accounts or send out bulk-messages to players to inform them of changes on the server, which is also a common use-case for bulk-jobs.

Schedulers
Most of these platforms will not have an alternative to the SparkScheduler API, but it may be possible to use something native to the programming language you are using such as SetInterval() for JS and C# or threading for Python.

It is important to remember that if you create your own alternative to these features of the Spark API, you're not replacing everything GameSparks does under-the-hood so it is important to consider the performance impact of these custom features.

Migrating MetaCollections
You will also need to consider how you are going to migrate your existing MetaCollections to the new platform’s alternative. As mentioned above, in most cases, these platforms do not have a database you can just move your meta-docs into. You will therefore need to work out how to model the data to the destination platform and you might have to consider automating this process through the use of the platform’s native REST APIs rather than copying the data over manually.

GameSparks API Wrappers
When it comes to migrating your existing GameSparks code it is natural to consider creating wrappers for certain common functionality between GameSparks and the destination platform.

There will certainly be cases where this will speed things up for you during migration. Saving data to SparkPlayer and calls to MetaCollections could be replaced with a wrapper API around the destination platform’s alternatives for example. This would make copy/pasting code faster as you would not have to rewrite every call to the database or player object, you could just paste the details into your new wrapper API functions.

Just remember, when approaching this work, that any inefficiencies in your existing code will also be migrated. So, while API wrapper will speed up migration, they will hide issues that might cause you performance problems later on.

Asynchronous APIs
GameSparks did not use an asynchronous approach for its APIs.

When you execute a request to the database, it will hold up your script and wait for a response before continuing to execute the rest of your code. All of your GameSparks calls are synchronous. This is going to be an issue when it comes to adapting your code for migration.

Some of these destination platforms do not use synchronous calls and instead use asynchronous calls.

If using asynchronous calls, when you send a request to your database or a 3rd party service (HTTP request for example), your code will not wait for the response before moving on to the next command.

Therefore, in a case like this in GameSparks…

var coinsBal = Spark.getPlayer().getBalance("COINS");
Spark.setScriptData("coins", coinsBal);

You will always have null or undefined returned to the client because the script will not wait for the response from the database.

There are several ways to overcome this problem which will be discussed in the sections below as they are specific to each platform’s programming language and APIs.

Regardless of that kind of adaptation you need to make to migrate your code, asynchronous and synchronous calls also have their uses separately, so don't just consider converting all your database wrappers to synchronous and continue porting your code. In some cases, like logging and updates, you don't always need a response from the database before continuing with your script. Using asynchronous requests in these cases will speed up the script execution time so consider them wherever you don't need to get a response from the database or cloud-store.

Performance Bottlenecks
It is extremely important to perform an internal review of your Cloud-Code before undertaking a migration and porting your code over to the new platform.

These platforms dont operate the same as GameSparks does so any database and player-data flows which are currently inefficient will also be migrated to the new platform. Any bottlenecks you currently have in code are likely to be ported along with your code, even if you create a wrapper for your GameSparks code in the new platform.

Limitations
As with GameSparks, Cloud-Code on these platforms often have limitations you will need to be aware of ahead of time.

These limits can be something you are familiar with from GameSparks, like execution times, but there can also be limits like script size (lines or Kbs), concurrent requests (how many of these requests can be in-process at one time), how many custom requests can you create per game, etc.

These are covered in each of the sections below per platform, so make sure to review any limitations before deciding if migration will work for your game. Porting the Cloud-Code may be a major undertaking, but it would be worse if the work is all completed only to find the new platform cannot handle your Cloud-Code at load.

Beamable’s alternative to Cloud-Code is called Microservices. Similar to GameSparks, this feature allows you to create scripts which run on the server and allow you to send and receive custom code.

Microservices are very different from GameSparks however, so in this section we will discuss what the key differences are, and outline any limitations or notable features you need to know before starting your migration.

Cloud-Code: Microservices
The first thing to know about Microservices is that they are not once-off scripts which are triggered by the client, run on the server and then are done until the next time they are called. These Microservices are intended to group together similar functionality into a distinct service.

For example, if you need some custom scripts for serving player profile data, you might combine all those scripts into a “ProfileService” and add to that as you need more custom functionality. You should not be creating a new Microservice for each GameSparks event or module.

Each of these Microservices are deployed as standalone so you access them by referencing the Microservice itself, and then the name of the method you want to call in that Microservice. Beamable takes care of all the connection concerns and makes this very easy for you.

Microservices are written in C# and are created and edited through your Unity editor, which means there is no need for developers to switch between two different languages for frontend and backend development.

Key Concepts
There are some key aspects that make Beamable Microservices different from GameSparks Cloud-Code which we should briefly touch on before proceeding with direct migration aspects.

Containers & Docker
These Microservices use Docker containers. Containers might be a new concept for GameSparks developers but there isn't anything complicated you need to understand before using them. They are basically a package which contains all the software dependencies and config for your service/app so that it can be deployed anywhere with a single command, rather than needing to install and set up everything manually each time. This makes them lightweight and easy to scale.

Beamable takes care of all the setup and installation when it comes to these Microservices, so you don't even need to know they are there.

When you create a new Microservice script, you can sync it with your server instance, but you can also test and debug it locally. This means that there is no need to have a separate flow for testing through the portal with a test-harness like with GameSparks. Everything is done through the Unity editor and using your own IDE.

Asynchronous Calls
Beamable does not use synchronous calls for their Microservice API like GameSparks does. Therefore we will be using async functions instead. We will point out where this is important for each Microservice call we make, but for the most part you don't need to worry as it is usually a case of adding the “await” keyword to the call and making sure the method is async. Most IDEs will show you errors explaining that you need to handle these async calls which will make it easier to detect where they need to be used.

Modules
As mentioned, each of these Microservices should be built to serve a specific purpose, like containing all your player-profile scripts or all your multiplayer scripts, so it is not advised to load multiple libraries and classes to handle a multitude of functionality as you can in GameSparks Cloud-Code. You should aim to keep these Microservices as lightweight and as dedicated as possible.

Having said that, you can do something similar to GameSparks by loading custom or 3rd party libraries into these Microservices.

We cover an example of how to import libraries into Microservices in the section below on Http Requests. Importing custom content classes for use in Microservices is covered in the section below on Referencing Custom Content in Microservices.

Limitations
Similarly to GameSparks, Beamable Microservices come with their own limitations you need to consider.

There is a limit to the response time of any service call. This is 10 seconds, similar to GameSparks. However, with GameSparks, exceeding this limit results in the script being terminated and returning an error. With a Beamable Microservice call, you will get an error returned, but the script will continue to execute. It is extremely important to remember this as it means you can cause performance issues on your service-instance by letting the code continue to run after the timeout.

Missing Features
In the following sections we are going to go through some examples of how different GameSparks Cloud-Code components are handled in Beamble but we are going to leave out a few key features, so let's briefly talk about those here.

Bulk-jobs
There is currently no way to run something like the GameSparks bulk-jobs using Beamable. However, they do expose their APIs Microservices over REST. It is therefore possible to use an external service to run these jobs over a large number of players but it cannot be done with Beamable’s current tools.

Schedulers
Beamable currently does not have a built-in scheduler API but this feature is on their roadmap.

System Scripts & Every-Minute
There are currently no scripts which fire at set intervals in Beamable. However, since you can hit Beamables APIs over REST or through a Microservice you could utilize something like AWS CloudWatch and Lambdas to trigger specific Microservice scripts. We cover an example of this in another topic here.

Creating and Running Microservices
To create a new Microservice you will need to open the Microservice Manager window.

You will find this window in the Unity editor by going to the Window menu, then Beamable -> Open Microservice Manager

This can also be done from the Beamable Toolbox. Here you can create a new microservice using the Create New button. Name the service and click the blue Create button. You’ll now see your new microservice and its console.

Remember, these aren't just scripts that compile when you save them. They will compile when you make changes just like any other script in Unity, but in order to test those changes locally, you need to stop them using the Stop button then click Play to start them again. The Replay allows you to build and play with one click.

Once you are ready to push your changes to your backend you can click on the Publish button at the top of the Microservices Manager.

This will start the process of pushing the services to the backend. This will take some time and throughout the process you will see logs in the console. You can also see this progress if you open the portal and go to the Microservices tab.

Here you can see all of your Microservices and their current states, along with any deployments you made.

Note - You don't need to push your Microservices to your backend in order to start testing. You can run and connect to them locally and push once you are finished editing.

Executing Microservices
Executing a Microservice is pretty simple. Each Microservice you create has a “Client” class generated for it. This is what we use to execute the service calls.

For example my service called NewMicroservice has a class called NewMicroserviceClient. We can simply instantiate that and call one of the functions you created. Usually, when you create a new Microservice, Beamalbe adds a method called ServerCall() by default, but you can rename that if you want to.

So your Microservice might have a function like…

[ClientCallable]
public async Task<string> ServerCall()
{
  // This code executes on the server.
  return "hello world";
}

And your client can call this function like so…

NewMicroserviceClient _msClient = new NewMicroserviceClient();
string resp = await _msClient.ServerCall();

Note - Remember to build your service and restart it after any changes you make to the Microservice script.

MetaCollections
Beamable’s alternative to GameSparks MetaCollections is called Content. However, Content actually covers a number of different Beamable features, not just storing static data.

You can see if you click on the Create dropdown in the Content Manager tab that many other features are listed as Content.

The key concept to understand in relation to migrating your GameSparks features (not just MetaCollections) is that Content is cached, static definitions for your game’s features. This is more similar to SparkConfig rather than MetaCollections, but because all Content is cached and we can get Content with easy to use APIs available out of the box, Content is a good fit for replacing MetaCollections.

Example: Converting MetaCollection To Custom-Content
We will go through a quick example of how you might convert your existing GameSparks MetaCollections into a Content structure. For this example, we will use a common use-case where we need to get the base-description of an item. This item might be given to any player using its Id. Therefore, each item in the player’s inventory does not need to have all the details, we just need to know this player has x amount of itemId 10, and we can tell what itemId 10 is from the MetaCollection.

We can’t query Content however, we can only get individual content-objects by Id or by tag.

So an item in the “GameItems” MetaCollection might look like this...

{
  "_id": {
   "$oid": "591eda42c9e77c00012a6435"
  },
  "itemId": 1,
  "displayName": "Ember 1",
  "categoryId": "weapon",
  "starRating": 1,
  "powerModifier": 0.2,
  "iconId": "weapon_ember_portrait",
  "descriptionId": "ember",
  "modelId": "weapon_ember_full"
}

There won't be an existing Content definition that covers this case, so we will have to create our own.

You can start this by creating a new C# script in Unity, you can call this something like GameItemContent so that you know it comes from the GameItem MetaCollection structure. It would be helpful later on if you group these scripts together into a common folder. In these examples we call that folder GSContent.

Before we define any variables in our class, we need to change this from a MonoBehaviour to a ContentObject type. This will let us create new instances of this type from the Content manager, and save our GameItems to the manifest.

using Beamable.Common.Content;

[ContentType("game_items")]
[System.Serializable]
public class GameItemContent : ContentObject
{

}

If you go back to the Content Manager tab now, you will see your new Content type appear in the menu.

So now all we have to do is add the rest of our fields to the GameItemContent class.

public class GameItemContent : ContentObject
{
   public int itemId;
   public string displayName;
   public string categoryId;
   public int starRating;
   public float powerModifier;
   public string iconId;
   public string descriptionId;
   public string modelId;
}

Once those variables are added, you can go back to the Content Manager and add a new GameItem.

Using the inspector you can fill out the details for your item in the inspector tab. Once you have your items filled out, you can click on the Publish button at the top of the Content Manager tab. You will see a popup indicating which content needs to be synced.

If you want to make sure your Content was uploaded you can go to the admin portal. You should see a new category for your new Game Items, with the items you just published.

Referencing Custom Content in Microservices
This is a little complicated but it is a requirement if you want to be able to access custom content from your Microservices script.

If you only want to do validation client-side then this is no problem, you can skip this. However, for more security you might want validation to happen in the Microservice in which case you will need to follow these steps.

To allow the Microservice script to reference your custom content go to your GSContent folder and add a new Assembly Definition. We can call it CustomGSContentDef. You can do this by right-clicking in the folder, going to Create and then Assembly Definition.

You are going to need to add some assembly references to this definition. Make sure to hit the “apply” button before proceeding.

Note - It's common for this step to mess with your reference definitions for some of the files referencing the Beamable namespace. If this happens, first try to re-import the files from the folder. You can also restart Unity, which can help solve the problem. If that doesn't work, try removing the using statements in the scripts causing the problem and re-import them again.

Now we are going to add this assembly definition to our Microservice. You can find your Microservice folder at Beamable -> Microservices -> . You will find two files in there, click on the assembly reference file and add a reference for the CustomGSContentDef file we just created.

Remember to hit the “Apply” button to update the file.

Now we have covered Content as a replacement for MetaCollections and how to access custom Content in our Microservices. This process will be used in a number of other topics if you need to see examples. However, we will also repeat this in some examples below to help you understand how they can be used.

Agnostic Classes
It is possible to make custom content classes available for use in a microservice with the Agnostic attribute. To take advantage of this, include the [Agnostic] attribute above your custom content class. You can find out more about custom content and its functionality here.

[Agnostic]
[ContentType("MyCustomContent")]
public class MyCustomContent : ContentObject 
{
    public string customString;
    public int customInt;
}

HTTP Requests
Beamable does not specifically have a HTTP request API of their own like the GameSparks SparkHTTP API, so in order to send requests we can just the built in C# WebRequest class or you can use a library. Remember that you want to keep these Microservices as lightweight as possible so you should try to avoid importing libraries unnecessarily. Having said that, we are actually going to show how to import a popular JSON serializer so that we can parse the response of our web-request back into a format we can use.

In this example we are going to use a simple 3rd party API which will return a number of randomly generated names when hit. This is going to return a JSON string which we need to convert in order to use it in C#.

We will create a very simple class to model this data.

[System.Serializable]
public class NamesList
{
   public string[] names;
}

If you are following from the previous example, you should add this script to the common GS folder so we can assign it to the Microservice as an assembly reference.

Beamable Responses
The structure above might seem confusing to GameSparks developers or even experienced Unity developers, so it is important to cover a few notes about how Beamable parses Microservice responses.

Microservice responses are parsed using the Unity JsonUtility API. This API doesn't support deserializing many common root object types you might want to use. You cannot use dictionaries or lists for example. To get around this, you should use strictly typed objects. Within these objects you can add more complex structures.

This is the reason we are using the NamesList class here and why we need to add the assembly reference for our custom definitions below.

Adding libraries to Microservices is pretty simple. In the folder where your Microservice script is found (Assets -> Beamable -> Microservices) you will find the manifest file for your Microservice. You will need to add the Newtonsoft library dll to the Assembly References section. Remember to hit the “apply” button to make sure it gets updated.

Note - We won't cover importing the JSON.net package into Unity, but something to note is that in order for the Microservice to be able to build using the Newtonsoft dll you will have to allow that dll to be used on any platform. If you do not do this you will get an error when trying to build the Microservice indicating that the service cannot find the library.

Now you should be able to build your Microservice for the first time without errors.

The code we need here is pretty simple, we will list it step-by-step:

  1. Send the request to the API URL using the C# WebRequest API.
  2. Read the response as a stream and convert it to a JSON string.
  3. Convert the JSON string to a string[].
  4. Add this to our NamesList object
  5. Return to the client
private string apiUrl = "http://names.drycodes.com/10";

[ClientCallable]
public async Task<NamesList> GetNames()
{
  string respData;
  Debug.Log("Fetching Random Names...");
  // create a new webrequest //
  HttpWebRequest request = (HttpWebRequest)WebRequest.Create(apiUrl);
  // next we read the response as a stream until we get a JSON string //
  using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
  using (Stream stream = response.GetResponseStream())
  using (StreamReader reader = new StreamReader(stream))
  {
     respData = reader.ReadToEnd();
  }
  Debug.Log(respData);
  // we will convert this JSON string to a string array so we can return it  //
  NamesList namesList = new NamesList();
  namesList.names = JsonConvert.DeserializeObject<string[]>(respData);

  // example using the built in JsonUtility which requires a root object //
  // NamesList names = JsonUtility.FromJson<NamesList>("{ \"names\": " +respData+ "}");

  return namesList;
}

You will need to import the Newtonsoft.Json library and a few other C# libraries for the WebRequest and StreamReader classes, but once that is done, you should be able to build and run your Microservice.

You can also see an example of how to deserialize this data using the JsonUtility Beamable uses internally. This would save you having to import the JSON.net serializer, but it cannot handle complex data so we thought we would show both examples. With the JsonUtility API you need to provide a root name to this JSON string, which is why the example “names” field is added before parsing.

Now you can call this function from the client using the following code.

HttpTestMSClient _httpTestClient = new HttpTestMSClient();
NamesList resp = await _httpTestClient.GetNames();
for (int i = 0; i < resp.names.Length; i++)
{
   Debug.Log(resp.names[i]);
}

You will be able to see logs from your Microservices and then the names printed out in the console.

Callbacks: Endpoints
Beamable also lets you hit any Microservice function using a HTTP request. This would allow you to integrate a 3rd party callback, such as validating an email, updating a payment receipts, etc.

We have already covered how to create a Microservice, so we will show a very simple example. We will create a Microservice with a simple function which returns a “Hello World” string.

[ClientCallable]
public async Task<string> ServerCall()
{
  // This code executes on the server.
  return "hello world";
}

Up to this point, all the examples we showed would operate from the local Microservice or the backend but for this case we need to make sure this Microservice has been synced with the backend.

To do this, click on the “Publish” button in the Microservices Manager tab.

Here you will see options for deploying all your Microservices. We only need to deploy the “NewMicroservice” one we are testing with, however selecting only that Microservice actually causes the others to disabled, so keep that in mind.

Deployment can take several minutes throughout which you will see logs in the Unity console indicating progress. If you open up the Microservices dashboard of your portal you should see some updates.

Once you see the service state as “Running” you should be good to go.

Callback URL
To hit this endpoint you need the URL. The URL is structured as below…

https://api.beamable.com/basic/{CID}.{PID}.micro_{ServiceRoutingName}/{MethodRoutingName}

You can get the cid and pid from your Realm details which can be found by clicking the button in the top left of the portal.

Here you can see some details about your Realm.

You can test this through Postman, or just by sticking the URL in your browser.

Posting Data
Posting data to an endpoint is pretty simple, we just have to use a POST request and give the request a body.

For this example I'm going to use the same Microservice but modified to take a string and return the same string.

[ClientCallable]
public async Task<string> ServerCall(string input)
{
  // This code executes on the server.
  return input;
}

The body of your request has to be a specific structure. It needs to contain an array called “payload” which, for this example

Player Storage: Cloud-Save
This feature is called Cloud-Save in Beamable and it allows you to store player related information.

It is important to note that this is not the same as GDS or RuntimeCollections in GameSparks as the Beamable Cloud Save feature is not queried. It is instead stored as JSON data and synced to the backend from Unity’s data store (Application.persistantDataPath). You can treat this more like the SparkPlayer scriptData or privateData APIs in GameSparks Cloud-Code.

It is therefore more suited to player config or progression data, rather than complicated documents which need to be queried separately. This does not stop you migrating your existing data to suit this storage model, but you should consider what data can be exposed and stored on the client and what data should be only exposed through Microservices as this data has to be uploaded/downloaded on the client.

If you need an example of database access and storage, see the following section on Database Storage.

Example: Player Settings
For this example we will take a common GameSparks use-case for RuntimeCollection or GDS. We have a collection called “PlayerAccounts” which has a document structure like this…

{
  "_id": {
   "$oid": "5cf68431ceedc604e6f67b75"
  },
  "playerId": "5cf6843059fe981049f2c8a7",
  "displayName": "mehface",
  "language": "English",
  "volumeMusic": 50,
  "volumeSFX": 50,
  "notificationsPush": false,
  "notificationInGame": true,
  "autoCompleteWarning": true,
  "playerEmail": {
   "email": "not-registered",
   "status": "unverified"
  }
}

We know that we don't need the doc Id and the playerId fields because the Beamable SDK will make sure we are returning only our own user’s data. We also have two options here. We can decide to maintain this data on the client, in which case we will send and receive updates directly from the client. This might be unsafe in a case where we don't want to expose our player’s progress or inventory for example. Or, we can make this only accessible server-side through a

Microservice
Microservices cannot edit Cloud-Data, they can only read the data, so this might be important in your choice to use them.

We won't go into specific details in this example on how to set everything up in the client. There is an example from Beamable here which provides you with a similar example for uploading and downloading the data. All we’ve done in this example is changed the data to model our player settings example and created a constructor so that the player gets the default settings when their game gets set up for the first time.

The first thing we need is the GSPlayerSettings class which will outline the JSON example from above.

[System.Serializable]
public class GSPlayerSettings
{
   public string displayName;
   public string language;
   public int volumeMusic;
   public int volumeSFX;
   public bool notificationsPush;
   public bool notificationInGame;
   public bool autoCompleteWarning;
   public PlayerEmail playerEmail;
}

[System.Serializable]
public class PlayerEmail
{
   public PlayerEmail()
   {
       email = "not-registered";
       status = "unverified";
   }
   public string email;
   public string status;
}

As mentioned before, it's a good idea to put these custom classes into a common folder so you can share these data models in Microservices using an assembly reference.

From there, we have just modified the example in the tutorial linked above, to download/upload our PlayerSettings example instead of their auto-settings example.

// Download/Upload the current data stored for this player //
await beamableAPI.CloudSavingService.Init();
// get the data from the manifest //
await beamableAPI.CloudSavingService.EnsureRemoteManifest();
// Load settings //
GSPlayerSettings playerSettings = ReloadOrCreatePlayerSettings();

The main area we modified was where the settings were defined for the first time.

playerSettings = new GSPlayerSettings
{
   displayName = "mehface",
   language = "English",
   volumeMusic = 50,
   volumeSFX = 50,
   notificationsPush = false,
   notificationInGame = true,
   autoCompleteWarning = true,
   playerEmail = new PlayerEmail()
};

If you run the example you should see the new Cloud Data file in the portal under Cloud Saving.

Limitations
There is a size limit of 5mb per file on all Cloud-Save data. However, you can have as many files as you want on a player’s account.

Stats Service
There is another way to store data for the player in Beamable and that is through the Stats API.

Stats are different from Cloud Save. They are intended for use as analytics stats or KPIs associated with player accounts. They can be very useful for saving information which could be used later for analytics. We will show an example below using stats to save account settings like the last-login and the player registration status.

There is also an example here using Stats the player's level attribute for a leveling system.

Important
Stats are not intended to be used as a replacement for GameSparks JSON docs. Although they can be used to store strings, they should not be used to store player data as JSON strings.

Limitations
While there are no limitations on the data and number of stats you can apply to a player, it is important to note that Beamable strictly advises against using them to store JSON strings.

Database Storage: Microservice Storage
Beamable offers database storage in the form of their Microservice Storage feature. This is essentially a MongoDB instance hosted for your game by Beamable which can be easily connected to your Microservices. You can read about this feature here.

Setting Up A Storage Object
Setting up Microservice Storage is the same as setting up a new Microservice. In the Microservice Manager window, select Create New and pick the Storage option. Select which Microservice you want to add as a dependency and hit the Create button. This newly created Storage object will be accessible from the dependent microservices.

Writing To The Database
To write to the database, you first need to create a mapping class. A great place to create these mapping classes is in the autogenerated Storage Object class that was created earlier when we created the storage object. These classes can be found in Assets -> Beamable -> StorageObjects.

In this example, we will create a class to save player progression to the database. Be sure to include a field of type ObjectId, which comes from the MongoDB BSON package included with Beamable.

public class UserProgression
{
  public ObjectId Id;
  public long PlayerId;
  public int Level;
  public float Xp;
  public string Title;
}

From within a microservice we can access the storage object and write to it. The example below shows a client callable endpoint which writes to the satabase with updated player progression values.

[ClientCallable]
public async Task<bool> SaveProgression(int level, float xp, string title)
{
   // Success to return to client
   bool isSuccess = false;
   try
   {
       // Get the specific DataBase. This returns a connected Mongo Database
       var db = await Storage.GetDatabase<PlayerProgressionSO>();
       // Get a specific collection, where player progression is saved
       var collection = db.GetCollection<UserProgression>("playerProgression");
       // Filter for document to replace
       // We are looking for a document with a matching Player Id to the Player Id
          of the user who made the request
       var filter = new FilterDefinitionBuilder<UserProgression>()
                            .Eq(p => p.PlayerId, Context.UserId);
       // Replace the current document with the new information
       // If no document exists, create one
       var result = await collection.ReplaceOneAsync(filter, new UserProgression
       {
           Level = level,
           Xp = xp,
           Title = title,
           PlayerId = Context.UserId
       }, new ReplaceOptions
       {
           IsUpsert = true
       });
       // Was the document successfully replaced or created
       isSuccess = result.IsAcknowledged;
   }
   catch (Exception exception)
   {
       // Something went wrong, log the error
       Debug.LogError(exception.Message);
   }
   // Return the success flag to the client
   return isSuccess;
}

MongoDB Explorer Dashboard
Your microservice storage database can be viewed from your web browser. You can view both your local and remote instances. In the Microservice Manager click on the three-dot options button in the top right corner of your storage object.

Hover over the cloud or local menu and select “Goto data explorer”. This will take you to a Mongo Express page of your selected instance.

Here you can see your database named using the convention .

Select view to view the database and its collections. You will see your database’s collections and some general information about the database. This page allows you to manage your databases, collections and documents. This includes deleting, editing, creating and exporting collections and documents.

Reading From The Database
Reading from the database is similar to writing to it. You first have to get a connection to MongoDB and then get the specific collection you want to read from. Using FindAsync you can find various documents that fit the filtering criteria you provided. This will return a cursor, which you can iterate through.

The example below shows how we are returning a specific field of the player’s saved UserProgression object to the client.

[ClientCallable]
public async Task<string> GetTitle()
{
   // Get the specific data base. This returns a connected Mongo Database
   var db = await Storage.GetDatabase<PlayerProgressionSO>();
   // Get a specific collection, where player progression is saved
   var collection = db.GetCollection<UserProgression>("playerProgression");
   // Filter for the document we are trying to find
   // We are looking for a document, that matches the current users player id
   var filter = new FilterDefinitionBuilder<UserProgression>().Eq(p => p.PlayerId, Context.UserId);
   // We are trying to find the specific doc
   // The Batch size allows us to only look for 1 document
   var cursor = await collection.FindAsync(filter, new FindOptions<UserProgression>{BatchSize = 1});
   // We are grabbing the first document in the returned cursor
   var userProgression = cursor.First();

   // If the cursor was empty, we throw an error
   if (userProgression == null)
       throw new Exception($"Document not found for Player: {Context.UserId}");

   // Return the Players title to the client
   return playerProgression.Title;
}

Query Format
Microservice storage uses the standard MongoDB query format. You can use the FilterDefinitionBuilder class to more easily build document queries. You can find a quick start guide to querying MongoDB in C# here.

Indexing
As your database grows the IO speeds of your database will decline. You can make use of Indexes to improve the efficiency of read operations. The example below shows how to index the UserProgression collection using the PlayerId field. Since we will want to query the collection based on the PlayerId to get player specific documents, this will improve the query performance as your game scales.

public async Task IndexProgressionCollectionByPlayerId()
{
   var db = await Storage.GetDatabase<PlayerProgressionSO>();
   var collection = db.GetCollection<UserProgression>("playerProgression");
   var indexKeysDefinition = Builders<UserProgression>.IndexKeys.Ascending(p => p.PlayerId);
   await collection.Indexes.CreateOneAsync(new CreateIndexModel<UserProgression>(indexKeysDefinition));
}

Example 1: Daily Login Reward
The first example we are going to go through is intended to show how you can get and set some simple player data (SparkPlayer API), load some static server data (MetaCollections), deliver goods and currency to the player and return their rewards in the response.

There is a little set up needed for this example before we can start working with the Microservice so we will go through these steps first. We will not go into detail on how to set up Virtual Goods or Virtual Currencies because we have already covered those in other topics. Links to these topics are included below.

Virtual Currencies
In a previous topic here we covered how to create some Virtual Currencies. This is a simple process so we won't repeat it here, but we will show some code for crediting these currencies later on. We will use the COINS and GEMS currencies for this example.

Virtual Goods
In a previous topic here we covered how to create some GameSparks-style Virtual Goods.

We will make another category for the daily rewards so we can return those too.

Custom Content: Daily Rewards
We are going to have to create some custom content so we can process the rewards. We have already shown an example of how this can be done in the section above on MetaCollections, but we’ll go through it one more time specifically for these rewards.

We are going to have a very simple JSON structure for these rewards…

{
  "rewards": [
   {
     "rewardType": "currency",
     "currType": "COINS",
     "amount": 250
   },
   {
     "rewardType": "currency",
     "currType": "GEMS",
     "amount": 6
   },
   {
     "rewardType": "vg",
     "code": "daimond_sword",
     "amount": 1
   }
  ]
}

This is the kind of structure that is very simple to work with in GameSparks because we can load it as JSON or store it in a document in a MetaCollection, we could even use properties.

However, with Beamable, we need to make a Content Object out of this so we can work with it as a C# object.

To begin, you can create a C# script. In our case we called this script GSRewardContent. We are going to change this class from a MonoBehaviour to a ContentObject class and give it a content type. We mentioned in the section above about Content and Metacollections that you sometimes need to create an assembly reference to access this content from a Microservice. Refer to that section as you will have to do that for this case so we can check what rewards to deliver in our Microservice server-side.

We are then going to add definitions for our rewards inside the class so that we can create new rewards and configure them.

[ContentType("daily_reward")]
[Serializable]
public class GSRewardContent : ContentObject
{
   /// <summary>
   /// List of reward to be delivered to the player
   /// </summary>
   public List<RewardDef> rewards;
}

[Serializable]
public class GSReward
{
   /// <summary>
   /// List of reward to be delivered to the player
   /// </summary>
   public List<RewardDef> rewards;
}

[Serializable]
public class RewardDef
{
   public enum RewardType
   {
       COINS,
       GEMS,
       VG
   }
   public RewardType rewardType;
   public string vgCode = "n/a";
   public int amount;
}

You will notice the GSReward class that we also created. This is for parsing the response back from the Microservice. We can't return something complex like ContentObject so this is a simple version we’ll use so the response can be parsed.

If you save this script and head back into the Content Manager, you should see a new content-type appear.

You can create a new daily reward and add some rewards to it.

The next step will be to create a new Microservice which we will call after player login. Check out our topic here to see how to configure an authentication function. Remember that, where possible, try to group multiple service calls into Microservices instead of just creating one service for each call.

Since we need to add our custom content assembly definition to this microservice, you can go ahead and add that to the Assembly Definition References of your Microservice’s manifest file.

There are more details on how to do this in the section above on Referencing Custom Content in Microservices.

We need to return something meaningful from our post-login server function so that the client can detect what kind rewards have been delivered. Remember in previous sections we mentioned that Beamable needs strict object types in order to parse the response, this is why we created the GSReward class before.

There will be a number of steps to the code we need here:

  1. Load the stat from the player account and validate if it is null or not
  2. Convert the timestamp to a DateTime object
  3. Check If the last login was on a different date to the current date
  4. Deliver rewards, adding each reward to the GSReward object
  5. Update the stat to today’s timestamp
  6. Return the delivered rewards
[ClientCallable]
public async Task<GSReward> PostLogin(long playerId)
{
  string lastLoginKey = "lastLogin";
  string access = "public";
  Debug.Log($"Checking for daily reward {playerId}...");
  // declare the rewards //
  GSReward rewardsDelivered = new GSReward();
  rewardsDelivered.rewards = new List<RewardDef>();
  // First, we check the player's last login. If this is their first login //
  // we will initialize that stat //
  string statString = await Services.Stats.GetProtectedPlayerStat(playerId, lastLoginKey);
  // We might not have the Stat implemented yet, this is a common case when working from JS //
  // So lets check that here. //
  long lastLogin = 0; // If the stat does not exist, then this will automatically deliver the reward for today //
  if (!string.IsNullOrEmpty(statString))
  {
     lastLogin = Int32.Parse(statString);
  }
  // we will convert this to a DateTime object so we can better use it later //
  DateTime lastLoginDate = UnixTimeStampToDateTime(lastLogin);
  Debug.Log($"Last Login: {string.Format("{0:yyyy-MM-ddTHH:mm:ss.FFFZ}", lastLoginDate)}");
  // Now we can check if the dates match or not //
  if (DateTime.Now.Date != lastLoginDate.Date)
  {
     try
     {
        GSRewardContent dailyRewards = (GSRewardContent) await Services.Content.GetContent("daily_reward.tutorial_example");
        // loop through the rewards and deliver them to the user //
        foreach (RewardDef rewardDef in dailyRewards.rewards)
        {
           if (rewardDef.rewardType == RewardDef.RewardType.VG)
           {
              for (int i = 0; i < rewardDef.amount; i++)
              {
                 Debug.Log($"Granting {rewardDef.rewardType}, {rewardDef.vgCode}");
                 await Services.Inventory.AddItem("items.virtual_goods." + rewardDef.vgCode);
                 rewardsDelivered.rewards.Add(rewardDef);
              }
           }
           else
           {
              Debug.Log($"Granting {rewardDef.amount} {rewardDef.rewardType}");
              await Services.Inventory.AddCurrency("currency."+rewardDef.rewardType, rewardDef.amount);
              rewardsDelivered.rewards.Add(rewardDef);
           }
        }
     }
     catch (Exception e)
     {
        Debug.LogError(e.Message);
        return rewardsDelivered;
     }
  }
  // Save the current login date //
  long currTimestamp = ((DateTimeOffset) DateTimeOffset.UtcNow).ToUnixTimeSeconds();
  await Services.Stats.SetProtectedPlayerStat(playerId, lastLoginKey, currTimestamp.ToString());
  return rewardsDelivered;
}

public static DateTime UnixTimeStampToDateTime(long unixTimeStamp)
{
  // Unix timestamp is seconds past epoch
  System.DateTime dtDateTime = new DateTime(1970,1,1,0,0,0,0,System.DateTimeKind.Utc);
  dtDateTime = dtDateTime.AddSeconds(unixTimeStamp).ToLocalTime();
  return dtDateTime;
}

And then you can call this function using the following code…

PlayerServiceClient _playerServiceClient = new PlayerServiceClient();
GSReward resp = await _playerServiceClient.PostLogin(beamableAPI.User.id);
if (resp.rewards.Count == 0)
{
   Debug.Log("No Rewards Delivered...");
}
foreach (RewardDef reward in resp.rewards)
{
   if (reward.rewardType == RewardDef.RewardType.VG)
   {
       Debug.Log($"Virtual Good: {reward.vgCode}, {reward.amount}");
   }
   else
   {
       Debug.Log($"Currency: {reward.rewardType.ToString()}, {reward.amount}");
   }
}

You should be able to see your rewards being delivered from the console.

Emails

In this topic we are going to cover some of the options these alternative platforms offer related to email features.

GameSparks does not have its own email service but instead integrates the SendGrid API through the Spark.sendGrid() API. SendGrid is a very lightweight service to integrate, so in most cases it can be integrated via C#, JS, etc. You can also integrate it in the client if there is no alternative option for these alternative platforms. However, that would be very insecure as it would mean exposing your API credentials after which anyone could use them.

If the alternative platform does not have a Cloud-Code feature you could protect your credentials by sending the request to an AWS Lambda function first, and then to the SendGrid API.

In some cases, these alternative platforms also have their own email features which can be used for registration emails and password reset, so we will mention those here too.

While Beamable does have a Mail Service, it is not the same as GameSparks’ SendGrid API and is instead used for in-game mail, specifically player inbox and that sort of thing. You can see more about their Mail Service here.

In this section we will show you how to keep your existing SendGrid implementation and rebuild the SendGrid API in Beamable.

SendGrid API
If you wish to continue using your existing SendGrid account with Beamable, you can integrate the SendGrid API into a Microservice. You can check out our guide here for more information on how to create and use Microservices, we will only cover the basics here.

Setup & Preparation
Before you can integrate the SendGrid code you will need to make sure you have an APIKey setup in SendGrid. We won't cover that here, but if you are already registered on SendGrid there are very good tutorials on the flow when you set up a new API key. In fact, they even show example code of how to integrate SendGrid in a number of programming languages, which is what we are going to get working in our Microservice using Beamable.

However, these examples use the NuGet package for SendGrid and C# which can be tricky to get working with Unity correctly. We suggest getting the NuGet package manager plugin for Unity available here.

Once the package manager is installed you can search for SendGrid in the manager. Select the top one from the list and install it, this can take a while.

Once you have the Sengrid package installed you can use it with your new Microservice. Add it as an assembly reference with your Microservice.

Now we can add our code to the Microservice function call. It's pretty much the same as the example you would have gotten from setting up your API key in your SendGrid account.

  private string sendGridAPIKey = "<api-key>";

  [ClientCallable]
  public async void SendEmail(string email)
  {
     var client = new SendGridClient(sendGridAPIKey);
     var from = new EmailAddress("<your-company-email>", "<your-name>");
     var subject = "Sending with SendGrid is Fun";
     var to = new EmailAddress("<recipient-email>", "Example User");
     var plainTextContent = "and easy to do anywhere, even with C#";
     var htmlContent = "<strong>and easy to do anywhere, even with C#</strong>";

     // Create the Email //
     var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, htmlContent);
     // Send Email //
     var response = await client.SendEmailAsync(msg);
     // Check Response //
     var body = await response.Body.ReadAsStringAsync();
     Debug.Log(response.StatusCode.ToString());
  }

And that's it! You can then send an email out by calling the SendEmail request from the client.

Matchmaking

GameSparks MatchMaking is a very flexible and powerful feature. It incorporates a number of complex features like Real-Time servers and Matchmaking Scripts. These features may not be available for alternative platforms, so in this topic we are going to deal with two fundamental components needed to migrate your existing Matchmaking feature.

Thresholds
The basic Matchmaking config in GameSparks consists of a min and max set of players you would like to match with and a set of thresholds.

These thresholds are used to create conditions upon which matchmaking decisions can be made.

In GameSparks, these thresholds are pretty simple and are controlled by a single parameter called “skill” which is passed in through the MatchMakingRequest. Alternative platforms have similar functionality for their Matchmaking, though with different setup and API calls to enter a player into Matchmaking.

Matchmaking API
Oftentimes Matchmaking needs additional functionality in order for the feature to comply with the designers needs. There are a number of ways to do this discussed in the following section, but one way is with the SparkMatch API.

SparkMatch allows developers to control an existing instance of a match, add or remove players or edit the match-data payload. This is an extremely powerful tool to create custom match features however, it is not present for many alternative platforms. Where possible we will demonstrate workarounds for this.

Other Features
As mentioned above, the SparkMatch API can be used to control and extend the matchmaking feature, but it is also possible to add custom context to the match outside of the “skill” value passed into the MatchMakingRequest. You can do this with the “participantData” field, for example, matching only players from a specific region.

{
  "@class": ".MatchmakingRequest",
  "participantData": {
   "region" : "eu"
  },
  "skill": 0
}

This may not be possible in all alternative platforms but we will cover it where possible.

Matchmaking Scripts
Where an even more complex set of Matchmaking rules is required you might be using matchmaking scripts to manipulate player and match data during matchmaking. This is a very complex feature that is not common on other platforms. Where there is some overlap between this feature and the destination platforms matchmaking offering we will show examples, however, something to keep in mind is that where these platforms offer a “cancel matchmaking” API, you can do a lot of this work client-side by setting up custom timers that will cancel a matchmaking request after a given period of time and then issue another matchmaking request with different params.

GameLift FlexMatch
If these alternative platforms do not offer a solution to your existing Matchmaking configuration it is worth checking out AWS FlexMatch. FlexMatch offers a matchmaking service with a high-degree of configurability. In comparison with GameSparks, matchmaking criteria are designed through a script which allows you to add custom data to the match-instance and the player’s individual matchmaking ticket. You can also change criteria over time as with GameSparks thresholds and matchmaking scripts.

You can get more information on FlexMatch here and you can see an example of how to create a serverless implementation of the service here.

Beamable has a relatively straightforward matchmaking feature compared to GameSparks, however, there is a good deal of custom-code in order to set it up, so we will cover that in this topic.

Currently, Beamable only offers a basic matchmaking service. You cannot match by specific values or thresholds, like you can with GameSparks. Instead, everyone who is looking for a particular type of match is grouped together by default.

There is a guide here on their matchmaking functionality and this is also demonstrated in their example game here. We will cover the basics needed anyway so you have further information as to how this feature compares to GameSparks and how to migrate your code.

Note - This feature is still in development by Beamable and is actively being worked on to add features like thresholds and skill matching like you find in GameSparks. Check with Beamable to see what updates have been made to this feature since we completed this topic.

Game-Types
The first thing we need to start with is creating a new Game-Type content object. You can do this by going to the Content Manager window and right-clicking on the “game_types” menu option.

For this example the configuration is pretty simple, we don't need any rewards or leaderboard updates for this game-type, we just need a max-players (2 in the case of our 1v1 match) and a max wait duration after which the matchmaking will stop.

This SimGameType content object is going to act as the matchmaking options for the matches we want to create. We will use these in the next section.

You can see a few other attributes on this game-type object. “Min Players To Start” is the same as the minPlayers attribute in GameSparks matches. “Wait After Min Reached Secs” acts like the “Accept Min. Players” option for GameSparks thresholds. This is the number of seconds after which the match will revert to accepting the minimum number of players if there are no other players found.

There are two other options, Leaderboard Updates and Rewards. These are used by Beamable’s multiplayer service and can be ignored for matchmaking in this case.

Matchmaking API
The next step is to create a matchmaking API which can control our matches. This is pretty simple. It need to be able to:

  1. Take the match-type (the SimGameType we created above)
  2. Take a callback for match updates
  3. Take a callback for match complete
  4. Request matchmaking
  5. Cancel matchmaking
  6. Get a list of all other players in the match upon completion

This is roughly the same flow as GameSparks, but in Beamable it requires some customization.

GSMatchResult
The first thing we need is to create a class to represent the data we need out of a successful match. In GameSparks this is pretty simple, we need a matchId and a list of players and their Ids. In this example we aren't going to create a matchId because this is not automatically generated by Beamable, however, you could create a temporary group or room out of the match and give the match that ID if necessary. The other two attributes that can be useful is the target number of players and the remaining time.

Using this information you can match useful decisions as the match progresses.

/// <summary>
/// This is the object that will be returned from the match
/// </summary>
public class GSMatchResult
{
  /// <summary>
  /// List of playerIds for the players in the match
  /// </summary>
  public List<long> PlayersIds = new List<long>();
  /// <summary>
  /// The number of players required to match a match
  /// </summary>
  public int TargetPlayerCount;
  /// <summary>
  /// Remaining seconds in match
  /// </summary>
  public int SecondsRemaining;

  /// <summary>
  /// Creates a new instance of a match response used throughout matchmaking
  /// </summary>
  /// <param name="targetPlayerCount">The target number of players to complete a match</param>
  public GSMatchResult(int targetPlayerCount)
  {
     TargetPlayerCount = targetPlayerCount;
  }
}

We are creating a simplified version of the code-examples in the example game here. Check that example out for more details.

GSMatch
Now we can create the class that is going to handle all the matchmaking and callbacks. This is not too difficult but there are several parts that need to be considered.

  1. We need a loop which can keep checking for match updates at regular intervals. We have set this interval to 1 second which is usually fast enough for most games. This update process is going to be a new thread.
  2. We need to be able to cancel this thread and therefore cancel matchmaking for our player.
  3. We need to be able to raise a callback for updates (like new players joining the match), matchmaking completed (we found all the players we need) or the matchmaking process timed out.
/// <summary>
/// This is the match object where you can start the matchmaking process or cancel it
/// </summary>
public class GSMatch
{
  // Event callbacks
  public event Action<GSMatchResult> OnProgress;
  public event Action<GSMatchResult> OnComplete;
  public event Action<GSMatchResult> OnTimeout;

  private GSMatchResult _matchResult;
  private GSMatchResult _gsMatchResult;
  private MatchmakingService _matchmakingService;
  private SimGameType _simGameType;
  private CancellationTokenSource _matchmakingOngoing;

  /// <summary>
  /// Creates a new instance of the match
  /// </summary>
  /// <param name="matchmakingService"></param>
  /// <param name="simGameType"></param>
  public GSMatch(MatchmakingService matchmakingService, SimGameType simGameType)
  {
     _matchmakingService = matchmakingService;
     _simGameType = simGameType;
     _gsMatchResult = new GSMatchResult(_simGameType.maxPlayers);
  }

  /// <summary>
  /// Kicks off the matchmaking process
  /// Updates will be delivered using the event callbacks
  /// </summary>
  public async Task RequestMatch()
  {
     var handle = await _matchmakingService.StartMatchmaking(_simGameType.Id);
     try
     {
        _matchmakingOngoing = new CancellationTokenSource();
        var token = _matchmakingOngoing.Token;
        do
        {
           if (token.IsCancellationRequested) return;

           // Check if a new player has joined or left the match //
           if (handle.Status.Players.Count != _gsMatchResult.PlayersIds.Count)
           {
              _gsMatchResult.PlayersIds = handle.Status.Players;
              _gsMatchResult.SecondsRemaining = handle.Status.SecondsRemaining;
              OnProgress.Invoke(_gsMatchResult); // raise the progress update callback
           }

           // Tick down the matchmaking progress //
           MatchmakingUpdate update = new MatchmakingUpdate();
           update.players = handle.Status.Players;
           update.secondsRemaining = (handle.Status.SecondsRemaining-1);
           handle.Status.Apply(update);

           if (handle.Status.SecondsRemaining <= 0)
           {
              OnTimeout.Invoke(_gsMatchResult);
              await CancelMatchMaking();
              return;
           }

           await Task.Delay(1000, token);
        }
        while (!handle.Status.MinPlayersReached);
     }
     finally
     {
        _matchmakingOngoing.Dispose();
        _matchmakingOngoing = null;
     }
     // Invoke Complete //
     OnComplete.Invoke(_gsMatchResult);
  }

  /// <summary>
  /// Cancels the matchmaking process
  /// </summary>
  public async Task CancelMatchMaking()
  {
     await _matchmakingService.CancelMatchmaking(_simGameType.Id);
     _matchmakingOngoing?.Cancel();
  }
}

The important part of the class above is the RequestMatch() function. You can see where it is checking for a change in the player-count indicating someone has joined the match. It is also ticking down the match progress and checking if the match has ended.

Now that we have our request and response objects mocked up, we can look at an example of how to kick off matchmaking. For this example we will show an async method which you could call from a button click or anywhere else in your code.

async void StartMatchMaking()
{
   Debug.Log("Starting Matchmaking...");
   // get the game-type content for this match //
   var gameType = (SimGameType)await beamableAPI.ContentService.GetContent("game_types.1v1");
   Debug.Log($"GameType: {gameType.Id}...");
   // Now we can create our match object //
   GSMatch newMatch = new GSMatch(beamableAPI.Experimental.MatchmakingService, gameType);
   // Some examples of these matchmaking callbacks //
   // timeout callback //
   newMatch.OnTimeout += delegate(GSMatchResult result)
   {
       Debug.Log("Match Not Found...");
   };
   // progress callback - called when anything in the match is updated //
   newMatch.OnProgress += delegate(GSMatchResult result)
   {
       Debug.Log("Match Updated...");
       // How many players do we have atm //
       Debug.Log($" {result.PlayersIds.Count} / {result.TargetPlayerCount} Players...");
       Debug.Log($" {result.SecondsRemaining} Seconds Remaining...");
       foreach (long playerId in result.PlayersIds)
       {
          Debug.Log($"PlayerId: {playerId}");
       }
   };
   // Found Match callback //
   newMatch.OnComplete += delegate(GSMatchResult result)
   {
       Debug.Log("Match Found...");
       foreach (long playerId in result.PlayersIds)
       {
          Debug.Log($"PlayerId: {playerId}");
       }
       // >> use this player list to create a room or a game session //
   };
   await newMatch.RequestMatch(); // << Kick of matchmaking
}

You’ll need more than one player to test this process, but if you do kick off matchmaking for both players you should see the OnProgress and OnComplete callbacks being triggered and the logs appear in the console.

As mentioned already, from here you would want to do something with your player Ids like pass them to your multiplayer service.

Obviously if you are migrating from GameSparks to Beamable you will have your own multiplayer implementations. Those playerIds should be sufficient to get your players connected but if you do need a common Id between players like a matchId you could consider creating a temporary group, or even a temporary stat which you can apply to all players using an OID generator.