The Black Hole Game

Adding the Finishing Touches

Our Black Hole game is functional in terms of the mechanics of the game working and being fairly well balanced, so now we can spend time addressing the gameplay and “fun factor” of the game. The goal is to increase the replay value as much as possible. A neat idea for a game can fizzle pretty quickly unless there is a good reason for the player to come back. What is it about the game that would compel the player to keep playing for hours or days? The goal of an indie developer or a professional (obviously) is to sell a game. There are some games that will sell on the promise of short gameplay if the subject is compelling, but in this market replay sells. So, we’ll see what we can do to improve gameplay and replay value.

Modifying the GameModule Interface Class

To make it possible to leave the PlayingModule and then return to it without shutting down and restarting the game, an enhancement is needed that will reset the gameplay to the starting values. So, a change will be made to the interface class, GameModule. Note the new Reset() method. This will meet our needs. Each of the game modules that use this interface will have to implement the new method:

MySoundEffect Class

The MySoundEffect class, shown in Listing 24.1, was introduced back in Hour 18, “Playing Audio,” and we’ll need it here again. We’ll need to make a minor tweak so that it’s a little easier to use, by adding a Play() method directly to the class rather than requiring use of the instance object.

LISTING 24.1 Source Code for the MySoundEffect Class

[code]
public class MySoundEffect
{
private Game1 game;
private SoundEffect effect;
private SoundEffectInstance instance;
public MySoundEffect(Game1 game)
{
this.game = game;
effect = null;
instance = null;
}
public void Load(string assetName)
{
effect = game.Content.Load<SoundEffect>(assetName);
instance = effect.CreateInstance();
}
public void Play()
{
if (game.globalAudio)
instance.Play();
}
}
[/code]

There is only one sound effect in the Black Hole game—when the ship launches a satellite. I know, the game is shamefully lacking in the audio department. Can you think of any events in the game that would benefit from a sound clip?

GameOverModule Class

The GameOverModule class does not need to use the Reset() method, but it has to be added to the class nonetheless because IGameModule mandates it. The GameOver class displays a message on the screen and waits for the user to press the Return button. This is kind of a no-brainer screen, but it is an important part of the gameplay and helps to separate this functionality from PlayingModule. Figure 24.1 shows the screen, and Listing 24.2 contains the source code.

Game over, man! Game over!
FIGURE 24.1 Game over, man! Game over!

LISTING 24.2 Source Code for the GameOverModule Class

[code]
class GameOverModule : IGameModule
{
Game1 game;
Label lblTitle;
Button btnReturn;
public GameOverModule(Game1 game)
{
this.game = game;
}
public void Reset()
{
}
public void LoadContent(ContentManager content)
{
lblTitle = new Label(content, game.spriteBatch,
game.bigfont);
lblTitle.text = “GAME OVER!”;
Vector2 size = game.font.MeasureString(lblTitle.text);
lblTitle.position = new Vector2(400-size.X, 200);
btnReturn = new Button(content, game.spriteBatch,
game.guifont);
btnReturn.text = “Return”;
btnReturn.position = new Vector2(400, 430);
btnReturn.scaleV = new Vector2(2.0f, 1.0f);
}
public void Update(TouchLocation touch, GameTime gameTime)
{
btnReturn.Update(touch);
if (btnReturn.Tapped)
game.gameState = Game1.GameState.TITLE;
}
public void Draw(GameTime gameTime)
{
lblTitle.Draw();
btnReturn.Draw();
}
}
[/code]

OptionsModule Class

The OptionsModule class represents the Options screen in the game. This is often where game settings can be changed. In this small game, we’ll need one global setting to make the screen useful—a global audio on/off switch. The screen is shown in Figure 24.2. Listing 24.3 shows the source code for the class.

The Options screen.
FIGURE 24.2 The Options screen.

LISTING 24.3 Source Code for the OptionsModule Class

[code]
class OptionsModule : IGameModule
{
Game1 game;
Label lblTitle;
Button btnReturn;
Button btnAudio;
public OptionsModule(Game1 game)
{
this.game = game;
}
public void Reset()
{
}
public void LoadContent(ContentManager content)
{
lblTitle = new Label(content, game.spriteBatch,
game.bigfont);
lblTitle.text = “Options Screen”;
Vector2 size = game.font.MeasureString(lblTitle.text);
lblTitle.position = new Vector2(400-size.X, 10);
btnAudio = new Button(game.Content, game.spriteBatch,
game.guifont);
btnAudio.text = ““;
btnAudio.position = new Vector2(400, 240);
btnAudio.scaleV = new Vector2(4.0f, 1.0f);
btnReturn = new Button(content, game.spriteBatch,
game.guifont);
btnReturn.text = “Return”;
btnReturn.position = new Vector2(400, 430);
btnReturn.scaleV = new Vector2(3.0f, 1.2f);
}
public void Update(TouchLocation touch, GameTime gameTime)
{
if (game.globalAudio)
btnAudio.text = “Turn Sound OFF”;
else
btnAudio.text = “Turn Sound ON”;
btnAudio.Update(touch);
if (btnAudio.Tapped)
{
game.globalAudio = !game.globalAudio;
}
btnReturn.Update(touch);
if (btnReturn.Tapped)
game.gameState = Game1.GameState.TITLE;
}
public void Draw(GameTime gameTime)
{
lblTitle.Draw();
btnAudio.Draw();
btnReturn.Draw();
}
}
[/code]

TitleScreenModule Class

The TitleScreenModule class has seen some major improvements since the early/crude version shown in an earlier hour. Now the buttons are colorful and rotate around a huge version of the animated black hole borrowed right out of the actual gameplay. See Figure 24.3 for the picture, and Listing 24.4 for the code.

The title screen features rotating buttons.
FIGURE 24.3 The title screen features rotating buttons.

What happens to the buttons if you wait too long to make a selection? Actually, nothing! But it would be fun if they would fall into the black hole when the player waits too long!

LISTING 24.4 Source Code for the TitleScreenModule Class

[code]
class TitleScreenModule : IGameModule
{
Game1 game;
Label lblTitle;
Button[] btnMenu;
MassiveObject blackHole;
MassiveObject superCore;
Sprite background;
public TitleScreenModule(Game1 game)
{
this.game = game;
rand = new Random();
}
public void Reset()
{
}
public void LoadContent(ContentManager content)
{
lblTitle = new Label(content, game.spriteBatch, game.bigfont);
lblTitle.text = “The Black Hole Game”;
Vector2 size = game.font.MeasureString(lblTitle.text);
lblTitle.position = new Vector2(400-size.X, 10);
btnMenu = new Button[3];
btnMenu[0] = new Button(content, game.spriteBatch, game.guifont);
btnMenu[0].text = “PLAY!”;
btnMenu[0].scaleV = new Vector2(2.5f, 1.2f);
btnMenu[0].color = Color.Orange;
btnMenu[0].animations.Add(
new OrbitalMovement( new Vector2(400, 240), 40, 0, 0.05f));
btnMenu[1] = new Button(content, game.spriteBatch, game.guifont);
btnMenu[1].text = “OPTIONS”;
btnMenu[1].color = Color.DarkRed;
btnMenu[1].scaleV = new Vector2(2.1f, 1.0f);
btnMenu[1].animations.Add(
new OrbitalMovement(new Vector2(420, 220), 140, 0, 0.04f));
btnMenu[2] = new Button(content, game.spriteBatch, game.guifont);
btnMenu[2].text = “EXIT”;
btnMenu[2].color = Color.DarkSeaGreen;
btnMenu[2].scaleV = new Vector2(1.6f, 0.8f);
btnMenu[2].animations.Add(
new OrbitalMovement(new Vector2(380, 260), 240, 0, 0.03f));
background = new Sprite(game.Content, game.spriteBatch);
background.Load(“space”);
background.origin = Vector2.Zero;
blackHole = new MassiveObject(game.Content, game.spriteBatch);
blackHole.Load(“blackhole”);
blackHole.position = new Vector2(400, 240);
blackHole.scale = 4.0f;
blackHole.mass = 40;
blackHole.color = new Color(255, 100, 100, 200);
blackHole.velocityAngular = 0.1f;
superCore = new MassiveObject(game.Content, game.spriteBatch);
superCore.image = blackHole.image;
superCore.position = new Vector2(blackHole.position.X,
blackHole.position.Y);
superCore.scale = blackHole.scale * 0.4f;
superCore.mass = 60;
superCore.color = new Color(200, 100, 100, 180);
superCore.velocityAngular = 4.0f;
superCore.origin = new Vector2(64, 64);
}
public void Update(TouchLocation touch, GameTime gameTime)
{
blackHole.Update(gameTime);
blackHole.Rotate();
superCore.Update(gameTime);
superCore.Rotate();
int tapped = -1;
int n = 0;
foreach (Button btn in btnMenu)
{
btn.Update(touch);
btn.Animate();
if (btn.Tapped)
tapped = n;
n++;
}
switch (tapped)
{
case 0:
game.gameState = Game1.GameState.PLAYING;
break;
case 1:
game.gameState = Game1.GameState.OPTIONS;
break;
case 2:
game.Exit();
break;
}
}
public void Draw(GameTime gameTime)
{
background.Draw();
superCore.Draw();
blackHole.Draw();
lblTitle.Draw();
foreach (Button btn in btnMenu)
{
btn.Draw();
}
}
}
[/code]

Game1 Class

The primary source code for the game is found in Listing 24.5, for the Game1 class. Only a minor change is needed to support the new Reset() method. This makes it easier to manage swapping between modules. For instance, exiting the PlayingModule and returning to the TitleScreen, then back to PlayingModule, we need to make sure the game is reset. That happens inside Game1. A new state backup variable is also needed, oldState, to keep track of when the state changes, in order to know when to call Reset().

LISTING 24.5 Source Code for the Game1 Class

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
public enum GameState
{
TITLE = 0,
PLAYING = 1,
OPTIONS = 2,
GAMEOVER = 3
}
public GraphicsDeviceManager graphics;
public SpriteBatch spriteBatch;
public GameState gameState, oldState;
public Color backColor;
Random rand;
TouchLocation oldTouch;
IGameModule[] modules;
public bool globalAudio;
public SpriteFont font, guifont, bigfont;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
oldTouch = new TouchLocation();
rand = new Random();
backColor = new Color(32, 32, 32);
globalAudio = true;
gameState = GameState.TITLE;
oldState = gameState;
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
guifont = Content.Load<SpriteFont>(“GUIFont”);
bigfont = Content.Load<SpriteFont>(“BigFont”);
modules = new IGameModule[4];
modules[0] = new TitleScreenModule(this);
modules[1] = new PlayingModule(this);
modules[2] = new OptionsModule(this);
modules[3] = new GameOverModule(this);
foreach (IGameModule mod in modules)
{
mod.LoadContent(Content);
}
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
TouchCollection touchInput = TouchPanel.GetState();
TouchLocation touch = new TouchLocation();
if (touchInput.Count > 0)
{
touch = touchInput[0];
oldTouch = touch;
}
if (gameState != oldState)
{
oldState = gameState;
modules[(int)gameState].Reset();
}
//update current module
modules[(int)gameState].Update(touch ,gameTime);
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(backColor);
spriteBatch.Begin(SpriteSortMode.Deferred,
BlendState.AlphaBlend);
//draw current module
modules[(int)gameState].Draw(gameTime);
spriteBatch.End();
base.Draw(gameTime);
}
public bool BoundaryCollision(Rectangle A, Rectangle B)
{
return A.Intersects(B);
}
public bool RadialCollision(Sprite A, Sprite B)
{
float radius1 = A.image.Width / 2;
float radius2 = B.image.Width / 2;
return RadialCollision(A.position, B.position, radius1,
radius2);
}
public bool RadialCollision(Vector2 A, Vector2 B, float radius1,
float radius2)
{
float dist = Distance(A, B);
return (dist < radius1 + radius2);
}
public float Distance(Vector2 A, Vector2 B)
{
double diffX = A.X – B.X;
double diffY = A.Y – B.Y;
double dist = Math.Sqrt(Math.Pow(diffX, 2) +
Math.Pow(diffY, 2));
return (float)dist;
}
public float TargetAngle(Vector2 p1, Vector2 p2)
{
return TargetAngle(p1.X, p1.Y, p2.X, p2.Y);
}
public float TargetAngle(double x1, double y1, double x2,
double y2)
{
double deltaX = (x2 – x1);
double deltaY = (y2 – y1);
return (float)Math.Atan2(deltaY, deltaX);
}
}
[/code]

MassiveObject Class

In an earlier hour, we introduced the new MassiveObject class to make working with “gravity” calculations a bit easier than using global variables (or worse, having to modify Sprite). So, this class just inherits from Sprite and adds a few new tidbits of its own. Well, the class is now a full-blown entity that draws and updates, so we need to take a look at the changes. Listing 24.6 shows the source code for the class.

LISTING 24.6 Source Code for the MassiveObject Class

[code]
class MassiveObject : Sprite
{
public string name;
public double mass;
public Vector2 acceleration;
public float radius,angle;
public bool captured;
public int lifetime, startTime;
public MassiveObject(ContentManager content,
SpriteBatch spriteBatch)
: base(content, spriteBatch)
{
name = “object”;
mass = 1.0f;
acceleration = Vector2.Zero;
radius = 50.0f;
angle = 0.0f;
this.captured = false;
lifetime = 0;
startTime = 0;
}
public void Update(GameTime gameTime)
{
position.X += velocityLinear.X;
position.Y += velocityLinear.Y;
}
public override void Draw()
{
base.Draw();
}
public void Attract(MassiveObject other)
{
//calculate DISTANCE
double distX = this.position.X – other.position.X;
double distY = this.position.Y – other.position.Y;
double dist = distX*distX + distY*distY;
double distance = 0;
if (dist != 0.0) distance = 1 / dist;
//update ACCELERATION (mass * distance)
this.acceleration.X = (float)(-1 * other.mass * distX
* distance);
this.acceleration.Y = (float)(-1 * other.mass * distY
* distance);
//update VELOCITY
this.velocityLinear.X += this.acceleration.X;
this.velocityLinear.Y += this.acceleration.Y;
//update POSITION
this.position.X += this.velocityLinear.X;
this.position.Y += this.velocityLinear.Y;
}
}
[/code]

PlayingModule Class

Now we come to the primary gameplay class of the game, PlayingModule. There’s quite a bit of code here, so I won’t break it up; it will just be listed without interruption. The gameplay could use some fine-tuning and tweaking, but the gist of it is that the player is the captain of a spaceship (insert filler story here). The ship has been caught in the strong gravity field of a black hole and you must help it to escape. Or the ship is mining the black hole for energy and gets caught. Whatever the story, it’s a compelling little game that’s worth your time to study.

As for the logic, when the energy level goes below 50, the ship begins to rotate around the black hole. When the energy reaches 0, the ship begins to also lose its orbit and slowly move inward, closer and closer to the event horizon. The player helps the ship stay out of the black hole by launching satellites/probes to collect energy. When the satellites are caught by the ship again, they will have gathered energy from the radiation field surrounding the black hole. If the ship has at least 1 or more energy, it will begin moving outward away from the black hole. When at least 50 energy is accumulated again, the ship will stop rotating. Remaining fixed in one position is definitely the preferred way to play, since you can launch satellites into a “known good” angle where the return will maintain your energy. That’s it! Short and sweet.

Figure 24.5 shows the game running. The source code to the class is found in Listing 24.7.

The finished Black Hole game.
FIGURE 24.5 The finished Black Hole game.

The black hole simulated in this game uses a mass factor of only 100 total (between the blackHole and superCore objects), which is only 100 times more massive than the asteroids and satellites. It’s this way for gameplay, but in the real universe, a typical black hole will be much more massive than a typical star. Plus, there is a supermassive black hole at the center of the Milky Way galaxy with a mass 4 million times greater than that of the Sun!

LISTING 24.7 Source Code for the PlayingModule Class

[code]
public class PlayingModule : IGameModule
{
Game1 game;
Button btnFire, btnQuit;
Label lblAngle, lblPower;
HSlider hsAngle, hsPower;
Random rand;
Sprite background;
MassiveObject blackHole;
MassiveObject superCore;
MassiveObject ship;
float energy;
int startTime, lifetime;
MySoundEffect launchSound;
int lastLaunch, launchTime;
List<MassiveObject> objects;
public PlayingModule(Game1 game)
{
this.game = game;
rand = new Random();
objects = new List<MassiveObject>();
ship = new MassiveObject(game.Content,game.spriteBatch);
blackHole = new MassiveObject(game.Content,game.spriteBatch);
superCore = new MassiveObject(game.Content,game.spriteBatch);
hsAngle = new HSlider(game.Content, game.spriteBatch,game.guifont);
lblAngle = new Label(game.Content,game.spriteBatch,game.guifont);
hsPower = new HSlider(game.Content,game.spriteBatch,game.guifont);
lblPower = new Label(game.Content,game.spriteBatch,game.guifont);
btnFire = new Button(game.Content,game.spriteBatch,game.guifont);
btnQuit = new Button(game.Content,game.spriteBatch,game.guifont);
Reset();
}
public void Reset()
{
startTime = 0;
lifetime = 4000;
lastLaunch = 0;
launchTime = 2000;
energy = 100.0f;
ship.position = new Vector2(200, 240);
hsAngle.Value = 30;
hsPower.Value = 50;
ship.lifetime = 0;
ship.radius = 250;
ship.angle = MathHelper.ToRadians(180);
ship.rotation = MathHelper.ToRadians(90);
objects.Clear();
}
public void LoadContent(ContentManager content)
{
launchSound = new MySoundEffect(game);
launchSound.Load(“launch”);
background = new Sprite(game.Content, game.spriteBatch);
background.Load(“space”);
background.origin = Vector2.Zero;
blackHole.Load(“blackhole”);
blackHole.position = new Vector2(400, 240);
blackHole.scale = 2.0f;
blackHole.mass = 40;
blackHole.color = new Color(255, 100, 100, 210);
blackHole.velocityAngular = 0.1f;
superCore.image = blackHole.image;
superCore.position = new Vector2(blackHole.position.X,
blackHole.position.Y);
superCore.scale = blackHole.scale * 0.4f;
superCore.mass = 60;
superCore.color = new Color(200, 100, 100, 190);
superCore.velocityAngular = 4.0f;
superCore.origin = new Vector2(64, 64);
//player ship
ship.Load(“ship”);
ship.mass = 20f;
ship.scale = 0.2f;
//angle slider
hsAngle.SetStartPosition(new Vector2(170, 445));
hsAngle.color = Color.Orange;
hsAngle.Limit = 108;
//angle label
lblAngle.position = new Vector2(hsAngle.X, hsAngle.Y-40);
lblAngle.text = “ANGLE”;
//power slider
hsPower.SetStartPosition(new Vector2(530, 445));
hsPower.color = Color.Orange;
hsPower.Limit = 100;
//power label
lblPower.position = new Vector2(hsPower.X, hsPower.Y-40);
lblPower.text = “POWER”;
//fire button
btnFire.position = new Vector2(400, 440);
btnFire.color = Color.Orange;
btnFire.UseShadow = false;
btnFire.text = “LAUNCH”;
btnFire.scaleV = new Vector2(1.5f, 0.7f);
//quit button
btnQuit.text = “X”;
btnQuit.position = new Vector2(800 – 20, 480 – 20);
btnQuit.scaleV = new Vector2(0.3f, 0.5f);
}
public void Update(TouchLocation touch, GameTime gameTime)
{
//update user controls
btnFire.Update(touch);
hsAngle.Update(touch);
hsPower.Update(touch);
lblAngle.Update(touch);
lblPower.Update(touch);
btnQuit.Update(touch);
//update gameplay objects
blackHole.Update(gameTime);
blackHole.Rotate();
superCore.Update(gameTime);
superCore.Rotate();
UpdateObjects(gameTime);
UpdateShip(gameTime);
//check user input
if (btnFire.Tapped)
{
if (lastLaunch + launchTime < gameTime.TotalGameTime.
TotalMilliseconds)
{
lastLaunch = (int)gameTime.TotalGameTime.
TotalMilliseconds;
launchSound.Play();
CreateSatellite();
}
}
//rotate ship with slider
float angle = hsAngle.Value * 3.3f;
ship.rotation = MathHelper.ToRadians(angle);
lblAngle.text = “ANGLE:” + angle.ToString(“N0”);
//set power label
lblPower.text = “POWER:” + hsPower.Value.ToString();
//time to add another random asteroid?
if (startTime + lifetime < gameTime.TotalGameTime.
TotalMilliseconds)
{
startTime = (int)gameTime.TotalGameTime.TotalMilliseconds;
CreateAsteroid();
}
//user quit?
if (btnQuit.Tapped)
{
game.gameState = Game1.GameState.TITLE;
}
}
public void UpdateObjects(GameTime gameTime)
{
int time = gameTime.ElapsedGameTime.Milliseconds;
foreach (MassiveObject obj in objects)
{
if (!obj.alive) continue;
obj.Update(gameTime);
obj.Rotate();
obj.Animate(time);
obj.Animate();
//allow ship to collect energy satellite
if (obj.scale >= 1.0f && obj.name == “satellite”)
{
//when large, cause satellites to seek the ship
obj.Attract(ship);
//reset satellite to white when seeking ship
obj.color = Color.White;
//look for collision with ship
if (game.RadialCollision(obj.position, ship.position,
obj.size.X, 40))
{
obj.alive = false;
energy += obj.scale * 4;
if (energy > 200) energy = 200;
}
}
if (!obj.captured)
{
//only attract when object is near the black hole
if (game.RadialCollision(obj.position,
blackHole.position, obj.size.X, 500))
{
obj.Attract(blackHole);
//touching the outer edges of the black hole?
if (game.RadialCollision(obj.position,
blackHole.position, obj.size.X, 120))
{
//turn red when going through inner gravity well
obj.color = Color.Red;
if (obj.name == “satellite”)
{
obj.scale += 0.1f;
if (obj.scale > 5.0f) obj.scale = 5.0f;
energy += 0.1f;
if (energy > 200) energy = 200;
}
obj.Attract(superCore);
//object is caught by the black hole
if (game.RadialCollision(obj.position,
superCore.position, 16, 60))
{
obj.captured = true;
//set a lifetime delay once captured
obj.lifetime = 3000;
obj.startTime = (int)gameTime.TotalGameTime.
TotalMilliseconds;
//cause object to spin around the black hole
OrbitalMovement anim1 = new OrbitalMovement(
blackHole.position, 10 + rand.Next(40),
obj.rotation, -0.8f);
obj.animations.Add(anim1);
}
}
else
{
obj.color = Color.White;
}
}
}
//when captured, time runs out
if (obj.lifetime > 0)
{
if (obj.startTime + obj.lifetime < gameTime.
TotalGameTime.TotalMilliseconds)
obj.alive = false;
}
//see if object has gone too far out of bounds
if (obj.position.X < -200 || obj.position.X > 1000 ||
obj.position.Y < -200 || obj.position.Y > 700)
obj.alive = false;
}
}
public void UpdateShip(GameTime gameTime)
{
int time = gameTime.ElapsedGameTime.Milliseconds;
ship.Update(gameTime);
ship.Rotate();
ship.Animate();
//cause ship to fall into black hole
if (!ship.captured)
{
//update ship position
ship.X = 400 + (float)(Math.Cos(ship.angle) * ship.radius);
ship.Y = 240 + (float)(Math.Sin(ship.angle) * ship.radius);
//consume energy
energy -= 0.05f;
if (energy > 0)
{
//while we have energy, try to get away
ship.radius += 0.2f;
if (ship.radius > 250)
ship.radius = 250;
}
if (energy < 50)
{
//rotate ship around black hole (custom)
ship.angle += 0.01f;
if (energy <= 0)
{
energy = 0;
ship.radius -= 0.1f;
//ship is caught by the black hole
if (game.RadialCollision(ship.position,
superCore.position, 64, 40))
{
ship.captured = true;
ship.velocityAngular = 1.0f;
ship.lifetime = 5000;
ship.startTime = (int)gameTime.TotalGameTime.
TotalMilliseconds;
OrbitalMovement anim1 = new OrbitalMovement(
blackHole.position, 10 + rand.Next(40),
0, 0.4f);
ship.animations.Add(anim1);
}
}
}
}
//ship fell into black hole?
if (ship.lifetime > 0)
{
if (ship.startTime + ship.lifetime <
gameTime.TotalGameTime.TotalMilliseconds)
{
game.gameState = Game1.GameState.GAMEOVER;
return;
}
}
}
public void Draw(GameTime gameTime)
{
background.Draw();
superCore.Draw();
blackHole.Draw();
ship.Draw();
foreach (MassiveObject obj in objects)
{
if (obj.alive)
{
obj.Draw();
}
}
btnFire.Draw();
hsAngle.Draw();
hsPower.Draw();
lblAngle.Draw();
lblPower.Draw();
btnQuit.Draw();
string text;
text = “Energy “ + energy.ToString(“N0”);
game.spriteBatch.DrawString(game.font, text,
new Vector2(650, 0), Color.White);
}
public void CreateAsteroid()
{
MassiveObject obj = new MassiveObject(game.Content,
game.spriteBatch);
obj.Load(“asteroid”);
obj.columns = 8;
obj.totalFrames = 64;
obj.scale = 0.1f + (float)rand.NextDouble();
obj.size = new Vector2(60, 60);
obj.radius = 80;
//randomly place at top or bottom of screen
obj.position = new Vector2(rand.Next(100, 800), -100);
obj.velocityLinear = new Vector2(4.0f, (float)
(rand.NextDouble() * 6.0));
if (rand.Next(2) == 1)
{
obj.position.Y = -obj.position.Y;
obj.velocityLinear.Y = -obj.velocityLinear.Y;
}
obj.scale = (0.5f + (float)rand.NextDouble()) * 0.5f;
obj.mass = 1;
obj.velocityAngular = 0.001f;
obj.lifetime = 0;
obj.name = “asteroid”;
objects.Add(obj);
}
public void CreateSatellite()
{
MassiveObject obj;
obj = new MassiveObject(game.Content, game.spriteBatch);
obj.Load(“plasma32”);
obj.position = ship.position;
obj.mass = 1;
obj.scale = 0.5f;
obj.lifetime = 0;
obj.name = “satellite”;
//calculate velocity based on ship’s angle
float accel = 1 + (float)(hsPower.Value / 10);
float angle = ship.rotation – MathHelper.ToRadians(90);
float x = (float)Math.Cos(angle) * accel;
float y = (float)Math.Sin(angle) * accel;
obj.velocityLinear = new Vector2(x,y);
//use energy to launch satellite
energy -= 1;
objects.Add(obj);
}
}
[/code]

This concludes the Black Hole game, and, well, the whole book! I hope you have enjoyed working with the WP7 platform, the emulator, and XNA Game Studio. These tools really are very rewarding, and you can’t beat the price! If you have any questions or just want to chat, come visit my website at http://www.jharbour.com/ forum. See you there!

Rocket Science: Acceleration

Building the Game

The Black Hole game is based on most of the code we’ve developed in each of the previous hours of the book, and there really is no single previous hour that gets more credit than others since each hour has built upon the hours that came before. Let’s dig into the game by going over the major sections and get something up and running fairly quickly. Then we’ll continue our work on the game into the following hour, where it will get some polish and fine-tuning of the fun factor.

This game is based on the theories of Stephen Hawking. If you’re interested in black hole physics, be sure to read his popular books for more information! The Universe in a Nutshell is one of my favorites.

Gravity Well Regions

There are three regions around the black hole that affect game objects. The outer gravity well affects objects passing by, drawing them toward the black hole with relatively light force. This force is increased by an equal amount in the next two inner regions, with each region generating an equivalent gravity “tug” on objects. But the cumulative effect of all three gravity wells in the inner region of the black hole will cause objects to become hopelessly trapped.

The third and innermost region might be thought of as the event horizon, that region of a black hole where things disappear into the void, never to be seen again. It is this region that mathematics cannot penetrate, so while it appears that gravity increases toward infinity in the middle of a black hole, the truth of the matter is, there may be nothing at the very center of a black hole! The gravity might be so powerful that matter just rotates around the center of mass and no matter actually exists at that center point, which would be quite small at the center of a black hole. Then again, there might be a superdense material like a neutron star. It is at this point that physics just cannot explain it, because we don’t actually have a black hole nearby to study. Even if we did, it’s not like a spacecraft could be sent to investigate!

Figure 23.1 shows an illustration of the gravity well of the black hole in the game. The outer gravity well is quite large and draws most game objects inward at a steady “weight” or “pull,” with some help from the inner core of the black hole, also exerting force. The inner gravity well is the region where energy can be mined by the “Hawking” satellites. At any rate, that’s one of the enjoyable aspects of this game, pondering the infinite possibilities!

The code to simulate the gravitational pull of the black hole is coming up. Now let’s just take a look again at some of our earlier helper methods used in this game. All the collision code in the Black Hole game is based around this RadialCollision() method and its overloaded friend:

[code]
public bool RadialCollision(Sprite A, Sprite B)
{
float radius1 = A.image.Width / 2;
float radius2 = B.image.Width / 2;
return RadialCollision(A.position, B.position,
radius1, radius2);
}
public bool RadialCollision(Vector2 A, Vector2 B, float radius1,
float radius2)
{
float dist = Distance(A, B);
return (dist < radius1 + radius2);
}
public float Distance(Vector2 A, Vector2 B)
{
double diffX = A.X – B.X;
double diffY = A.Y – B.Y;
double dist = Math.Sqrt(Math.Pow(diffX, 2) +
Math.Pow(diffY, 2));
return (float)dist;
}
[/code]

The gravity well of the black hole covers most of the Windows Phone screen.
FIGURE 23.1 The gravity well of the black hole covers most of the Windows Phone screen.

Enhancing MassiveObject

Some minor changes need to be made to MassiveObject to support some new features needed in the game that were not in the example in the preceding hour. Following is what the class now looks like, with the new variables and updated constructor:

[code]
class MassiveObject : Sprite
{
public string name;
public bool captured;
public double mass;
public Vector2 acceleration;
public float radius, angle;
public int lifetime, startTime;
public MassiveObject(ContentManager content,
SpriteBatch spriteBatch)
: base(content, spriteBatch)
{
name = “object”;
mass = 1.0f;
acceleration = Vector2.Zero;
radius = 50.0f;
angle = 0.0f;
captured = false;
lifetime = 0;
startTime = 0;
}
// . . . note: some code omitted here
}
[/code]

Game1.cs

There are no changes to be made to Game1.cs, because the main source code file is now PlayingModule.cs. In the final hour coming up, we will again use the game state modules for a more polished gameplay experience.

Gameplay Source Code

The most significant code of the game is found in PlayingModule.cs. If you skipped ahead, you may have missed Hour 21, “Finite State Gameplay,” which explained how to use states to improve a game in many ways. The PlayingModule class is the primary gameplay class where the bulk of the game code will be found. The first lines of code in the class declare all the variables, including the key objects variable, defined as a List of MassiveObjects. We also see the black hole, the super core gravity well, and the player’s ship here, among other things. Figure 23.2 shows the game as it is just getting started, and Listing 23.1 shows the source code for the class.

The Black Hole game soon after startup.
FIGURE 23.2 The Black Hole game soon after startup.

LISTING 23.1 Source Code for the PlayingModule Class

[code]
public class PlayingModule : IGameModule
{
Game1 game;
SpriteFont font;
Random rand;
Sprite background;
MassiveObject blackHole;
MassiveObject superCore;
MassiveObject ship;
float energy = 100.0f;
int startTime, lifetime;
List<MassiveObject> objects;
public PlayingModule(Game1 game)
{
this.game = game;
rand = new Random();
startTime = 0;
lifetime = 4000;
}
public void LoadContent(ContentManager content)
{
font = content.Load<SpriteFont>(“WascoSans”);
background = new Sprite(game.Content, game.spriteBatch);
background.Load(“space”);
background.origin = Vector2.Zero;
blackHole = new MassiveObject(game.Content, game.spriteBatch);
blackHole.Load(“blackhole”);
blackHole.position = new Vector2(500, 240);
blackHole.scale = 2.0f;
blackHole.mass = 40;
blackHole.color = new Color(255, 100, 100, 200);
blackHole.velocityAngular = 0.1f;
superCore = new MassiveObject(game.Content, game.spriteBatch);
superCore.image = blackHole.image;
superCore.position = new Vector2(blackHole.position.X,
blackHole.position.Y);
superCore.scale = blackHole.scale * 0.4f;
superCore.mass = 60;
superCore.color = new Color(200, 100, 100, 180);
superCore.velocityAngular = 4.0f;
superCore.origin = new Vector2(64, 64);
//create objects list
objects = new List<MassiveObject>();
//create player ship
ship = new MassiveObject(game.Content, game.spriteBatch);
ship.Load(“ship”);
ship.position = new Vector2(200, 240);
ship.mass = 100f;
ship.scale = 0.2f;
ship.rotation = MathHelper.ToRadians(90);
}
[/code]

The Update() method is a bit monolithic at this stage, but the code is easier to follow this way than if it had been divided into several smaller methods. I usually divide a method like this when it grows too large to be easily maintained, but since the gameplay code in PlayingModule is only 300 lines long, there isn’t too much to consume here at once. There’s a lot going on here in Update(), but we won’t break up the code listing and break the flow of the code, which can be distracting.

First of all, the blackHole and superCore objects are updated. Then we go into a foreach loop that processes all the MassiveObject objects in the objects list (there’s a tongue twister!). Each object is updated, rotated, and animated. Within the foreach is where the bulk of the code is found for the game.

When one of the satellites grows to a certain size (from collecting energy), that triggers a subset of code here in the foreach block where the player’s ship actually attracts the satellite toward it, using the same code used to simulate the gravitational pull of the black hole on the same objects. The slight gravity “tug” causes the satellites to veer toward the ship and increase the chances of their being caught by it, without making it too easy for the player. After all, the ship doesn’t move yet, it only rotates in place!

Next up is the code that tugs objects inward toward the black hole, if they are in range. Figure 23.3 shows another screenshot of the game, this time with a lot of satellites in orbit. Note the addition of animated asteroids in the scene. The asteroids serve no purpose, but just fill in some detail to make the scene look more interesting. A new asteroid is added every few seconds at a random direction and velocity, and over time they do tend to add up to quite a large mass of rotation around the black hole, which only increases the fun factor. Now, there is also potential use for these asteroid sprites beyond just “for looks.”

A large number of objects are orbiting the black hole, and they tend to fall in quite frequently.
FIGURE 23.3 A large number of objects are orbiting the black hole, and they tend to fall in quite frequently.

Listing 23.2 contains the source code for the Update() method.

It’s quite a challenge to come up with mass values for the black hole, the super core, and each of the objects that not only result in a realistic simulation of gravity’s effect on objects of mass but also make the game fun. Fun is more important than realism, but we want to have a little of both if possible. But when a trade-off is required, always go with that which helps the game to sell: the fun factor.

LISTING 23.2 Source Code for the Update() Method

[code]
public void Update(TouchLocation touch, GameTime gameTime)
{
int time = gameTime.ElapsedGameTime.Milliseconds;
blackHole.Update(gameTime);
blackHole.Rotate();
superCore.Update(gameTime);
superCore.Rotate();
foreach (MassiveObject obj in objects)
{
if (!obj.alive) continue;
obj.Update(gameTime);
obj.Rotate();
obj.Animate(time); //frame animation
obj.Animate(); //mod animation
//allow ship to collect energy satellites for bonus energy
if (obj.scale > 3.0f && obj.name == “satellite”)
{
obj.Attract(ship);
obj.color = Color.White;
if (game.RadialCollision(obj.position, ship.position,
obj.size.X, 40))
{
obj.alive = false;
energy += obj.scale;
}
}
if (!obj.captured)
{
//attract when object is near the black hole
if (game.RadialCollision(obj.position,
blackHole.position, 10, 500))
{
obj.Attract(blackHole);
obj.Attract(superCore);
//is object touching the outer edges of the black hole?
if (game.RadialCollision(obj.position,
blackHole.position, 10, 120))
{
obj.color = Color.Red;
if (obj.name == “satellite”)
{
obj.scale += 0.1f;
energy += 0.5f;
}
obj.Attract(blackHole); //outer black hole
obj.Attract(superCore); //inner black hole
//oh no, object is caught by the black hole!
if (game.RadialCollision(obj.position,
superCore.position, 16, 60))
{
obj.captured = true;
obj.lifetime = 3000;
obj.startTime = (int)
gameTime.TotalGameTime.TotalMilliseconds;
OrbitalMovement anim1 = new OrbitalMovement(
blackHole.position, 10 + rand.Next(40),
obj.rotation, -0.8f);
obj.animations.Add(anim1);
}
}
else
{
obj.color = Color.White;
}
}
}
//when captured, time runs out
if (obj.lifetime > 0)
{
if (obj.startTime + obj.lifetime <
gameTime.TotalGameTime.TotalMilliseconds)
obj.alive = false;
}
//see if object has gone too far out of bounds
if (obj.position.X < -200 || obj.position.X > 1000 ||
obj.position.Y < -200 || obj.position.Y > 700)
obj.alive = false;
}
//update ship
ship.Update(gameTime);
ship.Rotate();
ship.Animate(time);
if (energy <= 0)
{
ship.Attract(blackHole);
//object is caught by the black hole
if (game.RadialCollision(ship.position, superCore.position,
64, 40))
{
ship.captured = true;
ship.lifetime = 3000;
ship.startTime = (int)
gameTime.TotalGameTime.TotalMilliseconds;
OrbitalMovement anim1 = new OrbitalMovement(
blackHole.position, 10 + rand.Next(40),
ship.rotation, -0.8f);
ship.animations.Add(anim1);
}
//done being squished?
if (ship.lifetime > 0)
{
if (ship.startTime + ship.lifetime <
gameTime.TotalGameTime.TotalMilliseconds)
ship.alive = false;
}
}
else
{
energy -= 0.05f;
ship.velocityLinear.X = 0.0f;
}
//check user input
if (touch.State == TouchLocationState.Released)
{
if (touch.Position.X > ship.position.X)
{
CreateSatellite();
}
else
{
if (touch.Position.Y < 200)
ship.velocityAngular = -0.01f;
else if (touch.Position.Y > 280)
ship.velocityAngular = 0.01f;
else
ship.velocityAngular = 0;
}
}
//time to add another random asteroid?
if (startTime + lifetime < gameTime.TotalGameTime.
TotalMilliseconds)
{
startTime = (int)gameTime.TotalGameTime.TotalMilliseconds;
CreateAsteroid();
}
//clean out the dead objects
foreach (MassiveObject obj in objects)
{
if (obj.alive == false)
{
objects.Remove(obj);
break;
}
}
}
[/code]

The Draw() method is next, with its source code in Listing 23.3. This is a rather small method because the gameplay objects are managed.

LISTING 23.3 Source Code for the Draw() Method

[code]
public void Draw(GameTime gameTime)
{
background.Draw();
superCore.Draw();
blackHole.Draw();
ship.Draw();
foreach (MassiveObject obj in objects)
{
if (obj.alive)
{
obj.Draw();
}
}
string text = “Ship rot “ + MathHelper.ToDegrees(
ship.rotation).ToString();
game.spriteBatch.DrawString(font, text, new Vector2(0, 0),
Color.White);
text = “Objects “ + objects.Count.ToString();
game.spriteBatch.DrawString(font, text, new Vector2(0, 20),
Color.White);
text = “Energy “ + energy.ToString(“N0”);
game.spriteBatch.DrawString(font, text, new Vector2(650, 0),
Color.White);
}
[/code]

Finally, we have two helper methods, CreateAsteroid() and CreateSatellite(), that generate a random asteroid and random satellite, respectively. These two methods, shown in Listing 23.4, are quite important to the gameplay because they determine whether the objects will actually move reasonably on the screen. I say reasonably rather than realistically because, again, we don’t want absolute realism; we want some realism with gobs of fun gameplay. The asteroids aren’t important to the gameplay, because they are just for looks, but we do want them to start off in such a way that they end up rotating around the black hole. Likewise, the satellite must be launched in such a way that it moves reasonably well. At this stage, our satellites move at a constant speed, but in the next (and final) hour, we will add GUI controls that allow the player to adjust the power.

LISTING 23.4 Source Code for the CreateAsteroid() and CreateSatellite() Methods

[code]
public void CreateAsteroid()
{
MassiveObject obj = new MassiveObject(game.Content, game.spriteBatch);
obj.Load(“asteroid”);
obj.columns = 8;
obj.totalFrames = 64;
obj.scale = 0.1f + (float)rand.NextDouble();
obj.size = new Vector2(60, 60);
obj.radius = 80;
//randomly place at top or bottom of screen
obj.position = new Vector2(rand.Next(100, 800), -100);
obj.velocityLinear = new Vector2(4.0f, (float)(rand.NextDouble() *
6.0));
if (rand.Next(2) == 1)
{
obj.position.Y = -obj.position.Y;
obj.velocityLinear.Y = -obj.velocityLinear.Y;
}
obj.scale = 0.4f;
obj.mass = 1;
obj.velocityAngular = 0.001f;
obj.lifetime = 0;
obj.name = “asteroid”;
objects.Add(obj);
}
public void CreateSatellite()
{
MassiveObject obj;
obj = new MassiveObject(game.Content, game.spriteBatch);
obj.Load(“plasma32”);
obj.position = ship.position;
obj.mass = 1;
obj.scale = 0.5f;
obj.lifetime = 0;
obj.name = “satellite”;
//calculate velocity based on ship’s angle
float accel = 4.0f;
float angle = ship.rotation – MathHelper.ToRadians(90);
float x = (float)Math.Cos(angle) * accel;
float y = (float)Math.Sin(angle) * accel;
obj.velocityLinear = new Vector2(x,y);
//load energy to launch
energy -= 1;
objects.Add(obj);
}
}
[/code]
This all sounds like fun, but is there even a way to lose the game? Certainly! If the player runs out of energy, the ship will fall into the black hole! At this stage, the ship just loses its “traction” or station-keeping thrusters and is drawn into the black hole, only to be whipped around by the acceleration code. Some sort of fantastic animation will have to be added so that the ship gets sucked into the black hole like the other objects—a task for the next hour! Figure 23.4 shows what happens now if the player runs out of energy. Another improvement to be made in the next hour is an energy bar rather than just a text display. We have a lot of work yet to do on this game, but it’s already showing promise.

Running out of energy spells doom for the poor ship and its crew!
FIGURE 23.4 Running out of energy spells doom for the poor ship and its crew!

The Physics of Gravity

Simulating Gravity

Gravity is an interesting phenomenon of physics. Every moment of every day, you are literally falling inward toward the center of the earth. The surface of the earth keeps you from falling farther, but the same force that causes a person parachuting out of an airplane to fall downward, which is the same force that causes satellites in orbit to gradually lose position and require constant adjustments, causes you to fall downward at every moment. It actually takes quite a bit of energy to stand up against the force of the earth’s gravity. Fortunately, a human body is relatively small, so the gravity exerted is not too great that we can’t survive. But try to imagine, the same gravity pulling you toward the center of the earth also keeps the moon in orbit! The moon is gradually losing its orbit, by the way. Over time, it will draw closer and closer to the earth and eventually collide. The time frame is huge, but it is happening nonetheless.

Escape Velocity

To break out of the earth’s gravity field requires a huge amount of thrust! The velocity required to do so is called escape velocity, which is the velocity required to escape the gravity of a planet or another large, massive object. Gravity, according to physics, is called gravitational potential energy. The term escape velocity is somewhat incorrect, but the term has stuck over the years. Velocity, as you have learned in this book, affects movement in a specific direction calculated with cosine (for X) and sine (for Y). Escape velocity is actually a speed, not a velocity, because an object moving at escape speed will break orbit no matter which direction it is moving in.

Earth’s escape velocity is 6.96 miles per second (11.2 kilometers per second). In terms of aircraft speed, that is Mach 34, about 10 times faster than a bullet! But these terms are applicable only to ballistic objects, like a rocket launched from the surface. Ballistic is a term that means something is fired or launched and then the object coasts once it reaches a desired speed. The ballistic rocket that launched the Apollo astronauts toward the moon had to reach escape velocity with two or more rocket stages, plus an outer space thruster that sent the spaceship coasting toward the moon. But a spacecraft lifting off from the earth does not need to reach this ballistic escape velocity if it can just maintain a consistent thrust for a longer period. A U.S. Air Force F-22 Raptor jet, for instance, has enough thrust to go ballistic in terms of its own weight (meaning it can continue to speed up while going straight up, without ever slowing down), with fuel being the only limitation.

Calculating “Gravity”

This is not really “gravity” we’re calculating, but rather the force two objects exert on each other. The result looks very much like the pull of gravity. Just note that the code we’re about to write is simplified for a gameplay mechanic, not for spacecraft trajectories.

The formulas involved in “rocket science” are not overly complicated, but we’re going to use a simpler technique to simulate gravity between any two massive objects. The end result will be similar as far as a game is concerned. What we need to be concerned with is the mass, position, and acceleration factor for each object.

Using these three basic pieces of information, we can cause two massive objects to affect each other gravitationally. During every update, the position, acceleration, and velocity are all updated. While the two objects are far apart, their interaction will be negligible. But as they draw closer, the acceleration factor will affect the velocity, which will cause the two objects to speed up toward each other. In the terms of rocket science, this is called “the whiplash effect.” NASA and other space agencies often use whiplash gravity to propel their spacecraft toward a destination more quickly, costing less fuel as a result. After we have a simulation running, we can try this out!

To cause two massive objects to interact, first we have to calculate the distance between them. This will have a direct effect on the amount of force the objects exert upon each other. This code takes into account the situation in which the two objects have collided, in which case the distance factor is inverted:

[code]
double distX = this.position.X – other.position.X;
double distY = this.position.Y – other.position.Y;
double dist = distX*distX + distY*distY;
double distance = 0;
if (dist != 0) distance = 1 / dist;
[/code]

Next, we use these distance values to calculate the acceleration of the object. Since this is directly affected by the distance to the other object, the acceleration will increase as the objects grow closer, which further increases acceleration. That is the nature of the whiplash effect, and it works well as long as the objects do not collide:

[code]
this.acceleration.X = (float)(-1 * other.mass * distX * distance);
this.acceleration.Y = (float)(-1 * other.mass * distY * distance);
[/code]

Velocity is updated directly with the acceleration values:

[code]
this.velocityLinear.X += this.acceleration.X;
this.velocityLinear.Y += this.acceleration.Y;
[/code]

The position, likewise, is updated directly with the velocity values:

[code]
this.position.X += this.velocityLinear.X;
this.position.Y += this.velocityLinear.Y;
[/code]

It is very easy to lose track of game objects that interact with this gravitational code. If two objects collide, so that the distance between them is zero, then they will fling away from each other at high speed. I recommend adding boundary code to the velocity and acceleration values so that this doesn’t happen in a playable game.

The Gravity Demo

The sample project can be found again under the name “Black Hole Game” in this hour’s resource files. Because all the concepts and code learned during these last remaining chapters is directly applied to the sample game in the final hour, the project will just grow and evolve, and we’ll continue to use the same name, as well as use code developed during previous hours. The example will be based on the game state example in the preceding hour, and we’ll use the PlayingModule.cs file rather than Game1.cs as has been the norm previously. Figure 22.1 shows the example from this hour running.

The “plasma” sprite is rotating around the black hole in an elliptical orbit.
FIGURE 22.1 The “plasma” sprite is rotating around the black hole in an elliptical orbit.

This program requires several bitmap files. You can copy them directly out of the project included in the book resource files, or create your own. For the “black hole” image, I have just created a filled black circle with transparency around the outside. The “plasma” image is an alpha blended particle that I’ve had for quite a while, and have used as a weapon projectile in some past games. The asteroid in this example is just window dressing, included to show how an object with an OrbitalMovement animation looks compared to the calculated trajectories of a MassiveObject object. To get a head start on the gameplay that will be needed for this game, I have added some code to cause the orbiting power satellites (currently represented just as a “plasma” ball) to get sucked into the black hole if they get too close. When this happens, the gravity code is replaced with the OrbitalMovement animation class. In Figure 22.2, we see that the projectile, or “plasma” sprite, has been captured by the black hole.

The “plasma” sprite has been captured by the black hole.
FIGURE 22.2 The “plasma” sprite has been captured by the black hole.

MassiveObject Class

A new class called MassiveObject, which inherits from Sprite, will handle our gravitational needs. Listing 22.1 provides the source code for this new class.

LISTING 22.1 Source Code for the MassiveObject Class

[code]
class MassiveObject : Sprite
{
public double mass;
public Vector2 acceleration;
public float radius,angle;
public bool captured;
public MassiveObject(ContentManager content,
SpriteBatch spriteBatch)
: base(content, spriteBatch)
{
mass = 1.0f;
acceleration = Vector2.Zero;
radius = 50.0f;
angle = 0.0f;
this.captured = false;
}
public void Update(GameTime gameTime)
{
}
public override void Draw()
{
base.Draw();
}
public void Attract(MassiveObject other)
{
//calculate DISTANCE
double distX = this.position.X – other.position.X;
double distY = this.position.Y – other.position.Y;
double dist = distX*distX + distY*distY;
double distance = 0;
if (dist != 0.0) distance = 1 / dist;
//update ACCELERATION (mass * distance)
this.acceleration.X = (float)(-1 * other.mass * distX
* distance);
this.acceleration.Y = (float)(-1 * other.mass * distY
* distance);
//update VELOCITY
this.velocityLinear.X += this.acceleration.X;
this.velocityLinear.Y += this.acceleration.Y;
//update POSITION
this.position.X += this.velocityLinear.X;
this.position.Y += this.velocityLinear.Y;
}
}
[/code]

PlayingModule.cs

This is the main source code file for the Gravity Demo, where the black hole and other MassiveObject (Sprite) objects are created, updated, and drawn. In other words, this is our main source code file. The original game state code is still present in the other files, but that has been removed from PlayingModule.cs. To return to the title screen, it’s still possible to set game.gameMode as before! Listing 22.2 shares the source code for the class.

LISTING 22.2 Source Code for the PlayingModule Class

[code]
public class PlayingModule : IGameModule
{
Game1 game;
SpriteFont font;
Random rand;
MassiveObject blackHole;
MassiveObject asteroid;
MassiveObject plasma;
public PlayingModule(Game1 game)
{
this.game = game;
rand = new Random();
}
public void LoadContent(ContentManager content)
{
font = content.Load<SpriteFont>(“WascoSans”);
blackHole = new MassiveObject(game.Content,
game.spriteBatch);
blackHole.Load(“blackhole”);
blackHole.position = new Vector2(400, 240);
blackHole.velocityAngular = -.05f;
blackHole.scale = 1.0f;
blackHole.mass = 100;
asteroid = new MassiveObject(game.Content,
game.spriteBatch);
asteroid.Load(“asteroid”);
asteroid.columns = 8;
asteroid.totalFrames = 64;
asteroid.size = new Vector2(60, 60);
asteroid.radius = 80;
asteroid.animations.Add(new OrbitalMovement(
new Vector2(400,240), 80, 0, 0.08f));
asteroid.scale = 0.5f;
plasma = new MassiveObject(game.Content,
game.spriteBatch);
plasma.Load(“plasma32”);
plasma.position = new Vector2(200, 240);
plasma.mass = 1;
plasma.velocityLinear = new Vector2(1.0f, 7.0f);
}
public void Update(TouchLocation touch, GameTime gameTime)
{
int time = gameTime.ElapsedGameTime.Milliseconds;
blackHole.Update(gameTime);
blackHole.Rotate();
asteroid.angle += 0.001f;
asteroid.Update(gameTime);
asteroid.Rotate();
asteroid.Animate(time);
asteroid.Animate();
plasma.Update(gameTime);
plasma.Rotate();
plasma.Animate();
if (!plasma.captured)
{
plasma.Attract(blackHole);
if (game.RadialCollision(plasma, blackHole))
{
plasma.captured = true;
OrbitalMovement anim1 = new OrbitalMovement(
blackHole.position, 20 + rand.Next(20),
plasma.rotation, 0.8f);
plasma.animations.Add(anim1);
CycleColorBounce anim2 = new CycleColorBounce(
0, 10, 10, 0);
plasma.animations.Add(anim2);
}
}
}
public void Draw(GameTime gameTime)
{
blackHole.Draw();
asteroid.Draw();
plasma.Draw();
string text = “Position “ +
((int)plasma.position.X).ToString() + “,” +
((int)plasma.position.Y).ToString();
game.spriteBatch.DrawString(font, text,
new Vector2(0, 0), Color.White);
float dist = game.Distance(plasma.position, blackHole.position);
text = “Distance “ + ((int)dist).ToString();
game.spriteBatch.DrawString(font, text,
new Vector2(0, 20), Color.White);
}
}
[/code]

Game1.cs

The source code to Game1.cs has not changed since the example in the preceding hour, but we need some reusable methods in this example added to the Game1.cs file, where they will be more useful. We have seen all of this code before, but just to be thorough, Listing 22.3 contains the complete code for the file with the additions.

LISTING 22.3 Main Source Code for the Example

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
public enum GameState
{
TITLE = 0,
PLAYING = 1,
OPTIONS = 2,
GAMEOVER = 3
}
public GraphicsDeviceManager graphics;
public SpriteBatch spriteBatch;
public GameState gameState;
SpriteFont font;
Random rand;
TouchLocation oldTouch;
IGameModule[] modules;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
oldTouch = new TouchLocation();
rand = new Random();
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
modules = new IGameModule[4];
modules[0] = new TitleScreenModule(this);
modules[1] = new PlayingModule(this);
modules[2] = new OptionsModule(this);
modules[3] = new GameOverModule(this);
foreach (IGameModule mod in modules)
{
mod.LoadContent(Content);
}
gameState = GameState.PLAYING;
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
TouchCollection touchInput = TouchPanel.GetState();
TouchLocation touch = new TouchLocation();
if (touchInput.Count > 0)
{
touch = touchInput[0];
oldTouch = touch;
}
//update current module
modules[(int)gameState].Update(touch ,gameTime);
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin(SpriteSortMode.Deferred,
BlendState.AlphaBlend);
//draw current module
modules[(int)gameState].Draw(gameTime);
spriteBatch.End();
base.Draw(gameTime);
}
public bool BoundaryCollision(Rectangle A, Rectangle B)
{
return A.Intersects(B);
}
public bool RadialCollision(Sprite A, Sprite B)
{
float radius1 = A.image.Width / 2;
float radius2 = B.image.Width / 2;
return RadialCollision(A.position, B.position,
radius1, radius2);
}
public bool RadialCollision(Vector2 A, Vector2 B,
float radius1, float radius2)
{
float dist = Distance(A, B);
return (dist < radius1 + radius2);
}
public float Distance(Vector2 A, Vector2 B)
{
double diffX = A.X – B.X;
double diffY = A.Y – B.Y;
double dist = Math.Sqrt(Math.Pow(diffX, 2) +
Math.Pow(diffY, 2));
return (float)dist;
}
public float TargetAngle(Vector2 p1, Vector2 p2)
{
return TargetAngle(p1.X, p1.Y, p2.X, p2.Y);
}
public float TargetAngle(double x1, double y1, double x2,
double y2)
{
double deltaX = (x2 – x1);
double deltaY = (y2 – y1);
return (float)Math.Atan2(deltaY, deltaX);
}
}
[/code]

Finite State Gameplay

Finite State Gameplay in Theory

Finite state refers to a condition that will have a limited number of possible values. The age of a person is a finite state value range from 0 to around 80, with no exceptions. To ensure that an age does not exceed the bounds, the actual limits must be used, not just the most common limits. So, age really should be 0 to 120, because even today there are a few people still alive who were born in the nineteenth century. Let’s study finite state theory and then write some sample code to see how well it works in a game.

Finite State Theory

The speed of a car is a finite state range from 0 to around 100. But what if we’re dealing with a sports car? In that case, the speed might go up to 200 or more (the Bugatti Veyron has a top speed of 260, but it costs a million dollars). Then, what about top fuel dragsters that can reach speeds in excess of 300 miles per hour? As you can see, the range is not easily determined because there are always exceptions to the rule, challenges for our assumptions. There are even land-speed-record cars that can go faster than these numbers. What we have to do, then, is reduce the scope of the object we’re describing with finite state variables. For instance, we might limit the scope to consumer passenger vehicles that cost less than $40,000, to come up with a safe range of 0 to 120 (as a reasonable upper limit).

Finite states need not be numeric or even purely composed of whole numbers, either. We can have a finite state variable dealing with very small numbers indeed, such as the trace widths of computer chips, measured in billionths of a meter (nanometer), or even non-numeric states. Consider a light switch. It is either on or off. There is no in-between value because to hold the switch in between still causes the light to be either on or off. There is no “somewhat on” or “somewhat off” in a two-state switch.

Enumerations can also be used to describe the possible states of an object. For instance, the condition of a food item at a grocery store might have these possible values: Fresh, Good, Aged, or Spoiled. It is the job of the store employees to keep track of the state of their food items. Usually, when the state of, say, a banana goes from Fresh to Good, the price will be discounted. When the state degrades from Good to Aged, the price will be reduced further for perhaps one final day, and then if it hasn’t sold, it is discarded.

We humans categorize items all day long. That’s a very significant way that our brains work. We are surprised upon encountering a new thing, which can bring on emotional excitement or intrigue! Have you ever noticed that when you find something new, you often feel like a child again? For children, everything in the world is a new experience almost every day, which is what made childhood so much fun for most people (all things being equal). When something new or weird is discovered, the first thing we do is try to categorize it. “Hey, look, it’s a bird! No, it’s a plane! No, it’s Superman!” Hunting in the deep forest is an exciting sport for many people because they never know what they’ll run into in the woods. Biologists are often attracted to the field because of the excitement of finding new species and trying to categorize them. The same might be said of anthropology, the study of ancient human remains, and the related field, archaeology, the study of their civilizations. I was attracted to computer science for the same reason: Often, code would produce completely unexpected results, which I found exciting. Indeed, all the sciences involve discovery at the root of the field, so naturally curious people are those who enjoy the unexpected challenge of categorizing new things.

State-Driven Game Entity Behavior

Teaching computers to recognize new things and categorizing them is one area of work at the height of artificial intelligence research today. We will be looking at just large-scale game state this hour, but the potential is here for giving behaviors to game entities (represented with Sprite objects in the sample game). The behaviors have been explored somewhat already in the Animation class, but that was intended primarily to accommodate drawing with special effects, like rotation and alpha fading. The OrbitalMovement class was actually a behavior packaged as an Animation subclass, so we might use that class again for our game. The states of a game entity might be classified in terms of simple navigation—choosing a direction and velocity and moving, or heading toward a target location at a certain velocity. Some decision-making logic might be added so that an entity will follow or run away from another entity. These are all behaviors that can be programmed into a reusable class that will give a game much more of a scripted quality rather than a “hard-programmed” quality, which tends to be less flexible.

“State” can be set and used in logic using something as simple as a number variable, where the state is a discrete value from 0 to 100, with each number representing a different behavior for the entity. The state value might be used as a lookup index into an enumeration of behaviors that are available to all entities in a game. Consider these, for example:

  • IDLE = 0
  • RANDOM = 1
  • CHASING = 2
  • FLEEING = 3
  • SHADOWING = 4
  • HIDING = 5

These are all properties that can be encoded into an enumeration so that an entity’s behavior is determined with a simple integer variable used as the index.

Adaptive States: Rudimentary A.I.

The use of a state variable and enumerated state values may be considered a simple form of intelligence, but complex systems are made of simple items and rules that determine how they interact. Consider how an ant colony with only reactionary behavior can accomplish the stripping of food from nearby shrubs without a guiding intelligence directing them? One might consider their simple behaviors collectively as a hive intellect. Agitate one of the ants so that it dies, and a pheromone is given off, causing all nearby ants to charge toward the threat and exude the pheromone themselves, so that soon half the colony is on the attack. A similar process directs bee behavior, and indeed all hive species.

A static state variable with an enumeration of behaviors works well for most games. But if more advanced behavior is needed, a change is needed to the way behaviors are chosen for the entity. The behaviors themselves don’t change. In other words, the action items in the enumeration do not change. What changes is the way in which the index arrives at a value that is meaningful within the enumeration.

Consider sprite movement and animation for a minute. A sprite has a position based on Vector2, with floating-point X and Y properties. These floats do not have to equal a specific whole number representing a pixel in order for the sprite to continue to draw. If the sprite is drawn only when the X and Y position values equal a whole number, the sprite will most often be invisible! What’s happening in the case of sprite rendering is a natural form of adaptive state-based behavior. In short, the value is rounded to arrive at the nearest whole number. How that value changes from 10.00378 to 10.00377 is the issue, not whether the whole-number part, 10, is affected. The decimal value might seem irrelevant since the sprite does not move from the 10 position unless the decimal crosses a rounding border (0.0 or 5.0), causing the whole number to change when evaluated. For instance, 9.9 is equivalent to 10.4 when the whole number is considered, even though these numbers could be a million miles apart in decimal terms. It’s the way in which the values change that concerns adaptive state programming.

Although the indexed enumeration structure does not change, the values must change for the adaptive algorithms to make any sense. Using the list given previously, updating the index from 0.0 toward 1.0 will cause the entity to go from IDLE to RANDOM, which might not make any sense in the game. But bumping the entity several times while it is IDLE might push it into a FLEEING state. This is just an example, because the enumeration would need to be relevant to the game at hand. Instead of the items being just casually ordered, they are ordered in terms of “fear” or “aggression,” and the RANDOM item has been removed because it does not make sense in this context (it is more of an ambivalent or apathetic behavior):

  • FLEEING = -2
  • HIDING = -1
  • IDLE = 0
  • SHADOWING = 1
  • CHASING = 2

In this new ordering, note that the values reflect the fear factor of the game entity. As long as nothing happens to it, the state will remain at IDLE. If another sprite bumps into it, the state might go down a bit. The amount of change is dependent on the gameplay, but if a value of 0.2 represents a rock being thrown by an enemy, then it will take three rock hits before the entity goes from IDLE to HIDING:

  1. state = 0 (IDLE)
  2. state -= 0.2 (-0.2)
  3. state -= 0.2 (-0.4)
  4. state -= 0.2 (-0.6)
  5. state = -0.6 (HIDING)

The HIDING state will persist until it reaches -1.5, which is rounded to the FLEEING state of -2. If our game entity starts throwing rocks back at the other guy, perhaps scoring a hit will increase the state by 0.3 or some other fine-tuned value. Also, a natural tendency to return to the IDLE or neutral state must be included in the game logic. This might be a small amount added to the state variable every frame, such as 0.001 (when negative) or -0.001 (when positive) so that inactivity will cause the entity to go back to IDLE. The natural balance factor should not be so strong that gameplay events are overridden. Getting hit or scoring a hit should always be far greater an impact (pun not intended) than the balancing value applied at every update.

Testing Game State

I have prepared an example for this hour that will demonstrate finite state programming in an effective way, while also launching the start of our sample game that will be built over the remainder

IGameModule

To get started, we will use an interface class to describe the format of all state classes so that they can be invoked from the main game with a generic call. In other words, we don’t want to write a bunch of conditions to look for a specific state and launch that screen; we want all the screens to have the same functionality so that they can be called from an indexed array. The methods listed here must be implemented in all classes that share this interface. Another thing about an interface class is that it can’t have any variables. There is an argument to be made in favor of just using an abstract class rather than an interface class. If you really need to have class variables and some private items, go with an abstract, because those cannot be defined in an interface.

[code]
interface IGameModule
{
void LoadContent(ContentManager content);
void Update(TouchLocation touch, GameTime gameTime);
void Draw(GameTime gameTime);
}
[/code]

Interface classes cannot contain variables (properties) or scope modifiers.

TitleScreenModule

The first module we’ll cover is the TitleScreenModule class. This class inherits from IGameModule. Since that is an interface class, TitleScreenModule must incorporate all the methods defined in IGameModule. Each of these screens is like a miniprogram on its own, and that’s the whole point—we don’t want the main game to get too complicated with variables for each game state. Having a class for every state might seem like overkill, but it helps keep the game more organized, which leads to better results. Figure 21.1 shows the title screen module, and the source code is found in Listing 21.1. There are three buttons that trigger a different game state (Start Game, Options, Game Over), and an Exit button that ends the program.

The title screen module.
FIGURE 21.1 The title screen module.

LISTING 21.1 Source Code for the TitleScreenModule Class

[code]
class TitleScreenModule : IGameModule
{
Game1 game;
SpriteFont font;
SpriteFont guifont;
Label lblTitle;
Button[] btnMenu;
public TitleScreenModule(Game1 game)
{
this.game = game;
}
public void LoadContent(ContentManager content)
{
font = content.Load<SpriteFont>(“WascoSans”);
guifont = content.Load<SpriteFont>(“GUIFont”);
lblTitle = new Label(content, game.spriteBatch, font);
lblTitle.text = “Title Screen”;
Vector2 size = font.MeasureString(lblTitle.text);
lblTitle.position = new Vector2(400-size.X/2, 10);
btnMenu = new Button[4];
btnMenu[0] = new Button(content, game.spriteBatch, guifont);
btnMenu[0].text = “Start Game”;
btnMenu[0].position = new Vector2(400, 160);
btnMenu[0].scaleV = new Vector2(3.0f, 1.2f);
btnMenu[1] = new Button(content, game.spriteBatch, guifont);
btnMenu[1].text = “Options”;
btnMenu[1].position = new Vector2(400, 250);
btnMenu[1].scaleV = new Vector2(3.0f, 1.2f);
btnMenu[2] = new Button(content, game.spriteBatch, guifont);
btnMenu[2].text = “Game Over”;
btnMenu[2].position = new Vector2(400, 340);
btnMenu[2].scaleV = new Vector2(3.0f, 1.2f);
btnMenu[3] = new Button(content, game.spriteBatch, guifont);
btnMenu[3].text = “Exit”;
btnMenu[3].position = new Vector2(400, 430);
btnMenu[3].scaleV = new Vector2(3.0f, 1.2f);
}
public void Update(TouchLocation touch, GameTime gameTime)
{
int tapped = -1;
int n = 0;
foreach (Button btn in btnMenu)
{
btn.Update(touch);
if (btn.Tapped)
tapped = n;
n++;
}
switch (tapped)
{
case 0:
game.gameState = Game1.GameStates.PLAYING;
break;
case 1:
game.gameState = Game1.GameStates.OPTIONS;
break;
case 2:
game.gameState = Game1.GameStates.GAMEOVER;
break;
case 3:
game.Exit();
break;
}
}
public void Draw(GameTime gameTime)
{
lblTitle.Draw();
foreach (Button btn in btnMenu)
{
btn.Draw();
}
}
}
[/code]

PlayingModule

The PlayingModule class represents the normal playing state of the game when the player is engaged in the main gameplay. If the player chooses to manually exit, or wins the game, or loses the game, then the state will return to either the title screen or the game over screen. The source code is found in Listing 21.2, and Figure 21.2 shows the output. There isn’t much to see here, but it’s important to simulate the flow of the game with a Return button that jumps back to the title screen.

The game playing module.
FIGURE 21.2 The game playing module.

LISTING 21.2 Source Code for the PlayingModule Class

[code]
class PlayingModule : IGameModule
{
Game1 game;
SpriteFont font;
SpriteFont guifont;
Label lblTitle;
Button btnReturn;
public PlayingModule(Game1 game)
{
this.game = game;
}
public void LoadContent(ContentManager content)
{
font = content.Load<SpriteFont>(“WascoSans”);
guifont = content.Load<SpriteFont>(“GUIFont”);
lblTitle = new Label(content, game.spriteBatch, font);
lblTitle.text = “Game Play Screen”;
Vector2 size = font.MeasureString(lblTitle.text);
lblTitle.position = new Vector2(400 – size.X / 2, 10);
btnReturn = new Button(content, game.spriteBatch, guifont);
btnReturn.text = “Return”;
btnReturn.position = new Vector2(400, 430);
btnReturn.scaleV = new Vector2(3.0f, 1.2f);
}
public void Update(TouchLocation touch, GameTime gameTime)
{
btnReturn.Update(touch);
if (btnReturn.Tapped)
game.gameState = Game1.GameStates.TITLE;
}
public void Draw(GameTime gameTime)
{
lblTitle.Draw();
btnReturn.Draw();
}
}
[/code]

OptionsModule

The game options screen would allow the player to change the audio levels or toggle the mute option, among other settings. Figure 21.3 shows the output, which looks similar to the preceding screen—and for good reason, because these should behave in a similar way but contain unique content. Listing 21.3 contains the source code for the class.

The game options module.
FIGURE 21.3 The game options module.

LISTING 21.3 Source Code for the OptionsModule Class

[code]
class OptionsModule : IGameModule
{
Game1 game;
SpriteFont font;
SpriteFont guifont;
Label lblTitle;
Button btnReturn;
public OptionsModule(Game1 game)
{
this.game = game;
}
public void LoadContent(ContentManager content)
{
font = content.Load<SpriteFont>(“WascoSans”);
guifont = content.Load<SpriteFont>(“GUIFont”);
lblTitle = new Label(content, game.spriteBatch, font);
lblTitle.text = “Options Screen”;
Vector2 size = font.MeasureString(lblTitle.text);
lblTitle.position = new Vector2(400 – size.X / 2, 10);
btnReturn = new Button(content, game.spriteBatch, guifont);
btnReturn.text = “Return”;
btnReturn.position = new Vector2(400, 430);
btnReturn.scaleV = new Vector2(3.0f, 1.2f);
}
public void Update(TouchLocation touch, GameTime gameTime)
{
btnReturn.Update(touch);
if (btnReturn.Tapped)
game.gameState = Game1.GameStates.TITLE;
}
public void Draw(GameTime gameTime)
{
lblTitle.Draw();
btnReturn.Draw();
}
}
[/code]

GameOverModule

The game over screen is shown in Figure 21.4. Like the previous two modules, this just displays the module name (using a Label control), and a Return button at the bottom. It would be filled in with actual content to reflect that the player either won or lost the round or the game. Does it seem as though there is a lot of duplicated code here, with the label and button and font and so forth? Don’t be concerned with optimization if that occurs to you while you’re looking at these source code listings. There is no real content here yet, but each module will have its own unique logic and functionality, and all we see so far is structure. Listing 21.4 contains the source code for the class.

The game over module.
FIGURE 21.4 The game over module.

LISTING 21.4 Source Code for the GameOverModule Class

[code]
class GameOverModule : IGameModule
{
Game1 game;
SpriteFont font;
SpriteFont guifont;
Label lblTitle;
Button btnReturn;
public GameOverModule(Game1 game)
{
this.game = game;
}
public void LoadContent(ContentManager content)
{
font = content.Load<SpriteFont>(“WascoSans”);
guifont = content.Load<SpriteFont>(“GUIFont”);
lblTitle = new Label(content, game.spriteBatch, font);
lblTitle.text = “Game Over Screen”;
Vector2 size = font.MeasureString(lblTitle.text);
lblTitle.position = new Vector2(400 – size.X / 2, 10);
btnReturn = new Button(content, game.spriteBatch, guifont);
btnReturn.text = “Return”;
btnReturn.position = new Vector2(400, 430);
btnReturn.scaleV = new Vector2(3.0f, 1.2f);
}
public void Update(TouchLocation touch, GameTime gameTime)
{
btnReturn.Update(touch);
if (btnReturn.Tapped)
game.gameState = Game1.GameStates.TITLE;
}
public void Draw(GameTime gameTime)
{
lblTitle.Draw();
btnReturn.Draw();
}
}
[/code]

Game1

Listing 21.5 contains the main source code for the example. This file contains the GameStates enumeration and shows how to instantiate (remember, that’s a fancy word that means “to create an object from the blueprint of a class”) each of the modules, and call their mutual Update() and Draw() methods. Despite having four different classes for the four modules, they are all defined as an array of IGameModule! That is the key to using these behavioral/state classes—being able to swap them at any time without rewriting much code. That array is then indexed with the state variable.

LISTING 21.5 Main Source Code for the Example

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
public enum GameStates
{
TITLE = 0,
PLAYING = 1,
OPTIONS = 2,
GAMEOVER = 3
}
public GraphicsDeviceManager graphics;
public SpriteBatch spriteBatch;
SpriteFont font;
Random rand;
TouchLocation oldTouch;
public GameStates gameState;
IGameModule[] modules;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
oldTouch = new TouchLocation();
rand = new Random();
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
modules = new IGameModule[4];
modules[0] = new TitleScreenModule(this);
modules[1] = new PlayingModule(this);
modules[2] = new OptionsModule(this);
modules[3] = new GameOverModule(this);
foreach (IGameModule mod in modules)
{
mod.LoadContent(Content);
}
gameState = GameStates.TITLE;
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
TouchCollection touchInput = TouchPanel.GetState();
TouchLocation touch = new TouchLocation();
if (touchInput.Count > 0)
{
touch = touchInput[0];
oldTouch = touch;
}
//update current module
modules[(int)gameState].Update(touch ,gameTime);
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend);
//draw current module
modules[(int)gameState].Draw(gameTime);
spriteBatch.End();
base.Draw(gameTime);
}
}
[/code]

Creating a Graphical User Interface

Creating the GUI Controls

A graphical user interface (GUI) is absolutely essential for a game to be successful, even if that means using nothing more than labels and buttons on the screen that the user can click on.

Sprite Class Improvements

Modifying the Sprite Class

To make the GUI controls more effective, the Sprite class must be tweaked just a little.

  1. We need to change the definition of p_content and p_spriteBatch from private to protected so that they will be accessible to classes that inherit from Sprite. This way, we can load assets and draw without creating new reference variables in every subclass. Open the Sprite class and make the change:
    [code]
    protected ContentManager p_content;
    protected SpriteBatch p_spriteBatch;
    [/code]
  2. Just to be sure we are on the same page despite the changes made to this class in the past, here is the Load() method. Ignore past changes and just note this current version, which shows that the size and origin properties have been moved out of the try block:
    [code]
    public virtual bool Load(string assetName)
    {
    try
    {
    image = p_content.Load<Texture2D>(assetName);
    }
    catch (Exception) { return false; }
    size = new Vector2(image.Width, image.Height);
    origin = new Vector2(image.Width / 2, image.Height / 2);
    return true;
    }
    [/code]
  3. Add an error-handling line to the Draw() method so that it won’t crash the program if the image is null. This is a common verification. Since our GUI controls will be using a few images in interesting ways, we just want to ensure that any image that is not loaded correctly won’t crash the program—instead, it will just not show up.
    [code]
    public virtual void Draw()
    {
    if (!visible) return;
    if (image == null) return;
    . . .
    }
    [/code]

GUI Base Class: Control

All the GUI classes will be found in the GUI.cs source code file for the sake of convenience. Within that file, the classes will be wrapped inside the GameLibrary namespace (the same namespace used by Sprite and Animation).

[code]
namespace GameLibrary
{
. . .
}
[/code]

The base GUI class is called Control, and it is primarily used to create a reference to the ContentManager, SpriteBatch, and SpriteFont objects used in a game—all of which are needed by the GUI. Control inherits from Sprite, so it supplies GUI controls (declared as subclasses of Control) with all the features of Sprite, including loading and drawing. Methods are declared as virtual or override so they can be used and overridden in each subclass. There are certainly more services the base class could provide, such as touch input, but it turns out (during development) that most of that code must reside in each individual class. Listing 20.1 contains the source code for the Control class.

LISTING 20.1 Source Code for the Control Class

[code]
public abstract class Control : Sprite
{
protected SpriteFont p_font;
public Control(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch)
{
p_font = font;
}
public override bool Load(string filename)
{
return base.Load(filename);
}
public virtual void Update(TouchLocation touch)
{
}
public override void Draw()

{
base.Draw();
}
}
[/code]

Label Control

A Label is the most fundamental type of GUI control, with the simple task of displaying a text message on the screen. This is more important than it might at first seem, because a Label control can be moved anywhere on the screen without affecting the call to Label.Draw() from the main program. This Label class is rather basic, providing a shadow feature with customizable Color properties for the text and shadow. Two Labels will be used in the sample project later in this hour. Listing 20.2 contains the source code for the Label class.

LISTING 20.2 Source Code for the Label Class

[code]
public class Label : Control
{
public string text;
public Color shadowColor;
public Color textColor;
public bool UseShadow;
public Label(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch, font)
{
text = ““;
color = Color.White;
textColor = Color.White;
shadowColor = Color.Black;
UseShadow = true;
}
public override void Update(TouchLocation touch)
{
base.Update(touch);
}
public override void Draw()
{
if (UseShadow)
{
p_spriteBatch.DrawString(p_font, text,
new Vector2(position.X – 2, position.Y – 2), shadowColor);
}
p_spriteBatch.DrawString(p_font, text, position, textColor);
}
public Vector2 TextSize()
{
return p_font.MeasureString(text);
}
}
[/code]

Button Control

A Button is the second most common type of control needed for a rudimentary GUI system. Our Button class will load a 64×64 bitmap file called button.png (which must be in the content project). The great thing about this is that you can replace the image with one of your own. Due to the way the class works, I recommend using an image with the same dimensions but with your own “skin” theme. The button used in the example this hour is a gray box with a white outline. An important feature for a Button control is to display text and respond to user tap events. Our Button goes further by allowing its background and text colors to be changed independently for a customized look. Listing 20.3 contains the source code for the Button class.

LISTING 20.3 Source Code for the Button Class

[code]
public class Button : Control
{
public string text;
public Color shadowColor;
public Color textColor;
public bool UseShadow;
public bool Tapped;
public Button(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch, font)
{
text = ““;
color = Color.White;
textColor = Color.White;
shadowColor = Color.Black;
UseShadow = true;
Load(“button”);
}
public override void Update(TouchLocation touch)
{
base.Update(touch);
Tapped = false;
if (touch.State == TouchLocationState.Pressed)
{
Rectangle rect = Boundary();
Vector2 pos = touch.Position;
Point point = new Point((int)pos.X, (int)pos.Y);
if (rect.Contains(point))
{
Tapped = true;
}
}
}
public override void Draw()
{
base.Draw();
Vector2 size = TextSize();
Vector2 pos2 = new Vector2(position.X + 2, position.Y + 2);
Vector2 pivot = new Vector2(size.X / 2, size.Y / 2);
p_spriteBatch.DrawString(p_font, text, position, shadowColor,
0.0f, pivot, 1.0f, SpriteEffects.None, zindex);
p_spriteBatch.DrawString(p_font, text, pos2, textColor, 0.0f, pivot,
1.0f, SpriteEffects.None, zindex);
}
public Vector2 TextSize()
{
return p_font.MeasureString(text);
}
}
[/code]

Horizontal Slider Control

A slider control makes it possible to adjust a setting or to control some aspect of a game directly by the user, and resembles a movable sliding lever on the screen. There are two types of slider: horizontal and vertical. Although one common class could be used for both slider orientations, it would be more coding work, so it is more effective to just separate them into HSlider and VSlider controls. This is definitely a complex type of control compared to Label and Button. HSlider loads three images, so these bitmap files must all be found in the content project for the GUI code to run properly:

  • hslider_bar.png
  • hslider_end.png
  • button.png

Remember, when you are creating your own game using these GUI controls, that you can skin the controls to your own liking. The slider button needn’t be a circle at all! It can be any shape, including a custom image or a picture of a dragon—it doesn’t matter, and it’s up to you!

The left and right end images are shared, so if you create a custom skin for the control, be sure that the end images are interchangeable. The middle piece is a line one (1) pixel wide, scaled to the width of the control (set with the HSlider.Limit property). If the limit is 100, the one-pixel-wide image is scaled by 100 times to reach the edge! The scale as well as other properties are borrowed from the base Sprite class embedded in Control, inherited by HSlider. There isn’t much error handling, so if you try to set Limit to a negative number, it just will not work right or will crash. Listing 20.4 contains the source code for the HSlider class.

LISTING 20.4 Source Code for the HSlider Class

[code]
public class HSlider : Control
{
public bool Moving;
public Vector2 start;
private int p_value;
private int p_limit;
Sprite sprLeftEnd, sprRightEnd, sprBar;
public HSlider(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch, font)
{
scale = 1.0f;
start = Vector2.Zero;
Load(“slider_tab”);
sprLeftEnd = new Sprite(content, spriteBatch);
sprLeftEnd.Load(“hslider_end”);
sprLeftEnd.origin = new Vector2(3, 16);
sprRightEnd = new Sprite(content, spriteBatch);
sprRightEnd.Load(“hslider_end”);
sprRightEnd.origin = new Vector2(0, 16);
sprBar = new Sprite(content, spriteBatch);
sprBar.Load(“hslider_bar”);
sprBar.origin = new Vector2(0, 16);
Limit = 100;
}
public int Value
{
get { return p_value; }
set
{
p_value = value;
if (p_value < 0) p_value = 0;
if (p_value > p_limit) p_value = p_limit;
position.X = start.X + p_value;
}
}
public int Limit
{
get { return p_limit; }
set
{
p_limit = value;
sprBar.scaleV = new Vector2((float)
(p_limit + this.image.Width+1), 1.0f);
}
}
public override void Update(TouchLocation touch)
{
base.Update(touch);
Moving = false;
if (touch.State == TouchLocationState.Moved)
{
Rectangle rect = Boundary();
Point point = new Point((int)touch.Position.X,
(int)touch.Position.Y);
if (rect.Contains(point))
{
Vector2 relative = Vector2.Zero;
relative.X = touch.Position.X – position.X;
position.X += relative.X;
Value = (int)(position.X – start.X);
if (position.X < start.X)
position.X = start.X;
else if (p_value > p_limit)
position.X -= relative.X;
Moving = true;
}
}
}
public override void Draw()
{
//draw ends
sprLeftEnd.position = new Vector2(start.X – 16, start.Y);
sprLeftEnd.color = this.color;
sprLeftEnd.Draw();
sprRightEnd.position = new Vector2(start.X + 16 + p_limit, start.Y);
sprRightEnd.color = this.color;
sprRightEnd.Draw();
//draw middle bar
sprBar.position = new Vector2(start.X – 16, start.Y);
sprBar.color = this.color;
sprBar.Draw();
//draw sliding circle
base.Draw();
//draw value text
Vector2 size = p_font.MeasureString(p_value.ToString());
p_spriteBatch.DrawString(p_font, p_value.ToString(), position,
Color.Black, 0.0f, new Vector2(size.X/2, size.Y/2), 0.6f,
SpriteEffects.None, 1.0f);
}
public void SetStartPosition(Vector2 pos)
{
position = pos;
start = pos;
}
}
[/code]

Vertical Slider Control

The Vertical Slider control, or VSlider, shares all the same functionality as HSlider, but calculations are shifted 90 degrees in a vertical orientation. So, all the “X” properties used in the HSlider’s functionality become “Y” properties in VSlider in order for it to work properly. Here are the bitmaps required by the control (and note that button.png is shared):

  • vslider_bar.png
  • vslider_end.png
  • button.png

Listing 20.5 contains the source code for the VSlider class.

LISTING 20.5 Source Code for the VSlider Class

[code]
public class VSlider : Control
{
public bool Moving;
public Vector2 start;
private int p_value;
private int p_limit;
Sprite sprTopEnd, sprBottomEnd, sprBar;
public VSlider(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch, font)
{
scale = 1.0f;
start = Vector2.Zero;
Load(“slider_tab”);
sprTopEnd = new Sprite(content, spriteBatch);
sprTopEnd.Load(“vslider_end”);
sprTopEnd.origin = new Vector2(16, 3);
sprBottomEnd = new Sprite(content, spriteBatch);
sprBottomEnd.Load(“vslider_end”);
sprBottomEnd.origin = new Vector2(16, 0);
sprBar = new Sprite(content, spriteBatch);
sprBar.Load(“vslider_bar”);
sprBar.origin = new Vector2(16, 0);
Limit = 100;
}
public int Value
{
get { return p_value; }
set
{
p_value = value;
if (p_value < 0) p_value = 0;
if (p_value > p_limit) p_value = p_limit;
position.Y = start.Y + p_value;
}
}
public int Limit
{
get { return p_limit; }
set
{
p_limit = value;
sprBar.scaleV = new Vector2(1.0f, (float)
(p_limit + this.image.Height + 1));
}
}
public override void Update(TouchLocation touch)
{
base.Update(touch);
Moving = false;
if (touch.State == TouchLocationState.Moved)
{
Rectangle rect = Boundary();
Point point = new Point((int)touch.Position.X,
(int)touch.Position.Y);
if (rect.Contains(point))
{
Vector2 relative = Vector2.Zero;
relative.Y = touch.Position.Y – position.Y;
position.Y += relative.Y;
Value = (int)(position.Y – start.Y);
if (position.Y < start.Y)
position.Y = start.Y;
else if (p_value > p_limit)
position.Y -= relative.Y;
Moving = true;
}
}
}
public override void Draw()
{
//draw ends
sprTopEnd.position = new Vector2(start.X, start.Y – 16);
sprTopEnd.color = this.color;
sprTopEnd.Draw();
sprBottomEnd.position = new Vector2(start.X, start.Y + 16 + p_limit);
sprBottomEnd.color = this.color;
sprBottomEnd.Draw();
//draw middle bar
sprBar.position = new Vector2(start.X, start.Y – 16);
sprBar.color = this.color;
sprBar.Draw();
//draw sliding circle
base.Draw();
//draw value text
Vector2 size = p_font.MeasureString(p_value.ToString());
p_spriteBatch.DrawString(p_font, p_value.ToString(), position,
Color.Black, 0.0f, new Vector2(size.X / 2, size.Y / 2), 0.6f,
SpriteEffects.None, zindex);
}
public void SetStartPosition(Vector2 pos)
{
position = pos;
start = pos;
}
}
[/code]

Demonstrating the GUI Controls

The initialization code for a GUI demo or a game using GUI controls will always be much more involved and code-intensive than the processing code where the controls are updated and drawn, because there are so many properties involved in creating and customizing a nice-looking, interactive GUI. Our example this hour demonstrates a GUI with Labels, Buttons, HSliders, and VSliders, and is quite functional, as you can see in Figure 20.1. The source code for the GUI Demo program is found in Listing 20.6.

The example demonstrates labels, buttons, and sliders.
FIGURE 20.1 The example demonstrates labels, buttons, and sliders.

On the left is a vertical slider used to adjust the background color. Why? Just to show that the slider works and does something interesting. Maybe in a game a VSlider would be used to adjust the power level of a catapult or an artillery gun. Really, the use for these controls is up to the game’s designer and is just implemented by the programmer (or team). On the right side are three buttons labeled RED, GREEN, and BLUE. Beside each button is a slider.

Clicking a button changes the color component to a random value from 0 to 255, and automatically moves the slider to that location. The slider can also be moved manually, and this in turn will change the button’s color to reflect the change to that color component. The end result of all this color manipulation is seen in the small, unassuming Exit button at the lower-right corner of the screen. Note that the Limit property of both HSlider and VSlider changes its overall size and defines the limits of the sliding button. The three color sliders have a range of 0 to 255, whereas the smaller vertical slider has a range of 0 to 100.

LISTING 20.6 Source Code for the GUI Demo Program

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont font;
Random rand;
TouchLocation oldTouch;
Label lblTitle, lblColor;
Button[] buttons;
HSlider[] hsliders;
VSlider vslider;
Color bgcolor;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
oldTouch = new TouchLocation();
rand = new Random();
bgcolor = Color.CornflowerBlue;
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
lblTitle = new Label(Content, spriteBatch, font);
lblTitle.text = “Graphical User Interface Demo”;
lblTitle.position = new Vector2(400 – lblTitle.TextSize().X / 2, 0);
//create buttons
buttons = new Button[4];
buttons[0] = new Button(Content, spriteBatch, font);
buttons[0].text = “RED”;
buttons[0].position = new Vector2(400, 150);
buttons[0].textColor = Color.Red;
buttons[0].color = Color.DarkRed;
buttons[0].scaleV = new Vector2(1.5f, 1.0f);
buttons[1] = new Button(Content, spriteBatch, font);
buttons[1].text = “GREEN”;
buttons[1].position = new Vector2(400, 230);
buttons[1].textColor = Color.Green;
buttons[1].color = Color.DarkGreen;
buttons[1].scaleV = new Vector2(1.5f, 1.0f);
buttons[2] = new Button(Content, spriteBatch, font);
buttons[2].text = “BLUE”;
buttons[2].position = new Vector2(400, 310);
buttons[2].textColor = Color.Cyan;
buttons[2].color = Color.DarkCyan;
buttons[2].scaleV = new Vector2(1.5f, 1.0f);
buttons[3] = new Button(Content, spriteBatch, font);
buttons[3].text = “Exit”;
buttons[3].position = new Vector2(750, 450);
buttons[3].scaleV = new Vector2(1.2f, 0.8f);
//create horizontal sliders for color editing
hsliders = new HSlider[3];
hsliders[0] = new HSlider(Content, spriteBatch, font);
hsliders[0].SetStartPosition(new Vector2(500, 150));
hsliders[0].color = Color.Red;
hsliders[0].Limit = 255;
hsliders[1] = new HSlider(Content, spriteBatch, font);
hsliders[1].SetStartPosition(new Vector2(500, 230));
hsliders[1].color = Color.LightGreen;
hsliders[1].Limit = 255;
hsliders[2] = new HSlider(Content, spriteBatch, font);
hsliders[2].SetStartPosition(new Vector2(500, 310));
hsliders[2].color = Color.Cyan;
hsliders[2].Limit = 255;
//create vertical slider for bg color editing
vslider = new VSlider(Content, spriteBatch, font);
vslider.SetStartPosition(new Vector2(140, 170));
vslider.color = Color.Yellow;
vslider.Limit = 100;
//create label for slider
lblColor = new Label(Content, spriteBatch, font);
lblColor.text = “Background Color”;
lblColor.position = new Vector2( 140 – lblColor.TextSize().X/2,
100);
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
TouchCollection touchInput = TouchPanel.GetState();
if (touchInput.Count > 0)
{
TouchLocation touch = touchInput[0];
oldTouch = touch;
lblTitle.Update(touch);
UpdateButtons(touch);
UpdateSliders(touch);
vslider.Update(touch);
lblColor.Update(touch);
}
base.Update(gameTime);
}
void UpdateButtons(TouchLocation touch)
{
//update buttons
int tapped = -1;
for (int n = 0; n < buttons.Length; n++)
{
buttons[n].Update(touch);
if (buttons[n].Tapped) tapped = n;
}
//was a button tapped?
int c = rand.Next(256);
switch (tapped)
{
case 0:
buttons[0].color = new Color(c, 0, 0);
hsliders[0].Value = c;
break;
case 1:
buttons[1].color = new Color(0, c, 0);
hsliders[1].Value = c;
break;
case 2:
buttons[2].color = new Color(0, 0, c);
hsliders[2].Value = c;
break;
case 3:
this.Exit();
break;
}
}
void UpdateSliders(TouchLocation touch)
{
//update horizontal sliders
int moving = -1;
for (int n = 0; n < hsliders.Length; n++)
{
hsliders[n].Update(touch);
if (hsliders[n].Moving) moving = n;
}
switch(moving)
{
case 0:
buttons[0].color = new Color(hsliders[0].Value, 0, 0);
break;
case 1:
buttons[1].color = new Color(0, hsliders[1].Value, 0);
break;
case 2:
buttons[2].color = new Color(0, 0, hsliders[2].Value);
break;
}
//colorize Exit button based on colors
buttons[3].color = new Color(hsliders[0].Value,
hsliders[1].Value, hsliders[2].Value);
//update vertical slider
if (vslider.Moving)
{
bgcolor = Color.CornflowerBlue;
bgcolor.R -= (byte)vslider.Value;
bgcolor.G -= (byte)vslider.Value;
bgcolor.B -= (byte)vslider.Value;
}
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(bgcolor);
spriteBatch.Begin(SpriteSortMode.Deferred,
BlendState.AlphaBlend);
lblTitle.Draw();
foreach (Button b in buttons)
b.Draw();
foreach (HSlider hs in hsliders)
hs.Draw();
vslider.Draw();
lblColor.Draw();
spriteBatch.End();
base.Draw(gameTime);
}
void print(int x, int y, string text, Color color)
{
var pos = new Vector2((float)x, (float)y);
spriteBatch.DrawString(font, text, pos, color);
}
}
[/code]

Reading and Writing Files Using Storage, Windows Phone Isolated Storage

Using Windows Phone Isolated Storage

XNA potentially supports many storage devices across all the game systems it targets, but on the WP7, there is only one type: isolated storage. Any type of game asset can be imported into the Content Manager through an existing or user-created content importer, and this is the preferred way to read game data that does not change. For data that might change, such as generated data files or user-created game levels, or saved games, we can access the file system to stream data to our game from isolated storage. Any type of data file can be opened and written to or read from using Storage.IO classes, which we will learn about here. Since XML is recognized as a resource file by Content Manager, we will use XML for our example in this hour. XML has the benefit of being versatile and human readable at the same time. But binary and text files can be used as well.

Saving a Data File

We will first learn to create a new file in isolated storage and then read the data back out of the file afterward. The first thing that must be done is to add the library System.Xml.Serialization to the project via the references. The Serialization library makes it very easy to convert a class or struct into a file and read it back again without our having to decode the file manually (by setting individual properties one at a time). Let’s add it to the project.

Adding XML Support to the Project

  1. Right-click References in the Content project and choose Add Reference from the pop-up context menu.
  2. Locate the library called System.Xml.Serialization in the list, as shown in Figure 19.1.

    The System.Xml. Serialization library.
    FIGURE 19.1 The System.Xml. Serialization library.

Now that the reference is set, we can use XML files in the project more easily.

Isolated Storage

To access a file in isolated storage, we have to create a file object using the IsolatedStorageFile class:

[code]
IsolatedStorageFile storage =
IsolatedStorageFile.GetUserStoreForApplication();
[/code]

IsolatedStorageFile.GetUserStoreForApplication() is a rather verbose method that creates the new storage object with linkage to the application’s (or game’s) private storage area, making it available for accessing files, directories, and so on. If the object doesn’t need to be made global to the project, a shorthand declaration can be used:

[code]
var storage = IsolatedStorageFile.GetUserStoreForApplication();
[/code]

Creating a New Directory

Next, a required step must be taken: A directory must be created for the application to store files in. The private or isolated storage area has room for dictionary-style key/value data as well as SQL database tables, so we can’t just toss files in there like one large file system—we have to create a directory. If you don’t create a directory first, an exception error will occur when you try to create a new file. We will use the storage object to create a directory. The IsolatedStorageFile class has a method called DirectoryExists() that returns true if a passed directory exists. CreateDirectory() is used to create a new directory. So, if the directory doesn’t already exist, we want to create it:

[code]
const string directory = “StorageDemo”;
if (!storage.DirectoryExists(directory))
storage.CreateDirectory(directory);
[/code]

Creating a New File

Now, we can create a file inside the directory. First, we have to check to see whether the file exists. WP7 does not support the FileMode.CreateNew option, which is supposed to overwrite a file if it already exists. Trying to do this generates an exception error, even though it works on Windows and Xbox 360. So, we have to delete the file first before creating it again. Usually this is not a problem because savegame data tends to be rather simple for most games. If you are working on a large, complex game, like an RPG, and there’s a lot of data, of course the game might support multiple savegame files, and you’ll have a mini file manager built into the game. But we’re just learning the ropes here, so we’ll do it the simple way to get it working. We use the FileExists() and DeleteFile() methods to get rid of the old save file:

[code]
const string filename = directory + “\savegame.dat”;
if (storage.FileExists(filename))
storage.DeleteFile(filename);
[/code]

Now we’re ready to create a new savegame file and write data to it. This is done with the IsolatedStorageFileStream() class:

[code]
var fstream = new IsolatedStorageFileStream(
filename, FileMode.CreateNew, storage);
[/code]

The FileMode enumeration has these values:

  • CreateNew = 1
  • Create = 2
  • Open = 3
  • OpenOrCreate = 4
  • Truncate = 5
  • Append = 6

Writing Data to the File with Serialization

Although any type of data file can be created, XML is quite easy to use, and an entire class or struct variable (full of game data) can be written to the file with only a couple lines of code. If you want to just write binary or text data to the file, that will work also at this point, but it’s so much easier to use serialization! Here is a simple struct we can use for this example:

[code]
public struct SaveGameData
{
public string Name;
public int Score;
}
[/code]

A new SaveGameData variable is created and the two properties are filled with data. This is where you would store actual game data in the properties in order to restore the game to this gameplay state later when the savegame file is loaded:

[code]
savedata = new SaveGameData();
savedata.Name = “John Doe”;
savedata.Score = rand.Next(500, 5000);
[/code]

Now, to write the data to the file, we have to create an XmlSerializer object, and then write the serialized object out to the file:

[code]
XmlSerializer serializer = new XmlSerializer(typeof(SaveGameData));
serializer.Serialize(fstream, savedata);
[/code]

At this point, the file has been created and data has been written to it that was contained in the savedata struct variable.

Loading a Data File

Loading a serialized XML file is very similar to the writing process. Of course, you may read a simple text or binary file and parse the data if that is more suitable for your needs, but I’m using serialization and XML because it’s so easy and likely to be the approach most game developers take with WP7 savegame data. The same storage object is created, but we don’t need any of the code to create a directory or delete the existing file (obviously), so the code to load the savegame file is much simpler:

[code]
var storage = IsolatedStorageFile.GetUserStoreForApplication();
[/code]

Likewise, the IsolatedStorageFileStream object is created in the same way:

[code]
var fstream = new IsolatedStorageFileStream(
filename, FileMode.CreateNew, storage);
[/code]

There is a second way to create the fstream file object variable: by creating the object in a using statement and then adding code that uses the object in the bracketed code block:
[code]
using (var fstream = new IsolatedStorageFileStream(
filename, FileMode.Open, storage)) { }
[/code]

The XmlSerializer object is created in a similar manner:

[code]
XmlSerializer serializer = new XmlSerializer(typeof(SaveGameData));
[/code]

The only difference really is a call to Deserialize() instead of Serialize(), and this method returns our savegame data as an object:

[code]
data = (SaveGameData)serializer.Deserialize(fstream);
[/code]

Just for curiosity’s sake, here is what the XML file looks like that is created by our code. If you were to serialize a more complex data type, like a Vector4, then the parameters within that class or struct would become sub-items in the XML structure.

[code]
<?xml version=”1.0”?>
<SaveGameData xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
xmlns:xsd=”http://www.w3.org/2001/XMLSchema”>
<Name>John Doe</Name>
<Score>1245</Score>
</SaveGameData>
[/code]

Creating the Storage Demo Example

We will go over a complete program that demonstrates how to save data to a save game file and then load it again, based on some rudimentary user input. Two buttons are created and displayed using our familiar Button class (which inherits from Sprite). This class requires a bitmap file called button.png, so be sure that it exists in the content project.

To verify that the example is working, we will want to run the program, save the data, close the program, and then rerun it and choose the load option to see that the data is still there. So, the example should read and write the data only when the user chooses to, not automatically. When the emulator is being used, exiting the program still preserves it in memory, but closing the emulator will erase all traces of the program and data files.

Closing the WP7 emulator will wipe the storage memory, including data files created by our example here, and any programs previously loaded from Visual Studio. But closing the program and rerunning it will reveal an intact file system. This happens because the emulator creates a new emulation state system when it is run, and that is not saved when it closes.

Figure 19.2 shows the output of the Storage Demo program.

The Storage Demo example shows how to read and write data.
FIGURE 19.2 The Storage Demo example shows how to read and write data.

Button Class

Just for the sake of clarity, Listing 19.1 shows the source code for the Button class. We have seen the code before, but it is required by the Storage Demo and is included again for clarity.

LISTING 19.1 Source Code for the Button Class

[code]
public class Button : Sprite
{
public string text;
private SpriteBatch p_spriteBatch;
private SpriteFont p_font;
public Button(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch)
{
p_spriteBatch = spriteBatch;
p_font = font;
Load(“button”);
text = ““;
color = Color.LightGreen;
}
public void Draw()
{
base.Draw();
Vector2 size = p_font.MeasureString(text);
Vector2 pos = position;
pos.X -= size.X / 2;
pos.Y -= size.Y / 2;
p_spriteBatch.DrawString(p_font, text, pos, color);
}
public bool Tapped(Vector2 pos)
{
Rectangle rect = new Rectangle((int)pos.X, (int)pos.Y, 1, 1);
return Boundary().Intersects(rect);
}
}
[/code]

Storage Demo Source

Here in Listing 19.2, we have the source code for the Storage Demo program, with the definition of the SaveGameData class as well.

LISTING 19.2 Source Code for the Storage Demo Program

[code]
public struct SaveGameData
{
public string Name;
public int Score;
}
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont font;
Random rand;
TouchLocation oldTouch;
Button[] buttons;
int current = -1;
bool loaded = false;
SaveGameData savedata;
const string directory = “StorageDemo”;
const string filename = directory + “\savegame.dat”;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
oldTouch = new TouchLocation();
rand = new Random();
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
//create save button
buttons = new Button[2];
buttons[0] = new Button(Content, spriteBatch, font);
buttons[0].text = “Save”;
buttons[0].position = new Vector2(100, 100);
//create load button
buttons[1] = new Button(Content, spriteBatch, font);
buttons[1].text = “Load”;
buttons[1].position = new Vector2(300, 100);
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
//get state of touch input
TouchCollection touchInput = TouchPanel.GetState();
if (touchInput.Count > 0)
{
TouchLocation touch = touchInput[0];
if (touch.State == TouchLocationState.Pressed)
{
current = -1;
int n = 0;
foreach (Button b in buttons)
{
int x = (int)touch.Position.X;
int y = (int)touch.Position.Y;
if (b.Boundary().Contains(x, y))
{
current = n;
break;
}
n++;
}
}
oldTouch = touch;
}
if (current == 0)
{
savedata = new SaveGameData();
savedata.Name = “John Doe”;
savedata.Score = rand.Next(500, 5000);
SaveData(savedata);
loaded = false;
current = -1;
}
else if (current == 1)
{
savedata = LoadData();
loaded = true;
current = -1;
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin(SpriteSortMode.FrontToBack,
BlendState.AlphaBlend);
print(0, 0, “Storage Demo”, Color.White);
foreach (Button b in buttons)
b.Draw();
if (loaded)
{
print(100, 200, “Loaded data:nn” +
“Name: “ + savedata.Name + “n” +
“Score: “ + savedata.Score.ToString() + “n”,
Color.White);
}
spriteBatch.End();
base.Draw(gameTime);
}
void print(int x, int y, string text, Color color)
{
var pos = new Vector2((float)x, (float)y);
spriteBatch.DrawString(font, text, pos, color);
}
private void SaveData(SaveGameData data)
{
var storage = IsolatedStorageFile.GetUserStoreForApplication();
//create directory for data
if (!storage.DirectoryExists(directory))
storage.CreateDirectory(directory);
//delete any existing file
if (storage.FileExists(filename))
storage.DeleteFile(filename);
//create new savegame file
using (var fstream = new IsolatedStorageFileStream(filename,
FileMode.CreateNew, storage))
{
XmlSerializer serializer = new XmlSerializer(
typeof(SaveGameData));
serializer.Serialize(fstream, data);
}
}
private SaveGameData LoadData()
{
SaveGameData data;
var storage = IsolatedStorageFile.GetUserStoreForApplication();
using (var fstream = new IsolatedStorageFileStream(filename,
FileMode.Open, storage))
{
XmlSerializer serializer = new XmlSerializer(
typeof(SaveGameData));
data = (SaveGameData)serializer.Deserialize(fstream);
}
return data;
}
}
[/code]

We now have the ability to create a savegame file and load it again! This greatly enhances the replay value of a game that would otherwise appear to have been freshly installed every time it is run. Use this feature to store game settings, player names, and high score lists, as well as generated game levels and anything else that needs to be remembered by the game for the next time.

Playing Audio, Windows Phone Audio

Getting Started with Windows Phone Audio

The audio system in XNA makes it possible to reproduce sound effects and music in two different ways, but the WP7 platform supports only one of them. First, we can play audio clips directly from audio files loaded at runtime, with support for the most common audio file formats. Second, in XNA, we can use Microsoft’s Cross-Platform Audio Creation Tool, also known as XACT, which is more often used for complex games with many audio files. The first approach involves loading and managing audio objects in our own code. The second approach leaves the details largely up to classes provided for working with XACT resources built at compile time and then made available from a container of objects. But we can’t use XACT with WP7 projects, so we’ll learn about the first and only option instead. We’ll cover the audio system in the Microsoft.Xna.Framework.Audio namespace with an example of the audio system.

Simple Audio Playback

There is one very easy way to get audio to play in an XNA project: by using the SoundEffect class. There is a drawback to using SoundEffect assets that really can’t be avoided: the tendency for the content project to become cluttered with asset files. Even so, the SoundEffect class is convenient and easy to use. A helper class called SoundEffectInstance is also used in conjunction with SoundEffect for audio playback. SoundEffect itself has a Play() method, but it is rudimentary. SoundEffectInstance.Play() is more capable and versatile.

Adding Audio Content

Before we can play an audio clip, we have to add it to the content system.

  1. First, right-click the Content project in Solution Explorer, choose Add, and then choose Existing Item, as shown in Figure 18.1. This opens the Add Existing File dialog box.

    Adding an existing audio file to the content project.
    FIGURE 18.1 Adding an existing audio file to the content project.
  2. Locate the audio file you want to add to the project. Select the file in the file browser and click the OK button.

Loading an Audio Asset File

The following audio file types can be added to an XNA project for use with the SoundEffect class, but remember that WP7 does not support XACT:

  • XAP
  • WAV
  • WMA
  • MP3

You can create an instance of the SoundEffect class with a variable declared in the globals section at the top of the class as follows:

[code]
SoundEffect clip;
[/code]

The SoundEffect object is created in the usual LoadContent() function where all other asset files are loaded:

[code]
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“Arial”);
clip = Content.Load<SoundEffect>(“sound_clip”);
}
[/code]

Playing an Audio Clip

The audio clip can be played back using the SoundEffect.Play() method. There is a simple version of Play() and an overloaded version, which gives you control over the Volume, Pitch, and Pan properties. The audio clip is played directly from the object like so:

[code]
clip.Play();
[/code]

Audio Clip Length

There is really only one useful property in the SoundEffect class: Duration. This property gives you the length of the audio clip in seconds. In the Draw() function of your sample program (Simple Audio Demo), you can print out the length of the audio clip.

[code]
string text = “Clip length: “ + clip.Duration.ToString();
spriteBatch.DrawString(font, text, Vector2.Zero, Color.White);
[/code]

When it comes down to playing audio clips in gameplay code, this class is not very helpful at all. What we need is a class with more functionality.

SoundEffectInstance

The SoundEffect class works, but not very well on its own. For one thing, a SoundEffect clip played does not work well in a complex audio environment (with music and different sound effects playing at the same time). For that, we need a helper class that can repeat playback with mixing support. That class is called SoundEffectInstance. This is really what we want to use when playing audio clips.

The SoundEffectInstance class enhances the basic functionality of SoundEffect with additional properties and methods that make it more useful in a real-world game. After loading the SoundEffect in LoadContent(), we can create an instance of the SoundEffectInstance class from its CreateInstance() method:

[code]
SoundEffect clip = Content.Load<SoundEffect>(“sound_clip”);
SoundEffectInstance clipInst = clip.CreateInstance();
[/code]

This class makes available several useful methods: Play(), Stop(), Pause(), and Resume(). And the Volume, Pitch, and Pan properties have been moved from parameters into real class properties, which we can modify outside of the Play() method. In addition, we can cause an audio clip to loop during playback with the IsLooping property.

MySoundEffect Class

I have prepared a helper class with essentially the sole purpose of just combining a SoundEffect and SoundEffectInstance object with a Load() method. We will use this helper class in the example coming up next. The source code for the class is found in Listing 18.1.

LISTING 18.1 Source Code for the MySoundEffect Class

[code]
public class MySoundEffect
{
private ContentManager p_content;
public SoundEffect effect;
public SoundEffectInstance instance;
public MySoundEffect(ContentManager content)
{
p_content = content;
effect = null;
instance = null;
}
public void Load(string assetName)
{
effect = p_content.Load<SoundEffect>(assetName);
instance = effect.CreateInstance();
}
}
[/code]

Creating the Audio Demo Program

The Audio Demo program is a complete example of audio for WP7 with some user input features as well, as shown in Figure 18.2. Five buttons are located on the screen that can be touched to play the five audio clips loaded into the program.

The Audio Demo program.
FIGURE 18.2 The Audio Demo program.

Button Class

The audio sample project uses a helper class called Button to draw numbered buttons on the screen that respond to screen touch input events. The Button class requires a bitmap file called button.png that is just a transparent image with a border around its edges. Listing 18.2 contains the source code for the Button class.

LISTING 18.2 The Button Class

[code]
public class Button : Sprite
{
public string text;
private SpriteBatch p_spriteBatch;
private SpriteFont p_font;
public Button(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font) : base(content, spriteBatch)
{
p_spriteBatch = spriteBatch;
p_font = font;
Load(“button”);
text = ““;
color = Color.LightGreen;
}
public void Draw()
{
base.Draw();
Vector2 size = p_font.MeasureString(text);
Vector2 pos = position;
pos.X -= size.X / 2;
pos.Y -= size.Y / 2;
p_spriteBatch.DrawString(p_font, text, pos, color);
}
public bool Tapped(Vector2 pos)
{
Rectangle rect = new Rectangle((int)pos.X, (int)pos.Y, 1, 1);
return Boundary().Intersects(rect);
}
}
[/code]

Audio Demo Source

The main source code for the Audio Demo project is found in Listing 18.3. The required assets are a button image and five audio files named clip1, clip2, clip3, clip4, and clip5.

LISTING 18.3 Source Code for the Audio Demo Program

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
TouchLocation oldTouch;
SpriteFont font;
Button[] buttons;
MySoundEffect[] sounds;
int current = -1;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
oldTouch = new TouchLocation();
}
protected override void Initialize()
{
base.Initialize();
this.IsMouseVisible = true;
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
//create buttons
buttons = new Button[5];
for (int n = 0; n < 5; n++)
{
buttons[n] = new Button(Content, spriteBatch, font);
buttons[n].text = (n+1).ToString();
buttons[n].position = new Vector2(100 + 110 * n, 200);
}
//create sound clips
sounds = new MySoundEffect[5];
for (int n = 0; n < 5; n++)
{
sounds[n] = new MySoundEffect(Content);
sounds[n].Load(“Audio//clip” + (n+1).ToString());
}
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
TouchCollection touchInput = TouchPanel.GetState();
if (touchInput.Count > 0)
{
TouchLocation touch = touchInput[0];
if (touch.State == TouchLocationState.Pressed)
{
current = -1;
int n = 0;
foreach (Button b in buttons)
{
int x = (int)touch.Position.X;
int y = (int)touch.Position.Y;
if (b.Boundary().Contains(x,y))
{
current = n;
sounds[current].instance.Play();
break;
}
n++;
}
}
oldTouch = touch;
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin(SpriteSortMode.FrontToBack,
BlendState.AlphaBlend);
spriteBatch.DrawString(font, “Audio Demo”, Vector2.Zero,
Color.White);
foreach (Button b in buttons)
{
b.Draw();
}
if (current > -1)
{
string text = “Clip length: “ +
sounds[current].effect.Duration.ToString();
spriteBatch.DrawString(font, text, new Vector2(0, 400),
Color.White);
}
spriteBatch.End();
base.Draw(gameTime);
}
}
[/code]

We have now fully covered the XNA audio system with an example of how to load and play audio files. The helper class, MySoundEffect, can be dropped into any WP7 project and used to more easily handle the audio needs of a game.

Windows Phone Using Location Services (GPS)

GPS 101

Let’s spend a few minutes to just learn the basics of GPS, in order to better use it in our code. What GPS boils down to—the nitty-gritty—are two floating-point numbers representing the X and Y position on the Earth. The X value has traditionally been called longitude, and the Y value has been known as the latitude. From a game programming perspective, this is an easier way to grasp the terms, but a naval veteran would scoff at the overly simplistic way this is being presented. We’ll gloss over issues of precision in order to grasp the concepts first.

Longitude represents the “X” or horizontal coordinate on the surface of the Earth, running east or west from the zero point.

Latitude represents the “Y” or vertical coordinate on the surface of the Earth, running north or south from the zero point.

The origin (0,0) is located about 400 miles off the western coast of Africa, southwest of Nigeria and south of Ghana. From that origin point, longitude increases to the right (east), and decreases to the left (west); latitude increases up (north), and decreases down (south). In other words, it is oriented exactly like the Cartesian coordinate system we’ve been using all along for our trig-heavy examples. This makes translating GPS coordinates for the purpose of making a reality game a cinch!

To help make sense of the coordinate system, Table 17.1 shows the approximate latitude and longitude values of several major cities in the world, formatted in a way that makes sense to game programmers (such that longitude comes before latitude— remember, we aren’t navigating here). Note that these are far from precise, just rough estimates to present the general location of each city. More precise GPS coordinates will include up to six decimal places of increasing precision, down to just 10 feet or less in granularity.

GPS Data for Major Cities

If you want to learn more about latitude and longitude coordinates, there is an interactive world map available online at http://itouchmap.com/latlong.html.

Windows Phone Location Services

XNA provides us with a geographic location service in a library located in a namespace called System.Device.Location. This library is not included in the project’s references by default, so we must add it to use this library in our program.

Adding the Location Services Library

  1. Right-click References in the Solution Explorer, and then choose Add Reference.
  2. In the dialog box that comes up, there is a list with the .NET tab already in view, as shown in Figure 17.1. Select System.Device from the list and click the OK button.
  3. The geographic location services library is in a namespace called System.Device.Location, which must be added with a using statement to any program that needs these services:
    [code]
    using System.Device.Location;
    [/code]

Using the Location Services

To read the current device’s GPS location, we create an object using the GeoCoordinateWatcher class:

[code]
GeoCoordinateWatcherSim watcher;
[/code]

It is okay to create the watcher object in Initialize() or LoadContent(), or in response to a user event:

[code]
watcher = new GeoCoordinateWatcher(GeoPositionAccuracy.Default);
[/code]

At this point, the object is created but is not yet receiving any GPS data. We have to create an event handler to handle position status change events. The trigger that causes such an event is movement of the device, which can be fine-tuned with the MovementThreshold property:

[code]
watcher.MovementThreshold = 20;
[/code]

Adding a reference to the System.Device library.
FIGURE 17.1 Adding a reference to the System.Device library.

The first event we’ll tap into is StatusChanged. A new event method will need to be created to correspond with the name of the method passed to this new event object. In this case, the example is using a string called statusText, which can be printed out from the main Draw() call. Optionally, a programmer-defined status could be set here and used elsewhere in the game:

[code]
watcher.StatusChanged += new EventHandler
<GeoPositionStatusChangedEventArgs>(watcher_StatusChanged);
void watcher_StatusChanged(object sender,
GeoPositionStatusChangedEventArgs e)
{
switch (e.Status)
{
case GeoPositionStatus.Disabled:
statusText += “Location service has been disabledn”;
break;
case GeoPositionStatus.Initializing:
statusText += “Location service is initializingn”;
break;
case GeoPositionStatus.NoData:
statusText += “Location service is not returning any datan”;
break;
case GeoPositionStatus.Ready:
statusText += “Location service is receiving datan”;
break;
}
}
[/code]

The actual movement of the GPS device triggers position change events that we can tap into with the PositionChanged event. A similar event method will have to be created for this event as well. In this example, a GeoCoordinate variable called coord is set using the passed parameter that contains the GPS location data:

[code]
watcher.PositionChanged += new EventHandler
<GeoPositionChangedEventArgs<GeoCoordinate>>(watcher_PositionChanged);
void watcher_PositionChanged(object sender,
GeoPositionChangedEventArgs<GeoCoordinate> e)
{
coord = e.Position.Location;
}
[/code]

Simulating Position Changes

The WP7 emulator does not have a GPS receiver, and even if your PC has one, the emulator doesn’t know how to use it—the emulator is a self-contained system that only uses the networking of your PC to simulate connectivity. I say “simulate” because in a real WP7 device, that Internet connection would come through the airwaves, presumably G3 or G4, depending on what the service provider supports.

There is a workaround for the limitation. If you want to create a game that uses location services, it’s a given you must be able to test it extensively, and even with a real WP7 device, testing GPS code can be a challenge. So, even with hardware, it may be preferred to develop this code with a GPS simulation rather than the real thing. With a simulation, you can define the location data yourself and write the gameplay code to respond to location data in a predictable way. Only the final testing stages of the game would need to be done “in the field.”

So, a question arises: How do we simulate GPS data?

The solution is to write a class that inherits from GeoLocationWatcher and then fill in data events with a timer that generates real-time updates via GeoLocation events. Voilà!

GeoLocationSim

There are three classes involved in the geographic location simulator. The first is GeoLocationSim, which inherits directly from GeoCoordinateWatcher, the main GPS class in XNA. There are quite a few properties, events, and methods defined in this abstract class that are required to pass this off as a legitimate GeoLocation class so that it works with normal GeoLocation code, but we don’t need all of that for testing purposes. Nevertheless, they are all required. In the sample project for this hour, I have added all three classes in a source file called GeoLocationSim.cs. First, take a look at Listing 17.1, the code for the sim class.

LISTING 17.1 Base GeoLocation Simulation Class

[code]
abstract public class GeoLocationSim : GeoCoordinateWatcher
{
private GeoPosition<GeoCoordinate> current;
private Timer timer;
public GeoLocationSim()
{
current = new GeoPosition<GeoCoordinate>();
Status = GeoPositionStatus.Initializing;
RaiseStatusChanged();
}
private void RaiseStatusChanged()
{
GeoPositionStatusChangedEventArgs args =
new GeoPositionStatusChangedEventArgs(Status);
if (StatusChanged != null)
{
StatusChanged(this, args);
}
}
private void RaisePositionChanged()
{
GeoPositionChangedEventArgs<GeoCoordinate> args =
new GeoPositionChangedEventArgs<GeoCoordinate>(current);
if (PositionChanged != null)
PositionChanged(this, args);
}
public void OnTimerCallback(object state)
{
try
{
if (Status == GeoPositionStatus.Initializing)
{
Status = GeoPositionStatus.NoData;
RaiseStatusChanged();
}
StartGetCurrentPosition();
TimeSpan next = GetNextInterval();
timer.Change(next, next);
}
catch (Exception)
{
throw;
}
}
protected void UpdateLocation(double longitude, double latitude)
{
GeoCoordinate location = new GeoCoordinate(latitude, longitude);
if (!location.Equals(current.Location))
{
current = new GeoPosition<GeoCoordinate>(
DateTimeOffset.Now, location);
if (Status != GeoPositionStatus.Ready)
{
Status = GeoPositionStatus.Ready;
RaiseStatusChanged();
}
RaisePositionChanged();
}
}
abstract protected TimeSpan GetNextInterval();
abstract protected void StartGetCurrentPosition();
//override base property
public GeoPositionPermission Permission
{
get { return GeoPositionPermission.Granted; }
}
//override base property
public GeoPosition<GeoCoordinate> Position
{
get { return current; }
}
//override base event
public event EventHandler<GeoPositionChangedEventArgs
<GeoCoordinate>> PositionChanged;
//override base method
public void Start(bool suppressPermissionPrompt)
{
Start();
}
//override base method
public void Start()
{
TimeSpan span = GetNextInterval();
timer = new Timer(OnTimerCallback, null, span, span);
}
//override base property
public GeoPositionStatus Status
{
get;
protected set;
}
//override base event
public event EventHandler
<GeoPositionStatusChangedEventArgs> StatusChanged;
//override base method
public void Stop()
{
timer.Change(Timeout.Infinite, Timeout.Infinite);
Status = GeoPositionStatus.Disabled;
RaiseStatusChanged();
}
//override base method
public bool TryStart(bool suppressPermissionPrompt, TimeSpan timeout)
{
Start();
return true;
}
}
[/code]

Filling in GPS Data with Timing

SampleGeoCoord is a helper class that is used to fill in GPS position data with timing. Each position coordinate corresponds to a one-second interval at which the position update event is triggered. So, this class supplies longitude, latitude, and time.

[code]
public class SampleGeoCoord
{
public double Longitude { get; set; }
public double Latitude { get; set; }
public TimeSpan Time { get; set; }
public SampleGeoCoord(double Longitude, double Latitude, int seconds)
{
this.Longitude = Longitude;
this.Latitude = Latitude;
this.Time = new TimeSpan(0, 0, seconds);
}
}
[/code]

GeoCoordinateWatcherSim

The GeoCoordinateWatcherSim is our main workhorse simulation class, inheriting directly from GeoLocationSim. This class puts the GeoLocationSim properties, methods, and events to work using data populated within an array of SampleGeoCoord objects. In the example coming up that uses this class, I’ve centered the coordinates around Los Angeles, with 60 seconds of random locations within a radius of about 100 miles around the city coordinates (-118, 34). Listing 17.2 contains the code for the GeoCoordinateWatcherSim class.

LISTING 17.2 Usable GeoCoordinateWatcherSim Worker Class

[code]
public class GeoCoordinateWatcherSim : GeoLocationSim
{
List<SampleGeoCoord> events;
int currentEventId;
Random rand = new Random();
public GeoCoordinateWatcherSim(GeoPositionAccuracy accuracy)
{
currentEventId = 0;
events = new List<SampleGeoCoord>();
//create random coordinates in Los Angeles
for (int n = 1; n < 60; n++)
{
double Long = -118 – rand.Next(2) – rand.NextDouble();
double Lat = 33 + rand.Next(2) + rand.NextDouble();
events.Add(new SampleGeoCoord(Long, Lat, n));
}
}
private SampleGeoCoord Current
{
get
{
return events[currentEventId % events.Count];
}
}
protected override void StartGetCurrentPosition()
{
this.UpdateLocation(Current.Longitude, Current.Latitude);
currentEventId++;
}
protected override TimeSpan GetNextInterval()
{
return Current.Time;
}
}
[/code]

Creating the Geo Position Demo

Let’s write a program to demonstrate the GeoCoordinateWatcherSim class in action. The example requires only a font, because it just prints out the longitude and latitude of the geographical coordinate data and the status of the watcher. The code for the Geo Position Demo program is found in Listing 17.3, and Figure 17.2 shows the program running. Note that this example will work on a WP7 device without the simulated data with a single line change, from

[code]
watcher = new GeoCoordinateWatcherSim(…);
[/code]

to

[code]
watcher = new GeoCoordinateWatcher(…);
[/code]

The Geo Position Demo simulates GPS movement.
FIGURE 17.2 The Geo Position Demo simulates GPS movement.

LISTING 17.3 The Geo Position Demo Program

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
TouchLocation oldTouch;
Random rand;
SpriteFont font;
string statusText = ““;
GeoCoordinateWatcherSim watcher = null;
GeoCoordinate coord = null;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
oldTouch = new TouchLocation();
}
protected override void Initialize()
{
base.Initialize();
StartGeoLocation();
}
protected override void LoadContent()
{
rand = new Random();
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
}
protected override void UnloadContent()
{
base.UnloadContent();
watcher.Stop();
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin(SpriteSortMode.FrontToBack,
BlendState.AlphaBlend);
spriteBatch.DrawString(font, “Latitude: “ +
coord.Latitude.ToString(“0.000”),
new Vector2(100, 10), Color.White);
spriteBatch.DrawString(font, “Longitude: “ +
coord.Longitude.ToString(“0.000”),
new Vector2(100, 30), Color.White);
spriteBatch.DrawString(font, statusText,
new Vector2(100, 100), Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
void StartGeoLocation()
{
coord = new GeoCoordinate();
//try to create geo coordinate watcher
if (watcher == null)
{
statusText += “Starting location service…n”;
watcher = new GeoCoordinateWatcherSim(
GeoPositionAccuracy.Default);
watcher.MovementThreshold = 20;
watcher.StatusChanged += new EventHandler
<GeoPositionStatusChangedEventArgs>(
watcher_StatusChanged);
watcher.PositionChanged += new EventHandler
<GeoPositionChangedEventArgs<GeoCoordinate>>
(watcher_PositionChanged);
watcher.Start();
}
}
void watcher_StatusChanged(object sender,
GeoPositionStatusChangedEventArgs e)
{
switch (e.Status)
{
case GeoPositionStatus.Disabled:
statusText += “Location service has been disabledn”;
break;
case GeoPositionStatus.Initializing:
statusText += “Location service is initializingn”;
break;
case GeoPositionStatus.NoData:
statusText += “Location service is not returning any datan”;
break;
case GeoPositionStatus.Ready:
statusText += “Location service is receiving datan”;
break;
}
}
void watcher_PositionChanged(object sender,
GeoPositionChangedEventArgs<GeoCoordinate> e)
{
coord = e.Position.Location;
}
}
[/code]

There are many uses for GPS tracking, not to mention potential multiplayer games, but one thing to keep in mind is that GPS only provides location data, but there’s no transmitting of that data. After the location is received, that’s it—it’s data, and it’s not transmitted anywhere. GPS is read-only. So, if you have in mind a game, there must still be a network infrastructure connecting all the players, wherein each player will transmit his or her GPS location to the other players over the network. The WP7 platform supports Xbox Live for networking, so that is likely the next subject to study if you’re interested in making a networked game.

Drawing with Z-Index Ordering

Prioritized Drawing

We already have the ability to perform prioritized “z-index” drawing of a sprite image with the SpriteBatch.Draw() method, and we have been using the z-index parameter all along, just set to a value of zero. This effectively gave every sprite the same priority. When that is the case, priority will be based entirely on the order at which sprites are drawn (in gameplay code). SpriteBatch.Draw() has the capability to automatically prioritize the drawing of some sprites over the top of other sprites using this z-index buffer.

What does “z-index” mean, you may be wondering? When doing 2D sprite programming, we deal only with the X and Y coordinates on the screen. The Z coordinate, then, is the position of the sprite in relation to other sprites that overlap each other. The range for the z-index goes from 0.0 to 1.0. So, a sprite with a z-index priority of 0.6 will draw over a sprite with a z-index priority of 0.3. Note that 1.0 is the highest, and 0.0 is the lowest.

Sprite Class Changes

Adding Z-Buffering to the Sprite Class

A very minor change is required to enable z-index drawing in our Sprite class. We’ll go over those changes here.

  1. Add a zindex variable to the class:
    [code]
    public float zindex;
    [/code]
  2. In the Sprite class constructor, initialize the new variable:
    [code]
    zindex = 0.0f;
    [/code]
  3. Replace the hard-coded zero with the zindex variable in the SpriteBatch.Draw() calls inside Sprite.Draw() (there are two of them):
    [code]
    public void Draw()
    {
    if (!visible) return;
    if (totalFrames > 1)
    {
    Rectangle source = new Rectangle();
    source.X = (frame % columns) * (int)size.X;
    source.Y = (frame / columns) * (int)size.Y;
    source.Width = (int)size.X;
    source.Height = (int)size.Y;
    p_spriteBatch.Draw(image, position, source, color,
    rotation, origin, scale, SpriteEffects.None, zindex);
    }
    else
    {
    p_spriteBatch.Draw(image, position, null, color, rotation,
    origin, scaleV, SpriteEffects.None, zindex);
    }
    }
    [/code]

Adding Rendering Support for Z-Buffering

To use a z-buffer for prioritized drawing, a change must be made to the call to SpriteBatch.Begin(), which gets drawing started in the main program code. No change is made to SpriteBatch.End(). There are actually five overloads of SpriteBatch.Begin()! The version we want requires just two parameters: SpriteSortMode and BlendState. Table 16.1 shows the SpriteSortMode enumeration values.

SpriteSortMode Enumeration

The default sorting mode is Deferred, which is what SpriteBatch uses when the default Begin() is called. For z-index ordering, we will want to use either BackToFront or FrontToBack. There is very little difference between these two except the weight direction of each sprite’s z-index.

When using BackToFront, smaller z-index values have higher priority, with 0.0 being drawn over other sprites within the range up to 1.0.

When using FrontToBack, the opposite is true: Larger z-index values (such as 1.0) are treated with higher priority than lower values (such as 0.0).

It works best if you just choose one and stick with it to avoid confusion. If you think of a z-index value of 1.0 being “higher” in the screen depth, use FrontToBack. If 0.0 seems to have a higher priority, use BackToFront.

In our example, we will use FrontToBack, with larger values for the z-index having higher priority.

Z-Index Demo

To demonstrate the effect z-index ordering has on a scene, I have prepared an example that draws a screen full of sprites and then moves a larger sprite across the screen. Based on the z-index value of each sprite, the larger sprite will appear either over or under the other sprites. In this example, the screen is filled with animated asteroid sprites, and the larger sprite is an image of a shuttle. Figure 16.1 shows the demo with the shuttle sprite drawn on top of the first half of the asteroids.

The shuttle appears over the first half of the rows.
FIGURE 16.1 The shuttle appears over the first half of the rows.

Figure 16.2 shows the shuttle sprite farther across the screen, where it appears under the second half of the rows of asteroid sprites (which have a higher z-index than the shuttle sprite).

The shuttle appears under the second half of the rows.
FIGURE 16.2 The shuttle appears under the second half of the rows.

Wrapping Around the Screen Edge

To assist with this demo, a new Animation subclass has been written to automatically wrap a sprite around the edges of the screen. This completely eliminates any gameplay code that would otherwise have to be added to the Update() method. This is called synergy—when a combination of simple things produces awesome results! We’re starting to see that happen with our sprite and animation code now. Wrapping around the edges of the screen might be considered more of a behavior than an animation.

[code]
public class WrapBoundary : Animation
{
Rectangle boundary;
public WrapBoundary(Rectangle boundary)
: base()
{
animating = true;
this.boundary = boundary;
}
public override Vector2 ModifyPosition(Vector2 original)
{
Vector2 pos = original;
if (pos.X < boundary.Left)
pos.X = boundary.Right;
else if (pos.X > boundary.Right)
pos.X = boundary.Left;
if (pos.Y < boundary.Top)
pos.Y = boundary.Bottom;
else if (pos.Y > boundary.Bottom)
pos.Y = boundary.Top;
return pos;
}
}
[/code]

Z-Index Demo Source Code

Listing 16.1 contains the source code for the Z-Index Demo. Note the code highlighted in bold—these lines are relevant to our discussion of z-buffering.

LISTING 16.1 Source Code for the Z-Index Demo

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
TouchLocation oldTouch;
Random rand;
SpriteFont font;
List<Sprite> objects;
Texture2D asteroidImage;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
oldTouch = new TouchLocation();
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
rand = new Random();
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
//create object list
objects = new List<Sprite>();
//create shuttle sprite
Sprite shuttle = new Sprite(Content, spriteBatch);
shuttle.Load(“shuttle”);
shuttle.scale = 0.4f;
shuttle.position = new Vector2(0, 240);
shuttle.rotation = MathHelper.ToRadians(90);
shuttle.velocityLinear = new Vector2(4, 0);
Rectangle bounds = new Rectangle(-80, 0, 800 + 180, 480);
shuttle.animations.Add(new WrapBoundary(bounds));
shuttle.zindex = 0.5f;
objects.Add(shuttle);
//load asteroid image
asteroidImage = Content.Load<Texture2D>(“asteroid”);
//create asteroid sprites with increasing z-index
for (int row = 0; row < 13; row++)
{
float zindex = row / 12.0f;
for (int col = 0; col < 8; col++)
{
Sprite spr = new Sprite(Content, spriteBatch);
spr.image = asteroidImage;
spr.columns = 8;
spr.totalFrames = 64;
spr.animations.Add(new FrameLoop(0, 63, 1));
spr.position = new Vector2(30 + 60 * row, 30 + col * 60);
spr.size = new Vector2(60, 60);
spr.zindex = zindex;
objects.Add(spr);
}
}
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
foreach (Sprite spr in objects)
{
spr.Rotate();
spr.Move();
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend);
foreach (Sprite spr in objects)
{
spr.Animate();
spr.Draw();
}
spriteBatch.End();
base.Draw(gameTime);
}
}
[/code]

Transforming Frame Animations

Drawing Frames with Color Mods

We will first test the frame animation system with color modifications, then get into transform mods, and then create a new frame animation mod system as well.

Pivot Optimization

The sprite’s pivot point used for rotation, called origin, is a potentially serious source of difficulty and head scratching. When recurring events or calculations can be automated, they should be. Anytime the sprite frame size is changed, the origin should be modified along with it. This calls for either a method or a property. Presently, both size and origin are defined as public Vector2 variables in Sprite:

[code]
public Vector2 size;
public Vector2 origin;
[/code]

We want to make a change so that it doesn’t wreck any previous code that relies on the Sprite class, although those prior examples could just use an earlier version of the class if necessary. Perhaps the easiest way to do this is with a property, with size made private. This will require several changes in the Sprite class, as size is used quite a bit.

Sprite Class Modifications to Support, Well, Modifications

We will make changes to the Sprite class from the preceding chapter to support color and transform animation of a sprite with frame animation support.

  1. First, we’ll make a variable change in Sprite:
    [code]
    //public Vector2 size;
    private Vector2 p_size;
    [/code]
  2. Next, we’ll take care of initialization in the constructor:
    [code]
    //size.X = size.Y = 0;
    size = Vector2.Zero;
    [/code]
  3. Now we’ll work on the new property, with the changes made to the pivot/origin being done automatically:
    [code]
    public Vector2 size
    {
    get { return p_size; }
    set
    {
    p_size = value;
    origin = new Vector2(p_size.X / 2, p_size.Y / 2);
    }
    }
    [/code]
  4. One of the additional locations in the class where size is used is in Load(), which must be modified as follows. Note that with the property now being used, the new line of code causes origin to be changed as well! Now we’re already starting to see the benefit of this property right inside the Sprite class, and it will be equally helpful outside. Now, we can still manually change the origin whenever needed (for instance, recall the clock example from Hour 13, which needed to set the origin for the clock hands manually for it to rotate correctly). Just remember that changing size automatically changes the pivot point (origin).
    [code]
    public bool Load(string assetName)
    {
    try
    {
    image = p_content.Load<Texture2D>(assetName);
    origin = new Vector2(image.Width / 2, image.Height / 2);
    }
    catch (Exception) { return false; }
    //size.X = image.Width;
    //size.Y = image.Height;
    size = new Vector2(image.Width, image.Height);
    return true;
    }
    [/code]

Revisiting CycleColorBounce

The CycleColor class was introduced back during Hour 12 as a way to fade any color in a desired direction, going to fully red, or full whiteout, or fade to black, or any combination in between. A subclass was then developed, called CycleColorBounce, that could do the same color transforms, but would not automatically stop animating when the limits were reached. Instead, it would continue to cycle. We also considered a class called SolidColor that did no real color changes, but that would be helpful in some cases when just a solid color is needed without changing the base Sprite.color property manually. In other words, without creating an additional “old color” style property in the class, SolidColor allowed a sprite to change color temporarily and then return.

Despite the relative simplicity of color modification in principle, it turns out that— ironically—our color mod classes are the most complex. Here again in Listing 15.1 are the Animation subclasses for reference, since we are using them in the example.

LISTING 15.1 Review of CycleColor Class for Reference (Not New Code)

[code]
public class CycleColor : Animation
{
public int red, green, blue, alpha;
public CycleColor(int red, int green, int blue, int alpha)
: base()
{
this.red = red;
this.green = green;
this.blue = blue;
this.alpha = alpha;
animating = true;
}
public override Color ModifyColor(Color original)
{
Color modified = original;
if (animating)
{
int R = original.R + red;
int G = original.G + green;
int B = original.B + blue;
int A = original.A + alpha;
if (R < 0 || R > 255 || G < 0 || G > 255 ||
B < 0 || B > 255 || A < 0 || A > 255)
{
animating = false;
}
modified = new Color(R, G, B, A);
}
return modified;
}
}
public class CycleColorBounce : CycleColor
{
public int rmin, rmax, gmin, gmax, bmin, bmax, amin, amax;
public CycleColorBounce(int red, int green, int blue, int alpha)
: base(red,green,blue,alpha)
{
rmin = gmin = bmin = amin = 0;
rmax = gmax = bmax = amax = 255;
}
public override Color ModifyColor(Color original)
{
Color modified = original;
if (animating)
{
int R = original.R + red;
int G = original.G + green;
int B = original.B + blue;
int A = original.A + alpha;
if (R < rmin)
{
R = rmin;
red *= -1;
}
else if (R > rmax)
{
R = rmax;
red *= -1;
}
if (G < gmin)
{
G = gmin;
green *= -1;
}
else if (G > gmax)
{
G = gmax;
green *= -1;
}
if (B < bmin)
{
B = bmin;
blue *= -1;
}
else if (B > bmax)
{
B = bmax;
blue *= -1;
}
if (A < amin)
{
A = amin;
alpha *= -1;
}
else if (A > amax)
{
A = amax;
alpha *= -1;
}
modified = new Color(R, G, B, A);
}
return modified;
}
}
[/code]

Listing 15.2 shows some source code showing how we will test color modification in the upcoming example; we will delay going into the example until the next section, which features a combined example.

LISTING 15.2 Sample Source Code Demonstrating the Latest Sprite Changes

[code]
//create skeleton sprite
skelly = new Sprite(Content, spriteBatch);
skelly.Load(“skeleton_attack”);
skelly.position = new Vector2(425, 240);
skelly.size = new Vector2(96,96);
skelly.scale = 2.0f;
skelly.columns = 10;
skelly.totalFrames = 80;
skelly.animations.Add(new CycleColorBounce(1, 2, 2, 0));
objects.Add(skelly);
[/code]

We haven’t tried to do anything this complex yet. The question is, will it work—will color and frame animations work correctly “out of the box” or will changes be needed?

The particular sprite sheet used for the color mod example is shown in Figure 15.1. As you can see, it is an animated skeleton with a sword (courtesy of Reiner Prokein via his website, http://www.reinerstilesets.de). We will see this awesome skeleton sprite in the upcoming example.

Animated skeleton warrior used as an example with color modification.
FIGURE 15.1 Animated skeleton warrior used as an example with color modification.

Drawing Frames with Transform Mods

Now we will test the frame animation system with transform modifications. Color modification is rather trivial compared to transform modifications, which involve gradually affecting movement, rotation, or scaling over time, very much in parallel with how frame animation changes the image over time. When we’re dealing with transforms, though, the image size and position are crucial for these mods to work correctly. If an image is loaded in from a bitmap file that has dimensions of 1024×1024, and then transforms are applied to the sprite based on this image size, most likely the sprite will not even show up on the WP7 screen, because frame animation is also being done, and the image is being thrown out of the range of the screen, in all likelihood. So, the initial setup of the sprite’s properties is essential to make transformed frame animation work correctly.

Figure 15.2 shows the sprite sheet for the animated swordsman also featured in the example (also courtesy of Reiner Prokein, http://www.reinerstilesets.de).

Animated swordsman used to demo two combined animations.
FIGURE 15.2 Animated swordsman used to demo two combined animations.

Custom Frame Animation

We have been using the simple version of the Sprite.Animate() method so far, passing just the time value to cause the animation to run at a consistent frame rate. But there is a second version of Sprite.Animate() that is called from this first one, giving us the ability to fine-tune or manually intervene in the way the animation runs. Here are the two methods again for reference (covered in the preceding hour):

[code]
public void Animate(double elapsedTime)
{
Animate(0, totalFrames-1, elapsedTime, 30);
}
public void Animate(int startFrame, int endFrame,
double elapsedTime, double speed)
{
if (totalFrames <= 1) return;
startTime += elapsedTime;
if (startTime > speed)
{
startTime = 0;
if (++frame > endFrame) frame = startFrame;
}
}
[/code]

The second method allows us to pass the start frame and end frame for a specific range of frames to be animated. By default, the first Animate() method just draws all frames (0 to totalFrames-1). This is a simple looped animation: When the last frame is reached, the animation system loops back around to the first frame again. But there are more advanced forms of frame animation that can be performed as well, such as a frame bounce rather than a loop: When the last frame is reached, the animation direction reverses downward toward the first frame again, and then reverses again in the positive direction. This can be actually quite useful if the artwork has only half of the total frames needed to create a complete loop, and instead the bounce technique must be used.

Now we come to the dilemma: The game loop does not have room for custom, manual method calls! Here is the code in Update() that updates the sprite list:

[code]
//update all objects
foreach (Sprite spr in objects)
{
spr.Rotate();
spr.Move();
}
And here is the code in Draw() that animates and draws all the sprites:
foreach (Sprite spr in objects)
{
spr.Animate(); //mods
spr.Animate(gameTime.ElapsedGameTime.Milliseconds); //frames
spr.Draw();
}
[/code]

It is true that we can jump into these foreach loops and make manual changes to a specific sprite in the list. Or we can just leave a sprite out of the list and handle it separately. That works and there’s nothing wrong with that approach. But it kind of defeats the benefits of having an automated game loop, in which sprites are updated and drawn automatically based on properties. This describes a data-driven game loop, and it is simply unmatched in versatility and power when it comes to building a complex game. In other words, we want this to work correctly; we really don’t want to bypass it. If a new feature is needed, it’s better to work that into the loop than to make exceptions.

What we need is a new modification method in the Animation class. Do you see where we’re going with this?

Updating the Animation Class

The Sprite class will retain the two new Animate() methods from the preceding hour, and that form of frame animation can be used when it will suffice. But we will now add a more versatile frame animation system that uses the Animation class, fully merging the functionality of color and transform animation with frame animation. This calls for some changes to the Animation class, but no changes to the Sprite class are needed because it already supports multiple animations.

Add the following new method called ModifyFrame() to the Animation class as shown:

[code]
public class Animation
{
. . . some portions skipped
public virtual int ModifyFrame(int current)
{
return current;
}
}
[/code]

New Modifications to the Sprite Class

To make the new frame animation work with Sprite, some things must be modified.

  1. A new variable, frameDir, must be added to the class:
    [code]
    public int columns, frame, totalFrames, frameDir;
    [/code]
  2. It is initialized in the constructor:
    [code]
    columns = 1;
    frame = 0;
    totalFrames = 1;
    frameDir = 1;
    [/code]
  3. The Animate() method can be updated like so (note the line in bold). Remember, this call to ModifyFrame() is the default or base method call, and the current frame is passed as the sole parameter. Any class that inherits from Animate and overrides ModifyFrame() will have the animation range set to specific values.
    [code]
    public void Animate()
    {
    if (animations.Count == 0) return;
    foreach (Animation anim in animations)
    {
    if (anim.animating)
    {
    color = anim.ModifyColor(color);
    position = anim.ModifyPosition(position);
    rotation = anim.ModifyRotation(rotation);
    scaleV = anim.ModifyScale(scaleV);
    frame = anim.ModifyFrame(frame);
    }
    else
    {
    animations.Remove(anim);
    return;
    }
    }
    }
    [/code]

New Animation Subclass Examples

Strangely enough, that’s it! That’s all we had to do to add frame animation support to the Animation class. Now we can create custom subclasses that inherit from Animation to create any kind of special frame animation we need. Listing 15.3 gives two sample animations involving frame modifications that you can study as a reference.

LISTING 15.3 New Animations

[code]

public class FrameLoop : Animation
{
public FrameLoop(int start, int end, int current, int direction)
: base()
{
animating = true;
}
public override int ModifyFrame(int start, int end, int current,
int direction)
{
current += direction;
if (current > end)
current = start;
else if (current < start)
current = end;
return current;
}
}
public class FrameBounce : Animation
{
public FrameBounce(int start, int end, int current, int direction)
: base()
{
animating = true;
}
public override int ModifyFrame(int start, int end, int current,
int direction)
{
current += direction;
if (current > end)
{
direction *= -1;
current = end;
}
else if (current < start)
{
direction *= -1;
current = start;
}
return current;
}
}
[/code]

If you want frame animation to continue to function as it did before, one option is to automatically add a FrameLoop object to the animation list when a sprite is created (from the Sprite constructor). But it’s probably better to just work with the more advanced system now in place.

The FrameLoop class duplicates the existing functionality of parameterized Sprite.Animate(), which updates frame animation. Those two methods could now be removed since FrameLoop replicates that functionality, but I will just leave them alone since they might be useful. Next, the FrameBounce class handles the situation in which an animation will go from start frame to end frame, and then reverse direction down toward start frame again, rather than looping.

Figure 15.3 shows an animated dragon that will also be used in the example.

 Animated dragon used to demo OrbitalMovement and FrameBounce.
FIGURE 15.3 Animated dragon used to demo OrbitalMovement and FrameBounce.

As of the preceding hour, we had an updated Draw() that handles sprite frame animation like so:

[code]
foreach (Sprite spr in objects)
{
spr.Animate(); //mods
spr.Animate(gameTime.ElapsedGameTime.Milliseconds); //frames
spr.Draw();
}
[/code]

The second line in the method, the second call to Animate(), can now be removed, after FrameLoop or FrameBounce has been added as an animation. But if you do remove that line, you must be sure to add FrameLoop or FrameBounce (or some variation thereof) to the animation list after a sprite has been created. For instance:

[code]
//create dragon sprite
dragon = new Sprite(Content, spriteBatch);
dragon.Load(“dragon”);
dragon.animations.Add(new FrameLoop(48, 56, 48, 1));
[/code]

Animation Mods Demo

We need a complete example to demonstrate how the new improvements to the animation system work. Figure 15.4 shows the final output of the demo for this hour. On the left is the swordsman character animated with FrameLoop and ThrobBounce.

The final Animation Mods Demo program animates three sprites with several animations at once.
FIGURE 15.4 The final Animation Mods Demo program animates three sprites with several animations at once.

The New ThrobBounce Class

The ThrobBounce class inherits from Throb and perpetuates the “throb” animation by overriding the animating property (making it continue when the animation normally ends). For reference, the original Throb class is shown here in Listing 15.4 as well since it is inherited.

LISTING 15.4 New ThrobBounce Class (Throb Repeated for Reference)

[code]
public class Throb : Animation
{
public float startScale, endScale, speed;
private bool p_started;
public Throb(float startScale, float endScale, float speed)
: base()
{
p_started = false;
animating = true;
this.startScale = startScale;
this.endScale = endScale;
this.speed = speed;
}
public override Vector2 ModifyScale(Vector2 original)
{
if (!animating) return original;
Vector2 modified = original;
if (!p_started)
{
modified.X = startScale;
modified.Y = startScale;
p_started = true;
}
modified.X += speed;
modified.Y += speed;
if (modified.X >= endScale)
speed *= -1;
else if (modified.X <= startScale)
animating = false;
return modified;
}
}
public class ThrobBounce : Throb
{
public ThrobBounce(float startScale, float endScale, float speed)
: base(startScale, endScale, speed)
{
}
public override Vector2 ModifyScale(Vector2 original)
{
//keep it going
Vector2 scale = base.ModifyScale(original);
if (!animating)
{
animating = true;
speed *= -1;
}
return scale;
}
}
[/code]

The Animation Mods Demo Source Code

Here in Listing 15.5 is the complete source code for the Animation Mods Demo that wraps up all the techniques of this hour in one short example.

LISTING 15.5 Source Code for the Animation Mods Demo Program

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
TouchLocation oldTouch;
Random rand;
SpriteFont font;
List<Sprite> objects;
Sprite swordsman, skelly, dragon;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
oldTouch = new TouchLocation();
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
rand = new Random();
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
//create object list
objects = new List<Sprite>();
//create swordsman sprite
swordsman = new Sprite(Content, spriteBatch);
swordsman.Load(“swordsman_walking”);
swordsman.position = new Vector2(150, 240);
swordsman.size = new Vector2(96, 96);
swordsman.columns = 8;
swordsman.totalFrames = 64;
swordsman.animations.Add(new FrameLoop(0, 63, 1));
swordsman.animations.Add(new ThrobBounce(0.5f, 4.0f, 0.1f));
objects.Add(swordsman);
//create skeleton sprite
skelly = new Sprite(Content, spriteBatch);
skelly.Load(“skeleton_attack”);
skelly.position = new Vector2(425, 240);
skelly.size = new Vector2(96,96);
skelly.scale = 2.0f;
skelly.columns = 10;
skelly.totalFrames = 80;
skelly.animations.Add(new FrameLoop(0, 79, 1));
skelly.animations.Add(new CycleColorBounce(1, 2, 2, 0));
objects.Add(skelly);
//create dragon sprite
dragon = new Sprite(Content, spriteBatch);
dragon.Load(“dragon”);
dragon.position = new Vector2(700, 240);
dragon.size = new Vector2(128, 128);
dragon.scale = 1.5f;
dragon.columns = 8;
dragon.totalFrames = 64;
dragon.animations.Add(new FrameBounce(48, 55, 1));
dragon.animations.Add(new OrbitalMovement(
dragon.position, 50, 0, 0.1f));
objects.Add(dragon);
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
//update all objects
foreach (Sprite spr in objects)
{
spr.Rotate();
spr.Move();
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
foreach (Sprite spr in objects)
{
spr.Animate();
spr.Draw();
}
spriteBatch.DrawString(font, ““, Vector2.Zero, Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
[/code]

The sprite animation system is now completely functional, with lots of room for growth and with support for custom Animation subclasses. In a professional game, these subclasses would be programmed by designers with Lua script code, allowing designers to make their own custom animations, and the job of the programmers essentially being done at this point with regard to the Animation and Sprite classes. But we aren’t getting into scripting, so feel free to create your own awesome new animation classes to see what interesting effects you can come up with! In the following hour, we will study one last subject related to sprites: z-index ordering. This will allow our sprites to draw over each other with some having higher priority than others.