How to Create an Asynchronous Multiplayer Game Part 2: Saving the Game State to Online Database

Asynchronous multiplayer games allow two or more people to play together without the need to be participating in the session at the same time. A key component of supporting asynchronous gameplay is saving the game state to an online database so that it can be retrieved by you and your opponents when it’s time to take the next turn. This article will explain how I achieved game state storing and reloading for my cross-platform game, Hero Mages, and provide insights and code examples helpful for doing the same with your games.

This article will explain:

  1. How to represent game state as data using simple 2D grid representation and command log based representation
  2. How to write game data to an online database using server side extensions with Smart Fox Server Pro.
Requirements
  • Prerequisite knowledge
    • Experience developing turn based games
    • Familiarity with ActionScript 3.0
    • Knowledge of how to setup a MySQL database
    • Knowledge of how to write Smart Fox Server extensions
  • Required products
  • User level
    • Intermediate to Advanced

Representing the Game State as Data

The game state consists of everything that defines the current status of the game board: the layout of the game board, the positions of game pieces, the current attributes and effects of any characters in play, cards in each player’s hand, and (if it’s important to your game) the series of moves that led up to present state of the game. How to best represent a game as data varies based on the complexity of these factors. I will explain two methods I used for Hero Mages: simple 2D grid representation and command log based representation.

Simple 2D Grid Representation

A two-dimensional array can be used to represent the position of game pieces on a game board grid. For example, a simple game of tic-tac-toe might be represented as follows:

1
2
3
4
5
//CODE EXAMPLE 1: TIC TAC TOE REPRESENTED AS 2D ARRAY
var ticTacToeGameState:Array = [];
ticTacToeGameState[0] = [X, O, X];
ticTacToeGameState[1] = [X, X, O];
ticTacToeGameState[2] = [O, X, O];

This array based representation words adequately enough in the context of program code, but for an async game, it’s necessary to store this representation to an online database using a flat data structure. It’s best to use as little information as possible when representing the game state in order to conserve bandwidth and server space.

Assuming we know our tic-tac-toe grid is always 3×3, this same game could be represented using a flat character string:

1
2
//CODE EXAMPLE 2: TIC TAC TOE REPRESENTED AS FLAT STRING
var ticTacToeGameState:String = "XOXXXOOXO";

Upon retrieval of the game state data, we can recreate the two dimensional array shown above:

1
2
3
4
5
6
7
8
9
//CODE EXAMPLE 3: CONVERT 2D GAME STRING TO 2D ARRAY
var ticTacToeGameGrid:Array = [];
for(var i:int = 0; i < 3; i++){
     var gridRow:Array = [];
     for(var j:int = 0; j < 3; j++){
          gridRow.push(ticTacToeGameState.charAt(i+j));
     }
     ticTacToeGameGrid.push(gridRow);
}

Hero Mages utilizes this simple 2D grid representation to store map layouts as a series of X’s and O’s. X’s represent walls, O’s represent open spaces, and start location are a series of numerical combinations representing player team and specific unit type placement areas.

Command Log Based Representation

Many grid-based games can be fully represented by a two dimensional array flattened out to a single line text string as explained above. Games that depend on when particular game moves take place will benefit from a command log based representation.

Benefits of Command Log Based Representation

The goal of a command log representation is to leverage the game engine to recreate a saved game state by providing the list of game commands that led up to the current state. Commands are stored as abbreviated notation to conserve file space and bandwidth. Upon receiving the command log, the game engine literally “plays” the game as instructed up to the last command. During this time, animations are disabled so that the game can be recreated instantly.

Using a command log to recreate a game ensures that all essential game details: the cards that were played, the pieces on board, and the status of those pieces are accurately represented in exactly the same fashion that led up to the game play state in the first place. Additionally, the command log shows a complete history of what has happened since the start of the game. This is extremely helpful for asynchronous games because the player can review the list of moves to recall what events led up to their current position and plan their strategy accordingly. It’s also possible to reanimate select moves that a player returning from to an async game did not have the opportunity to see (I’ll discuss this last part in a future article).

Abbreviated Game Notation

The challenge of writing an effective command log based representation of a game is devising a form of notation that is as descriptive as necessary and as concise as possible. It’s important to define a series of expected standards for the command format. Here’s a look at how I built the syntax for Hero Mages:

  1. Each individual command is enclosed in bracketed c tags “<c>command</c>
  2. Command properties are separated by pipes “|” and property assignments are delineated by equal sign “=”
  3. All commands contain a property that defines the command type called “cT”. The different command types are symbols derived from abbreviating the corresponding function name. For example, a command that instructed a unit to use an action would look like “cT=uA”
  4. Complex data structures such as units and actions are represented with unique numerical ids.
    1. Units are assigned ids based on the order they are added to the game board.
    2. Spells are assigned ids based on their index in the deck array.
    3. Abilities are assigned ids based on their index in the units’ abilities array.
  5. Targets of a command are separated by commas and grid coordinates (x and y) are separated by semi-colons.

By following a strict notation and referencing complex, instanced objects with numerical ids, game commands can be represented as simple strings using the following function:

1
2
3
4
5
6
7
8
9
//CODE EXAMPLE 4: OBJECT TO STRING FUNCTION
function objectToString(object:Object, separator:String, valAssignment:String):String{
     var string:String = "";
     for (var prop:* in object){
          string += prop + valAssignment + object[prop] + separator;
     }
     string = string.substr(0, string.length - separator.length);
     return string;
}

If your object contains nested arrays, you will need to first encode those arrays as strings with unique separator and assignment characters:

1
2
3
//CODE EXAMPLE 5: CONVERT ARRAY TO STRING
var myArray:Array = [4, 5, 6, 7];
var myStringArray:String = myArray.toString();

And here’s what the assembly of a complete game command looks like in Hero Mages:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//CODE EXAMPLE 6: GENERATE GAME COMMAND
function generateUseActionCommandString():String{
     //Create a new object to store command properties
     var HM_UseAction:Object = new Object();

     //Store the command type: "uA" represents "useAction"
     HM_UseAction.cT = "uA";
     
     //The abilityUser is a complex, custom datatype
     //So, we store the id of the unit using ability
     HM_UseAction.uId = abilityUser.unitId;      
     
     //The unit's ability is also a complex, custom datatype
     //So, we store the id that represents its index in the abilities array
     HM_UseAction.i = abilityIndex;
     
     //pT represents the primary targets
     //In actual game, a function discerns between target types (units, spaces)
     //Here, we simply convert the array of choices to a comma deliniated string
     HM_UseAction.pT = primaryTargetsToActOn.toString();
     
     //Encapsulate the command within c-tags
     var strCmd:String = "<c>"+objectToString(HM_UseAction, "|", "=")+"</c>";
     
     //A preview of the assembled command
     trace(strCmd) //<c>cT=uA|uId=1|i=1|pT=4,5,6"</c>
     
     //Return the command
     return strCmd;
}

For reference, the following function can be used to convert string commands back into objects:

1
2
3
4
5
6
7
8
9
10
//CODE EXAMPLE 7: STRING TO OBJECT FUNCTION
function stringToObject(string:String, separator:String, valAssigment:String):Object{
     var object:Object = new Object();
     var props:Array = string.split(separator);
     for(var i:int = 0; i < props.length; i++){
          var vals:Array = props[i].split(valAssigment);
          object[vals[0]] = vals[1];
     }
     return object;
}

Writing the Game to Database

Once the game application is capable of representing its save state as a simple text file, the next step is writing the game’s save data to an online database so it can be retrieved asynchronously by other players. In order to do this, you’ll need a web server with MySQL, SQL, or another form of database as well as a web service or server to communicate with the database, run necessary queries, and send/receive data to/from the application.

Hero Mages uses Smart Fox Server for real-time multiuser connectivity (online chat and synchronous gameplay) so I leverage my existing server-side code to handle communication with the database. Hero Mages data is stored in a MySQL database which I’ve previously setup using my hosting provider, GoDaddy.com. The reason I enjoy working with SmartFoxServer is that I can use MySQL directly with ActionScript 1.0 and not have to worry about knowing how to program in PHP or other server-side languages.

Defining Tables to Store Game Data

It’s important to start by defining tables that will be used to store the game information in the database. Hero Mages uses two sets of tables for this purpose:

The table “hm_games” is used to store all of the relevant game data. The “cmdLog” field will store the actual list of notated commands the game engine will use to rebuild the game state.

hm_games
Field Type Notes
ID_GAME int(10) Stores the unique game record id
ranked tinyint(4) Indicates whether or not game is counted for rank
timeRecorded int(10) The epoch time in seconds game was last updated
version varchar(16) Indicates client version game was created with
cmdLog TEXT The game state represented via command log
status tinyint(4) Indicates whether the game is in progress or complete
timeCreated int(10) The epoch time in seconds game was created
whoseTurn mediumint(8) The user id of the player whose turn is active
timeLastTurn int(10) The epoch time in seconds last turn was completed
isAsync tinyint(4) Indicates whether or not game is async or synced

The table “hm_gameresults” is used to store player-specific information related to the game. All of the players for a particular game are connected to the hm_games table via ID_GAME. This table stores the result (whether or not the player won or lost), rating change (if game is ranked), and will also be developed further later to help determine which animations the player needs to see when they rejoin the game.

hm_gameresults
Field Type Notes
ID_MEMBER mediumint(8) The unique id of player participating in this game
ID_GAME int(10) The unique id of the game record for this result
result tinyint(4) The outcome of the game for this player (win/loss)
ratingChange tinyint(4) The change in players rating for ranked games

Creating a New Game Record

Hero Mages was designed for synchronous multiplayer game play, and I haven’t yet developed a user interface design for an asynchronous match-making system. However, the existing game creation screen will work perfectly for the purposes of illustrating how to save a new game record to the database.

Game Creation Screen for Hero Mages

The basic communication flow between the game client and online server works like this:

  1. Once game host has configured all game options to their satisfaction, they will press “Start Game”
  2. The game client formats the game settings as a notated game command, sends this command to the server, and awaits a response
  3. The server side script receives the command to create a new game record, executes a MySQL statement to create a new entry in the hm_gameresults table as well as new records in the hm_gameresults table for each player.
  4. If the server’s database operations are completed successfully, the server returns a response to the client indicating the newly created game record’s ID_GAME. If the server operations fail, a response is returned to the client indicating that the new game could not be created.
  5. If client receives ID_GAME, the game host repackages the command notation with this property and sends the start game command out to all users. If a “failed operation” is returned, game client shows user an error message.

Note: If you need assistance learning how to write a server-side extension for Smart Fox Server Pro, follow the excellent tutorials available at: http://www.smartfoxserver.com/docs/1x/

Once the notated game command is assembled, the following function is used to send the command to the server side extension:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//CODE EXAMPLE 8: SEND CREATE GAME COMMAND
private function sendStartGameCommand(HM_GameVars:Object):void{
     //Store a reference to the created game settings object for use later
     gameVarObj = HM_GameVars;
     
     //Check to see that smartFox is connected and that there are at least 2 players
     if(smartFox.isConnected == true && playerSettingsList.length > 1){
          //Send the command to create new game record to Smart Fox Server extension
          //Commands can be sent as string or xml; normally, I use string for speed
          //In this case, I use xml to save the work of encoding to string
          smartFox.sendXtMessage("HMServer", "CreateGameRecord", HM_GameVars, "xml");
     }
     else{
          //If this is a practice game with only 1 player, no need to store to database
          //Fire game up immediately
          fireUpGameWithRecordID(-1);
     }
}

On the server side, I add a new condition for the “Create Game” command which handles the insertion of a new game record in the database:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//CODE EXAMPLE 9: CREATE GAME RECORD IN DATABASE
function handleRequest(cmd, params, user, fromRoom, protocol){
     if(protocol == "xml"){
          //....CODE FOR OTHER EXTENSION COMMANDS OMITTED....
         
          else if(cmd == "CreateGameRecord"){
               //EXTRACT THE PLAYER INFORMATION AND TURN ORDER FROM THE RECEIVED COMMAND
               var players = String(params.pS).split(",");
               var randomTurnOrders = params.rTO.split(",");
               
               //GENERATE THE MYSQL STATEMENT TO ADD NEW GAME RECORD BASED ON GAME SETTINGS
               var gameRecordSQL = "INSERT into hm_games (ranked, timeCreated, version, status, timeRecorded, whoseTurn, cmdLog) VALUES ("
               gameRecordSQL += "'" + params.r + "', "                               //ranked
               gameRecordSQL += "'" + Math.floor(getTimer() / 1000) + "', "     //timeCreated
               gameRecordSQL += "'" + params.v + "', "                               //version
               gameRecordSQL += "'" + 1 + "', "                                      //status
               gameRecordSQL += "'" + Math.floor(getTimer() / 1000) + "', "     //timeRecorded
               gameRecordSQL += "'" + stringToObject(players[randomTurnOrders[0]], ";", ":").hmId + "', "          //whoseTurn
               gameRecordSQL += "'" + "<c>" + objectToString(params, "|", "=") + "</c>" + "'"  //cmdLog
               gameRecordSQL += ")";
               
               //EXECUTE MYSQL COMMAND AND CHECK IF IT WAS SUCCESSFUL
               success = dbase.executeCommand(gameRecordSQL);
               if(success == false){
                    //IF THIS FAILS, WE NEED TO REPORT BACK AN ERROR TO CLIENT
                    trace("UNABLE TO CREATE GAME RECORD");
                    response.error = "Unable to create new game record in database";
               }
               else{
                    //ONCE GAME RECORD IS ADDED, GRAB ITS ID (WE KNOW ITS THE LAST INSERTED RECORD)
                    sql = "SELECT LAST_INSERT_ID()"
                    var queryRes = dbase.executeQuery(sql);
                    var dataRow = queryRes.get(0);
                   
                    //STORE THE GAME RECORD ID IN OUR RESPONSE OBJECT
                    response.id = dataRow.getItem("LAST_INSERT_ID()");
                   
                    //CREATE A STATEMENT TO INSERT A NEW RECORD IN GAME RESULTS TABLE FOR EACH PLAYER
                    var gameResultsSQL = "INSERT into hm_gameresults (ID_GAME, ID_MEMBER, result, ratingChange) VALUES ";
                    for(var i = 0; i < players.length; i++){
                         //CONVERT THE PLAYER OPTIONS FROM STRING TO OBJECT
                         //THIS IS SO WE CAN EXTRACT PROPERTIES LIKE PLAYER ID
                         var playerOptions = stringToObject(players[i], ";", ":")
                         
                         gameResultsSQL += "(LAST_INSERT_ID(), '" + playerOptions.hmId + "', '" + "-2" + "', '" + "0" + "')"
                         if(i < players.length - 1){
                              gameResultsSQL += ", ";
                         }
                    }
                    success = dbase.executeCommand(gameResultsSQL);
                    if(success == false){
                         //IF THIS FAILS, WE NEED TO REPORT BACK AN ERROR TO CLIENT
                         trace("UNABLE TO CREATE GAME RESULTS RECORD IN DATABASE");
                         response.error = "Unable to create game results records in database";
                    }
               }
          }
         
          //....CODE FOR OTHER EXTENSION COMMANDS OMITTED....
     }
}

Back on the client side, I add a new condition for responding to the “Create Game” command sent from server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//CODE EXAMPLE 10: RECEIVE SERVER SIDE RESPONSE
private function onExtensionResponse(evt:SFSEvent):void{
     //EXTRACT RESPONSE TYPE AND RESPONSE DATA FROM SFSEvent
     var type:String = evt.params.type;
     var dataObj:Object = evt.params.dataObj;
     
     //....CODE OMITTED....
     
     //EXTRA COMMAND FROM RETURNED DATA OBJECT
     cmd= dataObj.cmd;
     var error:String = dataObj.error;
     
     //....CODE OMITTED....
     
     if(error != ""){
          //IF RESPONSE RETURNS AN ERROR, SHOW USER A MESSAGE PROMPT
          showPrompt("HM_MessagePrompt", cmd + " Error", error);
     }
     else{
          //....CODE OMITTED....
         
          //ADD CONDITION FOR SERVER RESPONSE CREATE GAME RECORD
          else if(cmd == "CreateGameRecord"){
               //INSTRUCT GAME OPTION SCREEN TO FIRE UP GAME RECORD
               gameOptionsScreen.fireUpGameWithRecordID(dataObj.id);
          }
          //....CODE OMITTED....
     }
}

This calls the final function of the game lobby to send out the start game command

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//CODE EXAMPLE 11: FIRE UP GAME
public function fireUpGameWithRecordID(gameRecordId:int):void{
     //Recall in previous step we stored gameVarObj for future use
     //Here, we add the database id for the new game record
     gameVarObj.gId = gameRecordId;
     
     //Assemble the game command into abbreviated string notation
     var HM_Command:String = Utils.objectToString(gameVarObj, "|", "=");
     
     //Add the command to client side que
     addToCommandQue(HM_Command);
     
     //Send the command to start game to any live players
     if(smartFox.isConnected == true){
          smartFox.sendCmd(HM_Command);
     }
}

Updating the Game State

With the game record created in the online database and an ID referencing the record available to the game client, changes to the game state can be easily recorded by appending new game commands to the command log field.

In the first article of this series, I consider when updates to the game state should take place based on the style of game being created. In the case of Robot Entertainment’s Hero Academy, which grants players a 5-move turn with the ability to undo actions before submitting a turn, updates to the game state naturally take place at the conclusion of a turn. Hero Mages, by contrast, allows players to interact with each of their available units, cast spells, and make attacks that cause damage based on the results of dice rolls. Due to the random nature of outcomes, an undo feature isn’t plausible for Hero Mages. Therefore, I decided to update my game’s command log each time the player sends a command.

Because Hero Mages can also be played in real time, I decided it was best to upgrade my existing server extension (used for communicating game commands to live players) to also handle updates to the game state stored on the database. This way, I’d make the most efficient use of bandwidth by only having to have the client send the command to the server one time.

Here’s the block of code that handles the game state update:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//CODE EXAMPLE 12: UPDATE GAME RECORD
function handleRequest(cmd, params, user, fromRoom, protocol){
     if(protocol == "str"){
         
          //GENERATE LIST OF RECIPIENTS THE SERVER WILL SEND THIS COMMAND TO
          //....CODE OMITTED....
         
          //params[2] stores game record id
          //If this game record id is included, we need to write this command to stored game log   
          if(params[2] != undefined){        
               if(params[1].indexOf("cT=eT") != -1){//If this is an end turn command
                    //Convert notated command into object
                    var cmdObj = stringToObject(params[1]+"", "|", "=");   
                    //Get the id of player whose turn is next
                    var nextTurnId = cmdObj.nId;
                    //Write update to game record in database
                    sql = "UPDATE hm_games set cmdLog = CONCAT(cmdLog, '<c>" + params[1] + "</c>'), timeRecorded = " + Math.floor(getTimer() / 1000) + ", timeLastTurn  = " + Math.floor(getTimer() / 1000) +", whoseTurn = "+nextTurnId+" WHERE ID_GAME = " + params[2];
               }
               else{
                    //Write update to game record in database
                    sql = "UPDATE hm_games set cmdLog = CONCAT(cmdLog, '<c>" + params[1] + "</c>'), timeRecorded = " + Math.floor(getTimer() / 1000) +" WHERE ID_GAME = " + params[2];
               }
               success = dbase.executeCommand(sql);
               if(success == false){
                    //THE DATABASE DID NOT RECORD THE MOVE CORRECTLY
                    //CREATE A NEW RESPONSE TO NOTIFY GAME CLIENT OF THE ERROR
               }
          }
           _server.sendResponse([params[1]], -1, null, recipients, "str");
          return;
     }
}

Coming Next

This article has covered the first and most fundamental step of creating an asynchronous multiplayer game: representing your game state as data and storing that data to an online database. Next in the series I’ll share how to restore the game state from the database as well as how to seamlessly transition between async multiplayer mode and real-time online play using Flash, ActionScript, and Smart Fox Server extensions.

About Ross Przybylski

Ross Przybylski is a game designer, Flash developer, and technical consultant. He is the founder of D20Studios, LLC and the creator of Hero Mages, a cross-platform, multiplayer strategy game. He is also the Director of Flash Development for Reflection Software, an innovative eLearning services provider company. You can learn more about Ross on his website and follow him on Twitter @RossD20Studios.
This entry was posted in Asynchronous Multiplayer, Game Development and tagged , , , , , , , . Bookmark the permalink.

6 Responses to How to Create an Asynchronous Multiplayer Game Part 2: Saving the Game State to Online Database

  1. Pingback: How to Create an Asynchronous Multiplayer Game Part 3: Loading Games from the Database | Indie Flash Blog

  2. Zhongcheng Zhou says:

    Your articles is excellent. I’m develop an asynchronous multiplayer game using smartfox server. You give me many instructions. or else I don’t know how to do.

  3. Pingback: How to Create an Asynchronous Multiplayer Game Part 4: Matchmaking | Indie Flash Blog

  4. Pingback: 分享制作异步多人游戏的方法和经验 | GamerBoom.com 游戏邦

  5. Jon Draper says:

    Thanks for posting this article about setting up your own async server. I’m tinkering with the idea myself, thinking it would be better rather than using an off the shelf setup. I’ve already got a setup where I can collect and send a string to a mySQL database, put it in and also piece together info to send back. So the core principles seem to be in place… I just need to work out what I actually want to send and receive. I’ve got a quick question regarding where to host the database and scripts (mine will be PHP or Javascript). At the moment I’ve got it running on a local test server using MAMP which is cool. When I decide to put it on a server Is there anything to stop me using my normal website hosting company to run the async, it already supports PHPmyAdmin, but not sure about bandwidth being a problem later in life…. is it crazy to try and use a normal website hosting space for this.. you mentioned you previously used GoDaddy, did you change just because of the Action Script 1.0 compatibility over at Smart Fox Server Pro or is it a better server for this sort of thing regardless of code. thanks for any help. My game is a long way off being complete, I have a working pass n play prototype and I’m now slowly trying to integrate the back end for async play… and I’m trying to get my head around a lot at once.. before I take a fresh look at the graphics and finalise the smaller elements of gameplay. thanks Jon.

  6. Warren says:

    I absolutely love your blog and find a lot of your post’s to be exactly what I’m looking for. can you offer guest writers to write content to suit your needs? I wouldn’t mind publishing a post or elaborating on a number of the subjects you write about here. Again, awesome web log!