This is a little tutorial covering using ActionScript 3, PHP and AMFPHP to create a MySQL-based High Score Database.  You should have some familiarity with each as this isn’t exactly a “Beginner’s How-To.” For a recent game project I’ve been working on, one of the requirements was a simple High Score Database. After finishing it, I thought I’d post about how I went about coding it. Let’s jump right in with the ActionScript first…

So from the game’s .as files, the idea was to display a DataGrid that shows all the scores submitted to the database. I also wanted to create a ScoresDB class that handles all of my database calls and parses the database results, all ready to be added to the DataGrid.

So that we’re all on the same page, Main.as will refer to the main class that handles adding the DataGrid to the stage, and handles other game functions. ScoresDB.as will refer to the ScoresDB class that handles the AMFPHP/PHP/MySQL calls. HighScore.php will refer to the AMFPHP Service that actually interacts with the MySQL database and returns result sets.

In Main.as, I first created a DataProvider ( DP ) Object that the DataGrid ( DG ) would be using as the data to display in the grid. After creating the DP, I passed a reference to that DP into the constructor of ScoresDB.

[sourcecode lang=”php”]
public class Main extends MovieClip
{
. . . .
   // Creating class variables
   private var scores:ScoresDB;
   public var dbData:DataProvider;
. . . .
   // In the function where I create the data provider and datagrid…
   dbData = new DataProvider();
   scores = new ScoresDB( dbData );
. . . .
[/sourcecode]

Then, in the ScoresDB.as file:

[sourcecode lang=”php”]
public class ScoresDB extends EventDispatcher
{
. . . .
   // Class variables, a private “reference” to that DP
   private var dbData:DataProvider;
. . . .
   // ScoresDB Constructor function creates a new DataProvider
   // then sets it equal to the reference to the Main.as DP
   public function ScoresDB( dp:DataProvider ):void
   {
      dbData = new DataProvider();
      dbData = dp;

. . . .
[/sourcecode]

You could also do this without setting that reference, and simply having the following ScoresDB.as functions return a DP object. But I like to do it this way. Yes, it removes some encapsulation with our DataProvider objects, but to me, it simplifies things. And that’s more the point of OOP.

I created a MovieClip called “scoreData” that contains a DataGrid called “dg”. So in the following code when you see scoreData.dg , it’s simply referring to the DataGrid object that’s already been added to the scoreData MovieClip. To initialize and configure my DG, this is my initDG method which gets called as soon as the MovieClip scoreData is added to the stage. I found that online, sometimes this function would get called before the MC was fully on the stage and it would throw errors. So adding an EventListener for the Event.ADDED_TO_STAGE to the scoreData MC was a great way to know exactly when it was safe to initialize the DataGrid.

As the first line, I’m simply removing that event listener (or explicitly ensuring that it is removed) and then I’m creating the DataGridColumns that the DG will use.

[sourcecode lang=”php”]
private function initDG( e:Event ):void
{
   scoreData.removeEventListener( Event.ADDED_TO_STAGE , initDG );

   var rankDGC:DataGridColumn = new DataGridColumn( “Rank” );
   rankDGC.width = 75;
   rankDGC.sortOptions = Array.NUMERIC;

   var nameDGC:DataGridColumn = new DataGridColumn( “Name” );
   nameDGC.width = 175;

   var scoreDGC:DataGridColumn = new DataGridColumn( “Score” );
   scoreDGC.sortOptions = Array.NUMERIC;
   scoreDGC.width = 125;

var dateDGC:DataGridColumn = new DataGridColumn( “Posted” );
dateDGC.width = 125;

scoreData.dg.addColumn( rankDGC );
scoreData.dg.addColumn( nameDGC );
scoreData.dg.addColumn( scoreDGC );
scoreData.dg.addColumn( dateDGC );

scoreData.dg.dataProvider = dbData;
scores.getAllScores();
}
[/sourcecode]

Lines 7 & 13 show something you may or may not be familiar with. Without setting the column’s .sortOptions property to Array.NUMERIC, it will try to sort the columns as Strings. So you would see a list that looks like:

  • 1
  • 10
  • 11
  • 2

instead of what you Actually want ( sorted Numerically ):

  • 1
  • 2
  • 10

After creating the columns and setting their properties, in lines 19-22 I add the columns to the scoreData.dg DataGrid and then all is well as far as columns go. Quick note, when I called
new DataGridColumn( “Rank” );
that was setting the internal, as well as the externally displayed name to “Rank”. This will come into play soon.

Line 24: sets my dbData DataProvider object as the DataGrid’s … Data Provider… and ScoresDB already has a reference to that variable, so whenever I change the contents of dbData inside ScoresDB, it will now automatically be the data displayed in my DataGrid.

Line 25: calls scores.getAllScores(); scores is my ScoresDB object, so let’s look at the getAllScores() method.

[sourcecode lang=”php”]
public function getAllScores():void
{
dispatchEvent( new ProgressEvent( ProgressEvent.PROGRESS ) );
gw.call(“HighScores.getAllScores”, res );
}
[/sourcecode]

That’s it. And the only reason I specifically am dispatching an event is simply so that Main.as knows when it needs to show the swirling “Loading Data” type image, otherwise this method would just be one line.

Before we get into the PHP class, and the getAllScores method in PHP, lets look at the full ScoresDB.as class and talk about that.

[sourcecode lang=”php”]
package zf
{
import flash.net.*;
import fl.data.DataProvider;
import flash.events.SecurityErrorEvent;
import flash.events.NetStatusEvent;
import flash.events.EventDispatcher;
import flash.events.ProgressEvent;
import flash.events.Event;

public class ScoresDB extends EventDispatcher
{
private const GATEWAY_URL:String = “http://localhost/amfphp/gateway.php”;
private const SEP:String = ‘/’;

private var gw:NetConnection;
private var res:Responder;
private var dbData:DataProvider;

public function ScoresDB( dp:DataProvider ):void
{
dbData = new DataProvider();
dbData = dp;

gw = new NetConnection();

gw.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
gw.addEventListener(NetStatusEvent.NET_STATUS , netStatusErrorHandler);
gw.connect( GATEWAY_URL );

res = new Responder(onResult, onFault);
}

private function securityErrorHandler(event:SecurityErrorEvent):void
{
dispatchEvent( new Event( Event.COMPLETE ) );
trace(“securityErrorHandler: ” + event );
}

private function netStatusErrorHandler(event:NetStatusEvent):void
{
dispatchEvent( new Event( Event.COMPLETE ) );
trace(“NetStatusErrorHandler: ” + event.info.code );
}

public function getAllScores():void
{
dispatchEvent( new ProgressEvent( ProgressEvent.PROGRESS ) );
gw.call( “HighScores.getAllScores” , res );
}

public function addScore( pName:String , pScore:String ):void
{
dispatchEvent( new ProgressEvent( ProgressEvent.PROGRESS ) );
gw.call( “HighScores.addScore” , res , pName , pScore );
}

private function onResult(responds:Object):void
{
dispatchEvent( new Event( Event.COMPLETE ) );

if( typeof responds == ‘boolean’ )
{
// addScore returns a boolean of type ‘true’ if added successfully
// or ‘false’ if adding score to the DB was unsuccessful
// we could then handle that visually here in Flash
}
else
{
// parse the ArrayCollection
var result:Array = responds.serverInfo.initialData;
for( var i:uint=0; i < result.length; i++ ) { var year:String = result[i][5].substr( 0 , 4 ); var month:String = result[i][5].substr( 5 , 2 ); var day:String = result[i][5].substr( 8 , 2 ); dbData.addItem({ Rank: i + 1 , Name: result[i][1] , Score: result[i][2] , Posted: month + SEP + day + SEP + year }); } } } private function onFault(responds:Object):void { dispatchEvent( new Event( Event.COMPLETE ) ); for(var i in responds) { trace(responds[i]); } } } } [/sourcecode] Alrighty... what's going on here... Line 13: Constant string URL to the AMFPHP Gateway
Line 14: Constant string Separator used to separate the date later
Line 16: gw is a NetConnection object for out Gateway
Line 17: res is the Responder object holding the data returned from the gw call
Line 18: dbData is our previously mentioned DataProvider reference holder
Line 27: Adding an Event Listener to listen for SecurityErrors, this is useful when setting up your secure sandbox
Line 28: Adding an Event Listener to listen for NetStatusEvent Errors, this is also primarily used in setup to make sure you’re hitting the right location for the Gateway, and to handle other NetStatusEvent Errors
Line 31: Initializes the Responder object, setting the function handling “Ok” results to onResult, and “Bad” results to “onFault”
Line 56: This is how you send variables from AS3, through AMFPHP to the PHP Class. We’ll see where these come in later.
Line 63: The responds param will either give us an ArrayCollection of data, or in the case when we call addScore, the result will either be true or false depending on if MySQL successfully adds the data or not. You could add logic here to display to the user on successfully adding the data.
Line 75-77: Substrings out the year, month, and day from the MySQL DateTime field.
Line 79: Adds a new item as an object to the DataProvider
Line 80: As the results are returned pre-ordered by score descending, the highest score will be first. Since our array is zero-based, i + 1 will begin our Rank column with 1, 2, … and so on.
Line 80-83: “Rank” , “Name” , “Score” , and “Posted” were the column names we gave to the DataGridColumns
Line 81: Name is currently returned in the result[ i ][ 1 ] index
Line 82: Score is currently returned in the result[ i ][ 2 ] index
Line 83: Posted is going to be the previously substringed-out data in the format: month + SEP + day + SEP + year
Line 89: The onFault function simply traces out any data in the responds object

Alrighty… so that takes care of the ActionScript. Now let’s take a look at the PHP:

[sourcecode lang=”php”]
< ?php class HighScores { var $limit = 25; var $host = "localhost"; var $user = "username"; var $pass = "password"; var $db = "database"; var $table = "scoresTable"; function __construct() { mysql_connect( $this->host , $this->user , $this->pass );
mysql_select_db( $this->db );
}

/**
* Retrieves tutorial data
* @returns title, description, and url
*/
function getTopScores()
{
return mysql_query( “SELECT * FROM $this->table ORDER BY score DESC LIMIT $this->limit ” );
}

function getAllScores()
{
return mysql_query( “SELECT * FROM $this->table ORDER BY score DESC” );
}

function addScore( $pName , $pScore )
{
$created = date( “Y-m-d H:i:s”);
$cleanName = mysql_real_escape_string( $pName );
$cleanScore = intval( $pScore );

return mysql_query( “INSERT INTO $this->table SET `name` = ‘{$cleanName}’ , `score` = $cleanScore , `created` = ‘{$created}’ “);
}
}
?>
[/sourcecode]

Breaking this down….

Line 2: HighScores is the name of the class, and looking back up at the ActionScript file you’ll notice that’s how we refer to the methods in this class. To call the addScore method, we used HighScores.addScore()
Line 4-9: Defining some class variables for our database, as well as $limit if we wanted to be able to change the number of results returned by getTopScores();
Line 10-14: This is the PHP Constructor class. It is called any time we create a HighScores object, and by adding the mysql_connect and mysql_select_db functions
Line 26-28: The getAllScores() function simply returns the mysql_query result
Line 31-37: The addScore function accepts our two params from the ScoresDB.as call. We’re also using PHP to create our Date object
Line 34: This is the last line of defense between user data and our database. For any String data that you accept from a user, you should pass it through PHP’s mysql_real_escape_string() function. It helps to safeguard from mysql injection attacks.
Line 35: PHP’s intval() function ensures that whatever data comes in gets converted to an int.

Download ActionScript and PHP Source Code
-Edited to fix forloop

Categories:

0 Comments

Leave a Reply

Avatar placeholder