Android Guide

Getting Started

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 all devices running Android 2.3.3+. You can check out our API Reference for more detail on the SDK.

Heroic Labs' Android SDK makes use of the OpenTSDB Async library for asynchronous execution. This helps to keep games responsive and code more maintainable. For more information, please see the section on Callbacks below. The SDK also uses OkHttp to make reliable HTTP requests.

Install

Maven Central

The SDK is available from Maven Central. Include the library in your project:

dependencies {
  compile 'com.heroiclabs:heroiclabs-sdk-android:0.6.3'
}

Make sure you have the required permissions in your Manifest file:

<uses-permission android:name="android.permission.INTERNET"/>

Connect with the API

Below we build a new Client with your API key (from Hub) and execute a ping request:

Client client = HttpClient
  .builder("your api key here")
  .build();

Callback<Void, Throwable> errorCallback = new Callback<Void, Throwable>() {
  @Override
  public Void call(Throwable err) throws Exception {
    if (err instanceof ErrorResponse) {
      ErrorResponse error = (ErrorResponse) err;
      System.err.println("Server responsed with error " + error.getMessage());
    } else {
      System.err.println("Operation failed because " + err.getMessage());
      err.printStackTrace();
    }
    return null;
  }
};

client.execute(PingRequest.builder().build())
  .addCallback(new Callback<Void, Response<Ping>>() {
    @Override
    public Void call(Response<Ping> response) {
      System.out.println("Ping was successful");
      return null;
    }
  })
  .addErrback(errorCallback);

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.

Note

To keep code examples short, we'll reuse the `errorCallback` above throughout this guide.

Structure

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

Callbacks

Callbacks in Async's Deferred objects are very similar to Promises. They are invoked when the deferred object they're attached to is populated with a result. We can attach callbacks to handle the resolved value and to handle errors. You can attach multiple callback blocks together to allow for a chained execution of operations.

The only difference between addCallback and addCallbackDeferring is to ensure the correct type is enforced for the callback given. When chaining SDK calls you'll usually want to use addCallbackDeferring as the Client's execute method returns deferred objects.

client.execute(LoginAnonymousRequest.builder("unique device ID").build())
  .addCallbackDeferring(
    new Callback<Deferred<Response<Gamer>>, Response<Session>>() {
      @Override
      public Deferred<Response<Gamer>> call(Response<Session> response) {
        System.out.println("Successfully logged in anonymously.");

        // Let's retrieve our first session.
        Session session = response.get();

        // Now we can try getting the gamer's info.
        GamerGetRequest request = GamerGetRequest
          .builder(session)
          .build();
        return client.execute(request);
      }
  })
  .addCallbackDeferring(
    new Callback<Deferred<Response<CloudStorage>>, Response<Gamer>>() {
      @Override
      public Deferred<Response<CloudStorage>> call(Response<Gamer> response) {
        System.out.println("Nickname is: " + response.get().getNickname());

        // We can get the session again from the request/response chain.
        Session session = response.getRequest().getSession();

        // Maybe we'll load their save data.
        CloudStorageGetRequest request = CloudStorageGetRequest
          .builder(session, "SaveGame1")
          .build();
        return client.execute(request);
      }
  });
  // ...and more callbacks can follow!

Error Handlers

The SDK internally translates all the HTTP errors from OkHttp into ErrorResponse objects. These errors and non-HTTP exceptions that occur within the SDK code are passed via the addErrback method in the Async library to any registered error handlers.

Because the error that triggeres the 'errback' can be anything from a network IOException to exceptions thrown by game client code, the first thing you should do is check the instanceof of the input parameter.

.addErrback(new Callback<Void, Throwable>() {
  @Override
  public Void call(Throwable err) throws Exception {
    if (err instanceof ErrorResponse) {
      ErrorResponse error = (ErrorResponse) err;
      System.err.println("Server responsed with error " + error.getMessage());
    } else {
      System.err.println("Operation failed because " + err.getMessage());
      err.printStackTrace();
    }
    return null;
  }
})

Logging

The SDK uses SLF4J internally to provide raw debug insight into requests being sent and responses received. Game clients should add as dependencies and configure their own SLF4J-compatible logging implementations to suit their needs. There are many excellent Android-oriented implementations, such as logback-android and SLF4J Android.

This is entirely optional. If no logging implementation is provided, the SDK will default to no-op logging.

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 requiring a Session object when constructing a request.

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:

import android.provider.Settings.Secure;

String androidId = Secure.getString(getContext().getContentResolver(), Secure.ANDROID_ID);

SharedPreferences prefs = this.getSharedPreferences("YOUR.PACKAGE.NAME", Context.MODE_PRIVATE);
androidId = prefs.getString("androidId", androidId); // get existing androidId, if not use new androidId;
final SharedPreferences.Editor prefsEditor = prefs.edit();
prefsEditor.putString("androidId", androidId); //store this for future logons in case the session is expired

LoginAnonymousRequest request = LoginAnonymousRequest.builder(androidId).build();
client.execute(request)
  .addCallbackDeferring(new Callback<Void, Response<Session>>() {
    @Override
    public Void call(Response<Session> session) {
      System.out.println("Successfully logged in anonymously.");

      // You can store the session using your own Serializer/Deserializer too
      Codec codec = new JsonCodec();
      prefsEditor.putString("session", codec.serialize(session));
      return null;
    }
  })
  .addErrback(errorCallback);

We store the androidId into SharedPreferences so we can re-use the same identifier across the lifetime of the game. This prevents problems if the Android OS changes device identifiers in future updates.

Session Storage

The Session 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 SharedPreferences for a androidId to login with; we can store and restore the Session. 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 = prefs.getString("session", "");
if (cachedSession.equals("")) {
  // login user *as shown above* and store their session in SharedPreferences
  // i.e. prefsEditor.putString("session", codec.serialize(session));
} else {
  Codec codec = new JsonCodec();
  Session session = codec.deserialize(cachedSession, HeroicSession.class);
}

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 usually wipes SharedPreferences, though newer versions of Android will try to restore data) and applies an OS update which changes their androidId - when they reinstall the game their androidId 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 androidId; 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 - even non-Android devices.

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 Android SDK which can be downloaded here. Follow the Facebook Android 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 Android game.

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

AccessToken accessToken = AccessToken.getCurrentAccessToken();
if (accessToken != null) {
  LoginFacebookRequest request = LoginFacebookRequest.builder(accessToken.getToken()).build();
  client.execute(request)
    .addCallback(new Callback<Void, Response<Session>>() {
      @Override
      public Void call(Response<Session> response) {
        System.out.println("Successfully logged in via Facebook.");

        Session session = response.get();

        // You can store the session using your own Serializer/Deserializer too.
        Codec codec = new JsonCodec();
        prefsEditor.putString("session", codec.serialize(session));
        return null;
      }
    })
    .addErrback(errorCallback);
} else {
  // Login to Facebook first.
}

You can also link an existing Session by changing the loginRequest above with:

Request loginRequest = LoginFacebookRequest.builder(accessToken).session(session).build();

Manage the Session Client

Once a user is logged in we have an instance of a Session object. 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 different Activity classes.

One approach is to serialize the Session and pass it to the new activity via the Intent:

// In FirstActivity
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
intent.putExtra("session", codec.serialize(session));
startActivity(intent);

// In SecondActivity
Bundle bundle = getIntent().getExtras();
String value = bundle.getString("session");
Session session = codec.deserialize(value, HeroicSession.class);

Perhaps more convenient is to create a static helper class with commonly used object references, such as a Client, Session, or Codec, which would be accessble to all activities in the game. It's up to you how you decide to share data and object instances between activities, the SDK is designed to support any approach.

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 name or nickname during registration.

// Perhaps these fields come from an input form shown to the user.
String email = "example@example.com";
String password = "really-strong-password";
String passwordConfirmation = "really-strong-password";
String playerName = "player-name";

CreateEmailRequest request = CreateEmailRequest
  .builder(email, password, passwordConfirmation)
  .name(playerName)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Session>>() {
    @Override
    public Void call(Response<Session> response) {
      System.out.println("Successfully signed up with email.");

      Session session = response.get();

      // You can store the session using your own Serializer/Deserializer too
      Codec codec = new JsonCodec();
      prefsEditor.putString("session", codec.serialize(session));
      return null;
    }
  })
  .addErrback(errorCallback);

Login

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

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

LoginEmailRequest request = LoginEmailRequest
  .builder(email, password)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Session>>() {
    @Override
    public Void call(Response<Session> response) {
      System.out.println("Successfully logged in via Email.");

      Session session = response.get();

      // You can store the session using your own Serializer/Deserializer too
      Codec codec = new JsonCodec();
      prefsEditor.putString("session", codec.serialize(session));
      return null;
    }
  })
  .addErrback(errorCallback);

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";

SendResetEmailRequest request = SendResetEmailRequest
  .builder(email)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Void>>() {
    @Override
    public Void call(Response<Void> response) {
      System.out.println("Successfully sent email reset notification.");
      return null;
    }
  })
  .addErrback(errorCallback);

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, createdAt, 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:

GamerGetRequest request = GamerGetRequest.builder(session).build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Gamer>>() {
    @Override
    public Void call(Response<Gamer> response) {
      System.out.println("Successfully retrieved profile information.");
      System.out.println("Nickname: " + response.get().getNickname());
      return null;
    }
  })
  .addErrback(errorCallback);

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";

LinkFacebookRequest request = LinkFacebookRequest
  .builder(session, facebookAccessToken)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Void>>() {
    @Override
    public Void call(Response<Void> response) {
      System.out.println("Successfully linked Facebook profile.");
      return null;
    }
  })
  .addErrback(errorCallback);

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";
UnlinkFacebookRequest request = UnlinkFacebookRequest
  .builder(session, facebookId)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Void>>() {
    @Override
    public Void call(Response<Void> response) {
      System.out.println("Successfully unlinked Facebook profile.");
    }
  })
  .addErrback(errorCallback);

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";
CheckFacebookRequest request = CheckFacebookRequest
  .builder(facebookAccessToken)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Check>>() {
    @Override
    public Void call(Response<Check> response) {
      System.out.println("Successfully checked Facebook profile.");
      return null;
    }
  })
  .addErrback(errorCallback);

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:

GamerUpdateRequest request = GamerUpdateRequest
  .builder(session, "NewPlayerNickname")
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Void>>() {
    @Override
    public Void call(Response<Void> response) {
      System.out.println("Successfully update player nickname.");
      return null;
    }
  })
  .addErrback(errorCallback);

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:

Map<String, Object> data = new HashMap<String, Object> ();
data.put("tank_count", "3");

String datastoreTable = "inventory";
String key = "playerArmy";

DatastorePutRequest request = DatastorePutRequest
  .builder(session, datastoreTable, key, data)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Void>>() {
    @Override
    public Void call(Response<Void> response) {
      System.out.println("Successfully stored army data");
      return null;
    }
  })
  .addErrback(errorCallback);

You could also optionally update the permissions of the object:

request.permissions(1, 1); //Owner ReadWrite

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.

  • NONE
    Read=0, Write=0. 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.
  • READ_ONLY
    Read=1, Write=0. The object can only be read by the owner.
  • WRITE_ONLY
    Read=0, Write=1. The object can be be written by the owner but not read.
  • READ_WRITE
    Read=1, Write=1. The object can be read and written by the owner.
  • PUBLIC_READ
    Read=2, Write=0. The object can be read by the owner and all other players but not modified.
  • PUBLIC_READ_OWNER_WRITE
    Read=2, Write=1. 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.

Map<String, Object> data = new HashMap<String, Object> ();
data.put("solider_count", "10");

String datastoreTable = "inventory";
String key = "playerArmy";

DatastorePatchRequest request = DatastorePatchRequest
  .builder(session, datastoreTable, key, data)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Void>>() {
    @Override
    public Void call(Response<Void> response) {
      System.out.println("Successfully updated army data");
      return null;
    }
  })
  .addErrback(errorCallback);

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.

String datastoreTable = "inventory";
String key = "playerArmy";

DatastoreGetRequest request = DatastoreGetRequest
  .builder(session, datastoreTable, key)
  .owner("me");
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<DatastoreItem>>() {
    @Override
    public Void call(Response<DatastoreItem> response) {
      DatastoreItem storage = response.get();
      System.out.println("Success, army data is: " + storage.getPublic());
      return null;
    }
  })
  .addErrback(errorCallback);

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.

String datastoreTable = "inventory";
String key = "playerArmy";

DatastoreDeleteRequest request = DatastoreDeleteRequest
  .builder(session, datastoreTable, key)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Void>>() {
    @Override
    public Void call(Response<Void> response) {
      System.out.println("Successfully deleted army data.");
      return null;
    }
  })
  .addErrback(errorCallback);

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.public.tank_count: {0 TO *]";

DatastoreQueryRequest request = DatastoreQueryRequest
  .builder(session, query)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<SharedStorageQuery>>() {
    @Override
    public Void call(Response<DatastoreQuery> response) {
      DatastoreQuery results = response.get();
      System.out.println("Result count of query: " + results.getCount());
      for (DatastoreItem result : results) {
        System.out.println("Datastore Object: " + result.getDataAs(String.class));
      }
      return null;
    }
  })
  .addErrback(errorCallback);

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:

AchievementListRequest request = AchievementListRequest
  .builder(session)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<AchievementList>>() {
    @Override
    public Void call(Response<AchievementList> response) {
      int allAchievements = 0;
      int unlockedAchievements = 0;

      for (Achievement achievement : response.get()) {
        allAchievements++;
        if (achievement.isCompleted) {
          unlockedAchievement++;
        }
      }

      System.out.println("Retrieved " + allAchievements +
        " achievements of which " + unlockedAchievements +
        " achievements are completed.");
      return null;
    }
  })
  .addErrback(errorCallback);

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";

AchievementUpdateRequest request = AchievementUpdateRequest
  .builder(session, achievementId, zombiesVanquished)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Achievement>>() {
    @Override
    public Void call(Response<Achievement> response) {
      if (response.get().isCompleted()) {
        System.out.println("Achievement updated and it is now unlocked.");
      } else {
        System.out.println("Achievement updated.");
      }
      return null;
    }
  })
  .addErrback(errorCallback);

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:

AchievementListRequest request = AchievementListRequest
  .builder()
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<AchievementList>>() {
    @Override
    public Void call(Response<AchievementList> response) {
      for (Achievement achievement : response.get()) {
        System.out.println(achievement.getName() + " - " + achievement.getDescription());
      }
      return null;
    }
  })
  .addErrback(errorCallback);

This method is very similar to the one above where we set the Session 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";

LeaderboardRankGetRequest request = LeaderboardRankGetRequest
  .builder(session, leaderboardId)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<LeaderboardAndRank>>() {
    @Override
    public Void call(Response<LeaderboardAndRank> response) {
      LeaderboardAndRank leaderboardAndRank = response.get();
      System.out.println(leaderboardRank.getRank().getNickname() +
        " best rank is " + leaderboardRank.getRank().getRank());
      return null;
    }
  })
  .addErrback(errorCallback);

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";

LeaderboardUpdateRequest request = LeaderboardUpdateRequest
  .builder(session, leaderboardId, newScore)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Rank>>() {
    @Override
    public Void call(Response<Rank> response) {
      System.out.println("The user's best score: " + response.get().getRank());
      return null;
    }
  })
  .addErrback(errorCallback);

The Rank given back for the user in the successful callback is pretty handy. You could check their rank.getScore() and unlock an achievement for the user or warn them that their skill level is dropping by checking rank.getBestRank() versus their current rank.getRank(). 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";

LeaderboardGetRequest request = LeaderboardGetRequest
  .builder(leaderboardId)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Leaderboard>>() {
    @Override
    public Void call(Response<Leaderboard> response) {
      Iterator<Entry> itr = response.get().iterator();
      while(itr.hasNext()) {
        Entry leaderboardEntry = itr.next();
        System.out.println("Name: " + entry.getNickname() +
          ", Score: " + entry.getScore());
      }
      return null;
    }
  })
  .addErrback(errorCallback);

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:

LeaderboardListRequest request = LeaderboardListRequest
  .builder()
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Leaderboard>>() {
    @Override
    public Void call(Response<LeaderboardList> response) {
      for (Leaderboard leaderboard : response.get()) {
        System.out.println("Leaderboard's name is: " + leaderboard.getName());
      }
      return null;
    }
  })
  .addErrback(errorCallback);

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. You can then query the leaderboard to retrieve entries that only match a given set of social IDs.

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.
Map<String, Object> scoretags = new HashMap<String, Object>();
scoretags.put("race_condition", "wet");
long newScore = 7934;
// The ID for the leaderboard is in Hub
String leaderboardId = @"6aed1e8dbf104fccc384bb659069ba69";

LeaderboardUpdateRequest request = LeaderboardUpdateRequest
  .builder(session, leaderboardId, newScore)
  .scoretags(scoretags)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Rank>>() {
    @Override
    public Void call(Response<Rank> response) {
      System.out.println("The user's best score: " + response.get().getRank());
      return null;
    }
  })
  .addErrback(errorCallback);

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

String leaderboardId = "6aed1e8dbf104fccc384bb659069ba69";

LeaderboardRankGetRequest request = LeaderboardRankGetRequest
  .builder(session, leaderboardId)
  .withScoretags()
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<LeaderboardAndRank>>() {
    @Override
    public Void call(Response<LeaderboardAndRank> response) {
      LeaderboardAndRank leaderboardAndRank = response.get();
      System.out.println(leaderboardRank.getRank().getNickname() +
        " best rank is " + leaderboardRank.getRank().getRank());

      Codec codec = new JsonCodec();
      System.out.println("The user's scoretags is: " +
        codec.deserialize(leaderboardRank.getRank().getScoretags(), Map.class));
      return null;
    }
  })
  .addErrback(errorCallback);

Player-centric Leaderboards

Auto-offset allows you to automatically center (approximately) a leaderboard around the current player. To use auto-offset, simply toggle autoOffset in the builder:

LeaderboardRankGetRequest request = LeaderboardRankGetRequest
  .builder(session, leaderboardId)
  .autoOffset()
  .build();
// execute the request like above

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 ArrayList<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");

MatchmakingRequest request = MatchmakingRequest
  .builder(session, gamerIds)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Match>>() {
    @Override
    public Void call(Response<Match> response) {
      Match match = response.get();
      System.out.println("Let the battle commence!");
      return null;
    }
  })
  .addErrback(errorCallback);

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;

MatchmakingRequest request = MatchmakingRequest
  .builder(session, requiredPlayerCount)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Match>>() {
    @Override
    public Void call(Response<Match> response) {
      Match match = response.get();
      if (match == null) {
        System.out.println("You have been queued.");
      } else {
        System.out.println("Let the battle commence!");
      }
      return null;
    }
  })
  .addErrback(errorCallback);

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. You can also cancel the matchmaking request.

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:

MatchListRequest request = MatchListRequest
  .builder(session)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<MatchList>>() {
    @Override
    public Void call(Response<MatchList> response) {
      for (Match match : response.get()) {
        if (!match.isActive()) {
          // match has ended you'll want to reward points, etc.
          // you'll also want to filter it from your UI element
        } else if (match.getTurnGamerId().equals(gamer.getGamerId())) {
          // it's your turn to play!
        } else {
          // match is active but it's not your turn.
        }
      }
      return null;
    }
  })
  .addErrback(errorCallback);

Each match can store data as Strings 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 matches:

int lastTurnCount = 2;

// we now have all the newer turns since `lastTurnCount`
MatchTurnListRequest request = MatchTurnListRequest
  .builder(session, match.getMatchId())
  .turnId(lastTurnCount)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<TurnList>>() {
    @Override
    public Void call(Response<TurnList> response) {
      for (Turn turn : response.get()) {
        System.out.println("User " + turn.getGamerId() +
          " played turn number " + turn.getTurnNumber());
        System.out.println("Turn data: " + turn.getData());
      }
      return null;
    }
  })
  .addErrback(errorCallback);

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:

MatchTurnListRequest request = MatchTurnListRequest
  .builder(session, match.getMatchId())
  .turnId(0)
  .build();
// execute the request like above

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.getMatchId() 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
// For example - chess uses portable game notation (PGN)
String turnData = Collections.singletonMap("move", "e4 e5");

MatchTurnSubmitRequest request = MatchTurnSubmitRequest
  .builder(session, match.getMatchId(), lastTurnCount, nextPlayerGamerId)
  .data(turnData)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Void>>() {
    @Override
    public Void call(Response<Void> response) {
      System.out.println("Successfully submitted new turn.");
      return null;
    }
  })
  .addErrback(errorCallback);

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 updatedAt field across all the matches.

MatchUpdatesRequest request = MatchUpdatesRequest
  .builder(session)
  .since(match.getUpdatedAt())
  .build();
// execute the request like above

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:

MatchActionRequest request = MatchActionRequest
  .builder(session, match.getMatchId(), MatchActionRequest.Action.END)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Void>>() {
    @Override
    public Void call(Response<Void> response) {
      System.out.println("Successfully ended match.");
      return null;
    }
  })
  .addErrback(errorCallback);

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:

MatchActionRequest request = MatchActionRequest
  .builder(session, match.getMatchId(), MatchActionRequest.Action.LEAVE)
  .build();
// execute the request like above

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";
String function = "calculate_average_lap_time";
String trackName = "silverstone";

Map<String, Object> data = new HashMap<String, Object>();
data.put("track_name", trackName);

CloudCodeRequest request = CloudCodeRequest
  .builder(module, function, Map.class)
  .input(data)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Map>>() {
    @Override
    public Void call(Response<Map> response) {
      System.out.println("Average lap time for is " + response.get("average_lap_time"));
      return null;
    }
  })
  .addErrback(errorCallback);

Tip

You should use CloudCodeRequest.Builder.Session() 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.

MessageListRequest request = MessageListRequest
  .builder(session)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<MessageList>>() {
    @Override
    public Void call(Response<MessageList> response) {
      MessageList messages = response.get();
      System.out.println("Received " + messages.getCount() + " messages.");
      for (Message messaeg : messages) {
        System.out.println("Message " + message.getMessageId() +
          ": " + message.getSubject());
      }
      return null;
    }
  })
  .addErrback(errorCallback);

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.

String messageId = "message-id";

// this will set the message `read_at` field to the current server timestamp.
MessageReadRequest request = MessageReadRequest
  .builder(session, messageId)
  .withBody()
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Message>>() {
    @Override
    public Void call(Response<Message> response) {
      Message message = response.get();
      System.out.println("Retrieved message with subject: " +
        message.getSubject() + " with body: " + message.getBody());
      return null;
    }
  })
  .addErrback(errorCallback);

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.

String messageId = "message-id";

MessageDeleteRequest request = MessageDeleteRequest
  .builder(session, messageId)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Void>>() {
    @Override
    public Void call(Response<Void> response) {
      System.out.println("Message was deleted.");
      return null;
    }
  })
  .addErrback(errorCallback);

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.

PurchaseVerificationRequest request = PurchaseVerificationRequest
  .builder(session, PurchaseVerificationRequest.PurchaseType.PRODUCT, purchaseToken, productId)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<PurchaseVerification>>() {
    @Override
    public Void call(Response<PurchaseVerification> response) {
      PurchaseVerification verification = response.get();
      if (!verification.isPurchaseProviderReachable()) {
        // 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.isSeenBefore() && verification.isSuccess()) {
        // The purchase is valid
      } else if (verification.isSuccess()) {
        // 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
      }
      return null;
    }
  })
  .addErrback(errorCallback);

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

PurchaseVerificationRequest request = PurchaseVerificationRequest
  .builder(session, PurchaseVerificationRequest.PurchaseType.SUBSCRIPTION, purchaseToken, productId)
  .build();

Cloud Storage

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:

CloudStorageGetRequest request = CloudStorageGetRequest
  .builder(session, "SaveGame1")
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<CloudStorage>>() {
    @Override
    public Void call(Response<CloudStorage> response) {
      Map<String, String> map = (Map<String,String>) response.get().getValueAs(Map.class);
      System.out.println("Successfully retrieved cloud storage. User has collected " + map.get("coins_collected") +" coins");
      return null;
    }
  })
  .addErrback(errorCallback);

Write Object

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

Map<String, String> saveGame = new HashMap<String, String>();
saveGame.put("coins_collected", "2000");

CloudStoragePutRequest request = CloudStoragePutRequest
  .builder(session, "SaveGame1", saveGame)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Void>>() {
    @Override
    public Void call(Response<Void> response) {
      System.out.println("Successfully stored data to cloud storage.");
      return null;
    }
  })
  .addErrback(errorCallback);

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:

CloudStorageDeleteRequest request = CloudStorageDeleteRequest
  .builder(session, "SaveGame1")
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Void>>() {
    @Override
    public Void call(Response<Void> response) {
      System.out.println("Successfully deleted stored data.");
      return null;
    }
  })
  .addErrback(errorCallback);

Shared Storage

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.

Map<String, Object> data = new HashMap<String, Object> ();
data.put("tank_count", "3");
String key = "playerArmy";

SharedStoragePublicPutRequest request = SharedStoragePublicPutRequest
  .builder(session, key, data)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Void>>() {
    @Override
    public Void call(Response<Void> response) {
      System.out.println("Successfully stored army data");
      return null;
    }
  })
  .addErrback(errorCallback);

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.

Map<String, Object> data = new HashMap<String, Object> ();
data.put("solider_count", "10");
String key = "playerArmy";

SharedStoragePublicPatchRequest request = SharedStoragePublicPatchRequest
  .builder(session, key, data)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Void>>() {
    @Override
    public Void call(Response<Void> response) {
      System.out.println("Successfully updated army data");
      return null;
    }
  })
  .addErrback(errorCallback);

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.

String key = "playerArmy";

SharedStorageGetRequest request = SharedStorageGetRequest
  .builder(session, key)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<SharedStorage>>() {
    @Override
    public Void call(Response<SharedStorage> response) {
      SharedStorage storage = response.get();
      System.out.println("Success, army data is: " + storage.getPublic());
      return null;
    }
  })
  .addErrback(errorCallback);

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.

String key = "playerArmy";

SharedStoragePublicDeleteRequest request = SharedStoragePublicDeleteRequest
  .builder(session, key)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<Void>>() {
    @Override
    public Void call(Response<Void> response) {
      System.out.println("Successfully deleted army data.");
      return null;
    }
  })
  .addErrback(errorCallback);

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 *]";

SharedStorageQueryRequest request = SharedStorageQueryRequest
  .builder(session, query)
  .build();

client.execute(request)
  .addCallback(new Callback<Void, Response<SharedStorageQuery>>() {
    @Override
    public Void call(Response<SharedStorageQuery> response) {
      SharedStorageQuery results = response.get();
      System.out.println("Result count of query: " + results.getCount());
      for (SharedStorage result : results) {
        System.out.println("Shared Storage Object: " + result.getPublic());
      }
      return null;
    }
  })
  .addErrback(errorCallback);

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/heroiclabs/heroiclabs-sdk-android/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.