Thursday, 3 May 2012

Persisting Hi Scores


When you switch off the battery pack on the GamePack or unplug it from your computer, the AVR chip loses power and you lose all your data stored in standard memory. This means we need to write the hi scores to an area of memory that will persist even when power is lost in order to recover them again when the game restarts. The ATMega2560 comes with 4kb EEPROM. Data stored here will remain even when power is lost.

The WinAVR development tools contains a library to read and write from EEPROM and it is pretty simple to use. See this blog post for some details about using it. The save/load functionality is encapsulated in the HiScores class in Tetris.

Here are some code snippets from this class:

You need to include the library:

 #include <avr/eeprom.h>  

Detecting if valid data has been saved:

 uint8_t data_flag = eeprom_read_byte((uint8_t*)DATA_FLAG_ADDRESS);  
   
 if(data_flag == VALID_DATA_FLAG){  
   return true;  
 }else{  
   return false;  
 }  

It is dangerous to assume that the data you want is present in the address you are looking in without some kind of check to confirm the data exists. For example if you run the game for the very first time, no high score data will exist. Who knows what data is written to the addresses you expect to read from. This can be solved by setting a single byte address to a value that indicates that valid data exists. If you read this address and the value is correct, you can assume the other addresses also contain valid data. If not, then you should not assume valid data in the other addresses. This mechanism makes resetting the hi score very easy as you don't actually have to erase the data, just set the flag to not be valid so the existing data will be ignored and overwritten (you can see this in HighScore.cpp).

Save a name and score to EEPROM:

 //indicate there is valid data to be read  
 eeprom_write_byte((uint8_t*)DATA_FLAG_ADDRESS, VALID_DATA_FLAG);  
   
 //save the player name  
 for(int i=0 ; i<player_name_length_ ; i++){  
   eeprom_write_byte((uint8_t*)(DATA_NAME_ADDRESS+i), player_name[i]);  
 }  
 //terminate the name array  
 eeprom_write_byte((uint8_t*)(DATA_NAME_ADDRESS+player_name_length_), '\0');  
   
 //save the player score  
 eeprom_write_word((uint16_t*)(DATA_SCORE_ADDRESS), score);  

An example set of data saved in Tetris is as follows:

   Address:  1    2    3   4   5   6   7
   Data:        9   B   O   B  \0   123

Where address 1 contains the valid check flag (where a value of 9 means valid data exists). This gives us a name of BOB (terminated by the null character) and a hi score of 123. The score is a stored as a 2 byte word.


Load the name and score back from EEPROM:

 //extra character to include the terminating character  
 char* player_name = new char[player_name_length_+1];  
   
 for(int i=0 ; i<player_name_length_+1 ; i++){  
   player_name[i] = eeprom_read_byte((uint8_t*)(DATA_NAME_ADDRESS+i));  
 }  
   
 int score = eeprom_read_word((uint16_t*)(DATA_SCORE_ADDRESS));  

Note: It is important to terminate the player name with a null character so no garbage gets appended on the end when you are displaying it on the screen. The graphics function text(char*, int, int) that will take the player name will stop looking for characters when it encounters the null character so without it you may get all sorts of stuff appended to the name.

The LandingScreen loads the hi score data and displays it to the player. The NameEntryScreen saves the hi score data back. Simples.

You can download version 2 of the Tetris Eclipse project here.

Check out a video of the hi score persistence in action. You can also see the game transition screens implemented from the last post:


Watch the video on youtube if you want to see the annotations. They seem to have been chopped off in this tiny format and I don't see a way to make it any bigger.

Note: There is a super secret button combo to reset the high score. On the landing screen where it says press A to continue, if you hold down the B button then press A, the screen will say 'HI SCORE RESET' and then continue. 

Game state transitions


I thought I'd tackle the top item on the Tetris to-do list; adding persistent hi scores. It's a little sad to have played a game and have no evidence of your achievements. It also encourages replay through a little competition. As I started implementing this feature I realised that the current code structure was getting a little unwieldy and could do with some refactoring first.

The current game path is controlled by the main method in Tetris.cpp. It is essentially a procedural style list of things to do in a list that loops forever:

e.g.

while(true){
   gameLoop();
   gameOver();
}

But then what happens when you want to add new states the to path? For example a landing page or name entry screen if you get a hi score. It would probably look something like this:

while(true){
   showLandingPage();
   gameLoop(); 
   gameOver();

   if(gotHiScore()){
      saveHiScore();
   }
}

This structure can get messy quite quickly if you want to add more states, especially ones are only moved to on a particular condition. To solve this I have introduced a Screen base class into Tetris with an implementation for each game state. The Screen then encapsulates the state transition logic within it.

 class Screen {  
   
   virtual void activate();  
   virtual Screen* getNextScreen();  
 }  

example Screen implementation:

 class PlayScreen : public Screen {  
   
   void activate(){  
    //code to play a game of tetris  
   }  
   
   Screen* getNextScreen(){  
    return new GameOverScreen();  
   }  
   
 }  

This reduces the main loop to:

Screen screen = new LandingScreen();

while(true){
   screen.activate();
   screen = screen.getNextScreen();
}

and it never needs to change, even when new screens are added. It also encapsulates all the code required for that state in a single class. 

Tetris screen state transitions

You can download version 2 of the Tetris Eclipse project here. This new version includes the state transitions and hi score saving/loading (as described here)