MTaur

MTaur
MTaur

Wednesday, November 25, 2015

Sharpening the axe - MTaur's asset-loading class adventure

So it's been a while since I've written anything.  In addition to having a job that has nothing to do with gaming, I've gone back to learning programming instead of writing.  Between that and general procrastination, this blog was dead for a while.  If you don't want to read about programming in detail from a intermediate/beginner perspective, you can stop now.

Alright.  Who's still here?  There's going to be code for you to scrutinize, discuss, or even sample at the end, but there may be better ways to achieve the same thing.

Sharpening the axe - managing assets with Loader

It's often said that if Abraham Lincoln had a month to program a cherry tree chopping simulator, he'd spend three weeks setting up his asset loader classes.  Or something like that.


 





For the most part, I was a bit tired to working with Flash Professional's library feature, and I wanted to be able to drag-and-drop everything in Windows directly.  The price for that is that you have to figure out how the Loader class works.  Additionally, the Loader class creates instances, where the upshot of a library is that you get symbols to work with.

My goal for the Asset class was to store individual bitmaps as a Bitmap symbol and a MovieClip symbol.  I wanted to do this at most once for each image file (oddly enough, Actionscript can't load external bitmaps, but after you load a png, it stores it as a bitmap).

Along the way, I learned a bit about static properties and inheritance.  At first I wanted to make the image a static property of the Asset class, but that was no good.  It turns out that you can't just create a subclass Fireball that contains a fireball image and a separate Lightning subclass that contains an image of a lightning bolt - any static property of Asset is shared not just by all instances Asset, but by all instances of all subclasses.  What you would get is not a subclass which has its own personal static property, but a class which has to share its static property with all other Asset subclasses.

So the class Asset shouldn't have a property which stores one image.  It should be a static Vector which holds one of each asset when it is loaded.  The images should then indexed by their file path, that is, the string used in newURLRequest(filepath:String).  So it's like this:

var fireball: new Asset("fireball.png");

That would make one fireball.  If you want to spam a hundred of these, you should still only load the png file once and put it on file.  Behind the scenes, Asset stores the image in the static array imageSet, which is indexed by file path and name.  I want the Asset class to keep track of whether the png is already loaded or not, and then it can make copies.  It would work like this:

var fireballs: Array = [];

for (var i:int=0; 99; i++)

{
fireballs[i] = new Asset("fireball.png");
// fireballs[i] = new Asset("fireball.png").mc  to store only the movie clip in the array
// fireballs[i] = new Asset("fireball.png").img to store only the bitmap

// Could add them to stage, assign velocities, sizes, whatever if desired
}

var thunderBolts: Array = [];

for (var i:int = 0; 11; i++)
{
thunderBolts[i] = new Asset("lightning.png");
// Same idea, different data
}

Each instance of Asset knows which file it loaded, but also keeps track of what files are already "on the books" (have been requested) and which are fully loaded.  The Asset class generates an empty movie clip right away, but the magic of pointers allows you to slip in the loaded image in the couple of milliseconds it took to load once the loading is done.  This way, fireballs[7] doesn't throw a null object reference error.  Instead you get a pointer to imageSet["fireball.png"], which will be populated with a fireball faster than you can blink.  Each of the fireballs can be deleted and garbage collected, but the next time I throw 100 fireballs, the prototype fireball is already stored in memory from earlier.

The duty of making copies of the static elements of imageSet is handled in the Asset class. If loader is a Loader instance, then if loader has loaded fireball.png,

imageSet["fireball.png"] = Bitmap(loader.content); // ******, see * below

stores the image as a bitmap and files it away in the array.  But we want a copy for the particular instance, the non-static property img of Asset:

img = new Bitmap((imageSet["fireball.png"]).bitmapData);

That one took me a while - duplicating bitmaps dynamically can be done by turning bitmap into bitmapdata, and then casting the data as a new Bitmap.  At least I that's the cleanest duplication process I could find by Googling.

I think I prefer the MovieClip property mc, though, over the bitmap.  I might throw in some extra code to deal with registration points and so on, so that my coordinate systems will be nice and simple and everyone's feet will be centered at (0,0) locally.  This seems like a good place to put that code rather than elsewhere.

Anyway, here's the full example.  I plan to build more on this for my own project unless I get feedback suggesting that there are serious flaws to the approach:

* - one last technicality before the code - There is also a static vector of Loader objects, in correspondence with the unique URLRequests.  But I wanted to simplify the example slightly temporarily.  The actual code is below.





package 
{
    import flash.display.*;
    import flash.net.URLRequest;
    import flash.events.*;
    import flash.net.URLLoader;   
    import flash.text.*;
   
    public class Asset
        {

//            protected static var isLoaded: Boolean = false;
//            protected static var isLoading: Boolean = false;
//        Need a static vector of loaded images and URLs instead, with
//        protected boolean functions.
           
            private static var booked: Vector.<String> = new <String>[];
            private static var loaded: Vector.<String> = new <String>[];
            private static var imageSet: Array = [];
            private static var myLoaders: Array = [];
                // Associative array of loaders - one static loader
                // instance for each file.
            public var img: Bitmap = new Bitmap();
            public var mc: MovieClip = new MovieClip();
            private var file: String = "";

           
           
        public function Asset(filepath:String)
            {
                file = filepath;
               
                if(isLoading())
                {
                    trace(file + " is already loading!");
                }
               
                if( !(isLoaded() || isLoading() ) )
                    // Start loading when first called
                {
                    trace("Attempting to load " + file);
                    booked.push(file);               
                    myLoaders[file] = new Loader();
                    myLoaders[file].load(new URLRequest(file));
                }
               
                if(isLoading() )
                    // Prepare to store image when loaded
                {
                    myLoaders[file].contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, onError);
                    myLoaders[file].contentLoaderInfo.addEventListener(Event.COMPLETE, onComplete);
                }
               
                if(isLoaded() )
                    // Store image if loaded
                {
                    storeCopy();
                }
            }
           
        private function storeCopy()
            {
                    // img = new copy of static image
                img = new Bitmap((imageSet[file]).bitmapData);
                mc.addChild(img);
                    trace("Copy of " + file + " stored!");
            }
   
           
        private function onComplete(e:Event)
            {
                if (!isLoaded() )   
                    {
                        loaded.push(file);
                        trace("Loading of " + file + " finished!");
                            // stores loader content as bitmap
                        imageSet[file] = Bitmap(myLoaders[file].content);
                    }
                   

                storeCopy();
            }
       
        private function onError(e:ErrorEvent)
            {
                trace("There is no Winotaur!");
                trace(e.text);
                myLoaders[file].contentLoaderInfo.removeEventListener(Event.COMPLETE, onComplete);

            }
           
        private function isLoaded():Boolean
            {
                return (loaded.indexOf(file) > -1 );
            }
           
        private function isLoading():Boolean
            {
                return ((booked.indexOf(file) > -1) && !(loaded.indexOf(file) > -1) );
            }
           
        }
}



/* Main is a bit needlessly complicated.  It uses Timer to load things at different times, and it also gets an error on purpose. */


package  {
   
    import flash.display.*;
    import flash.display.Loader;
    import flash.net.URLRequest;
    import flash.events.*;
    import flash.net.URLLoader;
    import flash.utils.Timer;
    import Asset;
   
    public class Main extends MovieClip
        {

            var minotaur:MovieClip = new MovieClip();
            var asset = new Asset("minotaurFront.png");
            var lateMinotaur:MovieClip = new MovieClip();

            // Asset for lateMinotaur will try to load at a later time.
           
            var winotaur:MovieClip = new MovieClip();
           
            // There is no Winotaur!
            var asset3 = new Asset("winotaur.png");
           
        public function Main()
        {
            minotaur.scaleX = .3;
            minotaur.scaleY = .4;
            lateMinotaur.scaleX = .4;
            lateMinotaur.scaleY = .3;
            stage.addChild(minotaur);
            stage.addChild(lateMinotaur);
            stage.addChild(winotaur);
            minotaur.addChild(asset.mc);

            // Simulates load time for slow assets, 3 seconds
            var timer = new Timer(3000,1);
            timer.addEventListener(TimerEvent.TIMER_COMPLETE, commence);
            timer.start();           
           
        }
       

        public function commence(e:TimerEvent):void
        {
            trace("Ding!");
            var asset2 = new Asset("minotaurFront.png");
            lateMinotaur.addChild(asset2.mc);

        }

    }
   
}

/*  Attempting to load minotaurfront.png
Attempting to load winotaur.png
There is no Winotaur!
Error #2035: URL Not Found. URL: file:///C|/Sanguine%5FKingdoms/Loader%5FTest4/winotaur.png
Loading of minotaurfront.png finished!
Copy of minotaurfront.png stored!
Ding!
Copy of minotaurfront.png stored! */

3 comments:

  1. Load once, store twice. Kind of like what carpenters say you should do. I think.

    ReplyDelete
  2. Unhelpful tip: Code needs more comments! (Or maybe that is just me, I tend to go crazy with comments)

    ReplyDelete
    Replies
    1. If I had to work with other people or if I were actually good enough to consider writing a real tutorial/book, I'd think about it. For now, it works and I'm ok with it.

      Some books say that your function names should be so awesome and suggestive that you hardly even need comments, but that's a tall order. It's probably best to try to be that self-explanatory but then add comments anyway. Maybe?

      Delete