AMF & P2P Serialization Tricks

Play Bilu Ball with friends

My name is Caius and I have been working on a multiplayer real time football/air hockey football game Play Bilub http://www.bilub.com/ and I had to make the communication between players as light as possible.

I created a small example to show you how you can optimize the way flash sends objects over the wire.

I started off with this object:

package com.game.actions
{
import com.game.GameConstants;

import flash.utils.ByteArray;
import flash.utils.IDataInput;
import flash.utils.IDataOutput;
import flash.utils.IExternalizable;

public class PlayerInputAction
{
public var input:int;
public var senderId:uint;
public var scheduledFrame:uint;
public var hostFrame:uint;

public function PlayerInputAction():void
{
}
}
}

The following code returns:

registerClassAlias("com.game.actions.PlayerInputAction", com.game.actions.PlayerInputAction);
var player:PlayerInputAction = new PlayerInputAction();
var bytesElemByte:ByteArray = new ByteArray();
bytesElemByte.writeObject( player );
trace("PlayerInputAction FULL ", bytesElemByte.length) -- outputs: PlayerInputAction FULL 95 !!! WOW very big

1. First thing I noticed is that you need to keep the class alias name short. So i changed

registerClassAlias("3", com.game.actions.PlayerInputAction);

the same code outputs: PlayerInputAction FULL 62, an improvement but we can do better 🙂

 

2. Then i found this article http://jacksondunstan.com/articles/2248/comment-page-1#comment-152405 that discusses this problem as well. I implement the IExternalizable interface and the writeExternal & readExternal methods.

package com.game.actions
{
import com.game.GameConstants;

import flash.utils.ByteArray;
import flash.utils.IDataInput;
import flash.utils.IDataOutput;
import flash.utils.IExternalizable;

public class PlayerInputAction implements IExternalizable
{
public var input:int;//4 bytes
public var senderId:uint; //4 bytes
public var scheduledFrame:uint; //4 bytes
public var hostFrame:uint; //4 bytes

public var executed:Boolean = false;

public function PlayerInputAction():void
{
}

public function writeExternal( output:IDataOutput ): void
{
output.writeByte( input ) // 1 byte
output.writeUnsignedInt( senderId ) // 4 bytes
output.writeUnsignedInt( scheduledFrame ) // 4 bytes
output.writeUnsignedInt( hostFrame ) // 4 bytes

}

public function readExternal( dataInput:IDataInput ): void
{
input = dataInput.readByte()
senderId = dataInput.readUnsignedInt()
scheduledFrame = dataInput.readUnsignedInt()
hostFrame = dataInput.readUnsignedInt()
}

public function pack():ByteArray{
var output:ByteArray = new ByteArray();
output.writeByte( GameConstants.PLAYER_INPUT_ACTION );
output.writeByte( input )
output.writeUnsignedInt( senderId )
output.writeUnsignedInt( scheduledFrame )
output.writeUnsignedInt( hostFrame )
return output;
}

public function unpack( dataInput:ByteArray ):void{
input = dataInput.readByte()
senderId = dataInput.readUnsignedInt()
scheduledFrame = dataInput.readUnsignedInt()
hostFrame = dataInput.readUnsignedInt()
}

public function toString():String{
return "[PlayerInputAction] p-"+senderId+" scheduledFrame "+int(scheduledFrame);
}
}
}

 

This greatly decreases the size of the bytearray.

var player:PlayerInputAction = new PlayerInputAction();
var bytesElemByte:ByteArray = new ByteArray();
bytesElemByte.writeObject( player );
trace("PlayerInputAction FULL ", bytesElemByte.length) //outputs PlayerInputAction FULL  17

because of

output.writeByte( input ) // 1 byte
output.writeUnsignedInt( senderId ) // 4 bytes
output.writeUnsignedInt( scheduledFrame ) // 4 bytes
output.writeUnsignedInt( hostFrame ) // 4 bytes

+ 4 extra bytes that probably are the prototype of the object

3. Writting my own method pack() to serialize the object offered me the lowest size.

public function pack():ByteArray{
var output:ByteArray = new ByteArray();
output.writeByte( GameConstants.PLAYER_INPUT_ACTION ); // i had to add 1 byte that helps me know the type of that object.
output.writeByte( input )
output.writeUnsignedInt( senderId )
output.writeUnsignedInt( scheduledFrame )
output.writeUnsignedInt( hostFrame )
return output;
}
var player:PlayerInputAction = new PlayerInputAction();
var temp:ByteArray = player.pack();
trace("PlayerInputAction LIGHT ", temp.length) // PlayerInputAction LIGHT  14

So if you really want to have a light communication between clients, you can make a small effort and write your own pack/unpack methods to transform the object into a bytearray. It may take more time to implement but it is worth it 🙂

4. The last solution is a mix between the solution above that takes advantage of byte array compression. If the length of the byte array is small you do not need compress, but in my case for 2 players I have an average of 100 bytes and compressing returns 41 bytes.

public function pack():ByteArray{
			var gameByte:ByteArray = new ByteArray();
			gameByte.writeObject( this );
//			LoggerPro.instance.log(this, LoggerPro.DEBUG, "pack1", gameByte.length  )
			gameByte.compress( CompressionAlgorithm.DEFLATE );
			gameByte.position = 0;
			var resp:ByteArray = new ByteArray(); 
			resp.writeByte( GameConstants.GAME_STATE_ACTION );
			resp.writeBytes( gameByte );
//			LoggerPro.instance.log(this, LoggerPro.DEBUG, "pack2", resp.length )
			return resp;
		}
		
		public function unpack( resp:ByteArray ):GameStateAction{
			var payload:ByteArray = new ByteArray(); 
			resp.readBytes( payload )
			payload.position = 0;
			payload.uncompress( CompressionAlgorithm.DEFLATE );
			return payload.readObject();
		}

Also you can continue optimizing the size by using writeByte instead of writeUnsignedInt or writeInt, but in my case I have values > 255( 1 byte ) so I had to send 4 bytes per each variable.

 

You can see the game and the communication in action here Play Bilub http://www.bilub.com/ and I am looking forward for your feeback.

 

May the best BILU win 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *