Skip to content

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.

var sfso = new SFSObject();
sfso.PutByte("id", 10);
sfso.PutShort("hp", 5000);
sfso.PutVector2("pos", new Vector2(10.5, 20.7));
sfso.PutShortString("name", "Hurricane");   

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):

void sendData()
{
    ISFSObject sfso = new SFSObject()
    sfso.PutByte("id", 10);
    sfso.PutShort("hp", 5000);
    sfso.PutVector2("pos", new Vector2(10.5, 20.7));
    sfso.PutShortString("name", "Hurricane");

    // Send request to Zone level Extension on server side
    sfs.Send(new ExtensionRequest("data", sfso));
}

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:

(byte) id: 10
(short) hp: 5000
(vector2) pos: (10,5,20,7)
(short_string) name: Hurricane

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:

static void onObjectMessage(SFSObject sfso)
{
    var interceptors = new List<DualLaserInterceptor>();
    var foes = (ISFSArray) sfso.GetSFSArray("foes");

    if (foes != null)
    {
        for (int i = 0; i < foes.Size(); i++)
        {
            interceptors.Add(DualLaserInterceptor.FromByteArray(foes.GetByteArray(i)));
        }   
    }
}

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