AS3 Starling Tower Defense Tutorial – Part 8 – UI Menu Components

Bad UI will tarnish every good game design decision you’ve ever made about your game.

So, you’ve finally finished your game. It has well-thought-out gameplay mechanics that you perfected by talking it out (to yourself in the shower). You didn’t even know you were so clever until you started writing dialogue for your characters, now maybe you can leave coding behind and be a game writer? Maybe write for Conan? Yeah, totally. Now, you did spend way too much time running around with a sound recorder near your parents swimming pool to get that raw water sound you needed for that one level, but the sound design is superb you must say. You finally figured out Photoshop well enough to know that those filters are meant to be used by people that know what those settings mean, and who intentionally change them to make something interesting, not just willy nilly changing settings til something doesn’t look awful. And as best you can tell, your code was 100% bug-free when you uploaded it. You’re a gaming success story and tomorrow you’re sure all your indie game heroes will be emailing you. “They’re not just stupid games now are they, dad!” you’ll get to yell on the phone, self-righteous, vindicated by your success. But… you overlooked one tiny, massive thing. The UI.

The reviews start rolling in.

  • “…had to wait through a whole minute of intros and story before the main menu. A minute! For a flash game!” Well, there were a few logo animations I really had to have for the sites I posted it on, and I wanted the user to hear the clever story intro I wrote right up front at the beginning.
  • “…no info/feedback on what specific upgrades actually do for the player, you can level up “Gold” but what does that mean…” It’s goooold you idiot! More points means more gold!
  • “…takes 5 seconds for the Options screen to animate in so I can update a setting…every single time” But guys, that was a badass animation, I mean, I went to bed around 4:40am after finishing that…
  • “…no mute button… have to leave the game and go back to the main menu to adjust the sound…” You could just turn it on or off at the beginning of the game before you started, why interrupt the audio while you’re playing. It provides AMBIANCE!
  • “…had to click on every level on the map select screen to see which one to play next…” But.. but.. well, maybe I should’ve done a better job of guiding the player as to which level he/she just unlocked and can play next.

You get the point. If you had bad game mechanics, terrible sound design, etc, but had a great UI, it’d be a bad game. If you had great game mechanics, great sound design, but had bad UI, it still might be a great game, but you’re punishing the player for playing and most likely people will see it as a bad game. You already got the point, I’m preaching to the choir. And why am I preaching anyways? Get back to swabbin’ th’ deck, ye codemonkey!

A quick note on this post, I’ve broken this post into 2 (more) parts. UI Menu Components (sub-part 1) and UI Game Components (sub-part 2). So there’s a whole new post about the UI components I use in the game itself coming in the next few days! If you happen to read this before the other post is done, I should have it finished by friday. These UI posts take a bit more writing and images and stuff, but I’ll have all the component code shared with you by the end of the week, thanks!

Now, some of those example ‘reviews’ above deal more with UX (User Experience) than UI (User Interface), but that’s a whole different blog post. Or a whole different thesis. Being a coder, I’m going to approach UI from a very methodical standpoint of “I have a problem, I need to fix the problem, here’s your solution and it may or may not have an adequately tied bow on it, but it is a solution.” This whole tutorial series has been more or less my approach to creating a TD game, so we’ll continue that trend with my approach to UI. Remember, this is not a UI expert writing this, nor an artist.

I am a terrible, terrible designer. I Photoshop at a 3nd grade level. My stick figures are misshapen and anyone watching my choice in colors will wonder if I did more of the eating of the crayons (non-toxic, they said…) than the using of the crayons. But the fact remains… I want to make games. I wanted to make this demo. You can’t go running to a design friend every 5 minutes when you need a new placeholder graphic for this or that. So if you’ve checked out the finished demo then you already know that the nicest thing one could say about my UI is that it “is functional.” As this is a demo, I wanted to show how I made the very basic of buttons, menus, HUD, etc. Ignore the artwork before your eyes, and see deep, deep, deep down inside where the code beauty exists…

LoadGameButton.as

Problem

As a player, on the main menu screen, I want to be able to create a new game, load a saved game and delete a saved game.

Solution

This is a wide open problem with a large number of solutions. I won’t go into all the ones I didn’t choose, but here’s the way I went. The Solution: I want a single class to handle the logic for this. I need a “New Game” image, a “Load Game” image, a “Delete Game” image. I arbitrarily narrowed the requirements down to the player having 3 “game slots” so they could have 3 different saved games at a time. Each “game slot” (I’m going to drop the quotes now, we’re all adults here)… each game slot will have it’s own instance of this class. For each game slot, if there is no data saved in the slot, display the “New Game” Texture on the Button. If there is saved data in the slot, show the Load Game Texture and also enable a Delete button so the user can delete their saved game. If the user clicks the delete button, I also need a way to warn the player that deleting game data is forever and they may cry if they say yes (<--- Not done in the actual demo but mandatory in your game!!).

Execution

Here are my two button images (hell yeah, beveling and outer glow! I’m gonna take these skills and FrontPage and have the most awesome geocities account in all the WWW!).
new game button load game button

And here, you can see the two different states the button could be in. The delete button is below the Load Game button and isn’t visible or clickable on the New Game button slot. Above the buttons you can see the game slot name.
new and load game buttons

Now we’ll look at the code and go through programmatically what’s going on here.

From src/com/zf/ui/buttons/loadGameButton/LoadGameButton.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package com.zf.ui.buttons.loadGameButton
{
   import com.zf.core.Assets;
   import com.zf.core.Config;
   import com.zf.core.Game;
   import com.zf.utils.ZFGameData;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Button;
   import starling.display.Sprite;
   import starling.events.Event;
   import starling.text.TextField;
   import starling.textures.Texture;
 
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();
      }
   }
 
   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 _addDeleteGameBtn():void {
      _deleteGameBtn = new Button(Assets.deleteGameBtnT);
      _deleteGameBtn.x = 130;
      _deleteGameBtn.y = 105;
      addChild(_deleteGameBtn);
      _deleteGameBtn.addEventListener(Event.TRIGGERED, _onDeleteGameBtnClicked);
   }
 
   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 _onClick(evt:Event):void {
      setCurrentGameData();
      onGameSelected.dispatch();
   }
 
   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;
   }
}
}

Now, right off the bat, there is some dumb stuff going on here. Can you spot it? When the Button needs to change it’s Texture, I have to destroy it, create a whole new Button passing in the new texture, and handle removing and readding event listeners. That’s nuts. I should just be able to update change the Button Texture and be done with it. But we can’t. However, I found this helpful post on the Starling Forums on a Multi-State Button that you might find helpful. So, having to remove listeners, destroy the button, create a new Button and add new listeners is a pain, but lets weigh this against how often a player will be deleting their saved game data, and the fact that this is not part of the update-heavy Play State where every cycle and every tick and every saved render really counts. So this works and I’ll leave it like that for now.

MapSelectIcon.as

Problem

As a player, on the Map Select screen, I want to be able to see available maps, get some map info about them when I hover, and click to select the map I want to play next

Solution

Continuing the trend of purely functional, I use a single shrunk-down screenshot of each Map the user can play as the clickable button. When they hover over the image, they get a useless description of the map. And they can click the image to select the map and begin playing. Remember again that, as a hypocrite, I am showing you really terrible UI while telling you how you should have good UI. Most likely you would have a game map or something in the screen’s background, and these individual icons would be a lot smaller. Maybe they’d represent towns on the map, or countries, or just Level 1, Level 2… Either way, displaying good information about each map is a real boon to the player. You may keep track of and display things like: previous high score, summary of enemy waves, presence of epic boss at the end of the map, modifiers existing on that map (‘raining so -10%range/damage to archer towers’), number of lives the player gets on the map, etc. I always love popovers/tooltips as a way of receiving valuable, contextual information about something I am hovering over. But that’s just me.

Execution

So the following two tiny images are loaded into the game and represent the Map 1 and Map 2 icons.
Map Select 1Map Select 2

Lets look at those in proper glorious context though…
Map Select Screen

Below when I refer to the “map data” that gets passed in, I am referring to a new file that I don’t think I’ve gone over yet. We’ll look at this new JSON file now. This is from src/assets/json/maps/mapSelectData.json.

{
   "maps": [
      {
         "id": "map1",
         "file": "assets/maps/map1.json",
         "title": "First Map!",
         "desc": "This first map is easy!"
      },
      {
         "id": "map2",
         "file": "assets/maps/map2.json",
         "title": "Second Map!",
         "desc": "This second map is also easy!"
      }
   ]
}

It contains the list of maps and their map data for the MapSelect State. The “title” and “desc” attributes can be seen above. For the majority of cases, you would have the same types of icons for this map select screen used over and over. I.e. Town icon disabled, town icon enabled, town icon completed, etc. In my demo, I happen to use screenshots of the maps which, upon looking at this file, would go great in here. An attribute for which “iconImage” to use or something. However, I didn’t do that, and instead I’m passing in the texture I want for the button from the MapSelect State class when I initialize the MapSelectIcon’s class.

Alrighty… now, on to the code!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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;
         }
      }
   }
}
}

GameOptionsPanel.as

Problem

As a player, in the Play State, I need to be able to access game options to change sound settings.

Solution

In the game, I’ve got a very dinky “Options” Button in the top right corner. When the player clicks it, the GameOptionsPanel slides into view and lets the player change Music and SFX volume. These get saved to their main SharedObject data so once the player sets the sound volume, if they load any of the 3 game slots, or creates a new game in any of those slots, the sound settings persist. I don’t think a user would want different sound volumes for different games. That’s an assumption I made that may not be entirely user-friendly. If multiple people are playing on the same computer and they play different games, they may want different sound settings. It’s a chance I’m taking that really isn’t all that costly to have individual sound volumes for each game slot, but, this is the way I set it up.

Execution

Here is the in-game options panel:
Game Options Panel

As you can see, there is a separate volume control for the music volume and the SFX volume. Currently in the demo, since I don’t have any music playing, I simply run everything on the SFX volume, but were I to add music, I would just choose for the volume param to come from the Music Volume setting and not the SFX Volume. There is also a Quit Button here that lets the player quit out to the map select state. If the player clicks Cancel, no options are updated, and if the player clicks Save then we take the values of each of those volume settings and save them.

In this options panel, I use Feathers controls, specifically the Slider control. And I also use one of their themes, AzureMobileTheme. This theme requires that I include some assets to use it, and those can be found in the project level ./assets/ folder. It contains a fonts and images folder that it uses to skin controls. Alright, let’s look at the code found in com.zf.ui.gameOptions.GameOptionsPanel.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
package com.zf.ui.gameOptions
{
   import com.greensock.TweenLite;
   import com.zf.core.Assets;
   import com.zf.core.Config;
 
   import flash.geom.Point;
 
   import feathers.controls.Slider;
   import feathers.themes.AzureMobileTheme;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Button;
   import starling.display.Image;
   import starling.display.Sprite;
   import starling.events.Event;
   import starling.text.TextField;
 
public class GameOptionsPanel extends Sprite
{
   public var onActivated:Signal;
   public var onDeactivated:Signal;
   public var onQuitGame:Signal;
 
   private var _bkgd:Image;
   private var _musicSlider:Slider;
   private var _sfxSlider:Slider;
   private var _musicTF:TextField;
   private var _sfxTF:TextField;
   private var _fontName:String = 'Wizzta';
   private var _saveBtn:Button;
   private var _cancelBtn:Button;
   private var _quitBtn:Button;
 
   private var _startPos:Point;
   private var _endPos:Point;
   private var _activateTween:TweenLite;
   private var _deactivateTween:TweenLite;
   private var _sfxVolume:Number;
   private var _musicVolume:Number;
 
   public function GameOptionsPanel(startPos:Point, endPos:Point) {
      _startPos = startPos;
      _endPos = endPos;
 
      onActivated = new Signal();
      onDeactivated = new Signal();
      onQuitGame = new Signal();
 
      addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
   }
 
   private function onAddedToStage(evt:Event):void {
      removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
      _bkgd = new Image(Assets.gameOptionsBkgdT);
      addChild(_bkgd);
 
      new AzureMobileTheme(this);
      _musicSlider = new Slider();
      _musicSlider.name = 'musicVolume';
      _musicSlider.minimum = 0;
      _musicSlider.maximum = 100;
      _musicSlider.value = Config.musicVolume * 100;
      _musicSlider.step = 1;
      _musicSlider.page = 10;
      _musicSlider.x = 160;
      _musicSlider.y = 75;
      _musicSlider.width = 200;
      addChild(_musicSlider);
 
      _sfxSlider = new Slider();
      _sfxSlider.name = 'sfxVolume';
      _sfxSlider.minimum = 0;
      _sfxSlider.maximum = 100;
      _sfxSlider.value = Config.sfxVolume * 100;
      _sfxSlider.step = 1;
      _sfxSlider.page = 10;
      _sfxSlider.x = 160;
      _sfxSlider.y = 105;
      _sfxSlider.width = 200;
      addChild(_sfxSlider);
 
      _musicTF = new TextField(50, 32, _getPercentText(Config.musicVolume));
      _musicTF.fontName = _fontName
      _musicTF.x = 365;
      _musicTF.y = 60;
      _musicTF.color = 0xFFFFFF;
      _musicTF.fontSize = 24;
      addChild(_musicTF);
 
      _sfxTF = new TextField(50, 32, _getPercentText(Config.sfxVolume));
      _sfxTF.fontName = _fontName
      _sfxTF.x = 365;
      _sfxTF.y = 90;
      _sfxTF.color = 0xFFFFFF;
      _sfxTF.fontSize = 24;
      addChild(_sfxTF);
 
      _saveBtn = new Button(Assets.optsSaveBtnT);
      _saveBtn.x = 380;
      _saveBtn.y = 235;
      addChild(_saveBtn);
 
      _cancelBtn = new Button(Assets.optsCancelBtnT);
      _cancelBtn.x = 200;
      _cancelBtn.y = 235;
      addChild(_cancelBtn);
 
      _quitBtn = new Button(Assets.optsQuitBtnT);
      _quitBtn.x = 20;
      _quitBtn.y = 235;
      addChild(_quitBtn);
 
      x = _startPos.x;
      y = _startPos.y;
      visible = false;
      touchable = false;
   }

And continuing on in com.zf.ui.gameOptions.GameOptionsPanel.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
   public function init():void {
      _musicVolume = Config.musicVolume;
      _sfxVolume = Config.sfxVolume;
      _musicSlider.value = _musicVolume * 100;
      _sfxSlider.value = _sfxVolume * 100;
   }
 
   public function activate():void {
      init();
      visible = true;
      touchable = true;
      _addListeners();
      TweenLite.to(this, 1, {x: _endPos.x, y: _endPos.y, onComplete: _activateTweenComplete});
   }
 
   public function deactivate():void {
      _removeListeners();
      TweenLite.to(this, 1, {x: _startPos.x, y: _startPos.y, onComplete: _deactivateTweenComplete});
   }
 
   public function saveOptions():void {
      Config.musicVolume = _musicVolume;
      Config.sfxVolume = _sfxVolume;
      Config.saveGameOptions();
   }
 
   private function _onSaveTriggered(evt:Event):void {
      saveOptions();
      deactivate();
   }
 
   private function _onCancelTriggered(evt:Event):void {
      deactivate();
   }
 
   private function _onQuitTriggered(evt:Event):void {
      deactivate();
      onQuitGame.dispatch();
   }
 
   private function _sliderChangeHandler(evt:Event):void {
      var slider:Slider = Slider(evt.currentTarget),
          val:Number = slider.value / 100;
 
      switch(slider.name) {
         case 'musicVolume':
            _musicVolume = val;
            _musicTF.text = _getPercentText(_musicVolume)
            break;
         case 'sfxVolume':
            _sfxVolume = val;
            _sfxTF.text = _getPercentText(_sfxVolume)
            break;
      }
   }
 
   private function _getPercentText(percent:Number):String {
      return (int(percent * 100)).toString() + '%';
   }
 
   private function _activateTweenComplete():void {
      onActivated.dispatch();
   }
 
   private function _deactivateTweenComplete():void {
      visible = false;
      touchable = false;
      onDeactivated.dispatch();
   }
 
   private function _addListeners():void {
      _musicSlider.addEventListener(Event.CHANGE, _sliderChangeHandler);
      _sfxSlider.addEventListener(Event.CHANGE, _sliderChangeHandler);
      _saveBtn.addEventListener(Event.TRIGGERED, _onSaveTriggered);
      _cancelBtn.addEventListener(Event.TRIGGERED, _onCancelTriggered);
      _quitBtn.addEventListener(Event.TRIGGERED, _onQuitTriggered);
   }
 
   private function _removeListeners():void {
      _musicSlider.removeEventListener(Event.CHANGE, _sliderChangeHandler);
      _sfxSlider.removeEventListener(Event.CHANGE, _sliderChangeHandler);
      _saveBtn.removeEventListener(Event.TRIGGERED, _onSaveTriggered);
      _cancelBtn.removeEventListener(Event.TRIGGERED, _onCancelTriggered);
      _quitBtn.removeEventListener(Event.TRIGGERED, _onQuitTriggered);
   }
 
}
}

UpgradeOption.as

Problem

As a player, on the Upgrade screen, I want to see how many upgrade points I have, the different options that I can upgrade, and I want to be able to add or remove to those points at any time. I also want a “Reset” button so if I’ve added a lot of points, I can ‘clear the board’ and start allocating points from 0.

Solution

So, I wanted to hit the core points of the problem. Currently the Feathers NumericStepper is in beta, so it’s not actually available for me to use here. I was bummed about that. So I’ll make my own in a way. So I needed art assets: a + and – button with enabled and disabled states, and I wanted something that showed off that I was “putting points into it” so I went with the following that I drew all by myself!!

Execution

In the project folder, these get drawn onto my atlas.png tilesheet, but you can find the individual images in src/assets/images/ui
Upgrade Options
In order from Left to Right: upgrade_add_disabled.png, upgrade_add_enabled.png, upgrade_rank_notselected.png, upgrade_rank_selected.png, upgrade_sub_disabled.png, upgrade_sub_enabled.png.

And here we can see them in the game screen.
Upgrade Options In Game
Available Points are displayed in the upper right with the reset button, and to the left we’ve got our actual UpgradeOptions.as component. It’s made up of a text field with the name of the upgrade, a minus button, a plus button, and 5 ‘ranks’ that can be filled up.

Tragically missing from this design, which you should implement in your game, is what each point in each rank actually does for you. This is not documented anywhere, but right now, each point you put into Tower Attack Speed decreases time by 10%, Tower Range gets you a 15% range boost for the tower, and Tower Damage gives you 20% per rank. These should be built into the UI in a helpful way so the player always knows the cost of each decision.

Now let’s look at the code from com.zf.ui.upgradeOption.UpgradeOption.as. It basically handles creating a reusable UI component where we can pass in a few params and create those three “Tower Attack Speed”, “Tower Range” and “Tower Damage components.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package com.zf.ui.upgradeOption
{
   import com.zf.core.Assets;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Button;
   import starling.display.Image;
   import starling.display.Sprite;
   import starling.events.Event;
   import starling.text.TextField;
public class UpgradeOption extends Sprite
{
   public var optionChanged:Signal;
 
   private var _id:String;
   private var _label:String;
   private var _labelTF:TextField;
   private var _enabled:Boolean;
   private var _totalRanks:int;
   private var _currentRanks:int;
   private var _bonusPerRank:Number;
   private var _plusBtn:Button;
   private var _minusBtn:Button;
   private var _ranks:Array;
   private var _onStage:Boolean = false;
 
   public function UpgradeOption(id:String, label:String, numRanks:int, bonus:Number, enabled:Boolean = false) {
      _id = id;
      _label = label;
      _totalRanks = numRanks;
      _bonusPerRank = bonus;
      _enabled = enabled;
      optionChanged = new Signal(Boolean, Object);
 
      addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
   }
 
   public function onAddedToStage(evt:Event):void {
      removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
      _onStage = true;
      _ranks = [];
 
      _labelTF = new TextField(200, 35, _label, 'Wizzta', 30, 0xFFFFFF);
      _labelTF.y = -40;
      addChild(_labelTF);
      update();
   }
 
   public function update():void {
      _updateButtons();
      _updateRanks();
   }
 
   private function _updateButtons():void {
      _addPlusButton();
      _addMinusButton();
   }
 
   private function _updateRanks():void {
      var rank:Image,
          texture:String = '';
      for(var i:int = 0; i < _totalRanks; i++) {
         if(i < _currentRanks) {
            texture = 'upgrade_rank_selected'
         } else {
            texture = 'upgrade_rank_notselected';
         }
 
         rank = new Image(Assets.ta.getTexture(texture));
         rank.x = 70 + (30 * i);
         rank.y = 0;
         addChild(rank);
 
         _ranks.push(rank);
      }
   }
 
   public function set currentRanks(i:int):void {
      _currentRanks = i;
      if(_onStage) {
         update();
      }
   }
 
   public function getOptionValue():Object {
      return {
         'id': _id,
         'label': _label,
         'totalRanks': _totalRanks,
         'currentRanks': _currentRanks,
         'bonusPerRank': _bonusPerRank
      }
   }
 
   public function disable():void {
      _enabled = false;
      update();
   }
 
   public function enable():void {
      _enabled = true;
      update();
   }

And continuing with UpgradeOption.as…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
   private function _addPlusButton():void {
      // check if the _minusBtn already exists on stage
      if(contains(_plusBtn)) {
         _plusBtn.removeFromParent(true);
      }
 
      var texture:String = 'upgrade_add_disabled',
          addListener:Boolean = false;
      if(_enabled && _currentRanks < _totalRanks) {
         texture = 'upgrade_add_enabled';
         addListener = true;
      }
 
      _plusBtn = new Button(Assets.ta.getTexture(texture));
      _plusBtn.x = 40;
      _plusBtn.y = 0;
      _plusBtn.name = 'add';
      addChild(_plusBtn);
 
      if(addListener) {
         _plusBtn.enabled = true;
         _plusBtn.addEventListener(Event.TRIGGERED, _onButtonClicked);
      } else {
         _plusBtn.enabled = false;
         _plusBtn.removeEventListener(Event.TRIGGERED, _onButtonClicked);
      }
   }
 
   private function _addMinusButton():void {
      // check if the _minusBtn already exists on stage
      if(contains(_minusBtn)) {
         _minusBtn.removeFromParent(true);
      }
      var texture:String = 'upgrade_sub_disabled',
          addListener:Boolean = false;
      if(_enabled && _currentRanks > 0) {
         texture = 'upgrade_sub_enabled';
         addListener = true;
      }
 
      _minusBtn = new Button(Assets.ta.getTexture(texture));
      _minusBtn.x = 10;
      _minusBtn.y = 0;
      _minusBtn.name = 'sub';
      addChild(_minusBtn);
 
      if(addListener) {
         _minusBtn.enabled = true;
         _minusBtn.addEventListener(Event.TRIGGERED, _onButtonClicked);
      } else {
         _minusBtn.enabled = false;
         _minusBtn.removeEventListener(Event.TRIGGERED, _onButtonClicked);
      }
   }
 
   private function _addListeners():void {
      _plusBtn.addEventListener(Event.TRIGGERED, _onButtonClicked);
      _minusBtn.addEventListener(Event.TRIGGERED, _onButtonClicked);
   }
 
   private function _removeListeners():void {
      _plusBtn.removeEventListener(Event.TRIGGERED, _onButtonClicked);
      _minusBtn.removeEventListener(Event.TRIGGERED, _onButtonClicked);
   }
 
   private function _onButtonClicked(e:Event):void {
      var addedRank:Boolean = false;
      if(Button(e.currentTarget).name == 'add') {
         _currentRanks++;
         addedRank = true;
      } else {
         _currentRanks--;
      }
 
      update();
      optionChanged.dispatch(addedRank, getOptionValue());
   }
 
   public function destroy():void {
      _labelTF.removeFromParent(true);
      _labelTF = null;
 
      for(var i:int = 0; i < _ranks.length; i++) {
         _ranks[i].removeFromParent(true);
         _ranks[i] = null;
         _ranks.splice(i, 1);
      }
 
      _plusBtn.removeFromParent(true);
      _plusBtn = null;
 
      _minusBtn.removeFromParent(true);
      _minusBtn = null;
   }
}
}
  • Line 1-54 – these next two functions _addPlusButton() and _addMinusButton() are perfect candidates for some easy refactoring at a later date. I don’t know why I didn’t do this to start with, but here we are. These are essentially the exact same functions but they just have a few differences in the texture names to use, the name of the button, and the x value. I’ll go through the first function and you can apply those to the second. Oh, and some logic on how it enables/disables itself.
  • Line 3-5 – As the comment said, check to see if _plusBtn is already on this Sprite, if so, remove it
  • Line 9-12 – if this component is enabled and the _currentRanks is less than _totalRanks (meaning: I’ve got 3 points into this component that has a total of 5 points) then I can add more points to the component so set the texture to the add_enabled image and addListener is true (because I’ll want to add the event listeners)
  • Line 14-18 – creates a new Button with the proper texture and adds it to this Sprite
  • Line 20-26 – if addListener is true, add the event listeners to the plusBtn and enable it. If not, disable it (grays out the button) and remove the event listener
  • Line 56 – wrapper function to add event listeners to both buttons
  • Line 61 – wrapper function to remove event listeners to both buttons
  • Line 66 – event handler for when both the plus and minus buttons are clicked
  • Line 68-73 – check the name of the Button, if it’s ‘add’, then increment _currentRanks and set addedRank to true, otherwise decrement _currentRanks and leave addedRank to false.
  • Line 75 – update() is called to update the UI
  • Line 76 – optionChanged Signal is dispatched with if we added a rank or not, and we get the significant private variables, wrap them in an Object and send them off with this Signal as well
  • Line 79-94 – boring standard destroy() function except for 83-87 where we loop through the _ranks images, remove them from this Sprite, remove them from the _ranks array and null them out.

All of the other work is handled by the Upgrades State which we’ll look at another time.

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

Thanks!
-Travis

Series Navigation<< AS3 Starling Tower Defense Tutorial – Part 7 – Sounds, Stats, and SavesAS3 Starling Tower Defense Tutorial – Part 9 – UI Game Components >>

Leave a Comment

Your email address will not be published.