Brainroll Postmortem Part 5: Assets, Serialization & Next Steps
How serialization and assets are handled within Brainroll as well as some notes on what I plan on doing in the future.
I have been in somewhat of a transition period in my life hence my focus has not been on writing about Brainroll or dealing with anything programming related. Now when I feel like I’ve landed more in what I want to do and where I want to go in life I thought its time to wrap up the postmortem posts. Since in the previous post I talked about the key parts of Brainroll and in my opinion the most interesting parts when it comes to tech I will wrap this up by talking about how I dealt with Levels, assets & save files before finally wrapping up with talking about what I want to do next.
Asset system
The older versions of Brainroll were way friendlier when it comes to for example levels. It had a built-in level editor that would save levels into specific level files, in my mind I thought it would be cool if users would create levels and share with eachother like Mario Maker or something like that. That being very early in development I didn’t really have a plan for dealing with for example changes in the engine or levels, sure I can load up each level and make changes to them but what if I change key parts of how levels are loaded or structured? I noticed problems with this early which made me create some sort of versioning system for the level files as well as a way for the level editor to convert the level files to the newest version.
After some time I had my first user use the level editor, to them it was not only confusing as how to use the editor but it was hell for me to take care of building the system that would keep the levels up to date when changes happened to the game. I had thousands of lines of code to deal with conversions between formats and versions and different edge-cases and after some time I just decided that this system has to go. Brainroll didn’t ship with a level editor and instead I just converted each level to the most bare-bones simple string representation I could think of and hard-coded them into the game.
struct level_definition
{
u32 Width;
u32 Height;
u32 MinimumNumberOfMoves;
char *Name;
char *Level;
level_rope_connections RopeConnections;
};
/*
* W - Wall
* . - Floor
* 1 - Slide Player
* S - Sticky Roll
* P - Passable Solid
* p - Passable Hollow
* G - Goal
* D - Door
* R - Pressure Plate
* r - Rope (Put infront of tile to be connected to, Example: rW or rS)
* - Space is ignored when parsing
* # - Snow (Slime)
*/
STN_INTERNAL level_definition
Levels[] =
{
{ // 38.
13, 14,
53,
"Ascend",
"WWWWWWWWWWWWW"
"WWW.......WWW"
"W.....W.....W"
"W...........W"
"W....SSR....W"
"W....WWW....W"
"WW...WGW...WW"
"W....WDW....W"
"W...r...r...W"
"W..r.....r.WW"
"WWS.......S.W"
"W...........W"
"W1....S.....W"
"WWWWWWWWWWWWW",
{
4,
{ { 4, 8 }, { 5, 7 }, { 3, 9 } },
{ { 3, 9 }, { 4, 8 }, { 2, 10 } },
{ { 8, 8 }, { 7, 7 }, { 9, 9 } },
{ { 9, 9 }, { 8, 8 }, { 10, 10 } },
}
},
};
This piece of code shows an example level with the translation of what each character translates to in the world. These level definitions are used as a blue print to how a level should look in its initial state. In the structure I specify a width and height for the level followed by the least “number of moves” to complete the level. Then you give your level a name and specify the level itself. Optionally if the level uses ropes you specify how many ropes there are followed by their connections in tiles, each rope has a position and two tiles they are connected. Ropes can be connected to another rope to create a longer rope or a wall or something else to be attached to a object.
This system sure is booring and removes the original modding support I had planned but it worked perfectly to just wrap the game up and get to the finish line. However I will probably not do this again and if I work on another title with many levels I will deffinately spend more time building a robust system around handling them.
If we move on to talk about assets such as sprites and sound they are stored in pack files which I just randomly named to .mton files which stand for Maraton (the name of my engine). These files are very simple and what I’ve done is basically create a small program which I call packer, packer takes a list of files and just reads some data that my engine needs before copy pasting the file contents after eachother into one large file.
The structures used by packer and the engine looks like the following:
struct maraton_ff_texture
{
int32_t Width;
int32_t Height;
void *Pixels;
};
struct maraton_ff_sound
{
int32_t Channels;
int32_t SampleRate;
uint32_t SampleCount;
int16_t *Samples;
};
struct maraton_ff_asset
{
uint64_t DataOffset;
uint64_t DataSize;
union
{
maraton_ff_texture Texture;
maraton_ff_sound Sound;
};
};
So basically packer produces a file with a list of maraton_ff_asset
s which we on startup of Brainroll just read from disk into memory. If these files were larger than what they are we could improve this by making the files searchable through some sort of lookup table within the file format but I knew Brainroll wouldn’t have such requirements so I just settled with this. Packer uses external libraries to load the files and get their data, it uses stb_vorbis for loading .ogg files and stb_image for .png files.
Loading it within the game is as simple as opening the file, and for each maraton_ff_asset
we just read the next DataSize number of bytes and then we have everything we need to use it within the game after we cast it to either a sound or texture asset depending on what we’re loading.
Serialization
When it came time to remember the players progress I did think of my problems handling versioning with the old levels and hence I wanted to actually implement some form of serialization. My save games very and are very simple, the only information I really need to keep track of is what levels the user has completed and how many moves they completed each level in. I simply use two arrays of integers to store these values.
struct save_game_state
{
u32 LevelCompleted[SAVE_GAME_FILE_NUMBER_OF_LEVELS];
u32 NumberOfMoves[SAVE_GAME_FILE_NUMBER_OF_LEVELS];
};
The real magic is when these arrays needs to grow. I found a really simple and interesting technique that was used by Media Molecule for Little big planet.
The serializer presented as “LBP Serializer” uses a single function to perform both serialization and deserialization. When performing serialization of a structure we do it for each field within it. This means that we only have to prepare serialization functions for the base types that we want to use and then we can go ahead with easily serializing any structure we want.
The serialization or deserialization of a base type is very simple, we either are writing to a buffer or we are reading from that buffer, to do that we simply need to keep track of where in the buffer we are reading or writing to which we can do by storing a cursor or index into our buffer.
struct lbp_serializer
{
u32 DataVersion;
b32 IsWriting;
u32 Counter;
u8 *Buffer;
u32 BufferCursor;
};
STN_INTERNAL void
Serialize(lbp_serializer *LBPSerializer, uint32_t *Datum)
{
if (LBPSerializer->IsWriting)
{
uint32_t *Pointer = (uint32_t *)(LBPSerializer->Buffer + LBPSerializer->BufferCursor);
*Pointer = *Datum;
}
else
{
*Datum = *(uint32_t *)(LBPSerializer->Buffer + LBPSerializer->BufferCursor);
}
LBPSerializer->BufferCursor += sizeof(uint32_t);
}
#define ADD(_FieldAdded, _FieldName) \
if (LBPSerializer->DataVersion >= (_FieldAdded)) \
{ \
Serialize(LBPSerializer, &(Datum->_FieldName)); \
}
#define REM(_FieldAdded, _FieldRemoved, _Type, _FieldName, _DefaultValue) \
_Type _FieldName = (_DefaultValue); \
if (LBPSerializer->DataVersion >= (_FieldAdded) && \
LBPSerializer->DataVersion < (_FieldRemoved)) \
{ \
Serialize(LBPSerializer, &(_FieldName)); \
}
STN_INTERNAL void
Serialize(lbp_serializer *LBPSerializer, save_game_state *Datum)
{
// SAVE_GAME_SERIALIZATION_VERSION_INITIAL
{
for (u32 Index = 0;
Index <= SaveGameGetNumberOfLevelsForVersion(SAVE_GAME_SERIALIZATION_VERSION_INITIAL);
++Index)
{
ADD(SAVE_GAME_SERIALIZATION_VERSION_INITIAL, LevelCompleted[Index]);
}
for (u32 Index = 0;
Index <= SaveGameGetNumberOfLevelsForVersion(SAVE_GAME_SERIALIZATION_VERSION_INITIAL);
++Index)
{
ADD(SAVE_GAME_SERIALIZATION_VERSION_INITIAL, NumberOfMoves[Index]);
}
}
}
A set of helper macros are created for Adding and Removing from our serialized data, within the function where we serailize our main structure we can specify which version we’ve added something to the file or removed something from the file. This makes sure that if we’re reading a file that is for example older than when we’ve added something it will simply skip it, and in the case something was removed we will check when it was added and when it was removed and only serialize it if its present, this information is passed to the remove macro.
This creates a serialization system with very few lines of codes that is also very robust and can withstand a lot of changes to the codebase which is perfect. I will most likely try using this again. I have not tried it with very big set of data yet but I can imagine it would work pretty well with that also.
Moving on
Wrapping things up I want to talk a bit about the future, both for Brainroll and myself. I have since I started with Brainroll had the aspirations of releasing the game to multiple platforms, I wanted to do this at the time of release but life got inbetween things and my engine doesn’t have support for anything else than Windows. I have since the release of Brainroll made a lot of patches to my engine Maraton and I am currently in the process of releasing version 2 of it. This is a version where I’ve re-done a lot of the APIs making creating new games or applications as well as porting to several platforms way easier. I will not promise anything but I really want to have Brainroll running on atleast linux when I’m done with the project.
Other than the engine specific stuff I have always loved achievements in games. I didn’t get achievements prepared before the release of Brainroll as I got fully burned out at the end but I am deffinately coming back to it and adding them.
Future content? I did prepare additional mechanics and content for the game but I am unsure if I am going to proceed with adding them into the game. I have built up some sort of love/hate relationship with Brainroll over the past year making me hesitate spending more time creating levels for it but at the same time it might be a good idea to see it as a bit of a redemption arc for myself where I can fix some of the mistakes I never enjoyed with it. We’ll see!
I have a very nice urge right now to learn and experiment so while all of this is happening I think I will try to re-ignite my passion for programming by spending more time with making prototypes and learning new things. In addition to this I want to invite everyone who is reading this into development process a bit more by creating video content as well. It’s going to be pretty cool!
Did you know that you get access to the Maraton source code if you become a paid subscriber to my website? If you’re interested in learning or to be a part of the development then don’t hesitate and click the button below!