Tutorial: How to collect and store Highscores

1.0

Par makerSquirrel, il y a 5 ans

This is meant for beginners and for those who do not want to re-invent the wheel.

  • Do you work on your first game, but do not know how to store the Highscore values to the SD card?
  • Do you work on porting a gamebuino classic game and found a much-copied piece of code written by R0d0t that handles the Highscore thingy?
  • Is your name Drakker, who created an awesome game with a dummy for the Highscores but not the real thing?(https://gamebuino.com/community/topic/family-jewels)

Or even if you simply do not want to bother with minor details like that, but go on with your break through in world history, then I have good news for you: with just a few minutes you will be able to load and store Highscores for your game!


If you are a "eeew, even just a few minutes is too much" kind of person, then skip the tutorial, go to the github page linked above and copy the highscore.h file from the project (what? you don't even want to search the repository? Fine, here the link to the highscore file: https://github.com/makerSquirrel/makerbuino-firebuino/blob/master/Firebuino/highscore.h). In Firebuino.ino you can simply copy&paste the relevant code lines. The save format is explained in Sorunomes Tutorial over here: https://gamebuino.com/creations/gamebuino-save-format


If you are not, come on here:

If you go to the page:  you will find a ready-to-copy file for the HighScore class, simply put that file in the same directory of your .ino file and add an #include "highscore.h" there. Before I explain how you can connect that class to your game, I will have a look at the class itself (attentive coders will find some lines resembling R0d0ts GB Classic Highscore code, which was the starting point for this class...):

template <typename HighscoreType, uint8_t nHighScoreEntries, uint8_t gamerNameLength, const char * const highScoreName> class HighScore
{
[...]
public:
  HighScore(Color textColor = WHITE, Color bgColor = BLACK) : m_minHighScore(0),
  m_textColor(textColor), m_bgColor(bgColor), m_nameIsSet(strlen(highScoreName) > 2 ? true : false) {}

void setTextColor(const Color& newCol) { m_textColor = newCol; } void setBgColor(const Color& newCol) { m_bgColor = newCol; }

/// returns true if given value is a new highscore: bool showScore(HighscoreType score);

/// simple check, returns true if new highscore inline bool checkHighScore(HighscoreType score);

// writes highscore to SD Card, should not be called more often than needed (i.e. only if showScore returns true) void updateHighscore(HighscoreType newHighScore);

// prints highscores to screen, but only valid entries if loadHighscores() was called before at least once. void drawHighScores(); };

This is not the full class, I simply removed all the non-relevant stuff.

So, let's start with explaining the first line:

template <typename HighscoreType, uint8_t nHighScoreEntries, uint8_t gamerNameLength, const char * const highScoreName> class HighScore

This long and complicated looking thing is the template for the HighScore class. Templates are a way to make code more versatile (but unfortunately not more readable) and in contrast to the thing called inheritance, it does all its magic already when compiling the code, not when running the game. This is essential for us here, since we need to know how big your Highscore class is, before the game is started (relevant for the save format thingy). (Funny sidefact: because of the compile time magic done with templates, the template idea in C++ forms itself a Turing-complete language within the C++ language, which means that you can write code, which is basically executed while compiling and only the results are stored for run-timte. As I said, hilariously funny. Really.)

The code line above shows the declaration of the class. Here is one possible way to define it and initialize it:

extern const char g_highScoreName[] = "Fancy";

HighScore<uint32_t, 6, 13, g_highScoreName> g_highScore;

  • So the HighScoreType can be an unsigned int, like here an uint32_t or whatever you need (as long as it has the necessary operators needed for comparison and stuff), so uint16_t or float is okay as well.
  • The nHighScoreEntries thingy simply tells the class how many entries we want to have. Since the screen resolution is a limiting factor, using much more than 6 would lead to not all entries shown.
  • The gamerNameLength is just the maximum number of characters allowed to be written there, so 13 allows to write makerSquirrel, but not awesomeSquirrel. This is again limited mostly by the screen resolution.
  • At last you can give the Highscore an additional highScoreName, which you need to pass as last parameter: g_highScoreName This is mostly relevant if you have several game modes and want to be able to distinguish between them.

If you copy the lines above you already have a ready-to-use highscore class, but it will not store the data yet. For that please continue reading. And if you do not fully get the thing yet, don't worry, further down I will then show how to use it and there is still the Firebuino repository linked above, where you can see that class in action. Before that, let's discuss the member functions and how to use them:

void setTextColor(const Color& newCol) { m_textColor = newCol; }
void setBgColor(const Color& newCol) { m_bgColor = newCol; }

Well, these two functions allow to set the text and background color, simply call them in the setup() function...

/// returns true if given value is a new highscore:
bool showScore(HighscoreType score)
;

This can be used after the game is over and you simply want to show the score achieved. It additionally checks if the given score is a high-score and returns true then. And yes you totally got it right: the Highscore class does not handle the score itself, you simply count the score however you want to do that, the HighScore class then takes care of putting it into a list and into a save-able format (you really got that, didn't you? ô.Ô ). And don't forget: HighscoreType is just a placeholder for your type of Highscore; In the definition above this would be replaced during compilation by uint32_t .

/// simple check, returns true if new highscore
inline bool checkHighScore(HighscoreType score);

Simply checks if the lowest entry of the Highscore list is lower than given score. returns true if it is.

// writes highscore to SD Card, should not be called more often than needed (i.e. only if showScore returns true)
void updateHighscore(HighscoreType newHighScore);

The comment already explains the most of the relevant parts. So simply call checkHighScore with your score and if that returns true , call the updateHighscore thingy. This will open the keyboard so that the player can finally enter your hall of fame! One erratum here: the function does not store it to the SD Card, for that you have to call one more line (see example near the end of the tutorial).

// prints highscores to screen, but only valid entries if loadHighscores() was called before at least once.
void drawHighScores();

Again, the comment says the relevant part. this you can call after storing the value and/or via menu entry. Your game, your choice.

The only thing left to say is how to connect it to Sorunomes safe format thing. For that we simply have a look at the following code pieces:

extern const char g_highScoreName[] = "Fancy";
HighScore<uint32_t, 6, 13, g_highScoreName> g_highScore;
#define SAVE_VERSION 0
#define SAVE_HIGHSCORE 1

const SaveDefault savefileDefaults[] = {
  { SAVE_VERSION , SAVETYPE_INT, 1, 0 },
  { SAVE_HIGHSCORE, SAVETYPE_BLOB, sizeof(g_highScore), 0 }
};

The upper part shall be added somewhere before the function definitions in your ino file (and after the includes). We use the first entry in the save file defaults as a version number, which will allow us changing the defaults when we want to store other stuff as well (more details about that should be discussed in the save format tutorial).

void setup() {
  gb.begin();
  [...]
  gb.save.config(savefileDefaults);
  gb.save.get(SAVE_NEW, g_newHighScore);
  g_newHighScore.setTextColor(YELLOW);
  g_newHighScore.setBgColor(DARKBLUE);
  [...]
}

In the setup function we need to register the savefileDefaults and then can load our high scores. Since stuff loaded the first time ever is 0-initialized, setTextColorand setBgColoris a good Idea here (an init function in case of unset standards could be added to the HighScore class, but I am too lazy for that atm, sorry). as soon as the save format code finds an existing and compatible save file this will load the actual highscores.

Then comes your game magic part, where the player will have hours, days and years of fun until he/she finally reaches a game over. What could be done then will be shown in an example function below:

//Draw GAME OVER screen
void drawGameOver() {
  bool newHighScore = g_highScore.checkHighScore(score);
    g_highScore.showScore(score);
    if (newHighScore)  {
      g_highScore.updateHighscore(score);
      gb.save.set( SAVE_HIGHSCORE, g_highScore);
    }
    g_classicHighScore.drawHighScores();
  }

Well, that's basically it. Future improvements of that could add the SAVE_HIGHSCORE define as a template parameter to the class, so that the class can store itself instead of requesting the coder to call gb.save.set(...).

In the firebuino example linked via the github link one also can see how to use several highscores at once.


So have fun and post your games here if you have the Highscore class in use as well ;)