Tag: mysqli

Server-Side High Scores in Unity3D

When developing my 1945 game, I wanted a way for the high score that one player achieved to be visible to another player playing my game on my website as well. Not knowing where to start, I wandered about the internet for a little while (as I am wont to do on occasion) and I happened across this post on the Unify wiki: Server Side Highscores.

Looking through the code on that page, I was sad to see that it is woefully out of date, as the “tested working solutions” it lists use the deprecated mysql_* PHP functions instead of mysqli_* or PDO. In addition, the code as presented was hard coded within the game to a very specific structure of GUI elements in Unity. So while it didn’t really work for my purpose, it at least got me thinking in the right direction.

Detailed here is my solution to server-side high scores. I’ve aimed for a solution that is at least a little bit more secure and (in my opinion) a little bit more elegant as its completely independent from the GUI.

Note that I’m not going to go in detail on how to add the high score system itself to your game, as that’s going to be dependent on a lot of things within your game. In my 1945 game I have a fairly simple system consisting of two arrays: an array of TextMeshProUGUI elements for the names and another with the scores that I display on screen when the game ends. Those just get populated with the data fetched by the code I will be detailing here. If you do want to see my full scoreboard code, my 1945 project is available for perusal on my Bitbucket.

Database

First, you will want to set up and create your database. There are a lot of resources online on how to set up a database and is entirely dependent on how you are going to show off your game, so I won’t rehash that here. I’m going to assume that you’re able to set up your database and access it somehow. My web host supplies access to database management through phpMyAdmin, but as long as you can execute SQL somehow you will be fine.

The important part here is to set up a table, and the Unify wiki has a reasonable setup for this, so I’ll just paraphrase the instructions and copy the SQL code to create the table directly from there:

Run the following SQL on your database. If you get a SQL Syntax error, replace

TYPE=MyISAM;

with

ENGINE=MyISAM; 

as TYPE is deprecated.

CREATE TABLE `scores` (
   `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
   `name` VARCHAR(15) NOT NULL DEFAULT 'anonymous',
   `score` INT(10) UNSIGNED NOT NULL DEFAULT '0'
)
TYPE=MyISAM;

Server-Side Scripts

Here we will create three scripts: one that will hold our mysqli connection information, one to save scores in the database, and one to retrieve them.

mysqli-connect-gamename.php

This file contains the connection information, including the password to log on to your database, in plaintext. Therefore this file should not be placed in your “public_html/” folder. Anything in your public_html folder can be read by anyone given enough interest to do so, so this is to help protect your database.

That is important, so let me repeat that. DON’T PUT THIS FILE ANYWHERE IN YOUR “PUBLIC_HTML/” DIRECTORY.

The other scripts assume they are at the root directory of your web hosting. If you put it somewhere elsewhere, you will have to adjust the

require ('../../../mysqli_connect-gamename.php');

line in the other two PHP files appropriately. Of course, replace the database information in the script below with your actual database information.

<?php # Script 9.2 - mysqli_connect-1945.php

// This file contains the database access information.
// This file also establishes a connection to MySQL,
// selects the database, and sets the encoding.

// Set the database access information as constants:
DEFINE ('DB_USER', 'database-username');
DEFINE ('DB_PASSWORD', 'database-password');
DEFINE ('DB_HOST', 'localhost');
DEFINE ('DB_NAME', 'database-name');

// Make the connection:
$dbc = mysqli_connect (DB_HOST, DB_USER, DB_PASSWORD, DB_NAME) OR die ('Could not connect to MySQL: ' . mysqli_connect_error() );

// Set the encoding...
mysqli_set_charset($dbc, 'utf8');

addscore.php

This script saves the score sent by your game to the database. Put this in the same folder as your game (so in my case, “unity/1945/”). Make sure the secret key here and the secret key in the C# script match exactly. While not necessary, I recommend generating a key using a password generator. There are a lot of free online options such as the Lastpass or 1Password password generators (generating a password on either of their sites is completely free to use, no login needed).

<?php

require ('../../../mysqli_connect-1945.php');
 
// Strings must be escaped to prevent SQL injection attack.
$name = mysqli_real_escape_string($dbc, $_GET['name']);
$score = mysqli_real_escape_string($dbc, $_GET['score']);
$hash = $_GET['hash'];

$secretKey="mySecretKey"; # Change this value to match the value stored in the client script

$real_hash = md5($name . $score . $secretKey);
if($real_hash == $hash) {
    // Send variables for the MySQL database class.
    $query = "INSERT INTO scores VALUES (NULL, '$name', '$score');";
    $result = $dbc->query($query) or die('Query failed: ' . mysqli_error($dbc));
}
?>

display.php

This script retrieves the scores from the database to be displayed in game. It returns a tab separated list of 10 name/score pairs, with each pair on its own line. That is, it returns a list that looks like this:

Matthew Randolph    1000
Matt    950
ThatMatGuy    800
Gurravanna Drillemont    700
TheMonthOfFebruary    600
Salazar Salamander    550
MrMatt    540
SomeGuyNamedMatt    500
JustAMattPassingBy    400
TheBestKindOfMatt    100

Note that because we are escaping characters when we save them to the database, even if someone decided to try to pull a fast one on us and include a TAB character in their name, we’d still be protected from it, as it should show up as an escaped sequence of characters that represents TAB and not as a TAB itself.

 <?php

 require ('../../../mysqli_connect-1945.php');

$query = "SELECT * FROM `scores` ORDER by `score` DESC LIMIT 10";
$result = mysqli_query($dbc, $query) or die('Query failed: ' . mysqli_error($dbc));

$num_results = mysqli_num_rows($result);

for($i = 0; $i < $num_results; $i++)
{
     $row = mysqli_fetch_array($result);
     echo $row['name'] . "\t" . $row['score'] . "\n";
}
?>

Client-Side Code (a.k.a Your Game)

ScoreboardWebInterface.cs

Here we create a C# script in your game that will connect to your addscore.php and display.php scripts to send and retrieve scores within the game. Start by creating a new C# script and calling it ScoreboardWebInterface.

Lets open the script and start by adding the variables we will need. We need a reference to the secret key you created in addscore.php, string references to your addscore and highscore php scripts, and a property to hold the scores:

public class ScoreboardWebInterface : MonoBehaviour
{
    private string secretKey = "YourSecretKey"; // Edit this value and make sure it's the same as the one stored on the server
    public string addScoreURL = "https://yourwebsite.com/unity/yourgame/addscore.php?"; // be sure to add a ? to your url
    public string highscoreURL = "https://yourwebsite.com/unity/yourgame/display.php";
    
    private KeyValuePair<string, string>[] scores;
    public KeyValuePair<string, string>[] Scores
    {
        get { return scores; }
    }

Next we write the method to send a new score to the database. Because we’re using web resources, we need to call it as a coroutine so that our game doesn’t lock up while we’re waiting around for the response from the server about whether it worked or not.

// Send the new score to the database
public IEnumerator PostScore(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 = Utility.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.
    Debug.Log("Submitting score");
    WWW hs_post = new WWW(post_url);
    yield return hs_post; // Wait until the download is done
    Debug.Log("Score submitted");
 
    if (hs_post.error != null)
    {
        Debug.Log("There was an error posting the high score: " + hs_post.error);
    }
}

To fetch the scores from the database, we will use this next method.

// Get the scores from the database
public IEnumerator GetScores(Action<int> returnCode)
{
    scores = new KeyValuePair<string, string>[1];
    scores[0] = new KeyValuePair<string, string>("Loading Scores","");
        
    WWW hs_get = new WWW(highscoreURL);
    yield return hs_get;

    if (hs_get.error != null)
    {
        Debug.Log("There was an error getting the high score: " + hs_get.error);
        scores[0] = new KeyValuePair<string, string>("There was an error getting the high score", hs_get.error);
        returnCode(1);
    }
    else
    {
        // split the results into an array
        Regex regex = new Regex(@"[\t\n]");
        string[] rawScores = regex.Split(hs_get.text);

        // Restructure the string array into an array of KeyValuePairs
        scores = new KeyValuePair<string, string>[rawScores.Length / 2];
        int rawScoreIndex = 0;
        for (int i = 0; i < rawScores.Length / 2; i++)
        {
            scores[i] = new KeyValuePair<string, string>(rawScores[rawScoreIndex], rawScores[rawScoreIndex + 1]);
            rawScoreIndex += 2;
        }

        returnCode(0);
    }
}

This first fills the Scores property with the results from the database. Then, because as far as I know the return value of a Unity coroutine is consumed by the Unity engine itself, we need some other way of letting the rest of our code know the result of whether or not the GetScores function worked.

So we instead create an ‘out’ parameter of sorts with the Action<int> returnCode. You could make it more verbose by passing the result of an enum or something, but for my purposes I just wanted a pass-state (0) or a fail-state (1). Yes, I know it could be a boolean. Or a ReturnCode.Success/ReturnCode.Failure enum. And both of those would probably be better than the way I have it. Sometimes it can be hard to escape my time spent scripting with Unix. 😆

In case you want to check that you got all the code or you just want to copy the whole file, here is the entire ScoreboardWebInterface file in all its C# glory:

using System;
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;

public class ScoreboardWebInterface : MonoBehaviour
{
    private string secretKey = "YourSecretKey"; // Edit this value and make sure it's the same as the one stored on the server
    public string addScoreURL = "https://yourwebsite.com/unity/yourgame/addscore.php?"; //be sure to add a ? to your url
    public string highscoreURL = "https://yourwebsite.com/unity/yourgame/display.php";
    
    private KeyValuePair<string, string>[] scores;
    public KeyValuePair<string, string>[] Scores
    {
        get { return scores; }
    }

    // Send the new score to the database
    public IEnumerator PostScores(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 = Utility.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.
        Debug.Log("Submitting score");
        WWW hs_post = new WWW(post_url);
        yield return hs_post; // Wait until the download is done
        Debug.Log("Score submitted");
 
        if (hs_post.error != null)
        {
            Debug.Log("There was an error posting the high score: " + hs_post.error);
        }
    }

    // Get the scores from the database
    public IEnumerator GetScores(Action<int> returnCode)
    {
        scores = new KeyValuePair<string, string>[1];
        scores[0] = new KeyValuePair<string, string>("Loading Scores","");
        
        WWW hs_get = new WWW(highscoreURL);
        yield return hs_get;

        if (hs_get.error != null)
        {
            Debug.Log("There was an error getting the high score: " + hs_get.error);
            scores[0] = new KeyValuePair<string, string>("There was an error getting the high score", hs_get.error);
            returnCode(1);
        }
        else
        {
            // split the results into an array
            Regex regex = new Regex(@"[\t\n]");
            string[] rawScores = regex.Split(hs_get.text);

            // Restructure the string array into an array of KeyValuePairs
            scores = new KeyValuePair<string, string>[rawScores.Length / 2];
            int rawScoreIndex = 0;
            for (int i = 0; i < rawScores.Length / 2; i++)
            {
                scores[i] = new KeyValuePair<string, string>(rawScores[rawScoreIndex], rawScores[rawScoreIndex + 1]);
                rawScoreIndex += 2;
            }

            returnCode(0);
        }
    }
 
}

Scoreboard.cs

So now that we have a database and a way to read and write values into and out of our program, how do we actually use the data? For fetching the scores from the database, you are going to want these two lines of code, which need to be wrapped in a coroutine due to the yield return statement:

// Fetch the global high score list from database
int returnCode = -1;
yield return scoreboardWebInterface.GetScores(status => returnCode = status);

And to save the scores is as simple as calling:

StartCoroutine(scoreboardWebInterface.PostScores(playerName, playerScore));

Since an example is worth 1,000 words (or something like that), this is how I use these in my 1945 game. I call LoadScores when the game starts so that I know that its ready (or failed) well before we need to use the scores.

private IEnumerator LoadScores()
{
    // This initializes the default scores from Scores.cs
    scores = new Scores();

    // Fetch the global high score list from database
    int returnCode = -1;
    yield return scoreboardWebInterface.GetScores(status => returnCode = status);

    if (returnCode == 0)
    {
        highScoreLocation = HighScoreLocation.Global;
        for (int i = 0; i < scoreboardWebInterface.Scores.Length; i++)
        {
            scores.names[i] = scoreboardWebInterface.Scores[i].Key;
            scores.scores[i] = Int32.Parse(scoreboardWebInterface.Scores[i].Value);
        }
    }
    else if (returnCode == 1)
    {
        // An error occurred attempting to get the high scores, so fall back to the local high score list
        highScoreLocation = HighScoreLocation.Local;
        saveStatePath = Path.Combine(Application.persistentDataPath, "scoreboard.sgm");

        // Read the high scores from the local JSON file, if it exists
        if (File.Exists(saveStatePath))
        {
            String fileContents = File.ReadAllText(saveStatePath);
            JsonUtility.FromJsonOverwrite(fileContents, scores);
        }
    }

    for (int i = 0; i < scores.scores.Length; i++)
    {
        scoreTexts[i].text = scores.scores[i].ToString();
    }
    for (int i = 0; i < scores.names.Length; i++)
    {
        nameTexts[i].text = scores.names[i].ToString();
    }
}

And I call CommitScores after the player has completed their run. Because I have multiple ways for the commit to happen, I have a sanity check in there to make sure that it doesn’t get double-committed:

public void CommitScores()
{
    if (!scoresSubmitted)
    {
        if (highScoreLocation == HighScoreLocation.Global)
        {
            if (playerName != null && playerScore != 0)
            {
                StartCoroutine(scoreboardWebInterface.PostScores(playerName, playerScore));
                scoresSubmitted = true;
            }
            else
            {
                Debug.Log("Score has not been saved, as either the name or score values are null.");
            }
        }
        else if (highScoreLocation == HighScoreLocation.Local)
        {
            File.WriteAllText(saveStatePath, JsonUtility.ToJson(scores, true));
            scoresSubmitted = true;
        }
        else
        {
            Debug.Log("The high score location is undefined.");
        }
    }
}

And voila! You should now have the structures required to implement your own server-side high score table similar to the one I have in 1945:

Debugging Tips

The Debug.Log() command in Unity is a powerful tool for debugging problems. When you’re running a WebGL build, any Debug.Log() lines you include in your unity project will appear in the browser console, along with any other errors or warnings that occur. This is a super handy way to identify and fix problems that crop up while trying to set up your code on a website!

In Firefox, you can open the console by pressing Ctrl-Shift-K, in Internet Explorer and Edge by pressing F12, and in Chrome by pressing Ctrl-Shift-J. If you’re using a different browser, sorry, I’m not familiar with how to find your browser’s console. You’ll have to do a web search to figure it out!

You also have to be especially careful that you enter the URL’s exactly. If you type “http://” instead of “https://” or if you don’t include the “www.” in front of it when you need it, you will get an error in the console like “Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at $somesite” indicating that the request was blocked due to violating the CORS security rules.

While there could be other reasons why CORS blocks your request, the reasons I noticed were when I tried to go across from https:// to http:// or when I used an IFrame to embed my game in WordPress. IFrame has some weirdness with accessing external resources, even from the same site. If you want to publish this game on a WordPress site, you will likely want to look at my Embedding Unity Games with Shortcodes page.