AS3 Starling Tower Defense Tutorial – Part 9 – UI Game Components

In my previous tutorial on UI Menu Components I went over several UI components that mostly were found outside of the main Play State. The GameOptionsPanel is used in the Play State, but by “UI Game Components” I mean more the components that make up the HUD, the gold and HP counters, enemy healthbars that change when the enemy gets hit, those sorts of things. Last post was awful wordy, so this time we’re just going to get straight to more UI components!

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

NextWaveButton.as

Problem

As a player, in the Play State, at the beginning of a new Map I need to know where the badguys are going to be coming from (where they enter the map).

Solution

I need icons to represent where the enemies will enter the map and which direction they’re headed. It needs to stand out a bit from the square-ish nature of the tile map, so maybe some circles. And it should probably be animated or have some way of jumping out at the player so they know they can click the icons to start the game.

Execution

I’ve added these little green circles to my tilesheet.

Next Wave Buttons

They show up on the road next to where an Enemy group will be entering from.

Next Wave Buttons InGame

We need to make sure that the we grab the right texture so the arrow points in the correct direction, but other than that, this button is pretty simple. Oh, it also uses TweenLite to scale bigger and smaller so it grabs the player’s attention better on the screen.

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
package com.zf.ui.buttons.nextWaveButton
{
   import com.zf.core.Config;
   import com.zf.core.Game;
 
   import com.greensock.TweenLite;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Button;
   import starling.display.Sprite;
   import starling.events.Event;
   import starling.textures.Texture;
 
public class NextWaveButton extends Sprite
{
   public var onClicked:Signal;
 
   private var _btn:Button;
 
   public function NextWaveButton(btnT:Texture) {
      onClicked = new Signal();
 
      _btn = new Button(btnT);
      addChild(_btn);
 
      pivotX = _btn.width >> 1;
      pivotY = _btn.height >> 1;
 
      _btn.addEventListener(Event.TRIGGERED, _onBtnTriggered);
      expand();
   }
 
   public function fadeOut():void {
      TweenLite.to(this, .5, {alpha: 0, scaleX: 0, scaleY: 0, onComplete: destroy});
   }
 
   private function _onBtnTriggered(evt:Event):void {
      _btn.removeEventListener(Event.TRIGGERED, _onBtnTriggered);
      onClicked.dispatch();
   }
 
   private function expand():void {
      TweenLite.to(this, 0.5, {scaleX: 1.2, scaleY: 1.2, onComplete: contract});
   }
 
   private function contract():void {
      TweenLite.to(this, 0.5, {scaleX: 0.8, scaleY: 0.8, onComplete: expand});
   }
 
   public function destroy():void {
      _btn.removeFromParent(true);
      _btn = null;
 
      onClicked.removeAll();
 
      removeFromParent(true);
   }
}
}

HealthBar.as

Problem

As a player, in the Play State, I need to know how much health an Enemy has.

Solution

Let’s draw a health bar on the Enemy class. The Enemy class knows when it gets hit, it can manage updating the HealthBar.

Execution

This is a completely programmatically drawn health bar above the Enemy so there are no pretty pictures other than the HealthBar in action…

HealthBar

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
package com.zf.ui.healthBar
{
   import com.zf.objects.enemy.Enemy;
 
   import flash.display.Bitmap;
   import flash.display.BitmapData;
   import flash.display.Shape;
 
   import starling.display.Image;
   import starling.display.Sprite;
 
public class HealthBar extends Sprite
{
   private var _healthBar:Image;
   private var _healthCurrent:int;
   private var _healthMax:int;
   private var _drawWidth:int;
   private var _enemy:Enemy;
   private var _healthWidth;
   private var _percentDmg:Number;
   private var _s:Shape;
   private var _bmd:BitmapData;
 
   public function HealthBar(parentEnemy:Enemy, currentHP:int, maxHP:int, drawWidth:int = 20) {
      _enemy = parentEnemy;
      _healthCurrent = currentHP;
      _healthMax = maxHP;
      _drawWidth = drawWidth;
 
      _percentDmg = 0;
      _healthWidth = _drawWidth;
 
      _s = new Shape();
      update();
   }
 
   public function update():void {
      if(contains(_healthBar)) {
         _healthBar.removeFromParent(true);
      }
 
      _s.graphics.clear();
 
      // draw container
      _s.graphics.lineStyle(1, 0);
      _s.graphics.beginFill(0, 0);
      _s.graphics.drawRect(0, 0, _drawWidth, 5);
      _s.graphics.endFill();
 
      var fillCol:uint = 0x009999;
 
      if(_percentDmg > .35 && _percentDmg <= .69) {
         fillCol = 0xE3EF24;
      } else if(_percentDmg > 0 && _percentDmg <= .34) {
         fillCol = 0xFF0000;
      }
 
      // draw current health
      _s.graphics.lineStyle(0, fillCol, 1, true);
      _s.graphics.beginFill(fillCol);
      _s.graphics.drawRect(1, 1, _healthWidth - 2, 3);
      _s.graphics.endFill();
 
      _bmd = new BitmapData(_s.width, _s.height, true, 0);
      _bmd.draw(_s);
 
      _healthBar = Image.fromBitmap(new Bitmap(_bmd, "auto", true));
      _healthBar.touchable = false;
      _healthBar.x = _s.width >> 1;
      addChild(_healthBar);
   }
 
   public function takeDamage(dmg:Number):void {
      _healthWidth -= _drawWidth * (dmg / _healthMax);
      _percentDmg = _healthWidth / _drawWidth;
      update();
   }
}
}

ZFTextField.as

Problem

As a player, in the Play State, I need to know how many HitPoints I have left, how much Gold I have, and the current wave / total waves that are on this map.

Solution

So, as a Developer, these all sound like the same thing when abstracted out, “I need to display some value to the user, it will be similar to a few other values, # of HP or # of gold pieces or # of current waves / # total waves are all just integers.” So let’s make a reusable class that combines a TextField as the way to display something, but let’s at least try to make it slightly visual. So I want an icon next to each TextField letting the player know what this specific number represents. Also, this TextField isn’t really going to need to be updated often. If you think on the scale of 60FPS, 60 updates a second, the player will be quite alright waiting at least 20 of those 60 updates to know they got one more gold coin. So I want to create a way to let whatever is Managing this class know that this class does not need to be updated right now.

Execution

Here are 3 ZFTextFields in action. As we’ll see in the code, there are 3 visual components to each of these: an icon, a background shape, and a text field (the white numbers in the images). So I found some probably totally free artwork online to use as my icons. I found a coin to represent my Gold, a heart to represent health, and yeah, that’s a wave… of water… to represent Enemy waves. I know… I know… “don’t get too artsy-fartsy on us now dude… that almost looks half-decent and like you put some thought into it!”

ZFTextField

Let’s look at the code from com.zf.ui.text.ZFTextField.as now:

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
package com.zf.ui.text
{
   import com.zf.core.Assets;
 
   import flash.display.Bitmap;
   import flash.display.BitmapData;
   import flash.display.Shape;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Image;
   import starling.display.Sprite;
   import starling.text.TextField;
 
public class ZFTextField extends Sprite
{
   public var componentIsInvalid:Signal;
 
   private var _tf:TextField;
   private var _icon:Image;
   private var _textVal:String;
   private var _bkgdShape:Image;
 
   public function ZFTextField(iconName:String, fontName:String, startValue:String = '')  
   {
      var _s:Shape = new Shape();
      _s.graphics.lineStyle(2);
      _s.graphics.beginFill(0xFFFFFF);
      _s.graphics.drawRoundRect(0, 0, 80, 26, 8, 8)
      _s.graphics.endFill();
 
       var _bmd:BitmapData = new BitmapData(_s.width, _s.height, true, 0);
      _bmd.draw(_s);
 
      _bkgdShape = Image.fromBitmap(new Bitmap(_bmd, "auto", true));
      _bkgdShape.alpha = .4;
      _bkgdShape.x = 20;
      _bkgdShape.y = 5;
      _bkgdShape.touchable = false;
      addChild(_bkgdShape);
 
      _icon = new Image(Assets.ta.getTexture(iconName));
      _icon.x = 0;
      _icon.y = 2;
      _icon.touchable = false;
      addChild(_icon);
 
      _tf = new TextField(80, 32, startValue);
      _tf.fontName = fontName
      _tf.x = 30;
      _tf.y = 2;
      _tf.color = 0xFFFFFF;
      _tf.fontSize = 20;
      _tf.touchable = false;
      addChild(_tf);
 
      componentIsInvalid = new Signal(ZFTextField);
 
      touchable = false;
   }
 
   public function update():void {
      _tf.text = _textVal;
   }
 
   public function set text(val:String):void {
      _textVal = val;
      componentIsInvalid.dispatch(this);
   }
 
   public function get text():String {
      return _textVal;
   }
 
   public function destroy():void {
      componentIsInvalid.removeAll();
      componentIsInvalid = null;
 
      _tf.removeFromParent(true);
      _tf = null;
 
      _icon.removeFromParent(true);
      _icon = null;
 
      _bkgdShape.removeFromParent(true);
      _bkgdShape = null;
 
      _textVal = null;
   }
}
}

TowerSelectIcon.as

Problem

As a player, in the Play State, I need some visual representation of the Towers I will be using, and I want to be able to click on them to “create a tower”.

Solution

Since my Towers are just static, boring, box images, so are the TowerSelectIcon icons. In fact, it’s the exact same image as the tower! What economy of assets! …it’s sort of like… “economy of language”… but with art… assets. Oh well…

Execution

Very boring. There they are.
Tower 1 Tower 2

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
package com.zf.ui.towerSelect
{
   import com.zf.core.Assets;
 
   import flash.geom.Point;
 
   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;
 
public class TowerSelectIcon extends Sprite
{
   public var onHover:Signal;
   public var onClick:Signal;
 
   private var _icon:Image;
   private var _data:Object;
 
   public function TowerSelectIcon(data:Object) {
      _data = data;
      _data.level = 0;
 
      // currently just a static image
      _icon = new Image(Assets.ta.getTexture(_data.imageName));
      addChild(_icon);
 
      onHover = new Signal(Object);
      onClick = new Signal(Object, Point);
 
      addEventListener(TouchEvent.TOUCH, _onTouch);
   }
 
   private function _onTouch(evt:TouchEvent):void {
      var touch:Touch = evt.getTouch(this);
      if(touch)
      {
         switch(touch.phase) {
            case TouchPhase.BEGAN:
               // using Sprite's localToGlobal function to take the x/y clicked on locally
               // and convert it to stage x/y values
               onClick.dispatch(_data, localToGlobal(touch.getLocation(this)));
               break;
 
            case TouchPhase.HOVER:
               onHover.dispatch(_data);
               break;
         }
      }
   }
 
   public function destroy():void {
      removeEventListener(TouchEvent.TOUCH, _onTouch);
 
      onHover.removeAll();
      onClick.removeAll();
 
      _icon.removeFromParent(true);
      _icon = null;
 
      removeFromParent(true);
   }
}
}

WaveTileBar.as

Problem

As a player, in the Play State, I want to be able to see visually how many waves I have remaining. I want to see how long I have until the next wave, and I want to be able to click on a wave to speed it up so it spawns faster.

Solution

Taking a page from classic TD genre styles, I’ll create an image for each wave. They’ll be aligned on the bottom of the screen, slowly moving towards the left. When they reach the left of the screen, the wave tile will “break” and the enemies will come charging onto the Map.

Execution

This is the wave tile background bar image. Onto this background image I’ll place all of my individual WaveTiles, evenly spaced apart, and then I can move them each tick. We’ll look at the individual WaveTile class next.
WaveTileBkgd

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
123
124
125
126
127
128
package com.zf.ui.waveTile
{
   import com.zf.core.Assets;
   import com.zf.core.Config;
 
   import flash.geom.Point;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Image;
   import starling.display.Sprite;
public class WaveTileBar extends Sprite
{
   public var waveTileTouched:Signal;
 
   private const UPDATE_DELAY:int = 15;
   private const TILE_X_MOVE_PER_TICK:Number = 0.5;
   private const UPDATE_DELAY_HURRIED:int = 4;
   private const TILE_X_MOVE_PER_TICK_HURRIED:Number = 5;
 
   private var _tiles:Array;
   private var _data:Object;
   private var _waveXCt:int;
   private var _waveXBuffer:int = 10;
   private var _updateCt:int;
   private var _waveTileBarBkgd:Image;
   private var _barSpeed:int;
   private var _lastTouchedTileId:String;
   private var _updateDelay:int;
   private var _tileXMovePerTick:Number;
 
   public function WaveTileBar(data:Object) {
      _data = data;
      _tiles = [];
 
      _waveXCt = 0;
      _updateCt = 0;
      _barSpeed = 0;
 
      waveTileTouched = new Signal(String);
 
      // add background
      _waveTileBarBkgd = new Image(Assets.waveTileBkgdT);
      _waveTileBarBkgd.touchable = false;
      addChild(_waveTileBarBkgd);
 
      _setDelayToDefault();
 
      _createWaveTiles();
   }
 
   private function _setDelayToDefault():void {
      _updateDelay = UPDATE_DELAY;
      _tileXMovePerTick = TILE_X_MOVE_PER_TICK;
   }
 
   private function _setDelayToHurried():void {
      _updateDelay = UPDATE_DELAY_HURRIED;
      _tileXMovePerTick = TILE_X_MOVE_PER_TICK_HURRIED;
   }
 
   public function update():void {
      if(_tiles.length > 0 && _updateCt % _updateDelay == 0) {
         var len:int = _tiles.length;
         for(var i:int = 0; i < len; i++) {
            _tiles[i].x -= _tileXMovePerTick;
         }
 
         if(_tiles[0].x <= 0) {
            // If user touched this tile to speed all tiles up
            if(_tiles[0].id == _lastTouchedTileId) {
               // reset vars so bar doesnt go faster anymore
               _lastTouchedTileId = '';
               _setDelayToDefault();
            }
 
            waveTileTouched.dispatch(_tiles[0].id);
            _removeFirstWaveTile();
         }
      }
 
      _updateCt++;
   }
 
   private function _removeFirstWaveTile():void {
      _tiles[0].removeFromParent(true);
      // have the tile run it's destroy function
      _tiles[0].destroy();
      // remove it from the array
      _tiles.shift();
   }
 
   private function _createWaveTiles():void {
      var len:int = _data.numWaves;
      for(var i:int = 0; i < len; i++) {
         var tile:WaveTile = new WaveTile(_data.waveTiles[i]);
         tile.onClick.add(onTileTouched);
         tile.onHover.add(onTileHovered);
         tile.x = _waveXCt;
         addChild(tile);
 
         _tiles.push(tile);
 
         _waveXCt += tile.width + _waveXBuffer;
      }
   }
 
   public function onTileTouched(tileId:String):void {
      _lastTouchedTileId = tileId;
      _setDelayToHurried();
   }
 
   public function onTileHovered(tileId:String, tilePt:Point):void {
      //Config.log('WaveTileBar', 'onTileHovered', tileId + " hovered");
   }
 
   public function destroy():void {
      var len:int = _tiles.length;
      for(var i:int = 0; i < len; i++) {
         _tiles[i].onTouch.remove();
         _tiles[i].onHover.remove();
         _tiles[i].removeFromParent(true);
         _tiles[i].destroy();
      }
      _tiles = null;
   }
}
}

WaveTile.as

Now we’ll look at the individual Wave Tiles.

Wave Tiles

I basically just made a rounded rectangle in Photoshop, copied the layer 5 times, changed the color of each, and added an ElderScrolls rune sideways on the image. Oh and all the beveling and stuff as well. Let’s wrap this up by checking out the Wave Tile 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
70
71
72
73
74
75
package com.zf.ui.waveTile
{
   import com.zf.core.Assets;
   import com.zf.core.Config;
 
   import flash.geom.Point;
 
   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;
 
public class WaveTile extends Sprite
{
   public var onClick:Signal;
   public var onHover:Signal;
 
   private var _tileImg:Image;
   private var _data:Object;
 
   public function WaveTile(waveTileData:Object) {
      _data = waveTileData;
 
      _tileImg = new Image(Assets.ta.getTexture(_data.image));
      addChild(_tileImg);
 
      onHover = new Signal(String, Point);
      onClick = new Signal(String);
 
      addEventListener(TouchEvent.TOUCH, _onTileImageClick);
   }
 
   private function _onTileImageClick(evt:TouchEvent):void {
      var touch:Touch = evt.getTouch(this);
      if(touch)
      {
         switch(touch.phase) {
            case TouchPhase.BEGAN:
               onClick.dispatch(id);
               break;
 
            case TouchPhase.HOVER:
               onHover.dispatch(id, touch.getLocation(this));
               break;
         }
      }
   }
 
   public function get id():String {
      return _data.id;
   }
 
   public function get image():String {
      return _data.image;
   }
 
   public function get title():String {
      return _data.title;
   }
 
   public function get desc():String {
      return _data.desc;
   }
 
   public function destroy():void {
      removeEventListener(TouchEvent.TOUCH, _onTileImageClick);
      removeChild(_tileImg);
      onHover.removeAll();
      onClick.removeAll();
   }
}
}

That’s it for now! 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

I’ve got a great start on the next tutorial post on Bullets, Collisions, and Managers. Make sure you check back next Tuesday for another post!
Thanks!
-Travis

Series Navigation<< AS3 Starling Tower Defense Tutorial – Part 8 – UI Menu ComponentsAS3 Starling Tower Defense Tutorial – Part 10 – Bullets, Collisions, and Managers >>
FacebookTwitterGoogle+Share

Leave a Comment

Your email address will not be published.

1 Trackback

  1. Game UI Analysis | Caitlin Goodale (Pingback)