views:

511

answers:

3

I'm creating a 2d flash game (coded in flex/actionscript 3) where assets are downloaded when they are needed. Currently I have it setup like this:

AssetLoader.as

package
{
    import flash.display.Loader;
    import flash.net.URLRequest;

    public class AssetLoader extends Loader
    {
     //set vars
     private var url:String = "http://test.com/client/assets/";

     public function AssetLoader(url:String)
     {
            Logger.log("AssetLoader request: " + this.url + url);
            var request:URLRequest = new URLRequest(this.url + url);
            this.load(request);
     }
    }
}

Then, where I want to load the asset I do the following:

var asset:AssetLoader = new AssetLoader("ships/" + graphicId + ".gif");
asset.contentLoaderInfo.addEventListener(Event.COMPLETE, onShipAssetComplete, false, 0, true);

private function onShipAssetComplete(event:Event):void
{
    var loader:Loader = Loader(event.target.loader);
        shipImage = Bitmap(loader.content);
        shipImage.smoothing = true;
        addChild(shipImage);
}

The thing is, that this method doesn't check for already downloaded assets, so it will redownload them the second time the same asset is being requested (I think).

So, what I need is an array where all downloaded assets are stored, and on request the name of this asset is checked for existance in the array. So if it has already been downloaded, that asset from memory must be returned rather than redownloaded.

I could make the assetloader a static class, but I have to wait for the event to fire when it's done downloading the image - so I can't simply let a static function return the corresponding image. Any idea how I should do this?

EDIT for an attempt after comments:

package
{
    import flash.display.Loader;
    import flash.events.Event;
    import flash.net.URLRequest;

    public final class AssetManager
    {
     private static var assets:Object = {};
     private static var preUrl:String = Settings.ASSETS_PRE_URL;

     public static function load(postUrl:String):*
     {
      if (assets[postUrl])
      { //when the asset already exists
       //continue
      }
      else
      { //the asset still has to be downloaded
       var request:URLRequest = new URLRequest(preUrl + postUrl);
       var loader:Loader = new Loader();
       loader.load(request);
       loader.contentLoaderInfo.addEventListener(Event.COMPLETE, 
             function(event:Event):void
             {
                 var loader:Loader = Loader(event.target.loader);
                 assets[postUrl] = loader.content;
             }, false, 0, true); 
      }
     }
    }
}

EDIT2: another attempt

package
{
    import flash.display.Loader;
    import flash.events.Event;
    import flash.events.EventDispatcher;
    import flash.net.URLRequest;

    public final class AssetManager
    {
     private static var assets:Object = {};
     private static var preUrl:String = Settings.ASSETS_PRE_URL;

     public static function load(postUrl:String):*
     {
      if (assets[postUrl])
      { //the asset already exists
       var dispatcher:EventDispatcher = new EventDispatcher();
       dispatcher.dispatchEvent(new CustomEvent(CustomEvent.LOAD_COMPLETE, assets[postUrl]));
      }
      else
      { //the asset still has to be downloaded
       var request:URLRequest = new URLRequest(preUrl + postUrl);
       var loader:Loader = new Loader();
       loader.load(request);
       loader.contentLoaderInfo.addEventListener(Event.COMPLETE, 
             function(event:Event):void
             {
                 var loader:Loader = Loader(event.target.loader);
                 assets[postUrl] = loader.content;
                 var dispatcher:EventDispatcher = new EventDispatcher();
                 dispatcher.dispatchEvent(new CustomEvent(CustomEvent.LOAD_COMPLETE, assets[postUrl]));
             }, false, 0, true); 
      }
     }
    }
}

Then, I try the following:

var asset:AssetManager = AssetManager.load("ships/" + graphicId + ".gif");
      asset.addEventListener(CustomEvent.LOAD_COMPLETE, onShipAssetComplete, false, 0, true);

But get an error, "undefined method addEventListener by a reference of the type static AssetManager" (roughly translated).

+1  A: 

You could add a static object (used as a dictionary with urls for assets as keys and the content for assets as values) in the AssetLoader class and in the same time keep using the class in the way you're using it right now.

private static var assets:Object = {};

The difference would be that your class would need to check against that static object if the URL for the content has already been requested previously. If it has, dispatch the complete event immediately. If it hasn't, follow the normal routine and don't forget to populate your static object with the newly loaded asset.



Update:

This is a quick example of what I meant. I haven't had time to test this, but it should work.

Note: You must invoke the loadAsset() method of the AssetLoader instances you create in order to actually load the asset. This is consistent with the way the Loader class we're extending works.

You should always add all event listeners BEFORE invoking the loadAsset() method. In your question you're calling the load() method from within the constructor and only afterwards add the event listener for Event.COMPLETE. This could produce strange results.

Here's the code:

package
{
  import flash.display.Loader;
  import flash.events.Event;
  import flash.net.URLRequest;


  public class AssetLoader extends Loader
  {
    private static const BASE_URL:String = 'http://test.com/client/assets/';

    public static var storedAssets:Object = {};

    private var assetURL:String;
    private var urlRequest:URLRequest;
    private var cached:Boolean = false;


    public function AssetLoader(url:String):void
    {
      trace('Loading: ' + url);
      assetURL = url;

      if (storedAssets[assetURL] != null)
      {
        cached = true;
        trace('Cached');
      }
      else
      {
        trace('Loading uncached asset');
        urlRequest = new URLRequest(BASE_URL + assetURL);
        contentLoaderInfo.addEventListener(Event.COMPLETE, OnAssetLoadComplete);
      }
    }

    public function loadAsset():void
    {
      if (cached)
        loadBytes(storedAssets[assetURL]);
      else
        load(urlRequest);
    }

    private function OnAssetLoadComplete(event:Event):void
    {
      storedAssets[assetURL] = contentLoaderInfo.bytes;
      trace('Loaded ' + contentLoaderInfo.bytesLoaded + ' bytes');
    }

  }

}


Update 2:

Here's how one would use the class above:

var assetLdr:AssetLoader = new AssetLoader("ships/" + graphicId + ".gif");
assetLdr.contentLoaderInfo.addEventListener(Event.COMPLETE, onShipAssetComplete);
assetLdr.loadAsset();

private function onShipAssetComplete(event:Event):void
{
    var shipImage:Bitmap = Bitmap(event.target.loader.content);
    // Do stuff with shipImage
}
Lior Cohen
Hey, I'm not quite sure what you want me to do. I can make a global library with the asset URLs and it's contents. When a asset is being requested, I think I can make it fire the complete event when the url is already found in the library. However.. how do I make this event then return the library content of this url? I don't quite understand.
Tom
I edited my original post with a first attempt. But I'm getting stuck as explained in the comments... maybe you or someone else have any suggestions?
Tom
Lior is probably suggesting you have a map that contains the resource url -> loaded resource. Something like `assets[url] = loadedContent`
James Fassett
Hi James. Yeah, I got that. I tried the same thing in my attempt (just that I use a 2d array rather than an associative array). However, when the download is complete I have no way to retrieve the downloaded asset's URL so I can not store it in the array. Any idea?
Tom
Ah -- I see. Use a closure when you do the load. I'll write an answer with an example.
James Fassett
I've updated the answer with some code.
Lior Cohen
Thanks Lior, but I'm getting two errors and don't know what to do with them. Unfortunately adobe thought it was a good thing to translate error messages to my own language (dutch), which makes absolutely no sense as programmers have no know english to program. This leaves me in the trouble of translating these rarely used dutch words into english, let me try: 1. at "this.contentLoaderInfo.dispatchEvent(Event.COMPLETE);"... implicit conversion of a string value in an unrelated type flash.events:Event. 2: incompatible overwrite (at public override function load():void)
Tom
I've modified the code to fix these two issues. Please use loadAsset() and not load() when loading assets using this class. See the code for more details.
Lior Cohen
Thanks.. well I can't stand asking but I've no idea where you want me to use the loadAsset function. I never used the load method with the Loader object before.
Tom
Actually, now I saw my old class, I see I did use the load method before. But I don't understand when to use that loadAsset function, as it simply runs the load method. While it should return the memory asset when it has already been downloaded.
Tom
Actually, it shouldn't return that.. as if it is not yet in memory it still has to be downloaded. I assume I need to register an event, but where and how? I don't see how this event is going to return the memory assets either...
Tom
Updated the code once again. Added several comments to clarify matters where possible and provided an example of how to use the class.
Lior Cohen
Thanks Lior, just one (big) problem: it keeps re-downloading. It is never entering the "if (storedAssets[assetURL] != null)" part, when requesting the same asset URL multiple times. I tested this. Isn't it because a new instance of the object is being made all the time, causing a new empty storedAssets array?
Tom
No time to test this thoroughly, but that shouldn't happen. Static variables belong in the context of the class they are defined in and not in instances. There might be a scoping issue, but this shouldn't be the case. I'll take a closer look into this tmw. 3AM, time to get some sleep :)
Lior Cohen
Actually, give it a shot without the "!= null" part. (storedAssets[assetURL]) instead.
Lior Cohen
Hi Lior. I tried that already, it's not working either.
Tom
When I do the following: storedAssets[assetURL] = assetBytes; trace(storedAssets[assetURL]); --> the trace returns null, even though we just stored assetBytes in it.. something is wrong with assetBytes?
Tom
Updated the code again. I've tested this one and it works for me as expected. There was a problem with the onAssetLoadComplete() method when converting the loaded bitmap into a ByteArray. Using BitmapData now and it works. Should have taken less time if I had the time to debug and play with this in the first place.
Lior Cohen
Hmm, but this will only work for Bitmaps? I need an universal asset class.
Tom
Update: even so, it's still not working. "var assetBytes:ByteArray = Bitmap(this.content).bitmapData as ByteArray; trace(assetBytes);" returns null.
Tom
Are the files you're loading loaded from the same domain you are loading your SWF from? And as for you needing a universal asset class - adjust the class to support assets other than bitmaps. You can do this by testing for the mime-type or extension of the assets you are loading.
Lior Cohen
Yes, same domain. Bitmap(this.content).bitmapData as ByteArray simply returns null. Any idea?
Tom
I have updated the code once again. I've made several modifications to allow assets to be of any type without using type specific code. I have tested this with SWF files and JPG/PNG files. This works as expected. This is my final update for this answer, unless you're looking for a freelancher, in which case, sorry, but I am too busy with my existing job :)
Lior Cohen
I'm sorry Lior Cohen, but your answers, although helpful, seemed to raise questions. It all works now though, thanks for your awesome assistance!
Tom
Most welcome, mate. Good luck with your project.
Lior Cohen
Thanks.. accepted your answer.
Tom
Well, almost afraid to say, but the cached version seems to cause a crash. When the caches version is being loaded, I get the following error in dutch: "TypeError: Error #1034: Afgedwongen typeomzetting is mislukt: kan flash.display::MovieClip@5c13421 niet omzetten in flash.display.Bitmap. at GameShip/onShipAssetComplete()" - means something like "type convertion has failed, can not convert flash.display::MovieClip@... to flash.display.Bitmap.
Tom
Hey tom. Did you get this one figured out or are you still stuck with it? I could take a closer look at the code again if you're still struggling with this one.
Lior Cohen
+1  A: 

Hey Tom,

Perhaps you should take a look at Bulk Loader. It does the kinds of things your looking to do. If you really want to use a custom solution, it would be a great point of reference, but why reinvent the wheel?

Tyler.

Tyler Egeto
Hmmm, it seems to have a bit too many features for what I want. It will probably cause extra resource usage and load.
Tom
For our game dev, we've built a non-static loader that pre-loads all the graphics per level as the level loads up. Because we know all the loading happens there, we know the assets will be available when we access them. We simply store all URL's in an array, then load them one at a time until all done, then dispatch a single complete event. We've also implemented asset "aging", so if a asset goes unused for X level loads we through it out to save memory resources.
Tyler Egeto
Unfortunately I need to load most assets on run-time, so this wouldn't fit for me. Also, the class you recommended seems to be made for bulk loading, that's not really what I want. Thanks though.
Tom
+1  A: 

Here is an alteration of your load command to capture the resourceId

public function load(postUrl:String):*
{
    var index:int;
    if ((index = assetExists(postUrl)) != -1)
    {
        dispatchEvent(new CustomEvent(CustomEvent.LOAD_COMPLETE, asset[postUrl]));
    }
    else
    { 
        //the asset still has to be downloaded
        var request:URLRequest = new URLRequest(preUrl + postUrl);
        var loader:Loader = new Loader();
        loader.load(request);
        loader.contentLoaderInfo.addEventListener(Event.COMPLETE, 
        function(event:Event)
        {
            // NOTE: not sure on scoping here ...
            // pretty sure you're this will be the Loader
            // if not just read it off the event like you were before
            assets[postUrl] = content;
            dispatchEvent(new CustomEvent(CustomEvent.LOAD_COMPLETE, asset[postUrl]));
        }, false, 0, true);
    }
}

/* In a new file */
public class CustomEvent extends Event
{
    public static const LOAD_COMPLETE:String = "CustomEvent_LoadComplete";

    // If you know the type you should use it (e.g. Sprite/DisplayObject)
    public var content:*;

    public function CustomEvent(type:String, _content:*)
    {
        content = _content;
        super(type);
    }
}

Note: when you write an Event descendant you should also override the toString and clone methods. I've also cheated on the constructor since you may want to pass through weakReferences and things like that.

James Fassett
Thanks, updated first post. Now I'm not sure how to continue. Think I need to fire an event that the asset is obtainable (on both situations). But how to do this with a static public class?
Tom
You are inheriting from Loader. The load method doesn't need to be static. Loader inherits from EventDispatcher so it can dispatch events. I suggest creating a custom event with a property for the loaded content and dispatching that. `dispatchEvent(new CustomLoadEvent(Event.COMPLETE, content))`
James Fassett
Sorry, but what's a CustomLoadEvent? I don't know about that object. I know the Event object but how to pass content to it? Could you maybe give an example if that's not too much asked? Thanks.
Tom
updated my response to show how to write a CustomEvent. You can make your own events just by sub-classing Event.
James Fassett
Thanks, getting "undefined method dispatchEvent" however, even with "import flash.events.*;". Any idea?
Tom
Update: updated my original post with second attempt. More is explained there.
Tom