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!
[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
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.
They show up on the road next to where an Enemy group will be entering from.
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.
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);
}
}
}
- Line 22 – create a new Signal to dispatch when this NextWaveButton is clicked
- Line 24-25 – create a new Button with the Texture passed in and add it to this Sprite
- Line 27-28 – set the pivotX and pivotY to half the width and half the height. This is Starling’s version of a “registration point” so when we assign an x,y coordinate, it will be from the center of this Sprite
- Line 30 – add the click event listener
- Line 31 – call expand() to start the in-out pulsing animation
- Line 34-36 – called by HudManager when this or any other NextWaveButton is clicked. This makes the NextWaveButton shrink to a scale of 0 and fade out, then when the tween is complete, it calls destroy to remove itself
- Line 38-41 – event handler for the TRIGGERED event listener. This function removes the listener and dispatches the onClicked Signal
- Line 44 – expand() kicks of a TweenLite.to function that means this Sprite is going to scaleX/scaleY up to 1.2 times it’s normal size (expanding) over the course of 0.5 seconds. After that 0.5 seconds when the Tween is complete, it will call contract().
- Line 48 – contract() kicks of a TweenLite.to function that means this Sprite is going to scaleX/scaleY down to 0.8 times it’s normal size (shrinking) over the course of 0.5 seconds. After that 0.5 seconds when the Tween is complete, it will call expand(). You got it… it’s an animation loop!
- Line 51 – sup destroy()… keep doing your thing, bro…
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…
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();
}
}
}
- Line 26 – hang on to a reference to the Enemy that this HealthBar is attached to
- Line 27 – set the current health
- Line 28 – set the maximum health
- Line 29 – set the width that this HealthBar should be
- Line 31 – currently our percentDmg should be 0 as we have not taken any damage. However, as I’m looking at this, I’m already taking in the currentHP param, for the sake of robustness, I should be using _percentDmg = _healthCurrent / _healthMax; Maybe I’ll have enemies that start off without full health or something… maybe that’s an ability or skill your character can unlock?
- Line 32 – set the width of the health bar
- Line 33 – initializes the _s:Shape. Since we use those every time this bar updates, let’s save a few cycles not re-instantiating them every game tick.
- Line 34 – call update() to actually draw the HealthBar
- Line 37 – this draws the health bar on the Sprite
- Line 38-40 – again, in straight AS3, you could reuse the graphics layer, but since we’re doing things Starling’s way, we have to draw on a graphics layer of a flash.display.Shape, then transfer that to an actual starling.display.Image. Anyways, if the _healthBar is already visible, remove it from this Sprite as we’re about to re-draw it.
- Line 42 – my Shape’s graphics class needs to be clear()’ed so there are no previous health bars drawn on the same area
- Line 45 – here I’m drawing a black rectangle. The lineStyle has a thickness of 1, and its color is black
- Line 46 – beginFill has a color of black and an alpha of 0
- Line 47 – drawRect draws an empty rectangle that’s _drawWidth pixels wide and 5 pixels high.
- Line 48 – endFill applies the fill to our lines
- Line 50 – so now we need to get the color of the inner filling to our healthbar. The color (and width) of this inner rectangle makes up the real nature of our health bar. The more damage the Enemy has sustained, the shorter the health bar becomes right and I have it change colors from a weird bluegreen, to yellow, to red the more damage the Enemy has taken. Set it to the default blueish color.
- Line 53 – if the Enemy has sustained between 36% to 69% damage, change to yellow
- Line 55 – if the Enemy has sustained between 1% to 35% damage, change to red
- Line 52-56 – in GemCraft Labyrinth , for example, this color is changing almost constantly so they’ve got something similar to this with a lot more steps along the way.
- Line 59-62 – now we’re doing the same thing we did with the black container rectangle, but we’re just doing it a little smaller
- Line 59 – lineStyle(0, fillCol, 1, true) I’m setting my line style to have 0 thickness. I want this to be the smallest lines Flash can make since I’m inside this other rectangle with a height of 5 pixels. It’s a very small space. Set the color of the line to the fillCol value, set the alpha to 1 (which is the default) and then set pixelHinting to true so the inner rectangle looks as best as possible given it’s tiny nature on the screen.
- Line 60 – set the fill color to fillCol
- Line 61 – drawRect(1, 1, _healthWidth – 2, 3) – draw the rectangle starting 1 pixel from the left, and 1 pixel from the top of the other rectangle (thus making the other rectangle a 1pixel border around my healthbar). The width of the healthbar is _healthWidth – 2. If the HealthBar’s width was 10 and the Enemy was at full health, _healthWidth would be 10. Since we’ve already moved this inner rectangle 1 pixel in to the right, the healthbar would draw essentially all the way to pixel 11. So we subtract 2 from the value so that at full health, the width of the inner bar would be 8. And with the 1 pixel offset, this gives us 1 full pixel on the right border of the healthbar. Set this rectangle’s height to be 3 for the same reason. Max height of 5, we’ve already started this rectangle 1 pixel from the top and we need 1 pixel at the bottom as a border there too, so the height of this inner rectangle will be 3.
- Line 64 – create a new flash.display.BitmapData object that has a width & height the size of the _s:Shape graphic, then I set transparency to true and the fillColor is set to 0 so this will have proper transparency.
- Line 65 – draws the _s:Shape into the BitmapData
- Line 67 – Starling cannot use Flash display classes. The two are just not compatible in almost every way. Now, Starling doesn’t have any way to draw vector graphics like flash does, which is a huge drawback, but here’s a way around it. We’ve created the _s:Shape and drew what we needed to draw, we converted it into a BitmapData Object. Here is where the two languages merge. I can create a starling.display.Image class using a Bitmap. This can be an actual jpg/png file passed in, or I can create a new flash.display.Bitmap Object and pass that in. Image has a static function on the class called fromBitmap() and it takes a Bitmap. WOOT! So I just take my BitmapData healthbar, convert it to a new Bitmap() by passing in the _bmd:BitmapData, pixelSnapping stays at the default “auto” and smoothing is changed to true… again, maybe a bit excessive, but I’m trying to get this tiny graphic that’s been enlarged already to maintain its boyish good looks and charm.
- Line 68 – make the new health bar image not touchable
- Line 69 – set the x of the _healthBar to be half of the width of the shape. This essentially helps center the _healthBar over the Enemy.
- Line 70 – add our sweetass new healthbar to the Sprite man!
- Line 73 – takeDamage() is the public class that Enemy calls when it gets hit with a Bullet and has taken damage. It passes that damage Number into takeDamage() and…
- Line 74 – update the width of our health bar by dividing the amount of damage sustained by the max health we could have. This should give us a fractional total (ex. maxHealth is 10, dmg is 2. 2/10 = 0.2.) which we multiply by our total _drawWidth. So if the total _drawWidth was 20 and we’ve just taken 20% damage, 20 * 0.2 = 4. So we need to remove 4 from our _healthWidth value. Hope that made sense!
- Line 75 – find the total percentage of damage we’ve sustained by dividing _healthWidth by _drawWidth. Say _healthWidth is 5 (pixels) and our total _drawWidth is 10 (pixels) then we’ve sustained 0.5 (50%) damage.
- Line 76 – after the maths, update the healthbar visuals
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!”
Let’s look at the code from com.zf.ui.text.ZFTextField.as now:
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;
}
}
}
- Line 26 – jump right in to creating a new Shape. This will end up being the _bkgdShape.
- Line 27 – lineStyle is going to have a thickness of 2. The default color is 0 (black)
- Line 28 – fill the rounded rectangle with white (0xFFFFFF)
- Line 29 – draw a rounded rectangle, x/y is 0,0, width of 80, height of 26, and the elipseWidth and elipseHeight are both 8. Setting that 8 to a higher value results in a more pill-shaped rounded rectangle. Setting it lower towards 0 results in sharper, less rounded rectangle corners.
- Line 30 – fill in the shape
- Line 32 – create a new BitmapData Object the width and height of the Shape, with transparency to true and the fill color to 0
- Line 33 – draw the Shape into the BitmapData Object
- Line 35 – create a new Image from the new Bitmap created from _bmd with smoothing set to true
- Line 36 – set the alpha to .4 so the player can see the background still, but the white background and the white text in the TextField don’t wash each other out. This is probably a bad idea. You probably shouldn’t set your backgrounds to white when you have white text. Learn from this cautionary tale of tragic game design!
- Line 37-40 – set the x & y values, set touchable to false, then add it to the Sprite
- Line 42 – create a new Image from the texture passed in via the iconName param. The image is already in the TextureAtlas so we just need to get the Texture so we can pass it to the Image.
- Line 43-46 – set the x & y values, set touchable to false, then add it to the Sprite
- Line 48 – create a new starling.text.TextField with a width of 80, height of 32 and the starting text set to startValue
- Line 49-55 – set the TextField’s font to the font name passed in via the fontName param. Set the x & y values, set the font color to white and the font size to 20, set touchable to false, then add the TextField to the Sprite
- Line 57 – create a new componentIsInvalid Signal that passes a ZFTextField class as data when it dispatches. componentIsInvalid is what will let the Managing class know that this TextField has changed and this component needs to be updated.
- Line 59 – set touchable to false on this component. It is purely visual. No touchy!
- Line 63 – when update() is called, simply pass whatever is in _textVal into the TextField’s text property. Since this component specifically tells its Manager when it needs to be updated, we can set our update() function up like this. update() only gets called when the component wants it to be. Otherwise, if we were re-setting that _tf.text value to _textVal on every single game tick, 60 times a second, that would be ridiculous. FUNNY STORY… as I typed that, I went back and looked at my HudManager to see how it handles this Signal. It adds the ZFTextField that dispatched the Signal to an _invalidComponents array, then when HudManager updates, it loops through any _invalidComponents and calls update. Guess what… after calling update() on each _invalidComponent, I was never removing the freshly updated component from the array. So… my _invalidComponents array would just keep growing and growing, and these ZFTextField components were literally doing exactly the same thing I just said was ridiculous, and even moreso because since I was never removing them, they were trying to update every time they showed up in the array. Ridiculous! That has been fixed now.
- Line 67-70 – other classes can call this public setter to set the text of ZFTextField’s TextField. Update the _textVal with whatever value was passed in, then dispatch the compoentIsInvalid Signal passing this
- Line 72 – public getter, lets other classes get whatever value is in _textVal
- Line 75-89 – destroy this component by removing any listeners from the Signal, remove the TextField, icon, and background shape, then null out the _textVal
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.
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);
}
}
}
- Line 24-25 – get the tower data object from the data param and set our data’s level to 0. Setting that to 0 is just for the Tower Info stuff… so if you hover over this Tower, the info area will show this is a level 0 Tower with all of its stats.
- Line 28-29 – create the actual Tower image from the tower data and add the _icon to the Sprite
- Line 31-32 – create two new Signals called onHover and onClick so we can let the Manager know when the player is hovering over the image, or has clicked the image.
- Line 34 – add a touch event listener to this Sprite so we’ll know when the player is hovering or clicking on this image
- Line 45 – if the player actually clicks on the icon, I need to dispatch the onClick Signal with the tower _data, but also with the location of this touch. Remember the TowerManager adds a mouse move listener and keeps that tower icon connected to the mouse, so we need to get the actual Stage values of x,y where the player clicked. I call Sprite’s localToGlobal function and pass it the touch event’s location. That will translate the local coordinates (maybe the player clicked at (15, 10) on this specific Sprite, if we didn’t call localToGlobal, the TowerManager would create the Tower image at (15,10) on the actual Stage way up in the top left corner of the screen. So calling localToGlobal takes that (15,10) and converts it to wherever the player clicked but referencing the Stage. So that (15,10) would become (542, 84) or something. Anyways, dispatch that stage-based Point so TowerManager can do it’s thing.
- Line 49 – if the player is hovering over the icon, dispatch the onHover Signal so we can update the Tower Info area
- Line 55 – destroy this image by removing the touch event listener, removing listeners from the Signals, remove the icon and remove this Sprite.
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.
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;
}
}
}
- Line 16-19 - these constants set up exactly how fast the WaveTileBar should update its WaveTiles and how far to move each tile when it updates
- Line 16 - UPDATE_DELAY - currently at 60 FPS, delay how many frames before updating tiles so 15 means update tiles four times a second
- Line 17 - TILE_X_MOVE_PER_TICK - each time we update them, move them half a pixel
- Line 28 - UPDATE_DELAY_HURRIED - when the player clicks a tile, we update the tiles every 4 ticks
- Line 19 - TILE_X_MOVE_PER_TICK_HURRIED - when the player clicks a tile, the tiles move 5pixels each tick
- Line 24 - _waveXBuffer of 10 means there's 10 pixels between each tile.
- Line 33 - take the enemyWaveDate Object and hold on to it
- Line 40 - create a new Signal to be dispatched if a player clicks on a WaveTile
- Line 43-45 - create the background Image, make it non-touchable, and add it to this Sprite
- Line 52-55 - _setDelayToDefault sets _updateDelay and _tileXMovePerTick to their default constant values
- Line 57-60 - likewise, _setDelayToHurried() gets called if a player clicks a tile, and we set the _updateDelay and _tileXMovePerTick to the "hurried" constant values
- Line 63 - when update is called, if there are WaveTiles left in _tiles, and we have waited the proper _updateDelay, then we can update the tiles
- Line 66 - loop through all available _tiles and subtract _tileXMovePerTick from each tile's x value. This moves all the tiles to the left at the same pace.
- Line 69 - after updating all the tiles, if the tile closest to the left side of the screen (_tiles[0]) has an x value that's less than or equal to 0, meaning it's touched the left side of my WaveTileBar background, then...
- Line 71 - if the id of _tiles[0] is the same as the _lastTouchedTileId, meaning, the player touched this tile to speed up all the tiles, then...
- Line 73-74 - set the _lastTouchedTileId to an empty string and set my delays and x tick values back to defaults
- Line 77 - dispatch our waveTileTouched Signal sending out the wave id as the data
- Line 78 - remove our _tile[0] WaveTile
- Line 86-90 - remove _tile[0] from this Sprite, let tile[0] run its destroy() function to clean itself up, then call _tiles.shift() which takes that 0th array element and just bumps it right out of the array.
- Line 93 - creates all the WaveTile tiles
- Line 96 - loop through the number of waves and create a WaveTile for each, passing in the waveTiles data to the WaveTile
- Line 97-100 - add listeners for when the WaveTile dispatches its onClick and onHover Signals. Set the x value of the WaveTile to _waveXCt, and add the tile to this Sprite
- Line 102 - push the tile onto the _tiles array so we can hang on to it
- Line 104 - update our _waveXCt var by adding to it the width of the tile + our _waveXBuffer. This is what gives each tile an x value that's evenly spaced down the line.
- Line 109-110 - handles when a WaveTile is touched. Set _lastTouchedTileId to the tileId passed in and set our delays and x tick values to the 'hurried' values
- Line 114 - if a wave tile is hovered over, I had wanted to display some wave data about the wave, but I didn't. However, the mechanism is still here whereby I can easily do such a thing
- Line 117 - destroy loops through any remaining tiles and removes event listeners and removes them from the Sprite
WaveTile.as
Now we'll look at the individual 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.
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();
}
}
}
- Line 27-28 - create the wave tile image from the data passed in and add it to this Sprite
- Line 30-31 - set up the Signals
- Line 33 - add a touch event listener to this Sprite
- Line 36-50 - when the WaveTile is touched, dispatch the onClick Signal, if it is just hovered over, dispatch the onHover Signal
- Line 52-66 - simple getters exposing private wave data to other classes
- Line 68 - destroy the WaveTile by removing its touch event listener, removing the WaveTile image, and removing listeners from its Signals
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
0 Comments