Unity Guide

Getting Started

The Heroic Labs platform provides a collection of game services known as the Game API. These services have been specially designed to work on any games device like consoles and VR headsets; and across games platforms like iOS, Android, and Windows Phone. The goal is to help game studios build beautiful social and multiplayer games which work at massive scale.

This SDK is one of a collection of official clients which are open-source on GitHub. The SDK is designed for use with Unity3D. It requires Unity version 4.0.x and higher (including 5.x) and is compatible with all Unity build targets. It can be used with Unity Pro and Unity Free licenses. You can check out our API Reference for more detail on the SDK.

Heroic Labs's Unity SDK makes use of coroutines and delegates for asynchronous execution which will not block the Unity main thread. This helps to keep games responsive. For more information, please see the section on Callbacks below.

Download

The code for the SDK is managed on GitHub with all versions and compiled releases downloadable here. Each ZIP package contains the source code for the specific version as well as a pre-compiled DLL in the "bin/Release/" folder. The "GameUpSDK.dll" is the file you need to add to your Unity project.

Install

To install the DLL in your project go into the Unity editor menu and select "Assets -> Import New Asset" and select the "bin/Release/GameUpSDK.dll" file. This will place the DLL in your Unity project's Assets folder.

Connect with the API

Once the SDK is installed create a new GameObject in your game and attach a new C# script component to it. Then add the following import:

using GameUp;

Copy and paste this code in your script, for example, in the Start method:

Client.ApiKey = "1fb234d5678948199cb858ab0905f657";

Client.Ping ((status, reason) => {
  Debug.LogFormat ("Could not connect to API got '{0}' with '{1}'.", status, reason);
});

The API key shown above must be replaced with one of your own from our Developer Hub. Run your game. A request will be sent to the Game API which will verify your API key is valid and our service is reachable.

Structure

The SDK is a collection of data classes and a couple of helper classes which handle the network requests and interact with the Unity Scripting API. All data returned from the Game API is deserialized into data classes which are immutable objects.

Callbacks

Each method in Client and SessionClient take one or more delegates which you implement to process the response received from the Game API. This approach keeps the SDK asynchronous; it uses coroutines to make network calls. These will not block the Unity event loop. This means that you "ask" for some information and some milliseconds later you'll get a callback with the response data.

Error Handlers

Requests which result in an error of some kind are passed through to the ErrorCallback delegate you define when invoking the method. The callback returns a HTTP Status Code and a detailed error message to help you debug the request.

The signature for the error delegate looks like:

Client.ErrorCallback errorHandler = (status, reason) => {
  // Add your own error handler logic. i.e.
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
};

Login and Authentication

Interaction with the Game API is split broadly into two categories. Requests which can be performed with just an API key and those which can only be performed by a user who has been logged into the service. The separation between these two levels of authentication is represented by the Client and the SessionClient classes in the SDK.

Heroic Labs offers a few different login options to make it easy for users to get started in a game.

Anonymous Login

This is the most effortless login option for users. It is done entirely without user interaction, so the user experience is completely frictionless.

It requires no external SDK integration and no signup or login dialogs of any kind. Anonymous Login uses a unique identifier to create an account in the service. The unique identifier is an external source of truth for the user.

For example, let's say you've decided you don't want to require a user to signup or login at all to play your game. You can use an identifier on their device to identity them with the service:

string uid = PlayerPrefs.GetString("UID");
if (string.IsNullOrEmpty (uid)) {
  uid = SystemInfo.deviceUniqueIdentifier;
  PlayerPrefs.SetString ("UID", uid);
}

Client.LoginAnonymous (uid, (SessionClient sessionClient) => {
  // we have a SessionClient to make requests with - let's check it's connected
  sessionClient.Ping (() => {
    Debug.Log ("Ping was successful with a valid apikey and token");
  }, (status, reason) => {
    Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
  });
}, (status, reason) => {
  Debug.LogErrorFormat ("Could not login user with ID: '{0}'.", uid);
});

We store the deviceUniqueIdentifier into PlayerPrefs so we can re-use the same identifier across the lifetime of the game. This prevents problems when platforms like iOS or Android change device identifiers in future OS updates.

Session Client Storage

The sessionClient received in a callback from login operations can itself be cached on the device. It is designed to be cached to reduce the need for a login request at the start of each game session.

For example, before checking PlayerPrefs for a "UID" to login with; we can store and restore the SessionClient. Imagine your users have started the game and you display a splash screen. While the splash screen is visible you could restore their session:

string cachedSession = PlayerPrefs.GetString ("SESSION");
if (String.IsNullOrEmpty (cachedSession)) {
  // login user *as shown above* and store their session in PlayerPrefs
  // i.e. PlayerPrefs.SetString("SESSION", sessionClient.Serialize ());
} else {
  SessionClient sessionClient = SessionClient.Deserialize (cachedSession);
}

Tradeoffs

Anonymous Login offers the most effortless user experience for new users but it is not without tradeoffs. If a user deletes the game (which wipes PlayerPrefs) and applies an OS update which changes their deviceUniqueIdentifier - when they reinstall the game their UID will be different so they won't be able to restore their old user account.

This may be an acceptable tradeoff for your game but if not you can link an Anonymous account with a social profile. This enables the account to be restored without a UID; instead it uses a Facebook or Google profile. This way a user can easily recover their account, save data and progress on a new device.

Social Token Login

This is the best option if you plan to use features from a social login provider like Facebook or you need more direct integration with a social network. You must handle social login yourself and send information to the Game API which creates or updates a user account.

This login option is available for Facebook and Google accounts, and requires handling the OAuth2 login flow in your game client.

Facebook

You will need the Facebook Unity SDK which can be downloaded here. Follow the Facebook Unity Examples on how to add the asset to your project. You must also complete the instructions on Facebook's developer guide on how to configure your iOS or Android game.

Now we can work with the Facebook SDK to complete a login:

// you must call FB.Init as early as possible at game startup
if (!FB.IsInitialized) {
  FB.Init (() => {
    if (FB.IsInitialized) {
      FB.ActivateApp();
      // Use a Facebook access token to create a user account
      var accessToken = Facebook.Unity.AccessToken.CurrentAccessToken.TokenString;
      Client.LoginOAuthFacebook (accessToken, null, (SessionClient sessionClient) => {
        // You now have a SessionClient with a linked Facebook account.
      }, (status, reason) => {
        Debug.LogErrorFormat ("Could not login to Facebook got '{0}'.", reason);
      });
    }
  });
}

// Execute in a button or UI component within your game
FB.Login("email", (ILoginResult result) => {
  if (FB.IsLoggedIn) {
    var accessToken = Facebook.Unity.AccessToken.CurrentAccessToken.TokenString;
    Client.LoginOAuthFacebook (accessToken, null, (SessionClient sessionClient) => {
      // You now have a SessionClient with a user account created with Facebook.
    }, (status, reason) => {
      Debug.LogErrorFormat ("Could not link Facebook account got '{0}'.", reason);
    });
  } else {
    Debug.LogErrorFormat ("Could not login to Facebook got '{0}'.", result.Error);
  }
});

You will need to pause and unpause your game when the Facebook SDK takes focus. For more complex examples on how to login with Facebook have a look here.

You can also link an existing sessionClient by changing the method call above with:

if (FB.IsLoggedIn) {
  // current sessionClient instance has the current logged in account
  Client.LoginOAuthFacebook (FB.AccessToken, sessionClient, (SessionClient sessionClient) => {
    // sessionClient from callback has linked social account *use it instead*
  }, (status, reason) => {
    Debug.LogErrorFormat ("Could not link Facebook account got '{0}'.", reason);
  });
}

Manage the Session Client

Once a user is logged in we have an instance of a SessionClient. You've seen above how we can store and restore this object each time the game is started but we also need to manage the variable so it can be shared across GameObjects. There are a few different ways to do this but the most common uses GameObject.GetComponent.

For example, let's imagine we've setup and stored a reference to the sessionClient instance in a class called StartupScript which executes as early as possible at game startup. We could access the public instance from another script like so:

public class SomeGameScript : MonoBehaviour {
  void Update() {
    StartupScript startupScript = GetComponent<StartupScript>();
    SessionClient sessionClient = startupScript.sessionClient;
    // do whatever we need with the SessionClient instance
  }
}

If you only ever plan to have one instance of SessionClient in your game (i.e. one logged in user) you could consider using a Singleton to maintain the reference to the instance. These are often referred to as "manager" classes. This is the most common scenario we've seen with games developed with the SDK.

For more information have a look at the Unity3d documentation here.

Tip

In rare cases you may want to manage the lifetime of the sessionClient more explicitly for performance reasons. This could mean using a global shared reference or an object pool if you need multiple instances.

Game Center

You can login a user with their Game Center credentials. You'll need to dive into native Objective-C code as the UnityEngine.SocialPlatforms.GameCenter doesn't expose enough information to enable authentication integration between Heroic Labs and Apple.

Once you have a generated Identity Verification Signature you can authenticate the user with Heroic Labs as below:

// These are passed in via your native Obj-C code...
string playerId;
string bundleId;
long timestamp;
string base64salt;
string base64signature;
string publicKeyUrl;

Client.LoginGameCenter (playerId, bundleId, timestamp, base64salt, base64signature, publicKeyUrl, (SessionClient sessionClient) => {
  // You now have a SessionClient with a linked Game Center account.
}, (status, reason) => {
  Debug.LogErrorFormat ("Could not login user with ID: '{0}'.", uid);
});

Email Login

Players can also use an email/password combination to take advantage of the Heroic Labs service. The platform exposes JSON endpoints to give developers maximum flexibility on how they’d like to design and integrate email-based login within their games.

Registration

To register an account with Heroic Labs, you need to provide a valid email address, password, password confirmation, and an optional player name. You can also optionally provide a nickname during registration.

string email = "example@example.com";
string password = "really-strong-password";
string passwordConfirmation = "really-strong-password";
string playerName = "player-name";

Client.CreateGameUpAccount(email, password, passwordConfirmation, playerName, (SessionClient sessionClient) => {
  // Store session just as before
}, (status, reason) => {
  Debug.LogErrorFormat ("Could not register account - got '{0}'.", reason);
});

Login

When a player has created an account they can login with:

string email = "example@example.com";
string password = "really-strong-password";

Client.LoginGameUp(email, password, (SessionClient sessionClient) => {
  // Store session just as before
}, (status, reason) => {
  Debug.LogErrorFormat ("Could not login - got '{0}'.", reason);
});

Password Reset

A reset password request will send an email to the player. This email contains a link to a page where they can set a new password.

string email = "example@example.com";

Client.ResetEmailGameUp(email, () => {
  // Password reset request was sent.
}, (status, reason) => {
  Debug.LogErrorFormat ("Could not login - got '{0}'.", reason);
});

User Accounts

The Login features in Heroic Labs handle the creation, update, and social link with user accounts in the service. Almost all features of the Game API involve requests you can only perform within the context of a user playing a game. With a user account you can save their games, trigger achievements, submit new scores to the leaderboards, etc.

Profile

The account contains useful fields like nickname, created_at, and other fields. Some fields like location are optional because we may not have the information available in the linked social profile(s). In these cases the field will return null.

An account can have multiple linked profiles. These profiles can be of anonymous, email or social types. Profiles enable the user to access their account using different authentication mechanisms.

To retrieve the account for a user:

sessionClient.Gamer ((Gamer user) => {
  Debug.LogFormat ("The user's name is '{0}'.", user.Name); // may be null
  Debug.LogFormat ("The user's nickname is '{0}'.", user.Nickname);
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

You can link multiple profiles to an account. This will enable the user to access the account using the linked profiles:

Note

You cannot link a profile that is already connected to a different account. First unlink the profile then link it to this account.

string facebookAccessToken = "your-facebook-access-token";
Client.linkFacebook(sessionClient, facebookAccessToken, () => {
  Debug.Log ("Facebook linking successful");
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

You can unlink a profile from an account similary to the linking process. Please note that the unlinking operation will fail if the account has only one profile.

Upon successful unlinking, the unlinked profile is deleted from the system and can be linked to another account.

string facebookId = "facebook-id";
Client.unlinkFacebook(sessionClient, facebookId, () => {
  Debug.Log ("Facebook Profile was unlinked successfully");
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Check Profile

You can check if a profile already exists in the system before attempting to link it to an account. This can also check if the current user is the owner of the profile as well:

string facebookAccessToken = "your-facebook-access-token";
Client.checkFacebook(sessionClient, facebookAccessToken, (bool exists, bool currentGamer) => {
  Debug.Log ("Profile exists? " + exists);
  Debug.Log ("Is it current gamer? " + currentGamer);
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Update nickname

You can update a user's nickname. The new value can only contain alphanumeric character and underscore. It should be between 8 and 32 characters:

sessionClient.UpdateGamer ("NewPlayerNickname", () => {
  // Nickname was updated.
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Datastore

The Datastore is our database system for your games. You can store global game configuration, objects per player, and share objects between players. It has a flexible query language that allows you to search a table for objects which match the conditions and filters you provide. You can build complex gameplay for PvP battle games, share maps, or other objects generated by players, and more.

Players can create any number of JSON objects. Each object can have a maximum size of 8KB. Each Datastore table can be configured with default permissions and each object can provide their own permissions.

For more information on the Datastore have a look at our concept documentation.

Write Objects

Each object is added to the Datastore with a key name and optionally an owner. An object can be written via Cloud Code or by a game client; when it's written by a game client it is always owned by the logged in player.

These objects must be valid JSON and conform to a table's schema. A schema is generated implicitly to match the fields of the first object written if the table is empty, or it can be modified in Hub.

An object can be updated if its permissions are valid. An object's permissions can also be modified when it is written to the Datastore. For example an object which has owner read+write permissions could be updated and have it's permissions modified to owner read only, of course you won't be able to modify this object again after that write completes.

A quick example on how to write an object to the Datastore without a change to its current permissions:

Dictionary<string, object> data = new Dictionary<string, object> ();
data.Add ("tank_count", 3);

String storageTable = "inventory";
String storageKey = "playerArmy";
sessionClient.DatastorePut (storageTable, storageKey, data, () => {
  Debug.LogFormat ("Successfully stored army data.");
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

You could also optionally update the permissions of the object:

sessionClient.DatastorePut (storageTable, storageKey, data, DatastorePermission.ReadWrite, () => {
  Debug.LogFormat ("Successfully stored army data.");
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

There are five different permissions that an object can have. The possible permissions are the same at the table and object level although an object's permissions must always be more restrictive than the table's default permissions.

  • DatastorePermission.None
    The object cannot be written or read by the client. A first write will succeed (which sets the permission) but follow up requests will fail.
  • DatastorePermission.ReadOnly
    The object can only be read by the owner.
  • DatastorePermission.WriteOnly
    The object can be be written by the owner but not read.
  • DatastorePermission.ReadWrite
    The object can be read and written by the owner.
  • DatastorePermission.PublicRead
    The object can be read by the owner and all other players but not modified.
  • DatastorePermission.PublicReadOwnerWrite
    The object can be read by the owner and all other players and modified by the owner.

Update Objects

You can update an object to "merge" new JSON fields into it's structure. You must ensure the schema has been updated to include the extra fields. You can also update individual fields in an existing object.

An update will create the key if it does not exist. The update can also modify a deeply nested JSON structure.

Dictionary<string, object> data = new Dictionary<string, object> ();
data.Add ("solider_count", 10);

sessionClient.DatastoreUpdate (storageTable, storageKey, data, () => {
  Debug.LogFormat ("Successfully updated army data.");
  // If we include the write code above the updated object state would be:
  // {
  //   tank_count: 3,
  //   solider_count: 10
  // }
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Read Objects

Every object you read from the Datastore is either owned by the current logged-in player, another player, or is not owned by any player (i.e. global). Search queries let you find objects owned by other players. You can use the value me to retrieve objects owned by the current player if you've not fetched their profile in another request.

Each object returned has a metadata field and a data field. The metadata holds owner, permissions, and other additional object information.

sessionClient.DatastoreGet (storageTable, storageKey, "me", (DatastoreObject datastoreObject) => {
  Object soldierCount;
  datastoreObject.data.TryGetValue("soldier_count", out soldierCount);
  Debug.LogFormat ("Successfully retrieved solider count: {0}", soldierCount);
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

You can retrieve the value of a global object:

String globalStorageKey = "configuration";

sessionClient.DatastoreGet (storageTable, globalStorageKey, (DatastoreObject datastoreObject) => {
  Object version;
  datastoreObject.data.TryGetValue("version", out version);
  Debug.LogFormat ("Successfully retrieved version info: {0}", version);
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Delete Objects

The current logged-in player can delete objects from the Datastore. A delete request to a key for an object which does not exist will return success.

sessionClient.DatastoreDelete (storageTable, storageKey, () => {
  Debug.LogFormat ("Successfully deleted army data.");
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Queries

The most powerful component of the Datastore is the query engine. It allows you to search across all objects in the game. It has a flexible query language inspired by Lucene so complex conditions like filters and rules can be expressed easily as queries.

The results of a Datastore query includes the entire object.

string query = "value.data.tank_count: {0 TO *]";

sessionClient.DatastoreQuery (storageTable, query, (DatastoreSearchResultList results) => {
  Debug.LogFormat ("Result of Datastore query: {0}.", results.Count);
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

For more information on the Lucene syntax, filters, and sort order; please refer to the Datastore concept documentation.

Achievements

Achievements are a great way to reward gamers, encourage exploration, and increase replayability. You can implement achievements in your game to encourage players to try out new features they might not normally use, or to approach your game with entirely different play styles.

There are two types of achievements in the Game API. Normal achievements can be rewarded after a one-off action is completed in-game. Incremental achievements require cumulative updates to be completed.

Achievements can be defined in one of 3 different states. Visible, Secret, and Hidden. The state configured for the achievement determines how much information is hidden away from a user when the achievements are displayed. This can be a great way to encourage a user to find out how to unlock an achievement whose description is not visible.

You must create achievements in Hub. Each achievement is created with a public ID which can be used to sort or rearrange the display order and a private ID which is used to make achievement requests and can only be found in Hub.

Fetch Achievement Progress

Let's say you want to show a nice UI element with all of the achievements a user has unlocked so far as well as their progress towards incremental achievements:

sessionClient.Achievements ((AchievementList achievementList) => {
  foreach (Achievement achievement in achievementList) {
    Debug.LogFormat ("'{0}' - '{1}'.", achievement.Name, achievement.Description);
    // you could add each achievement to the UI element and show it
  }
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Remember hidden achievements will not appear in the list of achievements until they have been unlocked.

Update Progress/Unlock

The heart of every game is the game state which is changed during a play session. When you update the game state for a user it is often a great opportunity to update any progress on achievements and display any unlocks.

For example, let's imagine we have a Tower Defense game where you protect your land from zombies. You could add an incremental achievement in Hub called "Zombie Blitzer" with a description "Vanquish 500 zombies!" and a count of 500. In game you will update your achievement like this:

int zombiesVanquished = 30; // you'd grab this data from your game state
// The ID for the achievement is in Hub
string achievementId = "12c00a1e6dff4a6fac7e517a8dd4e83f";
sessionClient.Achievement (achievementId, zombiesVanquished, () => {
  Debug.Log ("Updated progress on achievement.");
}, (Achievement achievement) => {
  Debug.LogFormat ("Achievement unlocked! - '{0}'.", achievement.Name);
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

In the code above we have 3 callbacks. The 3rd one is the usual ErrorCallback but we have two others each of which could be called. The first one is executed when the Game API responds to indicate the conditions for the achievement unlock have not been met yet but the achievement state has been updated.

The second one will return the Achievement and all of it's information when the achievement is unlocked. In the example above the condition for unlock is 500 zombies vanquished - in the code we incremented the count by 30.

Fetch All Achievements

Most users want to see what achievements can be unlocked in the game somewhere at the start of their play session. This helps provide an extra challenge during the play experience. We recommend you have a button in your main menu to show the list of achievements. You don't need the user to be logged into the game to make this request.

When a player clicks on the button you would retrieve all achievements and display a UI element:

Client.Achievements ((AchievementList achievementList) => {
  foreach (Achievement achievement in achievementList) {
    Debug.LogFormat ("'{0}' - '{1}'.", achievement.Name, achievement.Description);
    // you could add each achievement to the UI element and show it
  }
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

This method is very similar to the one in the SessionClient and uses the same data structure AchievementList but the response contains the global list of achievements available in the game.

Leaderboards

Leaderboards are a great way to add a social and competitive element to any game. They can be a huge help to drive up competition among your users. Just like achievements; leaderboards must be created in Hub.

Each leaderboard is created with a sort order and a name. The ID for the leaderboard is used to make requests within the game.

Player's Rank

Displaying the current user's rank and the top 50 in the leaderboard is simple. Just request the information from the leaderboard by its "ID":

// The ID for the leaderboard is in Hub
string leaderboardId = "6aed1e8dbf104fccc384bb659069ba69";
sessionClient.LeaderboardAndRank (leaderboardId, (LeaderboardAndRank leaderboard) => {
  Rank userRank = leaderboard.Rank;
  Debug.LogFormat ("'{0}' best rank is '{1}'.", userRank.Name, userRank.Ranking);
  foreach (Leaderboard.Entry entry in leaderboard.Leaderboard) {
    Debug.LogFormat ("Name: '{0}' - Score: '{1}'.", entry.Name, entry.Score);
    // you could add each leaderboard entry to a UI element and show it
  }
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

The sort order defined in the leaderboard setup on Hub will determine whether a larger or smaller value is considered the best scores.

Update

You'll want to update a user's score in a leaderboard usually at the end of some kind of play sequence. In a racing game you'll submit lap time scores to the leaderboard at the end of the race.

To update or submit a new score to a leaderboard:

long newScore = 7934;
// The ID for the leaderboard is in Hub
string leaderboardId = "6aed1e8dbf104fccc384bb659069ba69";
sessionClient.UpdateLeaderboard (leaderboardId, newScore, (Rank rank) => {
  Debug.LogFormat ("The user's best score is '{0}'.", rank.Score);
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

The Rank given back for the user in the successful callback is pretty handy. You could check their rank.Score and unlock an achievement for the user or warn them that their skill level is dropping by checking rank.BestRank versus their current rank.Ranking. It's a great way to challenge your players!

Fetch Leaderboard

When the game loads at startup it can be pretty handy to show a leaderboard and what sort of scores are most impressive at the time. You could have a button in your main menu to show this leaderboard. You don't need the user to be logged into the game to make this request.

When a player clicks on the button you would retrieve all leaderboard entries and display them in a UI element:

string leaderboardId = "6aed1e8dbf104fccc384bb659069ba69";
Client.Leaderboard (leaderboardId, (Leaderboard leaderboard) => {
  foreach (Leaderboard.Entry entry in leaderboard) {
    Debug.LogFormat ("Name: '{0}' - Score: '{1}'.", entry.Name, entry.Score);
    // you could add each leaderboard entry to a UI element and show it
  }
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

This method is very similar to .LeaderboardAndRank(...) in the SessionClient and uses the same data structure Leaderboard but the response contains the list of leaderboard entries without the player's rank information.

List All Leaderboards

If you'd like to add or change leaderboards after your game is live, you can fetch all the leaderboards in the system and render them in your game dynamically:

Client.Leaderboards ((LeaderboardList leaderboards) => {
  foreach (Leaderboard leaderboard in leaderboards) {
    Debug.LogFormat ("Leaderboard's name is '{0}'.", leaderboard.Name);
  }
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Social Leaderboards

Social Leaderboards work by filtering leaderboard entries to IDs that match the player's friends.

You'll need to store the current player's social ID in each leaderboard entry submitted:

long newScore = 7934;
string socialId = "player-facebook-id";
string scoretags = null; //optional
// The ID for the leaderboard is in Hub
string leaderboardId = "6aed1e8dbf104fccc384bb659069ba69";
sessionClient.UpdateLeaderboard (leaderboardId, newScore, scoretags, socialId, (Rank rank) => {
  Debug.LogFormat ("The user's best score is '{0}'.", rank.Score);
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

You can then query the leaderboard to retrieve entries that only match a given set of social IDs:

string leaderboardId = "6aed1e8dbf104fccc384bb659069ba69";
string[] socialIds = new String[2];
socialIds[0] = "facebook-friend-id";
socialIds[1] = "facebook-friend-2-id";

sessionClient.Leaderboard (leaderboardId, withScoretags, limit, offsetKey, socialIds, (LeaderboardAndRank leaderboardAndRank) => {
  Leaderboard leaderboard = leaderboardAndRank.Leaderboard;
  //Rank is recalculated against the list of friends.
  Rank rank = leaderboardAndRank.Rank;
  Debug.LogFormat ("The user's best score is '{0}'.", rank.Score);
  foreach (Leaderboard.Entry entry in leaderboard) {
    Debug.LogFormat ("Name: '{0}' - Score: '{1}'.", entry.Name, entry.Score);
  }
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Advanced Leaderboards

Score Value and Display Hint

Leaderboard scores can be of any numeric values including numbers with precision points. You can use the Display Hint (set during the Leaderboard Creation) to instruct the game client to show the value using a certain format. This becomes very handy for leaderboards that track the time, such as longest played sessions, where the score is a numeric value in seconds and the Display Hint is a format string that converts the seconds to the appropriate representation.

Scoretags

Scoretags allow you to add arbitrary JSON data to a leaderboard entry. For example, in a racing game, you might want to store the lap time and the weather condition of the day as a rainy day could affect the time.

// Set the race condition for this leaderboard entry.
IDictionary<string, object> scoretags = new Dictionary<string, object> ();
scoretags.Add ("race_condition", "wet");
long newScore =7934;
// The ID for the leaderboard is in Hub
string leaderboardId = "6aed1e8dbf104fccc384bb659069ba69";

sessionClient.UpdateLeaderboard (leaderboardId, newScore, scoretags, (Rank r) => {
  Debug.LogFormat ("The user's best score is '{0}'.", rank.Score);
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

To read the scoretags you can request them with the entries on the leaderboard:

string leaderboardId = "6aed1e8dbf104fccc384bb659069ba69";
boolean withScoretags = true;
sessionClient.Leaderboard (leaderboardId, withScoretags, (LeaderboardAndRank leaderboardAndRank) => {
  Leaderboard leaderboard = leaderboardAndRank.Leaderboard;
  Rank rank = leaderboardAndRank.Rank;

  Debug.LogFormat ("The user's best score is '{0}'.", rank.Score);
  Debug.LogFormat ("The user's scoretags is '{0}'.", rank.ConvertScoretags<String>());
  foreach (Leaderboard.Entry entry in leaderboard) {
    Debug.LogFormat ("Name: '{0}' - Score: '{1}'.", entry.Name, entry.Score);
  }
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Player-centric Leaderboards

Auto-offset allows you to automatically center (approximately) a leaderboard around the current player:

string leaderboardId = "6aed1e8dbf104fccc384bb659069ba69";
bool withScoretags = true;
int limit = 10;
bool autoOffset = true;
string[] socialIds = null;

sessionClient.Leaderboard (leaderboardId, withScoretags, limit, autoOffset, socialIds, (LeaderboardAndRank leaderboardAndRank) => {
  Leaderboard leaderboard = leaderboardAndRank.Leaderboard;
  Rank rank = leaderboardAndRank.Rank;
  Debug.LogFormat ("The user's best score is '{0}'.", rank.Score);
  foreach (Leaderboard.Entry entry in leaderboard) {
    Debug.LogFormat ("Name: '{0}' - Score: '{1}'.", entry.Name, entry.Score);
  }
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Auto-reset

Leaderboards can be reset daily, weekly or monthly as you request in the configuration on Hub. All entries on the leaderboard are automatically deleted at the configured time/date. This is useful for weekly and monthly leaderboards.

Leaderboard Tags

If you have lots of leaderboards in the system, you can group similar ones together using tags. Tags are arbitrary information attached to the leaderboard. You can have up to 8 tags per leaderboard.

Multiplayer

Multiplayer games bring users together to interact and compete in new and exciting ways. Heroic Labs makes it easy for users to instantly find opponents anywhere in the world, and jump right into a competitive play session.

This multiplayer feature is designed for Turn-based gameplay. Each multiplayer session is managed in a "Match". The match is initiated when enough players are available to fulfil the match setup requirements. A match lasts for as many turns as you like.

Inside a match each user submits data when it's their turn and indicates which of the other players should go next. This rotation between players happens until the game logic decides the match should end.

The multiplayer lifecycle consists of four key phases: matchmaking, match creation, turn submission, and match completion. Let's go through each of these in turn (pun intended)!

Direct Match Creation

Before a user can join a match they must create one. They can do this by requesting an automatic matchmaking session or by having a pre-selected list of opponents (Gamer IDs). The opponent(s) can then check their match list and participate in the match:

List<String> gamerIds = new List<String>();
// The current gamer is part of the match and will be set as the first turn taker.
gamerIds.Add ("gamer-id-1");
gamerIds.Add ("gamer-id-2");
gamerIds.Add ("gamer-id-3");

sessionClient.CreateMatch (gamerIds, (Match match) => {
  // It is your turn to play!
  Debug.Log ("Let the battle commence!");
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Matchmaking

Before a user can join a match they must create one. A match is requested with a number of players required to fulfil the multi-user play session. The number of players allowed in a match request is between 2 and up to 16.

The reason every user must create a match to be able to join one is because we've designed the API to optimise for shortest wait times. We believe this brings the most fun play experience in multiplayer games especially on mobile devices.

When every user requests to create a match we check to see if there are enough waiting users to fulfill the player count requirement. If so, they are selected and a new match is created and returned immediately to the game. If not, the user is added to a "play queue" and will be matched as soon as enough players are available.

Card-based battle games are a nice example where Turn-based matches work very well. Let's say we wanted to create a new 4 player card battle you'd add a button to your game menu where a user would request a match:

int requiredPlayerCount = 4;
sessionClient.CreateMatch (requiredPlayerCount, (Match match) => {
  // It is your turn to play!
  Debug.Log ("Let the battle commence!");
}, () => {
  Debug.Log ("No match immediately available - user has been queued.");
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Remember with this approach you don't request specific users as opponents. This would make it much much harder to get enough play sessions available in your multiplayer games. Instead your users are matched or queued up automatically to start the gameplay as soon as possible!

Matchmaking Queue Status

If there are no immediate matches available, the current gamer is queued and will be allocated a match as soon as one becomes available. You can request to see the status of the matchmaking queue:

sessionClient.GetMatchQueueStatus ((MatchQueueStatus queueStatus) => {
  Debug.Log ("You were queued at: " + queueStatus.QueuedAt);
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

You can also cancel the matchmaking request:

sessionClient.MatchDequeue (() => {
  Debug.Log ("Matchmaking request cancelled.");
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Match Listing

New matches are created as soon as the player count requirement for the match request is fulfilled. In our 4 player card battle game example you may not be the user who fulfilled the match requirement; you should check for the user periodically whether they've been matched.

You can fetch a list of matches for the user which you could display in a UI element:

sessionClient.GetAllMatches ((MatchList matchList) => {
  foreach (Match match in matchList) {
    if (!match.Active) {
      // match has ended you'll want to reward points, etc.
      // you'll also want to filter it from your UI element
    } else if (match.TurnGamerId.Equals(gamer.GamerId)) {
      // it's your turn to play!
    } else {
      // match is active but it's not your turn.
    }
    Debug.LogFormat("The match has ID: '{0}'.", match.MatchId);
    Debug.LogFormat("The current match turn is '{0}'.", match.TurnCount);
    foreach (String playerNickname in match.Gamers) {
      Debug.LogFormat ("Player '{0}' is in the match.", playerNickname);
    }
  }
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Each match can store data as a string which are submitted as part of each turn. We'll cover this in more detail below. You can retrieve the turns you need to sync up to the latest match state. In this example we'll use one of the match variables from our matchList:

int lastTurnCount = 2;
sessionClient.GetTurnData (match.MatchId, lastTurnCount, (MatchTurnList matchTurnList) => {
  // we now have all the newer turns since `lastTurnCount`
  foreach (MatchTurn matchTurn in matchTurnList) {
    // we can update the match state to sync it with the most recent turns
    Debug.LogFormat ("User '{0}' played turn number '{1}'.", matchTurn.Gamer, matchTurn.TurnNumber);
    Debug.LogFormat ("Turn data: '{0}'.", matchTurn.Data);
  }
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

It's important to cache the turn which has been last seen in the match with whatever match state you keep for each of the matches a user is playing. This is so you only need to request the minimal list of turns to bring the game up to date; but if you ever want to restore all list of match turns from the very first turn use 0:

sessionClient.GetTurnData (match.MatchId, 0, (MatchTurnList matchTurnList) => {
  // same as above but you have all turns right from the start of the match
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Turn Submission

Every user in a match will often have more than one turn in a play session. The turns will continue until the game decides based on the game logic that the match is complete.

When it is a user's turn you can submit data in whichever format you want to use (i.e. JSON, XML, plain text), and need to also send who should play next and the last turn count this player saw. This is useful to make sure the player is making a move based on the most up to date match state.

For example, in our card-based battle game example you'd keep track of the last turn seen for this match, as well as the list of players, and use the match.MatchId to submit a turn:

// you'll need to keep track of match state in multiplayer games
int lastTurnCount = 2;
string nextPlayerNickname = "abc";

// you should use whatever format best represents your match state changes
string turnData = "e4 e5"; // For example - chess uses portable game notation (PGN)

sessionClient.SubmitTurn (match.MatchId, lastTurnCount, nextPlayerNickname, turnData, () => {
  Debug.Log ("Your turn has been submitted.");
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Multi-match Status Update

You can request a status update for every match in the match list in one request since a given timestamp. The response data contain match status along with all new turn data across all the matches.

The timestamp is a Unix timestamp and it should be the highest value of the updated_at field across all the matches.

sessionClient.GetChangedMatches(match.UpdatedAt, (MatchChangeList list) => {
  Debug.Log (list.Count + " matches have changed.");
}, failure);

Match Completion

When the game decides a match should end you can submit a request to mark the match as completed. This request can only be made by the user whose turn it currently is in the match.

For example, let's imagine your user is playing their 4 player card-based battle game and it becomes their turn. The game state for the match shows that the previous player made a bad choice of battle card and your user has won the game.

You can mark the game complete with:

sessionClient.EndMatch (match.MatchId, (string matchId) => {
  Debug.LogFormat ("The match '{0}' has ended.", matchId);
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

In some games you'll want to let your players forfeit the match. Heroic Labs only allows players to forfeit the game when it is not their turn. To forfeit the match for the user when it is not their turn:

sessionClient.LeaveMatch (match.MatchId, (string matchId) => {
  Debug.LogFormat ("You have left the match '{0}'.", matchId);
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Cloud Code

Cloud Code is a Lua-based code execution service for the Heroic Labs platform. It allows you to execute custom logic on demand, on a schedule and even after specific events have occured. To learn more about Cloud Code, take a look at Cloud Code Guide.

You can invoke a Cloud Code function directly from the client:

string module = "race"; //from Hub
string function = "calculate_average_lap_time";
string trackName = "silverstone";

Dictionary<string, string> data = new Dictionary<string, string> ();
data.Add ("track_name", trackName);

Client.executeCloudCodeFunction (module, function, data, (IDictionary<string, object> result) => {
  object averageTime;
  result.TryGetValue("average_lap_time", out averageTime);
  Debug.Log ("Average lap time for track {0} is {1}", trackName, averageTime.ToString());
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Tip

You should use sessionClient.executeCloudCodeFunction() if your function requires a user profile.

Mailboxes

Mailboxes are an in-game persistent message inbox for each user. Each user has a dedicated mailbox that can store up to 100 messages. Once the mailbox becomes full, the oldest message is dropped to make room for the newer message.

Have a look at the concept documentation to learn about sending mailbox messages.

List Messages

You can retrieve a list of messages which are newer than a given timestamp. By default, this will only return the message envelopes to save bandwidth on mobile devices, however you can request the message bodies to be included in the result in the same listing operation.

sessionClient.MessageList (false, (MessageList messageList) => {
  Debug.Log("Received {0} messages", messageList.Count);
  foreach (Message message in messageList) {
    Debug.LogFormat ("Message {0} has subject: {1}", message.MessageId, message.Subject);
  }
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Read Messages

You can mark a message as read in the system. Once a message is read, the read_at field is set to a positive integer and the message cannot be mark as unread. This field does not change upon further reads of the same message.

// this will set the message `read_at` field to the current server timestamp.
sessionClient.MessageGet ("message-id", true, (Message message) => {
  Debug.Log("Retrieved message with subject: {0} with body: {1}", message.Subject, message.Body.ToString());
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Delete Messages

Deleting a message can be done with a simple call to the platform. All records of the message is removed immediately once the message is deleted (or expired). This operation cannot be reversed.

sessionClient.MessageDelete ("message-id", () => {
  Debug.Log("Message was deleted.");
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

In-App Purchase Verification

Hard, good-quality work deserves reward. The vast majority of gamers understand that the quality of game experiences they've come to expect cannot feasibly be delivered free of charge, and are willing to fairly compensate you. This is where In-App purchases are best used in your games.

Unfortunately there are always a small number of users who expect something for nothing, and are willing to cheat their way to premium rewards and content. There are a number of ways to trick a game into delivering rewards or content which haven't been purchased. These are known as In-App Purchase attacks.

In our best effort to support you and protect the revenue potential in your games we have made it extremely simple to defend against many of these purchase attacks. We have developed a verification API which connects with Google and Apple to verify each purchase transaction.

You will need to configure your game in Hub to enable this feature but once setup the game client logic is very simple.

Google

When you perform a purchase with Google's In-App Purchase API you will receive a "transaction receipt" as part of the information returned. This should immediately be verified before you enable the perk associated with the purchase.

For example, in a side-scroll platformer game you might give extra lives or special weapons to a user for their in-app purchase. Immediately after you complete the purchase through Google's API you should send the purchase receipt for verification:

string purchaseToken = "abc"; // The token Google returns for the purchase.
string productId = "com.mygame.extralife"; // The ID for the product which the user just purchased.
sessionClient.PurchaseVerifyGoogleProduct (purchaseToken, productId, (PurchaseVerification verification) => {
  if (!verification.PurchaseProviderReachable) {
    // Sometimes Google's IAP API fails when it does you must decide whether to verify
    // later or accept the purchase. We recommend you accept it - it keeps users happy!
  } else if (!verification.SeenBefore && verification.Success) {
    // The purchase is valid
  } else if (verification.Success) {
    // The purchase may be a repeat but is valid if you're verifying a restored purchase
  } else {
    // We recommend you reject the purchase because it has failed verification
  }
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Google has a separate IAP API for subscription purchases which need to be validated with a separate method:

string purchaseToken = "abc"; // The token Google returns for the purchase.
string subscriptionId = "com.mygame.extralifesubscription"; // The ID for the subscription which the user just purchased.
sessionClient.PurchaseVerifyGoogleSubscription (purchaseToken, subscriptionId, (PurchaseVerification verification) => {
  // Same logic as above
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Apple

The Apple In-App Purchase API works similarly to Google's although you do not need to distinguish between subscriptions and one-off purchases with verification.

string purchaseReceipt = "abc"; // The receipt Apple returns for the purchase.
string productId = "com.mygame.extralife"; // The ID for the product which the user just purchased.
byte[] payload = Encoding.UTF8.GetBytes (purchaseReceipt);
string receipt = System.Convert.ToBase64String (payload);
sessionClient.PurchaseVerifyApple (receipt, productId, (PurchaseVerification verification) => {
  if (!verification.PurchaseProviderReachable) {
    // Sometimes Apple's IAP API fails when it does you must decide whether to verify
    // later or accept the purchase. We recommend you accept it - it keeps users happy!
  } else if (!verification.SeenBefore && verification.Success) {
    // The purchase is valid
  } else if (verification.Success) {
    // The purchase may be a repeat but is valid if you’re verifying a restored purchase
  } else {
    // We recommend you reject the purchase because it has failed verification
  }
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Platform Specific Handlers

Working with In-App Purchase APIs from platform specific vendors is one area when it's not possible to use Unity's platform abstractions to help with the development of your games. The IAP services can only be used on the Android and iOS mobile deployment targets. You can use Unity's Platform Dependent Compilation definitions to handle code which is platform specific:

 #if UNITY_ANDROID
  // Use a library for Google's IAP API with the `PurchaseVerifyGoogleProduct(...)`
  // and `PurchaseVerifyGoogleSubscription(...)` methods.
 #endif

 #if UNITY_IOS
  // Use a library for Apple's IAP API and the `PurchaseVerifyApple(...)` method.
 #endif

Cloud Storage

Note

Cloud Storage has been replaced with the Datastore. It has a simpler API packed with more features like explicit object permissions and global data. You can read more about it on this page.

The first thing your game will do after a user has logged in is offer the option to set game preferences and select a save game slot. With Cloud Storage you can store up to 8KB of data per user. A user can then access their information in a secure manner within the game.

You cannot use Cloud Storage to share information between users; each user's data is stored privately and cannot be accessed by another user in the game.

Read Object

Cloud Storage is a key-value storage service. This means you give each piece of information you want to store a name used as a key to identify it when you want to read it back.

For example, if you name each save slot in your game something like "SaveGame1", "SaveGame2", etc. You could read back the information in your first save slot:

sessionClient.StorageGet ("SaveGame1", (IDictionary<string, string> values) => {
  string coinsCollected;
  values.TryGetValue ("coins_collected", out coinsCollected);
  Debug.LogFormat ("The user has collected '{0}' coins.", coinsCollected);
}, (status, reason) => {
  if (status == 404) {
    // no key for this user in cloud storage - you may want to handle this scenario
    return;
  }
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Write Object

Let's say you want to save the coinsCollected from a user's game progress to Cloud Storage. You can build up a IDictionary<string, string> and store it in a key named "SaveGame1":

IDictionary<string, string> saveGame = new Dictionary<string, string>();
saveGame.Add ("coins_collected", "2000");

sessionClient.StoragePut ("SaveGame1", saveGame, () => {
  Debug.Log ("Successfully saved game.");
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Delete Object

In some cases a user may want to delete their first save game perhaps to make room for another playthrough. To delete information for a key named SaveGame1:

sessionClient.StorageDelete ("SaveGame1", () => {
  Debug.Log ("Successfully deleted game.");
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Shared Storage

Note

Shared Storage has been replaced with the Datastore. It has a simpler API packed with more features like explicit object permissions and global data. You can read more about it on this page.

Shared Storage enables any player to find and share content created by other players. This is great for PvP battles, ghost players in racing games, and open-world games where players can react to other players' content.

Players can create any number of shared JSON objects that can be found and retrieved using our powerful query engine.

Each shared object is divided into two regions. Both regions are searchable by all players in a game. The public region of a shared object can be modified directly by the game client while the protected region is only modifiable via Cloud Code.

For more information on Shared Storage have a look at our concept documentation. You can also find information on Cloud Code in the guide.

Write Shared Objects

Similar to Cloud Storage you can store objects to Shared Storage in keys. These objects must be valid JSON. Shared objects can only be written to the public region by the game client.

A write request to a key that does not exist will implicitly create it so you don't need to check if the object exists before a write operation. Shared objects can be written and rewritten at any time; as often as a game requires.

Dictionary<string, object> data = new Dictionary<string, object> ();
data.Add ("tank_count", 3);
string storageKey = "playerArmy";
sessionClient.SharedStoragePut (storageKey, data, () => {
  Debug.LogFormat ("Successfully stored army data");
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Update Shared Objects

The update operation allows you to "merge" new JSON into a public region of an object. An update will create the key if it does not exist. The update can also modify a deeply nested JSON structure.

Dictionary<string, object> data = new Dictionary<string, object> ();
data.Add ("solider_count", 10);
sessionClient.SharedStorageUpdate (storageKey, data, () => {
  Debug.LogFormat ("Successfully updated army data.");
  // Final object server state:
  // {
  //   tank_count: 3,
  //   solider_count: 10
  // }
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Read Shared Objects

When you read objects from Shared Storage they are returned with both the public and protected regions. If one of these regions has never been written that field will contain null.

sessionClient.SharedStorageGet (storageKey, (SharedStorageObject data) => {
  Debug.LogFormat ("Successfully retrieved army data: {0}", data.ConvertPublic());
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Delete Shared Objects

Game clients can only delete the public region of a key and only the user who owns the object can delete it.

A delete request on a key which does not exist or does not have a public region will return success.

sessionClient.SharedStorageDelete (storageKey, () => {
  Debug.LogFormat ("Successfully deleted army data");
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

Queries

The most powerful feature available in Shared Storage is the query engine. It allows you to search all shared objects from within a game. It has a flexible query language inspired by Lucene so complex conditions like filters and rules can be expressed easily as queries.

In addition to the powerful search engine, you can use filter keys to limit the scope of the search query to only data that match a given shared object key for all players. This is useful if you have similar JSON fields across different stored JSON objects.

The results of a Shared Storage query include both the public and protected regions of objects.

string query = "value.public.tank_count: {0 TO *]";
sessionClient.SharedStorageSearchGet (query, (SharedStorageSearchResults results) => {
  Debug.LogFormat ("Result of shared storage query: {0}.", results.Count);
  foreach (SharedStorageObject result in results) {
    Debug.Log ("Shared Storage Object: {0}.", result.ConvertPublic());
  }
}, (status, reason) => {
  Debug.LogErrorFormat ("The request failed with '{0}' and '{1}'.", status, reason);
});

For more information on the Lucene syntax, filter keys and sort order; please refer to the Shared Storage concept documentation.

Feedback

We're constantly improving the Heroic Labs platform and services; our goal is to help game studios build beautiful social and multiplayer games which work at massive scale.

We create new features, optimise the performance of the platform, and provide the foundation to rapidly build games as large as Boom Beach, Dota 2, and eventually World of Warcraft!

We welcome all feedback you have about the service, no matter how small. We're especially interested in limitations you encounter that delay or prevent you from completing your next great game.

You can reach us anytime at support@heroiclabs.com.

Next Steps

This guide contains code samples and information on how to use all of the features in the Game API. You can find more detailed information on what each of the features in the Game API can do as well as why they've been designed the way they have in our main documentation.

Nevertheless we are obsessed with creating the perfect developer experience. It should be effortless to integrate our SDK and intuitive to familiarise yourself with the feature set. If you have any questions or need additional features please let us know.

GitHub Contribution

The codebase for this SDK is available on GitHub. All code is licensed as Apache2 which means you can modify the code, redistribute it, and can repurpose it for whatever use you have.

We would be grateful if you'd upstream changes which make the codebase easier to work with. We welcome any pull requests you suggest.

Issue Tracker

We use GitHub's issue tracker to handle all communication on bug reports and feature requests. If you find a bug, have any questions about the code, or would like to propose improvements to the design of the SDK please do open an issue:

https://github.com/gameup-io/gameup-unity-sdk/issues

Support

You can always reach us at support@heroiclabs.com or via our contact page. If you have custom feature requests which you'd like us to develop specially for your games let us know.