In my previous post on External Loading using LoaderMax in Starling, I began going through the first State of the game, the GameLoad State. This post will go through each game state in detail as to why it’s there and what it does. Also, I show off and talk about a few UI components that I’ve created as they come up in the various States.

[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/2013 – added the Upgrade State

So, the following game states can currently be found in the com/zf/states/ folder:

  • GameLoad – the very first state! This is in charge of loading all of the initial assets and sounds and was outlined in the previous post
  • GameOver – this state provides post-battle/map feedback to players. Did they win? Lose? How many enemies did they kill? etc…
  • MapLoad – this state is in charge of loading map-specific assets which I’ll cover later
  • MapSelect – this state displays whatever “Map/Level Select” info, it also holds the Upgrades button to take you to the Upgrades State (when finished)
  • Menu – this state is your main Title/Menu type state. It’s the first screen the player gets to after the main game loads
  • Play – this state holds the actual action. It’s the biggest and most complex of the states.
  • Test – this is just a dummy state I created when I wanted just a blank IZFState Sprite to load to. It provides a quick way to jump straight to an empty screen so you can test UI components by themselves without having to go all the way through all the screens
  • Upgrades – this state allows the player to upgrade various stats and such

So first off, having a separate game state basically gives me a way to compartmentalize “scenes” or “screens” or whatever you want to call them; States. I gave a description of each state above so hopefully you have a better idea of how I’ve grouped my States. Again, since GameLoad was discussed last post, I’m going to skip it here.

GameOver.as

The following are the only really relevant functions in the State. As we saw from GameLoad, in the constructor we keep a reference to the main Game object, and add an event listener so we know when this State gets added to the stage. At that point, onAddedToStage runs and really sets up the state. This is the pattern I’ve used for all of the states.

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

   _gameStateBkgd = new GameStateBkgd('Game Over');
   addChild(_gameStateBkgd);

   _tryAgainBtn = new Button(Assets.tryAgainBtnT);
   _tryAgainBtn.x = 500;
   _tryAgainBtn.y = 400;
   _tryAgainBtn.addEventListener(Event.TRIGGERED, _onTryAgainTriggered);
   addChild(_tryAgainBtn);

   _gameStatsText = new ScrollText();
   _gameStatsText.x = 100;
   _gameStatsText.y = 120;
   _gameStatsText.width = 200;
   _gameStatsText.height = 400;
   _gameStatsText.isHTML = true;
   addChild(_gameStatsText);
   _gameStatsText.text = Config.totals.toHtml();
}

private function _onTryAgainTriggered(evt:Event):void {
   _game.changeState(Game.MAP_SELECT_STATE);
}

GameStateBackground component

The whole file? Yup… imports and all! So I got tired of adding an Image to hold the background, and a TextField to hold the title on every State, so being the resourceful and OOP-minded codemonkey that I am, I pulled that code into its own class to reuse it. Genius!… unless you’ve taken even one CompSci 101 course and then this is just basic stuff.

package com.zf.ui.gameStateBkgd {
   import com.zf.core.Assets;

   import starling.events.Event;
   import starling.display.Image;
   import starling.display.Sprite;
   import starling.text.TextField;

   public class GameStateBkgd extends Sprite
   {
      private var _bkgd:Image;
      private var _title:TextField;
      private var _titleText:String;

      public function GameStateBkgd(titleText:String) {
         _titleText = titleText;
         addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
      }

      public function onAddedToStage(e:Event):void {
         removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);

         _bkgd = new Image(Assets.gameStateBkgdT);
         addChild(_bkgd);

         _title = new TextField(700, 125, _titleText, "Wizzta", 100, 0xCC2323, true);
         _title.pivotX = 0;
         _title.pivotY = 0;
         _title.x = -50;
         _title.y = -10;
         addChild(_title);
      }
		
      public function destroy():void {
         _bkgd.removeFromParent(true);
         _bkgd = null;

         _title.removeFromParent(true);
         _title = null;
      }
   }
}

MapLoad

So, GameLoad loads the main game at the very beginning. Why have a MapLoad? What does it do?

There are arguments for and against this State even being here. Wait, let me back up. So, the GameLoad State exists to run after the player selects a Map to play. So Player clicks on map1’s icon, that icon sets Config.selectedMap and tells Game to changeState to MapLoad. As we’ll see in the code, MapLoad then takes Config.selectedMap and loads a specific map JSON file. The map JSON file contains all of the metadata about map1 including number of rows and columns, tile width/height (so it can change map to map), Enemy wave data, enemy groups and types, and all of the tile data needed to generate the map.

So, there are arguments for and against this State even being here. Why not load all the map JSON files in at the very beginning with GameLoad to save the user some time for each map? Sure, I could. You could pretty easily update the code to load all that stuff during GameLoad and do away with this State altogether. The flipside of this argument is that I’ve got roughly 35Kb+ worth of JSON data in one of those files. Times however many maps I have. Say even at 10 maps, you’re looking at a over a third of a Mb of raw data parsing into Objects that are residing in memory which the user may never even need. This way, only the current map that’s about to be played gets loaded into memory.

A great compromise between these two sides would be to load the map’s JSON file only when the user selects to play that map and then Save that map JSON object somewhere. That way if the user finishes the map and clicks try again and selects the same map, you have already loaded the file once, the map JSON object is still “cached” somewhere in your code, so just reuse it. I just thought about this right now and I like it. But, I didn’t write the code that way. I wrote it to load the map JSON file every time. See, here’s a way you can already make your own version better!

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

   _gameStateBkgd = new GameStateBkgd('Loading...');
   addChild(_gameStateBkgd);

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

   _playBtn = new Button(Assets.playMapBtnT);
   _playBtn.pivotX = _playBtn.width >> 1;
   _playBtn.x = 400;
   _playBtn.y = 450;
   _playBtn.alpha = 0.2;
   addChild(_playBtn);

   Game.zfLoader.onProgress.add(onProgress);
   Game.zfLoader.onComplete.add(onLoadComplete);

   // Load the map data
   var map:String = Config.PATH_JSON + 'maps/' + Config.selectedMap + '.json';
   Game.zfLoader.addToLoad(map, handleMapData, Config.selectedMap, false);
}

public function onLoadComplete():void {
   Game.zfLoader.onProgress.remove(onProgress);
   Game.zfLoader.onComplete.remove(onLoadComplete);
   Config.log('GameLoad', 'onLoadComplete', "MapLoad Complete");
   _playBtn.alpha = 1;
   _playBtn.addEventListener(Event.TRIGGERED, _onPlayMapTriggered);
}
		
public function onProgress(ratio:Number):void {
   Config.log('MapLoad', 'onProgress', "LOADING :: " + ratio * 100);
   _progBar.ratio = ratio;
}

public function handleMapData(cbo:CallBackObject):void {
   Config.currentMapData = Utils.JSONDecode(cbo.data);
}

private function _onPlayMapTriggered(evt:Event):void {
   _game.changeState(Game.PLAY_STATE);
}

public function destroy():void {
   _gameStateBkgd.destroy();
   _gameStateBkgd = null;

   _progBar.removeFromParent(true);
   _progBar = null;

   _playBtn.removeEventListener(Event.TRIGGERED, _onPlayMapTriggered);
   _playBtn.removeFromParent(true);
   _playBtn = null;
}

MapSelect.as

public function onAddedToStage(e:Event):void {
   removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
			
   _gameStateBkgd = new GameStateBkgd('Map Select');
   addChild(_gameStateBkgd);
			
   _map1 = new MapSelectIcon(Assets.mapData.getMapById('map1'), 'misc/mapSelect_map1');
   _map1.x = 100;
   _map1.y = 120;
   _map1.onClick.add(onMapSelectClicked);
   _map1.onHover.add(onMapSelectHovered);
   addChild(_map1);

   _map2 = new MapSelectIcon(Assets.mapData.getMapById('map2'), 'misc/mapSelect_map2');
   _map2.x = 100;
   _map2.y = 300;
   _map2.onClick.add(onMapSelectClicked);
   _map2.onHover.add(onMapSelectHovered);
   addChild(_map2);
			
   _mapText = new ScrollText();
   _mapText.x = 95;
   _mapText.y = 475;
   _mapText.width = 400;
   _mapText.height = 200;
   _mapText.isHTML = true;
   addChild(_mapText);
			
   _gameStatsText = new ScrollText();
   _gameStatsText.x = 400;
   _gameStatsText.y = 120;
   _gameStatsText.width = 200;
   _gameStatsText.height = 400;
   _gameStatsText.isHTML = true;
   addChild(_gameStatsText);
   _gameStatsText.text = Config.currentGameSOData.toHtml();

   _upgradesBtn = new Button(Assets.upgradesBtnT);
   _upgradesBtn.x = 525;
   _upgradesBtn.y = 425;
   _upgradesBtn.addEventListener(Event.TRIGGERED, onUpgradesBtnClicked);
   addChild(_upgradesBtn);
}

private function onUpgradesBtnClicked(evt:Event):void {
   _game.changeState(Game.UPGRADES_STATE);
}
		
public function onMapSelectClicked(map:Object):void {
   Config.addGameAttempted();
   Config.selectedMap = map.id;
   _game.changeState(Game.MAP_LOAD_STATE);
}
		
public function onMapSelectHovered(map:Object):void {
   if(map.id != _activeMapId) {
      _activeMapId = map.id;
      _activeMap = map;
      _updateMapText();
   }
}

private function _updateMapText():void {
   var fontOpenTag:String = '';
   var fontCloseTag:String = '';
   var txt:String = fontOpenTag + 'Map Title: ' + _activeMap.title + fontCloseTag + '
' + fontOpenTag + 'Desc: ' + _activeMap.desc + fontCloseTag; _mapText.text = txt; }

MapSelectIcon.as

com.zf.ui.mapSelectIcon.MapSelectIcon.as is a component I wrote to hold map data, an image, a textfield, and some logic. I pass in the map data Object, and a texture name to the constructor to kick things off.

package com.zf.ui.mapSelectIcon
{
   import com.zf.core.Assets;
	
   import org.osflash.signals.Signal;
	
   import starling.display.Image;
   import starling.display.Sprite;
   import starling.events.Touch;
   import starling.events.TouchEvent;
   import starling.events.TouchPhase;
   import starling.text.TextField;
	
public class MapSelectIcon extends Sprite
{
   public var onHover:Signal;
   public var onClick:Signal;
		
   private var _icon:Image;
   private var _data:Object;
   private var _tf:TextField;
		
   public function MapSelectIcon(data:Object, textureName:String) {
      _data = data;

      _icon = new Image(Assets.ta.getTexture(textureName));
      addChild(_icon);
			
      _tf = new TextField(200, 60, _data.title, 'Wizzta', 32, 0xFFFFFF);
      _tf.x = -45;
      _tf.y = 100;
      addChild(_tf);

      onHover = new Signal(Object);
      onClick = new Signal(Object);
			
      addEventListener(TouchEvent.TOUCH, _onTouch);
   }
		
   public function destroy():void {
      onHover.removeAll();
      onClick.removeAll();

      _icon.removeFromParent(true);
      _icon = null;

      _tf.removeFromParent(true);
      _tf = null;
			
      removeFromParent(true);
   }
		
   private function _onTouch(evt:TouchEvent):void {
      var touch:Touch = evt.getTouch(this);
      if(touch) {
         switch(touch.phase) {
               case TouchPhase.BEGAN:
                  onClick.dispatch(_data);
                  break;

               case TouchPhase.HOVER:
                  onHover.dispatch(_data);
                  break;
            }
         }
      }
   }
}

Menu.as

The Menu State is my “Title” main menu type screen where the user is presented with my fancy logo and new/load game options.

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

   _bkgd = new Image(Assets.bkgdT);
   addChild(_bkgd);

   _logo = new Image(Assets.logoT);
   _logo.x = 75;
   _logo.y = 75;
   addChild(_logo);

   _game1 = new LoadGameButton('Game 1', 'game1');
   _game1.x = 50;
   _game1.y = 450;
   _game1.onGameSelected.add(_onGameSelected);
   addChild(_game1);
			
   _game2 = new LoadGameButton('Game 2', 'game2');
   _game2.x = 300;
   _game2.y = 450;
   _game2.onGameSelected.add(_onGameSelected);
   addChild(_game2);

   _game3 = new LoadGameButton('Game 3', 'game3');
   _game3.x = 525;
   _game3.y = 450;
   _game3.onGameSelected.add(_onGameSelected);
   addChild(_game3);
}

private function _onGameSelected():void {
   _game.changeState(Game.MAP_SELECT_STATE);
}

LoadGameButton.as

public class LoadGameButton extends Sprite
{
   public var onGameSelected:Signal;

   private var _gameNameTF:TextField;
   private var _gameName:String;
   private var _gameId:String;
   private var _gameData:ZFGameData;
   private var _texture:Texture;
   private var _gameExists:Boolean;
   private var _loadGameBtn:Button;
   private var _deleteGameBtn:Button;

   public function LoadGameButton(gameName:String, gameId:String)  {
      _gameName = gameName;
      _gameId = gameId;

      var o:Object = Game.so.getGameData(_gameId);
      _gameExists = !_isEmptyGameDataObject(o);

      if(_gameExists) {
         _gameData = ZFGameData.fromObject(o);
         _texture = Assets.loadGameBtnT;
      } else {
         _texture = Assets.newGameBtnT;
      }

      onGameSelected = new Signal();

      _loadGameBtn = new Button(_texture);
      _loadGameBtn.addEventListener(Event.TRIGGERED, _onClick);
      addChild(_loadGameBtn);

      _gameNameTF = new TextField(150, 30, _gameName, 'Wizzta', 40, 0xFFFFFF);
      _gameNameTF.y = -40;
      addChild(_gameNameTF);

      if(_gameExists) {
         _addDeleteGameBtn();
      }
   }

   private function _addDeleteGameBtn():void {
      _deleteGameBtn = new Button(Assets.deleteGameBtnT);
      _deleteGameBtn.x = 130;
      _deleteGameBtn.y = 105;
      addChild(_deleteGameBtn);
      _deleteGameBtn.addEventListener(Event.TRIGGERED, _onDeleteGameBtnClicked);
   }

   private function _onClick(evt:Event):void {
      setCurrentGameData();
      onGameSelected.dispatch();
   }

   public function setCurrentGameData():void {
      Config.currentGameSOID = _gameId;
      if(_gameExists) {
         Config.currentGameSOData = _gameData;
      } else {
         Game.so.createGameData(Config.currentGameSOID, true);
      }
   }

   public function destroy():void {
      _gameNameTF.removeFromParent(true);
      _gameNameTF = null;
      _loadGameBtn.removeEventListener(Event.TRIGGERED, _onClick);
      onGameSelected.removeAll();

      if(_deleteGameBtn) {
         _removeDeleteGameBtn();
      }
   }

   private function _removeDeleteGameBtn():void {
      _deleteGameBtn.removeFromParent(true);
      _deleteGameBtn.removeEventListener(Event.TRIGGERED, _onDeleteGameBtnClicked);
      _deleteGameBtn = null;
   }

   private function _onDeleteGameBtnClicked(evt:Event):void {
      Game.so.createGameData(_gameId, true);
      _removeDeleteGameBtn();
      _loadGameBtn.removeFromParent(true);
      _loadGameBtn.removeEventListener(Event.TRIGGERED, _onClick);
      _loadGameBtn = new Button(Assets.newGameBtnT);
      addChild(_loadGameBtn);
      _loadGameBtn.addEventListener(Event.TRIGGERED, _onClick);
   }

   private function _isEmptyGameDataObject(obj:Object):Boolean {
      var isEmpty:Boolean=true;

      for (var s:String in obj) {
         isEmpty = false;
         break;
      }

      // If the object has data, see if it has Relevant data
      if(!isEmpty && obj.mapsAttempted == 0) {
         isEmpty = true;
      }
      return isEmpty;
   }
}

Test.as

Test is just a basic shell of a IZFState. This allows me to go straight from GameLoad and changeState to Test, instead of going to Menu. I can add any new UI components or whatever here so I can see them on the stage and test them. This came in really handy when I was working on the in-game GameOptionsPanel component that you can see if you actually go through the menus, start a game, then click the options button. The Test State makes things really fast and easy to test.

public class Test extends Sprite implements IZFState
{
   private var _game:Game;

   public function Test(g:Game)
   {
      _game = g;
      addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
   }
		
   private function onAddedToStage(evt:Event):void {
      removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
   }

   public function update():void {
   }

   public function destroy():void {
   }
}

And finally we get to see the Upgrade state!

Upgrade.as

package com.zf.states
{
   import com.zf.core.Assets;
   import com.zf.core.Config;
   import com.zf.core.Game;
   import com.zf.ui.gameStateBkgd.GameStateBkgd;
   import com.zf.ui.upgradeOption.UpgradeOption;
   import com.zf.ui.upgradeOption.UpgradeOptionVO;

   import starling.display.Button;
   import starling.display.Sprite;
   import starling.events.Event;
   import starling.text.TextField;

public class Upgrades extends Sprite implements IZFState
{
   private var _game:Game;
   private var _mapSelectBtn:Button;
   private var _gameStateBkgd:GameStateBkgd;
   private var _upgradeOptions:Array;
   private var _ptsTotal:int;
   private var _ptsSpent:int;
   private var _ptsAvail:int;
   private var _labelTF:TextField;
   private var _ptsTF:TextField;
   private var _resetBtn:Button;

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

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

      var upgrades:Object = Game.so.getGameDataProperty('upgrades');

      _ptsTotal = upgrades.ptsTotal;
      _ptsSpent = upgrades.ptsSpent;
      _ptsAvail = _ptsTotal - _ptsSpent;

      _gameStateBkgd = new GameStateBkgd('Upgrades');
      addChild(_gameStateBkgd);

      _labelTF = new TextField(200, 35, 'Available Points', 'Wizzta', 30, 0xFFFFFF);
      _labelTF.x = 550;
      _labelTF.y = 90;
      addChild(_labelTF);

      _ptsTF = new TextField(200, 35, _ptsAvail.toString(), 'Wizzta', 30, 0xFFFFFF);
      _ptsTF.x = 650;
      _ptsTF.y = 90;
      addChild(_ptsTF);

      if(_ptsTotal > 0) {
         _resetBtn = new Button(Assets.resetUpgradesBtnT);
         _resetBtn.x = 700;
         _resetBtn.y = 135;
         _resetBtn.addEventListener(Event.TRIGGERED, onResetBtnClicked);
         addChild(_resetBtn);
      }
      _upgradeOptions = [];

      _mapSelectBtn = new Button(Assets.mapSelectBtnT);
      _mapSelectBtn.x = 525;
      _mapSelectBtn.y = 450;
      _mapSelectBtn.addEventListener(Event.TRIGGERED, onMapSelectBtnClicked);
      addChild(_mapSelectBtn);

      addUpgradeOptions([
         upgrades.towerSpd,
         upgrades.towerRng,
         upgrades.towerDmg
      ]);
   }

   public function addUpgradeOptions(opts:Array):void {
      var len:int = opts.length,
          uo:UpgradeOption,
          vo:UpgradeOptionVO,
          shouldEnable:Boolean = (_ptsAvail > 0);

      for(var i:int = 0; i < len; i++) {
         vo = new UpgradeOptionVO(opts[i]);
         uo = new UpgradeOption(vo.id, vo.label, vo.totalRanks, vo.bonusPerRank, shouldEnable);
         uo.x = 100;
         uo.y = 200 + (100*i);
         addChild(uo);
         uo.currentRanks = vo.currentRanks;
         _upgradeOptions.push(uo);
      }

      if(shouldEnable) {
         toggleUpgradeOptions(true);
      }
   }

   public function toggleUpgradeOptions(enable:Boolean):void {
      var len:int = _upgradeOptions.length;
      for(var i:int = 0; i < len; i++) {
         if(enable) {
            _upgradeOptions[i].optionChanged.add(onOptionChange);
            _upgradeOptions[i].enable();
         } else {
            _upgradeOptions[i].optionChanged.remove(onOptionChange);
            _upgradeOptions[i].disable();
         }
      }
   }

   public function resetUpgradeOptionsToZero():void {
      var len:int = _upgradeOptions.length;
      for(var i:int = 0; i < len; i++) {
         _upgradeOptions[i].enable()
         _upgradeOptions[i].currentRanks = 0;
         // since we created a new button, have to re-add listener
         _upgradeOptions[i].optionChanged.add(onOptionChange);
      }
   }

   public function onOptionChange(addedRank:Boolean, opt:Object):void {
      if(addedRank) {
         _ptsSpent++;
      } else {
         _ptsSpent--;
      }
      _ptsAvail = _ptsTotal - _ptsSpent;

      _updatePtsAvailTF();

      Config.currentGameSOData.updateFromUpgrades(opt, _ptsTotal, _ptsSpent, _ptsAvail, true);
      if(_ptsAvail <= 0) {
         toggleUpgradeOptions(false);
      }
   }

   public function onResetBtnClicked(e:Event):void {
      _ptsSpent = 0;
      _ptsAvail = _ptsTotal;
      resetUpgradeOptionsToZero();
      _updatePtsAvailTF();
   }

   private function _updatePtsAvailTF():void {
      _ptsTF.text = _ptsAvail.toString();
   }

   private function onMapSelectBtnClicked(evt:Event):void {
      _game.changeState(Game.MAP_SELECT_STATE);
   }

   public function update():void {
   }

   public function destroy():void
   {
      _mapSelectBtn.removeFromParent(true);
      _mapSelectBtn = null;
			
      _ptsTF.removeFromParent(true);
      _ptsTF = null;
			
      _labelTF.removeFromParent(true);
      _labelTF = null;
			
      if(contains(_resetBtn)) {
         _resetBtn.removeFromParent(true);
         _resetBtn.removeEventListener(Event.TRIGGERED, onResetBtnClicked);
         _resetBtn = null
      }
			
      var len:int = _upgradeOptions.length;
      for(var i:int = 0; i < len; i++) {
         _upgradeOptions[i].destroy();
      }

      _gameStateBkgd.destroy();
   }
}
}

So, we've reached the end of our post on States! Yay! What's that? We missed one? The Play State? Oh... whoops... yeah, that sucker is large enough for it's own post :). Next time I'll discuss the Play state, and it will lead us into the good stuff! Enemies! Towers! GOOOOLD!

Feel free to check out the actual ZFStarlingTDDemo I created.

Thanks for reading, I hope this was helpful.
-Travis

Categories:

2 Comments

Leave a Reply

Avatar placeholder