Serialization using Protobuf
by admin
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.
protobuf de-serialization can be slow at times. It all depends on how much data you are packing into these objects.
You’re comparing apples and oranges. Of course working with anything DOM-ish is going to be a pain. Protobuf on the other hand is basically a scema compiler, like JAXB or xmlbeans, except it works for C++. Don’t blame the markup language – blame the tools.
Also, you’re still doing useless work. Ideally you should be able to use the deserialized objects as-are. Unfortunately protobuf makes this difficult by hiding the variables behind getters and setters.
Plug: I wrote an XML schema compiler for C++ a while back. Although hackish, it outputs classes which are easy to use and inherit (no getters or setters – just plain member variables): https://github.com/Tjoppen/james
Overall, I <3 protobufs, but since you're looking for downsides:
* You cannot specify a custom allocator for protobufs. It will use new and delete whether you like it or not. The protobuf devs do not plan on changing this.
* The library depends on static initialization in a few places. This issue can be worked around by tweaking the code (yay open source), and probably wouldn't affect most people, but I had to butt heads with it.
Oh, one more (:
* If you are deserializing from a stream, protobufs do not themselves know when to stop reading. You have to store the length information separately.
Also look into msgpack (http://msgpack.org/). It’s used by Redis and others and boasts of better performance when compared to Protobufs (sorry, can’t find the link for that).
Excellent intro to protobufs.
As a heads up, regarding the following:
> 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.
The apostrophe is not used to pluralize nouns ending in vowels. It’s simply “cameras.”
If you need help, read this:
http://en.wikipedia.org/wiki/Apostrophe
[…] So why pick this platform? Because it’s a challenge. I’ve been meaning to make something for it ever since I first got it. I have the skills, all I need now is persistence. Every day, before work, I launch Visual Studio and try to get something done. Last week I worked on level loading. I was using a hardcoded level definition that just didn’t cut it anymore. So I turned to my favorite serialization library: Protobuf. […]