Games have had high score lists for a very long time. In fact, for many past games high score lists were the main reason to keep playing. While this idea worked well for arcade games where strangers could compete asynchronously against each-other it didn’t initially translate to console and other single player games. Clearly, it’s not as much fun to beat your past high score as it is to beat someone else’s. That’s where the idea of a global high score list comes in; The high score list lives on a server instead of next to the game code. This is a simple way to re-ignite competition between players and extend a game’s replay value.
Obviously, placing the high score list on a server introduces some new complications. Where before you could simply read and write to a local file you now must perform some sort of web service call! Don’t worry, while implementing a client-server model in your next game may sound challenging it’s really straightforward. I’ll show you how below.
Server
First let’s look at the server side of the equation. I’m going to implement a very simple system which spans two web pages and operates solely via HTTP. I’m going to assume the following:
- You have access to a running server (can be local)
- You are able (or willing to learn) to read PHP
Don’t worry the code is incredibly straightforward in this example, so it
should be easy to understand if you are familiar with Java. To start,
create a file post.php. Your game will call this page when trying to
save a score. It should return “success” when it writes the score and
“failure” when it doesn’t.
<?php
/** Read in a username and score and save them to a file. */
// read variables from POST data.
$username = @$_POST['name'];
$score = @$_POST['score'];
// verify the username is set and not empty
if (!isset($username) || $username == "") {
echo "failure";
exit;
}
// verify the score is set and a number
if (!isset($score) || !is_numeric($score)) {
echo "failure";
exit;
}
// format the username and score as a comma delimited row
$entry = $username . "," . $score . "\n";
// append entry to the score file
if (!file_put_contents("scores.csv", $entry, FILE_APPEND)) {
echo "failure"; // failed to write to file
exit;
}
echo "success";
?>
See, I told you it was straightforward. As you can see we’re saving
everything in a comma delimited file. If you anticipate a massive number
of scores then it might make more sense to refactor the above code to use
a database. To register a high score your game will call
http://server/post.php and pass name=foobar&score=1234 as POST data.
Next let’s create a page to retrieve a list of high scores. Start by
creating board.php. This should return some number of top scores. Since
CSV is a pretty neutral format we’ll keep using it here.
<?php
/** Return a list of top scores. */
// read variables from GET data
$num_scores = $_GET['num_scores'];
// read each line in scores.csv as a string into an array
$scores = file("scores.csv");
// define a comparator to sort items by score
function compare($s1, $s2) {
// split the strings by their delimiter
$a1 = explode(",", $s1);
$a2 = explode(",", $s2);
// compare the scores
return $a2[1] - $a1[1];
}
// sort the array of scores
usort($scores, "compare");
// output the requested number of top scores
for ($i = 0; $i < $num_scores && $i < count($scores); $i++) {
echo $scores[$i];
}
?>
So your game will call http://server/board.php?num_scores=10 to retrieve
the top ten high scores. The server will respond with a CSV file
containing each player-score pair. That’s it! Now that the server is
implemented let’s turn to the client (read game).
Client
Fortunately, things are even easier on the game side. Since we’ve been
leveraging HTTP we can simply use Java’s built in URL class! Here’s an
example call to post.php.
public static boolean post(String name, int score) {
// target URL to post to
String target = "http://server/post.php";
// data to write to the server
String content = "name=" + URLEncoder.encode(name, "US-ASCII");
content += "&score=" + URLEncoder.encode(score, "US-ASCII");
// open connection to write
URL url = new URL(target);
URLConnection c = url.openConnection();
c.setConnectionTimeout(1000); // timeout after 1 second
c.setDoOutput(true); // we're going to write to the server
DataOutputStream o = new DataOutputStream(c.getOutputStream());
// write the content
o.writeBytes(content);
o.flush();
o.close();
// read response and check for success
DataInputStream i = new DataInputStream(c.getInputStream());
String response = i.readLine();
return response.equals("success");
}
Alright, so Java doesn’t win any points for conciseness. Still I think
this is pretty straightforward. Notice that I use URLEncoder to encode
the parameters I’m sending to the server. This is a necessary step if you
intend to allow special characters to be used in, say, the user’s name.
For example, if you want to support names such as “John Doe”.
Next let’s read the scores back in from the server. This is simple after the last example:
public static String[] fetch(int num) {
// target URL to post to
String target = "http://server/board.php";
String target += "?num_scores=" + URLEncoder.encode(num, "US_ASCII");
// open connection to read
URL url = new URL(target);
URLConnection c = url.openConnection();
c.setConnectionTimeout(1000); // timeout after 1 second
DataInputStream i = new DataInputStream(c.getInputStream());
// read lines into an array and return
LinkedList<String> lines = new LinkedList<String>();
String line;
while ((line = i.readLine()) != null) {
lines.add(line);
}
return lines.toArray(new String[0]);
}
See, easy. Notice that this time we’re performing a GET request
meaning that we pass num_scores in through the url. Also note that I do
not transform the data from CSV format to something suitable for printing
on-screen. I’ll leave that as an exercise for you!
Conclusion
That’s that, you now have no good reason not to have a global high score list in your next game. If you do end up using this implementation let me know; I’m especially interested in what limitations you’ve found. Before you run off and start making the next killer shooter, though, let me leave you with one warning.
There is no security in this approach
You should worry that someone will enter in fake information or spam your
server. Right now my approach doesn’t have any sort of security in place
to prevent that. I would start by adding a password of some sort to
prevent people from calling post.php. Of course, I don’t believe it’s
possible to ship said password with your game code without compromising
it, but at least you’ll stop most people. If there’s interest I can make a
second post on this topic to look at different ways to combat spam and
cheaters.
Hello, my name is Alex Schearer. I grew up in New York and currently live in Seattle.
9 Comments
Great! But I get this error
Parse error: syntax error, unexpected T_VARIABLE, expecting ‘;’ in board.php on line 22
Sorry about that, there are some minor errors with the script. I’ve updated things now such that they should work. Thanks for bringing this to my attention! Also if there is interest I can post a new method for doing global high score lists which allows you to:
Let me know and I’ll prepare things and publish them.
Hey, when I run post.php?name=something&score=1234
I always get the first error
// verify the username is set and not empty if (!isset($username) || $username == “”) { echo “failure0″; exit; }
Do you know what is causing it?
I commented first 2 errors. Now I get succes with post.php but When I go to board.php?num_scores=10 I only get this , , , , ,
Systat, to answer your first question, that won’t work because you’re passing in GET data. You need to pass POST data to post.php in order for the values to be set. See this page for an explanation of the two.
As for your second problem, I believe that though you were getting “success” from post.php you were actually only inserting empty values. Please try correcting your usage for post.php and then board.php should work.
Hope that helps, let me know if it still isn’t working.
Hehe, sorry for spamming your website but I fixed it, you need to use
@$GET instead of @$POST in your post.php script.
Hey, not a problem I’m glad we’re covering this topic, and I’m sure this is a common problem.
You’re right that switching to GET data will allow you to enter data through a URL, but I would advice you to keep it as POST data which has certain properties which fit well with the action you’re trying to accomplish. The only downside is you can’t simply type in the URL and hit enter to see the thing take effect. You’ll either need to write a form or write code which posts back to the server. If you are using PHP it’s as simple as calling
file_put_contents.quick question,
how will this hold up with large data, for example, for over 1,000,000 scores, wouldn’t it slow down?
Hi DJ, good question. For a large data set I would replace the file mechanism with a database. That would speed up reading the results. I would also advise using some form of pagination — i.e. only fetch 50 results at a time — so that you don’t wind up bringing the entire data set into memory at once. Finally, in my experience if you high score list is that large chances are it is not compelling any longer. Getting to to the top of such a list would be beyond most players after all. If you anticipate that level of engagement I would suggest grouping high scores by a social network — i.e. only show users their friends high scores — which keeps things competitive.