SFSObject and SFSArray
SFSObject and SFSArray represent a platform-neutral, high level objects that abstract the data transport between client and server. They are used to respectively represent data in form of a Map/Dictionary or List/Array; they can be nested to create complex data structures and they support many different data types (from bytes to integers, doubles, strings and a lot more). These two classes provide fine-grained control over each data element sent over the network and offer very efficient serialization using the SmartFoxServer binary protocol.
Let's consider this simple example: we need to send some data relative to a combat vehicle in a multiplayer game.
The example uses use a single byte for very small integer values, a short (signed 16-bit) for larger values and a Vector2 for 2D coordinates expressed as 32-bit floating point. We also send a string as ShortString type which encodes any string up to 8 bit length (255 chars)
With this approach data can be described precisely based on the amount of storage required allowing to use only the necessary amount of bytes per packet. Being able to save a few bytes may not seem like much but, when the server is handling hundreds of thousands of packets per second the impact on bandwidth is significant.
Supported data types
Here is a list of all types supported by SFSObject and SFSArray:
| Type | Bytes | Ranges and limits |
|---|---|---|
| Null | 1 | N/A |
| Bool | 1 | true/false |
| Byte | 1 | unsigned 8 bit Int, 0 .. 2^8 |
| Short | 2 | signed 16 bit Int, -2^15 .. 2^15 |
| Int | 4 | signed 32 bit Int, -2^31 .. 2^31 |
| Long | 8 | signed 64 bit Int, -2^63 to 2^63 |
| Float | 4 | 32 bit floating point |
| Double | 8 | 64 bit floating point |
| Vector2 | 8 | 2x 32 bit floats |
| Vector3 | 12 | 3x 32 bit floats |
| ShortString | variable | UTF-8 encoded, up to 2^8 bytes |
| String | variable | UTF-8 encoded, up to 2^15 bytes |
| Text | variable | UTF-8 encoded, up to 2^31 bytes |
| BoolArray | variable | max array len: 2^15 items |
| ByteArray | variable | max array len: 2^31 items |
| ShortArray | variable | max array len: 2^15 items |
| IntArray | variable | max array len: 2^15 items |
| LongArray | variable | max array len: 2^15 items |
| FloatArray | variable | max array len: 2^15 items |
| DoubleArray | variable | max array len: 2^15 items |
| StringArray | variable | max array len: 2^15 items |
| SfsArray | variable | max array len: 2^15 items |
| SfsObject | variable | max key len: 8 bit, max 2^15 items |
| ShortStringArray | variable | max array len: 2^15 items |
| Vector2Array | variable | max array len: 2^15 items |
| Vector3Array | variable | max array len: 2^15 items |
Array types are particularly useful when transmitting lists of values that are all of the same type, as the result is a very compact data structure. On the other hand, if you need to send a list of heterogeneous items, SFSArray will be the best choice.
Example of usage
SFSObject/SFSArray are used extensively in Extension development (see the relative sections in this doc for in-depth information), where they are employed in every request and response. By expanding the example provided at the beginning of this article, let's see a full use case.
The client needs to transmit the following data to a server Extension (sfs is the SmartFox class instance):
The server Extension will receive the same data as the parameters object in one of its request handlers:
public class DataRequestHandler extends BaseClientRequestHandler
{
@Override
public void handleClientRequest(User sender, ISFSObject params)
{
byte id = params.getByte("id");
short health = params.getShort("hp");
Vector2 pos = params.getVector2("pos");
String name = params.getShortString("name");
// More game logic ...
...
}
}
Inspecting an SFSObject/SFSArray
We can take a closer look at these two classes and see what happens behind the scenes. Both objects provide useful methods to dump their content in a tree-like or hex-dump format.
By calling SFSObject.GetDump() we can see the entire content of the object/array, in the format (type) name: value:
For a lower level view we can use SFSObject.GetHexDump() to get a binary representation, as it is sent or received.
Binary Size: 43
12 00 04 02 69 64 02 0A 02 68 70 03 13 88 03 70 ....id...hp....p
6F 73 16 41 28 00 00 41 A5 99 9A 04 6E 61 6D 65 os.A(..A....name
13 09 48 75 72 72 69 63 61 6E 65 ..Hurricane
Packet compression
Data in transit might be zlib compressed, if packets are larger than a specific size. This however is a step that happens outside the SFSObject/Array processing. The hex dump always provides the uncompressed version of the data.
Custom serialization
There are use cases where you may need to serialize custom classes, such as objects that represent game entites: NPCs, enemies, world objects, etc. Our recommendation is to use binary serialization and sending the resulting data as byte arrays.
Here's an example to clarify the concept. We have an entity in our game called DualLaserInterceptor, some kind of enemy space ship in a shooting game:
class DualLaserInterceptor
{
Vector2 pos; // 8 bytes
Int16 hp = 1000; // 2 bytes
Int16 mainLaserDP = 25; // 2 bytes
Int16 auxLaserDP = 35; // 2 bytes
byte speed = 10; // 1 byte
// = 15 bytes total
// ... methods ...
byte[] toByteArray()
{
var buff = new ByteBuffer();
buff.PutFloat(pos.X);
buff.PutFloat(pos.Y);
buff.PutInt16(hp);
buff.PutInt16(mainLaserDP);
buff.PutInt16(auxLaserDP);
buff.Put(speed);
return buff.Array();
}
static DualLaserInterceptor fromByteArray(byte[] data)
{
var buff = new ByteBuffer(data);
var instance = new DualLaserInterceptor();
instance.pos = new Vector2(buff.GetFloat(), buff.GetFloat());
instance.hp = buff.GetInt16();
instance.mainLaserDP = buff.GetInt16();
instance.auxLaserDP = buff.GetInt16();
instance.speed = buff.Get();
return instance;
}
}
The class has five properties and it would be very convenient to be able to serialize and deserialize it with a couple of dedicated methods. This way we can have a compact binary representation that can be sent over the wire.
To do so we use a utility class from the SFS API called ByteBuffer. In C# we provide it with the API, while equivalent classes are available in other languages such as Java (see java.nio.ByteBuffer).
Now we can use it very conveniently:
var interceptors = new DualLaserInterceptor[10];
// populate enemies
var foes = new SFSArray();
foreach (DualLaserInterceptor enemy in interceptors)
{
foes.AddByteArray(enemy.ToByteArray());
}
var gameData = new SFSObject();
// ... more data
gameData.PutSFSArray("foes", foes);
sfs.Send(new ObjectMessageRequest(gameData));
Here we've packed a collection of enemies into an SFSArray, each serialized as byte data. Then we nest the SFSArray inside an SFSObject with the rest of the game state (or whatever needs to be updated) and send it.
Similarly we'll be able to receive the objects as binary data and rebuild their relative objects:
Calling our static constructor FromByteArray(...) we can rebuild all of the serialized entities. An alternative to this approach would be to looking into cross-platform serializers such as ProtoBuf or Flatbuffers
Best practices
Finally here are a few recommendations to optimize data when using SFSObject/SFSArray:
-
use short key names: every SFSObject key is encoded and sent as ShortString, keeping these keys as short as possible will save bandwidth when the traffic increases. Recommended max length: 3-4 characters
-
use the right types: especially for numeric values you can save bytes by using smaller types, if you can predict their range
-
use arrays: when possible use SFSArray, rather than SFSObject, to pack a number of related properties. This way you avoid the overhead of encoding the relative key names
-
custom serialization: for unsupported types, such as custom classes: if you have objects that can be serialized to binary you can always transmit them as byte arrays and rebuild them on the receiving side, and viceversa. See the examples provided in this article