Implementing Whispersync for Games 2.2

Before continuing, confirm that you have completed all steps in Setting Up Your GameCircle Configuration and Initializing GameCircle in Your Game. Whispersync for Games will not function unless you have configured and initialized GameCircle.

Overview

To integrate Whispersync for Games in your game, follow the steps in this section. Whispersync enables you to get and set values in a data map. It is the first solution to offer both auto-conflict resolution and player-choice conflict resolution as options, and it queues when a device is offline. You can set up the data map in just a few minutes.

The GameCircle SDK incorporates the Login with Amazon service to manage Whispersync authentication. For more information, see the Login with Amazon documentation.

This section begins with How Whispersync for Games Works and other information that you need to know in order to use the Whispersync service in your game. Implementation steps with code samples follow in these sections:

  1. Step 1. Set Up the Data You Want to Sync
  2. Step 2. Add Accessor Names to Your Code
  3. Step 3. Test Your Whispersync Implementation

How Whispersync for Games Works

Prior to Whispersync for Games, many developers implemented separate methods to store game data to disk and to cloud. Whispersync replaces your local storage solution and provides the added benefit of background synchronization with the cloud and Android and iOS devices.

The GameDataMap interface enables you to get and set numbers and strings, and to organize them in lists and maps. GameDataMap is a first-class citizen that you can treat as a variable, pass into a method, or return from a method.

Here’s how you get your game data:

	GameDataMap gameDataMap = AmazonGamesClient.getWhispersyncClient().getGameData(); 

Unlike the GameCircle leaderboard and achievements clients, the Whispersync client is available immediately after initializing AmazonGamesClient. Whispersync data is always accessible, even when the player is not registered with Amazon or is offline.

GameDataMap provides several ways to access your data. For example, to retrieve a player’s highest score:

	SyncableNumber highScore = gameDataMap.getHighestNumber("highScore");

In this example, because you retrieve highScore as a highest number, it will always reflect the maximum value ever assigned to it, from any device.

To set the high score:

// Where 1000 represents a player's score, not a maximum
highScore.set(1000);

Conflict Resolution Options

Amazon offers two conflict resolution options to enable the best possible player experience for each game:

  • Auto resolution, in which your game automatically syncs the best score (highest or lowest, depending on the game), most recent achievements, and most recent purchases across all devices and to the Amazon cloud, without further action on both the player and the developer’s part. To implement auto-conflict resolution, use any of the Whispersync data type described below, except for DeveloperString (which is used exclusively for manual conflict resolution).
  • Manual resolution via DeveloperString, in which the developer manually performs the conflict resolution. You should first get and deserialize both locally and remotely stored strings, and then set specific values via game logic to determine the current game state. If there is no easy way to auto-resolve, you can optionally prompt the player for input.

    For implementation details of manual resolution via DeveloperString, see Example 4 below.

    IMPORTANT: Amazon recommends that you exercise caution with this approach because your game logic may differ from players’ expectations. For example, if a player buys an item for 100 Coins on offline Device A, and later buys a different item with the same 100 Coins on offline Device B, the player may want to keep the item from Device A, which represents the older data. In this case, it may be best to involve the player via a popup to let them decide which item they would rather keep. Alternatively, you may choose to model the above situation with auto-resolvable types such as SyncableAccumulatingNumber and SyncableStringSet.

When Syncing Occurs

The GameCircle SDK automatically synchronizes Whispersync data with the cloud. Whispersync enables syncing based on two event types:

  • Active sync events occur when you call the synchronize() method, and when a customer signs in to your game. However, syncing occurs even if you never call the synchronize() method, due to passive sync events.
  • Passive sync events occur any time that you choose to change game data, such as by setting a high score or accumulating number. Passive sync events are batched, rather than synced immediately. Whispersync throttles passive sync events to conserve network bandwidth and battery life.
    • Regardless of throttling, local storage is always synced. Whispersync stores local game data in your application’s storage space in an obfuscated text file.
    • Throttling does not cause data to be lost; the customer’s data is synced on the next active or passive sync attempt.
    • When throttling occurs, a callback is made to WhispersyncEventListener.onThrottled(), a Java class that you can choose to extend.
      • AmazonGamesClient.getWhispersyncClient().setWhispersyncEventListener(new WhispersyncEventListener() {
          public void onNewCloudData() {
        	 // refresh visible game data
          }
        
          // The following three methods are mainly useful for debugging purposes and don't have to be overridden
          
          public void onDataUploadedToCloud() {
          }
        
          public void onThrottled() {
          }
        
          public void onDiskWriteComplete() {
          }
        }); 

    Note: If a player deregisters a Kindle device or deregisters a game on an Android or iOS device, data managed in Whispersync may be merged with another Amazon account upon re-registration. This can occur on Kindle devices if a player deregisters while a game is running and then registers the device to another Amazon account. It can occur on Android or iOS devices if a player deregisters a game and then registers the same game to another Amazon account. It is unlikely that data you manage in Whispersync will be merged across Amazon accounts because players are unlikely to deregister their Kindle devices and games and re-register them to other players.

    Forcing a Sync

    Use this code to force a sync:

    • In Java:
      AmazonGamesClient.getWhispersyncClient().synchronize(); 
    • In C++, Using our JNI Interface
      AmazonGames::WhispersyncClient::synchronize();  

    Offline Scenarios

    Whispersync easily manages offline scenarios involving mergeable data types. For example, say that a player starts a game online and unlocks six levels, each with a rating of one star. The player then might go offline with another device and play the first three levels again, and earn three stars on each level. When the offline device goes back online and syncs, both devices will then show three stars on levels 1 – 3 and one star on levels 4 – 6.


    Syncable Data Types

    The table below shows the data types that you can sync, the GameDataMap accessors for each data type, and sample use cases.

    Table 1: Syncable Data Types
    Whispersync 2.2 Data Type Data Type Represents GameDataMap Accessors Sample Use Case
    SyncableNumber A highest, lowest or most recent number. getHighestNumber Stars earned on level 7
    getLowestNumber Fastest lap time
    getLatestNumber Most recent lap time
    SyncableNumberList A list of numbers in ascending, descending or latest order. getHighNumberList List of N best game scores
    getLowNumberList List of N fastest lap times
    getLatestNumberList List of N most recent scores
    SyncableAccumulatingNumber A number that can be incremented and decremented. getAccumulatingNumber Total distance
    Remaining health potions
    SyncableString The most recent string. getLatestString Most recent car added to collection
    SyncableStringList A list of strings, ordered by most recent. getLatestStringList Names of last N enemies encountered
    SyncableStringSet A set of strings.*

    * Special Case This is an unbounded set of strings, to which items can only be added, as long as the set doesn’t duplicate an existing string.
    getStringSet Levels completed for a non-linear game
    GameDataMap A nested map that enables hierarchical data. getMap A data map for each level in a game

    Step 1. Set Up the Data You Want to Sync

    Model your game data

    Model your game data by using the basic types in the Syncable Data Types table. Add new accessors simply by retrieving them by name; if they don’t exist, they’ll be created on the fly and synchronized from that point on.

    You can model both simple and complex game data by using Whispersync’s syncable data types, and, if necessary, by nesting data maps.

    Start by setting the name and type of game data that you want to synchronize. It’s not strictly necessary to do this all in one place, or even before the variables are actually used. But having a single, authoritative definition can improve code clarity and make it easier to understand and maintain the structure of your game data.

    Example 1: Maintaining a list of spells mastered

    In the snippet below, for example, we can see at a glance what is stored and how the developer laid it out. The game maintains a list of all spells the player has ever mastered, as well as the last few power-ups collected. In addition, it will store data for each level, including the number of stars earned, fastest completion time, and total time played for each level.

    public static final int NUM_LEVELS = 10;
    public static final int MAX_POWERUPS = 5;
    
    . . .
    
    GameDataMap gameDataMap;
    
    protected void setupGameData() {
       // Save a reference to the root object for later use.
       gameDataMap = AmazonGamesClient.getWhispersyncClient().getGameData();
    
       // Create a sub-map for each level.
       GameDataMap[] levelMaps = new GameDataMap[NUM_LEVELS];
       for(int i = 0; i < NUM_LEVELS; i++) {
    	  levelMaps[i] = gameDataMap.getMap("level" + i);
    
    	  // Track the highest score (stars) obtained for each level.
    	  SyncableNumber starsEarned = levelMaps[i].getHighestNumber("starsEarned");
    
    	  // Remember the fast time to completion.
    	  SyncableNumber fastestTime = levelMaps[i].getLowestNumber("fastestTime");
    
    	  // Record total time playing each level.
    	  SyncableAccumulatingNumber totalTime = levelMaps[i].getAccumulatingNumber("totalTime");
       }
    
       // Maintain an inventory of spells mastered.  Spell names will be
       // added to this over time (and can never be removed).
       SyncableStringSet spells = gameDataMap.getStringSet("spells");
    
       // Establish the size of a FIFO list of power-ups.
       //This list will always hold the last MAX_POWERUPS power-ups.
       SyncableStringList powerUps = gameDataMap.getLatestStringList("powerUps");
       powerUps.setMaxSize(MAX_POWERUPS);
    } 
    Example 2: Tracking a player’s experience level

    Here’s a second example employing getGameData(). The snippet looks up a player’s experience level and updates the level if certain criteria are met. In this case, any player who has performed magic is automatically elevated to magician’s apprentice.

    // Get the global game data map for the current player.
    GameDataMap gameDataMap = AmazonGamesClient.getWhispersyncClient().getGameData();
    
    // Look up the object representing the player’s experience level.
    // If none exists, one will be created.
    SyncableNumber experience = gameDataMap.getHighestNumber("experience");
    
    // If the player has ever used a magic spell, they’re at least a
    // magician’s apprentice.
    if (hasUsedMagic() && MAGICIANS_APPRENTICE > experience.asLong()) {
      // As long as this device is online (or as soon as it is),
      // local and cloud storage will be synced.
      experience.set(MAGICIANS_APPRENTICE);
    } 
    
    Example 3: Creating hierarchical data

    This example illustrates how to use embedded maps to create a hierarchical data structure. In this case, the top-level GameDataMap singleton contains a child GameDataMap object for each level in the game.

    This method also shows how to look up the syncable object accessors corresponding to a particular type. You can use this approach to iterate over existing objects, rather than simply passing a string to one of the GameDataMap accessors, which might have the undesirable side effect of instantiating a new syncable element.

    GameDataMap getLevelData(String name) {
       GameDataMap gdm = AmazonGamesClient.getWhispersyncClient().getGameData();
    
       // Get all the string keys associated with GameDataMap objects.
       Set<String> maps = gdm.getMapKeys();
    
       // Look for a match among the maps.
       for (String s : maps) {
    	  if (s.equals(name)) {
    		 // A map exists for the name specified.
    		 return gdm.getMap(s);
    	  }
       }
    
       // No match found. Don't create one. 
       return null;
    } 
    Example 4: Manually resolve conflicts via DeveloperString

    To implement manual conflict resolution in your game, write code modeled on these samples:

    // The map used by GameCircle to store all the syncable data types
    private GameDataMap gameDataMap;
    
    // The structure you're using to save all game data, such as levels unlocked and available coins
    private GameData gameData;
    
    private void initializeGameCircle() {
        // Get the GameDataMap
        gameDataMap = AmazonGamesClient.getWhispersyncClient().getGameData();
    
        // Create your own game data holder
        gameData = new GameData();
        
        // Set up listeners to handle conflicts after syncing with the cloud
        AmazonGamesClient.getWhispersyncClient().setWhispersyncEventListener(new WhispersyncEventListener() {
            public void onNewCloudData() {
                handlePotentialGameDataConflicts();
            }
    
            public void onDataUploadedToCloud() {
                handlePotentialGameDataConflicts();
            }
        }
    }
    
    private void saveGameData() {
        // Create a developer string where a player’s game data will be stored
        SyncableDeveloperString developerString = gameDataMap.getDeveloperString("gameData");
        developerString.setValue(gameData.serialize());
    }
    
    private void handlePotentialGameDataConflicts() {
        SyncableDeveloperString developerString = gameDataMap.getDeveloperString("gameData");
    
        // Once cloud data is available on the device, GameCircle can check for conflicts
        if (developerString.inConflict()) {
            // Deserialize both local and cloud strings
            GameData localValue = new GameData(developerString.getValue());
            GameData cloudValue = new GameData(developerString.getCloudValue());
            
            // Manually merge the local and the cloud value; the logic could be as simple as
            // picking one of them or asking the customer to do so
            GameData mergedValue = mergeGameData(localValue, cloudValue);
            
            // Set a new value and resolve the conflict
            // Mark the conflict as resolved, which enables GameCircle to save the final data to the cloud
            developerString.setValue(mergedValue.serialize());
            developerString.markAsResolved();
        }
    }
    
    // A class that holds your game data
    class GameData {
        // Your game data
        private ArrayList<String> itemsEarned = new ArrayList<String>();
        private ArrayList<String> levelsUnlocked = new ArrayList<String>();
        private int coinsCollected;
    
        public GameData() {
            // Initialization logic
        }
    
        public GameData(String serializedGameData) {
            // Game data deserialization logic.
        }
    
        // Logic to serialize the string; all game data should be stored in a single string
        public String serialize() {
            String serializedGameData = "";
            
            // Game data serialization logic.
            
            return serializedGameData;
        }
    }
    

    Step 2. Add Accessor Names to Your Code

    The following code snippets demonstrate how to add accessors (getters) to your code.

    Table 2: Whispersync Code Snippets
    Accessor Name Code Snippet
    SyncableNumber
       HighestNumber






       Lowest Number






       LatestNumber

    	// Retrieve the highest number of stars earned on the current level.
    	SyncableNumber starsEarned = gameDataMap.getHighestNumber("starsEarned");
    	System.out.println("Stars earned on current level: " + starsEarned.asLong());  
    	// Save the current lap time if it's the fastest one (lowest value).
    	SyncableNumber fastestLapTime = gameDataMap.getLowestNumber("fastestLapTime");
    	fastestLapTime.set(getCurrentLapTime());
    	System.out.println("Fastest lap time so far: " + fastestLapTime.asDouble());  
    	// Determine the id of the last obstacle blown up.
    	SyncableNumber lastTargetId = gameDataMap.getLatestNumber("lastTargetId");
    	System.out.println("Id of last thing destroyed: " + lastTargetId.asLong());  
    SyncableNumberList
       HighNumberList












       LowNumberList











       LatestNumberList

    	// Set the number of high scores to preserve.
    	SyncableNumberList highScoresList = gameDataMap.getHighNumberList("highScoresList");
    	highScoresList.setMaxSize(MAX_HIGHSCORES);
    	. . .
    	
    	// Retrieve the a list of the highest scores recorded.
    	SyncableNumberElement[] highScores = highScoresList.getValues();
    	for(int i = 0; i < highScores.length; i++) {
    	  System.out.println("Score #" + i + ": " + highScores[i].asLong());
    	}  
    	// Set the number of fastest lap times to preserve.
    	SyncableNumberList fastestLapTimesList = gameDataMap.getLowNumberList("fastestLapTimesList");
    	fastestLapTimesList.setMaxSize(MAX_FASTESTLAPTIMES);
    	
    	. . .
    	
    	// Retrieve the list of fastest laps clocked so far.
    	SyncableNumberElement[] fastestLapTimes = fastestLapTimesList.getValues();
    	for(SyncableNumberElement sne: fastestLapTimes) {
    	  System.out.println("Lap time: " + sne.asDouble());
    	}  
    	// Get a list of the most recent items collected (defaults to length 3).
    	SyncableNumberElement[] items = gameDataMap.getLatestNumberList("itemsList").getValues();
    	for(int i = 0; i < items.length; i++ ) {
    	  System.out.println("item[" + i + "] = " + items[i].asLong());
    	}
    	
    	. . .
    	
    	// Record an item whenever the player collects one.
    	gameDataMap.getLatestNumberList("itemsList").add(collectedItem.getId());  
    SyncableAccumulatingNumber
       AccumulatingNumber
    	// Add to the total time spent solving the current puzzle.
    	SyncableAccumulatingNumber totalTime = gameDataMap.getAccumulatingNumber("totalTime");
    	totalTime.increment(getTimeElapsed());
    	
    	. . .
    	
    	// Reduce a player's energy, clamping it to 0.0 if necessary.
    	SyncableAccumulatingNumber energy = gameDataMap.getAccumulatingNumber("energy");
    	energy.decrement(getPowerDrain());
    	if(0.0 > energy.asDouble())
    	  energy.increment(energy.asDouble());  
    SyncableString
       String
    	// Retrieve the value of the current level.
    	SyncableString curLevelName = gameDataMap.getLatestString("curLevelName");
    	System.out.println("The current level is called " + curLevelName);  
    SyncableStringList
       StringList
    	// Set the number of phrases an NPC parrot will remember.
    	SyncableStringList phrasesList = gameDataMap.getLatestStringList("phrasesList");
    	phrasesList.setMaxSize(MAX_PHRASES);
    	
    	. . .
    	
    	// Display the last few phrases the player has taught his parrot.
    	SyncableStringElement[] phrases = phrasesList.getValues();
    	for(int i = 0; i < phrases.length; i++) {
    	  System.out.println("phrase[" + i + "] = " + phrases[i].getValue());
    	}
    	
    	. . .
    	
    	// Teach the player's parrot some new phrases.
    	phrasesList.add("Ahoy, matey");
    	phrasesList.add("Avast!");   
    SyncableStringSet
       StringSet
    	// Display all enemies encountered in the game so far.
    	Set<SyncableStringElement> allEnemies = gameDataMap.getStringSet("allEnemiesSet").getValues();
    	for(SyncableStringElement sse : allEnemies) {
    	  System.out.println("Encountered enemy: " + sse.getValue());
    	}  
    GameDataMap
    	// Nested maps can be used just like the root map
    	GameDataMap antarctica = gameDataMap.getMap("Antarctica");
    	antarctica.getHighestNumber("highScore").set(getScore());
    

    Step 3. Test Your Whispersync Implementation

    You can test your Whispersync for Games implementation by using a single test device. Whispersync doesn’t require nickname whitelisting. If the game is registered, Whispersync will work.

    Note: There is no mechanism for clearing Whispersync test data. Amazon recommends that you test Whispersync by using accounts that you’ve created just for testing.

    Follow these steps to test Whispersync:

    1. On a test device, sign into your game.
    2. Play your game until you hit a milestone that you’ve set as a syncable event, such as reaching a new level.
    3. Save and close the game.
    4. On the device, navigate to Android’s Settings page and clear your game’s data.
    5. Sign into the game and observe whether the game presents the correct level.

    During QA, you may want to know when throttling is occurring. To make throttling more visible during testing, you can implement an event listener, as shown in this snippet:

    client.getWhispersyncClient().setWhispersyncEventListener(new WhispersyncEventListener() {
      @Override
      public void onThrottled() {
    	 Toast.makeText(StarGameActivity.this, "SLOW DOWN!", Toast.LENGTH_LONG).show();
      }
    });  

    Non-Syncable Data

    Some types of game state data can’t be merged automatically. For example, say that a player starts a chess match on an online device, and then continues playing the game on two offline devices. The player will almost certainly make different moves on each offline device. When the two offline devices later go online, the individual moves and final game piece positions can’t be reconciled automatically among the three devices. For this type of data, consider using SyncableDeveloperString to manually resolve conflicts. Possible resolution strategies include:

    • Ask the player to select either the local or the cloud value (this is the recommended approach).
    • Select a value programmatically. This approach may work only for games that have measurable progress, such as the number of pieces placed in a jigsaw puzzle.


    Best Practices

    • The best customer experience is to enable your customers to sync consumables. If you worry that some customers will use in-game currency to make multiple offline purchases, add code to determine if a device is offline and then block offline purchases.
    • Don’t store personally identifiable information by using Whispersync for Games. Amazon may revoke your access to GameCircle if you use Whispersync to store any data other than game metadata.
    • Calling WhispersyncClient.flush() forces game data to be written to the device’s file system. You don’t need to call flush() each time you change your game data, because changes are automatically persisted to local storage via a background thread. However, if you know your app is about to close, calling flush() will save your game data by performing a blocking write to the file system.
    • Store binary data (byte array) to a String by using Base64, and then use the SyncableString data type to store the data in Whispersync. Do not use device-specific formatting for your data, as Whispersync can synchronize across different Android devices.

    Before Publishing, Associate Your Game with a Security Profile

    After adding Whispersync or other GameCircle features to your game, you can submit the game to the Amazon mobile store.

    Important: Most developers choose to have Amazon sign their applications. To enable this, you must associate a security profile with your game in the Amazon Mobile App Distribution Portal before submitting the game to Amazon for testing. Your game will not launch if you do not associate a security profile with your game before submitting it. The reason is that when Amazon signs your app, a new API key is generated that is linked to the security profile, rather than to the game.

    Developers who followed the steps in Setting Up Your GameCircle Configuration already have a security profile that they can associate with their game.

    For step-by-step instructions for associating your game with a security profile, see Publishing to the Amazon Mobile Store.


    Take the Next Step: Add More GameCircle Features in Your Game

    Now that you've integrated Whispersync for Games into your game, take the next step and add Achievements and Leaderboards (if you haven't done so already).


    Return to the API overview

Unavailable During Maintenance