The Black Hole Game

0
409

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!