2D Platform Games Part 10: Improved Level Management and Storage
IMPORTANT! To run the pre-compiled EXEs in this article, you must have Windows 7 Service Pack 1 with Platform Update for Windows 7 installed, or Windows 8.
This article builds upon the demo project created in 2D Platform Games Part 9: Storing Levels in Files / Level Editors. Start with 2D Platform Games Part 1: Collision Detection for Dummies if you just stumbled upon this page at random!
Download source code and compiled EXE for the code in this article as well as the complete source code and compiled EXE for the level editor.
In Part 9 we looked at how to store and retrieve game levels from files in a format that could be used both by the game itself and by an auxiliary level editor application. In this article we will make things a little more robust and usable with some improvements:
- Allow the game to load the specified level as a command-line argument
- Re-factor all of the level data into a single class so it can be version-controlled and remove duplicate variable declarations in the game and level editor
- Add functions to the
Platform
class to create platforms from theGeometryDefinition
s stored in the level files, neatening things up by making them more object-oriented and removing duplicate code from the game and level editor
Let’s get cracking!
Command-line arguments in Windows applications
In console C++ applications we can just use argc
and argv
from the parameter list in main()
to inspect each command-line argument. Unfortunately the entry point for Windows applications – WinMain()
– doesn’t provide us with this data. PJ over at My Coding Misadventures (a nice Win32 blog I might add) has written a great article on Retrieving comamnd line parameters from WinMain in win32 if you want the full details, suffice to say that if you are using Visual Studio you can cheat and use the compiler-defined variables __argc
and __argv
(or __wargv
for wide-character arguments) to do the job as follows:
Game class definition:
// Default level name to load std::string levelFilename;
Game class constructor:
// Level to load (may be from command line) levelFilename = "default.splevel"; if (__argc > 1) levelFilename = StringFactory(__argv[1]);
In SetupResources()
:
LoadLevel(levelFilename); // instead of LoadLevel("default.splevel");
StringFactory()
is a helper template function in Simple2D defined as:
// Convert numbers and chars to strings easily template <typename String> string StringFactory(String s) { stringstream ss; ss << s; return ss.str(); }
(include sstream
)
Piece of cake!
Moving all level data into a single class object
Our main motivation for doing this is that Boost.Serialization
(discussed in Part 9) provides versioning for classes, which is very useful during the development of your game as the level file format is volatile while features are added and changed constantly. As a side benefit, it saves duplication on defining variables in the game and level editor such as the player’s start position and background image filenames and keeps everything tidy and in one place. Additionally, when we need to add new items to the level definition, we can just add them to the class definition and they will be automatically serialized and de-serialized in level files and automatically available to both the game and the level editor.
In our project so far, we require the following data to fully define a level (with more to be added in later parts):
- The player’s starting X and Y position in the game world
- Definitions for each platform (this includes their position, shape and any movement parameters – we have already defined the
GeometryDefinition
class for this) - The filenames for the background and parallax background
We can roll this into a class pretty easily. We place it in SharedPlatformCode.h
so it is available to both the game and level editor:
// Definition of an entire level class LevelDefinition { public: // Player start position in level float playerStartX, playerStartY; // Geometry definitions for each platform vector<GeometryDefinition> worldGeometry; // Background and mediumground filenames WCHAR backgroundFile[1000]; WCHAR mediumgroundFile[1000]; // Load/save a level from/to file static LevelDefinition Load(std::string filename); void Save(std::string filename); private: // Allow Boost::serialization access to private members friend class boost::serialization::access; // This template describes how to serialize and deserialize the level into/from a file template <typename Archive> void serialize(Archive &ar, const unsigned int version) { ar & playerStartX; ar & playerStartY; ar & worldGeometry; ar & backgroundFile; ar & mediumgroundFile; } }; // Version 0: initial BOOST_CLASS_VERSION(LevelDefinition, 0) // Prevents warnings from Boost when serializing enums BOOST_CLASS_TRACKING(LevelDefinition, boost::serialization::track_never)
As with our previous serializable object GeometryDefinition
, when you make a change to LevelDefinition
, you will increment the version number in the BOOST_CLASS_VERSION
macro and update the serialize()
method appropriately. Note that we have a vector array of GeometryDefinition
s as part of our level definition class, but if the format of GeometryDefinition
changes, you do not need to increment LevelDefinition
‘s version number, only that of GeometryDefinition
(ie. the class whose definition actually changes).
To be able to serialize a vector, make sure to include boost/serialization/vector.hpp
. Notice that one advantage of serializing a vector in one operation rather than iterating over it and serializing each item one at a time is that you no longer need to manually store the size of the vector (number of items in it). Boost.Serialization
takes care of it for you.
Loading and saving the level is now accomplished via two methods in the class itself (in SharedPlatformCode.cpp
):
// Level definition loader LevelDefinition LevelDefinition::Load(std::string filename) { std::ifstream LevelFile(filename); boost::archive::text_iarchive ia(LevelFile); LevelDefinition level; ia >> level; LevelFile.close(); return level; } // Level definition saver void LevelDefinition::Save(std::string filename) { std::ofstream LevelFile(filename); boost::archive::text_oarchive oa(LevelFile); oa << *this; LevelFile.close(); }
A couple of things to note: the loading function is a static
member of the class, meaning that you don’t need to instantiate a LevelDefinition
object before calling it. You simply call LevelDefinition::Load()
and it returns a newly loaded and initialized LevelDefinition
instance for you. On the other hand, the saving function must be called on an instance, and serializes a copy of itself to the specified file.
We now go through the game and level editor code and remove the definitions and any initialization of playerStartX
, playerStartY
, backgroundFilename
, mediumgroundFilename
and worldGeometry
from the game and level editor’s main class definitions and methods (see the source code for this), and in the editor only, replace them with:
// The level we are editing LevelDefinition level;
(the game does not need the level definition once the level has been generated, whereas the editor must be able to modify it on the fly)
Next we have to re-factor the load and save functions. Previously loading a level looked something like this:
void MyProgram::LoadLevel(std::string filename) { std::ifstream LevelFile(filename); boost::archive::text_iarchive ia(LevelFile); // Player start position in level ia >> playerStartX; ia >> playerStartY; // Number of geometry objects int numObjects; ia >> numObjects; // Geometry definitions for each platform worldObjects.clear(); GeometryDefinition d; for (int i = 0; i < numObjects; i++) { ia >> d; Platform p; /* DIFFERENT PLATFORM INITIALIZATION CODE GOES HERE FOR THE GAME AND LEVEL EDITOR */ worldObjects.push_back(p); } ia >> backgroundFilename; ia >> mediumgroundFilename; LevelFile.close(); /* IN THE GAME, CODE TO CALCULATE THE WORLD BOUNDING BOX, LEVEL EXTENTS ETC. GOES HERE */ }
The game and level editor both use their own versions of the Platform
class, so we can move the per-platform initialization code which is unique to each application into specialized Platform
constructors. Loading a level then becomes reduced to:
The level editor:
void MyProgram::Load(std::string filename) { level = LevelDefinition::Load(filename); worldGeometry.clear(); for (auto it = level.worldGeometry.begin(); it != level.worldGeometry.end(); it++) worldGeometry.push_back(Platform(*it)); }
The game:
void MyProgram::LoadLevel(std::string filename) { // Load the definition LevelDefinition level = LevelDefinition::Load(filename); // Set player start position playerStartX = level.playerStartX; playerStartY = level.playerStartY; // Configure platforms worldObjects.clear(); for (auto it = level.worldGeometry.begin(); it != level.worldGeometry.end(); it++) worldObjects.push_back(Platform(*it)); // Configure backgrounds background = MakeImage(level.backgroundFile); mediumground = MakeImage(level.mediumgroundFile); /* BOUNDING BOX, LEVEL EXTENTS CODE ETC. STAYS HERE UNCHANGED */ }
In the level editor, saving the level is changed from:
void MyProgram::Save(std::string filename) { std::ofstream LevelFile(filename, std::ios::out); boost::archive::text_oarchive oa(LevelFile); // Player start position in level oa << playerStartX; oa << playerStartY; // Number of geometry items int s = worldObjects.size(); oa << s; // Geometry definitions for each platform for (auto it = worldObjects.begin(); it != worldObjects.end(); it++) oa << (*it).def; // Name of background and mediumground oa << backgroundFilename; oa << mediumgroundFilename; LevelFile.close(); }
To:
void MyProgram::Save(std::string filename) { // Update all the geometry definitions level.worldGeometry.clear(); for (auto it = worldGeometry.begin(); it != worldGeometry.end(); it++) level.worldGeometry.push_back(it->def); // Perform the save level.Save(filename); }
The editor does not modify the geometry definitions in the level definition directly, so we first replace the level’s geometry with that being used by the level editor, then save it.
The level editor also needs a function to create a blank level when the application starts or the user chooses to create a new level from the toolbar:
// Create a blank level void MyProgram::CreateDefaultLevel() { // Set default background filenames wcscpy_s(level.backgroundFile, L"bg.png"); wcscpy_s(level.mediumgroundFile, L"mg.png"); background = MakeImage(level.backgroundFile); // No geometry level.worldGeometry.clear(); worldGeometry.clear(); // Set default player position level.playerStartX = 0; level.playerStartY = 0; }
We could make this a method of LevelDefinition
, but the code is only needed by the level editor so we choose to just use a function instead.
This just leaves the issue of creating the Platform
constructors which initialize the object using the information from a GeometryDefinition
.
Object-oriented platform creation from a definition object
In the level editor this is a cinch since Platform
only contains the Direct2D geometry of the platform, and the definition itself:
// Create a platform from a geometry definition Platform::Platform(GeometryDefinition &d) { geom = MyProgram::Tools.CreateGeometry(d); def = d; }
In the game, we have to do a bit more work as we need the initial bounding box of the geometry and to configure the X and Y pathing animations if the platform is moving. We also have to set the surface type and initial position of the platform in the constructor’s initializer list:
// Create a platform from a geometry definition Platform::Platform(GeometryDefinition &def) : surface(def.surface), defaultX(def.topLeftX), defaultY(def.topLeftY), moving(def.moving) { geom = MyProgram::Tools.CreateGeometry(def); bounds = geom.GetBounds(); if (def.moving) { boost::function<double (double)> mfX, mfY; if (def.moveFuncX == MT_Linear) mfX = Animations::Linear; if (def.moveFuncX == MT_Cosine) mfX = Animations::Cos; if (def.moveFuncX == MT_Sine) mfX = Animations::Sin; if (def.moveFuncY == MT_Linear) mfY = Animations::Linear; if (def.moveFuncY == MT_Cosine) mfY = Animations::Cos; if (def.moveFuncY == MT_Sine) mfY = Animations::Sin; x = Animation(mfX, def.moveIntervalX, def.moveAmplitudeX, def.moveBaseX, def.moveCropLowerX, def.moveCropUpperX, def.moveCycleTypeX, false); y = Animation(mfY, def.moveIntervalY, def.moveAmplitudeY, def.moveBaseY, def.moveCropLowerY, def.moveCropUpperY, def.moveCycleTypeY, false); } }
And that’s it. We now have a fully encapsulated, serializable and easily extensible level definition class!
Storing as byte data
Note: to reduce file sizes and make it slightly harder for people to abuse your level files, I recommend using binary archives rather than text archives in Boost.Serialization
to save and load the levels. The former writes everything as raw data, whereas the latter (which we have been using so far) converts all numbers to a human-readable text format. This change is made in the source code from Part 12 onwards, but in brief, you can just change:
#include <boost/archive/text_oarchive.hpp> #include <boost/archive/text_iarchive.hpp>
to:
#include <boost/archive/binary_oarchive.hpp> #include <boost/archive/binary_iarchive.hpp>
and be sure to include the std::ios::binary
flag when you open level files for input or output:
// Loading std::ifstream LevelFile(filename, std::ios::binary); boost::archive::binary_iarchive ia(LevelFile); ... // Saving std::ofstream LevelFile(filename, std::ios::binary); boost::archive::binary_oarchive oa(LevelFile); ...
Updates to the Level Editor
This part includes a new version of the level editor with the following changes and improvements:
- The save dialog now automatically defaults to the name of the current file
- Opening a file will give a warning if there unsaved changes to the current file
- The file being edited is no longer marked as changed when a mouse drag ends unless there is a selected object
- The
LevelDefinition
file format is now used
You Have Now Leveled Up
In Part 11 we are going to look at some tricky collision detection problems that can arise in our game so far, and how to solve them. Until next time!
-
February 6, 2013 at 18:342D Platform Games Part 9: Storing Levels in Files / Level Editors « Katy's Code
-
February 19, 2013 at 05:422D Platform Games Part 11: Collision Detection Edge Cases for The Uninitiated « Katy's Code
-
March 15, 2013 at 15:16Tetris: Adding polish with music, sound effects, backgrounds, game options, an intro sequence and other tweaks | Katy's Code
-
January 4, 2014 at 00:372D Platform Games Part 12: A Framework for Interactive Game Objects | Katy's Code