Introduction
Entity-Component systems, we’ve learned, are easy to implement and maintain; the elegance is basically “baked in” due to the way components and entities are married in the Entity Manager.
One particularly tidy aspect of an entity-component system is how well it lends itself to data persistence, or in practical terms: saving game state. Let’s take a look.
Where Is State?
In a conventional object oriented design, state is scattered all over the place, embedded in your far-flung object instances. But in E-C everything is neatly gathered together under one roof: the Entity Manager. This manager knows every entity “instance” along with every entity’s components, which are where the entity state data are stored.
Therefore, persist the entity manager to disk and you’ve saved the game in its entirety. Load from disk to memory and you’ve just loaded the game. It really is that easy.
Serialization in Ruby
Serialization, also called marshaling, is simply the act of converting a data structure into a format that can be easily stored and later retrieved.
Ruby provides a number of ways for you to serialize data. The language has YAML serialization as well as a binary format built right in, and with a third-party library you can output JSON. (This highlights just another of the many beautiful things about Ruby: its delightful, flexible propensity to easy customization.)
Textual serialization formats like YAML are convenient because they are human-readable and easy to debug. Ultimately, however, you will likely want to serialize to a binary format. We do this using Ruby’s baked-in marshal_dump mechanism. Not only is binary serialization ten to twenty times faster than YAML serialization — important for a game of any scale — it obfuscates your save-game data. This is desirable to deter cheating and reverse-engineering of your code.
Here’s how you configure a Ruby object for binary data serialization:
That was an attempt to be wry. 😉 But seriously: many, even most, of your components will probably be serializable without any modification if they are basic data stores. SpatialState is a good example: look in that class, you’ll find no special serialization instructions. As they say, It Just Works — most of the time. Sometimes, however, an object doesn’t lend itself to Ruby’s default marshaling strategy. A good example of this is our Renderable component, which has an @image instance variable containing binary libGDX Texture data. We don’t want to serialize the binary data, only the image filename. (And subsequently, when deserializing the data from disk, we will restore the binary image data using the filename.)
For these kinds of objects we help Ruby understand how to serialize and deserialize the data via the marshal_dump and marshal_load methods:
def marshal_dump [@id, @image_fn, @scale, @rotation] end def marshal_load(array) @id, @image_fn, @scale, @rotation = array @image = Texture.new(Gdx.files.internal(image_fn)) end
Here you can see that I’ve instructed Renderable to serialize to a straightforward four-tuple: a simple array. Then, inflating the object becomes a matter of reading that tuple and then reading the image data from disk.
Saving and Restoring Game State
So, to save the entire game state we need to:
- Serialize the entity manager and all its entities and components.
- Save that serialized data to disk.
To restore game state we just need to:
- Load the serialized data from disk.
- Deserialize, or inflate, the data back into the entity manager.
Much like we did for the Renderable component, we supply the entity manager with marshal_dump and marshal_load methods to help it cleanly serialize and deserialize. As before, we’re just defining the tuple of useful information that we care to persist:
def marshal_dump [@id, @ids_to_tags, @tags_to_ids, @component_stores] end def marshal_load(array) @id, @ids_to_tags, @tags_to_ids, @component_stores = array end
Note that serialization “cascades” downward — by serializing the component stores, we then automatically serialize the entities and their components, whereby each component instance is itself serialized. The result is one big data structure that we can save to disk.
How do we save it to the disk and load it back later? Predictably, Ruby makes this easy:
File.open("savedgame.dat", "w") do |file| file.print Marshal::dump(@entity_manager) end
if File.size? 'savedgame.dat' @entity_manager = Marshal::load( File.open( 'savedgame.dat' ) ) @entity_manager.game = self end
That really is it. No tricks or hidden mirrors. In a handful of lines of code you just added game persistence. Elegant, beautiful, succinct: all the things that make Ruby so great.
YAML Serialization
If you’d like to experiment with YAML serialization while you learn how it works, it is easy to implement. Here’s how you serialize an entire object:
YAML::dump(@some_object)
For the tricky components like Renderable that need a little “helping hand” with serialization, you’ll want to implement encode_with and init_with instead of marshal_dump and marshal_restore:
def encode_with(coder) coder['image_fn'] = @image_fn coder['scale'] = @scale coder['rotation'] = @rotation end def init_with(coder) @image_fn = coder['image_fn'] @scale = coder['scale'] @rotation = coder['rotation'] @image = Texture.new(Gdx.files.internal(image_fn)) end
(Prior to Ruby 1.9 this was done with to_yaml_properties, but that has been deprecated.)
Loading from disk becomes:
YAML::load( File.open( 'savedgame.yaml' )
If you would like a deeper discussion of Ruby serialization in general, I recommend this blog post by Alan Skorkin.
Coming Up
Don’t forget, all of this code is in the Github repository. In the next installment I’ll add a few fun features just to illustrate how easy it is to work with an Entity-Component game codebase…