3 - Score System
Score system
..° An array containing the 8 best scores
..° A layout displaying the 8 best scores
..° On game over the player is taken to the scores and his current score is compared to the scores in the array. If the score is greater than one registered score, the new score is put in the array and the former score gets put one row down/out of the array.
..° Scores are saved locally (on Local Storage) so that from a session to another, the player can improve and beat his own scores.
The Score System is the most "complex" part of the game. It is based on plugins that are generally obscure to beginners (Arrays and Local Storage).
It's in fact that the mental model on those plugins is far more obscure as you don't have a visual representation of their content like a Sprite for example, (you see the texture set on the sprite, if you double click it you can see the animation frames. Array and Local Storage don't let you do that).
They are pure data structures.
Arrays are often a difficult notion to grasp at first for beginners. There is documentation about it like the manual entry, this tutorial for beginners with arrays, the "how do I FAQ", section "Arrays".
Reading multiple times through this documentation helps, as well as practising on how to use arrays.
To quickly summarize, an array could be considered a collection of variables, stored in cells, cells you access to through its coordinates/an index.
You can set the size of an array, you can order the values in it, and use up to 3 dimensions for your coordinates (X, Y and Z).
Local Storage has similarities with arrays. It's also a data structure, but this one doesn't sit in the RAM (memory) of the computer, it writes directly datas on the hardrive.
Each value is referred to thanks to a "key", a string that helps to identify the value. You don't set the size of the Local Storage, you just go with the 5 Mo granted by HTML5 specification (10 Mo in Chrome).
Also note it's functioning is asynchronous. You set an action and then set up an event that will trigger on completion (or error) of said-action.
The Score System is spread across the three layouts of the project.
In the "Splash" layout (and more precisely in the "esSplash" event sheet) resides the code that sets the default scores (event 2 in "esSplash") and/or loads the saved scores from the Local Storage to populate the score array (event 3/6 in "esSplash").
This array used to store the scores is "arrScore". Its "Global" property being set to "Yes", it is created once on start of the "Splash" layout and "survives" through all the layouts, instead of being destroyed when passing from a layout to another (what happens to non-global objects).
In the "Score" layout, the scores are displayed and tested. (more on this later)
In the "Game" layout, the score is the score for the current game being played. It is stocked in the global variable "Score" as mentioned briefly in the Basic Mechanics.
This variable is reset on start of the layout (event 1 in "esGame").
On "GameOver", the user being sent to the layout "Score", the variable is being compared to the stored scores (event 5 in "esScore").
+ Breaking down the code
..° Putting default values in the Local Storage
"Splash" is the first layout of the project. It also acts as "loader layout" (layout that will allow to load the textures for the rest of the game and allow to make a loading bar while the loading occurs).
As it is executed first, its "On start of layout" event acts as the "initialisation" for the game.
This means that the initialisation actions (like setting the values to some variables) will happen here.
Note: I ended up implementing the loading/default for the scores in the "Splash" layout, because at first "Splash" was to also display the scores. And then, as I was implementing/making the project, thinking about the new feature "Loader layout", I decided to split things, scores would have their own layout.
But as I had already created the "arrScore" array in the "Splash" layout, and kept it global from here, it was "simpler" to keep it that way.
In your own score system, nothing would prevent you to load/set default scores from the "Score" layout itself. Just make sure not to load/set default scores each time you execute the layout.
In the case of the Score System, the game at start checks a simple thing. It makes sure a certain key exists in the Local Storage (so written on the player's hard drive).
The logic is here that the first time the game is executed, the expected key doesn't exist (event 2 in "esSplash").
So the game fills and save "default scores". The expected key will be created later down the process, so next time the player executes the game, this event won't be executed, but event 3 (in "esSplash") will as it is getting the content from the key ("AsteroidPlayerName") and from other keys as well ("AsteroidScores" and "AsteroidSound" (more on this one in the Audio system)).
Let's look more closely at the code:
Local Storage: Action Check item "AsteroidPlayerName" exists (in the event 1)
This "Local Storage" action just checks that a key "AsteroidPlayerName" exists, that is to say that in the Local Storage place in the hard drive there is a key with that name holding a value.
As explained earlier, in the logic, if the key does not exist, it means that the player is launching the game for the very first time and so it's kind of "the installation" of the game, taking advantage of this "time" of the game to store/set default scores/values.
"AsteroidPlayerName", is a key that should be unique as "Asteroid" is the name of the game and acts for me as a prefix. It's a personal convention, meaning you can name your keys however you want, but just remember it's always nice to make sure your key is unique and can only be set/used by your game. This is to prevent unexpected behavior/bugs.
This may trigger :
Local Storage : Condition On item "AsteroidPlayerName" is missing (event 2 in "esSplash")
This condition/event happens when the key checked in the "Check item exists" action does not exist.
As mentioned earlier, this is the part where we set a default score and save it on the hard drive.
System: Set layer "AskNameLayer" visible
The layer "AskNameLayer" contains the "form" that allows the player to input his name.
Since the key that is tested first is the player's name ("AsteroidPlayerName"), the player is required to input said name before saving it to the Local Storage.
This input is made thanks to the textbox "InputPlayerName" in the layer "AskNameLayer".
More on the player's name input later.
txtInstructions: Set invisible
Asteroid: Set invisible
Those two actions hide "scenery" elements for the time the player won't have inputed his name.
"Asteroid" is an instance of the "Asteroid" object made in the "Game" layout.
"txtInstructions" is a text object that will display the controls for the game on the splash layout and while the game is loading.
The textbox object "InputPlayer" is focused, meaning that the object will be "selected" and so, keyboard inputs will end up adding letters to the textbox directly.
txtFeedback: Set text "Press ""RETURN"" to validate your name" & Newline & "Make sure that your name is not empty and is not ""0"""
The text object "txtFeedback" delivers some instructions to the player.
As it is a string, the whole sentence is between quotes (""). Around the word "RETURN" you can notice double quotes. Those allow to still display the quote (") character, without C2 believing it is the end of the string.
The player has to input his name and press the "Return" key to validate. If the name is "0", the name won't be accepted (because such a name could break the Score System, more on this later).
arrScore: Set value at (0, 0) to "Scirra"
to
arrScore: Set value at (7, 2) to 1
Those actions set the default scores. These actions "manually" fill the array with default values.
The array "arrScore" is of size (8, 3). (set in the properties of "arrScore")
Here the array is considered a 2 dimensions array. It uses a couple of coordinates: X and Y.
Nevertheless its indexes/coordinates to access the values are 0 based, so X can be equal to 0, 1, 2, up to 7.
An index X = 8 would mean that the array is actually 9 cells wide. This cell is out of the boundaries of the array and will return 0 if tried to be accessed.
This picture represents the organisation and content of the array "arrScore".
Eight horizontal columns represent the eight high scores the game will store and manage. It's also the X axis of index/coordinate.
Each X coordinate is a high score. This is one dimension of the array, a serie of X cells that can hold a value (string or number).
For the need of this system, each score is composed of the player's name, the score he did, the wave he died.
Those informations are stored in the rows, in another dimension of the array: Y.
You can store/access values at the intersections of the columns and rows.
It also means that now each X "contains" a one dimension array.
You know at what indexes (coordinates) the values are stored in and you can set them thanks to the "Array: Set value at()" action and retrieve them thanks to the "Array.at()" expression which uses the same system of coordinates.
So consider the line of code, it sets the value at coordinates X = 0, Y = 0 to be "Scirra".
If you look at the table, you'll see that indeed, at the intersection of the 0 column and 0 row, the value is "Scirra".
It is the Player Name (Y = 0) of the first/highest score (X = 0).
In the Score System it has this meaning because I designed the system to work like that.
If I want to reference the Player Name of any high score, I know that I can use a X coordinate from 0 to 7 and the Y coordinate will be 0 and only 0.
There are other ways to fill an array, you can find some examples in the how do I FAQ, section "Arrays".
Local Storage: Set item "AsteroidScores" to arrScore.AsJSON
This actions saves some datas on the hard drive of the user in the Local Storage. The key is named "AsteroidScores" and its value is the content of "arrScore" as a string in JSON format.
What that means is that the entire content of the array arrScore (so the values "manually" filled in in the previous actions) is converted to a format that allows it to be held in a single string and save it in a single key in the Local Storage.
This is especially useful because of the action "Array: Load JSON" that is used in event 3, as the process of filling in default values has already been made and the game just needs to load the saved data from the user's Local Storage.
The last two actions in event 2 are for the Audio System and will be discussed later.
..° Loading values from the Local Storage
Local Storage : Condition On item "AsteroidPlayerName" exists (event 3 in "esSplash")
Compared to the previous event the key already exists and so data are loaded from the hard drive and set into the correct variables and arrays thanks to the "Get Item" actions.
So here, if a key "AsteroidPlayerName" can be found in the hard drive of the player, then event 3 will be executed, and this isn't the first time the game is executed on this computer.
System: Set layer "AskNameLayer" invisible
This action hides the "AskNameLayer" layer.
The player don't need to input his name since there's already a saved key containing his name.
This also means that there are scores to be retrieved from the Local Storage.
txtInstructions: Set visible
Asteroid: Set visible
Contrary to event 2, and since "AskNameLayer" is invisible, the "scenery" is displayed directly.
txtFeedback: Set text to "Loading in progress"
The text object used to give feedback to the player is set to "Loading in progress" in anticipation of the "loader layout" mechanism.
Local Storage: Get item "AsteroidPlayerName"
This action will get and set the global variable "PlayerName"'s value to the key "AsteroidPlayerName" (the name of the player that is written on the player's harddrive) value in the Local Storage in the event number 4.
Local Storage: Get item "AsteroidScores"
As mentioned earlier, this action will trigger event number 5 and loads and fills the array "arrScore" with the JSON string saved in the local key "AsteroidScores" in the Local Storage.
That's why, in event 2, when setting default values, those default values are saved in a Local Storage key.
This makes sure the second time the player executes the game, event 3 has some values to load from the Local Storage.
The last action is about Audio and will be discussed later in the tutorial.
..° Validating the player name
Event 7 happens when the player presses the "Return" key.
Nevertheless, "Return" here is used to validate the player's name AND go from the "Splash" layout to the "Score" layout.
Event 9 in "esSplash" is the event that validates and "saves" the player's name.
It is achieved thanks to the condition :
System: Layer "AskNameLayer" is visible
Since the inputs for the name of the player are visible, the player pressed "Return" to validate his input.
The following conditions just make sure the content of the textbox is neither empty ( "" ) or 0 (which could lead to bugs later on scores display).
Now for the actions:
System: Set PlayerName to InputPlayerName.text
The global variable "PlayerName" is set to the content of the textbox "InputPlayerName".
Local Storage: Set item "AsteroidPlayerName" to PlayerName
This creates/sets the key "AsteroidPlayerName" (do you remember, that is the key that is tested to check if the game has already been executed once on this device or not) to the value of the global variable "PlayerName".
Function: Call "DisplayScore()"
This action calls the function "DisplayScore" from the event sheet "esScore" without any parameter. (we'll discuss the display score system a bit later down)
System: Set layer "AskNameLayer" invisible
Asteroid: Set visible
txtFeedback: Set text to "Press ""RETURN"" to see the scores"
txtInstructions: Set visible
And since at this point of the code the player's name input is achieved, now is the time to hide the "AskNameLayer", display the scenery "Asteroid" and "txtInstructions" (which displays the keyboard controls to play the game) and the feedback to the user (the fact that now if the user presses "Return" he'll be taken to the "Score" layout).
Event 8 is there to take the player to the layout "Score", but this will be discussed in the finishing touches (loader layout).
..° Displaying the scores
To display the scores, I duplicated the "Splash" layout. Remember a note earlier that said that at first I had the splash and scores in the same layout. At the very beginning of the project, I had the layers "AskNameLayer" and "ScoreLayer" in the very same layout.
By duplicating the layout, renaming it, deleting the unnecessary layer in each layout, I then had two layouts, each with its own focus on a specific task.
The "Score" layout displays the stored high scores, allow the player to access the "Game" layout and checks if, back from a game played, the current score is worthy of being stored as a high score.
The display code happens in the "DisplayScore" function in "esScore" events sheet.
A function, in that case, is an event which actions will only execute once the function has been called (through the Function: Call "Function" action). Functions are called by name, can have some parameters and either run on their own or even return a value allowing them to be called as part of an expression.
In our case, all we want to do is execute the actions to display the scores when we encounter the "Function: Call "DisplayScore()"" action.
Event 9 is a loop (system: Repeat). It means that the actions and subevents will repeat themselves for the given number of times. (They will repeat during this tick, the code will "pause" for the time the loop is completed).
Here, it corresponds to "arrScore.Width" (the number of X for the array "arrScore", that is to say 8).
Event 10 works in pair with event 11 (which is a "Else" condition).
For each time the loop is iterated, either sub event 10 or 11 will be executed (because they are sub events to the loop event, they'll get tested each iteration).
Event 11 will only be executed if event 10 hasn't been executed.
arrScore: Value at (loopindex, 0) = not 0
This condition tests if the value in the array "arrScore" stored at the coordinates X = loopindex and Y = 0 is not equal to 0.
Loopindex is the current "index" (iteration value) for the loop "Repeat" (event 9).
On the first iteration, loopindex is equal to 0, then 1, then 2, etc... until it reaches the value "arrScore.width".
Here, making sure the value is not equal to 0 allows to make sure there is the expected datas stored in the array (player's name, score and wave number), and that the current iteration of the loop isn't reading out of the bounds of the array (for example X = 9). A blank cell in an array returns "0" as default value.
That's why earlier, the code prevented the player to input "0" as its name.
This events creates a "line" of text in the layout.
Actually, it spawns one instance of three different text objects : "txtNameScore", "txtBestScore" and "txtWaveBest".
Those three text objects each display one value (a Y coordinate in the array), which, as a line, displays the name, the score and the wave number for the current score (the current X coordinate in the array).
Each iteration of the loop creates one new row of text objects and fill them with the values from the array.
System: Create object txtNameScore on layer "ScoreLayer" at (0 , StartingY + Loopindex * 35 )
This action creates a new instance of the object "txtNameScore" that is made in the layer "ScoreLayer" at the coordinates X = 0 and Y = StartingY + Loopindex * 35.
"StartingY" is the local variable above event 9. It is a value that acts as reference. I want the text objects to be created at least at an Y coordinate of 432.
I used a local variable here because this value is used in three different actions, and as I was developing the project, fiddling to find the correct coordinates that suited me, it was quicker to change only the value of the local variable in one spot rather than changing it in three different actions.
Also, with each new line, the Y coordinate of the newly spawned instances needs to be bigger (more and more close to the bottom of the layout).
That's where "Loopindex 35" comes in play. The newly instance's Y coordinate is 432 to which is added 35 pixels of spacing multiplied by the number of iteration. (The first instance is spawned at Y = 432; the eighth instance is spawned at Y = 712 => 432 + 35 8).
txtNameScore: Set text to arrScore.At(loopindex,0)
Sets the text "txtNameScore" to display the name stored in the array "arrScore" for the current high score. (X = loopindex, the current iteration of the Repeat loop; Y = 0, the name).
txtNameScore: Set font face to "Arial" (normal)
This action makes sure the rows of high scores are displayed as normal (and not as bold). Indeed the first instance of the object "txtNameScore" is set to display a bold font.
When a new instance of the object is created, it keeps the same properties as its "original instance". But here, that's not what I wanted, so this action here does the job of displaying the font as not bold.
txtNameScore: Set font size to 12 pt
Very much like the previous action, this one sets the size of the text displayed by "txtNameScore" to 12 points.
The following actions are pretty much the same, but applies to the object "txtScoreBest" and "txtWaveBest".
The only thing changing are:
+ the X coordinate when the object is created (since they are to be aligned as a row)
+ the Y coordinate in "arrScore" (0 = Name, 1 = Score, 2 = Wave) accordingly to the text being filled.
All the coordinates for the text objects were found and set through trial and error until I was satisfied with them.
Event 11 executes if event 10 has not executed, so if the value at X = loopindex and Y = 0 in "arrScore" is equal to 0.
And the event does stop the loop.
The loop would end nevertheless, but this event 11 allows to make sure in case the loop tries to go out of the bounds of the array, the scores won't display a line of "0" as a high score, which would be considered a bug.
That's also why if a player would have its name as "0", and had the 4th high score for example, the scores from 5 to 8 wouldn't be displayed. That's why I put some code in the validation of the player's name input to prevent the player's name to be "0".
..° Checking if the score is worth of being stored as high score after a game
Do you remember in the basic mechanics, the "game over" event? It was event 15 in "esGame".
This event sets the global variable "Played" to 1 and takes the player to the "Score" layout.
That's why the code to check if the current score is worthy of being stored as a high score is in the "Start of layout" event in "esScore".
You can notice event 2 (which is a sub event) that testes if the global variable "Played"'s value is equal "0".
For "Played"'s value to be "0", it means that the previous layout was "Splash".
The event sets "txtFeedback"'s text, the text object that tells the player what input is awaited, to "Press ""RETURN"" to play".
When pressing "Return" the player will be taken to the layout "Game" and play a game.
When the player's arrives from the "Game" layout after having played a game to the layout "Score", "Played" value is set to 1.
And so, event 2 in "esScore" is not executed, but event 3 is.
This is this event that sets the display on screen after a game.
txtFeedback: Set text to "You scored : " & Score & " points at wave number : " & Wave & newline & "Press ""RETURN"" to play"
It is a "default value" for the text object "txtFeedback". If the score is not a new high score, this is the "default" feedback.
Event 4 executes a loop (System: For).
I haven't given a name for the loop there as I'm having only one loop at that moment and that I can use Loopindex without the need to precise which loop I'm wanting the loopindex from.
The loop goes from 0 to 7 (8 high scores).
Event 5 is the true test to check if the current score is better (higher) than registered high scores.
System: int(arrScore.at(loopindex,1)) < Score
This condition checks if, in the array "arrScore", the value stored at X = loopindex (the current iteration of the loop; 0 to 7) and Y = 1 (Score) is less than the current global variable "Score", containing the score for the game that's just been played.
Remember here int() is an expression that allows to make sure the value returned is an integer.
Int() here is required since the values of the array were loaded from the Local Storage which only contain string. As an array can contain either numbers or strings, the expression makes sure we're going for a numerical comparison.
arrScore: Value at (Loopindex, 1) NOT = 0
This condition makes sure that the value stored in the array "arrStore" at X = loopindex and Y = 1 is not 0 (a blank cell/out of bounds of the array).
If the actions of event 5 executes it means that the current score is worth of being a new high score, in place of the current score stored at loopindex (the current iteeration in the loop).
txtFeedback: Set text to "You scored : " & Score & " points at wave number : " & Wave & newline & "Congratulations this is a new high score" & newline & "Press ""RETURN"" to play"
This action modifies the text of "txtFeedback" to notify the player his score is now one of the high scores.
It's a small feedback, but still a greeting feedback.
arrScore: Insert "Score" at index Loopindex on X axis.
This action of the array "arrScore" inserts a "new column" at the X coordinate "Loopindex".
Let's take back the example of arrScore.
Now let's consider that I just did a score of 3500 with the player name "Kyatric" at wave 6.
The "insert" action would result as such in memory :
It pushes "down the line" the other values, and creates a new "column".
The following actions just fill correctly the values.
arrScore: Set value at (loopindex, 0) to "PlayerName"
arrScore: Set value at (loopindex, 1) to "Score"
arrScore: Set value at (loopindex, 2) to "Wave"
Since the player comes back from the "Game" layout, the "Score" global variable's value is the score to save, "PlayerName" is the name of the player and "Wave" is the wave number the game ended at.
Stops the event 4 loop, since the score is at its correct position.
arrScore: Set size to (8, 3, 1)
Sets the size of the array "arrScore" so that it's width (X) is only 8.
The value that was formerly in X = 7 (Pirate Princess, 100, 1) just disappears from the content of the array.
Local Storage: Set item "AsteroidScores" to arrScore.AsJSON
If you remember earlier the moment the default scores are set up, they are saved in the Local Storage in the very same way.
Since the content of the array has just changed, it's time to save it again.
It will also allow the "DisplayScore" function to display this newly set high score and the others in correct order.
Warning though, Local Storage is asynchronous. It means the the saving operation happens as the rest of the actions happens as well.
In our case, there are very few informations to save, so the data should be available later on when we want to display.
But if you were to save a lot of data and it would take a few seconds to do so, you might want to "pause" the logic of your program so that the function trying to access those data won't happen before the saving is done and the appropriate trigger ("On item set") has triggered.
And so in the end the content of "arrScore" in memory looks like :
Event 6 executes (since there's no condition) once (since it is a sub event of the "On start of layout" event that executes only once, on start of the layout).
It just "resets" the value of the global variable "Played" to 0.
And it calls the "DisplayScore()" function to display the score.
That is the Score System in this Asteroid clone.