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);
}
- Line 4 – well looky there. Our first ZF-original UI component! I’ll discuss GameStateBackground just after this section. Here, I create one and add it to the stage
- Line 7-11 – since this is the Game Over screen, we need to add a Try Again button so the player can make it back to the map select screen to play again. Pretty standard changing the button’s x/y position and adding an event listener for when it is clicked
- Line 13-19 – create a new ScrollText (feathers.controls.ScrollText) component, adjust its dimensions, make sure isHTML is true, then add it to the stage
- Line 20 – set the text inside the ScrollText to the Config’s Totals output.
- Line 24 – when the try again button is clicked, tell Game to changeState back to the MapSelect screen so the player can choose his/her next map to play
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;
}
}
}
- Line 23-24 – create my _bkgd background Image using the gameStateBkgdT Texture from Assets and add that to the stage
- Line 26 – create a starling.text.TextField passing in width, height, text, fontName, fontSize, color, and if that TextField is rad or not (true). Actually I forget what the last param is. Any ideas? Ah, you can’t respond to me. Ok I looked it up and it is… BOLD! Which since I’m passing in true, just makes my text bold
- Line 29-33 – pivotX/pivotY are Starlings way of setting registration points. I still haven’t figured out why the Starling TextField likes to display all funky, but I had to set the x/y params to negative values to move them left and up (respectively) on the stage. Also, these numbers could still use some more tweaking. Oh well, add that sucker to stage
- Line 35-39 – remove both the background Image and the title TextField from the stage and 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;
}
- Line 4-5 – add the GameStateBkgd component
- Line 7-10 – add the ProgressBar component
- Line 19-20 – hook into my ZFLoader’s Signals so those functions get called as ZFLoader is loading data
- Line 22 – a wild comment appears!
- Line 23 – create our path to the map JSON file
- Line 24 – add map JSON file to ZFLoader to load
- Line 28-29 – onLoadComplete was called because the ZFLoader was done loading the map JSON file, so we can remove our listeners from those Signals
- Line 31-32 – back in Line 14, we set the play button’s alpha to 0.2 and did not attach an event listener, effectively making it not clickable and dim. Here in Line 31, since the map has loaded, add an event listener to the button and set it’s alpha to 1 so the player can clearly see this is what they should click next to Play the Map!
- Line 37 – onProgress gets triggered from ZFLoader dispatching its onProgress Signal. This is used to keep the progress bar moving
- Line 41 – handleMapData is the callback handler for when the map JSON file load completes. Convert the JSON data to an Object and save that in Config.currentMapData
- Line 45 – _onPlayMapTriggered gets called when the user clicks the Play Map button. Tell Game to changeState to Play so we can begin the map
- Line 49-57 – all States have a destroy() function. I usually don’t post them here because they’re boring, but since I haven’t really posted any, we’ll walk through this one. Each State is tasked with cleaning up after itself.
- Line 49-50 – call _gameStateBkgd’s destroy() function and null the object
- Line 52-53 – remove _progBar from the stage and null
- Line 55-57 – remove _playBtn’s event listener, remove it from stage, and null it
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;
}
- Line 7-12 – MapSelectIcon (com.zf.ui.mapSelectIcon.MapSelectIcon.as) is another component I made, we’ll look at that after this State. It’s basically just a Sprite that holds a TextField, an Image that listens for hover/click events and dispatches Signals, and a way to parse the map data (id, title, description, etc) into info the user could read. This is a very poor UI, but it works. Set the x/y coordinates, listen for MapSelectIcon to dispatch onClick or onHover Signals, and add to stage
- Line 14-19 – repeat for the second map
- Line 21-27 – add a ScrollText component to the stage, setting x/y/width/height and make sure isHTML is true then add to stage. This ScrollText will be used display the map info when a player hovers over one of the two map icons
- Line 29-36 – add another ScrollText to the stage, this one will be used to display SharedObject information that has been saved for this game. Again, please note that these ScrollTexts are a terrible way to do UI. A popover bubble with the text info, or some other way to display the information would be a much better choice. But this is a coding demo. Not to say coders can’t do fantastic UI!
- Line 38-42 – add a Button with the upgradesBtnT Texture to stage so the player can get to the Upgrades state
- Line 46 – player clicked the Upgrades button, tell Game to changeState to the Upgrade State
- Line 50 – player clicked on a map select icon, so call Config.addGameAttempted() function which simply handles adding 1 to the shared object’s “mapsAttempted” variable for this specific game. This might not be a great stat to track in a real game, but it was an easy one to show off in a demo
- Line 51 – set Config.selectedMap to the id in the map select icon’s map data so that MapLoad can use it to load the map JSON
- Line 52 – now that we’ve set the selectedMap id on Config, tell Game to changeState to the MapLoad State
- Line 56 – check to make sure this is a different map id than the id of the map info we’re already displaying. There’s no reason to update the ScrollText with the exact same map data, so if its the same data, don’t bother updating
- Line 57 – set the _activeMapId for future if-checks
- Line 58 – set the _activeMap to the map object dispatched by MapSelectIcon’s onHover Signal
- Line 59 – display the map info to the user
- Line 64-67 – handles when a user hovers over a map select icon,
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;
}
}
}
}
}
- Line 26-27 – create a new Image getting the Texture from the Assets TextureAtlas then add the Image to stage
- Line 29-32 – create a new TextField (starling.text.TextField) to hold the map’s title, set the x/y, then add to the stage
- Line 34-35 – instantiate two new Signals that will be dispatching an Object as data
- Line 37 – add a TouchEvent.TOUCH listener to this MapSelectIcon to know if players are hovering over it or clicking on it
- Line 41-50 – remove callback functions from the Signals, remove the Image and TextField from the stage, and remove this MapSelectIcon from its parent to destroy it
- Line 53 – my TouchEvent handler
- Line 54 – get the “touch” that happened on this MapSelectIcon object
- Line 55 – if there actually was a touch, then let’s check the touch phase
- Line 57-58 – TouchPhase.BEGAN is the equivalent to MouseEvent.MOUSE_DOWN. It’s when the user first clicks their mouse on the icon, if they do, then we’re going to dispatch our _data Object so whomever is listening can take that map data and do magical things with it
- Line 61-62 – TouchPhase.HOVER is MouseEvent.MOUSE_OVER so we know the user is moving their mouse over this icon. Huge complaint, there is no TouchPhase.HOVER_END or any way to know that the mouse has left the icon.
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);
}
- Line 4-5 – I chose not to use a GameStateBkgd object and just put the actual background Image on the stage here because the GameStateBkgd requires a title and I wanted my pretty logo to be the title on this screen
- Line 7-10 – create a new Image for my logo using the Assets.logoT Texture, set the x/y and add to stage
- Line 12-28 – _game1, _game2, and _game3 are all LoadGameButton components. Again, another component I created and again I’ll explain just after this State. Basically three times in a row I’m creating a new instance of a LoadGameButton class, passing in the “Game 1” title and the “game1” id, setting x/y, adding callbacks to LoadGameButton’s onGameSelected Signal, and then adding it to the stage
- Line 32 – when the user clicks one of the game buttons _onGameSelected() function gets called, which tells Game to changeState to the MapSelect 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;
}
}
- Line 18 – call Game’s SharedObjectManager’s getGameData and pass in the game id to see if there is any saved game data already
- Line 19 – check to see if the game object is empty
- Line 22-23 – if saved game data exists, set the _gameData object with the game data from the shared object and set _texture to be the load game button texture
- Line 25 – no game data exists, set the _texture to use the new game button texture
- Line 28 – create a new Signal for when the player clicks this LoadGameButton
- Line 30-32 – create the loadGameBtn using the Texture in _texture depending on if there was saved data or not, add an event listener to know when the button is clicked, then add to stage
- Line 34-36 – create a new TextField to hold the game title (“Game 1”) and add that to the stage
- Line 38-39 – if saved game data did exist, then call _addDeleteGameBtn to… add the delete game button.
- Line 44-48 – create a new button using the Assets.deleteGameBtnT Texture, set the x/y, add to stage, then add an event listener to know when it is clicked
- Line 52-53 – _onClick is called when the New/Load game button is clicked, it calls setCurrentGameData() and then dispatches its onGameSelected Signal
- Line 57 – set Config.currentGameSOID to this LoadGameButton’s _gameId
- Line 59 – if saved game data existed, set Config.currentGameSOData to this LoadGameButton’s _gameData object. Basically we’re getting Config set up to know the game ID and have game data ready for when we change states, so the next State can do something with this info
- Line 61 – if no saved game data existed, then tell Game’s SharedObjectManager to create a new saved game object on the player’s HD with this _gameId
- Line 67-73 – pretty standard, remove objects from stage, null them, remove event listeners, remove Signal callbacks, and if we actually did create a _deleteGameBtn, then remove that too
- Line 77-79 – removes the delete game button from stage, removes event listener, then nulls
- Line 83 – so there was saved game data, we added the delete game button to the stage, and now the user clicked it. Game.so.createGameData will basically rewrite this saved game slot with a fresh game object, erasing any game data that was saved there previously. UX Note: you should have a popup or some sort of notification letting the player know that this is IRREVERSIBLE and the player will lose all their data by continuing
- Line 84 – remove the delete button from stage
- Line 85-86 – remove the old _loadGameBtn from the stage and remove its event listener because…
- Line 87-89 – we’re going to create a whole new _loadGameBtn with the new “newGameBtnT” Texture (instead of the old load game texture), add it to stage, and add it’s event listener back
- Line 92 – _isEmptyGameDataObject is a helper function I wrote that should probably go in a Utils class at some point so other classes can use it
- Line 95-96 – iterates over the object that is passed in, if there are any members to that object at all, it isn’t empty and we break from the loop
- Line 101 – if the object is not empty, but there is no data actually saved here (as in, we created a new game object in memory, but nothing has actually been saved there yet), then also return that it is empty.
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();
}
}
}
- Line 36 - get the 'upgrades' property off our saved game data object
- Line 38-40 - set our local variables from the saved data. Total points, the points we've spent, and calculate the points available to spend
- Line 42-43 - add the background with an "Upgrades" title
- Line 45-48 - create the "Available Points" TextField and add it to the Sprite
- Line 50-53 - create the points TextField and add it to the Sprite. This could be the exact same TextField as the previous one, just updated differently. Not sure why I didn't do that.
- Line 55-60 - if the player has any points at all, add the reset Button
- Line 64-68 - add the Map Select Button to give the player a way back from the Upgrades State
- Line 70 - call addUpgradeOptions and pass in a new Array with the three upgrade options from the saved game object
- Line 77 - adds the UpgradeOption components to the State
- Line 83 - loop through all the options passed in
- Line 84 - create a new UpgradeOptionVO from the data passed in. This is just an Object.
- Line 85-88 - create a new UpgradeOption passing in the params, set it's x,y and add to the Sprite
- Line 89 - set the currentRanks on the UpgradeOption (which causes it to update and look pretty)
- Line 90 - push the UpgradeOption to our array
- Line 94 - if shouldEnable is true, toggle all the options and enable them
- Line 98 - toggles upgrade options enabling or disabling all of them
- Line 102-103 - loop through all UpgradeOptions and if we're enabling them, add a callback to the optionsChanged Signal and tell the UpgradeOption to enable()
- Line 105-106 - if we're disabling, remove the callback and call disable() on the UpgradeOption
- Line 113 - when the reset Button gets clicked, loop through all UpgradeOptions, enable them and set their current ranks to 0
- Line 121 - when an option changes...
- Line 122-127 - if they clicked the plus button, increment _ptsSpent, otherwise decrement it. Then update our _ptsAvail[able] based on the new changes.
- Line 129 - update the Points Available TextField
- Line 133 - if we have no more points available to spend, disable the upgrade options
- Line 137 - handles the reset button being clicked. Reset points spent to zero, set points available to the total (max) points we have, call the resetUpgradeOptionsToZero and then update the points available TextField
- Line 145 - set the text of our points available TextField to the _ptsAvail value
- Line 149 - when the Map Select Button is clicked, change the game state to the Map Select State
- Line 152 - no real updating here, but it is a IZFState so we have to have the function
- Line 155 - destroy() loops through the upgrade options and visual components and removes them
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
2 Comments