Unity3D – global Highscore for Android games without external tools
Sebastian Pohl - 19. April 2016We recently released a little game into the wild: Just Jumping on the Google Playstore.
Beside creating an easy to pick-up game with some sort of addictive gameplay we wanted to create a bit of a competition by collecting global highscores.
There are many options on how to achieve this, most of them include external tools or libraries. And we wanted to avoid that. DIY it was…
What you need to collect global highscores:
- A transfer method to get data from the device to where the scores a stored.
- A place where you can collect the highscores. Preferably a web server with a database.
- A way to identify players.
- Methods to make cheating hard or impossible.
Let’s start on what we need on the device: How do we store the scores, how do we prevent players from cheating (or make it harder to cheat) and how can we get data from the device to a server or something similar.
Storing the highscores locally
On the device you can use the PlayerPrefs. It is as easy as using
PlayerPrefs.SetInt("yourgame_score", 5);
to save an integer to the device. But this will save an easy to identify number on your phone or tablet. If someone was out to cheat, they could find a way to alter this value. So how do we prevent it?
We decided to use a technique used on the web for years: Hashing and salting. If you read the linked articles it may sound intimidating. But it comes down to creating a combination of numbers and characters that can only be created by using a certain input. And there is no (well ther are a few ways, but lets pretend no one will try) way to take such a string and calculate what the input was.
So what we could do is save not only the score but also a hash value to identify if the score saved is the one we intended to save.
PlayerPrefs.SetInt("yourgame_score", 5); PlayerPrefs.SetString("yourgame_hash", hashFunction(5));
But this leaves us with another vulnerability: If we take only the score to create a hash, it is very easy to guess how we created our hash. So we need to ’salt‘ it. This means adding a component that the player can not know to create a hash. Something like this.
int score = 5; PlayerPrefs.SetInt("yourgame_score", score); string saltedScore = score.ToString() + " dont cheat"; PlayerPrefs.SetString("yourgame_hash", hashFunction(saltedScore));
This will make it hard for the player to guess what is going on. And we can do something like this:
int score = PlayerPrefs.GetInt("yourgame_score"); string scoreHash = PlayerPrefs.GetString("yourgame_hash"); if ( scoreHash != hashFunction(score.ToString() + " dont cheat") ) { PlayerPrefs.SetInt("yourgame_score", 0); }
It simply resets the highscore if the hash is not matching the saved score.
So we have a way to store and retrieve scores and a reasonable protection against cheating. The next step is to get this data from the device to the server that keeps the scores.
Store the scores on a server
The first thing we need, if we want to build a global highscore list is method to identify each player so they can point at their score and brag about it.
You can make this as complicated as you want, we decided to use SystemInfo.deviceUniqueIdentifier from the standard libraries. It provides a unique id to each device.
At this point, we could just send the device id, the score and a hash to the server and call it a day. But we wanted to avoid sending personal information. So the unique device id will never be sent! How do we identify players? Again, by hashing. Instead of sending the value, we just send a hash. In theory the hash is as unique as the device id and there is no way to calculate the device id from the hash.
There is a small problem here we need to consider. By sending hashes to a server, we need the server to be able to create hashes in the exact same way so we can check if the hash we send matches the value we want to protect. That is why we used the md5 function from the Unify wiki (It creates the same hashes as the md5 function in PHP).
If we have the values we want to transport, a destination where to send them we only need a way to get them out. This is done by just calling a PHP script on the server. Something like the following code will send the scores:
int score = PlayerPrefs.GetInt("yourgame_score"); string scoreHash = PlayerPrefs.GetString("yourgame_hash"); string deviceHash = hashFunction (SystemInfo.deviceUniqueIdentifier.ToString()); string securityHash = hashFunction ( scoreHash + " anti cheat salt " + deviceHash ); WWW webRequest = new WWW("http://www.domain.tld/receiver.php?score=" + score + "&scoreHash=" + scoreHash + "&deviceHash=" + deviceHash + "&securityHash=" + securityHash );
This will send the current highscore from the device, along with a hash to identify the device as well as hashes to check for cheating to the server. It uses the WWW functionally unity provides.
All we need on the server is a php script that reads these values and saves them to a database.
$score = $_GET["score"]; $scoreHash = $_GET["scoreHash"]; $deviceHash = $_GET["deviceHash"]; $securityHash = $_GET["securityHash"]; if ( md5 ( $score . " dont cheat" ) == $hash1 && md5 ( $scoreHash . " anti cheat salt " . $deviceHash ) == $securityHash ) { $sql_insert_score = 'INSERT INTO yourgame_highscores (yourgame_highscores_id, yourgame_highscores_score, yourgame_highscores_scorehash, yourgame_highscores_devicehash, yourgame_highscores_security) VALUES ( NULL, \'' . $score . '\', \'' . $scoreHash . '\', \'' . $deviceHash . '\', \'' . $securityHash . '\' );'; $result = $db->query($sql_insert_score); }
This is only a small snippet example, the full code should do a lot more input checking and should only store one score per device.
But you can see how the server side code does the same hashing to check if the values provided are correct before it is saved to the database.
At this point we have a way to store and retrieve highscores locally on the device and also to send those scores to a server. We do a little bit of cheat-protection and we store the values to a database. From here we can do all kinds of thins: A normal highscore list, best players of the week, worst player ever….
Be assured that we are aware that this might not be the best possible way or the most secure option but it works for us and it was easy to implement! 🙂
If you have any questions, feel free to ask in the comments or send an email!