Adventures in Unity – 1.8 Obstacles

Official_unity_logo

Obstacles are static/non-moving  objects which appear on platforms with increasing frequency as the game progresses.

They are there to provide an extra challenge for the player and give a little more variety to the game.

Should a player collide with an obstacle, it’s instant death – game over – & so should be avoided at all costs.

The code itself it pretty straight forward – (Obstacles are created, they scroll left then are destroyed) – it’s a modified/simplified version of the code written to handle pickups.

 


 

For in-game obstacles I initially wanted to use a pyramid (Tetrahedron) shape – Since these are pointy and seem like the sort of thing a player should avoid. Unfortunately Unity doesn’t provide tetrahedrons as part of the basic 3D objects.

My first attempt at a workaround was to use cubes – Rotating them, and placing them halfway into the platform top – They looked okay-ish; but didn’t quite work out.

My second attempt was to use spheres – These looked much better – I nearly stuck with them – I just wasn’t convinced they looked dangerous enough.

Looking for an alternative – I found an article in Morten Nobel’s Blog detailing how to create a tetrahedron in code (rather than importing a model) – I used the code pretty much as-is – only making two very minor tweaks; 1. To make them self-construct when initialised & 2. To auto build a collision mesh.

Since I’m a big fan of procrastination, the code currently has all three obstacles types in place, setup as prefabs.

ObstacleCube – A 3D object Cube with default settings – implemented as as a prefab. A obstacle c# script is attached (used to manage collision response).

ObstacleCube

ObstacleSphere – A 3D object Sphere with default settings – implemented as a prefab. A obstacle c# script is attached (used to manage collision response).

ObstacleSphere

ObstacleTetrahedron – A custom object. Defined as a prefab, it contains the same components as ObstacleCube and ObstacleSphere – Except that the MeshFilter is empty. In addition it includes two extra C# scripts; obstacle – used to manage collision response & TetraHedron – Used to construct the object at runtime.

ObstacleTetra

 


 

ObstacleManager;

ObstacleManager is a C# script which manages all obstacles in game, covering their entire lifespan (initalisation, update & removal).

The public method AddObstacles is used to instantiate new obstacles (called externally by the PlatformManager script).

Once created they are then add to a list (List<GameObject> obstacles). Each frame the positions of all obstacles are updated.

Once an obstacle moves offscreen it is destroyed by ObstacleManager  to free up space.

 


 

AddObstacles;

public void AddObstacles(Vector3 inPosition, float inWidth)
{

     Vector3 obstaclePosition = new Vector3(inPosition.x, inPosition.y, inPosition.z-4.5f);


     float obstacleHalfWidth = obstacle.transform.localScale.x * 0.6f;
     float halfWidth = (inWidth / 2f)-obstacleHalfWidth;
     obstaclePosition.x = Random.Range(obstaclePosition.x - halfWidth, obstaclePosition.x + halfWidth);

     for (int counter=0; counter<9; counter++)
     {
          obstacles.Add(Instantiate(obstacle, obstaclePosition, Quaternion.identity) as GameObject);
          obstacles[obstacles.Count - 1].transform.position = obstaclePosition;
          obstaclePosition.z += 1;
     }

}

AddObstacles is called by the platformManager script. When called it creates a column of obstacles – Even though, only the centre obstacle is need (since the player doesn’t move on the Y axis) – a row is more visually appealing.

It receives two parameter values inPosition & inWidth. inPosition is the source/base position for obstacles it is about to instantiate. This can be used to add new obstacles to the middle of the platform. This is fine, looks good and works as an extra challenge in-game. However to keep things interesting I wanted to position the obstacles randomly along the platforms X axis. For this I use the inWidth variable.

 


 

Update;

Every loop the position of instantiated obstacles is updated, moving them from right to left; destroying any which move out of the game area;

// Update is called once per frame
void Update()
{

     //---

     if (GameController.gameState == GameController.GameState.InGame)
     {

          //---

          ScrollSpeed(); //set movement speed
          UpdatePositions(); //update platforms positions

          //---

     }

//---

}

//---
//---

//set movement speed
void ScrollSpeed()
{

     speed = 0.1f + (GameController.scrollSpeed * 0.004f);

     if (speed > maxSpeed)
          speed = maxSpeed;

}

 


 

Obstacle;

All obstacles have an attached script -all called Obstacle.

It’s is a simple script containing two methods;

Start – Which sets the obstacles colour after initialisation.

OnCollisionEnter – Checks to see if the player has collided with the obstacles – If a collision has occurred – PlayerDie() method is called.

 


 

Start;

Start defines the colour of the obstacle (currently they are all set to black);

void Start()
{

     //---

     Renderer rend = GetComponent(); //Ensure the obstacle's colour is set to black
     Color whichColour = Color.black;
     rend.material.SetColor("_Color", whichColour);
     rend.material.SetColor("_SpecColor", whichColour);
     rend.material.SetColor("_EmissionColor", whichColour);
     rend.material.SetColor("_ReflectColor", whichColour);

     //---

}

 


 

OnCollisionEnter;

OnCollisionEnter checks for collision with the player – Since obstacles are deadly, if a collision is found, I call the player.PlayDie method;

void OnCollisionEnter(Collision collision)
{

     if (collision.gameObject.CompareTag("Player")) //Ensure we are checking aginst the player.
     {

          Player player = collision.gameObject.GetComponent(); //Grab the player
          player.PlayDie();

     }

}

 


 

That’s pretty much it. I may expand obstacles a little in the future – But for now it gives me a framework to work from.

 


 

Play this build (WebGL)

 


 

Grab a copy of the project here

 


 

 


 

Next post: 1.9 Tighten & Tidy

Last post: 1.7d Highscores (Display)

Contents page.

 


 

Adventures in Unity – 1.7e Highscores (Add)

Official_unity_logo

 

Last part for highscores – A method to allow players who have achieved a new high score to enter their names. This probably should have been a simple screen containing two elements an input field and a enter button. However, for whatever reason I decided to make things a little more complex – Adding a virtual keyboard.

The front end for the virtual keyboard is based on the example provided in the Unity Samples: UI package – The back end I put together myself – With the bulk of the code for this section setup for managing user input. It can currently be controlled either by mouse or keyboard – Expanding to allow controller input ‘should’ be fairly straight forward.

The player doesn’t need to use the virtual keyboard to enter their name – They can always click on the input field and type their names directly – So there’s also a little code to ensure that only valid characters are accepted and that the maximum length is not exceeded should this approach be used.

To try and explain how I’ve put this together, I’ve split the description into two sections;

  1. Front End: GUI.
  2. Back End: Code

 


 

1. Front End: GUI

HighscoreAdd

HighscoreAdd – an empty component which groups together all the new highscore UI elements.

HighScoreTextTop

HighscoreAddTitle

A text component, used as the title of the screen

Window – Contains the input field and keyboard keys – The bulk of new highscore components.

Inputfield

HighscoreAddInputField

Displays the players name. Contents can be filled either by using the keyboard keys or by direct input. I set the character limit to 15 so that highscore names fit nicely into the highscore table. Although I only want to allow the player to enter alphanumeric I have the input type set as standard – Managing the input in code- the method InputFieldUpdate is called ‘on value changed‘ which checks the character types, only accepting valid characters.’One Edit End‘ calls a second script method ClickDone – This allows the player to press enter after tying in their name to add the new high score – rather than forcing them to click the done button.

Vertical Group – Is the start of the on-screen keyboard. It’s a component that itself just contains a Vertical Layout Group (script) and has as a child the Grid Component.

Grid – Another component setup primarily to manage the shape of the keyboard, is has two layout components attached Grid Layout Group (script) and LayoutElement (script). As children it has 40 buttons – one for each key.

GridCell-X – Forty of these, one for each key. They are setup in pretty much exactly the same way as all the other buttons in the game. An Animator shrinks the button slightly when clicked, and audioSource & event trigger produce a click sound when the key is selected and a relevant method is called On Click  – ClickAlphanumeric method is called for the alphanumeric keys, with individual methods for the space (clickSpace), delete (ClickDelete), caps (ClickCaps) and enter (ClickDone) keys.

 


 

2. Back End: Code

The backbone of the keyboard is a script called HighscoreAdd. This is primarily a functional script, primarily containing methods for handling key presses and ensuring only acceptable (Alphanumeric) values are used for the highscore name.

If the player clicks on an alphanumeric keyboard key, it calls the ClickAlphanumeric method.

public void ClickAlphaNumeric(string inValue)
{

     if (inputField.text.Length < inputField.characterLimit)
          inputField.text += inValue;

     UpdateIndex(inValue);

}

This method first checks to see if the input has reached maximum allowed characters as set in the input field, if there’s space, it adds the new character. Before calling UpdateIndex – a method which changes the colour of the button selected – To make the keyboard feel a little more interactive.

public void ClickSpace(Text inValue)
{

     if (inputField.text.Length < inputField.characterLimit)
          inputField.text += " ";

     UpdateIndex(inValue.text);

}

ClickDelete removes the last character entered by assigning the players name text with a substring of itself, starting from position 0 and ending at length-1;

public void ClickDelete(Text inValue)
{

     if (inputField.text.Length > 0)
          inputField.text = inputField.text.Substring(0, inputField.text.Length - 1);

     UpdateIndex(inValue.text);

}

ClickCaps switches characters between upper and lower case. When the player adds a letter by clicking a button, it adds the value of the buttons text/label to the players name. So when switching between upper and lower case, this method loops through each alphabetic button in turn and sets its label to an upper or lower case version of itself. This has the added effect of visually displaying the current case. I also change the colour of the caps button to indicate it’s state – currently red for uppercase and blue for lower case.

public void ClickCaps(Text inValue)
{

     useCaps = !useCaps;

     UpdateIndex(inValue.text);

     if (useCaps)
     {
          lastColor = Color.red;

          for (int counter = 0; counter < buttons.Length; counter++)
               buttons[counter].GetComponentInChildren().text = buttons[counter].GetComponentInChildren().text.ToUpper();
     }

     else
     {
          lastColor = Color.blue;

          for (int counter = 0; counter < buttons.Length; counter++)
               buttons[counter].GetComponentInChildren().text = buttons[counter].GetComponentInChildren().text.ToLower();
     }

}

ClickDone is run when either the DONE button is clicked or the player presses the return key while the input field is focused. This function first checks to see if the player has added a name – If there is no value, or only spaces have been added the generic player name ‘A Nony Moose’ is used instead. Once a player name is set, it passes it to the SetHighScore method in the gameController script to be added to the list of highscores.

public void ClickDone(Text inValue)
{

     //---

     GameObject gameControllerObject = GameObject.FindGameObjectWithTag("GameController");

     if (gameControllerObject != null)
     {
          GameController gameController = gameControllerObject.GetComponent();

          if (inputField.text == "" || OnlySpaces(inputField.text))
          inputField.text = "A Nony Moose";

          gameController.SetHighScore(inputField.text);
     }

     //---

}

 


 

UpdateIndex method is called by all Button functions to ensure the game knows which was the last button pressed, and to update the button highlight appropriately (By calling UpdateButtonIndex);

void UpdateIndex(string inValue)
{

     for (int counter = 0; counter < buttons.Length; counter++)
          if (inValue.ToUpper() == buttons[counter].GetComponentInChildren().text.ToUpper())
          {
               index = counter;
               UpdateButtonIndex();
               break;
          }

}

UpdateButtonIndex stores the index value of the last button clicked. It also stores the last colour of the button (its unpressed colour), before changing it’s current colour to show its selection (currently grey) – The reason the last colour is stored, despite the majority of keys being white is because the caps and done keys have their own colours – We need to ensure they are correctly returned to these colours once these keys are correctly deselected;

void UpdateButtonIndex()
{

     if (index < 0)             
          index = 0;         

     if (index >= buttons.Length)
          index = buttons.Length - 1;


     //if (index != lastIndex)
     {

          buttons[lastIndex].image.color = lastColor;
          lastColor = buttons[index].image.color;
          buttons[index].image.color = Color.grey;
          lastIndex = index;
     }

}

InputCheck, called from the update method, runs every loop. It allows the keyboard to be controller via the arrow and enter keys. Arrows to move around the keyboard – Enter to select the key and add the character to the players name. This input method is only valid if the input field isn’t currently selected – This is because the input field runs the ClickDone method if the return key is pressed;

bool lastFocused = false;
public void InputCheck(bool checkReturn)
{

     if (inputField.isFocused)
     {
     }

     else if (Input.GetKeyDown(KeyCode.LeftArrow))
     {

          if (index >= 0 && index % columnLength > 0)
          {
               index--;
               UpdateButtonIndex();
          }
     }

     else if (Input.GetKeyDown(KeyCode.RightArrow))
     {

          if (index == -1 || (index + 1) % columnLength > 0)
          {
               index++;
               UpdateButtonIndex();
          }
     }

     else if (Input.GetKeyDown(KeyCode.UpArrow))
     {
          if (index >= columnLength)
          {
               index -= columnLength;
               UpdateButtonIndex();
          }
     }

     else if (Input.GetKeyDown(KeyCode.DownArrow))
     {
          if (index != -1 && index + 1 <= buttons.Length - columnLength)
          {
               index += columnLength;
               UpdateButtonIndex();
          }
     }

     else if ((Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.Space)))
     {

          if (index < 0)
               index = 0;

          if (index >= buttons.Length)
               index = buttons.Length - 1;

          lastIndex = index;

          if (!checkReturn)
          {
               var pointer = new PointerEventData(EventSystem.current);
               ExecuteEvents.Execute(buttons[index].gameObject, pointer, ExecuteEvents.submitHandler);
               buttons[index].GetComponentInChildren().Play();
               }

     }

}

Finally; the InputFieldMethod, run every time a character is added by the player directly into the inputfield (by clicking on the inputfield component and typing using the computers keyboard, rather than the virtual keyboard). It checks to see if the last character added character is valid (alphanumeric or a space) – If it isn’t, it deletes the character, if it is it returns without making any changes;

    
public void InputFieldUpdate()
{

     if (inputField.text.Length > 0)
     {
     
          char chr = inputField.text[inputField.text.Length - 1];
     
          if ((chr < 'a' || chr > 'z') && (chr < 'A' || chr > 'Z') && (chr < '0' || chr > '9') && chr != ' ')
          {
               inputField.text = inputField.text.Substring(0, inputField.text.Length - 1);
          }

     }
        
}

 


 

Play this build (WebGL)

 


 

Grab a copy of the project here

 


 

 


 

Next post: 1.8 Obstacles

Last post: 1.7d Highscores (Display)

Contents page.

 


 

Adventures in Unity – 1.7d Highscores (Display)

Official_unity_logo

 

The backbone of the highscore system is in place – But theres still no method for the program to display highscores.

Neither of the two sources I’d used to far were able to help me here – The unify community code was more involved with the back-end of things & the YouTube guide [Unity Tutorial] Online Highscores 01 (dreamlo) only demonstrated a very basic method for displaying the top ten scores.

I’d intended to show the top fifty scores; which was clearly going to be too many to display all at the same time on a single screen. I figured the game needed the ability to list the highscores, and let the player scroll through them.

I thought this would be pretty easy to implement – unfortunately there’s currently not a lot of elements in Unity’s UI for handling this kind of thing – At least, not without putting in a lot of effort first.

Ultimately I was able to put something together, which I think does the job pretty well, but still isn’t quite as flexible as I’d liked.


 

Highscore display is made up of three main elements;

1. HighScorePanel prefab

HighscorePanel

This is a panel with three text components attached (position, Name, Score). Used to display the details of one highscore entry.

2. ScrollView component

HighscoreDisplay

This contains a list HighScorePanel objects & is used to display the top 50 scores.

3. HighScoreDisplay script

The meat of the highscore display process – This code requests highscore data and uses it to fill the scrollview component with the current data to display.

 


 

 

The HighScoreDisplay script,  run at startup, calls the function refreshHighScores, which every 30 seconds, (by way of the HighscoreController script), requests an updated list of highscore data;

    
IEnumerator refreshHighScores()
{
     while (true)
     {
          highScoreController.DownloadHighscores();
          yield return new WaitForSeconds(30);
     }
}

 

Once the HighscoreController refreshes the highscore list, it passes control back to the HighScoreDisplay script – By calling the OnHighScoresDownloaded function along with a list of all current highscores. OnHighScoresDownloaded uses the highscore data to populate the ScrollView with HighscorePanel prefabs, each of which containing one highscore entry;

 

public void OnHighScoresDownloaded(HighScore[] highScoreList)
{

     ClearList();

     for (int counter = 0; counter < highScoreList.Length; counter++)
     {

          //---

          GameObject go = Instantiate(hsPanel) as GameObject;
          go.transform.SetParent(hsContent.transform);
          go.transform.localScale = go.transform.localScale * canvas.scaleFactor;
          Text[] children = go.GetComponentsInChildren();
          children[0].text = (counter + 1).ToString();
          children[1].text = highScoreList[counter].userName;
          children[2].text = highScoreList[counter].score.ToString();

          //---

          if (counter == HighscoreController.lastHighScore)
          {

               string currentColour = "ff0000";

               children[0].text = "<color=#" + currentColour + ">" + children[0].text + "";
               children[1].text = "<color=#" + currentColour + ">" + children[1].text + "";
               children[2].text = "<color=#" + currentColour + ">" + children[2].text + "";

          }

     }

}

 

OnHighScoresDownloaded loops through each highscore entry, & for each it creates instance of a HighScorePanel prefab; adding the name, score and position in highscore to the prefabs attached Text components.

for (int counter = 0; counter < highScoreList.Length; counter++)
{

     //---

     GameObject go = Instantiate(hsPanel) as GameObject;
     go.transform.SetParent(hsContent.transform);
     go.transform.localScale = go.transform.localScale * canvas.scaleFactor;
     Text[] children = go.GetComponentsInChildren();
     children[0].text = (counter + 1).ToString();
     children[1].text = highScoreList[counter].userName;
     children[2].text = highScoreList[counter].score.ToString();

     //---
}

Setting the prefabs parent to the content component of the HighScorePanel component.
This attaches it to the scroll object allowing it to be displayed,

     go.transform.SetParent(hsContent.transform);

Also scaling the text, so that it will look approximately the same no matter what screen res is used;

     go.transform.localScale = go.transform.localScale * canvas.scaleFactor;

 

& Finally, If a new highscore has been achieved, after the new high score entry added the game displays the highscore table, highlighting the new entry by changing its colour;

 

if (counter == HighscoreController.lastHighScore)
{

     string currentColour = "ff0000";

     children[0].text = "<color=#" + currentColour + ">" + children[0].text + "";
     children[1].text = "<color=#" + currentColour + ">" + children[1].text + "";
     children[2].text = "<color=#" + currentColour + ">" + children[2].text + "";
}

 


 

Now I have a way display highscores list – But no method to allow players to add new highscores to the table.

 


 

Play this build (WebGL)

 


 

Grab a copy of the project here

 


 

 


 

Next post: 1.7e Highscores (Add)

Last post: Adventures in Unity – 1.7c Highscores (In-Game Backend)

Contents page.

 


 

Adventures in Unity – 1.7c Highscores (In-Game Backend)

Official_unity_logo

Once the server side code was in place, I needed methods to download the current high score list and upload new entries in-game.

The basic code for connecting to the server and retrieving scores was provided by the unify community ‘Server Side Highscores’ article.

I added the method Md5Sum (also provided by the unify community) to encode the data before sending to the server.

Expanding on this code using the  YouTube tutorial ‘[Unity Tutorial] Online Highscores 01 (dreamlo)‘ – Using IEnumerator when adding a new highscore or downloading the current highscore list provides a smoother in game experience for the player, since it allows the primary thread to manage the game, while the secondary threads manage the highscore table status.

 


 

The modified code was placed in a c# script called HighScoreController – Which I attached to an empty component also called HighScoreController.

Functionality within the HighScoreController script can be split into two fairly clear sections;

  1. Add New High Score.
  2. Download (format and store) current highscores list.

 


 

1. Add New High Score;

Adapting the method to add new highscores – Using IEnumerator to allow the code to run on a separate thread to the main game;

    
public static void AddNewHighScore(string userName, int score)
{
     instance.StartCoroutine(instance.UploadNewHighscore(userName, score));
}

// remember to use StartCoroutine when calling this function!
IEnumerator UploadNewHighscore(string name, int score)
{
     //This connects to a server side php script that will add the name and score to a MySQL DB.
     // Supply it with a string representing the players name and the players score.
     string hash = Md5Sum(name + score + secretKey);

     string post_url = addScoreURL + "name=" + WWW.EscapeURL(name) + "&score=" + score + "&hash=" + hash;

     // Post the URL to the site and create a download object to get the result.
     WWW hs_post = new WWW(post_url);
     yield return hs_post; // Wait until the download is done

     if (hs_post.error != null)
     {
          print("There was an error posting the high score: " + hs_post.error);
     }

     else
     {
          DownloadHighscores();
     }

}

With an associated method which encrypts highscores for sending (also provided by the Unify Community site);

    
public string Md5Sum(string strToEncrypt)
{

     System.Text.UTF8Encoding ue = new System.Text.UTF8Encoding();
     byte[] bytes = ue.GetBytes(strToEncrypt);

     // encrypt bytes
     System.Security.Cryptography.MD5CryptoServiceProvider md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
     byte[] hashBytes = md5.ComputeHash(bytes);

     // Convert the encrypted bytes back to a string (base 16)
     string hashString = "";

     for (int i = 0; i < hashBytes.Length; i++)
     {
          hashString += System.Convert.ToString(hashBytes[i], 16).PadLeft(2, '0');
     }

     return hashString.PadLeft(32, '0');

}

 


 

2. Download (format and store) current highscores list;

Expanding on the Unify Community method to download highscores as a string from the server. As with the AddNewHighScore method, DownloadHighScores is also modified from the Unify Community code using IEnuerator to allow threaded functionality;

public void DownloadHighscores()
{
     StartCoroutine("DownloadHighscoresFromDatabase");
}

// Get the scores from the MySQL DB to display in a Text.
// remember to use StartCoroutine when calling this function!
IEnumerator DownloadHighscoresFromDatabase()
{

     WWW hs_get = new WWW(highscoreURL);
     yield return hs_get;

     int counter = 0;
     for (; counter < hs_get.text.Length; counter++)
          if (hs_get.text[counter] == '#')
               break;

     string hs_got = hs_get.text.Substring(0, counter); //Substring(0, counter);

     if (hs_get.error != null)
     {
          print("There was an error getting the high score: " + hs_get.error);
     }
     else
     {
          FormatHighScores(hs_got);
          highScoreDisplay.OnHighScoresDownloaded(highScoresList);
     }

}

 


 

Note: The highscore data is returned by 000webhost as a text string – When returning the data, 000webhost  appends a little extra/unwanted info

<!-- Hosting24 Analytics Code -->
http://stats.hosting24.com/count.php
<!-- End Of Analytics Code -->

This code is used by 000webhost to check site usage – Which is fair enough, since they provide the hosting for free – But is unneeded and unwanted by the game – I definitely dont want it getting mixed up with the highscore table.

My quick and dirty fix to this;

  
int counter = 0;

for (; counter < hs_get.text.Length; counter++)
     if (hs_get.text[counter] == '#')
          break;

string hs_got = hs_get.text.Substring(0, counter); //Substring(0, counter);

On the server, after creating the highscore list, I append a ‘#’ to the end of the string. A character I know isn’t used either in the highscores (it cant be entered when adding a highscore) or in 000webhost’s appended text. I loop through the highscore text, searching for the position of the hash character – Once found, the code removes it and any text which follows.

 


 

After DownloadHighscoresFromDatabase has retrieved the highscore text; it calls FormatHighScores which parses the names and scores from the text and stores them for use in game;

void FormatHighScores(string TextStream)
{
     string[] entries = TextStream.Split(new char[] { '\n' }, System.StringSplitOptions.RemoveEmptyEntries);
     highScoresList = new HighScore[entries.Length];

     for (int counter = 0; counter < entries.Length; counter++)
     {

          string[] entryInfo = entries[counter].Split(new char[] { '|' });

          if (entryInfo.Length == 2)
          {

               //---

               string userName = entryInfo[0];
               int score = int.Parse(entryInfo[1]);
               highScoresList[counter] = new HighScore(userName, score);

               //---

          }

     }

     //---

}

FormatHighScores populates a list of HighScore struct. This is used by the HighscoreDisplay script to populate the highscore table – And by the HighScoreAdd script to check if the new score should be counted as a new entry;

public struct HighScore
{
     public string userName;
     public int score;

     public HighScore(string inUserName, int inScore)
     {
          userName = inUserName;
          score = inScore;
     }
}

 


 

This provides a backbone in-game to manage highscores – But I still need methods to display current high score & add new highscores.

 


 

Play this build (WebGL)

 


 

Grab a copy of the project here

 


 

 


 

Next post: 1.7d Highscores (Display)

Last post: 1.7b Highscores (In-Game Overview)

Contents page.