PageRenderTime 8ms CodeModel.GetById 20ms RepoModel.GetById 2ms app.codeStats 0ms

ActionScript | 1 lines | 1 code | 0 blank | 0 comment | 0 complexity | 85f9e427718043d6da29421bfc8270e7 MD5 | raw file
Possible License(s): LGPL-3.0
  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; import; 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="" target="_top"></a>. Please visit the * <a href="" 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 ? ( 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; } } }