Home  >  

Facebook App Case Study: Front-end Module

Author photo
AddThis Social Bookmark Button
Welcome back to our exciting series. In the last article, we discussed about the administrative part of the application, where we implemented a background activity to allow every application user to upload their own videos and add mistakes. This time we will work on the front end that will be visible to everyone by default and where the main game play will take place. Before we start with the development, there are important things that need to be considered. First of all, there will be 14 levels to play, 14 levels means 14 video clips, 14 movie snippets.
So how does the application front end look like? Here is a quick image:

article19_img6.png
This is what we want to achieve in this article. It's important to have the image in the mind to make sure we follow the same goal. There are familiar elements like the video controls etc. There are also less familiar objects like the “Hint” button and the text field at the bottom. On the right side, there is a small slider that will act as a level overview in the game. There are other small things that need to be added to the stage, but it's important that we see the main elements and how they fit together on stage.
In reality, the application will load the movieclips (ids) and the associated data right from a mySQL table.
Creating the database
Login into phpMyAdmin of your web host. Here is the image from my server side:

article19_img2.png
Create a database called movieexpert and add the following SQL that can be downloaded mb_db.sql.
With the SQL, we just created 14 levels of the game! Now in the database, we should have the tables “users” and “mistakes” created.

article19_img3.png
The table users is empty for now, it will be populated every time new users log in. On the other side, the table “mistakes” is already populated and contains 14 rows:

article19_img4.png
As we can see, the SQL statements now make sense because it was transformed into meaningful data. First we have the id of the table as the primary key. Then we have the mediaID which is in fact the id of the video on YouTube. Next we see the column “errors” that contains all the information about the errors in the clip. Finally there is the “allw” column which indicates that the video is allowed for playing in the application. If it is set to “no”, the video will not be shown.
With that, the database is prepared!
Preparing the AMFPHP class file
Because we will need to connect to this database, we can’t connect to it using pure ActionScript. We need AMFPHP to work on this part. Upload the AMFPHP base folder to the web server and add the following class file to the “services” folder:

<?php

class MBServices {
 
 	function MBServices(){
  	}
 
 	//connect to db
 	function connect(){
  		$env = getenv("DOCUMENT_ROOT");
  		require_once($env."/DBConstants.php");
  		$this -> connID = mysql_connect(DB_HOST, DB_USER, DB_PASS);
  		if($this -> connID){
   			if(@mysql_select_db(DB_NAME)){
    				$ip = $_SERVER["REMOTE_ADDR"];
    				return true;
    			}	
   		}
  		return false;
  	}
 
 	//get level of player
 	function getUserData($uid){
  		if(!$this -> connect()){
   			return false;
   		}
  		$sql = sprintf("SELECT level, points FROM users WHERE uid = ".$uid);
  		$result = mysql_query($sql) or die(mysql_error());
  		$resultArr = mysql_fetch_array($result);
  		$res = array($resultArr["level"], $resultArr["points"]);
  		$errors = $this -> loadErrors($resultArr["level"]);
  		array_push($res, $errors);
  		$sql = sprintf("SELECT mediaID FROM mistakes WHERE allw = 'yes'");
  		$result = mysql_query($sql) or die(mysql_error());
  		$mediaIDs = array();
  		while($resultArr = mysql_fetch_array($result)){
   			$mediaIDs[] = $resultArr["mediaID"];
   		}
  		array_push($res, $mediaIDs);
  		return $res;
  	}
 
 	//loadErrors
 	function loadErrors($mediaID){
  		if(!$this -> connect()){
   			return false;
   		}
  		$sql = sprintf("SELECT errors FROM mistakes WHERE mediaID = '".$mediaID."' AND allw = 'yes'");
  		$result = mysql_query($sql) or die(mysql_error());
  		$resultArr = mysql_fetch_array($result);
  		return unserialize($resultArr["errors"]);
  	}
 
 	//update level
 	function updateLevel($uid, $level){
  		if(!$this -> connect()){
   			return false;
   		}
  		$sql = "UPDATE users SET level = '".$level."' WHERE uid = ".$uid." LIMIT 1;";
  		$result = mysql_query($sql);
  		return mysql_affected_rows();
  	}
 
 	//save score
 	function saveScore($uid, $score){
  		if(!$this -> connect()){
   			return false;
   		}
  		$sql = "UPDATE users SET points = ".$score." WHERE uid = ".$uid." LIMIT 1;";
  		$result = mysql_query($sql);
  		return mysql_affected_rows();
  	}
 
 	//add errors to database
 	function saveError($id, $errors, $uid){
  		if(!$this -> connect()){
   			return false;
   		}
  		$sql = "INSERT INTO mistakes(mediaID, errors) VALUES('".$id."', '".serialize($errors)."');";
  		$result = mysql_query($sql);
  		$headers  = "From: \"MovieExpert User\" <noreply@mail.com>\r\n";
  		$headers = "Content-type: text/html\r\n";
  		mail("mirzahat@gmail.com", "New Video added!", "Id videa je: ".$id."<br/>Poslao: ".$uid, $headers);
  		return mysql_affected_rows() == 1 ? true : false;
  	}
 
}
  
?>
Save this file as MBServices.as and upload it to the services folder. The path should be similar to http://www.myserver.com/myapp/amfphp/services/MBServices.as.

article19_img5.png
In the connect method, there is the connecting procedure with the following lines of code:


$env = getenv("DOCUMENT_ROOT");
require_once($env."/DBConstants.php");
This indicates that the class file searches for the php file that contains the database access strings. So, let's create it!
Create a new php file called DBConstants.php with the following contents:


<?php

define("DB_HOST", "your_mysql_server");
define("DB_USER", "you_username");
define("DB_PASS", "your_password");
define("DB_NAME", "your_database_name");

?>
The process of spotting mistakes
Looking at the typical game scenario like on the image below:

article19_img7.png
The natural process of the game is that when a mistake in the movie occurs, like in this case Forest Gump, the player simply has to click on the mistake. In the Forest Gump scene for example, the mistake appears when Forest stops running. We can see on the images below, the weather in one scene in sunny, in the other scene it is different. That's is a typical continuity error.
Forest runs when it is cloudy:

article19_img8.png
Suddenly it is sunny:

article19_img9.png
Now, the point of the app is to spot this mistake and click on it. For this to happen, we need a button that will act as a hitarea on the video and that will be set to invisible (or better alpha set to 0). The button will appear on the mistake, as soon as the moment is over, the button will be removed.
Create a new MovieClip called “ErrorButton”.

article19_img10.png
Create a circle inside it with radius of 60 px.

article19_img11.png
That’s it. We are now ready with the assets and can move on to complete the actionscript.
Introducing the GameManager class
Like any other good game, MovieExpert has a class that manages the game, obviously it’s called GameManager. The class itself should be located in the classes.* folder of our project folder. Here is the source for GameManager.as:


package classes{
 
 	import classes.*;
 	import flash.events.*;
 
 	public class GameManager extends EventDispatcher {
  
  		protected var _uid:String = "";
  		protected var _level:String = "";
  		protected var _score:Number = 0;
  		protected var _errors:Array;
  		protected var _errNum:Number = 0;
  		protected var _errorsFound:Number = 0;
  		protected var _errorSaver = new ErrorSaver();
  		protected var _levels:Array;
  		protected var _levelCount:Number = 1;
  
  		public static var GAME_DATA_READY:String = "gameDataReady";
  		public static var VIDEO_SAVED:String = "videoSaved";
  		public static var GAME_SAVED:String = "gameSaved";
  		public static var LEVEL_PREPARED:String = "levelPrepared";
  		public static var GAME_OVER:String = "gameOver";
  
  		function GameManager() {
   			super();
   			_errorSaver.addEventListener(ErrorSaver.LEVEL_DATA_LOADED, setGameData);
   			_errorSaver.addEventListener(ErrorSaver.VIDEO_SAVED, videoSaved);
   			_errorSaver.addEventListener(ErrorSaver.SCORE_SAVED, scoreSaved);
   			_errorSaver.addEventListener(ErrorSaver.ERRORS_LOADED, errorsLoaded);
   		}
  
  		private function videoSaved(e:Event) {
   			dispatchEvent(new Event(GameManager.VIDEO_SAVED, true, true));
   		}
  
  		private function errorsLoaded(e:SuperEvent):void {
   			_errors = e.get("errors");
   			var i:Number = 0;
   			var len:Number = _errors.length;
   			for(i = 0; i < len; i++){
    				var err = _errors[i];
    				err.hint1 = unescape(err.hint1);
    				err.hint2 = unescape(err.hint2);
    				err.hint3 = unescape(err.hint3);
    			}
   			dispatchEvent(new Event(GameManager.LEVEL_PREPARED, true, true));
   		}
  
  		private function scoreSaved(e:Event):void {
   			
   			var i:Number = 0;
   			var len:Number = _levels.length;
   
   			for(i = 0; i < len; i++){
    
    				if(_levels[i] == _level){
     
     					if(_levels[i + 1]){
      
      						_level = _levels[i + 1];
      						_levelCount = i + 2;
      						loadNextLevel();
      
      					} else {
      
      						signalGameOver();
      
      					}
     					
     					break;
     
     				}
    
    			}
   			
   		}
  
  		private function loadNextLevel():void {
   			_errorsFound = 0;
   			_errorSaver.loadErrors(_level);
   			_errorSaver.updateLevel(_uid, _level);
   		}
  
  		private function signalGameOver():void {
   			dispatchEvent(new Event(GameManager.GAME_OVER, true, true));
   		}
  
  		private function setLevelCount():void {
   			var i:Number = 0;
   			var len:Number = _levels.length;
   			for(i = 0; i < len; i++){
    				if(_levels[i] == _level){
     					_levelCount = i + 1;
     					break;
     				}
    			}
   		}
  
  		//uid
  		public function set uid(i:String):void {
   			_uid = i;
   			_errorSaver.getUserData(_uid);
   		}
  
  		public function get uid():String {
   			return _uid;
   		}
  
  		//level data
  		public function setGameData(e:SuperEvent):void {
   			_level = e.get("level");
   			_score = Number(e.get("score"));
   			_errors = e.get("errors");
   			_levels = e.get("levels");
   			setLevelCount();
   			_errNum = _errors.length;
   			dispatchEvent(new Event(GameManager.GAME_DATA_READY, true, true));
   		}
  
  		//level
  		public function get level():String {
   			return _level;
   		}
  
  		//score
  		public function set score(e:Number):void {
   			_score = e;
   		}
  
  		public function get score():Number {
   			return _score;
   		}
  
  		//errors
  		public function get errors():Array {
   			return _errors;
   		}
  
  		//errorCount
  		public function get errorCount():Number {
   			return _errNum;
   		}
  
  		//levels
  		public function get levels():Array {
   			return _levels;
   		}
  
  		//levels
  		public function get levelCount():Number {
   			return _levelCount;
   		}
  
  		//uid
  		public function set errorsFound(e:Number):void {
   			_errorsFound = e;
   		}
  
  		public function get errorsFound():Number {
   			return _errorsFound;
   		}
  
  		public function saveError(mediaID:String, errors:Array):void {
   			_errorSaver.saveError(mediaID, errors, _uid);
   		}
  
  		public function save():void {
   			_errorSaver.saveScore(this.uid, this.score);
   		}
  
  	};
};
We do not have neither the time nor the space to examine the class in detail, we would need an extra article about it. For now, just save it as GameManager.as inside the classes folder.
The ErrorSaver class
The errorSave class is required to make calls to AMF and save the results in the database, call it from DB etc.


package classes{
 
 	import classes.*;
 	import flash.events.*;
 	import flash.net.*;
 
 	public class ErrorSaver extends EventDispatcher {
  
  		protected var gatewayUrl:String;
  		protected var gateway:NetConnection;
  
  		public static var VIDEO_SAVED:String = "videoSaved";
  		public static var LEVEL_DATA_LOADED:String = "levelDataLoaded";
  		public static var SCORE_SAVED:String = "scoreSaved";
  		public static var ERRORS_LOADED:String = "errorsLoaded";
  
  		function ErrorSaver() {
   
   			super();
   
   			gatewayUrl = "amfphp/gateway.php";
   			gateway = new NetConnection();
   			gateway.connect(gatewayUrl);
   
   		}
  
  		public function updateLevel(id, level):void{
   			var responder:Responder = new Responder(onUpdateLevel, onFault);
   			gateway.call("MBServices.updateLevel", responder, id, level);
   		}
  
  		public function saveError(id, errors, uid):void{
   			var responder:Responder = new Responder(onResult, onFault);
   			gateway.call("MBServices.saveError", responder, id, errors, uid);
   		}
  
  		public function getUserData(uid):void{
   			var responder:Responder = new Responder(onLevelResult, onFault);
   			gateway.call("MBServices.getUserData", responder, uid);
   		}
  
  		public function loadErrors(mediaID:String):void{
   			var responder:Responder = new Responder(onErrorsResult, onFault);
   			gateway.call("MBServices.loadErrors", responder, mediaID);
   		}
  
  		public function onErrorsResult(result) {
   			var e = new SuperEvent(ErrorSaver.ERRORS_LOADED, true, true);
   			e.add("errors", result);
   			dispatchEvent(e);
   		}
  
  		function onUpdateLevel(result) {
   			trace("Level updated...");
   		}
  
  		public function onLevelResult(result):void{
   			var e = new SuperEvent(ErrorSaver.LEVEL_DATA_LOADED, true, true);
   			e.add("level", result[0]);
   			e.add("score", result[1]);
   			e.add("errors", result[2]);
   
   
   
   			e.add("levels", result[3]);
   			dispatchEvent(e);
   		}
  
  		public function onScoreSaveResult(result):void{
   			dispatchEvent(new Event(ErrorSaver.SCORE_SAVED, true, true));
   		}
  
  		public function saveScore(uid:String, score:Number) {
   			var responder:Responder = new Responder(onScoreSaveResult, onFault);
   			gateway.call("MBServices.saveScore", responder, uid, score);
   		}
  
  		function onResult(result):void{
   			//greska...
   			if(result){
    				dispatchEvent(new Event(ErrorSaver.VIDEO_SAVED, true, true));
    			} else {
    			    trace("ErrorSaver::: Video could not be saved...");
    			}
   
   		}
  
  		function onFault(fault:Object):void{
   			trace('onFault invoked');
   			for (var prop in fault) {
    			    trace(prop + ": " + fault[prop]);
    			}
   		}
  
  	};
};
Make sure you save it as ErrorSaver.as in the classes.* folder.
Bringing it all together
So, right now we should have those important class files created: YouTubeLoaderPlus.as, ErrorSaver.as, GameManager.as and the PHP class file. First of all we need to know that the all the magic and all the gameplay will start from frame 1 of the fla file. So, open the fla from the previous article. We already have useful code on frame 1 that will be left there. It is the code that is needed to run the admin part. Here is what we have on frame 1 until now:

import classes.*;
import classes.choppingblock.video.*;

var videoHolder = new YouTubeLoaderPlus(); 
videoHolder.create();
addChild(videoHolder);

videoHolder.addEventListener(YouTubeLoaderPlus.PLAYER_READY, ytPlayerReady);
videoHolder.addEventListener(YouTubeLoaderPlus.VIDEO_READY, ytVideoReady);

function ytPlayerReady(e:Event):void{
 	videoHolder.loadVideoById("lPgxsGRpiuw");
}

function ytVideoReady(e:Event):void{
 	videoHolder.play();
}


var addVideoClip_mc = new AddVideoClip();
addChild(addVideoClip_mc);

addVideoClip_mc.addEventListener(AddVideoClip.VIDEO_BEING_ADDED, videoBeingAdded);
addVideoClip_mc.addEventListener(AddVideoClip.VIDEO_ID_ADDED, videoAddedForEditing);

function videoBeingAdded(e:Event):void{
 
 	videoHolder.stop();
 	videoHolder.visible = false;
 	
}

function videoAddedForEditing(e:SuperEvent){
 
 	var id = e.get("id");
 	videoHolder.visible = true;
 	videoHolder.editable = true;
 	videoHolder.loadVideoById(id);
 
}
Along with the class files we created in the last article, this is the fancy code that allow us to add own youtube videos. Pretty fancy 
Here is the revised code for frame 1:


import classes.*;
import classes.choppingblock.video.*;
import flash.utils.*;
import com.facebook.Facebook;
import com.facebook.net.FacebookCall;
import com.facebook.utils.FacebookSessionUtil;
import com.facebook.events.FacebookEvent;
import com.facebook.commands.feed.*;
import com.facebook.data.feed.*;
import com.facebook.data.feed.*;

stop();

//get the parameters from query string
var paramObj:Object = LoaderInfo(this.root.loaderInfo).parameters;

//create the video holder instance
var videoHolder = new YouTubeLoaderPlus();

videoHolder.create();
addChild(videoHolder);

//add all the listeners
videoHolder.addEventListener(YouTubeLoaderPlus.PLAYER_READY, ytPlayerReady);
videoHolder.addEventListener(YouTubeLoaderPlus.VIDEO_READY, ytVideoReady);
videoHolder.addEventListener(YouTubeLoaderPlus.ERROR_ADDED, videoErrorAdded);
videoHolder.addEventListener(YouTubeLoaderPlus.ERROR_SPOTTED, errorSpotted);
videoHolder.addEventListener(YouTubeLoaderPlus.CLICKED_FIRST_TIME, playerStarted);
	
//init the game manager and start a new game
var gameManager = new GameManager();
gameManager.addEventListener(GameManager.VIDEO_SAVED, errorsSaved);
gameManager.addEventListener(GameManager.LEVEL_PREPARED, levelPrepared);
gameManager.addEventListener(GameManager.GAME_OVER, gameOver);


gameManager.uid = String(paramObj["uid"]);

var errorCollector = new ErrorCollector();

function levelPrepared(e:Event):void{
 	
 	videoHolder.loadVideoById(gameManager.level);
 	videoHolder.setLevelData(gameManager.errors);
 	
 	
}

function playerStarted(e:Event):void{
 	//to do
}

//event that occurs when the youtube video is ready
function ytPlayerReady(e:Event):void{
 	videoHolder.loadVideoById(gameManager.level);
 	videoHolder.setLevelData(gameManager.errors);	
}

function ytVideoReady(e:Event):void{
 	videoHolder.play();
}

function gameSaved(e:Event):void{
 	videoHolder.loadVideoById(gameManager.level);
 	videoHolder.setLevelData(gameManager.errors);
}

function gameOver(e:Event):void{
 
}

function errorSpotted(e:Event):void{
 	
 	trace("You spotted the error...");
 	
}

function moveToNextLevel():void{
 	videoHolder.stop();
 	videoHolder.visible = false;
 	gameManager.save();
}

var addVideoClip_mc = new AddVideoClip();
addChild(addVideoClip_mc);

addVideoClip_mc.addEventListener(AddVideoClip.VIDEO_BEING_ADDED, videoBeingAdded);
addVideoClip_mc.addEventListener(AddVideoClip.VIDEO_ID_ADDED, videoAddedForEditing);

//event that occurs when the new video is submitted
function videoBeingAdded(e:Event):void{
 	videoHolder.stop();
 	videoHolder.visible = false;
 	cancel_btn.visible = true;
}

//video that occurs when completes the video submittal
function videoAddedForEditing(e:SuperEvent){
 	var id = e.get("id");
 	videoHolder.loadVideoById(id);
 	videoHolder.editable = true;
 	errorCollector.id = id;
 	save_btn.visible = false;
 	cancel_btn.visible = true;
}

//event that occurs when user adds the error
function videoErrorAdded(e:SuperEvent):void{
 	videoHolder.stop();
 	errorCollector.add(e.get("error"));
 	var num = errorCollector.length;
 	if(num > 0){
  		save_btn.visible = true;
  	}
}

//create the listeners
save_btn.addEventListener(MouseEvent.CLICK, saveRequested);
cancel_btn.addEventListener(MouseEvent.CLICK, cancelRequested);

//hide control buttons
save_btn.visible = false;
cancel_btn.visible = false;

//save the error from user
function saveRequested(e:MouseEvent):void{
 	gameManager.saveError(errorCollector.id, errorCollector.errors);
}

function cancelRequested(e:MouseEvent):void{
 	videoHolder.loadVideoById(gameManager.level);
 	videoHolder.setLevelData(gameManager.errors);
 	videoHolder.editable = false;
 	save_btn.visible = false;
 	cancel_btn.visible = false;
 	addVideoClip_mc.visible = false;
}

function errorsSaved(e:Event):void{
 	videoHolder.loadVideoById(gameManager.level);
 	videoHolder.setLevelData(gameManager.errors);
 	videoHolder.editable = false;
 	save_btn.visible = false;
 	cancel_btn.visible = false;
 	
 	addVideoClip_mc.visible = false;
}
The fun part
If we carefully followed the tutorial and made the adjustments, we should see the following result in the facebook canvas window.

article19_img12.png
The level loaded and this is a reason for us to (almost) throw a party!  The starts automatically with the first level which is the Simpsons intro. Now, somewhere in the middle of the intro, the magic should happen. We need to sport the mistake and the application needs to tells us that we are spotted the mistake. First of all, where is the mistake?
It’s in the garage:

article19_img13.png

article19_img14.png
Notice the broom in the garage. When Homer starts running, the broom disappears. That a goof! Right in the intro of the Simpsons, shame on them! :-)
So, press Play and when the moment occurs, click on the garage part in the video where the broom is missing. It should give the following output:

article19_img16.png
Yes! If that happens, then the heart and soul of the application is working. The main algorithm that allows the players to make the mistake in the move “clickable” is working fine. If this works on your side, then you have lots of reason to be proud of yourself since this is a great step toward a finished application.
What we need to do in the next installment is to complete the application with details like status messages, news publishing, level overviews, displaying of hints, logging into application, saving application state from session to session so the user does not need to start all over again.
Don’t panic if you did not understand everything well in this article. In the final article, we will talk about the details that will make this game complete. So don’t despair if something does not work. In the last article, I will also provide you the complete source code for the app that is currently running live. Also don’t forget to add your own clips with mistakes. 


To view the entire series please visit http://www.insideria.com/series-facebook-dev.html

Read more from Mirza Hatipovic. Mirza Hatipovic's Atom feed mirzahat on Twitter

Comments

1 Comments

Pavel said:

What prevents me from calling the amfphp method saveScore($uid, $score) with uid 13131323 of my friend, score 0? I would like to see some sample on how to make this app safe, I'm struggling with making secure facebook app for the "real world". Thanks.

Leave a comment


Tag Cloud

iPad

What's your take on the iPad? (Putting aside the Flash/iPad flame war)

Answer

Latest Features

Recommended for You

@InsideRIA on Twitter

Archives

  • Or, visit our complete archive.  

About This Site

Welcome to the premiere community site for all things RIA sponsored by O'Reilly Media and Adobe Systems Incorporated.