19.4 NightSky: A Delegation Event Model Example

 <  Day Day Up  >  

19.4 NightSky : A Delegation Event Model Example

Now that we have established a foundation for delegation event model implementations , let's use it in a real example: a starry night sky with animated shooting stars. The code discussed in this section is available at http:// moock .org/eas2/examples. This example breaks from earlier examples in this book, which focused primarily on application development. Here, we see how OOP and design patterns can be applied to motion graphics as well as application development. Of course, a random event generator is also perfect for video games , which make heavy use of OOP.

19.4.1 The Event Source

The event source for our example is Randomizer , a class that triggers an event named onRandomAction( ) at random intervals. Every n seconds, the onRandomAction( ) event has a user -defined chance of occurring. For example, a specific Randomizer instance might, every 5 seconds, have a 1-in-20 chance of triggering onRandomAction( ) .

The onRandomAction( ) event typically activates some random behavior in Randomizer 's listeners, such as the blinking of a cartoon character's eyes, the spawning of a random monster in a game, or, as in our case, the appearance of a shooting star animation in a night sky. The Randomizer class is a generally handy utility, so we store it in the package util .

19.4.2 The EventListener Subinterface

The EventListener subinterface in our example is RandomizerListener . The Randomizer class broadcasts only a single event, so the RandomizerListener interface defines only a single method ” onRandomAction( ) . Classes wishing to receive Randomizer events must implement RandomizerListener and define an onRandomAction( ) method. The RandomizerListener interface resides in the package util , along with the Randomizer class.

19.4.3 The EventObject Subclass

Each event broadcast by Randomizer is described by a RandomizerEvent instance ( RandomizerEvent is an EventObject subclass). The RandomizerEvent class provides a single method, getTimeSinceLast( ) , which reports the elapsed time since the previous onRandomAction( ) event was generated. The RandomizerEvent class resides in the package util , along with the Randomizer class and RandomizerListener interface.

19.4.4 The Event Listener Class

Our night sky application's event listener class is NightSky . It implements the RandomizerListener interface and defines an onRandomAction( ) method. When NightSky is constructed , it creates a sky background. Each time Randomizer invokes onRandomAction( ) on NightSky , the NightSky class creates a shooting star animation over the sky background.

19.4.5 The Overall Structure

Figure 19-2 shows the structure of the night sky application. Compare Figure 19-2 with the generic delegation event model structure shown earlier in Figure 19-1. Specifically, Randomizer is our EventSourceClass , RandomizerListener is our EventTypeListener , RandomizerEvent is our EventTypeEvent , and NightSky is our EventListenerClass . The parameter, l , passed to addRandomizerListener( ) and removeRandomizerListener( ) in Figure 19-2 is the listener to be added or removed.

Figure 19-2. Night sky application structure
figs/as2e_1902.gif

Now let's take a closer look at the code in the night sky application.

19.4.6 Randomizer, RandomizerListener, and RandomizerEvent

The Randomizer and RandomizerEvent classes and the RandomizerListener interface work together to provide event-broadcast services for the Randomizer class. Conceptually, every delegation event model implementation will have the same three participants ”a class that generates events ( Randomizer ), a class that describes those events ( RandomizerEvent ), and an interface that states the events ( RandomizerListener ). Let's work through the code for these participants in reverse order, starting with RandomizerListener .

The RandomizerListener interface and its single method, onRandomAction( ) , are shown in Example 19-4.

Example 19-4. The RandomizerListener interface
 import event.*; import util.*; /**  * Lists the methods that must be implemented by  * an object that wants to be notified of   Randomizer   events.  */ interface util.RandomizerListener extends EventListener {   /**    * Triggered when a random event has occurred (according to the    * odds and interval set for the   Randomizer   ).    *    * @param   e   A   RandomizerEvent   instance.    */   public function onRandomAction (e:RandomizerEvent):Void; } 

The onRandomAction( ) method defines a single parameter, e , of type RandomizerEvent . By restricting the type of event object that can be passed to onRandomAction( ) , we guarantee the validity of the event system: if the event source passes the wrong kind of object to the onRandomAction( ) method, the compiler generates an error. Or, if an event-listening class implements onRandomAction( ) but neglects to define a RandomizerEvent parameter, the compiler generates an error. Static typing ensures that the explicit contract between the event source and its event listeners is upheld.

Example 19-5 shows the code for the RandomizerEvent class. Event objects are simple in nature, acting merely as data-storage vessels used to transfer information from the event source to event listeners. In the case of RandomizerEvent , the information transferred is the time since the last event occurred.

Example 19-5. The RandomizerEvent class
 import event.*; import util.*; /**  * An event object describing a   Randomizer   event.  */ class util.RandomizerEvent extends EventObject {   // The number of milliseconds since the last event was broadcast.   private var timeSinceLast:Number;   /**    * Constructor    *    * @param  src   The event source (a   Randomizer   instance).    * @param  timeSinceLast  The number of milliseconds since the last event.    */   public function RandomizerEvent (src:Randomizer, timeSinceLast:Number) {     // Always pass event source to superclass constructor!     super(src);     // Record the time since the last event.     this.timeSinceLast = timeSinceLast;   }   /**    * Returns the time since the last event.    */   public function getTimeSinceLast ( ):Number {     return timeSinceLast;   } } 

Notice that the RandomizerEvent constructor receives two arguments: src , a reference to the Randomizer instance that created the RandomizerEvent , and timeSinceLast , the time elapsed since the last event.

 function RandomizerEvent (src:Randomizer, timeSinceLast:Number) {   super(src);   this.timeSinceLast = timeSinceLast; } 

In the constructor body, we pass the src reference to the RandomizerEvent 's superclass constructor. There, the reference is stored for access by the event listener receiving the RandomizerEvent object.

Passing the event source reference to the EventObject constructor is a required step that must not be skipped . If you do not pass the event source to the EventObject constructor, recipients of the event will not have access to the object that generated the event.


Example 19-6 shows the code for the Randomizer class. Read it over, then we'll study the important sections in detail.

Example 19-6. The Randomizer class
 import event.*; import util.*; /**  * Generates random events.  */ class util.Randomizer {   // The event listeners that will receive events.   private var listenerList:EventListenerList;   // The time in milliseconds between checks for a random event.   private var randInterval:Number;   // The time of the last random event.   private var lastEventTime:Date;   /**    * Constructor    *    * @param  interval  The time in milliseconds to wait between     *                   checks for a random event.    * @param  odds      The likelihood an event will be triggered at    *                   each check. An event has a 1-in-   odds   chance of    *                   being triggered.    */   public function Randomizer (interval:Number, odds:Number) {     // Create an   EventListenerList   to manage listeners.     listenerList = new EventListenerList( );     // Initialize the time of the most recent event.     lastEventTime = new Date( );     // Start checking for events.     start(interval, odds);   }   /**    * Register an object to receive random events.    */   public function addRandomizerListener (l:RandomizerListener):Boolean {     return listenerList.addObj(l);   }      /**    * Unregister an event listener object.    */   public function removeRandomizerListener (l:RandomizerListener):Boolean {     return listenerList.removeObj(l);   }   /**    * Start an interval to check for random events.    *    * @param  interval  The time in milliseconds to wait between     *                   checks for a random event.    * @param  odds      The likelihood an event will be triggered at    *                   each check.    */   public function start (interval:Number, odds:Number):Boolean {     if (interval > 0 && odds > 1) {       // Call   this.check( )   every   interval   milliseconds.       randInterval = setInterval(this, "check", interval, odds);       return true;     }     return false;   }   /**    * Stop checking for random events.    */   public function stop ( ):Void {     clearInterval(randInterval);   }   /**    * Restart the event-checking interval, possibly with new odds.    *    * @param  interval  The time in milliseconds to wait between     *                   checks for a random event.    * @param  odds      The likelihood an event will be triggered at    *                   each check.    */   public function restart (interval:Number, odds:Number):Void {     stop( );     start(interval, odds);   }    /**    * Checks to see if a random event occurs, based on the    * current odds.    *    * @param  odds      The likelihood an event will be triggered at    *                   each check.    */   private function check (odds:Number):Void {     // Local variables.     var rand:Number = Math.floor(Math.random( ) * odds);     var now:Date = new Date( );     var elapsed:Number;     // If the random event occurs...     if (rand == 0) {       // Determine the elapsed time since the last event.       elapsed = now.getTime( ) - lastEventTime.getTime( );       lastEventTime = now;       // Fire the event.       fireOnRandomAction(elapsed);     }   }   /**    * Invokes   onRandomAction( )   on all listeners.    *    * @param  elapsed   The amount of time since the last event notification.    */   private function fireOnRandomAction (elapsed:Number):Void {     // Create an object to describe the event.     var e:RandomizerEvent = new RandomizerEvent(this, elapsed);     // Get a list of the current event listeners.     var listeners:Array = listenerList.getListeners( );     // Broadcast the event to all listeners.     for (var i:Number = 0; i < listeners.length; i++) {       // Notice that we don't cast to   RandomizerListener   here.        // For an explanation of why the cast isn't required, see        // "   Array Elements and Type Checking   ," in Chapter 3.       listeners[i].onRandomAction(e);     }   } } 

Much of the Randomizer class code relates to the internal task of checking odds at specific time intervals, which is not our current focus. From the perspective of the delegation event model, we're concerned only with the listenerList property and the addRandomizerListener( ) , removeRandomizerListener( ) , and fireOnRandomAction( ) methods, used to manage event listener objects and broadcast the onRandomAction event.

When a Randomizer instance is constructed, it creates an EventListenerList instance, which it stores in the property listenerList :

 listenerList = new EventListenerList( ); 

The EventListenerList instance manages the Randomizer class's event listeners. To register an object to receive Randomizer events, we pass it to addRandomizerListener( ) , which, in turn passes it to listenerList.addObj( ) . The listenerList.addObj( ) method is what actually adds the object to the list of event listeners for the current Randomizer instance:

 public function addRandomizerListener (l:RandomizerListener):Boolean {   return listenerList.addObj(l); } 

To stop an object from receiving Randomizer events, we pass it to removeRandomizerListener( ) , which, like addRandomizerListener( ) , delegates its work to the listenerList object:

 public function removeRandomizerListener (l:RandomizerListener):Boolean {   return listenerList.removeObj(l); } 

Both addRandomizerListener( ) and removeRandomizerListener( ) return a Boolean indicating whether the registration or unregistration attempt succeeded. Registration succeeds (and addRandomizerListener( ) returns true ) if the object registering is not already registered. If the object is already registered, addRandomizerListener( ) returns false . Unregistration will fail if the object being unregistered is not in the list of currently registered event listeners.

Notice, again, that static typing ensures that only instances of classes that implement RandomizerListener can register to receive events from Randomizer . If a non- RandomizerListener is passed to addRandomizerListener( ) or removeRandomizerListener( ) , the compiler generates a type mismatch error. Hence, all registered listeners are guaranteed to define the onRandomAction( ) method, which Randomizer uses when broadcasting its event.

When it's time for the Randomizer class to broadcast an event, it invokes fireOnRandomAction( ) , passing the amount of time since the last event to that method:

 private function fireOnRandomAction (elapsed:Number):Void {   var e:RandomizerEvent = new RandomizerEvent(this, elapsed);   var listeners:Array = listenerList.getListeners( );   for (var i:Number = 0; i < listeners.length; i++) {     listeners[i].onRandomAction(e);   } } 

In the fireOnRandomAction( ) method, we first create a RandomizerEvent instance and pass it two arguments ”a reference to the current Randomizer instance and information describing the event, in this case, the time elapsed since the previous event:

 var e:RandomizerEvent = new RandomizerEvent(this, elapsed); 

Next , we retrieve the list of currently registered listeners from the listenerList :

 var listeners:Array = listenerList.getListeners( ); 

Finally, we invoke onRandomAction( ) on all registered listeners, passing each the event object we created earlier:

 for (var i:Number = 0; i < listeners.length; i++) {   listeners[i].onRandomAction(e); } 

Thus, the event is broadcast, and each listener happily determines its own appropriate response. For example, if we were using a Randomizer to control monsters in an adventure game, each monster (each listener object) could pick some random behavior ”perhaps attackPlayer( ) , flee( ) , or defend( ) . Or in our shooting star example, when the Randomizer instance invokes onRandomAction( ) on our NightSky instance, the NightSky instance responds by displaying a shooting star.

19.4.6.1 Multiple event types from a single event source

Before we move on to the use of our Randomizer class, it's worth noting that a single event source can legitimately define more than one kind of event. For example, in Java (from whence the delegation event model originates), the all-purpose user interface class, Component , defines two categories of mouse events: one for mouse motion and one for mouse input (i.e., mouseclicks). To register an object for mouse motion events in Java, we first ensure that it implements the MouseMotionListener interface, then we pass it to Component.addMouseMotionListener( ) . To register an object for mouse input events, we first ensure that it implements the MouseListener interface, then we pass it to Component.addMouseListener( ) . The same event source, Component , manages both types of events, keeping separate track of its event listeners by using two separate registration routines and two separate listener lists.

When an event source defines multiple types of events, its listeners can flexibly sign up to receive only a particular category of events, ignoring other events that are not of interest. To receive both events, however, a listener object must register twice (once for each event).

19.4.7 The NightSky Class

In our delegation event model example, the NightSky class is the event listener class; it receives events from Randomizer . In order to become eligible to receive Randomizer events, NightSky implements RandomizerListener .

Example 19-7 shows the code for the NightSky class.

Example 19-7. The NightSky class
 import util.*; /**  * Creates a sky full of stars and listens for   Randomizer   * events to create random shooting stars.  */ class nightsky.NightSky implements RandomizerListener {   // The movie clip in which to create the sky.   private var target:MovieClip;   // The sky movie clip.   private var sky_mc:MovieClip;   // The depth, in   target   , at which to create the sky. Defaults to 0.   private var skyDepth:Number = 0;   // The depth, in   sky_mc   , at which to create shooting stars. Defaults to 0.   private var starDepth:Number = 0;     /**    * Constructor    */   public function NightSky (target:MovieClip,                              skyDepth:Number, starDepth:Number) {     this.target = target;     this.skyDepth = skyDepth;     this.starDepth = starDepth;     makeSkyBG( );   }   /**    * Responds to random events from a   Randomizer   object.    * Creates a shooting star in the sky.    *     * @param   e   An object that describes the event.    */   public function onRandomAction (e:RandomizerEvent):Void {     trace("New shooting star! Time since last star: "           + e.getTimeSinceLast( ));     makeShootingStar( );   }   /**    * Creates a sky graphic by attaching a movie clip with the    * linkage identifier of   "skybg"   .    */   private function makeSkyBG ( ):Void {     sky_mc = target.attachMovie("skybg", "skybg", skyDepth);   }      /**    * Creates a shooting star by attaching a movie clip with the    * linkage identifier of   "shootingstar"   .    */   private function makeShootingStar ( ):Void {     // Create the shooting star in the sky movie clip.     sky_mc.attachMovie("shootingstar",                         "shootingstar" + starDepth, starDepth);     // Randomly position the shooting star.     sky_mc["shootingstar" + starDepth]._x = Math.floor(Math.random( )                                              * target.skybg._width);     sky_mc["shootingstar" + starDepth]._y = Math.floor(Math.random( )                                              * target.skybg._height);     // Put the next shooting star on a higher depth.     starDepth++;   } } 

The NightSky class uses what should now be familiar techniques to create an instance of a movie clip symbol depicting a sky background, namely MovieClip.attachMovie( ) .

Our focus in Example 19-7 is the onRandomAction( ) method, which handles onRandomAction events from Randomizer . The method is simple ”when the random event occurs, it creates a shooting star by invoking makeShootingStar( ) . For debugging purposes, onRandomAction( ) also displays the time elapsed since the last shooting star was created.

 public function onRandomAction (e:RandomizerEvent):Void {   trace("New shooting star! Time since last star: " + e.getTimeSinceLast( ));   makeShootingStar( ); } 

Reader exercise: try modifying the code so that the chance of a shooting star appearing dynamically decreases or increases depending on the amount of time elapsed since the last star. Hint ”use e.getSource( ) to access the Randomizer.restart( ) method.

19.4.8 Using NightSky in an Application

To use the NightSky class in an application, we create an instance of Randomizer and an instance of NightSky , then we use addRandomizerListener( ) to register the NightSky instance as a listener with the Randomizer instance. The following code shows the technique:

 import nightsky.*; import util.*; var sky:NightSky = new NightSky(bg_mc, 1, 0); var starRandomizer:Randomizer = new Randomizer(1000, 3); starRandomizer.addRandomizerListener(sky); 

The NightSky instance ( sky ) creates a sky graphic in the movie clip bg_mc at depth 1. Shooting stars in the sky will start appearing on depth 0. The Randomizer instance ( starRandomizer ) has a one-in-three chance of triggering onRandomAction( ) every second (i.e., every 1000 milliseconds).

The preceding code could appear in a class as part of a larger OOP application, or it could be placed directly on a frame in the timeline of a .fla file. Either way, the .fla that uses the NightSky instance must export two movie clip symbols, one for the sky ("skybg") and one for the shooting stars ("shootingstar").

For a working version of the night sky example, see http://moock.org/eas2/examples.

 <  Day Day Up  >  


Essential ActionScript 2.0
Essential ActionScript 2.0
ISBN: 0596006527
EAN: 2147483647
Year: 2004
Pages: 177
Authors: Colin Moock

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net