I’ve finally finished (well… 95%) with my AS3 Starling Game Engine Demo so I can finally start writing tutorials on how you can create a Tower Defense (TD) style game using Starling. Since I have so much info planned, and there’s so much code, and I’m a developer, and I think there’s a vast lack of intermediate/advanced AS3 tutorials on the web, this will probably not be the best tutorial series if you are a beginning coder. None of this stuff is incredibly complex or “expert” or anything, but I’ll probably skip over a lot of the very basics with AS3 in general and Starling in specific.

[toc]

Before you start, feel free to check out the finished product of my AS3 Starling TD Demo.
You can also find all of the code used in srcview.
Or you can download the whole project zipped up.
Or check out the repo on Bitbucket

**Updated 8/9/13 added the ZFLoader.as class and commentary since I removed the old posts from my old engine series.

If you’re a beginner, I would encourage you to use the resources I myself used to learn both AS3 (over the last 6ish years) and Starling (over the past 2 months):

Goals

I really want this stressed heavily up front. I am not a designer. My photoshop skills extend about as far as T-Rex arms. I am a developer that loves to dabble in Photoshop. This does not make my dabbles “good.” My lady is the artsy designer type and has not had anything to do with this tutorial series or game demo except to mock my dropshadow/glow SKILLZ! The point of this demo is to demonstrate the following codemonkey goals. This demo is not really meant to be something fun to play yet. Minimal balancing has been done. This is simply getting the code chops together and tutorials done so that you can learn a few things and take this knowledge (and even code) and build something that IS fun and is an actual enjoyable game experience. I will be taking this very code and using it to start a TD game myself (with the lady actually making things pretty and stylish).

Please be kind regarding the tragic state of my UI. It’s functional, damnit! 😀

In creating this tutorial, I had a few goals that I wanted to achieve from this game demo tutorial. And I have accomplished nearly all of them and Will accomplish these by the last tutorial.

Topics Covered

In this post I’ll cover:

Game Project Structure

Every project has its own structure. I haven’t come across any real “Best Practices” when it comes to structuring a game project in AS3. A lot of tutorials show snippets of game projects, or small classes but in a very loose (if not non-existent) OOP structure.

Here’s the general structure of my project inside the src/ folder and some general notes on why:

There are many other folders inside the src/ folder but those are all 3rd party libs that I’ll point out eventually.

Notes Before We Begin

I’ve tried to copy the code in the examples straight from the code you can find where it is hosted at BitBucket – ZFStarlingTutorial. For formatting purposes I have shortened tabs to 3 spaces and there’s a good chance that I may post functions and code out of the order in which they actually appear in the file to better group together functions that share the topic I’m discussing. Feel free to check out that whole repo to see the code in its (sometimes-changing) entirety. Also, some code shown may have lines missing from the core codebase found at the repo because what I’m discussing in the tutorial may have nothing to do with those lines of code, so I may remove them to try to keep this massive beast as fairly concise as possible.

Let’s Do This!

One of the things that tripped me up the hardest when making the transition to Starling from regular AS3 was External Loading. I got a trial subscription to Lynda to check out Lee Brimlow’s Starling game tutorial. That tutorial had everything being [Embed] tag loaded. There was no way to handle progress or really manage what you’re loading at that point. It was great for the tutorial, and that single series really jumpstarted my Starling skills and got me rolling along. But hey, we’re fancy developers wanting to make hardcore games right? Right?! So we need a way to load assets, to get the progress of those loading assets, and other fancy things like that.

I’ve been a huge fan of GreenSock’s TweenMax/Lite library since way back, and any chance I get to include some of his libs into my code, I go for it. Why reinvent the wheel right? LoaderMax is his loading lib that he’s created, and it’s a huge time-saver. I wrote a wrapper called ZFLoader that basically is like a loader manager sort of class. I will explain the ZFLoader below.

Game.as

com/zf/core/Game.as is the core class that begins the Game. It holds the game-wide ZFLoader instance, SharedObjectManager instance, SoundManager instance, and handles State for the whole game demo.

public function Game() {
   super();
   soundMgr = new SoundManager();		
   addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
}

private function onAddedToStage(evt:Event):void {
   removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);

   so.init();

   // set the initial state
   changeState(GAME_LOAD_STATE);
			
   _addUpdateListener();
}

public function changeState(state:int):void {
   var removed:Boolean = false;

   if(currentState != null) {
      _removeUpdateListener();
      currentState.destroy();
      removeChild(Sprite(currentState));
      currentState = null;
      removed = true;	
   }

   switch(state) {
      case GAME_LOAD_STATE:
         currentState = new GameLoad(this);
         break;

      case MENU_STATE:
         currentState = new Menu(this);
         break;

      case MAP_SELECT_STATE:
         currentState = new MapSelect(this);
         break;

      case GAME_LOAD_STATE:
         currentState = new GameLoad(this);
         break;

      case MAP_LOAD_STATE:
         currentState = new MapLoad(this);
         break;

      case PLAY_STATE:
         currentState = new Play(this);
         break;

      case GAME_OVER_STATE:
         currentState = new GameOver(this);
         break;
   }

   addChild(Sprite(currentState));
			
   if(removed) {
      // Add update listeners back
      _addUpdateListener();
      removed = false;
   }
}

GameLoad.as

com/zf/states/GameLoad.as is implements IZFState, which is just a simple implementation I created to specify that classes implementing IZFState will all have update() and destroy() functions. The GameLoad State handles loading my initial game assets, sound assets, etc. Anything that I need to have from Frame 1 until the user closes their browser gets handled here. This includes non-map-specific sounds, button textures, fonts, backgrounds, and further JSON data files.

public function GameLoad(game:Game) {
   _game = game;
   addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
}

private function onAddedToStage(evt:Event):void {
   removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);

   _progBar = new ProgressBar(500, 35);
   _progBar.x = 150;
   _progBar.y = 400;
   addChild(_progBar);

   Game.zfLoader.onProgress.add(onProgress);
   Assets.onInitialLoadComplete.add(onLoadComplete);

   Assets.loadInitialAssets();
}

public function onProgress(ratio:Number):void {
   _progBar.ratio = ratio;
}

public function onLoadComplete():void {
   Game.zfLoader.onProgress.remove(onProgress);
   Assets.onInitialLoadComplete.remove(onLoadComplete);

   Assets.init();
   _progBar.removeFromParent(true);
   _progBar = null;

   _game.changeState(Game.MENU_STATE);
}

I really wish I had saved or bookmarked wherever I got the ProgressBar code from. I know I copy/pasted it from some example somewhere, but I don’t remember now where it was from. Apologies for not giving credit where it’s due.

A quick note about the changeState function, were this going to be a more robust engine (as you might build on top of this code), you may or may not want to add valid States that a State may or may not change to. For example, from this GameLoad state, I would not want to allow myself to ever try to _game.changeState(Game.GAME_OVER_STATE); I shouldn’t be able to switch to a GameOver State, I haven’t even played the game yet nor even seen a title menu! So in my State classes I might have an allowableStates array or something similar just to help ensure I don’t accidentally try to jump to a state I shouldn’t be able to get to.

For more on State Machines and AS3:

ZFLoader.as

package com.zf.loaders
{
   import com.greensock.events.LoaderEvent;
   import com.greensock.loading.DataLoader;
   import com.greensock.loading.ImageLoader;
   import com.greensock.loading.LoaderMax;
   import com.greensock.loading.MP3Loader;
   import com.greensock.loading.XMLLoader;
   import com.greensock.loading.display.ContentDisplay;
   import com.zf.core.Config;
   import com.zf.utils.FileUtils;

   import org.osflash.signals.Signal;

public class ZFLoader
{
   private var className:String = 'ZFLoader';

   private static var instance	 : ZFLoader;
   private static var allowInstantiation	: Boolean;

   public var queue:LoaderMax;
   public var onProgress:Signal;
   public var onComplete:Signal;

   private var callBackArray:Array;

   public static function getInstance():ZFLoader {
      if (instance == null) {
         allowInstantiation = true;
         instance = new ZFLoader();
         allowInstantiation = false;
      }
      return instance;
   }

   public function ZFLoader() {
      if (!allowInstantiation) {
         throw new Error("Error: Instantiation failed: Use ZFLoader.getInstance() instead of new.");
      } else {
         initQueue();
      }
   }

   private function initQueue():void  {
      onProgress = new Signal();
      onComplete = new Signal();
      queue =  new LoaderMax({
         name:"mainQueue",
         onProgress:onProgressHandler,
         onComplete:onCompleteHandler,
         onError:onErrorHandler,
         autoLoad:true
      });
      callBackArray = [];
   }

   public function addToLoad(path:String, cb:Function, id:String = '', includeAssetPath:Boolean = true, startQueueLoad:Boolean = true, opts:Object = null):void  {
      var fileName:String = FileUtils.getFilenameFromPath(path),
          ext:String = FileUtils.getExtFromFilename(path);
      // if id was not supplied, use the filename minus extension
      if(id == '') {
         id = fileName;
      }
      // Set up the fullPath for the asset
      var fullPath:String = FileUtils.getFullPath(path, includeAssetPath, ext);

      var useRawContent:Boolean = false;
      if(ext == Config.IMG_JPG
         || ext == Config.IMG_GIF
         || ext == Config.IMG_PNG) 
      {
         useRawContent=true;
      }

      // handle callback queue
      callBackArray.push({
         'fileName': fileName,
         'cb': cb,
         'id': id,
         'useRawContent': useRawContent,
         'options': opts
      });

      Config.log(className, 'addToLoad', "Adding callback function for " + path + " to callBackArray index: " + (callBackArray.length - 1 ).toString());
      Config.log(className, 'addToLoad', "FileName: " + fileName + " || FullPath: " + fullPath + " || ext: " + ext);

      switch(ext) {
         // EXTENSIONS
         case Config.DATA_XML:
         case Config.DATA_PEX:
            queue.append(new XMLLoader(fullPath, {'name':id}));
            break;

         case Config.IMG_JPG:
         case Config.IMG_PNG:
         case Config.IMG_GIF:
            queue.append(new ImageLoader(fullPath, {'name':id}));
            break;

         case Config.DATA_JSON:
         case Config.DATA_FNT:
            queue.append(new DataLoader(fullPath, {'name':id}));
            break;

         case Config.DATA_MP3:
            queue.append(new MP3Loader(fullPath, {'name':id, 'autoPlay': false}));
            break;
      }

      if(startQueueLoad) {
         queue.load();
      }
   }

   public function addCallbackToQueueEvent(eventName:String, callback:Function):void {
      queue.addEventListener(eventName, callback);
   }

   public function removeCallbackToQueueEvent(eventName:String, callback:Function):void {
      queue.removeEventListener(eventName, callback);
   }

   public function onCompleteHandler(event:LoaderEvent):void {
      Config.log(className, 'onCompleteHandler', "Beginning to process " + event.target);

      // Array of Objects used to temporarily store things that have not fully been loaded yet
      // and are not ready for callback
      var nextPassArray:Array = [];
      // item will hold the data from callBackArray that we're processing
      var item:Object;

      while(callBackArray.length > 0)  {
         // remove the item from the array so we dont try to process it again
         // save it in item so we can process it there
         item = callBackArray.shift();

         Config.log(className, 'onCompleteHandler', "Processing fileName: " + item.id);

         // holds the content we've just loaded. may be an image (ContentDisplay) or xml or other data type
         var contentData:* = queue.getContent(item.id);

         // if contentData has not loaded yet, save the item and continue
         if((contentData is ContentDisplay && contentData.rawContent == undefined) || contentData == undefined)
         {
            Config.log(className, 'onCompleteHandler', "Moving " + item.id + " to the Next Pass");
            nextPassArray.push(item);
         }
         else
         {
            var data:*;
            if(item.useRawContent) {
               data = contentData.rawContent;
            } else {
               data = contentData;
            }
            item.cb(new CallBackObject(item.id, data, item.options));
         }
      }
      callBackArray = nextPassArray;

      if(callBackArray.length == 0) {
         onComplete.dispatch();
         Config.log(className, 'onCompleteHandler', event.target + " is complete!");
      }
   }

   public function onProgressHandler(event:LoaderEvent):void {
      Config.log(className, 'onProgressHandler', "progress: " + event.target.progress);
      onProgress.dispatch(event.target.progress);
   }

   public function onErrorHandler(event:LoaderEvent):void {
      Config.logError(className, 'onErrorHandler', "error occured with " + event.target + ": " + event.text);
   }
}
}

That’s the loading process. Now let’s go look at Assets and see what’s happening there.

Assets.as

com/zf/core/Assets.as is the core class that handles my demo’s art, sound, font, and JSON data assets. In mobile dev, this might be your R (Resources) class. Right off the bat, you’ll notice it’s filled with loads of public static var‘s. Yup. Before I cleaned this file up, I actually had double the number of variables because I was loading in the jpg/png assets into a Bitmap variable, then later in init() I was converting them to a Texture class. Thankfully I realized that I could skip the duplicate memory overhead and we’ll see in a bit how I saved some RAM.

If you scroll up just a bit, you’ll remember that from GameLoad.as, when that State gets added to stage, it calls Assets.loadInitialAssest(). It’s a very short function:

public static function loadInitialAssets():void {
   Game.zfLoader.addToLoad('initialAssets.json', onInitialAssetsLoadComplete);
   Game.zfLoader.addToLoad('sounds.json', onInitialSoundsLoadComplete);
}

I have two main initial files that act as a sort of “manifest” of assets to load. I’m experimenting with TreeFortress’ SoundAS sound manager and it has its own way to load sounds into its internal variables. SoundAS has a load() function and it will load your sound assets for you! However, at this time, it only has an onSoundLoadComplete and an onSoundLoadError Signal that it dispatches. They have not added any onSoundProgress type Signal, so I had no easy way to tie in my progress bar with SoundAS loading my assets. So, I’ll simply load them myself here then stuff them into SoundAS.

initialAssets.json and sounds.json (found at src/assets/initialAssets.json & src/assets/sounds.json) are simple JSON files that look like this:

initialAssets.json

{
   "files": [
      {
         "id": "logo",
         "convertToTexture": true,
         "file": "assets/images/logo.png"
      },
	  {
         "id": "atlas",
         "file": "assets/images/atlas.png"
      },
      {
         "id": "atlasXML",
         "file": "assets/images/atlas.xml"
      }
}

sounds.json

{
   "files": [
      {
         "id": "ding1",
         "file": "assets/sounds/ding1.mp3"
      },
      {
         "id": "escape1",
         "file": "assets/sounds/escape1.mp3"
      }
}

These are simply files that consist more or less of an array of “id” and “file” metadata. In initialAssets you’ll also see the extra “convertToTexture” key on several of the items, but not all. This is part of my optimization to not have to keep Bitmap versions of the files.

When initialAssets.json is done loading, it calls onInitialAssetsLoadComplete(). Likewise, when sounds.json finishes, it calls onInitialSoundsLoadComplete():

From Assets.as

public static function onInitialAssetsLoadComplete(cbo:CallBackObject):void {
   var initAssets:Object = Utils.JSONDecode(cbo.data);
   _itemsToLoad = initAssets.files.length;
   var obj:Object;
   for(var i:int = 0; i < _itemsToLoad; i++) {
      obj = initAssets.files[i];
      Game.zfLoader.addToLoad(obj.file, onAssetLoadComplete, obj.id, false, false, obj)
   }
   Game.zfLoader.queue.load();
}

public static function onInitialSoundsLoadComplete(cbo:CallBackObject):void {
   var sndAssets:Object = Utils.JSONDecode(cbo.data);
   _soundsToLoad = sndAssets.files.length;

   var obj:Object;
   for(var i:int = 0; i < _soundsToLoad; i++) {
      obj = sndAssets.files[i];
      Game.zfLoader.addToLoad(obj.file, onSoundAssetLoadComplete, obj.id, false, false)
   }
   Game.zfLoader.queue.load();
}

public static function onAssetLoadComplete(cbo:CallBackObject):void {
   _itemsToLoad--;
   Config.log('Assets', 'onAssetLoadComplete', 'LoadComplete: ' + cbo.name + " -- _itemsToLoad: " + _itemsToLoad);

   if(cbo.options != null 
      && cbo.options.hasOwnProperty('convertToTexture') && cbo.options.convertToTexture) 
   {
      // Add a T to the end of the file id, and auto convert it from bitmap
      Assets[cbo.name + 'T'] = Texture.fromBitmap(cbo.data);
   } 
   else 
   {
      Assets[cbo.name] = cbo.data;
   }

   if(_itemsToLoad == 0) {
      _itemLoadComplete = true;
      _checkReady();
   }
}

public static function onSoundAssetLoadComplete(cbo:CallBackObject):void {
   _soundsToLoad--;
   Config.log('Assets', 'onSoundAssetLoadComplete', 'LoadComplete: ' + cbo.name + " -- Size: " + cbo.data.bytesTotal + " -- _soundsToLoad: " + _soundsToLoad);
   Game.soundMgr.addSound(cbo.name, cbo.data);
			
   if(_soundsToLoad == 0) {
      _soundLoadComplete = true;
      _checkReady();
   }
}

public static function init():void {
   ta = new TextureAtlas(Texture.fromBitmap(atlas), XML(atlasXML));
			
   TextField.registerBitmapFont(new BitmapFont(Texture.fromBitmap(calistoMT), XML(calistoMTXML)));
   TextField.registerBitmapFont(new BitmapFont(Texture.fromBitmap(wizztaB), XML(wizztaXML)));
			
   towerData = Utils.JSONDecode(towerDataJSON);
   mapData = new MapData(Utils.JSONDecode(mapSelectJSON));
}

Before my Madden-esque play-by-play (line-by-line) breakdown of this block of code, a quick note about the CallBackObject class. It can be found in com/zf/loaders/CallBackObject.as and it is simply a 'struct' sort of data structure. I just wanted a very clearly-defined class that would have a name, data, and options member so I could call them later. BOOM! Back in the game...

Wrapping Up

Whew! We've finished loading all the assets we'll need to actually start the game. There is still some more loading after a user has selected a map to play, but it's only map-specific data. Tile data and enemy wave stuff. All of our sounds, spritesheets, fonts, and a vast majority of the art assets and heavy lifting background-sized jpg/png files have been loaded here. It's smooth sailing with hopefully a very limited amount of loading/processing in the future so the Player can jump straight into maps and start killing badguys!

This was probably the most boring way to kick off this tutorial series. You know you came for Tower and Enemies! That's the real meat of a TD game. Soon.... soon... I'm glad we got this out of the way because it was tricky trying to learn Starling and figure out how to use LoaderMax to load assets and get it to play nice with Starling. But now that that's done... we'll get into the good stuff next time!

Again, feel free to check out the finished product of my AS3 Starling TD Demo.
You can also find all of the code used in srcview.
Or you can download the whole project zipped up.
Or check out the repo on Bitbucket

Categories:

6 Comments

Leave a Reply

Avatar placeholder