[Golist] [goasap commit] r48 - in trunk/goasap/src_go/org/goasap: . utils

codesite-noreply at google.com codesite-noreply at google.com
Sun Jul 27 14:13:55 PDT 2008


Author: mosesoak
Date: Sun Jul 27 14:13:13 2008
New Revision: 48

Modified:
   trunk/goasap/src_go/org/goasap/GoEngine.as
   trunk/goasap/src_go/org/goasap/utils/PlayableGroup.as

Log:
0.5.1 Fixes PlayableGroup so that IPlayble items may dispatch their STOP or COMPLETE event immediately during start(). This fix works for sequence classes as well since SequenceItem extends PlayableGroup.

Modified: trunk/goasap/src_go/org/goasap/GoEngine.as
==============================================================================
--- trunk/goasap/src_go/org/goasap/GoEngine.as	(original)
+++ trunk/goasap/src_go/org/goasap/GoEngine.as	Sun Jul 27 14:13:13 2008
@@ -1 +1 @@
-/**
 * Copyright (c) 2007 Moses Gunesch
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.goasap {
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.TimerEvent;
	import flash.utils.Dictionary;
	import flash.utils.Timer;
	import flash.utils.getQualifiedClassName;
	import flash.utils.getTimer;
	
	import org.goasap.errors.DuplicateManagerError;
	import org.goasap.interfaces.IManageable;
	import org.goasap.interfaces.IManager;
	import org.goasap.interfaces.IUpdatable;
	import org.goasap.interfaces.ILiveManager;	

	/**
	 * Provides <code>update</code> calls to <code>IUpdatable</code> instances on their specified <code>pulseInterval</code>.
	 * 
	 * <blockquote><blockquote>
	 * <p><b>Using these Docs</b></p>
	 * 
	 * <p><i>Protected methods and properties have been excluded in almost all 
	 * cases, but are documented in the classes. Exceptions include key protected
	 * methods or properties that are integral for writing subclasses or understanding 
	 * the basic mechanics of the system. Many Go classes can be used as is without 
	 * subclassing, so the documentation offers an uncluttered view of their public
	 * usage.</i></p>
	 * 
	 * <p><b>Introduction to Go</b> <font color="#CC0000">[This section updated recently!]</font></p>
	 * 
	 * <p>The Go ActionScript Animation Platform ("GOASAP") is a lightweight, portable 
	 * set of generic base classes for buliding AS3 animation tools. It provides structure 
	 * and core functionality, but does not define the specifics of animation-handling 
	 * classes like tweens.</p>
	 * 
	 * <p>GoASAP could be broken up into the following general layers:
	 * <ul>
	 * <li><i>Compatibility:</i> In general, this layer can be used in about any animation 
	 * system. GoEngine, GoEvent, PlayStates and IPlayable.</li>
	 * <li><i>Items:</i> Base classes for utilities and animation items: PlayableBase, GoItem,
	 * LinearGo and PhysicsGo.</li>
	 * <li><i>Utilities:</i> You can write utility classes to manage items. Go ships with a 
	 * few common ones: a parallel item class called PlayableGroup, and several Sequence classes.</li>
	 * <li><i>Automation/Management:</i> GoEngine provides a simple, centralized and fully extensible 
	 * way for you to automate any time-based process and manage multiple items at once.</li>
	 * </ul></p>
	 * 
	 * <p>GoASAP provides an intentionally loose standard, in that it does not intend to limit
	 * the possibilities of what can be built with it. Its primary benefits are compatibility and
	 * synchronicity between animation systems, absolute extensibility into any time-based process,
	 * and a much faster and easier way to build your own animation tools from scratch.</p>
	 * 
	 * <p><i>Important: Store your custom Go classes in a package bearing your own classpath, not 
	 * in the core package! This will help avoid confusion with other authors' work.</i></p>
	 * 
	 * <p><font size="-2">You may modify any class in the goasap package to suit your project's needs. Your input 
	 * is valuable! Please join the mailing list and share your Go-based animation tools at the 
	 * GoPlayground repository. The GoASAP initiative is led by Moses Gunesch at 
	 * <a href="http://www.mosessupposes.com/" target="_top">MosesSupposes.com</a>. Please visit the 
	 * <a href="http://www.goasap.org/" target="_top">Go website</a> for more information.</font></p>
	 * </blockquote></blockquote>
	 * 
	 * <p><b>GoEngine</b> <font color="#CC0000">[This section updated recently!]</font></p>
	 * 
	 * <p>GoEngine sits at the center of the Go system, and along with the IUpdatable 
	 * interface is the only required element for using GoASAP. GoEngine manages tightly 
	 * synchronized item lists, since updating items in groups enhances efficiency. An 
	 * advantage of GoASAP is that wildly different animation systems can be used together
	 * in the same project. Their synchronous updates will remain as efficient as possible, 
	 * instead of fighting one another for processor cycles.</p>
	 * 
	 * <p>GoEngine's default pulse rate is ENTER_FRAME which yields the smoothest processing in the 
	 * Flash Player. However, it does not run on any one specific pulse. Instead, any object that is 
	 * IUpdatable may specify its own pulse rate, and items with matching pulses are automatically 
	 * grouped into update lists for efficiency. On a fine-tuning level, GoEngine uses a few other 
	 * tricks to try and provide the tightest possible visual synchronization for larger batches of
	 * animation items. It passes the clock time at the start of each update cycle to each item in 
	 * that list, which can be used in place of realtime to counteract any offset due to processing 
	 * lag during the cycle. Additionally, items that get added <i>during</i> an update cycle are 
	 * queued until the next update.</p>
	 * 
	 * <p>GoASAP's management layer is made up of three interfaces that are referenced by GoEngine: 
	 * IManager, ILiveManager and IManageable. Managers are always optional in GoASAP, and are only
	 * activated by calling <code>GoEngine.addManager()</code>. Managers can automate processes
	 * as items are added and removed, such as the included OverlapMonitor class which prevents
	 * property conflicts between items, or they can automate "live" processes that occur on each
	 * pulse. No live managers are included but an example might be a class that re-renders a 3D
	 * viewport after all 3D tweens have been processed. This can of course be done without a custom
	 * manager, but by using GoASAP you gain a unique ability to very cleanly and simply tie any
	 * custom routines in your project right into your animation processing, in perfect sync and 
	 * with maximum efficiency.</p>
	 * 
	 * <p></i>{In the game of Go, the wooden playing board, or Goban, features a grid
	 *  on which black & white go-ishi stones are laid at its intersections.}</i></p>
	 * 
	 * @see org.goasap.items.LinearGo LinearGo
	 * @see org.goasap.interfaces.IManager IManager
	 * @author Moses Gunesch
	 */
	public class GoEngine 
	{
		// -== Constants ==-
		
		public static const INFO:String = "GoASAP 0.5.0 (c) Moses Gunesch, MIT Licensed.";
		
		// -== Settable Class Defaults ==-
		
		/**
		 * A pulseInterval that runs on the player's natural framerate, 
		 * which is often most efficient.
		 */
		public static const ENTER_FRAME	: int = -1;

		// -== Protected Properties ==-
		
		// Note: Various formats for item data have been experimented with including breaking the item lists out into
		// a GoEngineList class, which was nicer-looking but did not perform well. Since GoEngine doesn't normally
		// require active work, this less-pretty but efficient flat-data format was opted for. A minor weakness of this
		// format is its use of a Dictionary, which means update calls are not ordered like they would be with an Array.
		// The Dictionary stores items' pulseInterval values, which is safer than relying on items to not change them.
		// Tests also show that Dictionary performs faster than Array for accessing and deleting items.
		private static var managerTable : Object = new Object(); // registration list of IManager instances
		private static var managers : Array = new Array(); // ordered registration list of IManager instances
		private static var liveManagers : uint = 0;
		private static var timers : Dictionary = new Dictionary(false); // key: pulseInterval, value: Timer for that pulse
		private static var items : Dictionary = new Dictionary(false); // key: IUpdatable item, value: pulseInterval at add.
		private static var itemCounts : Dictionary = new Dictionary(false); // key: pulseInterval, value: item count for that pulse
		private static var pulseSprite : Sprite; // used for ENTER_FRAME pulse
		private static var paused : Boolean = false;
		
		// These additional lists enables caching of items that are added during the update cycle for the same pulse.
		// This prevents groups & sequences from going out of sync by ensuring that each cycle completes before new items are added.
		private static var lockedPulses : Dictionary = new Dictionary(false); // key: pulseInterval, value: true
		private static var delayedPulses : Dictionary = new Dictionary(false); // key: pulseInterval, value: true
		private static var addQueue : Dictionary = new Dictionary(false); // key: IUpdatable item, value: true
		
		// -== Public Class Methods ==-
		
		/**
		 * @param className		A string naming the manager class, such as "OverlapMonitor".
		 * @return				The manager instance, if registered.
		 * @see #addManager()
		 * @see #removeManager()
		 */
		public static function getManager(className:String) : IManager
		{
			return managerTable[ className ];
		}
		
		/**
		 * Enables the extending of this class' functionality with a tight
		 * coupling to an IManager. 
		 * 
		 * <p>Tight coupling is crucial in such a time-sensitive context; 
		 * standard events are too asynchronous. All items that implement 
		 * IManageable are reported to registered managers as they add and 
		 * remove themselves from GoEngine.</p>
		 * 
		 * <p>Managers normally act as singletons within the Go system (which 
		 * you are welcome to modify). This method throws a DuplicateManagerError 
		 * if an instance of the same manager class is already registered. Use a 
		 * try/catch block when calling this method if your program might duplicate 
		 * managers, or use getManager() to check for prior registration.</p>
		 * 
		 * @param instance	An instance of a manager you wish to add.
		 * @see #getManager()
		 * @see #removeManager()
		 */
		public static function addManager( instance:IManager ):void
		{
			var className:String = getQualifiedClassName(instance);
			className = className.slice(className.lastIndexOf("::")+2);
			if (managerTable[ className ]) {
				throw new DuplicateManagerError( className );
				return;
			}
			managerTable[ className ] = instance;
			managers.push(instance);
			if (instance is ILiveManager) liveManagers++;
		}
		
		/**
		 * Unregisters any manager set in <code>addManager</code>.
		 * 
		 * @param className		A string naming the manager class, such as "OverlapMonitor".
		 * @see #getManager()
		 * @see #addManager()
		 */
		public static function removeManager( className:String ):void
		{
			managers.splice(managers.indexOf(managerTable[ className ]), 1);
			if (managerTable[ className ] is ILiveManager)
				liveManagers--;
			delete managerTable[ className ]; // leave last
		}
		
		/**
		 * Test whether an item is currently stored and being updated by the engine.
		 * 
		 * @param item		Any object implementing IUpdatable
		 * @return			Whether the IUpdatable is in the engine
		 */
		public static function hasItem( item:IUpdatable ):Boolean
		{
			return (items[ item ]!=null);
		}
		
		/**
		 * Adds an IUpdatable instance to an update-queue corresponding to
		 * the item's pulseInterval property.
		 * 
		 * @param item		Any object implementing IUpdatable that wishes 
		 * 					to receive update calls on a pulse.
		 * 					
		 * @return			Returns false only if this item was already in the
		 * 					engine under the same pulse. (If an existing item is added
		 * 					but the pulseInterval has changed it will be removed,
		 * 					re-added, and true will be returned.)
		 * 					
		 * @see #removeItem()
		 */
		public static function addItem( item:IUpdatable ):Boolean
		{
			// Group items by pulse for efficient update cycles.
			var interval:int = item.pulseInterval;
			if (items[ item ]) {
				if (items[ item ] == item.pulseInterval)
					return false;
				else
					removeItem(item);
			}
			if (lockedPulses[ interval ]==true) { // this prevents items from being added during an update loop in progress.
				delayedPulses[ interval ] = true; // flags update to clear the queue when the in-progress loop completes.
				addQueue[ item ] = true; // for tightest syncing of item groups, read the documentation under GoItem.update().
			}
			items[ item ] = interval; // Tether item to original pulseint. Used in removeItem & setPaused(false).
			if (!timers[ interval ]) {
				addPulse( interval );
				itemCounts[ interval ] = 1;
			}
			else {
				itemCounts[ interval ] ++;
			}
			// Report IManageable instances to registered managers
			if (item is IManageable) {
				for each (var manager:IManager in managers)
					manager.reserve( item as IManageable );
			}
			return true;
		}
		
		/**
		 * Removes an item from the queue and removes its pulse timer if
		 * the queue is depleted.
		 * 
		 * @param item		Any IUpdatable previously added that wishes 
		 * 					to stop receiving update calls.
		 * 					
		 * @return			Returns false if the item was not in the engine.
		 * 
		 * @see #addItem()
		 */
		public static function removeItem( item:IUpdatable ):Boolean
		{
			if (items[ item ]==null)
				return false;
			var interval: int = items[ item ];
			if ( -- itemCounts[ interval ] == 0 ) {
				removePulse( interval );
				delete itemCounts[ interval ];
			}
			delete items[ item ];
			delete addQueue[ item ]; // * see note following update
			// Report IManageable item removal to registered managers.
			if (item is IManageable) {
				for each (var manager:IManager in managers) 
					manager.release( item as IManageable );
			}
			return true;
		}
		
		/**
		 * Removes all items and resets the engine, 
		 * or removes just items running on a specific pulse.
		 * 
		 * @param pulseInterval		Optionally filter by a specific pulse 
		 * 							such as ENTER_FRAME or a number of milliseconds.
		 * @return					The number of items successfully removed.
		 * @see #removeItem()
		 */
		public static function clear(pulseInterval:Number = NaN) : uint
		{
			var all:Boolean = (isNaN(pulseInterval));
			var n:Number = 0;
			for (var item:Object in items) {
				if (all || items[ item ]==pulseInterval)
					if (removeItem(item as IUpdatable)==true)
						n++;
			}
			return n;
		}
		
		/**
		 * Retrieves number of active items in the engine 
		 * or active items running on a specific pulse.
		 * 
		 * @param pulseInterval		Optionally filter by a specific pulseInterval
		 *							such as ENTER_FRAME or a number of milliseconds.
		 * 
		 * @return					Number of active items in the Engine.
		 */
		public static function getCount(pulseInterval:Number = NaN) : uint
		{
			if (!isNaN(pulseInterval))
				return (itemCounts[pulseInterval]);
			var n:Number = 0;
			for each (var count: int in itemCounts)
				n += count;
			return n;
		}
		
		/**
		 * @return			The paused state of engine.
		 * @see #setPaused()
		 */
		public static function getPaused() : Boolean {
			return paused;
		}
		
		/**
		 * Pauses or resumes all animation globally by suspending processing,
		 * and calls pause() or resume() on each item with those methods. 
		 * 
		 * <p>The return value only reflects how many items had pause() or resume()
		 * called on them, but the GoEngine.getPaused() state will change if any 
		 * pulses are suspended or resumed.</p>
		 * 
		 * @param pause				Pass false to resume if currently paused.
		 * @param pulseInterval		Optionally filter by a specific pulse 
		 * 							such as ENTER_FRAME or a number of milliseconds.
		 * @return					The number of items on which a pause() or resume()
		 * 							method was called (0 doesn't necessarily reflect
		 * 							whether the GoEngine.getPaused() state changed, it
		 * 							may simply indicate that no items had that method).
		 * @see #resume()
		 */
		public static function setPaused(pause:Boolean=true, pulseInterval:Number = NaN) : uint
		{
			if (paused==pause) return 0;
			var n:Number = 0;
			var pulseChanged:Boolean = false;
			var all:Boolean = (isNaN(pulseInterval));
			var method:String = (pause ? "pause" : "resume");
			for (var item:Object in items) {
				var pulse:int = (items[item] as int);
				if (all || pulse==pulseInterval) {
					pulseChanged = (pulseChanged || (pause ? removePulse(pulse) : addPulse(pulse)));
					// call pause or resume on the item if it has such a method.
					if (item.hasOwnProperty(method)) {
						if (item[method] is Function) {
							item[method].apply(item);
							n++;
						}
					}
				}
			}
			if (pulseChanged)
				paused = pause;
			return n;
		}
		
		// -== Private Class Methods ==-
		
		/**
		 * Executes the update queue corresponding to the dispatcher's interval.
		 * 
		 * @param event			TimerEvent or Sprite ENTER_FRAME Event
		 */
		private static function update(event:Event) : void 
		{
			var currentTime:Number = getTimer();
			var pulse:int = (event is TimerEvent ? ( event.target as Timer ).delay : ENTER_FRAME);
			lockedPulses[ pulse ] = true;
			var doLiveUpdate:Boolean = (liveManagers > 0);
			var updated:Array;
			if (doLiveUpdate) updated = []; // syncs the live manager list to items actually updated
			for (var item:* in items) {
				if (items[ item ]==pulse && !addQueue[ item ]) {
					(item as IUpdatable).update(currentTime);
					if (doLiveUpdate) updated.push(item);
				}
			}
			lockedPulses[ pulse ] = false;
			if (delayedPulses[ pulse ]) {
				for (item in addQueue) 
					delete addQueue[ item ];
				delete delayedPulses[ pulse ];
			}
// updateAfterEvent() should not be needed as long as items follow tight-syncing instructions in GoItem.update() documentation.
//			if (pulse!=ENTER_FRAME) (event as TimerEvent).updateAfterEvent();
			if (doLiveUpdate)
				for each (var manager:Object in managers)
					if (manager is ILiveManager)
						(manager as ILiveManager).onUpdate(pulse, updated, currentTime);  // * see note
		}
// * note: In one rare case that has not been reported yet but is theoretically possible, the 'updated' list 
// passed could contain already-released items. This could only happen if the item is removed & released 
// just after the main update cycle but before the the doLiveUpdate() routine runs. If you encounter this issue
// please report it to the GoASAP mailing list, it's too involved to bother with before it's a problem.

		/**
		 * Creates new timers when a previously unused interval is specified,
		 * and tracks the number of items associated with that interval.
		 * 
		 * @param pulse			The pulseInterval requested
		 * @return				Whether a pulse was added
		 */
		private static function addPulse(pulse : int) : Boolean
		{
			if (pulse==ENTER_FRAME) {
				if (!pulseSprite) {
					timers[ENTER_FRAME] = pulseSprite = new Sprite();
					pulseSprite.addEventListener(Event.ENTER_FRAME, update);
				}
				return true;
			}
			var t:Timer = timers[ pulse ] as Timer;
			if (!t) {
				t = timers[ pulse ] = new Timer(pulse);
				(timers[ pulse ] as Timer).addEventListener(TimerEvent.TIMER, update);
				t.start();
				return true;
			}
			return false;
		}
		
		/**
		 * Tracks whether a removed item was the last one using a timer 
		 * and if so, removes that timer.
		 * 
		 * @param pulse			The pulseInterval corresponding to an item being removed.
		 * @return				Whether a pulse was removed
		 */
		private static function removePulse(pulse : int) : Boolean
		{
			if (pulse==ENTER_FRAME) {
				if (pulseSprite) {
					pulseSprite.removeEventListener(Event.ENTER_FRAME, update);
					delete timers[ ENTER_FRAME ];
					pulseSprite = null;
					return true;
				}
			}
			var t:Timer = timers[ pulse ] as Timer;
			if (t) {
				t.stop();
				t.removeEventListener(TimerEvent.TIMER, update);
				delete timers[ pulse ];
				return true;
			}
			return false;
		}
	}
}
\ No newline at end of file
+/**
 * Copyright (c) 2007 Moses Gunesch
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.goasap {
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.TimerEvent;
	import flash.utils.Dictionary;
	import flash.utils.Timer;
	import flash.utils.getQualifiedClassName;
	import flash.utils.getTimer;
	
	import org.goasap.errors.DuplicateManagerError;
	import org.goasap.interfaces.IManageable;
	import org.goasap.interfaces.IManager;
	import org.goasap.interfaces.IUpdatable;
	import org.goasap.interfaces.ILiveManager;	

	/**
	 * Provides <code>update</code> calls to <code>IUpdatable</code> instances on their specified <code>pulseInterval</code>.
	 * 
	 * <blockquote><blockquote>
	 * <p><b>Using these Docs</b></p>
	 * 
	 * <p><i>Protected methods and properties have been excluded in almost all 
	 * cases, but are documented in the classes. Exceptions include key protected
	 * methods or properties that are integral for writing subclasses or understanding 
	 * the basic mechanics of the system. Many Go classes can be used as is without 
	 * subclassing, so the documentation offers an uncluttered view of their public
	 * usage.</i></p>
	 * 
	 * <p><b>Introduction to Go</b> <font color="#CC0000">[This section updated recently!]</font></p>
	 * 
	 * <p>The Go ActionScript Animation Platform ("GOASAP") is a lightweight, portable 
	 * set of generic base classes for buliding AS3 animation tools. It provides structure 
	 * and core functionality, but does not define the specifics of animation-handling 
	 * classes like tweens.</p>
	 * 
	 * <p>GoASAP could be broken up into the following general layers:
	 * <ul>
	 * <li><i>Compatibility:</i> In general, this layer can be used in about any animation 
	 * system. GoEngine, GoEvent, PlayStates and IPlayable.</li>
	 * <li><i>Items:</i> Base classes for utilities and animation items: PlayableBase, GoItem,
	 * LinearGo and PhysicsGo.</li>
	 * <li><i>Utilities:</i> You can write utility classes to manage items. Go ships with a 
	 * few common ones: a parallel item class called PlayableGroup, and several Sequence classes.</li>
	 * <li><i>Automation/Management:</i> GoEngine provides a simple, centralized and fully extensible 
	 * way for you to automate any time-based process and manage multiple items at once.</li>
	 * </ul></p>
	 * 
	 * <p>GoASAP provides an intentionally loose standard, in that it does not intend to limit
	 * the possibilities of what can be built with it. Its primary benefits are compatibility and
	 * synchronicity between animation systems, absolute extensibility into any time-based process,
	 * and a much faster and easier way to build your own animation tools from scratch.</p>
	 * 
	 * <p><i>Important: Store your custom Go classes in a package bearing your own classpath, not 
	 * in the core package! This will help avoid confusion with other authors' work.</i></p>
	 * 
	 * <p><font size="-2">You may modify any class in the goasap package to suit your project's needs. Your input 
	 * is valuable! Please join the mailing list and share your Go-based animation tools at the 
	 * GoPlayground repository. The GoASAP initiative is led by Moses Gunesch at 
	 * <a href="http://www.mosessupposes.com/" target="_top">MosesSupposes.com</a>. Please visit the 
	 * <a href="http://www.goasap.org/" target="_top">Go website</a> for more information.</font></p>
	 * </blockquote></blockquote>
	 * 
	 * <p><b>GoEngine</b> <font color="#CC0000">[This section updated recently!]</font></p>
	 * 
	 * <p>GoEngine sits at the center of the Go system, and along with the IUpdatable 
	 * interface is the only required element for using GoASAP. GoEngine manages tightly 
	 * synchronized item lists, since updating items in groups enhances efficiency. An 
	 * advantage of GoASAP is that wildly different animation systems can be used together
	 * in the same project. Their synchronous updates will remain as efficient as possible, 
	 * instead of fighting one another for processor cycles.</p>
	 * 
	 * <p>GoEngine's default pulse rate is ENTER_FRAME which yields the smoothest processing in the 
	 * Flash Player. However, it does not run on any one specific pulse. Instead, any object that is 
	 * IUpdatable may specify its own pulse rate, and items with matching pulses are automatically 
	 * grouped into update lists for efficiency. On a fine-tuning level, GoEngine uses a few other 
	 * tricks to try and provide the tightest possible visual synchronization for larger batches of
	 * animation items. It passes the clock time at the start of each update cycle to each item in 
	 * that list, which can be used in place of realtime to counteract any offset due to processing 
	 * lag during the cycle. Additionally, items that get added <i>during</i> an update cycle are 
	 * queued until the next update.</p>
	 * 
	 * <p>GoASAP's management layer is made up of three interfaces that are referenced by GoEngine: 
	 * IManager, ILiveManager and IManageable. Managers are always optional in GoASAP, and are only
	 * activated by calling <code>GoEngine.addManager()</code>. Managers can automate processes
	 * as items are added and removed, such as the included OverlapMonitor class which prevents
	 * property conflicts between items, or they can automate "live" processes that occur on each
	 * pulse. No live managers are included but an example might be a class that re-renders a 3D
	 * viewport after all 3D tweens have been processed. This can of course be done without a custom
	 * manager, but by using GoASAP you gain a unique ability to very cleanly and simply tie any
	 * custom routines in your project right into your animation processing, in perfect sync and 
	 * with maximum efficiency.</p>
	 * 
	 * <p></i>{In the game of Go, the wooden playing board, or Goban, features a grid
	 *  on which black & white go-ishi stones are laid at its intersections.}</i></p>
	 * 
	 * @see org.goasap.items.LinearGo LinearGo
	 * @see org.goasap.interfaces.IManager IManager
	 * @author Moses Gunesch
	 */
	public class GoEngine 
	{
		// -== Constants ==-
		
		public static const INFO:String = "GoASAP 0.5.1 (c) Moses Gunesch, MIT Licensed.";
		
		// -== Settable Class Defaults ==-
		
		/**
		 * A pulseInterval that runs on the player's natural framerate, 
		 * which is often most efficient.
		 */
		public static const ENTER_FRAME	: int = -1;

		// -== Protected Properties ==-
		
		// Note: Various formats for item data have been experimented with including breaking the item lists out into
		// a GoEngineList class, which was nicer-looking but did not perform well. Since GoEngine doesn't normally
		// require active work, this less-pretty but efficient flat-data format was opted for. A minor weakness of this
		// format is its use of a Dictionary, which means update calls are not ordered like they would be with an Array.
		// The Dictionary stores items' pulseInterval values, which is safer than relying on items to not change them.
		// Tests also show that Dictionary performs faster than Array for accessing and deleting items.
		private static var managerTable : Object = new Object(); // registration list of IManager instances
		private static var managers : Array = new Array(); // ordered registration list of IManager instances
		private static var liveManagers : uint = 0;
		private static var timers : Dictionary = new Dictionary(false); // key: pulseInterval, value: Timer for that pulse
		private static var items : Dictionary = new Dictionary(false); // key: IUpdatable item, value: pulseInterval at add.
		private static var itemCounts : Dictionary = new Dictionary(false); // key: pulseInterval, value: item count for that pulse
		private static var pulseSprite : Sprite; // used for ENTER_FRAME pulse
		private static var paused : Boolean = false;
		
		// These additional lists enables caching of items that are added during the update cycle for the same pulse.
		// This prevents groups & sequences from going out of sync by ensuring that each cycle completes before new items are added.
		private static var lockedPulses : Dictionary = new Dictionary(false); // key: pulseInterval, value: true
		private static var delayedPulses : Dictionary = new Dictionary(false); // key: pulseInterval, value: true
		private static var addQueue : Dictionary = new Dictionary(false); // key: IUpdatable item, value: true
		
		// -== Public Class Methods ==-
		
		/**
		 * @param className		A string naming the manager class, such as "OverlapMonitor".
		 * @return				The manager instance, if registered.
		 * @see #addManager()
		 * @see #removeManager()
		 */
		public static function getManager(className:String) : IManager
		{
			return managerTable[ className ];
		}
		
		/**
		 * Enables the extending of this class' functionality with a tight
		 * coupling to an IManager. 
		 * 
		 * <p>Tight coupling is crucial in such a time-sensitive context; 
		 * standard events are too asynchronous. All items that implement 
		 * IManageable are reported to registered managers as they add and 
		 * remove themselves from GoEngine.</p>
		 * 
		 * <p>Managers normally act as singletons within the Go system (which 
		 * you are welcome to modify). This method throws a DuplicateManagerError 
		 * if an instance of the same manager class is already registered. Use a 
		 * try/catch block when calling this method if your program might duplicate 
		 * managers, or use getManager() to check for prior registration.</p>
		 * 
		 * @param instance	An instance of a manager you wish to add.
		 * @see #getManager()
		 * @see #removeManager()
		 */
		public static function addManager( instance:IManager ):void
		{
			var className:String = getQualifiedClassName(instance);
			className = className.slice(className.lastIndexOf("::")+2);
			if (managerTable[ className ]) {
				throw new DuplicateManagerError( className );
				return;
			}
			managerTable[ className ] = instance;
			managers.push(instance);
			if (instance is ILiveManager) liveManagers++;
		}
		
		/**
		 * Unregisters any manager set in <code>addManager</code>.
		 * 
		 * @param className		A string naming the manager class, such as "OverlapMonitor".
		 * @see #getManager()
		 * @see #addManager()
		 */
		public static function removeManager( className:String ):void
		{
			managers.splice(managers.indexOf(managerTable[ className ]), 1);
			if (managerTable[ className ] is ILiveManager)
				liveManagers--;
			delete managerTable[ className ]; // leave last
		}
		
		/**
		 * Test whether an item is currently stored and being updated by the engine.
		 * 
		 * @param item		Any object implementing IUpdatable
		 * @return			Whether the IUpdatable is in the engine
		 */
		public static function hasItem( item:IUpdatable ):Boolean
		{
			return (items[ item ]!=null);
		}
		
		/**
		 * Adds an IUpdatable instance to an update-queue corresponding to
		 * the item's pulseInterval property.
		 * 
		 * @param item		Any object implementing IUpdatable that wishes 
		 * 					to receive update calls on a pulse.
		 * 					
		 * @return			Returns false only if this item was already in the
		 * 					engine under the same pulse. (If an existing item is added
		 * 					but the pulseInterval has changed it will be removed,
		 * 					re-added, and true will be returned.)
		 * 					
		 * @see #removeItem()
		 */
		public static function addItem( item:IUpdatable ):Boolean
		{
			// Group items by pulse for efficient update cycles.
			var interval:int = item.pulseInterval;
			if (items[ item ]) {
				if (items[ item ] == item.pulseInterval)
					return false;
				else
					removeItem(item);
			}
			if (lockedPulses[ interval ]==true) { // this prevents items from being added during an update loop in progress.
				delayedPulses[ interval ] = true; // flags update to clear the queue when the in-progress loop completes.
				addQueue[ item ] = true; // for tightest syncing of item groups, read the documentation under GoItem.update().
			}
			items[ item ] = interval; // Tether item to original pulseint. Used in removeItem & setPaused(false).
			if (!timers[ interval ]) {
				addPulse( interval );
				itemCounts[ interval ] = 1;
			}
			else {
				itemCounts[ interval ] ++;
			}
			// Report IManageable instances to registered managers
			if (item is IManageable) {
				for each (var manager:IManager in managers)
					manager.reserve( item as IManageable );
			}
			return true;
		}
		
		/**
		 * Removes an item from the queue and removes its pulse timer if
		 * the queue is depleted.
		 * 
		 * @param item		Any IUpdatable previously added that wishes 
		 * 					to stop receiving update calls.
		 * 					
		 * @return			Returns false if the item was not in the engine.
		 * 
		 * @see #addItem()
		 */
		public static function removeItem( item:IUpdatable ):Boolean
		{
			if (items[ item ]==null)
				return false;
			var interval: int = items[ item ];
			if ( -- itemCounts[ interval ] == 0 ) {
				removePulse( interval );
				delete itemCounts[ interval ];
			}
			delete items[ item ];
			delete addQueue[ item ]; // * see note following update
			// Report IManageable item removal to registered managers.
			if (item is IManageable) {
				for each (var manager:IManager in managers) 
					manager.release( item as IManageable );
			}
			return true;
		}
		
		/**
		 * Removes all items and resets the engine, 
		 * or removes just items running on a specific pulse.
		 * 
		 * @param pulseInterval		Optionally filter by a specific pulse 
		 * 							such as ENTER_FRAME or a number of milliseconds.
		 * @return					The number of items successfully removed.
		 * @see #removeItem()
		 */
		public static function clear(pulseInterval:Number = NaN) : uint
		{
			var all:Boolean = (isNaN(pulseInterval));
			var n:Number = 0;
			for (var item:Object in items) {
				if (all || items[ item ]==pulseInterval)
					if (removeItem(item as IUpdatable)==true)
						n++;
			}
			return n;
		}
		
		/**
		 * Retrieves number of active items in the engine 
		 * or active items running on a specific pulse.
		 * 
		 * @param pulseInterval		Optionally filter by a specific pulseInterval
		 *							such as ENTER_FRAME or a number of milliseconds.
		 * 
		 * @return					Number of active items in the Engine.
		 */
		public static function getCount(pulseInterval:Number = NaN) : uint
		{
			if (!isNaN(pulseInterval))
				return (itemCounts[pulseInterval]);
			var n:Number = 0;
			for each (var count: int in itemCounts)
				n += count;
			return n;
		}
		
		/**
		 * @return			The paused state of engine.
		 * @see #setPaused()
		 */
		public static function getPaused() : Boolean {
			return paused;
		}
		
		/**
		 * Pauses or resumes all animation globally by suspending processing,
		 * and calls pause() or resume() on each item with those methods. 
		 * 
		 * <p>The return value only reflects how many items had pause() or resume()
		 * called on them, but the GoEngine.getPaused() state will change if any 
		 * pulses are suspended or resumed.</p>
		 * 
		 * @param pause				Pass false to resume if currently paused.
		 * @param pulseInterval		Optionally filter by a specific pulse 
		 * 							such as ENTER_FRAME or a number of milliseconds.
		 * @return					The number of items on which a pause() or resume()
		 * 							method was called (0 doesn't necessarily reflect
		 * 							whether the GoEngine.getPaused() state changed, it
		 * 							may simply indicate that no items had that method).
		 * @see #resume()
		 */
		public static function setPaused(pause:Boolean=true, pulseInterval:Number = NaN) : uint
		{
			if (paused==pause) return 0;
			var n:Number = 0;
			var pulseChanged:Boolean = false;
			var all:Boolean = (isNaN(pulseInterval));
			var method:String = (pause ? "pause" : "resume");
			for (var item:Object in items) {
				var pulse:int = (items[item] as int);
				if (all || pulse==pulseInterval) {
					pulseChanged = (pulseChanged || (pause ? removePulse(pulse) : addPulse(pulse)));
					// call pause or resume on the item if it has such a method.
					if (item.hasOwnProperty(method)) {
						if (item[method] is Function) {
							item[method].apply(item);
							n++;
						}
					}
				}
			}
			if (pulseChanged)
				paused = pause;
			return n;
		}
		
		// -== Private Class Methods ==-
		
		/**
		 * Executes the update queue corresponding to the dispatcher's interval.
		 * 
		 * @param event			TimerEvent or Sprite ENTER_FRAME Event
		 */
		private static function update(event:Event) : void 
		{
			var currentTime:Number = getTimer();
			var pulse:int = (event is TimerEvent ? ( event.target as Timer ).delay : ENTER_FRAME);
			lockedPulses[ pulse ] = true;
			var doLiveUpdate:Boolean = (liveManagers > 0);
			var updated:Array;
			if (doLiveUpdate) updated = []; // syncs the live manager list to items actually updated
			for (var item:* in items) {
				if (items[ item ]==pulse && !addQueue[ item ]) {
					(item as IUpdatable).update(currentTime);
					if (doLiveUpdate) updated.push(item);
				}
			}
			lockedPulses[ pulse ] = false;
			if (delayedPulses[ pulse ]) {
				for (item in addQueue) 
					delete addQueue[ item ];
				delete delayedPulses[ pulse ];
			}
// updateAfterEvent() should not be needed as long as items follow tight-syncing instructions in GoItem.update() documentation.
//			if (pulse!=ENTER_FRAME) (event as TimerEvent).updateAfterEvent();
			if (doLiveUpdate)
				for each (var manager:Object in managers)
					if (manager is ILiveManager)
						(manager as ILiveManager).onUpdate(pulse, updated, currentTime);  // * see note
		}
// * note: In one rare case that has not been reported yet but is theoretically possible, the 'updated' list 
// passed could contain already-released items. This could only happen if the item is removed & released 
// just after the main update cycle but before the the doLiveUpdate() routine runs. If you encounter this issue
// please report it to the GoASAP mailing list, it's too involved to bother with before it's a problem.

		/**
		 * Creates new timers when a previously unused interval is specified,
		 * and tracks the number of items associated with that interval.
		 * 
		 * @param pulse			The pulseInterval requested
		 * @return				Whether a pulse was added
		 */
		private static function addPulse(pulse : int) : Boolean
		{
			if (pulse==ENTER_FRAME) {
				if (!pulseSprite) {
					timers[ENTER_FRAME] = pulseSprite = new Sprite();
					pulseSprite.addEventListener(Event.ENTER_FRAME, update);
				}
				return true;
			}
			var t:Timer = timers[ pulse ] as Timer;
			if (!t) {
				t = timers[ pulse ] = new Timer(pulse);
				(timers[ pulse ] as Timer).addEventListener(TimerEvent.TIMER, update);
				t.start();
				return true;
			}
			return false;
		}
		
		/**
		 * Tracks whether a removed item was the last one using a timer 
		 * and if so, removes that timer.
		 * 
		 * @param pulse			The pulseInterval corresponding to an item being removed.
		 * @return				Whether a pulse was removed
		 */
		private static function removePulse(pulse : int) : Boolean
		{
			if (pulse==ENTER_FRAME) {
				if (pulseSprite) {
					pulseSprite.removeEventListener(Event.ENTER_FRAME, update);
					delete timers[ ENTER_FRAME ];
					pulseSprite = null;
					return true;
				}
			}
			var t:Timer = timers[ pulse ] as Timer;
			if (t) {
				t.stop();
				t.removeEventListener(TimerEvent.TIMER, update);
				delete timers[ pulse ];
				return true;
			}
			return false;
		}
	}
}
\ No newline at end of file

Modified: trunk/goasap/src_go/org/goasap/utils/PlayableGroup.as
==============================================================================
--- trunk/goasap/src_go/org/goasap/utils/PlayableGroup.as	(original)
+++ trunk/goasap/src_go/org/goasap/utils/PlayableGroup.as	Sun Jul 27 14:13:13 2008
@@ -1 +1 @@
-/**
 * Copyright (c) 2007 Moses Gunesch
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.goasap.utils {
	import flash.utils.Dictionary;
	
	import org.goasap.PlayStates;
	import org.goasap.PlayableBase;
	import org.goasap.events.GoEvent;
	import org.goasap.interfaces.IPlayable;
	import org.goasap.managers.Repeater;	

	/**
	 * Dispatched when the group starts.
	 * @eventType org.goasap.events.START
	 */
	[Event(name="START", type="org.goasap.events.GoEvent")]

	/**
	 * Dispatched when the group is paused successfully.
	 * @eventType org.goasap.events.PAUSE
	 */
	[Event(name="PAUSE", type="org.goasap.events.GoEvent")]

	/**
	 * Dispatched when the group is resumed successfully.
	 * @eventType org.goasap.events.RESUME
	 */
	[Event(name="RESUME", type="org.goasap.events.GoEvent")]

	/**
	 * Dispatched at the end the group if <code>repeater.cycles</code> is set to
	 * a value other than one, just before the group starts its next play cycle.
	 * @eventType org.goasap.events.CYCLE
	 */
	[Event(name="CYCLE", type="org.goasap.events.GoEvent")]

	/**
	 * Dispatched if the group is manually stopped.
	 * @eventType org.goasap.events.STOP
	 */
	[Event(name="STOP", type="org.goasap.events.GoEvent")]

	/**
	 * Dispatched after all children have dispatched a STOP or COMPLETE event.
	 * @eventType org.goasap.events.COMPLETE
	 */
	[Event(name="COMPLETE", type="org.goasap.events.GoEvent")]


	/**
	 * Batch-play a set of items and receive an event when all of them have finished.
	 * 
	 * <p>PlayableGroup accepts any IPlayable for its children, which can include
	 * tweens, other groups, sequences and so forth. The group listens for both 
	 * GoEvent.STOP and GoEvent.COMPLETE events from its children, either of which 
	 * are counted toward group completion.</p>
	 * 
	 * <p>The <code>repeater</code> property of PlayableGroup allows you to loop play 
	 * any number of times, or indefinitely by setting its cycles to Repeater.INFINITE. 
	 * GoEvent.CYCLE is dispatched on each loop and GoEvent.COMPLETE when finished.
	 * Other events dispatched include the GoEvent types START, STOP, PAUSE, and RESUME.</p>
	 * 
	 * @author Moses Gunesch
	 */
	public class PlayableGroup extends PlayableBase implements IPlayable {
		
		// -== Public Properties ==-
		
		/**
		 * Get or set the children array. Only IPlayable items are stored. Note that 
		 * unlike the methods <code>addChild</code> and <code>removeChild</code>, 
		 * setting this property will stop any group play currently in progress.
		 */
		public function get children():Array {
			var a:Array = [];
			for (var item:Object in _children)
				a.push(item);
			return a;
		}
		public function set children(a:Array):void {
			if (_listeners > 0)
				stop();
			for each (var item:Object in a)
				if (item is IPlayable)
					addChild(item as IPlayable);
		}
		
		/**
		 * The groups's Repeater instance, which may be used to make
		 * it loop and play more than one time.
		 * 
		 * <p>The Repeater's cycles property can be set to an integer, or
		 * to Repeater.INFINITE or 0 to repeat indefinitely.</p> 
		 * 
		 * <pre>
		 * var group:PlayableGroup = new PlayableGroup(tween1, tween2, tween3);
		 * group.repeater.cycles = 2;
		 * group.start();
		 * trace(group.repeater.currentCycle); // output: 0
		 * </pre>
		 */
		public function get repeater(): Repeater {
			return _repeater;
		}
		
		/**
		 * Determines the number of children currently being monitored 
		 * for completion by the group.
		 */
		public function get listenerCount() : uint {
			return _listeners;
		}

		// -== Protected Properties ==-
		
		/** @private */
		protected var _children: Dictionary = new Dictionary();
		
		/** @private */
		protected var _listeners: uint = 0;
		
		/** @private */
		protected var _repeater: Repeater;
		
		// -== Public Methods ==-
		
		/**
		 * Constructor.
		 * 
		 * @param items	Any number of IPlayable items as separate arguments, 
		 * 					or a single array of them.
		 */
		public function PlayableGroup(...items) {
			super();
			if (items.length > 0)
				this.children = ((items[ 0 ] is Array) ? items[ 0 ] : items);
			_repeater = new Repeater();
			_repeater.setParent(this);
		}
		
		/**
		 * Searches for a child with the specified playableID.
		 * 
		 * @param playableID	The item playableID to search for. (The item must have a
		 * 						property called <code>playableID</code> which is a general
		 * 						GoASAP convention established in PlayableBase.)
		 * @param deepSearch	If child is not found in the group, this option runs a 
		 * 						recursive search on any children that are PlayableGroup.
		 * @return				The SequenceStep with the matching playableID.
		 */
		public function getChildByID(playableID:*, deepSearch:Boolean=true):IPlayable {
			for (var item:Object in _children)
				if (item.hasOwnProperty("playableID") && item["playableID"]===playableID)
					return (item as IPlayable);
			if (deepSearch) {
				for (item in _children) {
					if (item is PlayableGroup) {
						var match:IPlayable = ((item as PlayableGroup).getChildByID(playableID, true));
						if (match) { return (match as IPlayable); }
					}
				}
			}
			return null;
		}
		
		/**
		 * Adds a single IPlayable to the children array (duplicates are rejected) and
		 * syncs up the group and child play-states based on various conditions.
		 * 
		 * <p>If both the group and the item being added are STOPPED, the item is simply
		 * added to the children list.</p>
		 * 
		 * <p>If both items are PAUSED or PLAYING (including PLAYING_DELAY for children), 
		 * the child is actively added to the group during play and will be monitored for 
		 * completion along with others.</p>
		 * 
		 * <p>In other cases where the child's state mismatches the group's state, there
		 * are several behaviors available. Normally if the second parameter <code>adoptChildState</code>
		 * is left false, the child's mismatched state will be updated to match the group's
		 * state. This can result in it being stopped, paused, or started/resumed and monitored 
		 * for completion along with other children. Passing true for <code>adoptChildState</code> 
		 * results in updating the group's state to match the child's. This option could be used, for 
		 * example, if you wanted to build a group of already-playing items without disrupting their
		 * play cycle with a start() call to the group.</p>
		 * 
		 * @param item				Any instance that implements IPlayable and uses PlayState constants.
		 * @param adoptChildState	Makes this group change its play-state to match the state of the new child.
		 * @return Success.
		 */
		public function addChild(item:IPlayable, adoptChildState:Boolean=false): Boolean {
			if (_children[ item ])
				return false;
			if (item.state!=_state) { // Resolve an obvious mismatched play state...
									  // Normally states are both STOPPED, so the following ugliness is rarely used.
				var primary:IPlayable = (adoptChildState ? item : this);
				var primaryPlaying:Boolean = (primary.state==PlayStates.PLAYING || primary.state==PlayStates.PLAYING_DELAY);
				var secondary:IPlayable = (adoptChildState ? this : item);
				var secondaryPlaying:Boolean = (secondary.state==PlayStates.PLAYING || secondary.state==PlayStates.PLAYING_DELAY);
				if ( !(primaryPlaying && secondaryPlaying) ) // Less obvious, but treat PLAYING_DELAY & PLAYING as "playing."
				{
					switch (primary.state) {
						case PlayStates.STOPPED:
							secondary.stop();
							break;
						case PlayStates.PAUSED: // This case works either way. Both START & PAUSE events will result.
							if (secondary.state==PlayStates.STOPPED)
								secondary.start();
							secondary.pause();
							break;
						case PlayStates.PLAYING: 
						case PlayStates.PLAYING_DELAY:
							if (secondary.state==PlayStates.PAUSED)
								secondary.resume();
							else if (secondary.state==PlayStates.STOPPED) {
								if (adoptChildState) {
									_state = PlayStates.PLAYING; // Group adopts child playing state
									dispatchEvent(new GoEvent( GoEvent.START));
								} 
								else {
									secondary.start();
								}
							}
							break;
					}
				}
			}
			// Saved until after possible play-state changes. Now we can base listening on this group's state.
			_children[ item ] = false;
			if (_state!=PlayStates.STOPPED)
				listenTo(item);
			return true;
		}
		
		/**
		 * Removes a single IPlayable from the children array.
		 * 
		 * <p>Note that if play is in progress when a child is added it does not
		 * interrupt play and the child is monitored for completion along with
		 * others.</p>
		 * 
		 * @param item		Any instance that implements IPlayable and uses PlayState constants.
		 * @return Success.
		 */
		public function removeChild(item:IPlayable): Boolean {
			var v:* = _children[ item ];
			if (v===null)
				return false;
			if (v===true)
				unListenTo( item );
			delete _children[ item ];
			return true;
		}
		
		/**
		 * Test whether any child has a particular PlayState.
		 * 
		 * <pre>
		 * // Example: resume a paused group
		 * if ( myGroup.anyChildHasState(PlayStates.PlayStates.PAUSED) ) {
		 *     myGroup.resume();
		 * }
		 * </pre>
		 * @see org.goasap.PlayStates PlayStates
		 */
		public function anyChildHasState(state:String): Boolean {
			for (var item:Object in _children)
				if ((item as IPlayable).state==state)
					return true;
			return false;
		}
		
		// -== IPlayable implementation ==-
		
		/**
		 * Calls start on all children. 
		 * 
		 * <p>If the group is active when this method is called, a <code>stop</code> call 
		 * is automated which will result in a GoEVent.STOP event being dispatched.</p>
		 * 
		 * @return Returns true if any child in the group starts successfully.
		 */
		public function start() : Boolean {
			stop();
			var r:Boolean = false;
			for (var item:Object in _children) {
				var started:Boolean = (item as IPlayable).start();
				if (started)
					listenTo(item as IPlayable);
				r = (started || r);
			}
			if (!r) return false; // all starts failed
			_state = PlayStates.PLAYING;
			dispatchEvent(new GoEvent( GoEvent.START));
			_playRetainer[ this ] = 1; // Developers - Important! Look up _playRetainer.
			return true;
		}

		/**
		 * If the group is active, this method stops all child items and 
		 * dispatches a GoEvent.STOP event.
		 * 
		 * @return Returns true only if all children in the group stop successfully.
		 */
		public function stop() : Boolean {
			if (_state == PlayStates.STOPPED)
				return false;
			_state = PlayStates.STOPPED;
			_repeater.reset();
			delete _playRetainer[ this ]; // Developers - Important! Look up _playRetainer.
			if (_listeners==0) {
				dispatchEvent(new GoEvent( GoEvent.COMPLETE ));
				return true;
			}
			var r:Boolean = true;
			for (var item:Object in _children) {
				unListenTo(item as IPlayable);
				r = ((item as IPlayable).stop() && r);
			}
			dispatchEvent(new GoEvent( GoEvent.STOP ));
			return r;
		}
		
		/**
		 * Calls <code>pause</code> on all children.
		 * 
		 * @return  Returns true only if all playing children in the group paused successfully
		 * 			and at least one child was paused.
		 */
		public function pause() : Boolean {
			if (_state!= PlayStates.PLAYING)
				return false;
			var r:Boolean = true;
			var n:uint = 0;
			for (var item:Object in _children) {
				var success:Boolean = (item as IPlayable).pause();
				if (success) n++;
				r = (r && success);
			}
			if (n>0) {
				_state = PlayStates.PAUSED; // state should reflect that at least one item was paused,
								  // while return value may indicate that not all pause calls succeeded.
				dispatchEvent(new GoEvent( GoEvent.PAUSE ));
			}
			return (n>0 && r);
		}
		
		/**
		 * Calls <code>resume</code> on all children.
		 * 
		 * @return	Returns true only if all paused children in the group resumed successfully
		 * 			and at least one child was resumed.
		 */
		public function resume() : Boolean {
			if (_state!= PlayStates.PAUSED)
				return false;
			var r:Boolean = true;
			var n:uint = 0;
			for (var item:Object in _children) {
				var success:Boolean = (item as IPlayable).resume();
				if (success) n++;
				r = (r && success);
			}
			if (n>0) {
				_state = PlayStates.PLAYING; // state should reflect that at least one item was resumed,
								  // while return value may indicate that not all resume calls succeeded.
				dispatchEvent(new GoEvent( GoEvent.RESUME ));
			}
			return (n>0 && r);
		}
		
		/**
		 * Calls <code>skipTo</code> on all children.
		 * 
		 * @return	Returns true only if all children in the group skipTo the position successfully
		 * 			and at least one child was affected.
		 */
		public function skipTo(position : Number) : Boolean {
			var r:Boolean = true;
			var n:uint = 0;
			position = _repeater.skipTo(_repeater.cycles, position); // TODO: TEST
			for (var item:Object in _children) {
				r = ((item as IPlayable).skipTo(position) && r);
				listenTo(item as IPlayable);
				n++;
			}
			_state = (r ? PlayStates.PLAYING : PlayStates.STOPPED);
			return (n>0 && r);
		}
		
		// -== Protected Methods ==-
		
		/**
		 * @private
		 * Internal handler for item completion.
		 * @param event		GoEvent dispatched by child item. 
		 */
		protected function onItemEnd(event:GoEvent) : void {
			unListenTo(event.target as IPlayable);
			if (_listeners==0) {
				complete();
			}
		}
		
		/**
		 * @private
		 * Internal handler for group completion.
		 */
		protected function complete() : void {
			if (_repeater.next()) {
				dispatchEvent(new GoEvent( GoEvent.CYCLE ));
				for (var item:Object in _children) {
					var started:Boolean = (item as IPlayable).start();
					if (started)
						listenTo(item as IPlayable);
				}
			}
			else {
				stop();
			}
		}

		/**
		 * @private
		 * Internal. Listen for item completion, keeping tight track of listeners.
		 * @param item	Any instance that extends IPlayable (IPlayable itself should not be used directly).
		 */
		protected function listenTo(item:IPlayable) : void {
			if (_children[ item ] === false) {
				item.addEventListener(GoEvent.STOP, onItemEnd, false, 0, true);
				item.addEventListener(GoEvent.COMPLETE, onItemEnd, false, 0, true);
				_children[ item ] = true;
				_listeners++;
			}
		}
		
		/**
		 * @private
		 * Internal. Stop listening for item completion.
		 * @param item	Any instance that extends IPlayable (IPlayable itself should not be used directly).
		 * @return Number of completion listeners remaining.
		 */
		protected function unListenTo(item:IPlayable) : void {
			if (_children[ item ] === true) {
				item.removeEventListener(GoEvent.STOP, onItemEnd);
				item.removeEventListener(GoEvent.COMPLETE, onItemEnd);
				_children[ item ] = false;
				_listeners--;
			}
		}
	}
}
\ No newline at end of file
+/**
 * Copyright (c) 2007 Moses Gunesch
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.goasap.utils {
	import flash.utils.Dictionary;
	
	import org.goasap.PlayStates;
	import org.goasap.PlayableBase;
	import org.goasap.events.GoEvent;
	import org.goasap.interfaces.IPlayable;
	import org.goasap.managers.Repeater;	

	/**
	 * Dispatched when the group starts.
	 * @eventType org.goasap.events.START
	 */
	[Event(name="START", type="org.goasap.events.GoEvent")]

	/**
	 * Dispatched when the group is paused successfully.
	 * @eventType org.goasap.events.PAUSE
	 */
	[Event(name="PAUSE", type="org.goasap.events.GoEvent")]

	/**
	 * Dispatched when the group is resumed successfully.
	 * @eventType org.goasap.events.RESUME
	 */
	[Event(name="RESUME", type="org.goasap.events.GoEvent")]

	/**
	 * Dispatched at the end the group if <code>repeater.cycles</code> is set to
	 * a value other than one, just before the group starts its next play cycle.
	 * @eventType org.goasap.events.CYCLE
	 */
	[Event(name="CYCLE", type="org.goasap.events.GoEvent")]

	/**
	 * Dispatched if the group is manually stopped.
	 * @eventType org.goasap.events.STOP
	 */
	[Event(name="STOP", type="org.goasap.events.GoEvent")]

	/**
	 * Dispatched after all children have dispatched a STOP or COMPLETE event.
	 * @eventType org.goasap.events.COMPLETE
	 */
	[Event(name="COMPLETE", type="org.goasap.events.GoEvent")]


	/**
	 * Batch-play a set of items and receive an event when all of them have finished.
	 * 
	 * <p>PlayableGroup accepts any IPlayable for its children, which can include
	 * tweens, other groups, sequences and so forth. The group listens for both 
	 * GoEvent.STOP and GoEvent.COMPLETE events from its children, either of which 
	 * are counted toward group completion.</p>
	 * 
	 * <p>The <code>repeater</code> property of PlayableGroup allows you to loop play 
	 * any number of times, or indefinitely by setting its cycles to Repeater.INFINITE. 
	 * GoEvent.CYCLE is dispatched on each loop and GoEvent.COMPLETE when finished.
	 * Other events dispatched include the GoEvent types START, STOP, PAUSE, and RESUME.</p>
	 * 
	 * @author Moses Gunesch
	 */
	public class PlayableGroup extends PlayableBase implements IPlayable {
		
		// -== Public Properties ==-
		
		/**
		 * Get or set the children array. Only IPlayable items are stored. Note that 
		 * unlike the methods <code>addChild</code> and <code>removeChild</code>, 
		 * setting this property will stop any group play currently in progress.
		 */
		public function get children():Array {
			var a:Array = [];
			for (var item:Object in _children)
				a.push(item);
			return a;
		}
		public function set children(a:Array):void {
			if (_listeners > 0)
				stop();
			for each (var item:Object in a)
				if (item is IPlayable)
					addChild(item as IPlayable);
		}
		
		/**
		 * The groups's Repeater instance, which may be used to make
		 * it loop and play more than one time.
		 * 
		 * <p>The Repeater's cycles property can be set to an integer, or
		 * to Repeater.INFINITE or 0 to repeat indefinitely.</p> 
		 * 
		 * <pre>
		 * var group:PlayableGroup = new PlayableGroup(tween1, tween2, tween3);
		 * group.repeater.cycles = 2;
		 * group.start();
		 * trace(group.repeater.currentCycle); // output: 0
		 * </pre>
		 */
		public function get repeater(): Repeater {
			return _repeater;
		}
		
		/**
		 * Determines the number of children currently being monitored 
		 * for completion by the group.
		 */
		public function get listenerCount() : uint {
			return _listeners;
		}

		// -== Protected Properties ==-
		
		/** @private */
		protected var _children: Dictionary = new Dictionary();
		
		/** @private */
		protected var _listeners: uint = 0;
		
		/** @private */
		protected var _repeater: Repeater;
		
		// -== Public Methods ==-
		
		/**
		 * Constructor.
		 * 
		 * @param items	Any number of IPlayable items as separate arguments, 
		 * 					or a single array of them.
		 */
		public function PlayableGroup(...items) {
			super();
			if (items.length > 0)
				this.children = ((items[ 0 ] is Array) ? items[ 0 ] : items);
			_repeater = new Repeater();
			_repeater.setParent(this);
		}
		
		/**
		 * Searches for a child with the specified playableID.
		 * 
		 * @param playableID	The item playableID to search for. (The item must have a
		 * 						property called <code>playableID</code> which is a general
		 * 						GoASAP convention established in PlayableBase.)
		 * @param deepSearch	If child is not found in the group, this option runs a 
		 * 						recursive search on any children that are PlayableGroup.
		 * @return				The SequenceStep with the matching playableID.
		 */
		public function getChildByID(playableID:*, deepSearch:Boolean=true):IPlayable {
			for (var item:Object in _children)
				if (item.hasOwnProperty("playableID") && item["playableID"]===playableID)
					return (item as IPlayable);
			if (deepSearch) {
				for (item in _children) {
					if (item is PlayableGroup) {
						var match:IPlayable = ((item as PlayableGroup).getChildByID(playableID, true));
						if (match) { return (match as IPlayable); }
					}
				}
			}
			return null;
		}
		
		/**
		 * Adds a single IPlayable to the children array (duplicates are rejected) and
		 * syncs up the group and child play-states based on various conditions.
		 * 
		 * <p>If both the group and the item being added are STOPPED, the item is simply
		 * added to the children list.</p>
		 * 
		 * <p>If both items are PAUSED or PLAYING (including PLAYING_DELAY for children), 
		 * the child is actively added to the group during play and will be monitored for 
		 * completion along with others.</p>
		 * 
		 * <p>In other cases where the child's state mismatches the group's state, there
		 * are several behaviors available. Normally if the second parameter <code>adoptChildState</code>
		 * is left false, the child's mismatched state will be updated to match the group's
		 * state. This can result in it being stopped, paused, or started/resumed and monitored 
		 * for completion along with other children. Passing true for <code>adoptChildState</code> 
		 * results in updating the group's state to match the child's. This option could be used, for 
		 * example, if you wanted to build a group of already-playing items without disrupting their
		 * play cycle with a start() call to the group.</p>
		 * 
		 * @param item				Any instance that implements IPlayable and uses PlayState constants.
		 * @param adoptChildState	Makes this group change its play-state to match the state of the new child.
		 * @return Success.
		 */
		public function addChild(item:IPlayable, adoptChildState:Boolean=false): Boolean {
			if (_children[ item ])
				return false;
			if (item.state!=_state) { // Resolve an obvious mismatched play state...
									  // Normally states are both STOPPED, so the following ugliness is rarely used.
				var primary:IPlayable = (adoptChildState ? item : this);
				var primaryPlaying:Boolean = (primary.state==PlayStates.PLAYING || primary.state==PlayStates.PLAYING_DELAY);
				var secondary:IPlayable = (adoptChildState ? this : item);
				var secondaryPlaying:Boolean = (secondary.state==PlayStates.PLAYING || secondary.state==PlayStates.PLAYING_DELAY);
				if ( !(primaryPlaying && secondaryPlaying) ) // Less obvious, but treat PLAYING_DELAY & PLAYING as "playing."
				{
					switch (primary.state) {
						case PlayStates.STOPPED:
							secondary.stop();
							break;
						case PlayStates.PAUSED: // This case works either way. Both START & PAUSE events will result.
							if (secondary.state==PlayStates.STOPPED)
								secondary.start();
							secondary.pause();
							break;
						case PlayStates.PLAYING: 
						case PlayStates.PLAYING_DELAY:
							if (secondary.state==PlayStates.PAUSED)
								secondary.resume();
							else if (secondary.state==PlayStates.STOPPED) {
								if (adoptChildState) {
									_state = PlayStates.PLAYING; // Group adopts child playing state
									dispatchEvent(new GoEvent( GoEvent.START));
								} 
								else {
									secondary.start();
								}
							}
							break;
					}
				}
			}
			// Saved until after possible play-state changes. Now we can base listening on this group's state.
			_children[ item ] = false;
			if (_state!=PlayStates.STOPPED)
				listenTo(item);
			return true;
		}
		
		/**
		 * Removes a single IPlayable from the children array.
		 * 
		 * <p>Note that if play is in progress when a child is added it does not
		 * interrupt play and the child is monitored for completion along with
		 * others.</p>
		 * 
		 * @param item		Any instance that implements IPlayable and uses PlayState constants.
		 * @return Success.
		 */
		public function removeChild(item:IPlayable): Boolean {
			var v:* = _children[ item ];
			if (v===null)
				return false;
			if (v===true)
				unListenTo( item );
			delete _children[ item ];
			return true;
		}
		
		/**
		 * Test whether any child has a particular PlayState.
		 * 
		 * <pre>
		 * // Example: resume a paused group
		 * if ( myGroup.anyChildHasState(PlayStates.PlayStates.PAUSED) ) {
		 *     myGroup.resume();
		 * }
		 * </pre>
		 * @see org.goasap.PlayStates PlayStates
		 */
		public function anyChildHasState(state:String): Boolean {
			for (var item:Object in _children)
				if ((item as IPlayable).state==state)
					return true;
			return false;
		}
		
		// -== IPlayable implementation ==-
		
		/**
		 * Calls start on all children. 
		 * 
		 * <p>If the group is active when this method is called, a <code>stop</code> call 
		 * is automated which will result in a GoEVent.STOP event being dispatched.</p>
		 * 
		 * @return Returns true if any child in the group starts successfully.
		 */
		public function start() : Boolean {
			stop();
			var r:Boolean = false;
			for (var item:Object in _children) {
				listenTo(item as IPlayable); // first in case of immediate STOP/COMPLETE.
				var started:Boolean = (item as IPlayable).start();
				if (!started)
					unListenTo(item as IPlayable);
				r = (started || r);
			}
			if (!r) return false; // all starts failed
			_state = PlayStates.PLAYING;
			dispatchEvent(new GoEvent( GoEvent.START));
			_playRetainer[ this ] = 1; // Developers - Important! Look up _playRetainer.
			return true;
		}

		/**
		 * If the group is active, this method stops all child items and 
		 * dispatches a GoEvent.STOP event.
		 * 
		 * @return Returns true only if all children in the group stop successfully.
		 */
		public function stop() : Boolean {
			if (_state == PlayStates.STOPPED)
				return false;
			_state = PlayStates.STOPPED;
			_repeater.reset();
			delete _playRetainer[ this ]; // Developers - Important! Look up _playRetainer.
			if (_listeners==0) {
				dispatchEvent(new GoEvent( GoEvent.COMPLETE ));
				return true;
			}
			var r:Boolean = true;
			for (var item:Object in _children) {
				unListenTo(item as IPlayable);
				r = ((item as IPlayable).stop() && r);
			}
			dispatchEvent(new GoEvent( GoEvent.STOP ));
			return r;
		}
		
		/**
		 * Calls <code>pause</code> on all children.
		 * 
		 * @return  Returns true only if all playing children in the group paused successfully
		 * 			and at least one child was paused.
		 */
		public function pause() : Boolean {
			if (_state!= PlayStates.PLAYING)
				return false;
			var r:Boolean = true;
			var n:uint = 0;
			for (var item:Object in _children) {
				var success:Boolean = (item as IPlayable).pause();
				if (success) n++;
				r = (r && success);
			}
			if (n>0) {
				_state = PlayStates.PAUSED; // state should reflect that at least one item was paused,
								  // while return value may indicate that not all pause calls succeeded.
				dispatchEvent(new GoEvent( GoEvent.PAUSE ));
			}
			return (n>0 && r);
		}
		
		/**
		 * Calls <code>resume</code> on all children.
		 * 
		 * @return	Returns true only if all paused children in the group resumed successfully
		 * 			and at least one child was resumed.
		 */
		public function resume() : Boolean {
			if (_state!= PlayStates.PAUSED)
				return false;
			var r:Boolean = true;
			var n:uint = 0;
			for (var item:Object in _children) {
				var success:Boolean = (item as IPlayable).resume();
				if (success) n++;
				r = (r && success);
			}
			if (n>0) {
				_state = PlayStates.PLAYING; // state should reflect that at least one item was resumed,
								  // while return value may indicate that not all resume calls succeeded.
				dispatchEvent(new GoEvent( GoEvent.RESUME ));
			}
			return (n>0 && r);
		}
		
		/**
		 * Calls <code>skipTo</code> on all children.
		 * 
		 * @return	Returns true only if all children in the group skipTo the position successfully
		 * 			and at least one child was affected.
		 */
		public function skipTo(position : Number) : Boolean {
			var r:Boolean = true;
			var n:uint = 0;
			position = _repeater.skipTo(_repeater.cycles, position); // TODO: TEST
			for (var item:Object in _children) {
				r = ((item as IPlayable).skipTo(position) && r);
				listenTo(item as IPlayable);
				n++;
			}
			_state = (r ? PlayStates.PLAYING : PlayStates.STOPPED);
			return (n>0 && r);
		}
		
		// -== Protected Methods ==-
		
		/**
		 * @private
		 * Internal handler for item completion.
		 * @param event		GoEvent dispatched by child item. 
		 */
		protected function onItemEnd(event:GoEvent) : void {
			unListenTo(event.target as IPlayable);
			if (_listeners==0) {
				complete();
			}
		}
		
		/**
		 * @private
		 * Internal handler for group completion.
		 */
		protected function complete() : void {
			if (_repeater.next()) {
				dispatchEvent(new GoEvent( GoEvent.CYCLE ));
				for (var item:Object in _children) {
					var started:Boolean = (item as IPlayable).start();
					if (started)
						listenTo(item as IPlayable);
				}
			}
			else {
				stop();
			}
		}

		/**
		 * @private
		 * Internal. Listen for item completion, keeping tight track of listeners.
		 * @param item	Any instance that extends IPlayable (IPlayable itself should not be used directly).
		 */
		protected function listenTo(item:IPlayable) : void {
			if (_children[ item ] === false) {
				item.addEventListener(GoEvent.STOP, onItemEnd, false, 0, true);
				item.addEventListener(GoEvent.COMPLETE, onItemEnd, false, 0, true);
				_children[ item ] = true;
				_listeners++;
			}
		}
		
		/**
		 * @private
		 * Internal. Stop listening for item completion.
		 * @param item	Any instance that extends IPlayable (IPlayable itself should not be used directly).
		 * @return Number of completion listeners remaining.
		 */
		protected function unListenTo(item:IPlayable) : void {
			if (_children[ item ] === true) {
				item.removeEventListener(GoEvent.STOP, onItemEnd);
				item.removeEventListener(GoEvent.COMPLETE, onItemEnd);
				_children[ item ] = false;
				_listeners--;
			}
		}
	}
}
\ No newline at end of file



More information about the GoList mailing list