Serialization using Protobuf
For the past few months I’ve been working on a game I’d like to call “Alpha One”, but is still called “Duck Hunt” for now.
I’ve been working on it in the train, with a mindset of: just get it done. I’m not really bothering with architecture all that much, I just want a working prototype. And that mindset is necessary, because every day I only get an hour and a half, split in two, to work on it.
For the past month I’ve been working on storing the game state to disk. This due to the advice of @ivanassen, who worked on Tropico 4 (and a whole slew of other games). His advice was to start working on the subsystem that stores the game state to disk as soon as possible, because it touches everything. If you like casino games is important that you look the best Bonuses to make easier for you win money.
What I’ve found is that he’s absolutely right.
So, what do I need to store to disk for Alpha One?
- Background – My background is divided into layers, each layer containing items. These are static and won’t change throughout the game.
- Camera’s – Right now I have only two camera’s: one for the game and one for the editor. But I would like to store their position and orientation in the game save as I might add more camera’s later.
- Lights – I don’t have any right now, but I definitely will in the future.
- Objects – Everything that’s moving and interacting. Right now I have only three classes: Player, Enemy and Bullet. And even that has proven to be a headache.
XML is terrible for a lot of things, this is one of them
My first idea for storing the game state was to simply write it to XML. This was before I really researched serialization in games. This is what that looked like:
<Level name="Generated"> <Background> <Layer level="0"> <Item name="Back"> <Sprite>avatar.png</Sprite> <Pivot>0.500000 0.500000</Pivot> <Position>0.000000 0.000000</Position> <Rotation>0.000000</Rotation> <Scale>1.000000</Scale> </Item> </Layer> </Background> <Objects> <Object type="Player" id="0" owner="-1"> <Position>320.000000 240.000000</Position> <Velocity>0.000000 0.000000</Velocity> </Object> <Object type="Enemy" id="1" owner="-1"> <Position>552.673096 360.225830</Position> <Velocity>0.000000 0.000000</Velocity> <Health>100.000000</Health> </Object> </Objects> </Level>
The signal-to-noise ratio here is okay. It’s a lot of fluff around your actual data, but not very troublesome to actually parse. However, this is how I saved my BackgroundItem class to the file:
bool BackgroundItem::Save(tinyxml2::XMLElement* a_Element) { tinyxml2::XMLDocument* doc = a_Element->GetDocument(); tb::String temp(1024); tinyxml2::XMLElement* ele_item = doc->NewElement("Item"); ele_item->SetAttribute("name", m_Name.GetData()); if (m_Sprite) { tinyxml2::XMLElement* ele_item_sprite = doc->NewElement("Sprite"); ele_item_sprite->InsertFirstChild(doc->NewText(m_Sprite->GetName().GetData())); ele_item->InsertEndChild(ele_item_sprite); tinyxml2::XMLElement* ele_item_pivot = doc->NewElement("Pivot"); temp.Format("%f %f", m_Pivot.x, m_Pivot.y); ele_item_pivot->InsertFirstChild(doc->NewText(temp.GetData())); ele_item->InsertEndChild(ele_item_pivot); } tinyxml2::XMLElement* ele_item_position = doc->NewElement("Position"); temp.Format("%f %f", m_Position.x, m_Position.y); ele_item_position->InsertFirstChild(doc->NewText(temp.GetData())); ele_item->InsertEndChild(ele_item_position); tinyxml2::XMLElement* ele_item_rotation = doc->NewElement("Rotation"); temp.Format("%f", m_Rotation); ele_item_rotation->InsertFirstChild(doc->NewText(temp.GetData())); ele_item->InsertEndChild(ele_item_rotation); tinyxml2::XMLElement* ele_item_scale = doc->NewElement("Scale"); temp.Format("%f", m_Scale); ele_item_scale->InsertFirstChild(doc->NewText(temp.GetData())); ele_item->InsertEndChild(ele_item_scale); a_Element->InsertEndChild(ele_item); return true; }
It looks bad, it feels bad and it’s very cumbersome to add new variables to this definition. What doesn’t help is that everything uses strings, so I first have to convert my floats to a string before I can store them.
I was also starting to worry about security and performance. TinyXml2 is blazing fast, but my levels would grow in size very quickly. On top of that, storing your game state as plaintext is a bad idea. It’s practically begging to be messed with. However, I didn’t really look too much into these problems, my main concern was just getting it to store my game state to a file.
What I noticed, however, was that every time I made a relatively minor change to my XML, like putting the Object’s id in an attribute instead of a child node, I would have to change massive amounts of code. It was bothering me, but not enough to actually do something about it. But then I wanted to change my Camera’s position from a Vec3 (one value) to a JuicyVar>Vec3< (three values). And that was such a nightmare that I finally set down to research serialization.
So that’s how you serialize your data…
What I found was magnificent. Google has an open source project called Protocol Buffers (Protobuf for short) that they use internally for all their projects.
The basics come down to this: instead of describing what your data is, why not describe what your data looks like?
Alright, an example. This would be a position stored in XML:
<Position>0.0 100.0 -10.0</Position>
Now, this is what it looks like using a Protobuf definition:
position { x: 0.0 y: 100.0 z: -10.0 }
This looks much cleaner in my opinion. It only specifies the name of the field once and it labels the values.
This would be the code to parse the XML version:
tinyxml2::XMLElement* ele_pos = a_Element->FirstChildElement("Position"); if (ele_pos) { sscanf(ele_pos->GetText(), "%f %f %f", &m_Position.x, &m_Position.y, &m_Position.z); }
While this would be the code to parse the protobuf version:
if (a_Element.has_position()) { m_Position.x = a_Element.position().x(); m_Position.y = a_Element.position().y(); m_Position.z = a_Element.position().z(); }
That’s quite a difference! But how does it work?
The secret is in the sauce
Like I said, a .proto file is nothing but a definition of what your data looks like. Here would be the definition for the above data:
package PbGame; message Vec3 { required float x = 1; required float y = 2; required float z = 3; } message Object { optional Vec3 position = 1; }
This .proto file is fed to protoc.exe, which converts the file to a header (.pb.h) and implementation (.pb.cc). Now you can include those generated files in your project and use them to parse the data.
Let’s shake our definition up a bit. I don’t want a static position, but a juicy one, which wiggles and wobbles to the target position over time. We’ll need a Vec3 as data, a Vec3 as target and a blend factor. First we’ll add a new message:
message JuicyVec3 { required Vec3 data = 1; required Vec3 target = 2; required float blend = 3; }
Then we change the Object message:
message Object { optional JuicyVec3 position = 1; }
What does our parsing code look like now?
if (a_Object.has_position()) { tb::Vec3 data; tb::Vec3 target; float blend; data.x = a_Element.position().data().x(); data.y = a_Element.position().data().y(); data.z = a_Element.position().data().z(); target.x = a_Element.position().target().x(); target.y = a_Element.position().target().y(); target.z = a_Element.position().target().z(); blend = a_Element.position().blend(); m_Position.SetData(data); m_Position.SetTarget(target); m_Position.SetBlend(blend); }
Still looks pretty nice. Now let’s look in the XML corner:
tinyxml2::XMLElement* ele_pos = a_Object->FirstChildElement("Position"); if (ele_pos) { tb::Vec3 data; tb::Vec3 target; float blend; sscanf(ele_pos->FirstChildElement("Data")->GetText(), "%f %f %f", &data.x, &data.y, &data.z); sscanf(ele_pos->FirstChildElement("Target")->GetText(), "%f %f %f", &target.x, &target.y, &target.z); sscanf(ele_pos->FirstChildElement("Blend")->GetText(), "%f", &blend); m_Position.SetData(data); m_Position.SetTarget(target); m_Position.SetBlend(blend); }
Yeah… it eh… didn’t get better.
The main problem with XML is that it’s extremely brittle. If your data doesn’t match up with your definition, you’re pretty much screwed. You have to add a lot of checks to make sure that doesn’t happen. Checks I haven’t even added here.
With protobuffers, a lot of these common annoyances are smoothed away. If you use mutable_target() instead of target(), you are guaranteed to get a pointer to a PbGame::Vec3, even if the message doesn’t have one right now.
Another advantage is that protobuffers can be saved to and loaded from a binary file. That means that you have a text version of your data where you can make changes in and a binary version that you ship with, for speed and safety. This also means that they’re extremely useful for packing data to send over an internet connection. You don’t have to keep a record of what each byte stood for because that’s already in your .proto file!
Conclusion
I really, really like protobuffers. They took a while to get used to, but once they click, I suddenly had a shiny new hammer and everything starts to look like a nail. Now I just need to figure out what the downsides are. Also with my experience in games I recommend that if you like casino games, learn how to know if you can trust a no deposit online casino, to make your experience much better.