Unity

Flatbuffers

March 7, 2017

Tags: , , , , , , , ,

FlatBuffers For Unity

The need to send/receive service related data sometimes means doing work at times you can’t control, which can mean dropped frames if the serializer isn’t up to speed. I’ve been trying to find a better way of serializing/deserializing data in Unity – json parsing can be quite slow and has the potential to trigger the GC if not careful.

FlatBuffers is a cross platform serialization library developed by Google that allows you to read and write directly to and from a data buffer, removing the performance hit of parsing you get using json. It’s specifically targeted at applications that need to send/receive a lot of data (such as multiplayer games) where performance needs to be optimal.

Setup

These are the steps to get FlatBuffers up and running:
– Generate the flatc compiler
– Generate the flatbuffers dll
– Create a schema for your project
– Generate classes from your schema
– Use the generated classes in your project

Generate Flatc Executable

You will need the flatc executable to generate classes from your schema. This is how you generate it on a mac.

  • Install CMake
  • Open CMake
  • Select the root of the FlatBuffers project as the source folder.
  • Choose a build folder
  • Click Generate
  • Choose XCode project from the Generator drop down
  • Click Done

This will generate an xcode project for you in the build folder you specified.
– Open the project in xcode and hit Run
This should generate a FlatC executable for you.

Generate Flatbuffers library

I’ve built a dll for version 1.6.0, and for the master branch (pulled the date of this post), which you can find with the compiler in this zip.

If you need a different version:

Download the latest code from github.
Open up the solution in the net folder in whichever IDE you use for dev.
Run the solution to produce a dll
Copy the dll to your Unity project

Create a Schema

FlatBuffer schemas use the Interface Definition Language syntax.
You can find a guide here with all available types.


namespace ParserTest;

struct Vec3 {
x:float;
y:float;
z:float;
}

enum PlayerState : byte
{
Resurrecting,
Alive,
Dead
}

enum GameState : byte
{
Countdown,
InPlay,
GameOver
}

table Player
{
state : PlayerState;
hp : uint ;
name : string;
position : Vec3;
kills : uint;
}

table Game
{
state : GameState;
players : [Player];
}

root_type Game;

Generate classes from the Schema

  • Open up a terminal window
  • Navigate to the schema folder
  • Run the following command:


flatc -n GameSchema.fbs --gen-onefile

Or, if you want to be able to change your buffer once created:


flatc -n GameSchema.fbs --gen-onefile --gen-mutable

Hook up the generated code

FlatBuffers takes an inside-out approach to creating tables – if you have a table A that holds a sub-table B, B needs to be created before A.


private FlatBufferBuilder builder;
private ByteBuffer sendBuffer;

private void CreateBuffers()
{
builder = new FlatBufferBuilder(1024);
Offset playerA = BuildPlayerState(“PlayerA”);
Offset playerB = BuildPlayerState(“PlayerB”);
Offset playerC = BuildPlayerState(“PlayerC”);
Offset playerD = BuildPlayerState(“PlayerD”);

var players = new Offset[4];
players[0] = playerA;
players[1] = playerB;
players[2] = playerC;
players[3] = playerD;
var playersOffset = Game.CreatePlayersVector(builder, players);
Game.StartGame(builder);
Game.AddState(builder, GameState.Countdown);
Game.AddPlayers(builder, playersOffset);
Offset game = Game.EndGame(builder);

builder.Finish(game.Value);
sendBuffer = new ByteBuffer(builder.SizedByteArray());
}

private Offset BuildPlayerState(string playerName)
{
var playerNameOffset = builder.CreateString(playerName);
Player.StartPlayer(builder);
Player.AddName(builder, playerNameOffset);
Player.AddPosition(builder, Vec3.CreateVec3(builder, 1, 2, 3));
Player.AddKills(builder, 0);
Player.AddState(builder, PlayerState.Resurrecting);
return Player.EndPlayer(builder);
}

I’ve made my data objects mutable, created the buffer upfront, and then mutate any small changes I need to make before resending, as that seemed to perform better for the small change I was making in this test (the position of a player), than recreating the game buffer each time (as recommended in the flatbuffers documentation). I guess this might be a case for a flatter data structure? Anyway, this is how I mutated the data and created the byte[] for sending:


var game = Game.GetRootAsGame(sendBuffer);
game.Players(0).Value.MutateHp(80);
SendData(sendBuffer.Data);

To read the object back:


var buf = new ByteBuffer(receivedBytes);
var gameState = Game.GetRootAsGame(buf);

And that’s it!

Performance Results

A quick disclaimer : the following give a quick eyeball comparison between FlatBuffers and some Json serializers…it’s not intended to provide any accurate benchmarking. Also, for the MiniJson test, I haven’t tried to pull any data from the dictionary, so it’s not doing as much work as the other serializers…like I said: just a rough comparison. The flatbuffers devs have done some proper benchmarking, which can be found here. If anyone knows a good library for benchmarking unity stuff (such as BenchmarkDotNet), please let me know!

Test Setup
Using Unity 5.5.2f and a samsung galaxy S7 phone.

Each serializer creates the data object/buffer in the wakeup, then on each frame it will change the position of a player, reserialize the data to a byte array/string and read it back to a format where the data can be used again (100 times).

I wanted to take a relative look at the GC as well as speed, so have used the Editor profiler (app in dev mode). These are the results:

Flatbuffers:

JsonUtility:

MiniJson:

NewtonSoft:

Conclusions

Pros
  • Speedy!
Cons
  • Learning curve
  • Changes to the data structure needs to be run through the flatc compiler and then the buffer creation code amended – it’s much easier to make mistakes.

Do the pros outweigh the cons? I guess it really depends on the use case…it’s essentially workflow and readability vs performance. As I read on Twitter yesterday: it’s much easier to optimize correct code, then correct optimized code. If you’re having performance issues around serializing/deserializing data then it’s definitely worth looking at, otherwise you’re probably just adding unnecessary overhead to your workflow.

Extras

Using Dictionaries with FlatBuffers

We use dictionaries in our current json structure and it would be nice to use this in framebuffers too. Unfortunately it’s not currently implemented very well. Looking at the docs, you can add dictionary-like behaviour to your flatbuffer schema by using a vector of tables, and specifying the key for the lookup using (key) next to the variable in the schema. For example, here I have a collection of levels, LevelData, and want to be able to look up a level by its id. The schema looks like this:


namespace ParserTest;

table Level
{
id : string (key);
name : string;
timeout : uint;
}

table LevelData
{
levels : [Level];
}

root_type LevelData;

To add this to a key-value type structure, you use CreateMySortedVectorOfTables, then you use LookupByKey on the other end to grab the Level by the level id, like so:


private void Start()
{
var builder = new FlatBufferBuilder(1024);
Offset levelA = BuildLevel(builder, "LevelA");
Offset levelB = BuildLevel(builder, "LevelB");
Offset levelC = BuildLevel(builder, "LevelC");
Offset levelD = BuildLevel(builder, "LevelD");
Offset levelE = BuildLevel(builder, "LevelE");
var levels = new Offset[4];
levels[0] = levelA;
levels[1] = levelB;
levels[2] = levelC;
levels[3] = levelD;

var levelsOffset = Level.CreateMySortedVectorOfTables(builder, levels);
LevelData.StartLevelData(builder);
LevelData.AddLevels(builder, levelsOffset);
Offset levelData = LevelData.EndLevelData(builder);
builder.Finish(levelData.Value);
var sendBuffer = new ByteBuffer(builder.SizedByteArray());
var data = sendBuffer.Data;
var buf = new ByteBuffer(data);
var receivedLevelData = LevelData.GetRootAsLevelData(buf);
var level = Level.LookupByKey(levelsOffset, “LevelA”, buf);
Debug.Log(level.Value.Id);
}

private Offset BuildLevel(FlatBufferBuilder builder, string levelId)
{
var levelName = builder.CreateString(levelId);
Level.StartLevel(builder);
Level.AddName(builder, levelName);
Level.AddId(builder, levelName);
Level.AddTimeout(builder, 6000);
return Level.EndLevel(builder);
}

This looks good in theory, but you need the vector offset to pass to the LookupByKey function, and this only seems to be available when you build the buffer…I’m not sure how to use this if receiving the data from elsewhere – which I would imagine is the main use case – so unfortunately this particular functionality seems pretty useless at the moment.

0 likes

Author

Your email address will not be published.