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 TreeFortress’ SoundAS. 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.
[toc]
*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"
}
]
}
- Line 4 – the sound ID we’re going to pass to SoundAS for this sound
- Line 5 – the path to the mp3 file we want to load for that sound ID
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
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 = 'Game Stats:
';
if(mapsWon) {
s += 'MAP WON!';
} else {
s += 'Map Lost :(';
}
s += '
';
s += Utils.objectToString(this, _props, '
', true);
return s;
}
}
}
- Line 7-14 – these are the 8 primary totals that I am keeping track of in this game demo.
- Line 17 – _props is basically the name of all the public vars that I want to log out in toString() and toHTML()
- Line 23 – gets called to reset the totals
- Line 38 – good for tracing Totals out to the console
- Line 42 – good for writing the totals into HTML and displaying in a ScrollText/TextArea
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
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 += '';
}
str += props[i] + ' = ' + int(obj[props[i]]);
if(toHTML) {
str += '';
}
str += lineBr;
}
return str;
}
}
}
- Line 7 – I’m not sure if this is Flash Builder’s fault, or Starling’s fault somehow, or who knows why, but for some reason when I imported the JSON lib into my Assets.as class and tried to use it in functions, FlashBuilder did squiggly lines and said that there was no such thing as that JSON.decode stuff. Very frustrating, especially when all of your files are .json. So I pulled this out to a Utils function, pass in the JSON String and this converts it and returns you a pretty Object. No more squigglies!
- Line 11-12 – this is a Util function I wrote to help me output the Totals object. It takes several params but most are defaulted to something.
- Line 17 – loop through my props array and begin building a String to output
- Line 19 – if I’m creating HTML, add the opening font tag
- Line 22 – add the props key followed by the value in the object. I’m typecasting to int so there are no crazy decimals in the totals.
- Line 25 – if creating HTML, add the closing font tag
- Line 28 – add the line break character
Ok, so we’ve trekked through sounds and stats, now it’s time for saves!
SharedObjectManager.as
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.
- Line 45 – so I want the SharedObject set up where data.gameOptions is independent of any specific saved game. So when you set up the sound in the game, that will be saved no matter which game slot (game1, game2, or game3) the player is using.
- Line 46-47 – since this is the init() function and we’re initializing things, set the volume settings to the default sound volume which is 0.75.
- Line 49-50 – make sure Config is also set to defaults
- Line 52-53 – otherwise, if the data.gameOptions has actually already been set (previous saved data) then use the values that were saved to populate Config’s volume settings.
- Line 63 – for use when completely creating a whole new saved game, if you call this passing in a name when there is already data saved there, it will Overwrite that data with empty data.
- Line 73 – I use this pattern in most of the functions… updateThenSave:Boolean = false. By default you should have to manually call save on the SharedObject, but if you pass in true, this will update whatever the function does, then save as well.
- Line 98 – this is a good function after a game has been played and you have a whole set of updated game totals to save. Just pass in the new game object.
- Line 115 – this is good if you want to update just a single item on the game data. Maybe the number of games played (because the map hasn’t been completed yet so we don’t have new data, just that we started a map).
- Line 142 – gets a single property from game data. so if you just wanted mapsWon and didn’t care about anything else, this would be your function. I’m noticing here I could definitely use the dot-naming scheme from lines 121-126 to let me get specific properties from the upgrades object, for example. But that’s missing here.
- Line 155 – this is used to set/save gameOptions
- Line 164 – this is my Dev Tool Nuclear Option! As you’re creating SharedObject data structures and figuring out how you want to structure your saved game data, you go through a lot of iterations. And adding Game.so.dev_WipeAllMem() in the first few lines of Game.as will completely reset all of your saved game data. This is great when you’ve moved things around and just want to nuke your data and start over.
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
1 Comment