Away3D Shoot’em’Up Tutorial – Sound Effects

by The Tech Labs 5

In this article we add some sound effects to the game.

Result

Download Source Files

Requirements

Adobe Flex v3.02

Away3D v3.3.3 source code

Flint 2.1.0 source code

Pre-Requisites

You should read the previous articles in this series.

Create from Scratch a Away3D Shoot’em’Up Game: Part 1
Create from Scratch a Away3D Shoot’em’Up Game: Part 2
Create from Scratch a Away3D Shoot’em’Up Game: Part 3
Create from Scratch a Away3D Shoot’em’Up Game: Part 4

Sound Effects

I remember playing Unreal when it was first released. The sniper rifle was one of my favourite weapons in multiplayer, but it sounded weak. Thankfully a patch was released that, among other things, gave the sniper rifle a much meatier sound effect. My frag count went up just because I loved the new sound so much.

Sound effects can add a whole new dimension to a game, and here we will look at how to get some sounds into our shoot’em’up.

ResourceManager.as

package
{
	import away3d.materials.BitmapMaterial;
	import away3d.materials.TransformBitmapMaterial;

	import mx.core.BitmapAsset;
	import mx.core.SoundAsset;

	public class ResourceManager
	{
		[Embed (source="../media/building1.png")]
		public static const Building1:Class;
		public static const Building1_BitmapAsset:BitmapAsset = new Building1();
		public static const Building1_Tex:BitmapMaterial = new BitmapMaterial(Building1_BitmapAsset.bitmapData, {smooth:true});

		[Embed (source="../media/building2.png")]
		public static const Building2:Class;
		public static const Building2_BitmapAsset:BitmapAsset = new Building2();
		public static const Building2_Tex:BitmapMaterial = new BitmapMaterial(Building2_BitmapAsset.bitmapData, {smooth:true});

		[Embed (source="../media/building3.png")]
		public static const Building3:Class;
		public static const Building3_BitmapAsset:BitmapAsset = new Building3();
		public static const Building3_Tex:BitmapMaterial = new BitmapMaterial(Building3_BitmapAsset.bitmapData, {smooth:true});

		[Embed (source="../media/roof1.png")]
		public static const Roof1:Class;
		public static const Roof1_BitmapAsset:BitmapAsset = new Roof1();
		public static const Roof1_Tex:BitmapMaterial = new BitmapMaterial(Roof1_BitmapAsset.bitmapData, {smooth:true});

		[Embed (source="../media/background1.png")]
		public static const Background1:Class;
		public static const Background1_BitmapAsset:BitmapAsset = new Background1();
		public static const Background1_Tex:TransformBitmapMaterial = new TransformBitmapMaterial(Background1_BitmapAsset.bitmapData, {smooth:true});

		[Embed (source="../media/player.png")]
		public static const Player:Class;
		public static const Player_BitmapAsset:BitmapAsset = new Player();
		public static const Player_Tex:BitmapMaterial = new BitmapMaterial(Player_BitmapAsset.bitmapData);

		[Embed (source="../media/enemy.png")]
		public static const Enemy:Class;
		public static const Enemy_BitmapAsset:BitmapAsset = new Enemy();
		public static const Enemy_Tex:BitmapMaterial = new BitmapMaterial(Enemy_BitmapAsset.bitmapData);

		[Embed (source="../media/bullet.png")]
		public static const Bullet:Class;
		public static const Bullet_BitmapAsset:BitmapAsset = new Bullet();
		public static const Bullet_Tex:BitmapMaterial = new BitmapMaterial(Bullet_BitmapAsset.bitmapData);

		[Embed(source="../media/gun1.mp3")]
		public static var Gun1Sound:Class;
		public static var Gun1FX:SoundAsset = new Gun1Sound() as SoundAsset;

		[Embed(source="../media/explosion.mp3")]
		public static var ExplosionSound:Class;
		public static var ExplosionFX:SoundAsset = new ExplosionSound() as SoundAsset;
	}
}

Sounds are embedded into the SWF file just like images. Here we embed two MP3 files, and create new SoundAssets from the embedded resources. It’s these SoundAsset objects that we will use to play the sound effects. Because Flash has built in support for MP3, we don’t have to worry about decoding the files before they are played.

As you might have guessed from the names of the sound files, these two sound effects will be played when there is an explosion, and when a weapon is fired. The weapons have been very simple to this point, with the Player creating a new instance of the Weapon class directly when it is time to shoot. This setup does not easily allow the player to have different types of weapons, or to be able to create multiple weapons with every shot. Since the sound effects will be played when the weapons are fired, we first need to create a more flexible system for the weapons.

The Weapon class used to include a logic property, which was a reference to a function to be called during the enterFrame function. My idea for this was to allow the Weapon class to create multiple varieties of weapons, where the function pointed to by the logic property would define how the weapon behaved, and the startup function would initialise the necessary properties. When I looked at this again I realised that the Weapon class would end up having numerous properties that related to only one type of weapon e.g. the simple weapon would have a direction property, and a homing missile might have the a nearest enemy property. This would quickly clutter the Weapon class.

By designing the Weapon class in a way that allowed it to create multiple weapon variations I had hoped to save myself some typing by not having to create a whole bunch of subclasses. It’s a lesson I keep learning over and over: shortcuts will bite you in the ass later on. So here the Weapon class is stripped down to include just the basic functionality common to all weapons.

Weapon.as

package
{
	import away3d.core.math.Number3D;
	import away3d.materials.BitmapMaterial;
	import away3d.primitives.Plane;

	public class Weapon extends MeshObject
	{
		protected static const WEAPON_X_LIMIT:Number = 70;
		protected static const WEAPON_Y_LIMIT:Number = 60;
		protected var damage:int = 0;

		public function Weapon()
		{
			super();
		}

		public override function shutdown():void
		{
			super.shutdown();
		}

		public function startupWeapon(engineManager:EngineManager, material:BitmapMaterial, position:Number3D, sizeX:Number, sizeY:Number, damage:int):Weapon
		{
			var plane:Plane = new Plane(
				{material:material,
				width:sizeX,
				height:sizeY,
				yUp:false});
			super.startupMeshObject(engineManager, plane);
			this.model.position = position;
			this.damage = damage;

			return this;
		}

		public override function enterFrame(dt:Number):void
		{
			if (this.model.x > WEAPON_X_LIMIT ||
				this.model.x < -WEAPON_X_LIMIT || 				this.model.y > WEAPON_Y_LIMIT ||
				this.model.y < -WEAPON_Y_LIMIT)
				this.shutdown();
		}

		public override function collision(other:MeshObject):void
		{
			if (other.collisionName == CollisionIdentifiers.ENEMY ||
				other.collisionName == CollisionIdentifiers.PLAYER)
				(other as DamageableObject).damage(this.damage);
			this.shutdown();
		}
	}
}

A new class, BasicWeapon, is created to define weapons that just move in a straight line. Even though it might not seem like a big deal, splitting the Weapon class in two will make the code a lot more manageable as more weapon types are added.

BasicWeapon.as

package
{
	import away3d.core.math.Number3D;

	public class BasicWeapon extends Weapon
	{
		protected static const WEAPON_SIZE:Number = 2;
		protected var speed:Number = 0;
		protected var direction:Number3D = null;

		public function BasicWeapon()
		{
			super();
		}

		protected function startupBasicWeapon(engineManager:EngineManager, position:Number3D, damage:int, xDir:Number, yDir:Number, speed:Number):BasicWeapon
		{
			this.startupWeapon(engineManager, ResourceManager.Bullet_Tex, position, WEAPON_SIZE, WEAPON_SIZE, damage);
			this.direction = new Number3D(xDir, yDir, 0);
			this.direction.normalize();
			this.speed = speed;
			return this;
		}

		public function startupBasicPlayerWeapon(engineManager:EngineManager, position:Number3D, damage:int, xDir:Number, yDir:Number, speed:Number):BasicWeapon
		{
			startupBasicWeapon(engineManager, position, damage, xDir, yDir, speed);
			this.collisionName = CollisionIdentifiers.PLAYERWEAPON;
			return this;
		}

		public function startupBasicEnemyWeapon(engineManager:EngineManager, position:Number3D, damage:int, xDir:Number, yDir:Number, speed:Number):BasicWeapon
		{
			startupBasicWeapon(engineManager, position, damage, xDir, yDir, speed);
			this.collisionName = CollisionIdentifiers.ENEMYWEAPON;
			return this;
		}

		public override function enterFrame(dt:Number):void
		{
			this.model.x += speed * dt;

			super.enterFrame(dt);
		}

	}
}

In order to easily define a selection of weapons for the player and the enemies to fire at each other, we need to separate the code for the weapon creation out of the Player class. Instead this code will be placed in a new class called WeaponManager.

WeaponManager.as

package
{
	import away3d.core.math.Number3D;

	import flash.utils.Dictionary;

	public class WeaponManager
	{
		protected var engineManager:EngineManager = null;
		protected var weaponDatabase:Dictionary = new Dictionary();

		public function WeaponManager()
		{
			super();
		}

		public function startupWeaponManager(engineManager:EngineManager):WeaponManager
		{
			this.engineManager = engineManager;

			var weapon1:Dictionary = new Dictionary();
			weaponDatabase[1] = weapon1;

			weapon1[1] = weapon1Level1;
			weapon1[2] = weapon1Level2;

			return this;
		}

		public function createWeapon(weapon:int, weaponLevel:int, position:Number3D):Number
		{
			if (isValidWeapon(weapon, weaponLevel))
			{
				return weaponDatabase[weapon][weaponLevel](position);
			}

			return 0;
		}

		public function isValidWeapon(weapon:int, weaponLevel:int):Boolean
		{
			if (weaponDatabase[weapon] != null)
			{
				if (weaponDatabase[weapon][weaponLevel] != null)
					return true;
			}

			return false;
		}

		public function weapon1Level1(weaponPosition:Number3D):Number
		{
			var timeToNextShot:Number = 0.4;
			var damage:int = 1;
			var xDir:Number = 1;
			var yDir:Number = 0;
			var speed:Number = 100;
			new BasicWeapon().startupBasicPlayerWeapon(this.engineManager, weaponPosition, damage, xDir, yDir, speed);

			new SoundEffect().startupSoundEffect(this.engineManager, ResourceManager.Gun1FX);

			return timeToNextShot;
		}

		public function weapon1Level2(weaponPosition:Number3D):Number
		{
			var timeToNextShot:Number = 0.4;

			var damage:int = 1;
			var xDir:Number = 1;
			var yDir:Number = 0;
			var yDir2:Number = 1;
			var yDir3:Number = -1;
			var speed:Number = 100;
			new BasicWeapon().startupBasicPlayerWeapon(this.engineManager, weaponPosition, damage, xDir, yDir, speed);
			new BasicWeapon().startupBasicPlayerWeapon(this.engineManager, weaponPosition, damage, xDir, yDir2, speed);
			new BasicWeapon().startupBasicPlayerWeapon(this.engineManager, weaponPosition, damage, xDir, yDir3, speed);

			new SoundEffect().startupSoundEffect(this.engineManager, ResourceManager.Gun1FX);

			return timeToNextShot;
		}

	}
}

Each weapon is defined by a number and a level. So weapon number 1 level 1 are simple bullets that move in a straight line. Weapon 1 level 2 might be three bullets fired out by the player.

The weaponDatabase property is a dictionary of weapon numbers mapped to another dictionary. This second dictionary contains the weapon levels mapped to functions. So by calling weaponDatabase[1][2]() you are calling the function that is mapped to weapon number 1 level 2.

For convenience we will refer to a weapon definition as the combination of the weapon number and level, and the functions that are called to create these weapon definitions will be referred to as weapon definition functions.

For this demo we have two weapon definitions, and to them we have mapped two weapon definition functions: weapon1level1 and weapon1level2. The code to populate the weaponDatabase property is in the startupWeaponManager function.

var weapon1:Dictionary = new Dictionary();
weaponDatabase[1] = weapon1;

First we create a new Dictionarty to reperent the weapon number, and assign it to the weaponDatabase dictionary.

weapon1[1] = weapon1Level1;
weapon1[2] = weapon1Level2;

Next the two weapon definition functions are mapped to the appropiate weapon levels.

public function weapon1Level1(weaponPosition:Number3D):Number
{
	var timeToNextShot:Number = 0.4;
	var damage:int = 1;
	var xDir:Number = 1;
	var yDir:Number = 0;
	var speed:Number = 100;
	new BasicWeapon().startupBasicPlayerWeapon(this.engineManager, weaponPosition, damage, xDir, yDir, speed);

	new SoundEffect().startupSoundEffect(this.engineManager, ResourceManager.Gun1FX);

	return timeToNextShot;
}

The weapon definition functions are quite simple. The variables that will be supplied to the Weapon startup function (the BasicWeapon startupBasicPlayerWeapon function in this case) are defined so they can be clearly identified, and then the weapon itself is created. This code is the equivalent of what used to be in the Player class when the Player created the weapons directly. These functions return the time that should elapse before the player or an enemy fires again.

You will also note that we are creating a new SoundEffect object along with the weapon. The SoundEffect class will be discussed later.

public function createWeapon(weapon:int, weaponLevel:int, position:Number3D):Number
{
	if (isValidWeapon(weapon, weaponLevel))
	{
		return weaponDatabase[weapon][weaponLevel](position);
	}

	return 0;
}

public function isValidWeapon(weapon:int, weaponLevel:int):Boolean
{
	if (weaponDatabase[weapon] != null)
	{
		if (weaponDatabase[weapon][weaponLevel] != null)
			return true;
	}

	return false;
}

The createWeapon and isValidWeapon functions provide an easy way to call the weapon definition functions by supplying the weapon number and weapon level, as well as the position that the weapon should be initially created at.

We have only two weapon definitions here, but you can imagine how much code there would be if we had 10 weapons each with 10 levels. That code would make the Player class a complete mess, so it makes sense to separate it out into the WeaponManager class.

Player.as

package
{
	import away3d.core.math.Number3D;
	import away3d.events.MouseEvent3D;
	import away3d.primitives.*;

	import flash.events.*;
	import flash.geom.*;
	import flash.media.*;

	import mx.core.*;

	public class Player extends DamageableObject
	{
		protected static const PLAYER_X_LIMIT:Number = 52;
		protected static const PLAYER_Y_LIMIT:Number = 38;
		protected static const PLAYER_WIDTH:Number = 16;
		protected static const PLAYER_HEIGHT:Number = 5;
		protected static const COLLISION_PLANE_WIDTH:Number = 150;
		protected static const COLLISION_PLANE_HEIGHT:Number = 100;
		protected static const PLAYER_SHIELDS:int = 10;
		protected var collisionPlane:MeshObject = null;
		protected var shooting:Boolean = false;
		protected var timeToNextShot:Number = 0;

		public function Player()
		{
			super();
		}

		public function startupPlayer(engineManager:EngineManager):Player
		{
			var plane:Plane = new Plane(
				{material:ResourceManager.Player_Tex,
				width:PLAYER_WIDTH,
				height:PLAYER_HEIGHT,
				yUp:false});
			super.startupMeshObject(engineManager, plane);

			var collisionPlaneMesh:Plane = new Plane(
				{material:new NullMaterial(),
				width:COLLISION_PLANE_WIDTH,
				height:COLLISION_PLANE_HEIGHT,
				yUp:false});
			collisionPlane = new MeshObject().startupMeshObject(engineManager, collisionPlaneMesh);
			collisionPlaneMesh.addEventListener(MouseEvent3D.MOUSE_MOVE, this.mouseMove3D);

			this.collisionName = CollisionIdentifiers.PLAYER;
			this.shields = PLAYER_SHIELDS;

			Application.application.pbarLife.setProgress(this.shields, PLAYER_SHIELDS);

			return this;
		}

		override public function shutdown():void
		{
			collisionPlane.model.removeEventListener(MouseEvent3D.MOUSE_MOVE, this.mouseMove3D);
			collisionPlane.shutdown();
			collisionPlane = null;
			super.shutdown();
		}

		override public function enterFrame(dt:Number):void
		{
			super.enterFrame(dt);

			timeToNextShot -= dt;
			timeToNextShot = timeToNextShot<0?0:timeToNextShot; 			if (timeToNextShot == 0 && shooting) 			{ 				var weaponPosition:Number3D = new Number3D(); 				weaponPosition.add(this.model.position, new Number3D(PLAYER_WIDTH / 2, 0, 0)); 				 				timeToNextShot = this.engineManager.MyApplicationManager.MyWeaponManager.createWeapon( 					this.engineManager.MyApplicationManager.MyPlayerWeapon, 					this.engineManager.MyApplicationManager.MyPlayerWeaponLevel,  					weaponPosition); 			} 				 			if (this.model.x > PLAYER_X_LIMIT)
				this.model.x = PLAYER_X_LIMIT;
			if (this.model.x < -PLAYER_X_LIMIT) 				this.model.x = -PLAYER_X_LIMIT; 			 			if (this.model.y > PLAYER_Y_LIMIT)
				this.model.y = PLAYER_Y_LIMIT;
			if (this.model.y < -PLAYER_Y_LIMIT)
				this.model.y = -PLAYER_Y_LIMIT;
		}

		public function mouseMove3D(event:MouseEvent3D):void
		{
			if (this.model)
			{
				this.model.x = event.sceneX;
				this.model.y = event.sceneY;
			}
		}

		public override function collision(other:MeshObject):void
		{

		}

		public override function mouseDown(event:MouseEvent):void
		{
			shooting = true;
		}

		public override function damage(amount:int):void
		{
			super.damage(amount);
			Application.application.pbarLife.setProgress(this.shields, PLAYER_SHIELDS);
		}

		public override function mouseUp(event:MouseEvent):void
		{
			shooting = false;
		}

		protected override function die():void
		{
			engineManager.MyApplicationManager.levelEnded();
			super.die();
		}
	}
}

With the weapon creation code now in the WeaponManager class, the Player has to be modified to call the createWeapon function instead of creating the weapon directly. The weapon number and weapon level properties are kept in the ApplicationManager.

timeToNextShot = this.engineManager.MyApplicationManager.MyWeaponManager.createWeapon(
		this.engineManager.MyApplicationManager.MyPlayerWeapon,
		this.engineManager.MyApplicationManager.MyPlayerWeaponLevel,
		weaponPosition);

ApplicationManager.as

package
{
	import mx.core.Application;

	public class ApplicationManager extends BaseObject
	{
		protected static const TIME_BETWEEN_BUILDINGS:Number = 2;
		protected static const TIME_BETWEEN_ENEMIES:Number = 1;
		protected static const TIME_TO_LEVEL_END:Number = 1;
		protected var timeToEndGame:Number = TIME_TO_LEVEL_END;
		protected var timeToNextBuilding:Number = 0;
		protected var timeToNextEnemy:Number = TIME_BETWEEN_ENEMIES;
		protected var levelHasEnded:Boolean = false;
		protected var myPlayerWeapon:int = 1;
		protected var myPlayerWeaponLevel:int = 1;
		protected var myWeaponManager:WeaponManager = null;
		public var myScore:int = 0;

		public function get MyWeaponManager():WeaponManager
		{
			return myWeaponManager;
		}

		public function get MyScore():int
		{
			return myScore;
		}

		public function get MyPlayerWeapon():int
		{
			return myPlayerWeapon;
		}

		public function get MyPlayerWeaponLevel():int
		{
			return myPlayerWeaponLevel;
		}

		public function set score(value:int):void
		{
			myScore = value;
			Application.application.lblScore.text = myScore;
		}

		public function ApplicationManager()
		{
			super();
		}

		public function startupApplicationManager(engineManager:EngineManager):ApplicationManager
		{
			this.startupBaseObject(engineManager);
			this.engineManager.addCollidingPair(CollisionIdentifiers.ENEMY, CollisionIdentifiers.PLAYER);
			this.engineManager.addCollidingPair(CollisionIdentifiers.ENEMY, CollisionIdentifiers.PLAYERWEAPON);
			this.myWeaponManager = new WeaponManager().startupWeaponManager(this.engineManager);

			return this;
		}

		public function startLevel1():void
		{
			timeToNextBuilding = 0;
			score = 0;

			new BackgroundPlane().startupBackgroundPlane(engineManager);
			new Player().startupPlayer(engineManager);

			// prepopulate the city
			for (var i:int = 0; i < 5; ++i)
			{
				var building:BackgroundBuilding = new BackgroundBuilding();
				building.startupBackgroundBuilding(engineManager);
				building.enterFrame((i + 1) * 2);
			}
		}

		public function levelEnded():void
		{
			levelHasEnded = true;
		}

		public override function shutdown():void
		{
			super.shutdown();
		}

		public override function enterFrame(dt:Number):void
		{
			timeToNextBuilding -= dt;
			if (timeToNextBuilding <= 0)
			{
				timeToNextBuilding = TIME_BETWEEN_BUILDINGS;
				new BackgroundBuilding().startupBackgroundBuilding(engineManager);
			}

			timeToNextEnemy -= dt;
			if (timeToNextEnemy <= 0)
			{
				timeToNextEnemy = TIME_BETWEEN_ENEMIES;
				new Enemy().startupBasicEnemy(engineManager);
			}

			if (levelHasEnded)
			{
				timeToEndGame -= dt;
				if (timeToEndGame <= 0)
				{
					engineManager.nextStateChange = "MainMenu";
				}
			}
		}
	}
}

The ApplicationManager creates the WeaponManager in the startupApplicationManager function, and holds the players weapon definition in the MyPlayerWeapon and MyPlayerWeaponLevel properties.

For a tutorial on adding sound effects we have focused a lot on how the weapons are created. But fear not, now that we have cleaned up the weapon definitions and the code to create these
weapons, we can now add some sound effects.

SoundEffect.as

package
{
	import flash.events.Event;
	import flash.media.SoundChannel;

	import mx.core.SoundAsset;

	public class SoundEffect extends BaseObject
	{
		protected var channel:SoundChannel = null;

		public function SoundEffect()
		{
			super();
		}

		public function startupSoundEffect(engineManager:EngineManager, sound:SoundAsset):SoundEffect
		{
			super.startupBaseObject(engineManager);

			channel = sound.play();
			channel.addEventListener(Event.SOUND_COMPLETE, soundComplete);
			return this;
		}

		override public function shutdown():void
		{
			if (channel != null)
			{
				channel.removeEventListener(Event.SOUND_COMPLETE, soundComplete);
				channel.stop();
				channel = null;
			}

			super.shutdown();
		}

		public function soundComplete(event:Event):void
		{
			shutdown();
		}

	}
}

Playing sound effects is actually very simple. The SoundAsset class (created in the ResourceManager) has a play function which returns a SoundChannel object. The function of the SoundEffect class is to play the SoundAsset, and stop the sound when the SoundEffect is shutdown.

public function startupSoundEffect(sound:SoundAsset):SoundEffect
{
	super.startupBaseObject();

	channel = sound.play();
	channel.addEventListener(Event.SOUND_COMPLETE, soundComplete);
	return this;
}

The startupSoundEffect takes the SoundAsset to be played, stores the SoundChannel that is returned when the SoundAsset is played, and watches for when the sound has finished playing.

public function soundComplete(event:Event):void
{
	shutdown();
}

The soundComplete function is called when the sound has finished playing. It just calls shutdown.

override public function shutdown():void
{
	if (channel != null)
	{
		channel.removeEventListener(Event.SOUND_COMPLETE, soundComplete);
		channel.stop();
		channel = null;
	}

	super.shutdown();
}

When the SoundEffect is shutdown, either because the sound has finished playing, or the EngineManager has called the shutdown function when changing states, the event listener is removed and the sound effect is stopped.

The SoundEffect class provides a kind of “fire and forget” way to create a sound effect. When you create it the sound will play, and it will clean itself up when the sound effect has finished, or, by virtue of that fact that it extends the BaseObject class, the EngineManager will clean it up when the state changes.

DamageableObject.as

package
{
	public class DamageableObject extends MeshObject
	{
		protected var shields:int = 0;

		public function DamageableObject()
		{
			super();
		}

		protected function die():void
		{
			new EmitterContainer().startupEmitterContainer(engineManager, new Explosion(this.model.position));
			new SoundEffect().startupSoundEffect(this.engineManager, ResourceManager.ExplosionFX);
			this.shutdown();
		}

		public function damage(amount:int):void
		{
			this.shields -= amount;
			if (this.shields <= 0)
				this.die();
		}
	}
}

We have already seen how the SoundEffect class is used in the WeaponManger. In addition we will add a sound effect when the player or an enemy dies. Because the DamageableObject class defines the die function that is called when an enemy or player dies, this is a logical place to add the sound effect.

We have made quite a few changes to accommodate the sound effects. Some of these changes were needed to fix up some shortsighted code design (it’s a rule I keep catching myself out with, but don’t take shortcuts), and some were needed to implement a more extensible way to define and create the weapons. However these changes did give us a clear place to create the sound effects, and will lead onto the topic of the next article: powerups.

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>