AS3 Starling Tower Defense Tutorial – Part 7 – Sounds, Stats, and Saves

We’ve knocked out the Big 2 posts already. Enemies and Towers have just been covered. Now we’re going to lump together Sounds and sound management, specifically with TreeFortressSoundAS. Then we’re going to take a look back at stats or “Totals” as the code refers to them (sorry for the misleading title, but ‘stats’ started with an ‘s’ and fit the pattern, but it’s really “Totals”). And finally we’re going to tie Totals up with a bow and make it persist wiiiith… our SharedObject Manager. I wrote this sucker myself, so you can blame me* when it decides to wipe three weeks of your hard-won stats.

*Note, please don’t blame me if this code does something unexpected!

Before we 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

Onward!

Sounds

Now that I look at it, I really don’t do a whole lot at all with sounds in this demo. It’s all very basic but I’ll walk through it nonetheless because a Flash game (or any kind of game) without sound is a very bad idea. Let’s revisit the whole process, starting with my initial sounds.json file.

Really quick though, in case my point hasn’t been made enough times in previous posts regarding loading files manually and not embedding them in your project, let’s take this very short list of sound files below and do some thought experiments. Imagine you’ve got a game completed and it’s posted up at Kongregate or ArmorGames. You chose to go the route of having all of your sound files embedded in your project via [Embed(source="path/to/my.mp3")], but now you want to change the path for an old var and point it to embed a new sound file. What happens when you want to do that? Well, Mr. CodeMonkey, you fire up your IDE and go change it, recompile, export your release build, push the new SWF up to wherever your game is hosted, and when the cache expires in the clients’ browsers, each individual player starts getting the new sounds whenever the new SWF loads.

If you had gone the route of loading sound files externally from a json file like the one below and you wanted to change paths, you or anyone on your team could open a text editor, change the path, upload the json file and the new mp3 file, and every cached swf is already loading that json file every time. It doesn’t care what those paths look like. So since its cached, it loads up super fast for the player, reads in that file and loads in the new sound with the player never knowing anything changed.

src/assets/json/sounds.json

{
   "files": [
      {
         "id": "ding1",
         "file": "assets/sounds/ding1.mp3"
      },
      {
         "id": "escape1",
         "file": "assets/sounds/escape1.mp3"
      },
      {
         "id": "shot1",
         "file": "assets/sounds/shot1.mp3"
      },
      {
         "id": "shot2",
         "file": "assets/sounds/shot2.mp3"
      },
      {
         "id": "banish1",
         "file": "assets/sounds/banish1.mp3"
      }
   ]
}

So let’s say players keep complaining about the shot1 sound. So we want to swap that out with a shot3.mp3 file that we just recorded or found. All we have to do is change that path
“file”: “assets/sounds/shot3.mp3″ and whatever was supposed to play sound “shot1″ will still play a sound, it’ll just be coming from shot3.mp3. No need to go into any class files and change any IDs anywhere or update any code!

As an interesting rabbit to chase that I thought about, I have not played around with this yet, but this seems like a really smart idea. So if you added some sort of “chance” or “frequency” param in there…

{
   "id": "shot1a",
   "file": "assets/sounds/shot1a.mp3",
   "freq": 0.25
},
{
   "id": "shot1b",
   "file": "assets/sounds/shot1b.mp3",
   "freq": 0.25
},
{
   "id": "shot1c",
   "file": "assets/sounds/shot1c.mp3",
   "freq": 0.50
},

Remember back in Tower.as the _playIfStateExists() function could generate a random number between 0 and 1. If you did some math when you read this sound metadata in, you could convert those frequencies to fall between 0 and 1. So, shot1c would have a 50% shot to get played as the sound this time, and shot1a and shot1b would each have a 25% chance. So each time your Tower fires, there’s a chance you’d play a slightly different sound. That’d help keep your players from getting bored with your sounds? Just a thought as I’m writing this.

So the sounds.json file gets read in and Assets.as calls each of the “file” params, loading each path/file. When the file loads, Assets calls

   Game.soundMgr.addSound(cbo.name, cbo.data);

cbo.name is the “id” param from sounds.json, and cbo.data is the actual MP3 Sound. SoundAS stores the sound object in a Dictionary and also saves it by id in an Object, so, you can call the sound by it’s String ID. I’m not going to post any code of theirs here because I’d hate to let that get stale, if they updated it and I’m sitting here with a code example from 2 versions ago. Go check it out for yourself at their git repo or check out the files that come with the project in src/treefortress/sound/.

So, we call addSound(), *magic* happens, and later on when we actually want to play the sound we call:

   Game.soundMgr.playFx(_soundData[state], Config.sfxVolume);

And remember, _soundData[state] keeps the ID for the sound I want to play, and Config.sfxVolume holds the current volume for sound effects. Currently, SoundAS has a few different ways to play your sounds. play() is the main function that actually plays a sound file. play() has a lot of params you can pass in. playFx() is one of their “convenience functions” which handles passing in a few params for you. It seemed much quicker and exactly what I wanted for this sound effect. SoundAS allows for the same sound to “overlap” or not. If you had a character in your game that when you clicked on them, they said “What do you want?” (probably in a funny voice I bet) and you keep clicking on him and one instance of the sound starts and tries to play, then you click again and it cuts off the sound (or worse, layers the same file on top of your first sound clip) and you keep doing this, you’ll never hear with the character says. But you could set those sounds to play and not allow multiple instances of the same sound to be played at the same time. Pretty smart.

playFx(), however, does allow for playing the same sound clip multiple times, as you would expect of a sound effect. Every time a tower fires I want that shot sound to fire no matter how many other towers are firing. And that’s why I’m using it.

So, sadly, that’s really all I’m using sounds for in this demo. I don’t have any music playing in the demo or really anything interesting happening sound-wise. But check out SoundAS. I’ve been working with it in my personal game that I’m working on and it’s been a really great experience. Definitely up there with Greensock’s Tween/Loading libs on how fast I was able to get started with someone else’s code library and make it do exactly what I wanted it to do and not cause me problems. Mo’ libs, Mo’ problems? Not with SoundAS, man!

Anyways… let’s move on to part two. Totals aka ‘stats’ aka ‘totals’ or whatever.

Totals.as

com.zf.core.Totals.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
package com.zf.core
{
   import com.zf.utils.Utils;
 
public class Totals
{
   public var totalDamage:Number;
   public var towersPlaced:int;
   public var enemiesSpawned:int;
   public var enemiesBanished:int;
   public var enemiesEscaped:int;
   public var bulletsFired:int;
   public var mapsCompleted:int;
   public var mapsWon:int;
 
   private var _str:String;
   private var _props:Array = ['totalDamage', 'towersPlaced', 'enemiesSpawned', 'enemiesBanished', 'enemiesEscaped', 'bulletsFired'];
 
   public function Totals() {
      reset();
   }
 
   public function reset():void {
      totalDamage 	= 0;
      towersPlaced 	= 0;
      enemiesSpawned 	= 0;
      enemiesBanished   = 0;
      enemiesEscaped 	= 0;
      bulletsFired 	= 0;
      mapsCompleted	= 0;
 
      // if player wins, update this to 1
      mapsWon           = 0;
 
      _str = '';
   }
 
   public function toString():String {
      return 'Game Stats:\n\n' + Utils.objectToString(this, _props, '\n');
   }
 
   public function toHtml():String {
      var s:String = '<font color="#FFFFFF" size="20">Game Stats: <br><br>';
      if(mapsWon) {
         s += 'MAP WON!';
      } else {
         s += 'Map Lost :(';
      }
      s += '</font><br><br>';
 
      s += Utils.objectToString(this, _props, '<br>', true);
 
      return s;
   }
}
}

That’s it there really. Most everywhere that updates those totals just does so explicitly since all the totals are public vars. Since this was such a short section let’s go ahead and throw in Utils since I just called it twice.

Utils.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
package com.zf.utils
{
   import com.adobe.serialization.json.JSON;
 
public class Utils
{
   public static function JSONDecode(s:String):Object {
      return com.adobe.serialization.json.JSON.decode(s);
   }
 
   public static function objectToString(obj:*, props:Array, lineBr:String = '\n', 
          toHTML:Boolean = false, fontSize:int = 20, fontColor:String = '#FFFFFF'):String 
   {
      var str:String = '',
          len:int = props.length;
 
      for(var i:int = 0; i < len; i++) {
         if(toHTML) {
            str += '<font color="' + fontColor + '" size="' + fontSize + '">';
         }
 
         str += props[i] + ' = ' + int(obj[props[i]]);
 
         if(toHTML) {
            str += '</font>';
         }
 
         str += lineBr;
      }
 
      return str;
   }
}
}

Ok, so we’ve trekked through sounds and stats, now it’s time for saves!

SharedObjectManager.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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
package com.zf.managers
{
   import com.zf.core.Config;
   import com.zf.utils.ZFGameData;
 
   import flash.net.SharedObject;
 
public class SharedObjectManager
{
   public var so:SharedObject;
 
   private static var instance	         : SharedObjectManager;
   private static var allowInstantiation : Boolean;
 
   private const LOCAL_OBJECT_NAME	 : String = 'zombieflambe_demo';
 
   /***
    * Gets the singleton instance of SharedObjectManager or creates a new one
    */
   public static function getInstance():SharedObjectManager {
      if (instance == null) {
         allowInstantiation = true;
         instance = new SharedObjectManager();
         allowInstantiation = false;
      }
      return instance;
   }
 
   public function SharedObjectManager() {
      if (!allowInstantiation) {
         throw new Error("Error: Instantiation failed: Use SharedObjectManager.getInstance() instead of new.");
      } else {
         init();
      }
   }
 
   /**
    * Initializes the class, if gameOptions hasn't been set before, create gameOptions
    * Otherwise it initializes Config gameOptions from the shared object
    */
   public function init():void {
      so = SharedObject.getLocal(LOCAL_OBJECT_NAME);
      if(!so.data.hasOwnProperty('gameOptions')) {
         // set up the global game options
         so.data.gameOptions = {
            musicVolume: Config.DEFAULT_SOUND_VOLUME,
            sfxVolume: Config.DEFAULT_SOUND_VOLUME
         };
         Config.musicVolume = Config.DEFAULT_SOUND_VOLUME
         Config.sfxVolume = Config.DEFAULT_SOUND_VOLUME
      } else {
         Config.musicVolume = so.data.gameOptions.musicVolume;
         Config.sfxVolume = so.data.gameOptions.sfxVolume;
      }
   }
 
   /**
    * Create a new ZFdata object in name's place in data
    *
    * @param {String} name the current game name
    * @param {Boolean} updateThenSave if true, the function will call save, otherwise the user can save manually
    */
   public function createGameData(name:String, updateThenSave:Boolean = false):void {
      so.data[name] = new ZFGameData();
      so.data.gameOptions = {
         musicVolume: Config.DEFAULT_SOUND_VOLUME,
         sfxVolume: Config.DEFAULT_SOUND_VOLUME
      };
 
      // Reset config values
      Config.musicVolume = Config.DEFAULT_SOUND_VOLUME;
      Config.sfxVolume = Config.DEFAULT_SOUND_VOLUME;
      if(updateThenSave) {
         save();
      }
   }
 
   /**
    * Gets a whole block of game data
    *
    * @param {String} name the current game name
    * @returns {Object} the game data Object requested by game name
    */
   public function getGameData(name:String):Object {
      return (so.data[name]) ? so.data[name] : {};
   }
 
   /**
    * Sets a whole block of game data
    *
    * @param {Object} data the data we want to add
    * @param {String} name the current game name or blank to use Config.currentGameSOID
    * @param {Boolean} updateThenSave if true, the function will call save, otherwise the user can save manually
    */
   public function setGameData(data:Object, name:String = '', updateThenSave:Boolean = false):void {
      if(name == '') {
         name = Config.currentGameSOID;
      }
 
      so.data[name] = Object(data);
 
      if(updateThenSave) {
         save();
      }
   }
 
   /**
    * Sets a single property from game data
    *
    * @param {*} data the data we want to add
    * @param {String} prop the name of the property to update
    * @param {String} name the current game name or blank to use Config.currentGameSOID
    * @param {Boolean} updateThenSave if true, the function will call save, otherwise the user can save manually
    */
   public function setGameDataProperty(data:*, prop:String, name:String = '', updateThenSave:Boolean = false):void {
      if(name == '') {
         name = Config.currentGameSOID;
      }
 
      // check for nested property
      if(prop.indexOf('.') != -1) {
         // happens when you pass in 'upgrades.ptsTotal' will split by the . and
         // pass data in to so.data[name]['upgrades']['ptsTotal']
         var props:Array = prop.split('.');
         so.data[name][props[0]][props[1]] = data;
      } else {
         so.data[name][prop] = data;
      }
 
      if(updateThenSave) {
         save();
      }
   }
 
   /**
    * Gets a single property from game data
    *
    * @param {String} prop the name of the property to update
    * @param {String} name the current game name or blank to use Config.currentGameSOID
    * @returns {*} the game data property requested
    */
   public function getGameDataProperty(prop:String, name:String = ''):* {
      if(name == '') {
         name = Config.currentGameSOID;
      }
      return so.data[name][prop];
   }
 
   /**
    * Sets the global gameOptions Object on the SO
    *
    * @param {Object} data the gameOptions data we want to add
    * @param {Boolean} updateThenSave if true, the function will call save, otherwise the user can save manually
    */
   public function setGameOptions(data:Object, updateThenSave:Boolean = false):void {
      so.data.gameOptions.musicVolume = (!isNaN(data.musicVolume)) ? data.musicVolume : 0;
      so.data.gameOptions.sfxVolume = (!isNaN(data.sfxVolume)) ? data.sfxVolume : 0;
 
      if(updateThenSave) {
         save();
      }
   }
 
   public function dev_WipeAllMem():void {
      createGameData('game1', true);
      createGameData('game2', true);
      createGameData('game3', true);
   }
 
   /**
    * Gets the global gameOptions Object from the SO
    *
    * @returns {Object} the saved gameOptions data
    */
   public function getGameOptions():Object {
      return so.data.gameOptions;
   }
 
   /**
    * Checks to see if a game name exists on the SO
    *
    * @param {String} name the game name we want to check for to see if it exists
    * @returns {Boolean} if the game exists or not
    */
   public function gameExists(name:String):Boolean {
      return (so.data[name])
   }
 
   /**
    * Saves the SO to the user's HD
    */
   public function save():void {
      so.flush();
   }
}
}

I left the comments in this file for some reason, so I’ll just add some commentary around those.

That’s the SharedObjectManager. We’ll be looking at a few places that we use the SharedObjectManager in the next post on UI.

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 6 – TowersAS3 Starling Tower Defense Tutorial – Part 8 – UI Menu Components >>

Leave a Comment

Your email address will not be published.