My name is Caius and I have been working on a multiplayer real time football/air hockey football game 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 http://www.bilub.com/ and I am looking forward for your feeback.
May the best BILU win 🙂