Debug Console for Flash and Unity – Part 1

One of the most interesting thing when you develop a game is to test each feature easily. It’s not possible for a developper to walk through a level to actualy check if cinematic trigger is working or to check if AI of a boss is normal.
A developper wants to activate easily the ‘godmod’ for example, that’s why it’s necessary to create a Console that will make the developper’s life better.

The Shell

The shell is the core of your console. The shell should allow developper to register commands, to execute a command, to get the syntax and to get the list of registered commands.

Dictionnaries are not the most optimized collection object but they are really interesting to use. I have different way to manipulate object when I use different language but the global idea don’t change.

For example in C# I’m a fond of using structure but it’s not existing in ActionScript and I prefere avoid to use internal class. That’s why I’ll use 2 disctionnary in AS3 and only one in C#.

Action Script 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package multiFreid.core
{
    import flash.utils.Dictionary;
    import multiFreid.error.ConsoleError;
    /**
     * The shell will centralize all command that developpers need to create for a project
     * @version 1.0
     * @author multifred
     */

    public class Shell
    {
        protected var commands:Dictionary;
        protected var manuals:Dictionary;
        protected static var instance:Shell;
       
        public function Shell()
        {
            commands = new Dictionary();
            manuals = new Dictionary();
            instance = this;
        }
       
        /**
         * Register a new command
         * @param   cmd - the command
         * @param   callback - the callback function
         * @param   manual - The details about the command
         * @throws  ConsoleError - Throws if the cmd is already set in the command list
         */

        static public function RegisterCmd(cmd:String, callback:Function, manual:String = "42"):void
        {
            if (instance.commands[cmd])
            {
                throw ConsoleError.DUPLICATE_CMD;
            }
            else
            {
                instance.commands[cmd] = callback;
                instance.manuals[cmd] = manual;
            }
        }
       
        /**
         * Execute the command
         * @param   cmd
         * @param   args
         */

        public function execute(cmd:String, args:Array):void
        {
            if (commands[cmd]!=null)
            {
                var f:Function = commands[cmd] as Function;
                f(args);
            }
            else
            {
                throw ConsoleError.UNKNOWN_CMD;
            }
        }
        /**
         * Get all commands
         * @return  the list of registered commands
         */

        public function getCommands():Array
        {
            var arr:Array = new Array();
            for ( var cmd:String in commands)
            {
                arr.push(cmd);
            }
            return arr;
        }
        /**
         * get the manual the specified command
         * @param   cmd the command
         * @return  the manual
         */

        public function getManual(cmd:String):String
        {
            if (manuals[cmd] != null)
            {
                return manuals[cmd];
            }
            else
            {
                throw ConsoleError.UNKNOWN_CMD;
            }
        }
       
    }

}

To register your commands the syntaxe will be simple :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
        protected function InitDefaultCmd():void
        {
            Shell.RegisterCmd(  "list",
                                cmdDisplayList,
                                "List all available commands");
                               
            Shell.RegisterCmd(  "cls",
                                cmdClear,
                                "clear the screen");
                               
            Shell.RegisterCmd(  "man",
                                cmdDisplaySyntaxe,
                                "man <cmd> display the manual of the specified command <cmd>");
                               
            Shell.RegisterCmd(  "exit",
                                cmdExit,
                                "Close the console without prompt");
        }

C#

In C# you cannot use a function in an argument as easily as it is in ActionScript. You’ll need to use delegate:

1
public delegate <Treturn> MyDelegate(<T0> args, ...);

Delegate is be a powerful tool, use it !

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
using UnityEngine;
using System.Collections;
using System;

namespace Multifred.System
{
    public class Shell {
        #region Delegate
        /// <summary>
        /// CMD callback - The Delegate.
        /// </summary>
        public delegate void CMDCallback(ArrayList args);
        #endregion
        #region init
        static protected Shell _instance;
        static public Shell instance
        {
            get{
                if(_instance==null)
                {
                    _instance = new Shell();
                }
                return _instance;
            }
        }
        protected struct Entry
        {
            public string cmd;
            public CMDCallback callback;
            public string manual;
        };
        protected Hashtable entries;
        /// <summary>
        /// Initializes a new instance of the <see cref="Shell"/> class.
        /// </summary>
        public Shell()
        {
            entries = new Hashtable();
        }
        #endregion
        #region entry commands
        static protected void RegisterCommand(Entry cmd)
        {
            instance.entries.Add(cmd.cmd, cmd);
        }
        static public void RegisterCommand(string cmd, CMDCallback callback)
        {
            RegisterCommand(cmd, callback, "No information");
        }
        /// <summary>
        /// Registers a new command.
        /// </summary>
        /// <param name='cmd'>
        /// Cmd - the command the developper wish to register
        /// </param>
        /// <param name='functionName'>
        /// Function name - the callback function
        /// </param>
        /// <param name='manual'>
        /// Manual.
        /// </param>
        /// <exception cref="">
        /// Throw an exception if the specified cmd is already registered
        /// </exception>
       
        static public void RegisterCommand(string cmd, CMDCallback callback, string manual)
        {
            Entry command;
            if(instance.entries.Contains(cmd))
            {
                throw new Exception("The command '"+cmd+"' already exist.");
            }
            command.cmd = cmd;
            command.callback = callback;
            command.manual = manual;
            RegisterCommand(command);
        }
        /// <summary>
        /// Executes the callback of the specified command with the specified args
        /// </summary>
        /// <param name='cmd'>
        /// cmd - the command the user used
        /// </param>
        /// <param name='args'>
        /// args - the arguments for the cmd
        /// </param>
        /// <returns>
        /// <c>true</c> if the cmd is found.
        /// </returns>
        public bool Execute(string cmd, ArrayList args)
        {
            if(entries.Contains(cmd))
            {
                Entry val= (Entry)entries[cmd];
                val.callback(args);
                return true;
            }
            return false;
        }
        #endregion
        #region default commands
        /// <summary>
        /// Do nothing.
        /// </summary>
        /// <param name='args'>
        /// Arguments - useless args
        /// </param>
        private void doNothing(ArrayList args)
        {
            return;
        }
        public string getManual(string cmd)
        {
            if(entries.Contains(cmd))  
            {
                Entry e = (Entry)(entries[cmd]);
                return e.manual;
            }
            return null;
        }
       
        public ArrayList getCommands()
        {
            ArrayList arr =new ArrayList();
            foreach(DictionaryEntry entry in entries)
            {
                arr.Add(entry.Key);
            }
            return arr;
        }
        #endregion
    }
}

And here how use it :

1
2
3
4
5
6
7
8
9
10
11
12
        Shell.RegisterCommand( 
            "man",
            new Shell.CMDCallback(callbackManual),
            "Display the manual of the command\n\tSyntax:\t\tman <cmd>");
        Shell.RegisterCommand(
            "cls",
            new Shell.CMDCallback(callbackCLS),
            "Clear the debug console's screen");
        Shell.RegisterCommand(
            "list",
            new Shell.CMDCallback(callbackList),
            "Display the list of all register function");

An interface now ?

Now that you have a working shell you will need to create a Command-Line Interface (CLI). The CLI is a display object so the way it work is completely different because of the framework you use to develop.
In the second part of this tuto I’ll create a CLI for Flash and for Unity and you will see big differences !

Cache some contents

Thanks for all your comments it’s been a while I haven’t post new article. I’ve been very busy but I will try to be more steady.

Today I come with a problem I often met : How to optimize my loadings ?

In various situation you need to load data and keep it available, for that I create an helpfull class, I call it ContentCache.

How does it work ?

I use the Dictionnary class to stock data, Dictionnaries are a very powerfull HashTable because you can use any object as key to get your value.
Thanks to that I can use the URL of the loaded object as key and be sure that I won’t load 2 times the same item !

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package  multiFreid.net
{
    import flash.display.Loader;
    import flash.display.LoaderInfo;
    import flash.events.Event;
    import flash.events.EventDispatcher;
    import flash.events.IOErrorEvent;
    import flash.net.URLRequest;
    import flash.utils.Dictionary;
    /**
     * ...
     * @author multifred
     */

    public final class ContentCache extends EventDispatcher
    {
        private var _cache:Dictionary;
        protected static var _totalLoaded:Number;
        public static function get totalLoaded():Number { return _totalLoaded; }
        protected static var _instance:ContentCache;
       
        public function ContentCache()
        {
            _totalLoaded = 0;
            _cache = new Dictionary();
        }
       
        public static function getInstance():ContentCache
        {
            if (!_instance)
                _instance = new ContentCache();
            return _instance;
        }
        /**
         * Load content if it is not in the cache
         * @param   path    Path of the content to load
         * @return  The loader
         */

        public function getContent(path:String):Loader
        {
            var urlRequest:URLRequest  = new URLRequest(path);
            if (_cache[urlRequest.url])
            {
                return _cache[urlRequest.url] as Loader;
            }
           
            var loader:Loader = new Loader();
           
            loader.contentLoaderInfo.addEventListener(Event.COMPLETE, completeHandler);
            loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, errorHandler);
            _cache[urlRequest.url] = loader;
            loader.load(urlRequest);
           
            return _cache[urlRequest.url] as Loader;
        }
        /**
         * COMPLETE event Handler, track the loaded Data
         * @param   evt
         */

        private function completeHandler(evt:Event):void
        {
            var li:LoaderInfo = (evt.currentTarget as LoaderInfo)
            li.removeEventListener(Event.COMPLETE, completeHandler);
            li.removeEventListener(IOErrorEvent.IO_ERROR, errorHandler);
           
            _totalLoaded += li.bytesTotal;
            trace(li.bytesTotal);
            dispatchEvent(new Event(Event.COMPLETE));
        }
       
        /**
         * IOError Handler : It will remove the loader from the cache
         * @param   evt
         */

        private function errorHandler(evt:IOErrorEvent):void
        {
            var li:LoaderInfo = (evt.currentTarget as LoaderInfo)
            li.removeEventListener(Event.COMPLETE, completeHandler);
            li.removeEventListener(IOErrorEvent.IO_ERROR, errorHandler);
           
            dispatchEvent(new IOErrorEvent(IOErrorEvent.IO_ERROR));
            _cache[(evt.currentTarget as Loader).loaderInfo.url] = null;
        }
       
    }

}

I keep using the Loader object to still have access to the ‘contentLoadeInfo’ property.
Actually, I wish to create a Class I could call CachedData were I’ll load as well URLLoader and Loader and then the getContent function shall return an Object.

If you have any suggesion I will be glad to think on it. You can contact me or follow me on twitter @multifreid

Les shader en flash

Pixel bender toolkit

Adobe permet aux développeurs avertis de créer des filtres personnalisées pour Photoshop et Flash. Sous Flash ces filtres sont utilisables en tant que Shader.

Le fichier généré par Pixel Bender est un fichier binaire et comme le langage utilisé pour générer ce code est du C++, celui-ci a l’énorme avantage d’être moins gourmand en ressource.

Exemple

Télécharger les fichiers de l’exemple

Pour ce tuto j’ai été très inspiré par le tutoriel de ActiveTuts, qui explique comment créer un effet de distorsion d’un écran CRT. Tutoriel que je vous invite à regarder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<languageVersion : 1.0;>

kernel CRTBuzz
<   namespace : "multifreid";
    vendor : "MultiFreid";
    version : 1;
>
{
    input image4 src;
    output pixel4 dst;
    //Parameter that will move the different channel
    parameter float2 deltaRed;
    parameter float2 deltaGreen;
    parameter float2 deltaBlue;

    void
    evaluatePixel()
    {
        pixel4 red, green, blue;

        red = sampleNearest(src,outCoord()+deltaRed);
        green = sampleNearest(src,outCoord()+deltaGreen);
        blue = sampleNearest(src,outCoord()+deltaBlue);

        dst = red;
        dst.g = green.g;
        dst.b = blue.b;
    }
}

La fonction evaluatePixel() est la fonction qui etre appelé pour produire le pixel final (dst) à partir de l’image source (src).

SampleNearest permet d’effectuer un échantillonnage du pixel de l’image aux coordonnées indiquées en paramètre. Ici on crée un échantillonnage pour chaque décalage passé en paramètre du shader.

Enfin on crée dst à partir de ces trois échantillonnages.

Faites vos tests puis exportez le filtre (File>Export filter for Flash Player…). Pixel Bender  crée alors un fichier .pbj qui est le code binaire de notre filtre.


Dans le code flash il nous faut crée un filtre que l’on pourra utilisé comme DropShadowFilter ou BlurFilter.

Pour cela je crée la classe RGBDistortFilter qui étend ShaderFilter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package  {
    import flash.display.Shader;
    import flash.filters.ShaderFilter;

    public class RGBDistortFilter extends ShaderFilter {
        public static var theShader:Shader;
        public function RGBDistortFilter(redX:Number=0, redY:Number=0, greenX:Number =0, greenY:Number =0, blueX:Number = 0, blueY:Number = 0) {
            // constructor code
            if(theShader)
            {

                theShader.data.deltaRed.value = [redX, redY];
                theShader.data.deltaGreen.value = [greenX, greenY];
                theShader.data.deltaBlue.value = [blueX, blueY];
                super(theShader);
                this.bottomExtension = this.leftExtension =this.rightExtension = this.topExtension = Math.ceil(Math.max(Math.abs(redX), Math.abs(redY),Math.abs(greenX),
                                                            Math.abs(greenY), Math.abs(blueX), Math.abs(blueY)));
            }
        }
    }
}

Enfin dans la classe Main, j’y fait appel comme suit :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package  {
    import flash.display.MovieClip;
    import flash.events.Event;
    import flash.net.URLLoader;
    import flash.net.URLLoaderDataFormat;
    import flash.net.URLRequest;
    import flash.display.Shader;
    import com.greensock.TweenLite;

    public class Main extends MovieClip{
        var urlLoader:URLLoader;
        var valFilter:Object;

        public function Main() {
            // constructor code
            urlLoader =new URLLoader();
            urlLoader.dataFormat = URLLoaderDataFormat.BINARY;
            urlLoader.addEventListener(Event.COMPLETE, onComplete);
            urlLoader.load(new URLRequest("RGBDistortFilter.pbj"));

             valFilter= new Object();

            valFilter.xr=0;
            valFilter.yr=0;

            valFilter.xg=0;
            valFilter.yg=0;

            valFilter.xb=0;
            valFilter.yb=0;
        }

        function onComplete(evt:Event=null)
        {
            if(evt)
            {
                RGBDistortFilter.theShader = new Shader();
                RGBDistortFilter.theShader.byteCode = evt.currentTarget.data;
            }
            var amplitudeMax:Number = 5;
            TweenLite.to(valFilter, .2, {
                yr: (Math.random()-.5)*amplitudeMax,
                yg: (Math.random()-.5)*amplitudeMax,
                yb: (Math.random()-.5)*amplitudeMax,
                onUpdate:updateFilter,
                onComplete:onComplete});
        }

        function updateFilter()
        {
            var filter: RGBDistortFilter = new RGBDistortFilter(valFilter.xr, valFilter.yr, valFilter.xg, valFilter.yg, valFilter.xb, valFilter.yb);
            titre.filters = [filter];
        }

    }
}

J’utilise la library de tween de greensock pour faire l’animation du filtre. mais il existe de nombreuses techniques pour ça (comme le bruit de Perlin par exemple).

Séparer le code des média

Quel intérêt?

Lors d’un projet on est rarement seul à travailler dessus. Et donc il peut arriver que le ou les graphistes aient à modifier les médias pendant que le ou les développeurs avancent sur le projet.

Il arrive aussi souvent d’avoir un projet multilingue. La séparation des média et des codes peut faciliter la tâche. Sur certain projet il suffira d’externaliser les textes dans un XML et ça ira ! Mais qu’en est-il des logos ? des FX qui possède du textes ? ou même l’intégration de texte dans le décors ? Très souvent un XML ne suffit pas pour toutes ces problématiques.

Il y a d’autres projets qui nécessite de changer de style à l’occasion de Noel, Halloween, l’été ou pour n’importe quelle raison ! Il faut pouvoir changer de média sans avoir à changer de code.

Une classe pour contrôler ses médias

Pour pouvoir gérer complètement et facilement la séparation du code et des médias j’ai créé la classe SharedLibrary.

Télécharger l’exemple

package  multifred.utils
{
	import flash.display.DisplayObject;
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.display.Loader;
	import flash.events.IOErrorEvent;
	import flash.events.ProgressEvent;
	import flash.net.URLRequest;
	import flash.display.LoaderInfo;
	import flash.system.ApplicationDomain;

	/**
	 * ...
	 * @author MultiFred
	 */
	public class SharedLibrary extends EventDispatcher
	{
		protected var _applicationDomain:ApplicationDomain;
		private var loader:Loader;
		/**
		 * Charge un swf et recupere son ApplicationDomain. Cette diffuse les evenement Event.COMPLETE, ProgressEvent.PROGRESS, IOErrorEvent.IO_ERROR et Event.INIT.
		 * @param	url 	URL de la bibliothèque (fichier swf)
		 */
		public function SharedLibrary(url:String)
		{
			_applicationDomain=new ApplicationDomain()
			loader = new Loader();
			loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onComplete);
			loader.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS, onProgress);
			loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, onError);
			loader.contentLoaderInfo.addEventListener(Event.INIT, onInit);
			loader.load(new URLRequest(url));
		}

		public function get applicationDomain():ApplicationDomain {
			return _applicationDomain;
		}

		public function get percent():Number {
			var val:Number
			try
			{
				val=loader.contentLoaderInfo.bytesLoaded / loader.contentLoaderInfo.bytesTotal;
			}
			catch (err:Error)
			{
				val=0
			}
			return val;
		}
		public function getClass(className:String):Class
		{
			return _applicationDomain.getDefinition(className) as Class;
		}

		public function createClip(className:String):DisplayObject
		{
			var TheClass:Class;
			var theClip:DisplayObject;

			TheClass = getClass(className);
			theClip = new TheClass();
			return theClip;
		}

		private function onComplete(evt:Event) {
			/*
			* Chargement des classes contenus dans le fichier swf chargé
			*/
			// récupération d'une instance d'ApplicationDomain
			// sur la propriété applicationDomain du contentLoaderInfo (e.target)
			_applicationDomain = LoaderInfo(evt.target).applicationDomain;
			dispatchEvent(new Event(Event.COMPLETE));
		}

		private function onProgress(evt:ProgressEvent) {
			dispatchEvent(new ProgressEvent(ProgressEvent.PROGRESS,false, false, evt.bytesLoaded, evt.bytesTotal));
		}

		private function onError(evt:IOErrorEvent) {
			dispatchEvent(new IOErrorEvent(IOErrorEvent.IO_ERROR, false, false, evt.text));
		}

		private function onInit(evt:Event) {
			dispatchEvent(new Event(Event.INIT));
		}

	}

}

Cette classe va charger le fichier .swf à l’URL spécifié et va renvoyer les évènements INIT et COMPLETE. Une fois ce fichier chargé, vous pouvez récupérer les clips qui ont été exportés.

Pour cela je crée le fichier media.fla qui contient une animation de chargement. Au moment où je crée le Movieclip de l’animation je coche la case « Export for ActionScript » et je nomme la class « Loader ». Une fois cela terminé je compile media.swf.

Je crée les fichiers main.fla et Main.as qui contiennent le code.

package  {
	import flash.display.MovieClip;
	import multifreid.utils.SharedLibrary;
	import flash.events.Event;

	public class Main extends MovieClip{
		var library:SharedLibrary;
		var clipLoader:MovieClip;

		public function Main() {
			// constructor code
			library = new SharedLibrary("media.swf");
			library.addEventListener(Event.COMPLETE, onComplete);
		}

		function onComplete(evt:Event)
		{
			clipLoader = library.createClip("Loader") as MovieClip;
			this.addChild(clipLoader);
			clipLoader.x = stage.stageWidth*.5;
			clipLoader.y = stage.stageHeight*.5;

		}

	}

}

Compilez main.swf et vous verrez votre animation se jouer.

Les Web Services en ActionScript 3.0

Très souvent lorsque l’on fait du jeu Facebook, on se retrouve à devoir gérer des communication client-serveur régulièrement. Au lieu d’écrire chaque envoie de requête à plusieurs reprises dans le code, j’ai créé une classe WebService qui me simplifie la tâche. Cette classe n’a pas pour but de réinventer la roue mais de faciliter l’intégration des web services.

Le but est de créer une requête HTML avec une syntaxe la plus simple possible :

webService.createRequest("url_relative_de_la_requete", {var0:val0 ,var1:val1, ...});

Une pile de requêtes?

Une autre problématique, que ma classe gère, est la gestion d’une pile de requêtes. Il m’est déjà arrivé d’avoir affaire à un serveur relativement lent et qui crash car il doit gérer trop de requête simultanément. Pour cette raison, j’ai fait en sorte que ma classe WebService gère une pile de requêtes. De plus, j’ai indiqué l’URL du serveur en variable de classe, car je considère qu’il n’y a qu’un seul serveur par projet (ce qui est vrai dans quasiment tous les cas). Mais dans le cas où ce n’est pas le cas, il suffit d’initialiser urlBase à une chaîne de caractère vide et de passer une url absolue en paramètre à createRequest.

package multiFreid.net
{
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import flash.net.URLRequestMethod;
	import flash.net.URLVariables;
	import flash.net.navigateToURL;

	/**
	 * @author MultiFreid
	 **/
	public class WebService
	{

		public static var urlBase:String;
		protected var pile:Array;
		protected var current:URLRequest;

		/**
		 * Constructor
		 * @param	plateformURL	the plateform url : "http://my.domain.net/"
		 */
		public function WebService(plateformURL:String=null)
		{
			if(plateformURL)
				urlBase = plateformURL;
			pile = new Array();
		}

Pour cela je crée une classe RequestStruct qui me permettra de gérer ma pile. Dans un autre langage de programmation j’aurai géré cela avec une structure de donnée (struct) mais l’AS3 ne gère pas cela. Cette classe n’est pas public et donc est rajouté à la fin de la classe WebService.

//...
}
import flash.net.URLLoader;
import flash.net.URLRequest;
class RequestStruct{

	private var _loader:URLLoader;
	private var _params:Object;
	private var _request:URLRequest;

	public function RequestStruct(urlLoader:URLLoader, r:URLRequest, params:Object=null) {
		_loader = urlLoader;
		_params = params?params:{};
		_request = r;
	}
	public function get request():URLRequest { return _request; }
	public function get loader():URLLoader { return _loader; }
	public function get callbackComplete():Function { return _params.onComplete as Function;}
	public function get callbackError():Function { return _params.onError as Function;
	//d'autres getters pour personnaliser vos paramètres
}

une fois que vous avez cette « structure » on va pouvoir gerer notre pile de requete.

Dans la classe WebService, on crée les getters stackSize et currentRequest qui renvoie respectivement la taille de la pile (int) et l’URLRequest en cours de traitement.

Puis on crée la fonction createRequest qui est la fonction qui va gérer la création des requête.

public function createRequest(relativeURL:String, param:Object=null){
	var loader:URLLoader = new URLLoader();
	var request:URLRequest;
	var url:String;
	url = urlBase?urlBase + relativeURL:relativeURL;
	request = new URLRequest(url);
	//...

Il va falloir maintenant ajouter les URLVariables à l’URLRequest pour cela on doit déterminer si les propriétés passées dans params sont à envoyer avec la requête ou bien s’il s’agit d’un mot clé ou non. Pour cela on crée la fonction isKeyWord (voir fonctions annexes) qui prend en paramètre le nom de la propriété (string) et renvoie un booléen.

	//...
	if (param){
		var variables:URLVariables = new URLVariables();
		for (var prop:String in param) {
			if (!isKeyWord(prop)){
				variables[prop] = param[prop];
			}
		}
		if (param.method && (param.method == URLRequestMethod.GET)){
			request.method = URLRequestMethod.GET;
		}
		else{
			request.method = URLRequestMethod.POST;
		}
		request.data = variables;
	}
	//...

Enfin on ajoute la requête à la pile et on crée les EventListeners (voir fonctions annexes).

	//...
	loader.addEventListener(Event.COMPLETE, onComplete);
	loader.addEventListener(IOErrorEvent.IO_ERROR, onFail);
	if(pile.push(new RequestStruct(loader, request, param))== 1){
		loader.load(request);
		current = request;
	}
}//end of createRequest

Utilisation de WebService

Un petit test avec notre cher ami google :

import multiFreid.net.WebService;
import flash.events.Event;
import flash.net.URLVariables;
import flash.net.URLRequestMethod;
import flash.net.navigateToURL;

WebService.urlBase="http://www.google.fr/";

var ws:WebService = new WebService();
ws.createRequest("search", {q:"test", onComplete:onComplete, method:URLRequestMethod.GET});
ws.createRequest("search", {q:"test1", onComplete:onComplete, method:URLRequestMethod.GET});
ws.createRequest("search", {q:"test2", onComplete:onComplete, method:URLRequestMethod.GET});
ws.createRequest("search", {q:"test3", onComplete:onComplete, method:URLRequestMethod.GET});
ws.createRequest("search", {q:"test4", onComplete:onComplete, method:URLRequestMethod.GET});
ws.createRequest("search", {q:"test5", onComplete:onComplete, method:URLRequestMethod.GET});
ws.createRequest("search", {q:"test6", onComplete:onComplete, method:URLRequestMethod.GET});

function onComplete(evt:Event):void
{
	trace("completed request :: ", ws.currentRequest.url, (ws.currentRequest.data as URLVariables).toString());
	navigateToURL(ws.currentRequest, "_blank")
}

vous pouvez admirer les fenêtres qui s’ouvre les unes à la suite !

N.B. : La fonction onComplete sera toujours exécutée avant le changement de requête.

Les fonctions annexes

/**
 * Test whether the string is a key word or not
 * @param	st	string to test
 * @return	true if 'st' is a keyword
 */
protected function isKeyWord(st:String):Boolean
{
	return st == "onComplete" || st == "onError" || st == "method"; //To complete when keyword are added
}
/**
 * the callback function executed when the current RequestStruct dispatch Event.COMPLETE
 * @param	evt		the Event.
 */
protected function onComplete(evt:Event) {
	var lo:RequestStruct = pile.shift() as RequestStruct;
	trace("Network ::", "REQUETES RESTANTES :", pile.length);
	if (lo.callbackComplete != null) {
		lo.callbackComplete(evt);
	}
	try
	{
		var next:RequestStruct = pile[0] as RequestStruct;
		current = next.request;
		next.loader.load(next.request);
	}
	catch (err:Error)
	{
		current=null;
	}
}
/**
 * the callback function executed when the current RequestStruct dispatch IOErrorEvent.IO_ERROR
 * @param	evt		the Event to dispatch
 */
protected function onFail(evt:IOErrorEvent) {
	var lo:RequestStruct = pile.shift() as RequestStruct;
	if (lo.callbackError != null) {
		lo.callbackError(evt);
	}
	try
	{
		var next:RequestStruct = pile[0] as RequestStruct;
		current = next.request;
		next.loader.load(next.request);
	}
	catch (err:Error)
	{
		current=null;
	}
}
/**
* Get the URL of the current RequestStruct
**/
public function get currentRequest():URLRequest{return current;}
/**
 * Get the size of the stack of request
**/
public function get stackSize():uint{return pile.length;}

Vous pouvez trouver le fichier .as complet et fonctionnel ici