Game state serialization strategies

In this article we’re going to analyze a few strategies to serialize game state data between client and server, and discuss the pros and cons of each approach.

» Serialization basics

In computer terms data serialization is the process of converting data structures into a format that can be stored or transmitted over the network. SmartFoxServer 2X abstracts this process via two main objects, SFSObject and SFSArray, which are used pervasively throughout the server and client API.

These objects abstract the data transport between client and server and provide fine-grained control of the data sent over the network, handled by the SFS2X binary protocol.

This is an example of a custom message sent from client to server:

ISFSObject item = new SFSObject();
item.putUtfString("name", "Golden Sword");
item.putInt("damagePoints", 120);
item.putInt("durability", 40);
item.putBool("isMagic", true);

send(item, user);

SFSObjects and SFSArrays support 20 basic types, including  typed collections, and deal with the low level details of serialization to binary data. This is what we usually call low level serialization: in other words the process of turning object data to a binary format.

In this article, however, we’ll focus on and discuss high level serialization which is the process of converting your game data to and from SFSObjects.

» Serializing custom objects

Since every game keeps state (be it a simple high score list or a complex inventory with hundreds of items) there will be custom objects in our game modeling such data. Serialization comes into play when we need to transmit game state updates to clients when changes occur.

We need to take custom data from our server Java Extension and send them to clients which may be even running on an entirely different language and platform, such as Unity, HTML 5, Flash etc…

For the sake of simplicity let’s say we have this Java class describing an element of  game state:

public class SpaceShip
{
  byte type; 

  short x;
  short y;

  int shieldPoints;
  int damagePoints;
}

SpaceShip ships[] = new SpaceShip[10];

Let’s say we want to update clients about the spaceships contained in the array. We have essentially three options.

#1 Inline serialization

We can build a series of SFSObject, one per spaceship instance, inline in our code, add them to an SFSArray and finally send the whole thing:

ISFSArray shipArray = new SFSArray();

for (SpaceShip ship : spaceShips)
{
	ISFSObject shipObj = new SFSObject();

	shipObj.putByte("t", ship.type);
	shipObj.putShort("x", ship.x);
	shipObj.putShort("y", ship.y);
	shipObj.putInt("sp", ship.shieldPoints);
	shipObj.putInt("dp", ship.damagePoints);

	shipArray.addSFSObject(shipObj);
}

ISFSObject packet = new SFSObject();
packet.putSFSArray("ships", shipArray);

send("cmd", packet, someUser);

While this approach is essentially correct and works fine, it could quickly pollute your code with large blocks of Class <–> SFSObject inter-conversions, causing also a lot of code repetition and general untidiness. A far better approach to this type of conversion would be to retrofit our game classes with a couple of useful methods.

#2 Encapsulated serialization

Keeping the same idea of approach #1, we’re going to refactor our initial SpaceShip class adding a toSFSObject() and fromSFSObject() methods and replacing string literals with constants, to avoid duplication.

public class SpaceShip
{
	static final String KEY_TYPE = "t";
	static final String KEY_X = "x";
	static final String KEY_Y = "y";
	static final String KEY_SHIELD = "sp";
	static final String KEY_DAMAGE = "dp";

	// ...

	byte type; 

	short x;
	short y;

	int shieldPoints;
	int damagePoints;

	// ...

	public static SpaceShip fromSFSObject(ISFSObject shipObj)
	{
		SpaceShip ship = new SpaceShip();

		ship.type = shipObj.getByte(KEY_TYPE);
		ship.x = shipObj.getShort(KEY_X);
		ship.y = shipObj.getShort(KEY_Y);
		ship.sp = shipObj.getInt(KEY_SHIELD);
		ship.dp = shipObj.getInt(KEY_DAMAGE);

		return ship;
	}

	public ISFSObject toSFSObject()
	{
		ISFSObject shipObj = new SFSObject();

		shipObj.putByte("t", KEY_TYPE);
		shipObj.putShort("x", KEY_X);
		shipObj.putShort("y", KEY_Y);
		shipObj.putInt("sp", KEY_SHIELD);
		shipObj.putInt("dp", KEY_DAMAGE);

		return shipObj;
	}
}

Notice how the fromSFSObject() method is marked as static since it acts as an alternate constructor, where we pass the SFSObject representing the data obtained from the other end of the communication (in this case it is the client, but it could be also the server).

Our code for sending the array of SpaceShip items becomes much cleaner now:

ISFSArray shipArray = new SFSArray();

for (SpaceShip ship : spaceShips)
{
	shipArray.addSFSObject(ship.toSFSObject());
}

ISFSObject packet = new SFSObject();
packet.putSFSArray("ships", shipArray);

send("cmd", packet, someUser);

Also every time we need to send an instance of a SpaceShip object we can directly invoke the toSFSObject() method, without resorting to more inline serialization code.

NOTE: on the client side we will need to create a specular version of the same SpaceShip class so we can exchange the same data in both ways, which is usually needed.

#3 Reflection based serialization

SFSObject and SFSArray allow to serialize custom classes provided that they implement the SerializableSFSType. This in turn allow for a selection of native types to be automagically serialized via reflection.

Here is how the SpaceShip class would look like in this case:

public class SpaceShip implements SerializableSFSType
{
	byte type; 

	short x;
	short y;

	int shieldPoints;
	int damagePoints;
}

Pretty straightforward. We don’t need any manual serialization method, just marking the class as SerializableSFSType. The interface doesn’t require any method to be implemented but allows the internal serializer to manage types via reflection.

The code to serialize the SpaceShip array looks like this:

ISFSArray shipArray = new SFSArray();

for (SpaceShip ship : spaceShips)
{
	shipArray.addClass(ship);
}

ISFSObject packet = new SFSObject();
packet.putSFSArray("ships", shipArray);

send("cmd", packet, someUser);

This looks very similar to what we have done in the previous example with the notable difference that we’re using the SFSArray’s addClass() instead of addSFSObject().

On the client side we need to create a specular class that must:

  • have the same exact name
  • be located in the same package / namespace
  • use the same exact field names of the same type (or equivalent, as explained in the docs)

Naturally there are a number of caveats to keep in mind when using this approach:

  • Reflection is more demanding, if you plan to serialize 1000s of objects you may incur in performance issues both on server side and on clients with limited CPU resources (e.g. low end mobile devices)
  • Not all types are serializable, we provide a table of what is available here.
  • Not all client technologies support reflection (e.g. HTML 5) and some have limited reflection capabilities (Actionscript 3)
  • Inheritance is not possible with this approach, so complex class hierarchies will not work
  • Classes are serialized slightly less efficiently, in terms of size

For more details and examples on how reflection based serialization works you can check our documentation.

» Best approach and conclusions

Manually encapsulated serialization (approach #2) is usually the best and safest choice:  it provides flexibility, control and very little performance costs. It can be easily maintained when state objects are changed and it allows to filter out data that doesn’t need to be transmitted.

On the other hand reflection based serialization (approach #3) can also help in situations where clients use strong typed languages, such as Java and C#, and the use of serialization is lightweight. The advantage here is that you don’t have to write any extra code for it to work.

Finally we’re not proponents of the first inline serialization method because it usually leads to more messy code and repetition, making model changes harder to track down and maintain.