Home > Game Development Walkthroughs > Tutorial: A Modern Approach To Implementing High Score Tables in C++ using STL and Boost

Tutorial: A Modern Approach To Implementing High Score Tables in C++ using STL and Boost

September 22, 2012 Leave a comment Go to comments

In this article I shall demonstrate a modern, object-oriented, re-usable technique to adding a local high score table to your C++ Windows games using STL and Boost. I shall use our SimpleTetris game of which this article is a follow-on.

Please note that this is by no means an efficient implementation, it is designed to just show the principle of the technique.

Download: Source code (.cpp) | Source code (.h) | Executable

Storage location

High score tables should be persistent which means the data should not be lost when the game is closed and re-opened at a later time. Therefore the high score table needs to be stored to disk, and our first task is to figure out where to store them.

In Windows, there are three recommended storage locations (the full list can be found in MSDN’s KNOWNFOLDERID documentation):

Local application data (FOLDERID_LocalAppData), which is for data that should be bound to both the user, the machine and the application. For example, redefinable game controls would be stored here as different machines may use different keyboard layouts, and each user may want their own control settings.

Roaming application data (FOLDERID_RoamingAppData), which is for data that should be bound to the user and the application but can be shared across machines. High score tables and save game files would be stored here and any other application data that is not machine-specific.

Per machine application data (FOLDERID_ProgramData), which is for data that should be bound to the application and the machine but not the user. Graphics card and sound settings for a game would be stored here as different machines use different hardware but all users on a given machine have the same hardware at their disposal.

The recommended protocol to avoid filename clashes with other applications is to store your files in sub-folders of the above locations in the format \ManufacturerName\AppName\Version, for example in this case \DJKaty.com\SimpleTetris\1.2. You can use code like this to resolve the correct path when your application starts up:

// Resolve application storage path
if (ManufacturerName != "" && AppName != "" && Version != "")
{
    WCHAR *path;
    SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, NULL, &path);
    DataPath = StringFactory(path) + "\\" + ManufacturerName + "\\" + AppName + "\\" + Version + "\\";</code>

    // Create the path (including all sub-directories) if it doesn't already exist
    SHCreateDirectoryEx(NULL, DataPath.c_str(), NULL);
}

NOTE: If you are using my Simple2D library, you can use Simple2D.DataPath to fetch the appropriate location which is automatically initialized when the application starts.

Setting up Boost

You’ll need the following includes for the remaining code in this tutorial:

// High score-related includes
#include <set>
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/serialization/string.hpp>
#include <boost/algorithm/string.hpp>

Structure of a single score

We shall now define a class to store a single score. Note that although I have included this in SimpleTetris.h, for re-usability it should ideally be implemented in a separate header file. Making an individual score class re-usable is a little tricky because different games will want to store different information, for example difficulty level, fastest time etc. In SimpleTetris we will store the player’s name, score, maximum level reached and number of lines cleared. Ideally you will want to subclass Score to something like eg. TetrisScore to add these extra details but here I’ve just been lazy and put it all in the base class for simplicity:

struct Score
{
	std::string name;
	int score;
	int level;
	int linesCleared;

	// Comparator for STL sorts
	bool operator>(Score const &s) const { return score > s.score; }

	// Quick initialisation
	Score() : name(""), score(0), level(1), linesCleared(0) {}
	Score(std::string n, int s) : name(n), score(s), level(1), linesCleared(0) {}
	Score(std::string n, int s, int l, int lc) : name(n), score(s), level(l), linesCleared(lc) {}

private:
	// Serialization helpers
	friend class boost::serialization::access;

	template <typename Archive>
	void serialize(Archive &ar, const unsigned int version)
	{
		ar & name;
		ar & score;
		ar & level;
		ar & linesCleared;
	}
};

Relax! It’s not as complicated as it looks! The first four lines simply store the score data itself – a single player name, score, level and number of lines cleared.

The comparator line defines the rule for sorting scores into order on the table. In the usual case the highest scores should appear top (and therefore be the first entries in the table), and the condition score > s.score merely compares a new score to an existing score to decide whether it should be placed above or below it in the table. Later we will use a structure known as an STL Multiset to store the entire score table. This is a structure which has the nice benefit that when you add an entry (score) to it, the sorting is done automatically for you, which is perfect for our needs and saves effort by merely adding this one line of code to the Score class.

The next three lines simply define constructors to make adding new scores quick, with defaults if not all of the optional details are supplied.

In the private declaration section, we specify that we will be using the Boost.Serialization library to store the score in a file. Do not worry about what it all means, simply understand that by including these lines you set up persistent storage in a way that you can just forget about. If you need to store different details, simply add or remove lines from inside serialize() similar to those already there.

Structure of the whole high score table

We now need to store a group of these single Score structures into a class which represents the entire high score table. Here is the basic code:

class HighScoreTable
{
private:
	// Location of high score file
	std::string scoreFile;

	// Save high scores
	void save();

	// Load high scores
	void load();

public:
	// Number of scores to store
	int NumScores;

	// The most recently entered name
	std::string LastName;

	// The scores
	std::multiset<Score, std::greater<Score> > Scores;

	// Add new score
	void NewScore(std::string name, int score, int level, int linesCleared);

	// Check if a score is a new high score (returns high score table position or 0)
	int HighScore(int score);

	// Constructor
	HighScoreTable() {}
	HighScoreTable(std::string path, int qty = 10, std::string name = "", int first = 10000, int step = 1000);
};

The scoreFile member will store the directory path to where high scores should be saved to disk as we discussed in the first section – the path should be passed in to the constructor. We also provide the opportunity to configure the number of scores that will be stored in the table, and if this is our first application run and the high score table doesn’t exist yet, what the first score should be and how much to subtract from it for each subsequent lower entry (in the example above, default scores will be 10000, 9000, 8000 etc. down to 1000 for a total of 10 entries).

The line which actually defines in-memory storage for the high score table is:

std::multiset<Score, std::greater<Score> > Scores;

This specifies that we want a multiset of scores using the comparison function we defined earlier in the Score struct. Why a multiset and not a normal set? This is because the high score table might contain more than one score that is the same, but a regular set only allows the storage of unique values. A multiset, on the other hand, allows multiple identical values.

Implementation of the high score table

Let us now turn to the engine that makes the high score table work. First we need a constructor that creates a high score table if none exists from a previous session:

// High score table constructor
HighScoreTable::HighScoreTable(std::string path, int qty, std::string name, int first, int step) : scoreFile(path + "scores.txt"), NumScores(qty), LastName(name)
{
	// Make a default score table if none exists
	DWORD attr = GetFileAttributes(scoreFile.c_str());
	if (attr == INVALID_FILE_ATTRIBUTES)
	{
		for (int i = 0, s = first; i < NumScores; i++, s -= step)
			Scores.insert(Score("Someone", s));

		save();
	}

	// Load high score table
	load();
}

We check to see if the score table file is stored on disk, and if not, we create and save a new one with the default settings.

Saving and loading is a breeze with Boost because it takes care of the file format and serialization for us:

// Save high score table
void HighScoreTable::save()
{
	std::ofstream ScoreFile(scoreFile, std::ios::out);
	boost::archive::text_oarchive oa(ScoreFile);

	// Last name used
	oa << LastName;

	for (auto i = Scores.begin(); i != Scores.end(); i++)
		oa << *i;

	ScoreFile.close();
}

// Load the high score table
void HighScoreTable::load()
{
	std::ifstream ScoreFile(scoreFile);
	boost::archive::text_iarchive ia(ScoreFile);

	// Last name used
	ia >> LastName;

	Scores.clear();
	Score s;

	while (!ScoreFile.eof())
	{
		ia >> s;
		Scores.insert(s);
	}

	ScoreFile.close();
}

Note we are also saving and loading the last name that was entered into the high score table. This is a convenience function so that when the player has another go and manages another high score, their name can already be entered as the default to save them re-typing it every time. This isn’t necessary, but it’s a nice touch.

Adding new scores now becomes fairly simple too:

// Add a score to the high score table
void HighScoreTable::NewScore(std::string name, int score, int level, int linesCleared)
{
	// There will now be NumScores + 1 scores in the table, automatically sorted
	Scores.insert(Score(boost::algorithm::trim_copy(name), score, level, linesCleared));

	// Remove the lowest score (which may be the one we just added, leaving the table unchanged)
	auto i = Scores.end(); i--;
	Scores.erase(i);

	// Remember last name used
	LastName = name;

	// Save the high score table
	save();
}

We use boost::algorithm::trim_copy to remove any leading and trailing spaces from the name the user has typed in, to keep things neat on the display. Notice the implementation here makes things very simple: we don’t need to actually check in advance if a newly achieved score is a high score or not. In all cases, at the end of every game, we just call NewScore, it gets added to the table, and if it happens to be the lowest score (and therefore not a high score), it gets pruned and the table is ultimately left unchanged. We therefore completely eliminate the need to check whether a score is indeed a high score or not.

Finally, we throw in another convenience function to return the position a potential new score would be on the high score table. This is used for one purpose, and that is so that when the user is prompted to enter their score, the user interface can position the text box in the appropriate row of the high score table if so desired. Its use is completely optional.

// Return position a score would go in the high score table or 0 if not
int HighScoreTable::HighScore(int score)
{
	int pos = 1;
	for (auto i = Scores.begin(); i != Scores.end() && score <= i->score; i++, pos++)
		;

	return (pos <= NumScores? pos : 0);
}

Plugging the high score table into the game

This is very game-dependent, but assuming you are using the framework presented in SimpleTetris and SimpleAsteroids, the first task is to add a new game state which I have called EnterName here:

// Possible game states
enum GameStates { Menu, Playing, GameOver, Pause, EnterName };

Next up, if we are in the game over state and the user has pressed a key to continue, we decide whether to go back to the main menu or prompt the user to enter their name:

bool SimpleTetris::OnKeyDown(int key, int rc, bool prev)
{
	// Space pressed in game over state returns to the menu
	if (gameState == GameOver && key == ' ' && !prev)
	{
		// Check if high score has been achieved
		if (highscores.HighScore(score))
			SetGameState(EnterName);
		else
			SetGameState(Menu);

		return true;
	}
...

In UpdateObjects(), we check if the user has pressed Enter after typing in their name, and it is at this point the score is actually added to the table and the game state returns to the main menu:

void SimpleTetris::UpdateObjects()
{
	// Check if user has just finished entering their name
	if (gameState == EnterName)
		if (highScoreNameBox.ReturnPressed)
		{
			// Update high score table
			highscores.NewScore(highScoreNameBox.Text, score, level, linesCleared);

			// Turn off text control
			highScoreNameBox.Off();

			// Go to game menu
			score = 0;
			SetGameState(Menu);
		}
...

Notice that highScoreNameBox is a TextControl object from my Simple2D library.

Rendering the high score table

This is actually the most complicated part of the whole endeavour. If you don’t have a library to provide you with an editable text control and 2D font rendering, you are going to be up against it. Once again I have used Simple2D here, so you will have to adapt your code significantly if using another library, but for those of you using Simple2D, here is how to do it.

First of all, when the EnterName game state begins, we create a text control for the user to enter their name:

// Game state transition
void SimpleTetris::SetGameState(GameStates state)
{
	gameState = state;

	switch (gameState) {

...
	case EnterName:
		// Make high score name entry box
		int hsPos = highscores.HighScore(score);

		highScoreNameBox = TextControl(
			140, 170 + hsPos * 15 - 15, 100, 15, 12, highscores.LastName, highScoresHeaderFormat, MakeBrush(Colour::Orange),
			MakeBrush(Colour::CornflowerBlue), 4, 4, this);
		highScoreNameBox.On();
		highScoreNameBox.GiveFocus();
		break;
	}
}

The constructor parameters to TextControl() are in the Simple2D documentation, but generally it just specifies position, height, width, colour of the text and outline box, font to use, default text and so on. Note that we use the LastName member to populate the field with the last used name for ease of use.

The main work happens in DrawScene():

...
	// Display menu
	if (gameState == Menu || gameState == EnterName)
	{
...
		// High score headers
		SetTextFormat(highScoresHeaderFormat);
		SetBrush(Colour::White);

		Text(140, 155, "Name");
		Text(260, 155, "Score");
		Text(360, 155, "Level");
		Text(440, 155, "Lines");

		// High scores
		SetTextFormat(highScoresFormat);

		int c = 0;
		int hsPos = highscores.HighScore(score);

		for (auto i = highscores.Scores.begin(); i != highscores.Scores.end() && c < highscores.NumScores; i++, c++)
		{
			// Skip over a line where a new high score is to be entered, if applicable
			if (hsPos == c + 1) { c++; hsPos = 0; if (c == highscores.NumScores) break; }

			Text(140, 170 + c * 15, i->name);
			Text(260, 170 + c * 15, StringFactory(i->score));
			Text(360, 170 + c * 15, StringFactory(i->level));
			Text(440, 170 + c * 15, StringFactory(i->linesCleared));
		}

		// High score name entry
		if (gameState == EnterName)
		{
			Text(10, highScoreNameBox.y, "Enter your name:", Colour::White, highScoresFormat);

			// Name entry box
			DrawRoundedRectangleWH(highScoreNameBox.x - 5, highScoreNameBox.y + 1,
				highScoreNameBox.w + 10, highScoreNameBox.h + 2, 5, 5, Colour::Blue);

			// New score details
			Text(260, highScoreNameBox.y, StringFactory(score), Colour::Orange, highScoresHeaderFormat);
			Text(360, highScoreNameBox.y, StringFactory(level), Colour::Orange, highScoresHeaderFormat);
			Text(440, highScoreNameBox.y, StringFactory(linesCleared), Colour::Orange, highScoresHeaderFormat);
		}
	}
}

I’ve omitted everything that isn’t relevant to high score rendering here. The first thing to note is that we render the high score table in two game states: both when the main menu is displayed, and when the name of a player who achieved a high score is being entered.

The first part of the code merely prints headers for each column in the high score table (the first 6 lines).

The for loop displays each high score on its own row. There is some awkward code at the start of this to skip over the line where a new high score is being entered, as we don’t want the existing one to overwrite the text box; rather, we want to shift all the scores down one place, as it’s important to remember the new high score hasn’t actually been saved in the table yet – that only happens after the user presses Enter.

The final section of the code just draws an outline around the text control (none is provided by default) and the details of the new score, if one is currently being entered.

And for the grand finale, here is what it all looks like:

SimpleTetris high score table

It certainly seems like a lot of work, but ultimately it only takes a few hours to implement the high score table, most of the legwork is in the actual rendering.

I hope you found this article useful. Next time we will look at how to re-define the controls in your game. Enjoy!

  1. September 22, 2012 at 21:51

    Your HighScoreTable class has quite many public members that should probably be private.
    The numerical values in the final display should probably be right aligned 🙂

    As a style note, I tend to prefer having all the public stuff on the top, because after all it’s the public interface of the class, it’s what customers should use.
    Private on top shows first the implementation details that really should not interest your clients 🙂

    • September 22, 2012 at 22:02

      The only member that should probably be private is numScores (looking at the casing I used I probably put public: in the wrong place, I will move it)? LastName and Scores are accessible so that they can be rendered by external graphics code. NewScore is public so that scores can be added, and IsHighScore is public as a convenience function.

      Alignment is a stylistic choice I suppose, this code is 3 months old, I believe I tried it right-aligned and just preferred it the other way 😛

    • September 22, 2012 at 22:04

      Correction to myself: NumScores is public so the rendering code knows how many scores to display, although I suppose it could query the size of Scores to do that.

  1. September 29, 2012 at 21:51

Share your thoughts! Note: to post source code, enclose it in [code lang=...] [/code] tags. Valid values for 'lang' are cpp, csharp, xml, javascript, php etc. To post compiler errors or other text that is best read monospaced, use 'text' as the value for lang.

This site uses Akismet to reduce spam. Learn how your comment data is processed.