AS3 – AMFPHP High Score Database

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.

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 );
. . . .

Then, in the ScoresDB.as file:

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;

. . . .

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.

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();
}

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.

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

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.

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]);
         }
      }
   }
}

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:

< ?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}' ");
   }
}
?>

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

FacebookTwitterGoogle+Share

12 Comments

 Add your comment
  1. In the ScoresDB.as file that you have created up there. Line 73 doesn’t make any sense because the syntax on that for loop is incorrect. Tell me what I’m doing wrong here. Thanks.

  2. Nope, you’re absolutely right. Part of the code got cut off or deleted in copy/pasting different blocks apparently… It’s updated now to show the proper for loop code. Also I dropped a link in there so you can download the sourcecode… didn’t realize I had forgotten that sorry.

  3. Wow, thanks for the quick response. Very impressive.

  4. The other question I had was that in your PHP Class HighScores, I believe that in line 22 for instance, that ” $this-&amp;amp;gt;limit ” is an incorrect statement. Some of the data may be getting stripped by wordpress?

  5. Yes.. right again.. I just uploaded a fresh set of source files… the AS code was also referencing “StreamHighScores” instead of just “HighScores”

  6. I’m having troubles understanding this statement:

    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
    }

    My calls always return objects, so I don’t understand how this is useful? Do you get calls that are something other than objects? Any help here would be appreciated.

  7. Yes… Actually if you read the commented code there, the addScore method is the only method that returns a boolean true/false if the database insert was successful. So that part is blocked out so that addCode wont throw an error, and also so you can handle things if responds comes back false (didnt successfully add the score data )

  8. In certain browsers when I test my game, the scoreboard breaks the script, and the results never show. I think it’s because the browser doesn’t think the connection is secure. Do you know anything about this and what I can do to fix it?

  9. A Couple things to check… first of all make sure the scores are actually being inserted into the database. That could be more of an issue with a bad SQL statement or an error somewhere in the PHP. If the scores are getting to the DB, do you have a link or could you elaborate on “scoreboard breaks the script”?

  10. I’m getting this error. Any idea what it means?

    Object of class HighScores could not be converted to string
    41
    AMFPHP_RUNTIME_ERROR

  11. Good example. I’m using a XMLListCollection and have ftliering working. However I want to add the same type of label you have for the panel status: {arrColl.length}/{arrColl.source.length}. My XMLListCollection is called tixXML and I can get the displayed size (tixXML.length) however how do I get the length of the unfiltered tixXML? After I filter the results tixXML.length changes to the number of displayed results however I also want the length of the unfiltered results? Do I need to set a var to the original amount or is there a property I can access?Thanks in advance.

  12. Hey Jenny, I tried to pull this project back up to check, but it’s so old and out of date, and the new AMFPHP 2.2 is so different, this old tutorial doesn’t really work with it. You’re definitely right though, get the length when all your scores return as the original length, then you can do your math. Good luck! At some point I should probably just completely redo this as its very out of date hehe. Thanks!

Leave a Comment

Your email address will not be published.

2 Trackbacks

  1. nrfpt.net » Blog Archive » Settling on an high scores system (Pingback)
  2. nrfpt.net » Blog Archive » A word for amfphp (Pingback)