Archive for the ‘AS3’ Tag
Quick XML attributes parsing
Scenario: you’re loading XML data where the bulk of the fields are stored as node attributes. You now want to transfer these node attributes onto typed objects so that you’ll have compile-time validation of your data integration.
One solution here is to laboriously write an accessor for every XML attribute that sets the value to the corresponding class field… hmmm. Tedious. So, I’ve turned to using a quick solution that works like a charm:
function parseAttributes($obj:Object, $atts:XMLList):Object
{
for (var j:int=0; j < $atts.length(); j++)
{
var $prop:String = $atts[j].name();
if ($obj.hasOwnProperty($prop))
{
if ($obj[$prop] is Boolean) $obj[$prop] = ($atts[j] == "1" || $atts[j] == "true");
else $obj[$prop] = $atts[j];
}
}
return $obj;
}
An implementation that copies all XML attributes over to a class object looks like this:
var $xml:XML = <myNode name="greg" job="flash"/>; var $myObj:MyObject = new MyObject(); parseAttributes($myObj, $xml.@*);
The parseAttributes() method receives a object class instance and an XMLList of attributes. It then runs through the XMLList and attempts to set each XML value on the object if a corresponding property exists on the class. Of course, this begs the question of datatyping… How are XML attribute strings set as numeric properties, right? Well, apparently this is a very handy built in feature of the E4X XML parser in ActionScript3. While I’ve never found explicit documentation saying that E4X automatically converts data types, it seem to work perfectly. E4X apparently checks the recipient property’s datatype, and then parses the XML field accordingly.
That said, here are my observations: [int] works (“5″ == 5), [Number] works (“5.321″ == 5.321), and [uint] works (“0xFF0000″ == 0xFF0000). The one exception here seems to be [Boolean]. E4X appears to evaluate all Boolean recipients as “true”, even if the XML value is “0″ or “false”. So, I’ve written a specific catch into the above method to handle Boolean cases. If the recipient property is a Boolean, parseAttributes() will only evaluate the XML field as being true for a value of “1″ or “true”.
onReleaseOutside() in AS3
Some AS2 developers probably noticed quickly that our old friend, the onReleaseOutside handler, does not have an ActionScript3 counterpart. While that old handler’s uses were limited, it played an essential role in AS2 drag-and-drop implementations since a rolled-out cursor still needs to call back to the drag target to disable dragging.
Well, the onReleaseOutside event is now obsolete thanks to AS3′s new event model; so I don’t fault Adobe in the least bit for getting rid of it. However, this is yet another case of point where ratified ActionScript methodology has gone undocumented. If Adobe does have one fault with AS3, it’s that they assume these crossovers are obvious and intuitive to the average Flash tech. And that, unfortunately, has been extremely alienating to Flash’s single biggest user-base.
Anyhow, let’s get back to the issue at hand: how do we manage an onReleaseOutside event in AS3? Quite simply, we listen to the stage for ANY MOUSE_UP event. Here’s what a simple drag-and-drop script looks like in AS3:
myClip.addEventListener(MouseEvent.MOUSE_DOWN, this._onMousePress);
function _onMousePress($event:MouseEvent):void {
stage.addEventListener(MouseEvent.MOUSE_UP, this._onMouseRelease);
myClip.startDrag();
}
function _onMouseRelease($event:MouseEvent):void {
stage.removeEventListener(MouseEvent.MOUSE_UP, this._onMouseRelease);
myClip.stopDrag();
}
A* Pathfinding With AS3
When I earned my degree as a designer, I never thought I’d end up writing a blog post about AI. However, I took a crack at writing an A* (A-star) pathfinding algorithm about five years ago for the avatar movement system in the first generation of the Lassie Engine (built using Macromedia Director and Lingo). My entire source of reference for that first implementation was The Beginner’s Guide to Pathfinding Algorithms. If you’re interested in pathfinding theory, that article is an absolute gold mine. I won’t even pretend that I could do a better job of explaining the algorithm’s nuts and bolts, so I’ll just stick to what an AS3 implementation looks like.
First, we need a data structure of a grid to run searches across. I’m generally drawn to XML for raw data structures like this since it’s easy to compose and maintain by hand. Here’s a simple XML grid of points:
Save as: grid.xml
<nodes> <node id="n1" x="25" y="25" join="n2,n3"/> <node id="n2" x="110" y="110" join="n1,n3,n4"/> <node id="n3" x="50" y="180" join="n1,n2,n5,n6"/> <node id="n4" x="225" y="90" join="n2,n5,n6"/> <node id="n5" x="190" y="160" join="n3,n4"/> <node id="n6" x="230" y="170" join="n3,n4"/> </nodes>
The above XML creates a grid that looks like this:

That XML structure is extremely simple. Each node is given an ID, an X and Y coordinate, and has a comma-separated list of other node ID’s that it connects to.
Next, we move on to the simple geometry structures that we’ll feed into our pathfinder. First is the Node object. While we’ll load our grid data as XML, we’ll want to parse that into typed Objects to give our implementation compile-time validation. So, we’d load our XML and then parse the nodes into this structure:
Save as: com/lassie/player/geom/Node.as
package com.lassie.player.geom
{
import flash.geom.Point;
public final class Node extends Point
{
// public
public var id:String = "";
// private
private var _neighbors:Array;
public function Node(key:String="", x:int=0, y:int=0):void
{
super(x, y);
id = key;
_neighbors = new Array();
}
/*
* Returns all current data as a new Node object.
*/
public function cloneNode():Node
{
var node:Node = new Node(id, x, y);
node.parseNeighbors(_neighbors.join(","));
return node;
}
/*
* Parses a list of comma-seperated node id's into the array of neighbors.
*/
public function parseNeighbors(csv:String):void
{
for each (var j:String in csv.split(",")) addNeighbor(j);
}
/*
* Adds a new neighbor Id.
*/
public function addNeighbor(id:String):void
{
if (!containsNeighbor(id) && id != "") _neighbors.push(id);
}
/*
* Gets a neightbor Id by numeric index.
*/
public function getNeighbor(index:int):String
{
if (index >= 0 && index < _neighbors.length) return _neighbors[index];
return null;
}
/*
* Gets the number of neighbors assigned to this node.
*/
public function get numNeighbors():int
{
return _neighbors.length;
}
/*
* Tests if the specified node id is among this node's neighbors.
*/
public function containsNeighbor(id:String):Boolean
{
return _neighbors.indexOf(id) > -1;
}
/*
* Appends an additional namespace key onto all Node references. Avoids conflicts during Grid unions.
*/
public function expandNamespace(key:String):void
{
id += key;
for each (var j:String in _neighbors) j += key;
}
/*
* Trace object to string.
*/
public override function toString():String
{
return "[Node] id:"+ id +", x:" + x + ", y:" + y + ", neighbors:(" + _neighbors + ")";
}
}
}
Now we need a Path object. Paths will be the actual search routes that our A* algorithm will create, branch, prioritize, and purge as it works. Here’s the Path class:
Save as: com/lassie/player/geom/Path.as
package com.lassie.player.geom
{
public final class Path
{
// public
public var length:int = -1;
public var bestCase:int = -1;
public var nodes:Array;
// private
private var _path:Array;
public function Path($length:Number=-1, $bestCase:Number=-1, $path:Array=null):void
{
length = $length;
bestCase = $bestCase;
_path = ($path != null) ? $path : new Array();
}
public function destroy():void
{
_path = null;
nodes = null;
}
/*
* Returns all current data as a new Path object.
*/
public function clone():Path
{
return new Path(length, bestCase, _path.slice());
}
/*
* Tests if path has been initialized with actual pathing data.
*/
public function get hasLength():Boolean
{
return length + bestCase >= 0;
}
/*
* Returns the last node id contained within the path.
*/
public function get lastElement():String
{
return _path.slice(-1)[0];
}
/*
* Tests if this path contains a node Id.
*/
public function containsNode($id:String):Boolean
{
return _path.indexOf($id) > -1;
}
/*
* Adds a node to the path if not already present.
*/
public function addNode($id:String):void
{
if (!containsNode($id)) _path.push($id);
}
/*
* Trace object to string.
*/
public function toString():String
{
return "[Path] length:"+ length +", nodes:("+ _path +")";
}
}
}
And finally we need a grid class. A Grid will hold Nodes, keyed by their respective Id’s, and will have the actual A* implementation within it to do the heavy lifting of grid searches.
Save as: com/lassie/player/geom/Grid.as
package com.lassie.player.geom
{
import flash.geom.Point;
public final class Grid extends Object
{
private var _nodes:Object;
public function Grid():void
{
super();
_nodes = new Object();
}
/*
* Parses an XML structure into the grid object.
*/
public function parseXML($xml:XML):void
{
// loop through all <node> XML items
for each (var $nodeXML in $xml.children())
{
// create a new Node object for each XML item
var $node:Node = new Node($nodeXML.@id, $nodeXML.@x, $nodeXML.@y);
$node.parseNeighbors($nodeXML.@join);
// register node by Id within the grid
_nodes[$node.id] = $node;
}
}
/*
* Gets a node object by ID name
*/
public function getNodeById($id:String):Node
{
if (_nodes[$id] != undefined) return _nodes[$id] as Node;
return null;
}
/*
* Finds the shortest path between two nodes.
*/
public function findPath($startId:String, $goalId:String):Path
{
// queue of paths to search
var $stack:Array = new Array(new Path(0, 0, [$startId]));
// best path to goal
var $best:Path = new Path();
// shortest distance to each node reached thus far
var $reachedNodes:Object = new Object();
// cycle counter (for debug and optimization)
var $cyc:int = 0;
// UNTIL ALL SEARCH PATHS HAVE BEEN ELIMINATED
while ($stack.length > 0)
{
// pull the first path out from the search stack
var $searchPath:Path = $stack.shift() as Path;
// pull the last node element from the path
var $searchNode:Node = getNodeById($searchPath.lastElement);
// EXTEND PATH ONE STEP TO ALL NEIGHBORS
// creating X new paths
for (var j:int=0; j < $searchNode.numNeighbors; j++)
{
// Branch: duplicate current search path as a new branch
var $branch:Path = $searchPath.clone();
// Pull and expand upon each of searchNode's neighbor Id's.
var $expandNode:String = $searchNode.getNeighbor(j);
// REJECT PATHS WITH LOOPS
// if branch does NOT already contain next search node
if (!$branch.containsNode($expandNode))
{
// get coordinates of previous, current, and goal nodes
var $prevCoord:Node = getNodeById($branch.lastElement);
var $currentCoord:Node = getNodeById($expandNode);
var $goalCoord:Node = getNodeById($goalId);
// extend branch after having referenced last end-point.
$branch.addNode($expandNode);
// UPDATE BRANCH LENGTH AND UNDERESTIMATE
$branch.length += Point.distance($prevCoord, $currentCoord);
$branch.bestCase = $branch.length + Point.distance($currentCoord, $goalCoord);
// TRACK SHORTEST DISTANCE TO EACH NODE REACHED
// attempt to pull a distance-to-node from the register of reached nodes.
// if node has not yet been reached, register it with the current branch length.
var $shortest:Number = $reachedNodes[$expandNode];
if (isNaN($shortest)) $shortest = $branch.length;
// TEST IF PATH IS WORTH KEEPING (keep if:)
// - if branch is as short or shorter than the best distance to the current expansion node
// - and if a best-path has not been found yet, OR if this branch's best case scenario is still shorter than the best path.
if ($branch.length <= $shortest && (!$best.hasLength || $branch.bestCase < $best.length))
{
// log as best path to current search node
$reachedNodes[$expandNode] = $branch.length;
// If the expansion node is the goal, save branch as the parth to beat.
// Otherwise, add the branch back into the search stack.
if ($expandNode == $goalId) $best = $branch;
else $stack.push($branch);
}
}
}
// PRIORITIZE SEARCH PATHS
// sort paths by best-case scenario so that most likely paths are searched first.
var $priority:Function = function($a:Path, $b:Path):int
{
if ($a.bestCase < $b.bestCase) return -1;
else if ($a.bestCase > $b.bestCase) return 1;
else return 0;
}
$stack.sort($priority);
$cyc++;
}
return $best;
}
}
}
Now let’s put all that together. Create a new Flash document in the root of your class path and put that “grid.xml” file in with it. Then in frame-1 actions of the root timeline, just add the following:
import com.lassie.player.geom.Grid;
var $xmlLoader:URLLoader = new URLLoader();
$xmlLoader.addEventListener(Event.COMPLETE, this._onXMLLoaded);
$xmlLoader.load(new URLRequest("grid.xml"));
function _onXMLLoaded($event:Event):void
{
// retrieve the loaded XML data
var $xml:XML = new XML($event.target.data);
// create a new grid and parse the loaded XML into it.
var $grid:Grid = new Grid();
$grid.parseXML($xml);
// find a path between two node Id's
trace($grid.findPath("n1", "n5"));
trace($grid.findPath("n1", "n6"));
}
If you run that script as is, you should get the following output:
[Path] length:298, nodes:(n1,n3,n5) [Path] length:316, nodes:(n1,n2,n4,n6)
Those are traces of two separate path searches run across the grid. The first search connected n1 and n5 to one another through their common neighbor of n3. The second search took a dramatically different route from n1 to n6 that branched through n2 and n4. Why? Because it was slightly shorter to run through two connections rather take the single connection through n3.
More on saving images out of Flash
I posted previously on generating a bitmap image of a MovieClip and writing it from Flash to a remote server. However, that previous post was a very early. Now that I’ve implemented the process within an actual project scenario, I’ll follow up here with some revised code that streamlines the task with greater efficiency.
1) Server-side. First thing first: you’ll need a PHP script (or other server-side flavor) to receive binary image data and write it to a file. This revised PHP script is even simpler than before; just save this to a PHP file and place it on a PHP enabled server:
<?php $file = $_GET['file']; $fp = fopen( $file, 'wb' ); fwrite( $fp, $GLOBALS[ 'HTTP_RAW_POST_DATA' ] ); fclose( $fp ); echo $file; ?>
2) Image encoders. Next, you’ll need the Adobe AS3 corelib which has a collection of classes for JPG and PNG image encoding. Download and extract the corelib’s source library into your main class library. The following image service classes will build upon these image encoders.
3) Image services. Assemble the following three classes into a library. These classes will take care of capturing a DisplayObject’s image and then sending it to your server to be saved. Note: you’ll need to update the ImageCaptureService class member “serverPath” to point to your image service PHP file (from step #1).
3.a) Save this class as: com/gmacwilliam/images/ImageCaptureService.as
package com.gmacwilliam.images
{
import flash.events.EventDispatcher;
import flash.events.Event;
import flash.utils.ByteArray;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.net.URLRequestMethod;
import flash.net.URLVariables;
public class ImageCaptureService extends EventDispatcher
{
public static const FILE_WRITE_COMPLETE_EVENT:String = "ImageFileWriteComplete";
// UPDATE THIS...
public static var servicePath:String = "http://www.yourServerHere.com/save_image.php";
public function ImageCaptureService():void
{
super();
}
/* write
* @desc: Writes image data to file.
* @param: image data byte array.
* @param: path/name of file to write.
*/
protected function write(img:ByteArray, filename:String):void
{
// configure data to send
var req:URLRequest = new URLRequest(ImageCaptureService.servicePath +"?file="+ filename);
req.contentType = "application/octet-stream";
req.method = URLRequestMethod.POST;
req.data = img;
// send data to file service
var write:URLLoader = new URLLoader();
write.addEventListener(Event.COMPLETE, this.onWriteComplete);
write.load(req);
}
private function onWriteComplete(evt:Event):void
{
// cleanup write operation
var write:URLLoader = evt.target as URLLoader;
write.removeEventListener(Event.COMPLETE, this.onWriteComplete);
dispatchEvent(new Event(ImageCaptureService.FILE_WRITE_COMPLETE_EVENT));
}
}
}
3.b) Save this class as: com/gmacwilliam/images/JPGCapture.as
package com.gmacwilliam.images
{
import flash.display.DisplayObject;
import flash.display.BitmapData;
import com.adobe.images.JPGEncoder;
public final class JPGCapture extends ImageCaptureService
{
public function JPGCapture(clip:DisplayObject, filePath:String="", quality:int=50):void
{
super();
var jpg:JPGEncoder = new JPGEncoder(quality);
var bmp:BitmapData = new BitmapData(clip.width, clip.height, false, 0xFFFFFF);
bmp.draw(clip);
write(jpg.encode(bmp), filePath);
}
}
}
3.c) Save this class as: com/gmacwilliam/images/PNGCapture.as
package com.gmacwilliam.images
{
import flash.display.DisplayObject;
import flash.display.BitmapData;
import com.adobe.images.PNGEncoder;
public final class PNGCapture extends ImageCaptureService
{
public function PNGCapture(clip:DisplayObject, filePath:String=""):void
{
super();
var bmp:BitmapData = new BitmapData(clip.width, clip.height, true, 0x00000000);
bmp.draw(clip);
write(PNGEncoder.encode(bmp), filePath);
}
}
}
4) Instantiation. These services are extremely easy to use… Just instantiate them with a reference to a DisplayObject and a server path at which to write the image file. Also, the JPGCapture class will accept an optional quality setting (a number between 0 and 100).
var saveJPG:JPGCapture = new JPGCapture(myDisplayObject, "images/flashimg.jpg", 85); var savePNG:PNGCapture = new PNGCapture(myDisplayObject, "images/flashimg.png");
You can listen to boh object for an “ImageCaptureService.FILE_WRITE_COMPLETE_EVENT” event to know when the image data has finished getting sent to the server.
While these image services are very similar to the ones in my last post, there has been an important change: they now send both binary and text data to the server in a single server call, rather than breaking the two formats out into seperate calls. This was achieved by dropping the text data (the filename) into GET variables so that POST could be allocated entirely to binary image data.
Rendering an image outline stroke
While Flash 8 image filters added some excellent (and easy to use) graphics capabilities to the Flash display arsenal, I feel like there was a blatant omission from the filter set… what about a simple outline filter that will render a border around an image? Of course, you can fake the effect with a glow filter set to 1000% strength, but that still has a soft edge and doesn’t accommodate aliased graphics. So, I’ve been working on a script to render an outlined image based on the source image’s alpha channel. Here’s the code thus far:
Save as: com/gmacwilliam/graphics/ImageOutline.as
package com.gmacwilliam.graphics
{
import flash.display.IBitmapDrawable;
import flash.display.BitmapData;
import flash.display.Bitmap;
import flash.display.DisplayObject;
import flash.geom.Rectangle;
import flash.geom.Point;
public class ImageOutline
{
private static var _color:uint;
private static var _hex:String = "";
private static var _alpha:Number = 1;
private static var _weight:Number = 2;
private static var _brush:Number = 4;
/**
* Renders a Bitmap display of any DisplayObject with an outline drawn around it.
* @note: see param descriptions on "outline" method below.
*/
public static function renderImage(src:IBitmapDrawable, weight:int, color:uint, alpha:Number=1, antialias:Boolean=false, threshold:int=150):Bitmap
{
var w:int = 0;
var h:int = 0;
// extract dimensions from actual object type.
// (unfortunately, IBitmapDrawable does not include width and height getters.)
if (src is DisplayObject)
{
var dsp:DisplayObject = src as DisplayObject;
w = dsp.width;
h = dsp.height;
}
else if (src is BitmapData)
{
var bmp:BitmapData = src as BitmapData;
w = bmp.width;
h = bmp.height;
}
var render:BitmapData = new BitmapData(w, h, true, 0x000000);
render.draw(src);
return new Bitmap(ImageOutline.outline(render, weight, color, alpha, antialias, threshold));
}
/**
* Renders an outline around a BitmapData image.
* Outline is rendered based on image's alpha channel.
* @param: src = source BitmapData image to outline.
* @param: weight = stroke thickness (in pixels) of outline.
* @param: color = color of outline.
* @param: alpha = opacity of outline (range of 0 to 1).
* @param: antialias = smooth edge (true), or jagged edge (false).
* @param: threshold = Alpha sensativity to source image (0 - 255). Used when drawing a jagged edge based on an antialiased source image.
* @return: BitmapData of rendered outline image.
*/
public static function outline(src:BitmapData, weight:int, color:uint, alpha:Number=1, antialias:Boolean=false, threshold:int=150):BitmapData
{
_color = color;
_hex = _toHexString(color);
_alpha = alpha;
_weight = weight;
_brush = (weight * 2) + 1;
var copy:BitmapData = new BitmapData(src.width+_brush, src.height+_brush, true, 0x00000000);
for (var iy:int=0; iy <= src.height; iy++)
{
for (var ix:int=0; ix <= src.width; ix++)
{
// get current pixel's alpha component.
var a:Number = (src.getPixel32(ix, iy) >> 24 & 0xFF);
if (antialias)
{
// if antialiasing,
// draw anti-aliased edge.
_antialias(copy, ix, iy, a);
}
else if (a > threshold)
{
// if aliasing and pixel alpha is above draw threshold,
// draw aliased edge.
_alias(copy, ix, iy);
}
}
}
// merge source image display into the outline shape's canvas.
copy.copyPixels(src, new Rectangle(0, 0, copy.width, copy.height), new Point(_weight, _weight), null, null, true);
return copy;
}
/**
* Renders an antialiased pixel block.
*/
private static function _antialias(copy:BitmapData, x:int, y:int, a:int):BitmapData
{
if (a > 0)
{
for (var iy:int = y; iy < y+_brush; iy++)
{
for (var ix:int = x; ix < x+_brush; ix++)
{
// get current pixel's alpha component.
var px:Number = (copy.getPixel32(ix, iy) >> 24 & 0xFF);
// set pixel if it's target adjusted alpha is greater than the current value.
if (px < (a * _alpha)) copy.setPixel32(ix, iy, _parseARGB(a * _alpha));
}
}
}
return copy;
}
/**
* Renders an aliased pixel block.
*/
private static function _alias(copy:BitmapData, x:int, y:int):BitmapData
{
copy.fillRect(new Rectangle(x, y, _brush, _brush), _parseARGB(_alpha * 255));
return copy;
}
/**
* Utility to parse an ARGB value from the current hex value
* Hex string is cached on the class so that it does not need to be recalculated for every pixel.
*/
private static function _parseARGB(a:int):uint
{
return parseInt("0x"+ a.toString(16) + _hex);
}
/**
* Utility to parse a hex string from a hex number.
*/
private static function _toHexString(hex:uint):String
{
var r:int = (hex >> 16);
var g:int = (hex >> 8 ^ r << 8);
var b:int = (hex ^ (r << 16 | g << 8));
var red:String = r.toString(16);
var green:String = g.toString(16);
var blue:String = b.toString(16);
red = (red.length < 2) ? "0" + red : red;
green = (green.length < 2) ? "0" + green : green;
blue = (blue.length < 2) ? "0" + blue : blue;
return (red + green + blue).toUpperCase();
}
}
}
The class has two public render methods: “outline()” receives and returns raw BitmapData data, so requires some pre and post methodology to be used in a display pattern. However, the “renderImage()” method is written to be display-ready. Feed it a reference to any DisplayObject and some render parameters, and it will return a Bitmap object that can be added directly to the stage. All render parameters are documented within the class comments, but for quick reference, the basic params include: [thickness, color, alpha, antialias]. A simple implementation would look like this:
import com.gmacwilliam.graphics.ImageOutline; var outline:Bitmap = ImageOutline.renderImage(myMovieClipRef, 2, 0xFF0000, 1, false); addChild(outline);
It’s a work in progress. It’ll render a pixel-perfect outline on aliased graphics, which is a huge plus if you need to work with jagged edges. The anti-aliasing methodology has been quite a challenge and still plateaus along some curves, in which case the 1000% glow filter may still look better. If anyone has experience in rendering anti-aliased pixels, please feel free to comment with tech notes!
AS3 Countdown Timer
Chapter three of Tucker Bowen’s Lassie game Something Amiss is on the horizon, so it’s time to get the countdown going on the Lassie website! What can is say? we love game releases!
So as I was putting the countdown banner together I recalled that this is a subject that I’ve been asked about several times in the past and have answered on several web forums: how do you make a countdown timer in Flash? My previous responses to this question have always been in AS2, so lets do something fresh and write it in AS3. Here’s a count down/up timer display composed as a class…
Save as: com/gmacwilliam/display/Countdown.as
package com.gmacwilliam.display
{
import flash.display.Sprite;
import flash.utils.Timer;
import flash.events.TimerEvent;
import flash.events.Event;
import flash.text.*;
public class Countdown extends Sprite
{
private var _countUp:TextField;
private var _countDown:TextField;
private var _timer:Timer;
private var _startTime:Number;
private var _expireTime:Number;
private var _range:Number;
public function Countdown(refreshSecs:Number, year:int, month:int=1, day:int=1, hour:int=0, minute:int=0, second:int=0, ms:int=0):void
{
super();
// create TextField displays
_countUp = new TextField();
_countDown = new TextField();
_countUp.defaultTextFormat = _countDown.defaultTextFormat = new TextFormat("_sans", 24);
_countUp.antiAliasType = _countDown.antiAliasType = AntiAliasType.ADVANCED;
_countUp.autoSize = _countDown.autoSize = TextFieldAutoSize.LEFT;
_countUp.selectable = _countDown.selectable = true;
_countUp.y = 0;
_countDown.y = 50;
addChild(_countUp);
addChild(_countDown);
// configure start, end, and duration values.
_startTime = new Date().getTime();
_expireTime = new Date(year, month-1, day, hour, minute, second, ms).getTime();
_range = _expireTime - _startTime;
// configure timer interval.
_timer = new Timer(refreshSecs * 1000);
_timer.addEventListener(TimerEvent.TIMER, this._onRefresh);
_timer.start();
refresh();
}
/**
* Refreshes countdown display.
*/
public function refresh(evt:Event=null):void
{
var elapsed:Number = new Date().getTime()-_startTime;
_countUp.htmlText = _small("count up: ") + _getTimeDisplay(elapsed).toUpperCase();
_countDown.htmlText = _small("count down: ") + _getTimeDisplay(_range-elapsed).toUpperCase();
}
/**
* Disposes of countdown display so that it is eligible for garbage collection.
*/
public function dispose():void
{
_timer.stop();
_timer.removeEventListener(TimerEvent.TIMER, this._onRefresh);
_timer = null;
}
/**
* Generates countdown string from remaining time value.
*/
private function _getTimeDisplay(remainder:Number):String
{
// days
var numDays:Number = Math.floor(remainder/86400000);
var days:String = numDays.toString();
remainder = remainder - (numDays*86400000);
// hours
var numHours:Number = Math.floor(remainder/3600000);
var hours:String = (numHours < 10 ? "0" : "") + numHours.toString();
remainder = remainder - (numHours*3600000);
// minutes
var numMinutes:Number = Math.floor(remainder/60000);
var minutes:String = (numMinutes < 10 ? "0" : "") + numMinutes.toString();
remainder = remainder - (numMinutes*60000);
// seconds
var numSeconds:Number = Math.floor(remainder/1000);
var seconds:String = (numSeconds < 10 ? "0" : "") + numSeconds.toString();
remainder = remainder - (numSeconds*1000);
// milliseconds
//var numMilliseconds:Number = Math.floor(remainder/10);
//var milliseconds:String = (numMilliseconds < 10 ? "0" : "") + numMilliseconds.toString();
return "<FONT SIZE='24'>"+ days + _small("days") + hours + _small("hrs") + minutes + _small("mins") + seconds + _small("secs") + "</FONT>";
}
/**
* Utility for wrapping value labels in a tag with smaller text.
*/
private function _small(label:String):String
{
return "<FONT SIZE='12'> "+label+"</FONT> ";
}
/**
* Timer event handler to call refresh.
*/
private function _onRefresh(evt:Event):void
{
refresh();
}
}
}
Implementation of that example class is pretty simple. Just import the class into a FLA document, create an instance of the Countdown class and add it to your display. When creating a Countdown object, constructor parameters are as follows:
new Countdown(refreshIntervalSeconds:Number, year:int, [month:int=1, day:int=1, hour:int=0, minute:int=0, second:int=0, millisecond:int=0]);
The class will take care of creating all TextField displays, so you don’t need any supporting library items. The full instantiation of this countdown display looks like this:
import com.gmacwilliam.display.Countdown; // Countdown displaying the time until Halloween 2008. Refreshes once a second. addChild(new Countdown(1, 2008, 10, 31));
You’ll notice that this example includes timers that are counting both up and down to the target time. You probably won’t ever need to display both values at the same time, but this should illustrate how to arrive at either value. Feel free to adapt and adjust the class to your needs!
Common AS3 Errors
Despite their seemingly over-abundance while getting started with AS3, the new error messaging is extremely helpful and specific. While error messages are tedious at first, they eventually become extremely helpful once you understand what they mean. If you make a mistake, they tell you exactly what you did wrong.
So, here’s a short list of errors that I commonly encounter due to routine code clumsiness.
A. Compiler Errors (will prevent a movie from publishing.)
1067: Implicit coercion of type [datatype] to an unrelated type [datatype].
While the wording is fancy, the nature of this error is extremely simple: you’re trying to put a square peg into a round hole; or in technical terms, your data types don’t mesh. An example scenario that would cause this error is this:
var testing:int = new Date();
Notice the problem? The “testing” variable is typed as an “int” but is getting a “Date” object assigned to it. AS3 is very strict about data types, so will force you to make sure they always line up.
1120: Access of undefined property [name].
You’re trying to reference a value that was never created. If you get this while referencing a property of another object, you’re probably trying to access a property that is not defined as a public member of that object’s class. If you get this error in response to a value that you’ve defined within the script that you’re writing, then you probably forgot to declare the value with a “var” statement before its first reference.
1151: A conflict exists with definition [name] in namespace internal.
You’re trying to declare a value that has already been created. Make sure that you haven’t declared multiple variables by the same name within the same script. A common “gotcha” that I get hit with comes from using the same iterator for multiple loops, like so:
function errorDemo():void
{
var list:Array = new Array(0, 1, 2);
for (var i:int = 0; i < something; i++) {
// do something.
}
for (var i:int = 0; i < something; i++) {
// do something else.
}
}
Notice the error? I’ve declared the “i” iterator variable with a “var” statement twice. To fix that, I’d need to use a different iterator name for the second loop, or else just declare the second iterator as “i = 0″.
B. Runtime Errors (will NOT prevent movie publishing; occur at runtime.)
Error #2006: The supplied index is out of bounds.
You’re trying to reference a depth (index) within an object’s display list that does not exist. For example, you’ll encounter this error if you call “removeChild(2);” on a display list that only has one child object. The third child that you referenced there (yes, third) does not exist. Keep in mind that display lists are zero-indexed, so the lowest item in the list has an index of “0″ and the highest item in the display list has an index of “numChildren-1″.
Error #2007: Parameter child must be non-null.
You’re trying to supply a child-accessor method (addChild(), removeChild(), getChildAt(), etc) with a null object reference. An example scenario that would cause this error is this:
var child; removeChild(child);
While the above example will compile, it will fail at runtime because the child reference is not populated with a valid DisplayObject target before being used in a child-accessor method.
Error #2025: The supplied DisplayObject must be a child of the caller.
Tricky one. The foundation principal behind this error is the definition of a parent-child relationship: a parent object contains a child object within its display list RIGHT NOW. As soon a child is removed from its parent’s display list, their parent-child relationship is broken. So, this error occurs when you perform an action like using “removeChild()” on an object that is not contained within the target’s display list. In other words, an object cannot remove a child that it does not contain. There are several patterns that you can use to avoid conflicts. Among my most common is this:
var child:DisplayObject;
if (child != null && this.contains(child))
{
removeChild(child);
}
The above pattern will avoid both a #2007 and #2025 error, since we’ve validated that the child both exists and that it is contained within our display list before trying to remove it. Just be sure to test that the object is not null FIRST, otherwise the “contains()” operation could throw a #2007 error.
Another common cause of the #2025 error is when a child-accessor method tries to work with children that are contained within another object’s display list. It doesn’t work. Child-accessor methods only work on the callee’s own children. So, when working between object scopes, I find the most reliable method to work with child displays is to call all operations relative to the child, like so:
child.parent.removeChild(child);
In the above example, we can call this operation from any scope that has a reference to the child object, and we’re sure that the operation will target the correct parent.
The Singleton Pattern with AS3
Since I mentioned it in a previous article, I’ll expand upon the Singleton design pattern and cover its implementation within AS3…
First, what is it? As the name implies, a singleton is a single-instance class created within your application. Why would you want to limit a class to being instantiated only once? Quite simply, for global use. When storing a single set of values that need to be accessible to all other objects within the application (such as application settings or scores), you want to make sure that all objects are looking to the same set of source values; at which time, a singleton is your best friend.
So, how do we limit a class to a single instance that all other objects can reference? The solution is brilliantly simple: store the one, single class instance as a static member of its own class; then all other objects can reference that one instance by importing the class and referencing the static attribute. However, this begs another question… why ever create a class instance when you could just store all the singleton’s data as static members, like we do with Flash’s “Math” class? This is a good consideration, and using a static model works fine when you just need a switchboard for storing values. However, the static model does not give us all the features and flexibility that an object instance might… for example, we cannot dispatch events when values are changed without an instance of an EventDispatcher at our disposal. So, we use the Singleton pattern when we need an object instance.
Finally, implementation… The Singleton pattern has a rigid structure and is ultra-simple to set up. The basic requirements are that we have a private static member to store a class instance, a public static getter that will either return the one class instance or else create it if it does not yet exist, and finally some means of security to prevent other objects from ever being allowed to create an instance of the class. So, here it is:
package
{
public class Singleton extends EventDispatcher
{
private static var _instance:Singleton;
public static function get instance():Singleton
{
if (_instance == null) _instance = new Singleton(new SingletonEnforcer());
return _instance;
}
public function Singleton(enforcer:SingletonEnforcer):void
{
super();
}
}
}
internal class SingletonEnforcer {}
The above is the full extent of the AS3 singleton pattern. The only tricky aspect of this pattern is the “enforcer”, which prevents other classes from instantiating our singleton. This enforcer works thanks to AS3′s ability to define private classes available only to the main packaged class. By requiring a private class as an argument of the constructor, we are effectively eliminating the ability of any other object in our application to meet instantion requirements for our singleton… Incidentally, I’d like to extend a handshake to the diabolically clever AS3 engineer out there who thought up that security measure. It is truly brilliant.
So, from the above framework you just need to flesh out the class with additional instance properties and methods that are specific to your application. To access those properties and methods from other classes, you just need to import the Singleton class, and then reference attributes as Singleton.instance.myCustomValue.
Dynamically browsing MovieClip frame labels
Frame labels are great for keeping track of graphical states on a MovieClip timeline, but let’s face it: their usefulness was limited in AS2 due to the fact that you had to know that they existed before you could reference them. Any AS2 developer can probably think of a time in their past when they built a large state machine based on hard-coded frame labels that MovieClips would need to implement. Inevitably it didn’t matter how solid your code was in those cases; your app could–and would–still break unless you remembered to include ever silly “main”, “secondary”, and “my-cool-screen” frame label defined within code.
But there’s good news to be had here: all that has been addressed in AS3. In fact, the frame label model is unbelievably cool now! The first and most basic new feature is the MovieClip.currentLabel property. That gives us the frame label of the current timeline frame. While that may seem pretty straight forward, it’s a huge step up from AS2 where the current frame label was not available at run-time (…which always seemed like a pretty big oversight to me).
However, I think the best new feature is the MovieClip.currentLabels property, which gives an array of ALL frame labels on a timeline, along with their corresponding frame numbers. AWESOME. That means that we can quickly and easily set up a scenario that will let us browse all of a MovieClip’s labeled timeline frames at run-time. Here’s an example:
import flash.display.FrameLabel;
// _displayClip = MovieClip with timeline to access.
// _select = Flash UI ComboBox component instance.
_displayClip.stop();
_select.removeAll();
_select.addEventListener(Event.CHANGE, this._onSelectLabel);
// get and populate all of displayClip's frame labels in the select menu
for each (var j:FrameLabel in _displayClip.currentLabels)
{
// add list items with FrameLabel object properties: name, frame.
_select.addItem({label:j.name, data:j.frame});
}
function _onSelectLabel(evt:Event):void
{
// update displayClip when a new frame is selected
_displayClip.gotoAndStop(_select.selectedItem.data);
}
If you set that scenario up in a new Flash document and give the _displayClip a few frame labels to browse through, then you’ll be able to select and display the various frames in your MovieClip using the combobox (…just be sure to put different graphics on each labeled timeline frame so that you can see a change in appearance when you switch between frames!).
So, what can you do with this? I’ve used it in the new Lassie Shepherd editor so that a developer can select frame labels to display within externally loaded SWF media. It works like a charm. This would also be a good means to build a really simple navigation scheme: you’d set up and label different content frames along a timeline, load that movie into a shell, and then you could populate navigation options within the shell based on the frame labels. The beauty here is that if you ever added new content frames to the loaded media, they’d plug right into the shell movie that rendered the navigation.
Script syntax coloring for an editable TextField
Syntax coloring makes a code developer’s life much easier. For those unfamiliar with the concept, consider how ActionScript renders in shades of blue with green strings (by default) within the Flash Actions panel. That color-coding helps focus the eye on relevant keywords in the script.
As I’ve been moving forward on developing Lassie Shepherd’s script panel, I wanted to incorporate some form of syntax coloring to aid the game developer. So, I wrote up a quick class to parse and apply keyword coloring based on standard scripting syntax. Valid keywords can be assigned within the class’ static “keywords” array. Also, this generation of the script only accepts single quotes (apostrophes) as string delimiters.
1) Save as: com/gmacwilliam/parse/SyntaxColoring.as
package com.gmacwilliam.parse
{
import flash.text.TextField;
import flash.text.TextFormat;
public class SyntaxColoring
{
public static var keywords:Array = new Array("if", "gotoAndStop", "play", "true", "false");
public static var backgroundColor:Number = 0xFFFFFF;
public static var foregroundColor:Number = 0x000000;
public static var keywordColor:Number = 0x0000FF;
public static var stringColor:Number = 0x009900;
public static function renderField(tf:TextField):void
{
// apply field background color
tf.backgroundColor = backgroundColor;
// set field-read starting index
var index:int = 0;
// render all lines of text
while(index < tf.text.length)
{
index = renderLine(tf, index) + 1;
}
}
public static function renderLine(tf:TextField, caretIndex:int):int
{
// create text formats
var colorBase:TextFormat = tf.getTextFormat();
colorBase.color = foregroundColor;
var colorKeyword:TextFormat = tf.getTextFormat();
colorKeyword.color = keywordColor;
var colorString:TextFormat = tf.getTextFormat();
colorString.color = stringColor;
// get start and end indicies of the current line
var sindex:int = tf.getFirstCharInParagraph(caretIndex);
var eindex:int = sindex + tf.getParagraphLength(caretIndex);
eindex = Math.min(eindex, tf.text.length);
// if paragraph has any text
if (sindex < eindex)
{
// apply black default text format
tf.setTextFormat(colorBase, sindex, eindex);
// extract source block of text to format
var src:String = tf.text.substr(sindex, eindex-sindex);
// strip all parenthesis and spaces from line and split into an array
var raw:String = src.split(" ").join("");
raw = raw.split("(").join(",");
raw = raw.split(")").join(",");
var lineWords:Array = raw.split(",");
var index:int = 0;
// KEYWORDS
// loop through array of line words
for (var j:int = 0; j < lineWords.length; j++)
{
var word:String = lineWords[j];
// color any words that match a keyword
if (keywords.indexOf(word) > -1)
{
index = src.indexOf(word, index);
tf.setTextFormat(colorKeyword, sindex+index, sindex+index+word.length);
}
}
// STRINGS
// reset render index
index = 0;
while(index > -1)
{
// find index of next quote
index = src.indexOf("'", index);
if (index > -1)
{
// if quote was found...
var qo:int = index; // quote open
var qc:int = src.indexOf("'", index+1); // quote close
var so:int = sindex+qo; // string open
var sc:int = (qc < 0) ? eindex : so+(qc-qo)+1; // string close
// apply text color to closed string or to everything after quote
tf.setTextFormat(colorString, so, sc);
// update render index with close-quote position
index = (qc < 0) ? -1 : qc+1;
}
}
}
// return end-index of rendered text line
return eindex;
}
}
}
Implementation of this class on a TextField is very simple, and the script is surprisingly efficient. Since AS3 can now track the caret’s line index within an editable text field, the full field only needs to be parsed and rendered once on initial load. After that, only the line containing the caret needs to be re-rendered when the field is changed. Here’s the implementation:
import com.gmacwilliam.parse.SyntaxColoring;
var color_txt:TextField; // << create an editable text field on stage
color_txt.addEventListener(Event.CHANGE, this.handleColorize);
// render field with initial formatting
SyntaxColoring.renderField(color_txt);
// handle field updates
function handleColorize(evt:Event):void
{
SyntaxColoring.renderLine(color_txt, color_txt.caretIndex);
}
Wire it up and try entering “gotoAndStop(‘sfoo’);” in your editable TextField. It should look quite a bit like ActionScript!
Leave a Comment