Kaolin Fire with GUD Issues 0 through 5

kaolin fire presents :: AS3 101 :: as3 animation concepts


The stage, the timeline, and you


Making it move

The stage, the timeline, and you

The Timeline

Action Script was built around the timeline, taking its metaphor from video. Originally, the timeline was purely managed as "frames" (key frames and tween frames and what have you). These frames would run at a fixed frame rate, showing animations. All AS3 runs at a fixed (set at compile-time) frame rate (typically 30 or 60 FPS); most programmers will actually only work with one "frame" (or two frames when dealing with preloaders—we'll worry about that later), but this still sets the base rate for how fast your program runs.

Deconstructing the SWF tag

Going back to a minimal "Main" class (in Main.as)

package { import flash.display.MovieClip; [SWF(width="640",height="480",frameRate="30",backgroundColor="#000000")] public class Main extends MovieClip { } }

This defines the width and height, in pixels, of the swf that you are compiling; the background color (in standard web #RRGGBB hex triplet), and, essentially the clock rate. So what happens when it ticks...?

ON_ENTER_FRAME

Action Script 3 is an event-based language. ON_ENTER_FRAME is one of the primary events—the system will call whatever function(s) you specify once per clock tick, presuming your object is on the stage. An instance of your first class is explicitly added to the stage, so we won't worry about that yet.

package { import flash.display.MovieClip; import flash.display.DisplayObject; import flash.events.Event; // set our application's frame rate, width, height, and background color [SWF(width="640",height="480",frameRate="30",backgroundColor="#000000")] public class Main extends MovieClip { // this embeds the file "smiley2.png" into your swf and // makes the image data available through the class myGraphic // available as the Class myGraphic [Embed(source="smiley2.png")] private var myGraphic:Class; // this holds a DisplayObject, which you will add to the class to display it private var smiley:DisplayObject; // a counter for our animation private var tick:int; // our class constructor, the entry point for our program; as the primary class // for our application, an instance of it is automatically added to the stage public function Main() { // create a DisplayObject from our smiley2.png and store it in smiley smiley = new myGraphic() as DisplayObject; // and add the smiley to this class's display list addChild(smiley); tick = 0; // set our function to be called every ENTER_FRAME event addEventListener(Event.ENTER_FRAME,myUpdateFunction); } public function myUpdateFunction(e:Event):void { // update the smiley's position based on the current tick smiley.x = 320 + 50 * Math.cos(tick/20); smiley.y = 240 + 50 * Math.sin(tick/20); // and then increment the tick tick++; } } }

What are we looking at?

In our constructor/initialization, we register our function "myUpdateFunction" to listen to any Event.ENTER_FRAME events that this instance of our Main class dispatches. That will be dispatched 30 times per second (set by the frameRate parameter of SWF). We set our "tick" member to 0.

Every 30 seconds, then, myUpdateFunction is called with an Event (we're calling that event "e", but we don't bother referring to it in our function). We also specify that the function returns "void" type, which essentially means it doesn't return anything. Taking the event and having the return type void makes our function match the signature that the event system expects.

Each time myUpdateFunction is called, we change smiley's x and y coordinates based on tick, and update tick. When compiled and run, our smiley rotates in a circle.

Okay, let's add more events

The most common events you'll be working with, beyond ENTER_FRAME, are CLICK, ROLL_OVER, and ROLL_OUT, all properties of MouseEvent.

A quick note on nesting: all events, by default, start with the object they most directly apply to; that's not always the desired case, however. For instance, with "buttons" you'll generally nest a graphic (or a few graphics) inside a larger object, but you want to get events at the level of that larger object.

In order to tell the system that an object should react in some way with the mouse, set its "mouseEnabled" property to true. To keep it from passing events to inner elements, set the "mouseChildren" property of that object to false (i.e. don't send events to that object's children). To get the little hand icon, etc, set the object's "buttonMode" property to true. And then to get events, you addEventListener for the events you want, per above. So if we're going to roll a quick button together, we could:

var mybutton:Sprite = new Sprite(); mybutton.mouseChildren = false; mybutton.mouseEnabled = true; mybutton.buttonMode = true; mybutton.width = 20; mybutton.height = 20;

On its own, that's a button. You can add it to the stage, but it won't look like it's there, and you haven't tied anything to its events.

mybutton.addEventListener(MouseEvent.CLICK,myClickFunction); public function myUpdateFunction(e:MouseEvent):void { trace("clicked"); }

Trace is a handy flash function that spits out just about anything you want it to to the trace log. Setting things up to see the trace log can be a pain on OS X; FlashDevelop mostly "just works" (when you're building in debug mode).

Now a few sidesteps....

Anonymous functions

One nice thing with as3 is the anonymous function. If you've got a simple function you want to throw into an event listener, for instance, you don't have to define a member function on the class; you can just inline it:

mybutton.addEventListener(MouseEvent.CLICK,function(e:MouseEvent):void { trace("clicked"); });

Note that the function is defined entirely inside the addEventListener parameters, and needs to be closed before the addEventListener function call is closed.

AS3 Graphics library and drawing

Sprites have a graphics member class that lets you draw programatically. You can generate rectangles, circles (or ovals), lines, and the like.

var g:Graphics = mybutton.graphics; g.beginFill(0xffffee,1.0); // color and alpha g.drawRect(0,0,20,20); // draw a rectangle g.endFill();

Let's put all these together

package { import flash.display.MovieClip; import flash.display.Sprite; import flash.display.Graphics; import flash.display.DisplayObject; import flash.events.Event; import flash.events.MouseEvent; [SWF(width="640",height="480",frameRate="30",backgroundColor="#ffffcc")] public class Main extends MovieClip { [Embed(source="smiley2.png")] private var myGraphic:Class; private var smiley:DisplayObject; private var tick:int; private var mybutton:Sprite; public function Main() { smiley = new myGraphic() as DisplayObject; addChild(smiley); tick = 0; // adds an anonymous inline function to respond to ENTER_FRAME events on the // application instance of your Main class addEventListener(Event.ENTER_FRAME,function(e:Event):void { smiley.x = 320 + 50 * Math.cos(tick/20); smiley.y = 240 + 50 * Math.sin(tick/20); tick++; }); mybutton = new Sprite(); mybutton.mouseChildren = false; mybutton.mouseEnabled = true; mybutton.buttonMode = true; // by default, mybutton.useHandCursor is true for "buttonMode" objects mybutton.x = 100; mybutton.y = 100; var g:Graphics = mybutton.graphics; g.beginFill(0x66ff66,1.0); // color and alpha g.drawRect(0,0,60,60); // draw a rectangle g.endFill(); addChild(mybutton); mybutton.addEventListener(MouseEvent.CLICK,function(e:MouseEvent):void { // toggle the smiley's visibility (set its new visibility to (not) its current visibility) smiley.visible = !smiley.visible; // get the point that was clicked, relative to the object trace(e.localX,e.localY); // get the point that was clicked, relative to the stage trace(e.stageX,e.stageY); // you can also access the object that was clicked as "target" // note that you can access where an object was last clicked from the object itself // without using an event listener--you just don't know THAT it was clicked.... // these are also relative to the object at the time that it was clicked trace(e.target.mouseX,e.target.mouseY); }); addEventListener(MouseEvent.ROLL_OVER,function(e:MouseEvent):void { var g:Graphics = mybutton.graphics; g.beginFill(0xffff66,1.0); // color and alpha g.drawRect(0,0,60,60); // draw a rectangle g.endFill(); }); addEventListener(MouseEvent.ROLL_OUT,function(e:MouseEvent):void { var g:Graphics = mybutton.graphics; g.beginFill(0x66ffff,1.0); // color and alpha g.drawRect(0,0,60,60); // draw a rectangle g.endFill(); }); } } }

Note: you can set a sprite's width and height, but in doing so your access to its graphics object will fail (or using its graphics object to draw will silently fail, anyway).

Try these

  • add another smiley somewhere else on the screen, rotating counter-clockwise
  • make the second smiley start out not visible, so it toggles opposite
  • turn both smileys into buttons and make them trace "1" and "2" respectively

Future lessons

There's a handy built-in button class called SimpleButton that gives you a few things for free (switching images for hover and clicked states, for instance). TODO: branch into SimpleButton, making separate classes, object visibility, text + css, ... and object scope (wrt member objects and why we name them at class level versus function level). Also "hitArea" on an object.





I am soooo fake pre-loading this image so the navigation doesn't skip while loading the over state.  I know I could use the sliding doors technique to avoid this fate, but I am too lazy.