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):
- Lee Brimlow’s GotoAndLearn – hands-down one of the best sites that helped me learn AS3 way back.
- Starling’s Home – has links to tutorials, forums, docs, etc.
- Starling’s Forums
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.
- Code a start-to-finish TD game using Starling, not just a bunch of loosely-knit mini-projects
- Create 2 types of Enemies with different properties that could be configured from a JSON file
- Create 2 types of Towers with different properties that could be configured from a JSON file and can be upgraded during play, increasing its stats
- Create 2 different Maps with different paths configured from JSON map files
- Use solid OOP design: reusable, extensible classes intelligently using hierarchy but favoring composition
- Have a majority of the demo actually done before starting to reduce the incidence of rewriting code, updating posts, and generally becoming confusing instead of helpful
- Use AS3 Signals as often as possible instead of Starling/Flash events
- Play/Pause capability
- Incorporate keyboard events (‘P’/’Space’ for pause…)
- Configurable options UI with sound settings
- Begin building reusable UI components that I can use in future games
- SharedObject Management: save stats and such
Topics Covered
In this post I’ll cover:
- Overview of game project structure
- Loading game assets from a core JSON file
- Converting AS3 Bitmap objects to Starling Textures
- Loading sound assets using TreeFortress’ SoundAS
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:
- ZFStarlingTDDemo.as – this is the “Main” class, or document class, or class that actually kicks things off and runs when the game starts. Everything starts here.
- /assets – for storing JSON files, art, sound, and other assets
-
/com –
-
/zf – contains the main code that I’ve personally written. zf being ZombieFlambe (dot com! :D)
- core – contains project-wide/’core’ classes that are mostly key across any game state
- loaders – contains my ZFLoader main game loading class as well as any other classes central to loading
- managers – contains my manager classes that control and orchestrate game objects during various states
- objects – contains the various game classes/objects to be used during the main game Play state
- states – contains the different states used during the course of the game. This includes loading states, game play states, game over feedback to the player, etc.
- ui – contains different UI components that I created for use during the demo
- utils – contains the sort of catch-all kinds of helper classes and utility classes used to help centralize code
-
/zf – contains the main code that I’ve personally written. zf being ZombieFlambe (dot com! :D)
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;
}
}
- Line 3 – initialize the SoundManager class for use throughout the whole Game project. More on that in a later tutorial
- Line 4 – make sure Game gets added to stage before we do anything
- Line 10 – initialize my SharedObjectManager so I can check if any saved game data exists, and I can use that from the start
- Line 13 – change the state to the GameLoad state
- Line 18 – starts the changeState function that changes the game’s state
- Line 21 – on first load, currentState will be null, so we don’t need to do any of this. However, subsequent calls to changeState requires the following
- Line 22 – removes the addEventListener(Event.ENTER_FRAME,…) call so no states update. I added this because it seemed sometimes while I was transitioning from one state to another, functions in a previously-destroyed state would still try to run during update() (probably due to missing an active event listener in a file somewhere so the state never got fully garbage collected
- Line 23 – call destroy() on currentState, which is required in-turn to call destroy() on any assets or objects that that State creates. States are the upstanding citizens here required by Game.as LAW to clean up after themselves. Game calls destroy() on the State, the State calls destroy() on its created objects, those objects call destroy() on anything they created, and so on.
- Line 24 – remove the actual State (which all extend starling.display.Sprite) from the stage
- Line 25 – sets currentState to null to free up memory
- Line 29-57 – contains the switch statement that lets us handle creating any State we need on the fly
- Line 31 – since GAME_LOAD_STATE was passed in from Line 13, we’re going to make currentState = a new GameLoad State
- Line 60 – add that State to the stage, thus kicking off that State’s added to stage listener
- Line 62-65 – if we actually removed a currentState from the stage during lines 21-26, then we want to re-add that Event.ENTER_FRAME listener so all update() functions will continue to be called
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);
}
- Line 2 – save a reference to the Game object so we can get back to changeState()
- Line 3 – wait until the State is added to stage to do any setup
- Line 7 – remove listener since we’re now on the stage and don’t need it
- Line 9-12 – create a new ProgressBar (com/zf/ui/ProgressBar.as), position it, and add it to the stage
- Line 14 – this is the first example of AS3-Signals in action. The ZFLoader class dispatches an onProgress Signal, and I’m wanting to “listen” for that signal (“event”), and I want my onProgress() function to be called when Game.zfLoader.onProgress dispatches.
- Line 15 – same thing, Assets will dispatch an onInitialLoadComplete Signal and I’m giving it my onLoadComplete function as an event handler/callback function
- Line 17 – tell Assets to begin loading
- Line 21 – when Game.zfLoader dispatches that onProgress Signal, it also sense a decimal number, the progress ratio of data to load and data loaded. This is a number between 0 and 1. We’ll look at ZFLoader (com/zf/loaders/ZFLoader.as) a bit later in this post
- Line 25-26 – Assets has dispatched an onInitialLoadComplete Signal so we know it’s done loading. I want to remove the functions I added to the Signals. This is exactly like how you would use removeEventListener
- Line 28 – tell Assets to begin initializing
- Line 29-30 – remove progress bar from the stage and null it out
- Line 32 – call Game’s changeState function and tell it we want to go to the Menu State next
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);
}
}
}
- Line 28-35 – ZFLoader is a Singleton class. getInstance() checks to see if an instance of this class has been created yet. If it has not been created yet, create the instance and return the instance. If it has been created, just return the instance that was created.
- Line 37-43 – If I try to use the class constructor by itself, var zfL = new ZFLoader() this will throw an error. If I call getInstance() and the instance has not been created, it will let me call the constructor. This calls initQueue to initialize my LoaderMax queue.
- Line 46-47 – create two Signals to be dispatched when there’s a Progress event or something completes loading.
- Line 48-54 – initialize queue to be a new LoaderMax Object and I pass in handlers for LoaderMax’s events
- Line 55 – create callBackArray as a new Array
- Line 58 – addToLoad() is called all over to add a path for ZFLoader to load. The params are path, a callback function, an id, if ZFLoader should include the default asset path (or if you’ve provided the full path already this can be false), if the queue should be loaded immediately (or false if you’re going to manually start the queue yourself), and an options Object
- Line 59 – get the filename from the path provided
- Line 60 – get the extension of the filename from the filename
- Line 68 – some types of loaded content require you to get the data from a ‘rawContent’ variable, and some from just a ‘content’
- Line 69-74 – if I’m loading a jpg, png, or gif (do people still use gif?) then I want to set the useRawContent to true because I’ll get the loaded content from the “rawContent” member.
- Line 77-83 – create a new Object and push that to the callBackArray that contains the filename, the callback function for that item, the id, if we should useRawContent or not, and any options that were passed in. We’ll look at what the options do later.
- Line 88 – switch on the file extension
- Line 92 – if the file extension is xml or pex (an xml-based file created by Particle Designer for particle effects), use the XMLLoader
- Line 98 – if the file extension is jpg, png, or gif use the ImageLoader
- Line 103 – if the file extension is json or fnt (a text file created by Glyph Designer to make bitmap fonts), use the DataLoader
- Line 107 – if the file extension is mp3, use the MP3Loader
- Line 120 – if startQueueLoad was passed in as true, start the queue loading, otherwise it’s up to whatever called addToLoad to start the queue themselves
- Line 116-122 – addCallbackToQueueEvent() is not used anywhere in the demo, but I’ve left it in the codebase because it can be helpful in the future. It lets me add specific callback handlers to specific queue events. removeCallbackToQueueEvent() removes it. So after everything gets loaded and the Map starts, let’s say, and I wanted to load something new in on the fly from my Map class, or TowerManager or some other class, I could quickly add the callback listening for the complete event and pass it a callback function, so when that got loaded, I could handle that loaded file in the callback from some other class somewhere. It’s a tough sell to imagine a use case for this, but there are some and it can be handy.
- Line 124 – onCompleteHandler() handles when the queue has completed loading Anything. This is not when the whole queue is complete so this is why this function gets a little convoluted. As we’ll see, some items may still be loading and not complete, so we need to keep those in our callBackArray… I’ll step through it.
- Line 129 – nextPassArray is an Array that will hold items that have not completed loading yet. Now remember, these hold simple Objects that are basically just my way of keeping an “index” of sorts of which items are in the actual LoaderMax queue. These are not references to the loading items themselves.
- Line 133 – while callBackArray has any items in it…
- Line 136 – shift the 0th element off callBackArray. shift() is destructive, meaning it changes callBackArray when I shift that element off.
- Line 141 – get the content for the item.id
- Line 144 – if the content has not loaded yet…
- Line 147 – push the item Object to nextPassArray so it can be kept in callBackArray for the next pass (next time onCompleteHandler gets called)
- Line 149 – else my content has been loaded and I need to handle it.
- Line 152 – if the item has useRawContent set to true (jpg/png/gif files)
- Line 153 – set data to contentData.rawContent
- Line 155 – set data to just contentData
- Line 160 – set callBackArray (which has become empty through the while loop) to the nextPassArray which only contains Objects that are still loading in the queue
- Line 162-165 – if there are no items in callBackArray, dispatch the onComplete Signal letting other classes know we’re done loading everything that has been placed in the queue.
- Line 168-170 – when the queue sends out Progress events, dispatch the onProgress Signal with the progress amount so I can update my loading progress bar
- Line 173-175 – if there’s an error log it out
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...
- Line 2 - initialAssets.json has completed loading and it comes back from ZFLoader as a CallBackObject... object. CallBackObject.data is where I store any contents that got loaded. In this case, since I loaded a JSON file, I know that cbo.data is going to be JSON data. So I call my Utils.JSONDecode() to get the actual JSON Object. A quick rant inside this bulletpoint (nobody expects a rant in a cleanly marked bulletpoint, these are supposed to be efficient and to the (bullet)point but no, here I go rambling)... ok, Starling for some reason doesn't play nice with Adobe's JSON decode/encode classes. I don't know why. Maybe it's my IDE, maybe Starling conflicts somehow? Either way, I don't care. We're developers. We work around stupidness all the time and I created a Utils class and started collecting some functions there to help me work around things.
- Line 5-8 - loop through all those {"id": "atlas", "file": "assets/images/atlas.png"} type objects that we loaded in from initialAssets.json and individually add them to the loader to be loaded. Check the addToLoad function signature in ZFLoader, I'm passing in the file name, a callback/complete handler, an ID to use to refer to that specific data, false to tell ZFLoader that I'm passing in my own full path to this asset and not to try to build its own path, false to tell ZFLoader not to immediately start loading the load queue because I manually will start it later on line 9, and finally the file object itself so that I can keep the file metadata intact to check and use the convertToTexture key later on if it exists
- Line 12-22 - this is exactly the same as onInitialAssetsLoadComplete except there's a different callback handler. This could probably be abstracted out and cleaned up to be the same function
- Line 25 - I'm keeping track of the number of items I have to load, decrementing that number here
- Line 26 - log out that this object is done loading
- Line 28-29 - check to make sure the CallBackObject's options var has something in it (isn't null), and that options has a "convertToTexture" key in there and that that key is true. I honestly don't know why I'm checking if it's true. It's not like I'd go through the trouble to add a "convertToTexture" and make it false, but who knows.
- Line 32 - so this is an asset that I have specified in initialAssets.json to auto convertToTexture. I made sure that every "id" in initialAssets.json matched up with a variable on Assets itself. I wanted to separate my Texture vars with an additional "T" at the end of their var name. For example, {"id": "logo", "convertToTexture": true, "file": "assets/images/logo.png"} has the "id" of "logo" and convertToTexture is true, so I'm going to put that in Assets.logoT. You can see on this line that I'm taking the cbo.name (which pertains to the "id" attribute from the JSON file (sorry)) and adding a + "T" to the name.
- Line 36 - for assets that do not need to be converted to a Texture, just simply use the name and set the data
- Line 39-42 - check if we've loaded all the initialAssets objects, if so, set the _itemLoadComplete flag to true and call _checkReady() to see if everything else Assets needs to load is done yet
- Line 45+ - again the sound handler is exactly the same for the most part except one small thing
- Line 48 - I'm calling my SoundAS sound manager that I keep statically on Game.soundMgr and I'm passing in the name/id of the sound and the sound object itself. SoundAS takes it from there storing it however until I need to use that sound
- Line 56 - init() gets called from the GameLoad State's onLoadComplete function after both initialAssets.json and sounds.json have fully finished loading their own assets.
- Line 57 - this is a bit of Starling business... creating the TextureAtlas class and passing in my spritesheet (atlas) and its accompanying XML data (atlasXML)
- Line 59-60 - more Starling business. I created a spritesheet of a font using GlyphDesigner. This was an incredibly easy process and it saves quite a bit of resources having a simple spritesheet of the font's characters I need as opposed to embedding whole chunks of fonts. I'm passing in Starling's BitmapFont object type and the accompanying XML data to register this font with Starling's TextField. This is something I enjoyed about Starling. I register the font with its spritesheet and XML, then I just refer to it by it's name when I create a Starling.text.TextField class and it knows how to render your text. Pretty slick!
- Line 62-63 - decode towerData.json and mapSelect.json into their own objects. More on those later.
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
6 Comments