Accessibility

Insult Dueler: Building a Flash game on Adobe AIR

Jamie Kosoy

Senior Developer, Big Spaceship

Adobe AIR is a thrilling new frontier for anyone who is accustomed to building browser-based games and applications. To show off some of the cool features of Adobe AIR, I've built a fun little game inspired by a mini-game in the LucasArts series Monkey Island. I wrote the game in ActionScript 3 and created all of the animations on the Timeline in Flash CS3 Professional. I'll share with you some of the thinking that went into the design of the game, how I used Adobe AIR features such as the local database, custom chrome, file management, and cross-platform window control. Finally, I'll show you how to package and deploy the finished game to the desktop using the new Adobe AIR extension for Flash CS3 Professional.

Requirements

In order to make the most of this article, you need the following software and files:

Adobe Flash CS3 Professional

Adobe AIR

Adobe AIR Update for Flash CS3 Professional

Sample files:

Prerequisite knowledge

This article is best suited for intermediate to advanced ActionScript developers.

Designing the user experience

In the original game you play Guybrush Threepwood, a would-be pirate who must learn to become a great swordfighter if he's ever to realize his pirate dreams. In this case, however, the key to great sword fighting is in the mind, not in the hands, because the game is all about insults and comebacks—the fighter with the sharper tongue wins.

My version of the game is a blend of Rock-Paper-Scissors meets Memory meets Fencing. To begin, you select your character and your (computer) opponent (see Figure 1). Next, you select an insult from a list of insults to throw your opponent's way. If your opponent has an appropriate comeback, he will take the lead and throw an insult your way. Then it's your turn to select an appropriate comeback to reclaim the lead. If you select the wrong comeback, you lose. However, if you can hold your ground, you'll advance to higher levels, and will be able to unlock hidden characters. The final character in the game is a boss, who uses a special set of insults. (However, the same comebacks you've already learned will work against him. It's up to you to figure out which comeback goes with what insult.)

Tip: When playing the game, losing a lot in advance is key. Using wrong comebacks over and over will enable you to learn even more. When you meet the boss, your insults are useless to you. It all depends on the comebacks. Slow and steady wins the race.

Select your character.

Figure 1. Select your character.

Note: In Figure 1, Insult Dueler is running on top of the browser.

Adding chrome

One of the cool things about Adobe AIR is that you can choose whether you want your app to use the standard window chrome (which has the look and feel of any other application window on your Windows or Mac OS system) or custom chrome. I'm a custom man myself, so I chose to go the custom chrome route—which also meant that I was responsible for adding my own Close and Minimize buttons (the Maximize button is not necessary for this game).
Luckily, all of the necessary code is built into Adobe AIR. For example, if you want to add minimize functionality to a window, all you need to do is this:

	    someDisplayObject.stage.nativeWindow.minimize();

Another fun feature of custom chrome is that you can build an exotic-looking window with its own transparency (see Figure 1). For this game, I even added a slider to control the alpha of the backmost movie clip, so users can choose their own transparency level.

But wait! There's more!

Keeping track of the score (and more)

It's one thing to manipulate an app window—to move it around or make it semi-transparent. It's another matter entirely to remember the location and appearance of the application when the user quit it. To pull this off, you'll need to write a file to the user's hard drive that stores the x and y coordinates as well as the alpha value of the window. For this game, I also saved the win/loss record—to add replay value. I saved all this information in an XML file, since it's pretty easy to read. My default XML file looks like this:

<prefs>
<stagePosition x='0' y='0' alpha='1' />
<record wins='0' losses='0' />
</prefs>;

When the application first opens, it'll attempt to read the file like this:

var s:FileStream = new FileStream();
var f:File = File.applicationStorageDirectory;    // this is a reference to a predetermined directory. check the air api for more.
f = f.resolvePath(Constants.FILE_PREFS_XML);    // basically converts a String to something the File class will understand.
            
s.addEventListener(Event.COMPLETE,_onPrefsReadSuccess,false,0,false); 
s.addEventListener(IOErrorEvent.IO_ERROR,_onPrefsReadIOError,false,0,false);
        
// opens, reads and when complete fires Event.COMPLETE (aka our _onPrefsLoaded function).
s.openAsync(f,FileMode.READ);

That's it. Seven lines of code (minus comments) attempts to open a file. If it opens, you'll get _onPrefsReadSuccess; otherwise you get _onPrefsReadIOError. The IO error will realistically happen if the file is corrupt or if it doesn't exist at all—in either case, the game will simply begin to animate artwork. The success function is much more interesting:

private function _onPrefsReadSuccess($evt:Event):void
{
Out.status(this,"_onPrefsLoaded");
		    
// hurrah! I know this is a returning user because the prefs xml has been created. let's set the user back where they left off. 
		    
var xml:XML = new XML($evt.target.readUTF());
		    
_mc.stage.nativeWindow.x = xml.stagePosition.@x;
_mc.stage.nativeWindow.y = xml.stagePosition.@y;
		    
_wins = xml.record.@wins;
_losses = xml.record.@losses;
		    
Out.info(this,"Stage Position: " + _mc.x.toString() + " x " + _mc.y.toString());
		    
// prefs are set, now I can animate the screen in.
_animateIn();
};

The app moves the stage window position to the number described in XML. It also stores the _wins and _losses as variables. Then it proceeds to display the content. What happens, after a game is won or lost, or the window gets dragged around? Have a look:

private function _updatePrefs():void
{
Out.status(this,"_updatePrefs");
		    
// for updating, I'll do a synchronized file write. since this is a small file, it shouldn't require too much time to process.
var s:FileStream = new FileStream();
var f:File = File.applicationStorageDirectory;
f = f.resolvePath(Constants.FILE_PREFS_XML);
		    
// open the file here
s.open(f,FileMode.WRITE);
		    
// create a copy of the schema I'll use for preferences xml, so I'm not editing the schema itself.
var xml:XML = Constants.SAVED_PREFS.copy();
		    
// populate the data I need.
xml.stagePosition.@x = _mc.stage.nativeWindow.x;
xml.stagePosition.@y = _mc.stage.nativeWindow.y;
		    
xml.record.@wins = _wins;
xml.record.@losses = _losses;
		    
Out.debug(this,"Prefs written as follows:");
Out.debug(this,xml.toXMLString());
		    
// and finally I write.
s.writeUTF(xml);
s.close();
};

This time set FileStream to open the file as writable (the s.open() line). Then do the opposite of the read: Grab the saved variables (window position, wins and losses) and write the XML data as UTF bytes. Easy as pie.

Now that I have a cool custom application, I shall fill that window with my game.

Building the engine

As mentioned earlier, this game is all about insults and comebacks. As you progress you unlock content. Although this is an extraordinarily simple game, the key to realizing this is keeping my code organized. I don’t want to wade through a hundred lines of game code to determine when a user selects a character. I want my character selection to be separate from my game. That way, if there’s a bug with a character selection screen I know just where to look. I want all the major pieces of functionality divided in a way that will allow me to describe specific functionality.

I follow a very loose organizational model for my code. The engine that drives this game is broken up to serve three types of classes: screens, game data, and user interface (UI).

To tie it all together, I have a Document Root class called Main. This class instantiates (new MyClass()) all of the other classes and listens for any events. Main is the hub that tells the other classes how to work together. In addition, Main is responsible for any preliminary loading.

The Constants class (for the stuff that never changes)

The Constants class contains all of the permanent, never-changing variables needed for the game to work. This class contains the entire script for the game in array format. The INSULTS array looks like this:

public static const INSULTS   :Array = [ "This is the -END- for you, you gutter-crawling cur!",
"Soon you'll be wearing my sword like a shish kebab!",
"My handkerchief will wipe up your blood!",
"People fall at my feet when they see me coming.",
"I once owned a dog that was smarter than you.",
"You make me want to puke.",
"Nobody's ever drawn blood from me and nobody ever will.",
"You fight like a dairy farmer.",
"I got this scar on my face during a mighty struggle!",
"Have you stopped wearing diapers yet?",
"I've heard you were a contemptible sneak.",
"You're no match for my brains, you poor fool.",
"You have the manners of a beggar.",
"I'm not going to take your insolence sitting down!",
"There are no words for how disgusting you are.",
"I've spoken with apes more polite then you.",
"You're as repulsive as a monkey in a negligee.",
"You're the ugliest creature I've ever seen in my life!",
"I'll hound you night and day!"];

Every other class imports Constants. If I want to output "You fight like a dairy farmer.", I can type trace(Constants.INSULTS[7]).

I have a similar array called COMEBACKS and yet another called BOSS_INSULTS. I'm not going to give those away so easily, though—you'll have to play the game to find the comebacks.

To offer the user a clue as to when the computer answers with an incorrect comeback, I added an array of guaranteed wrong comebacks. These comebacks are always wrong. Use 'em and you'll lose every time.

public static const GENERIC_WRONG_COMEBACKS :Array = [ "I am rubber you are glue.",
  "Oh yeah?",
  "This is going to hurt, isn't it?",
  "Uh... chicken parm?",
  "Golly, that's a good one!",
  "Look behind you! A three-headed monkey!",
  
  // these next two are really, really cheesy. I'm sorry. I can't resist the urge to cheese it up.
  "I'm terrible at this. I'm going to quit swordfighting and learn ActionScript instead.",
  "Well, I'm going to lose this battle... but at least I'll lose it in a transparent window."
];

So… how do I tell which character is which?

Constants also contains an array called CHARACTER_DATA. CHARACTER_DATA is the single most important variable in the site. Each value in the array is an object with the following properties:

In code, it looks a little like this:

public static var KEY_BABAR:int = 0; // now I can reference Guybrush from Constants by using Constants.KEY_BABAR
		    
public static var CHARACTER_DATA :Array = [{
KEY: KEY_BABAR,
SWF: "babar.swf",
PORTRAIT: "babar.jpg",
SHORT_NAME: "Babar",
LONG_NAME: "Babar, Lord of the French Bulldogs”,
TEXT_FOREGROUND: 0x000000,
TEXT_BACKGROUND: 0xFFFFFF,
BOSS: false,
UNLOCKED: true },
];

All I need to do is add additional objects for all the other characters.

You might be wondering about Constants.KEY_BABAR. I know I can access all the Guybrush data from Constants.CHARACTER_DATA[0], so why bother with the extra variable? There are two reasons. First, to protect myself. If I accidentally write Constants.CHARACTER_DATA[1] somewhere, the site uses the wrong character data, creating a bug. However, if I accidentally type Constants.CHARACTER_DATA[Constants.KEY_BABARR] (with an extra R), that variable doesn't exist and the compiler will tell me there's an error. Way easier to diagnose.

The second reason is to make the code easier to read for other developers. Don't you hate looking at code like this:

var a1 = b26;
b26 += mClip352["stuff_" + temp];

To another programmer, this is a meaningless mess of letters and numbers. KEY_BABAR, although more to type, is more eloquent. Another programmer will see that and know right away that whatever is going on is happening specifically to Guybrush.

Managing game data

The insults, comebacks, and characters are all stored in arrays. Because of this structure, I know that all the scoring and game progress is entirely based around these arrays. Thus I'll know when a new insult or comeback appears, all I need to do is store the key somewhere for retrieval the next time the user opens the game. But how can I manage saving progress?

The Adobe AIR framework comes complete with SQLite support. Basically, this means I have a collection of classes that allows me to create and control a local database entirely from ActionScript. I use this database to do the following:

  1. Open the database, or create it if it doesn't exist. I'll have three tables: insults, comebacks, and unlocked characters.
  2. Request the database to give me all the known insults, comebacks, and unlocked characters.
  3. Write to the database—to the respective table—each time a new insult or comeback is learned, or a character is unlocked.

The only issue is that this whole SQLLite-create-and-control-my-own-database is all brand new. Which means… uh oh… new syntax. Scary! Deep breaths…

_sql = new SQLConnection();
_sql.addEventListener(SQLEvent.OPEN,_onSQLConnectSuccess,false,0,true);
_file = File.applicationStorageDirectory;
_file = _file.resolvePath(Constants.FILE_DATABASE);
_sql.openAsync(_file, "update");

… wait a minute. That's it?

Yep. Well, not quite. But that's the basics. In five lines of code, I say the following:

  1. Create a SQLConnection and prepare an event to handle when the app is connected to a database. In this case, it'll fire _onSQLConnectSuccess.
  2. Create a new file; I use the same File class I use for reading and writing my XML preference file. In this case, Constants.FILE_DATABASE is set to insultDueler.db.
  3. Open it.

Okay… so, what if the database file doesn't exist yet? How does Adobe AIR know to create it? The syntax above will automatically create the file if it doesn't exist. If you didn't want that, modify the code as follows (changes in bold):

_sql = new SQLConnection();
_sql.addEventListener(SQLEvent.OPEN,_onSQLConnectSuccess,false,0,true);
_sql.addEventListener(SQLErrorEvent.ERROR,_onSQLConnectFail,false,0,true);
_file = File.applicationStorageDirectory;
_file = _file.resolvePath(Constants.FILE_DATABASE);
_sql.open(_file,false);

Now the app will fire a SQLErrorEvent if it can't find the database file. And the _open method is passed a false, specifically telling it not to auto-create the database file. _onSQLConnectFail looks like this:

 private function _onSQLConnectFail($evt:SQLErrorEvent):void
{
Out.debug(this,"_onSQLConnectFail");
		    
_isFirstInit = true;
_sql.openAsync(_file, "create");
};

I set an _isFirstInit Boolean to true, which means I can open the database again (this time auto-creating it). I know it's the first time I'm opening it, so I'll set it to be the smallest database possible (512 is the smallest alloId) and set it so it doesn't automatically clean excess data (that's the false). Since I'm already listening for _onSQLConnectSuccess, that'll be my next stop.

private function _onSQLConnectSuccess($evt:SQLEvent):void
{
var statement:SQLStatement = new SQLStatement();
statement.sqlConnection = _sql;
		    
if(_isFirstInit) statement.text = "CREATE TABLE " + Constants.TABLE_INSULTS + "(id INTEGER PRIMARY KEY AUTOINCREMENT,key INTEGER)";
else statement.text = "SELECT * FROM " + Constants.TABLE_INSULTS;
		    
Out.debug(this,"_onSQLConnectSuccess: " + statement.text);
		     
statement.addEventListener(SQLEvent.RESULT,_onSQLInsults,false,0,true);
statement.execute();
};

So I've connected. Now I'll create a new SQL statement. I'll use this statement to tell the database what I need to do. If you've seen mySQL queries before, this should all look eerily familiar. If not, here's what I'm telling the database to do: If I just created the database, create a new table. This table will be named INSULTS (the variable I have in Constants). The table will keep track of two variables: An ID that automatically counts up as I add more to the database (this is a pretty standard practice) and a KEY, which will be a variable I pass. Otherwise, I'll grab all of the rows in the existing table ("SELECT * FROM…") and wait for the result. When I get that data, I just need to turn it into an Array:

_knownInsults = [];
var results:SQLResult = $evt.target.getResult();
if(results) for(var i:int=0;i<results.data.length;i++) { _knownInsults.push(results.data[i].key); }

When a new insult needs to be added to the database, I'll run this function:

 public function addToTable($table:String,$key:Number):void
{
var statement:SQLStatement = new SQLStatement();
statement.sqlConnection = _sql;
statement.text = "INSERT INTO " + $table + "(KEY) VALUES(" + $key + ")";
		    
Out.debug(this,"addToTable: " + statement.text);
		    
statement.execute();
};
		    
addToTable(Constants.TABLE_INSULTS,5); // this example would add a new row into my database. Assuming this is the first row ever, the ID will automatically be 1 and the KEY will be 5.

The next time the app opens, these same functions will run, populating my _knownInsults array—all before the user sees a thing!

Building the menu screen

The menu screen is the screen containing all of the different selectable characters. I tried to emulate the interface of games like Street Fighter or Mortal Kombat. When in the menu screen is loaded, the user will see a list of portraits. Any of the characters still locked will have their portraits replaced by a generic lock icon and cannot be selected.

The movie clip passed to it contains two frame labels: IN and OUT (see Figure 2). These are called to play when the screen is meant to animate in and out respectively. At the end of each of these timelines is an animation event, a simple custom event I use to represent event hooks; a milestone on the Timeline.

figure 2

Figure 2. The Timeline for the movie clip passed to the MenuScreen class.

In Figure 2, the ActionScript right before the OUT label looks as follows:

stop();
dispatchEvent(new AnimationEvent(AnimationEvent.ANIMATE_IN));

In my class, I map the event being dispatched (AnimationEvent.ANIMATE_IN) to a method. This code is very simple:

 public function animateIn():void
{
_mc.gotoAndPlay("IN");
_mc.addEventListener(AnimationEvent.ANIMATE_IN_START,_onAnimateInStart,
false,0,true);
_mc.addEventListener(AnimationEvent.ANIMATE_IN,_onAnimateIn,false,0,true);
};

When the Timeline reaches Frame 23 (the frame that dispatches AnimationEvent.ANIMATE_IN), _onAnimateIn will execute and I'll know the movie clip is ready to fly.

For example, each of those movie clips you see in Figure 2 is a menuClip. When _onAnimateIn is fired, I set all of these movie clips to enabled = true, allowing the user to click on them.

In addition, these movie clips contain ROLLOVER, ROLLOUT labels, just like the IN and OUT labels, except this time, I'm going to use them for mouse events. Hence ROLLOVER and ROLLOUT. As they come in, they'll fire their own event (MenuEvent), which MenuScreen is also listening to. It reacts like this:

private function _onMenuItemPortrait($evt:MenuEvent):void
{
Out.info(this,"_onMenuItemPortrait");
		    
// if the character is unlocked, display their portrait. otherwise, grab the "Locked" icon from the Library and display that.
$evt.menuClip.empty.addChild(Constants.CHARACTER_DATA[$evt.key].UNLOCKED ? _portraitsArray[$evt.key].content : new Bitmap(new LibraryItem_Lock(1,1)));
};
		    
private function _onMenuItemAnimateIn($evt:MenuEvent):void
{
_menuDictionary[$evt.menuClip] = Constants.CHARACTER_DATA[$evt.key];
		    
Out.info(this,"_onMenuItemAnimateIn");
		    
// again, if the character is unlocked their button is active. otherwise set their button disabled.
if(Constants.CHARACTER_DATA[$evt.key].UNLOCKED)
{
$evt.menuClip.btn.addEventListener(MouseEvent.CLICK,_onMenuItemClick,false,0,true);
$evt.menuClip.btn.addEventListener(MouseEvent.ROLL_OUT,_onMenuItemRollOut,false,0,true);
$evt.menuClip.btn.addEventListener(MouseEvent.ROLL_OVER,_onMenuItemRollOver,false,0,true);
}
		    
$evt.menuClip.btn.enabled = Constants.CHARACTER_DATA[$evt.key].UNLOCKED;
};

This translates as follows: When a menuClip is ready to display a portrait (the JPG loaded in from Main), attach it to a movie clip named empty. Then, when menuClip is fully displayed, if that menu clip's corresponding character data (found in Constants) is unlocked, listen for the mouse events so I know when the user rolls over, rolls out, and clicks. Otherwise, disable the button so the user can't possibly be confused into selecting a locked character.

When the user has selected a character, MenuScreen tells Main which user character and opponent character the game screen needs to display. This chain of events unfolds like this:

// this is in MenuScreen. Main is listening for ScreenEvent.GAME_START.
dispatchEvent(new ScreenEvent(ScreenEvent.GAME_START,userKey,opponent.KEY));
		    
// this is in Main. This fires when MenuScreen dispatches the ScreenEvent above. _curScreen is the GameScreen.
private function _onGameStart($evt:ScreenEvent):void
{
_curScreen = Constants.SCREEN_GAME;
_screens[_curScreen].onCharactersSelected($evt);
};

Building the game screen

Like MenuScreen, the GameScreen class has an IN and an OUT (see Figure 3).

The GameScreen class is passed a movie clip with a Timeline that looks like this.

Figure 3. The GameScreen class is passed a movie clip with a Timeline that looks like this.

Each layer contains one movie clip. The result layer will say YOU WIN! or YOU LOSE!, depending on the outcome of the game. Layers userTxt and opponentTxt are text fields that I'll colorize with the Constants.CHARARACTER_DATA hexadecimal colors. This helps the user identify which character is talking.

Layer choices contains the list of possible insults or comebacks a gamer can choose from (see Figure 4).

This is the choice Timeline.

Figure 4. This is the choice Timeline.

Note the container layer underneath the mask layer. That's where I'm going to add in movie clips. Each clip I add contains a button and a text field. The button is used to detect when that particular clip is clicked. The text field will be populated with an insult or a comeback from the Constants array. Remember, the user has to learn new insults. If I just looped through adding the entire list there'd be no constraints. No constraints, no fun.

Here's how it happens, step by step:

public function setGameData($knownInsults:Array,$knownComebacks:Array):void
{
_knownInsults = $knownInsults;
_knownComebacks = $knownComebacks;

Main passes a list of known insults and comebacks as arrays. Where do those arrays come from, you ask? Ah, tsk tsk. You're getting ahead of me! I'll reveal the answer to that shortly. These arrays are numbers, and these numbers correlate to the position in the Constants.INSULTS and Constants.COMEBACKS arrays. In other words, if Main passes that the user knew the insult, "You fight like a dairy farmer," and the comeback, "How appropriate, you fight like a cow," and I traced _knownInsults and _knownComebacks, the output would be [7],[7]. Get it? Everything is based off the arrays in Constants, and the arrays in Constants never change.

for(var iii:Number=2;iii>=0;iii--) { _knownInsults.unshift(iii);  }

Earlier I mentioned that the user would start with 3 insults. This loop makes sure that the knownInsults array begins with [0,1,2]. So including the dairy farmer insult you and I are pretending the user has already learned, the final known insults array is [0,1,2,7] and the final known comebacks array is [7].

_knownInsultsObject = {};
_knownComebacksObject = {};
		    
for(var i:Number=0;i<_knownInsults.length;i++) { _knownInsultsObject["key_" + _knownInsults[i].toString()] = true; }
for(var ii:Number=0;ii<_knownComebacks.length;ii++) { _knownComebacksObject["key_" + _knownComebacks[ii].toString()] = true; }
};

Oh, a curveball! What on earth is an object doing in the middle of a game based around arrays? Remember when I talked before about excess for loops? Here's where it gets tricky.

I want my game to run fast. That’s a pretty reasonable demand, right? Right. In order for it to run fast, I know I need to avoid any sort of for or while looping. The less looping, the less math. The less math, the faster the game goes.

The problem is that to this point I’m using Arrays to house all of my data. Arrays are a cluster of variables (or values) organized by a numeric key. Arrays are especially useful when you need to access data in some sort of ordered fashion. By that, I mean if you know you'll need one item, then the next item (such as, say, a list of characters or insults), and the next and so on, arrays are wonderful. The problem is that unless you explicitly know the key you need to work with, the only way to figure it out is to loop through the entire array until you find the corresponding data. So I know I’ve got this:

_knownInsults == [0,1,2,7]

Let’s pretend the computer insults the user with the dairy farmer insult—insult Number 7. How can I tell if I need to save this to the database or not? I can loop through _knownInsults every round, but then I’ve got a for loop every round. That’s slow. That’s bad.

Instead, I create an object: _knownInsultsObject. I loop through one time—at the beginning of the game. This object is based on the array, so _knownInsultsObject will look like this:

_knownInsultsObject == { key0: true, key1: true, key2: true, key7: true };

Now I have a way of checking if a key is known or not without looping through to see if it already exists in _knownInsults. Consider this:

someTestVar = 7;   // blah blah blah dairy farmer.
if(_knownInsultsObject["key" + someTestVar.toString()) { /* I already know this insult */ };

This is lightweight. This is flexible. I have less looping; thus I have a faster game. And I get the exact same result.

The final, complete initialize function is thus:

public function setGameData($knownInsults:Array,$knownComebacks:Array):void
{
_knownInsults = $knownInsults;
_knownComebacks = $knownComebacks;
		    
// always add the first 3 insults, so the user has something to start with.
for(var iii:Number=2;iii>=0;iii--) { _knownInsults.unshift(iii);  }
		    
// let's organize the data in yet another way.
// this time, I'll use an object. I'll store all the known insults and comebacks.
  
_knownInsultsObject = {};
_knownComebacksObject = {};
		    
for(var i:Number=0;i<_knownInsults.length;i++) { _knownInsultsObject["key_" + _knownInsults[i].toString()] = true; }
for(var ii:Number=0;ii<_knownComebacks.length;ii++) { _knownComebacksObject["key_" + _knownComebacks[ii].toString()] = true; }
};

Now that the program knows what insults to pick from, let's create the menu. When the choice timeline (see Figure 3) finishes animating in, it dispatches an event (AnimationEvent.ANIMATE_IN) that GameScreen is waiting for. Let's step through that:

 // choices for the user
private function _onChoicesAnimateIn($evt:AnimationEvent):void
{
Out.info(this,"_onChoicesAnimateIn");
// if I'm a user round then I'll add insults. otherwise I'll use comebacks.
var known:Array = _isUserRound ? _knownInsults : _knownComebacks;
var actual:Array = _isUserRound ? Constants.INSULTS : Constants.COMEBACKS;

_isUserRound switches from true to false after each parry animation, as per our game rules. It's true to start, so let's pretend that it's true right now. That means array known will be _knownInsults, and array actual will be Constants.INSULTS. Okay?

 answerDictionary = new Dictionary(true);
for(var i=0;i<known.length;i++)
{
// I always start linked clips "LibraryItem_". You'll find this clip in the library.
clip = new LibraryItem_GameOption();
clip.y = (i * clip.height) + (i * 5);
            
// what is that dictionary for? hmm, hmm. mysterious.
__answerDictionary[clip] = known[i];

So now I run a for loop for every known insult. Each insult creates a new LibraryItem_GameOption(). This is the ActionScript 3 equivalent of attachMovie() – the GameOption clip is in the Library and will be added to the Display List.

Now, what is this business about new Dictionary()? A Dictionary is the secret brother of Array and Object. As I discussed earlier, arrays have numeric keys and objects have string keys. Dictionaries have objects as keys. Whoa. Heavy. For more on Dictionaries, read Grant Skinner's related post.

In this case, the key is the clip I just added to the Display list. The value is the number of the known insult. So:

_answerDictionary[(first GameOption)] = 0;
_answerDictionary[(first GameOption)] = 1;
_answerDictionary[(first GameOption)] = 2;
_answerDictionary[(first GameOption)] = 3;

When the GameOption clip is triggered, I'll immediately be able to use _answerDictionary[$evt.target.parent] (I'll be adding the listener to a button nested inside the GameOption clip) to figure out which insult was chosen.

 _mc.choices_mc.empty.addChild(clip);
clip.txt_mc.txt.text = actual[known[i]];
		    
// add our roll and click events
clip.btn.addEventListener(MouseEvent.ROLL_OUT,_onChoiceOut,false,0,true);
clip.btn.addEventListener(MouseEvent.ROLL_OVER,_onChoiceOver,false,0,true);
clip.btn.addEventListener(MouseEvent.CLICK,_onChoiceClick,false,0,true);
}

That's all I need. I've got each clip positioned, now I just need to wait for the user to click.

private function _onChoiceClick($evt:MouseEvent):void
{
_lastUserChoiceSelected = _answerDictionary[$evt.target.parent];
Out.debug(this,"Choice Selected: " + _lastUserChoiceSelected.toString());
_mc.choices_mc.gotoAndPlay("OUT");
};

I save the choice—the number of the insult selected, 7 (for "You fight l like a dairy farmer.") —as _lastUserChoiceSelected. And then I tell the choices movie clip to go away. Easy enough. So the choices movie clip animates away (the OUT label), and GameScreen is waiting for it to dispatch an event.

private function _onChoicesAnimateOut($evt:AnimationEvent):void
{
if(_isUserRound) _mc.userTxt_mc.glow_mc.txt.text = Constants.INSULTS[_lastUserChoiceSelected];
else
{
mc.userTxt_mc.glow_mc.txt.text = Constants.COMEBACKS[_lastUserChoiceSelected];
}
            
// put the text on the screen for a short while, then let the opponent respond.
var t:Timer = new Timer(_mc.userTxt_mc.glow_mc.txt.text.length * Constants.READ_SPEED);
t.addEventListener(TimerEvent.TIMER,_onUserTimer,false,0,true);
t.start();
};

The new Timer class is useful in place of setInterval. In this case, I write the text from the INSULTS array ("… dairy farmer") onto the screen. Finally. Then it stays up for a short while.

Now it's the computer's turn to make a comeback.

Creating a worthy opponent

Artificial intelligence for games is never about creating a realistic replica of what a user would do. It's about creating the illusion of a realistic replica of what a user would do. As simply as possible. In this case, the computer needs to be smart enough to occasionally spit the correct comeback at the user. But it also needs to be dumb enough to mess up. Otherwise the user would never be able to win, which doesn't sound like a very fun game to me.

For insults, this is easy enough—pick a random insult, remember its index (just like _lastUserChoiceSelected) and display the text on the screen. For comebacks it's only slightly trickier:

var shouldOpponentBeCorrect:Boolean = _(_random(0,2) == 0);
		    
if(shouldOpponentBeCorrect)
{
_lastOpponentChoiceSelected = _lastUserChoiceSelected;
_mc.opponentTxt_mc.glow_mc.txt.text = Constants.COMEBACKS[_lastOpponentChoiceSelected];
}
else
{
_lastOpponentChoiceSelected = -1;
_mc.opponentTxt_mc.glow_mc.txt.text = Constants.GENERIC_WRONG_COMEBACKS[_random(0,
Constants.GENERIC_WRONG_COMEBACKS.length-1)];
}
		    
var t:Timer = new Timer(_mc.opponentTxt_mc.glow_mc.txt.text.length * Constants.READ_SPEED);
t.addEventListener(TimerEvent.TIMER,_onOpponentTimer,false,0,true);
t.start();

_random() is a utility function I created to pick a random number between a minimum and maximum range. In this case, the minimum is 0, the maximum is 2. shouldOpponentBeCorrect is a Boolean set to true only if that random number is 0. In other words, roughly 33% of the time the computer will answer with the correct insult. Then I display the text on the screen for the opponent the exact same way I did for the user. That's all. Artificial Intelligence indeed.

A winner is you.

Suppose the gamer has chosen "You fight like a dairy farmer." I know that translates to Insult #7 in my array, counting from 0. I have this saved as _lastUserChoiceSelected. I know the computer has just selected a comeback, the index of which I have stored as _lastOpponentChoiceSelected.

Scoring is as simple as this:

// award 1 point to whoever won the round
if(_lastOpponentChoiceSelected == _lastUserChoiceSelected)
{
if(!_knownComebacksObject["key_" + _lastOpponentChoiceSelected.toString()] && !Constants.CHARACTER_DATA[_opponentKey].BOSS)
{
_knownComebacks.push(_lastOpponentChoiceSelected); // now I can use this comeback!
_knownComebacksObject["key_" + _lastOpponentChoiceSelected.toString()] = true;
dispatchEvent(new DatabaseSaveEvent(DatabaseSaveEvent.SAVE,_lastOpponentChoiceSelected,
Constants.TABLE_COMEBACKS));    // that's it. Main will ask the Database to save the comeback.
}
_score--;
}
else _score++;
		    
_userMC.gotoAndPlay("PARRY");
_opponentMC.gotoAndPlay("PARRY");

Add to _score if the user won the round, subtract if the opponent did. In addition, if the user lost the round but didn't know the comeback (which I can check from _knownComebackObjects), add that comeback to the _knownComebacks array. Now the user can use that comeback.

The next round is the same flow reversed:

  1. Opponent selects a random insult. Text displays, _lastOpponentChoiceSelected is saved.
  2. Choices animate in. Once in, populate the choices movie clip with LibraryItem_GameOptions, this time with the text of all known comebacks.
  3. User selects. The choices movie clip animates out. Now print the user's text on the screen.
  4. Compare scoring. Display the two characters "PARRY" animation.
  5. Repeat until score >= 2 (user wins) || score <= -2 (user loses).
  6. Save all that stuff to the hard drive. The user, content with the thrill of victory (or the frustrating of defeat) closes the game. 10 minutes later, they need to quench the thirst for cheesy 8 bit sword fighting once more. The user opens the game up again, which reads the XML file and SQL data written out to their hard drive earlier and picks up right where they left off.

Detecting Adobe AIR

Detecting Adobe AIR is essential for several reasons. First, a Flash app assumes it's been built for the browser. If you try to use an Adobe AIR method when not in Adobe AIR, your application will break. At Big Spaceship I built a simple utility class which I distribute called Environment. In it, I have a static function that returns a Boolean: true if this is in Adobe AIR, false if not:

public static function get IS_IN_AIR():Boolean {
return Capabilities.playerType == "Desktop";
};

IS_IN_AIR enables you to detect this pitfall at runtime, so your Adobe AIR methods only happen when needed.

In addition sometimes the flow of testing in the IDE is different than in final deployment. For example, in the IDE you can't create an XML file if it doesn't already exist. If you can detect that you're not in AIR, then you can program a way to bypass loading an XML file you know won't be there.

Packaging and deploying the game

So my game works. Now I need to get it out the world. How do I get it out to the world? (Write a tutorial for Adobe, that’s how.)

I built my game with the SWF file set to export for Flash Player 9. With the Adobe AIR extension in Flash CS3, I can change this setting to Adobe AIR right here. But I’ve got a nifty idea.

The AIR extension in Flash CS3 will export an AIR file directly. But then in order to test the file, I have to install the application and see if it’s running correctly. I’m a speed demon. I want to test in the IDE first, and then package for Adobe AIR. So what I did was set my main.fla—the the file that contains all the game mechanics and artwork—to export a normal, regular SWF. Now I can see the game immediately. My Environment.IS_IN_AIR Boolean is coming up false (since I’m not in Adobe AIR), but that’s okay. I can see the game work and diagnose bugs quickly.

Note: Why did I bother with a separate FLA for Adobe AIR, when I could just set main.fla to export as such? proxy.fla is my know-all, end-all AIR FLA file, that's why. It loads in main.swf, but doesn't actually care what main.swf does. I can reuse it over and over for porting SWF files to Adobe AIR. This gives me the flexibility to build my application for the browser and the desktop simultaneously! Environment.IS_IN_AIR will be false in the browser, so my app won't even bother to try to write to the file system. Talk about standards, right?

So okay, I’ve gotten the game pretty tight. Now it’s time to test in AIR. I created proxy.fla for just this moment. proxy.fla is the exports as Adobe AIR, and the only thing it does is load in main.swf. The proxy.fla file’s Document class reads like this:

package com.kosoy.insultdueler
{
// native AS3
import flash.net.URLRequest;
		    
import flash.display.Loader;
import flash.display.MovieClip;
	    
// AIRProxy is the Document Root class of proxy.fla.
public class AIRProxy extends MovieClip
{
public function AIRProxy()
{
var l:Loader = new Loader();
addChild(l);
l.load(new URLRequest("main.swf"));
};
};
}

proxy.swf loads in main.swf. Thus main.swf is in Adobe AIR. Even better, I could actually reuse proxy.fla for any SWF file I want to convert to Adobe AIR for the rest of my life. It’s super modular. I just need to name the SWF file main.swf.

It is extraordinarily difficult to test in Adobe AIR. It’s horrible. Painful. Don’t even try it.

I test the movie as I normally would. Instantly things are wildly different. My application lives! It’s got the pretty custom chrome, the transparency, the Minimize and Close buttons work, it even remembers my win-loss record and alpha and…

Wait a minute. This isn’t a real application yet. I haven’t packaged it and installed it. Where is it saving the database and the XML file to? Wherever proxy.fla is located, that’s where. This is great! Now I can open the XML file and see what is actually saved! I can see all the files coming to life. I didn’t have to do a single thing differently!

Okay, so I’ve tested in the IDE and I’ve tested in Adobe AIR. Now at long last I’m ready to build the final AIR file. Like I mentioned, the proxy FLA file is set to export to Adobe AIR. By selecting Commands > AIR – Application and Package Settings I can set everything I need to create an AIR file. In this case, I added the media and swf directories, set some custom icons and add a little bit of copy information. The end result looks like Figure 5.

Insult Dueler Application and Package Settings

Figure 5. Insult Dueler Application and Package Settings

Where to go from here

ActionScript 3.0 is a big step forward for Flash developers. For the first time, we have a "mature" programming language capable of reading any kind of data we can imagine. Adobe AIR takes things a step further, enabling writing all of that data to the desktop. For the first time, Flash designers and developers can start to take their expertise to the desktop.

Adobe AIR empowers developers to build applications of incredible complexity with ease. In one fell swoop I'm reading and writing an XML file, adding and reading data from a SQL database and building a cross-platform game. And all I had to learn was a tinsy bit more ActionScript 3.0. I even built in my own custom icons!

Oh, and in case anybody is wondering, as of this writing my win-loss record is 12 to 20. You would think I'd be better at this game, seeing how I'm the one that built it. Ah well. Only one way to get better, right? Just keep playing and playing and playing and playing…

If you want to learn more about ActionScipt 3.0, Grant Skinner and Senocular have blog-tomes filled with wonderful knowledge. The ActionScript 3 Language Reference is an obvious necessity for wading through the new syntax. And of course, Big Spaceship Labs is a wonderful resource from some of the finest developers on the planet.

For more inspiration, check out the sample apps in the Adobe AIR Developer Center for Flash, Flex, and HTML/Ajax.

To get started building Flash apps on Adobe AIR go to the Getting Started section of the Adobe AIR Developer Center for Flash or dig into Developing Adobe AIR Applications with Adobe Flash CS3.

About the author

Jamie Kosoy longs for a championship parade for a Philadelphia sports team. To distract himself from the endless agony, he works as a senior developer at Big Spaceship building world-class interactive experiences for clients like Nike, Coca-Cola, Stouffer's, and Sony Pictures. He resides in Brooklyn, New York, with his French bulldog. You can find Big Spaceship's development team blog at http://labs.bigspaceship.com.