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!

Using IronPython with Mono

What Is Mono?

Mono (http://www.mono-project.com/) is a run time along the same lines as the .NET Framework, and it includes much of the functionality of the .NET Framework. In fact, with each release, Mono gets a bit closer to .NET Framework functionality. However, don’t get the idea that Mono will ever exactly match the .NET Framework. Platform differences, Microsoft copyrights, and other issues will always keep Mono just a bit different from the .NET Framework. Even so, Mono can run a considerable number of .NET applications. The following sections describe Mono, its advantages and limitations, in greater detail.

An Overview of the Mono Family

You can obtain Mono for a considerable number of platforms. In fact, the makers of Mono add new platforms with every release. At one time, Mono worked on just a few Linux implementations, Windows, and the Mac OS X. Over time, Mono support has increased to the exciting list of platforms that follows.

  • LiveCD: This is actually an openSUSE 11.2.1 (http://www.opensuse.org/en/) LiveCD (a CD or DVD that contains a bootable image — see http://en.wikipedia.org/wiki/ Live_CD for details) that includes Mono 2.6.1.
  • Mac OS X: You can use this installation on a number of Mac versions including Mac OS X Tiger (10.4), Leopard (10.5), and Snow Leopard (10.6) (it may work on other versions as well, but you’re on your own for support). The download includes Mono, Cocoa#, and Gtk# (GIMP Toolkit Sharp). You need to download the Client Software Development Kit (CSDK), available on the Mono site, separately. There are separate downloads for the Intel and PowerPC platforms. You can learn more about Mac OS X at http://www.apple.com/macosx/.
  • openSUSE: You can use this download for the openSUSE 11.0, 11.1, and 11.2 platforms. You must have your own system with openSUSE installed to use it. You can download openSUSE at http://software.opensuse.org/. Just in case you’re interested, the SUSE part of the name stands for Software und System-Entwicklung, which translates to software and systems development.
  • SLES/SLED: You can use this download for SUSE Linux Enterprise Server (SLES) or SUSE Linux Enterprise Desktop (SLED). SLES and SLED are the paid versions of SUSE from Novell. As with openSUSE, you must have your own system with SLES or SLED installed to use this version of Mono. You can find out more about SLES and SLED at http://www.novell.com/linux/.
  • Virtual PC: This is actually an openSUSE 11.2.1 virtual PC image that includes Mono 2.6.1. You could use this download to check out Linux functionality for your IronPython application on your PC without leaving Windows. Of course, performance won’t be very good, but it will get the job done.
  • VMware: This is actually an openSUSE 11.2.1 VMware image that includes Mono 2.6.1. You’d use it to check your application for Linux functionality without leaving the host operating system.
  • Windows: You can officially use this download for Windows 2000, XP, 2003, and Vista. Testing shows that it also works fine for Windows 7 and Windows Server 2008. The download includes Mono for Windows, Gtk# (a graphics library to display a user interface onscreen), and XSP (eXtensible Server Pages, an alternate Web server for serving ASP.NET pages). You can also get the Mono Migration Analyzer tool as a separate download.
  • Other: This is a group of less supported platforms including Debian and Ubuntu. At least these two platforms have supported packages. You can also get Mono in an unsupported form for Solaris, Nokia, and Maemo. Theoretically, you could support yet other platforms by compiling the source code found at http://ftp.novell.com/pub/mono/sources-stable/.

Of course, this list contains only a summary of the main Mono downloads. There are a large number of Mono add-ons a well. For example, you can obtain Mono Tools for Visual Studio (http:// go-mono.com/monotools/download/) if you want to work with Mono directly from Visual Studio. Unfortunately, the current version of this product only works with Visual Studio 2008. The developer should provide a Visual Studio 2010 version soon. You can obtain a trial version of Mono Tools for Visual Studio (registration is required), but you must pay for the full version.

IronPython does include support for Silverlight development. If you plan to use IronPython for Web applications and need to support multiple platforms, you might want to look at Moonlight (http:// mono-project.com/Moonlight) instead. This Silverlight replacement works on the same platforms that Mono does and should also work fine with IronPython.

Some of the extensions to Mono are well outside the scope of this book, but are interesting to contemplate. For example, you can get Mono Touch (http://monotouch.net/) to develop applications for the iPhone and iPod Touch devices. The point is that you can probably find some form of Mono to meet just about any need, but using Mono fully means learning some new techniques, such as creating user interfaces using Gtk#.

Considering the Reasons for Using Mono

You already know the reasons that you’re using the .NET Framework and this chapter isn’t about changing your mind. The .NET Framework is stable and many developers love the functionality it provides them for building great applications. However, you could think of Mono as another tool to extend the range of your applications. If for no other reason, the fact that you could run your IronPython application on Linux or the Mac OS X makes Mono a good choice for some forms of application development. In sum, the main reason for using Mono in place of the .NET Framework is flexibility.

As previously mentioned, Mono and the .NET Framework aren’t precisely the same. The first thought that most developers will have is that compatibility issues will be bad, and to a certain extent, they do cause problems. However, Mono also provides functionality that you won’t find when working with the .NET Framework. Features such as Gtk# actually make Mono a better product. In addition, with Mono you have a lightweight Web server for ASP.NET pages, XSP, that works on every Mono platform. Therefore, the differences between Mono and the .NET Framework aren’t always bad — sometimes they become downright useful.

Mono does provide direct support for IronPython, but you need to use a newer version of Mono (see http://www.mono-project.com/Python for details). The support isn’t all that good. The section “Running the Application from the Command Line” later in this chapter demonstrates the problem of using the Mono implementation of IronPython. Even so, you do get IronPython support that will likely improve as Mono improves, so this is an area where you can expect Mono to grow as an IronPython platform. In reality, the Mono community is quite excited about IronPython. You can find tutorials for using IronPython in a Mono environment at http://zetcode.com/tutorials/ ironpythontutorial/. If you want to see IronPython running under Mono on a Linux system, see the screenshot and description at http://www.ironpython.info/index.php/Mono.

Understanding Mono Limitations

Don’t get the idea that every .NET application will instantly run on Mono. For example, while Mono includes support for Language Integrated Query (LINQ), the support isn’t perfect. The LINQ to SQL support works fine for many applications, but not all of them. The Mono developers realize that the support isn’t complete and they plan to work on it (see the release notes at http://www.mono-project .com/Release_Notes_Mono_2.6.1 for details).

There are some obvious limitations for using Mono that should come to mind immediately. Because the purpose of Mono is to work across platforms, the P/Invoke calls in your extensions aren’t going to work. A P/Invoke call causes your extension to provide Windows-specific support, so using it on Linux wouldn’t work no matter what product you tried. The previous chapters in the book have emphasized when a particular technique is unlikely to produce useful cross-platform results.

The current version of Mono doesn’t work with .NET Framework 4.0 applications. The applications won’t start at all — you see an error message instead. However, Mono does work fine with older versions of the .NET Framework. It’s only a matter of time before Mono supports the .NET Framework 4.0, so this is a short-term limitation that you can easily overcome by using an older version of the .NET Framework when building your application. Given that IronPython doesn’t currently support the .NET Framework 4.0 in many respects, this particular problem isn’t much of an issue for IronPython developers.

In a few cases, you have to look around to determine whether you’ll encounter problems using Mono for a particular task. For example, if your ASP.NET application uses Web Parts, you can’t use Mono (see http://www.mono-project.com/ASP.NET). You also can’t use a precompiled updateable Web site.

Using Mono on Windows Server 2008 Server Core

Early versions of Windows Server 2008 Server Core (Server Core for short) don’t come with any form of the .NET Framework. Consequently, you can’t run any form of .NET application on early versions of Server Core unless you use Mono. The lack of .NET Framework support on Server Core led some people to come up with odd solutions to problems, such as running PowerShell (see the solution at http://dmitrysotnikov.wordpress.com/2008/05/15/powershell-on-server-core/).

Fortunately, Microsoft decided to provide a limited version of the .NET Framework for Windows Server 2008 Server Core Edition R2. You can read about it at http://technet.microsoft.com/ library/dd883268.aspx. However, this version of the .NET Framework still has significant limitations and you might actually find it better to use Mono for your .NET applications. For example, while you can now provide limited support for ASP.NET on Server Core, you might actually find the Mono alternative, XSP, to provide the solutions you need for your application.

Mono has generated quite a bit of interest from the Server Core crowd, especially anyone who uses Server Core as their main server. Server Core has a number of advantages that makes it popular with small- to medium-sized companies. It uses far less memory and other resources, runs faster, runs more reliably, and has a far smaller attack surface for those nefarious individuals who want to ruin your day by attacking your server. You can find a complete article about running applications on Server Core using Mono at http://www.devsource.com/c/a/Architecture/ Mixing-Server-Core-with-NET-Applications/.

Obtaining and Installing Mono

It’s time to obtain and install your copy of Mono. Of course, the first step is to download the product. You can find the various versions of Mono at http://www.go-mono.com/mono-downloads/ download.html. This section assumes you’re installing Mono version 2.6.1 on a Windows system. If you need to install Mono on another system, follow the instructions that the Mono Web site provides for those versions. After you complete the download, follow these steps to perform the installation.

  1. Double-click the mono-2.6.1-gtksharp-2.12.9-win32-1.exe file you downloaded from the Mono Web site. You see a Welcome page.
  2. Click Next. You see a License page.
  3. Read the licensing information. Select I Accept the Agreement, and then click Next. You see the Information page shown in Figure 19-1. Unlike most Information pages, this one actually contains a lot of useful information. Make sure you review the information it contains and click on the links it provides as needed. Especially important for keeping updated on Mono is joining the mailing list (http://www.mono-project.com/Mailing_Lists) or forums (http://www.go-mono.org/forums/). You can find these links at the bottom of the Information page.

    Make sure you review this Information page because it contains useful information.
    Figure 19-1: Make sure you review this Information page because it contains useful information.
  4. Read the release information and then click Next. You see the Select Destination Location page shown in Figure 19-2. Normally, you can accept the default installation location. Some developers prefer a less complex path to Mono, such as simply C:Mono, to make it easier to access from the command line. The chapter uses the default installation location.

    Select an installation location for Mono.
    Figure 19-2: Select an installation location for Mono.
  5. Provide an installation location for Mono and then click Next. You see the Select Components page shown in Figure 19-3. The components you select depend on what you plan to do with Mono — you can always change your setup later if necessary. If your only goal is to try Mono for your existing .NET applications and to create some simple IronPython applications, you really don’t need the Gtk# and XSP support. This chapter assumes that you perform a Compact Installation to obtain a minimum amount of support for working with the IronPython sample application.

    Choose the Mono components that you want to install.
    Figure 19-3: Choose the Mono components that you want to install.
  6. Select the components you want to install and then click Next. You see the Select Start Menu Folder page. This is where you choose a name for the folder that holds the Mono components. The default name normally works fine.
  7. Type a name for the Start menu folder (or simply accept the default) and then click Next. You see the Ready to Install page. This page provides a summary of the options that you’ve selected.
  8. Review the installation options and then click Install. You see the Installing page while the installer installs Mono on your machine. After a few minutes, you see a completion dialog box.
  9. Click Finish. You’re ready to begin using Mono.

Creating an IronPython Application with Mono

It’s time to begin working with Mono and IronPython to create an application. Of course, you’ll want to know a bit more about how Mono works before you just plunge into the project, so the first step is to look at Mono from a command line perspective. The first section that follows shows how to create an IPY environment variable and use it to open the IronPython console using Mono whenever you need it. The sections that follow show how to create a project, build a simple IronPython application, and then test the application in a number of ways.

Working at the Command Line

Mono works differently than the .NET Framework. When you want to use the .NET Framework to execute an application, you simply double-click the application and it starts. The same doesn’t hold true for Mono. If you want to execute an application using Mono, you must open the Mono command prompt and start it by specifically specifying Mono. Unfortunately, this limitation has an unusual effect on working with IronPython because you can no longer access IPY.EXE using the Path environment variable. Instead, you must create a special IPY environment variable using the following steps.

  1. Double-click the System applet in the Control Panel and choose the Advanced tab. You see the System Properties dialog box.
  2. Click Environment Variables. You see the Environment Variables dialog box.
  3. Click New in the System Variables section of the Environment Variables dialog box if you want to use IronPython from any account on the machine or the User Variables section if you want to use IronPython only from your personal account. You see a New System Variable or New User Variable dialog box. Except for the title, both dialog boxes are the same.
  4. Type IPY in the Variable Name field.
  5. Type C:Program FilesIronPython 2.6 or the location of your IronPython installation in the Variable Value field.
  6. Click OK three times to add the new environment variable, close the Environment Variables dialog box, and close the System Properties dialog box. You’re ready to begin working with IronPython.

At this point, you’re ready to begin working with Mono. Choose Start ➪ Programs ➪ Mono 2.6.1 for Windows ➪ Mono-2.6.1 Command Prompt to display a Mono command prompt. When you see the Mono command prompt, type Mono “%IPY%IPY.EXE” and press Enter. You’ll see the usual IronPython console.

The first thing you should notice is that the .NET Framework version reporting by the IronPython console is slightly different from the one you normally see. There isn’t any problem with this difference. In fact, it’s the only difference you’re going to notice as you work with the IronPython console. Let’s give it a try so you can see for yourself. Type the following code and you’ll see the standard responses shown in Figure 19-4.

[code]
import sys
for ThisPath in sys.path:
print ThisPath
[/code]

Running IronPython under Mono doesn’t really look any different.
Figure 19-4: Running IronPython under Mono doesn’t really look any different.

If you compare the results you see when running IronPython under the .NET Framework with the results you see when running IronPython under Mono, you won’t notice any differences. In fact, you can try out the applications in this book, and you won’t see any differences at all unless you need to work with an extension or other outside code source (and you might not even see any differences then). Working with Mono simply means you have access to more platforms when working with IronPython, not that you have more limitations.

Defining the Project

The project you create for working with Mono is going to be just a little different from the one you create when working strictly with the .NET Framework. You’ll still start up IronPython using the Visual Studio IDE, but there’s an extra step now: you must start Mono first.

  1. Choose File ➪ Open ➪ Project/Solution. You see the Open Project dialog box shown in Figure 19-5.
    Use Mono as the starting point for your project.
    Figure 19-5: Use Mono as the starting point for your project.

     

  2. Highlight Mono.EXE in the Program FilesMono-2.6.1bin folder of your machine (unless you used a different installation folder) and click Open. Visual Studio creates a solution based on Mono.
  3. Right-click the Mono entry in Solution Explorer and choose Properties from the context menu. You see the General tab of the Properties window shown in Figure 19-6.

    Set the Mono configuration for your project.
    Figure 19-6: Set the Mono configuration for your project.
  4. Type “C:Program FilesIronPython 2.6IPY.EXE“ -D TestMono.py in the Arguments field (change the folder location to match your IronPython installation).
  5. Click the ellipses in the Working Directory field to display the Browse for Folder dialog box. Locate the folder that contains the project you’re working on and click OK. The project folder appears in the Working Directory field of the Properties window.
  6. Choose File ➪ Save All. You see a Save File As dialog box.
  7. Type the solution name in the Object Name dialog box and click Save.
  8. Right-click the solution entry in Solution Explorer and choose Add ➪ New Item. You see the Add New Item dialog box.
  9. Highlight the Text File template. Type TestMono.py in the Name field and click Add. Visual Studio adds the Python file to your project and automatically opens it for you.

Creating the Code

It’s time to add some code to the IronPython file. This example provides a listing of the modules that IronPython is using. If you compare this list to the one that IronPython provides when you run the application using the .NET Framework, you’ll see the modules in a different order, but otherwise the output is the same. Listing 19-1 shows the code used for this example.

Listin g 19-1: Creating a simple Mono test program

[code]
# Obtain access to the sys module.
import sys
# Output a list of modules.
print ‘IronPython Module Listingn’
for ThisMod in sys.modules:
print ThisMod, sys.modules[ThisMod]
# Pause after the debug session.
raw_input(‘nPress any key to continue…’)
[/code]

This example demonstrates a simple for loop to iterate through the list of modules found in the sys .modules dictionary. In this case, the code prints out two items. First, it prints out the module name. Second, it prints out the module information, which normally includes the module location. As always, the code ends with a pause, raw_input(), so that you can see the output before the window closes.

Running the Application from the IDE

Running the application is the first place you see some potential problems with using Mono. If you click Start Debugging, you see the No Debugging Information dialog box shown in Figure 19-7. If you click Yes, the program will run, but you won’t get any debugging support. This is one of the problems with using Mono exclusively. You’ll probably want to use the normal .NET Framework setup to debug your application first, and then move on to the Mono configuration described in this chapter to test the application under Mono.

Mono doesn’t provide any debugging support that Visual Studio understands.
Figure 19-7: Mono doesn’t provide any debugging support that Visual Studio understands.

To start the application successfully, choose Debug ➪ Start Without Debugging or press Ctrl+F5. The program will run normally and you’ll see the usual message at the end. Pressing Enter displays a second pause as shown in Figure 19-8. It seems that Mono provides its own pause so that you can see the results of executing the program, which is a nice touch for those times when you forget to add a pause of your own.

IronPython displays the list of modules found in the current setup.
Figure 19-8: IronPython displays the list of modules found in the current setup.

Running the Application from the Command Line

Interestingly enough, Mono does come with direct support for IronPython, but Mono supports IronPython 1.1, and the IronPython console supplied with Mono seems to do odd things. Open a Mono command prompt, type IPY, and press Enter. Now try typing 1+1 and pressing Enter. You’ll probably see results like those in Figure 19-9.

The IronPython console provided with Mono leaves a lot to be desired.
Figure 19-9: The IronPython console provided with Mono leaves a lot to be desired.

Of course, the question isn’t about the IronPython console, but whether it can run the example application. Press Ctrl+C to break out of the mess you’re seeing onscreen. Type Y and press Enter when you’re asked whether you want to stop the batch file. Then type IPY TestMono.py and press Enter. You’ll see that the application does indeed work, as shown in Figure 19-10. The number of modules is far smaller than the list shown in Figure 19-8, but it’s correct for the version of IronPython provided with Mono.

You can run the test application using the Mono version of IronPython.
Figure 19-10: You can run the test application using the Mono version of IronPython.

The picture isn’t completely gloomy. Developers are constantly trying new solutions for working with IronPython. You can find a potential fix for the problems described in this section of the chapter at http://ironpython-urls.blogspot.com/2009/06/mono-can-now-compile-ironpython-20 .html. The solution comes with the caveat that it might not work for you.

Interacting with Other .NET Languages under Mono

Mono originally focused its attention on C# development, but later added support for Visual Basic .NET as well. At this point, you can run any Visual Studio 2008–created application written under C# or Visual Basic.NET using Mono within the limitations described in the section “Understanding Mono Limitations” earlier in this chapter. Even DLR code appears to run fine in Mono within the current limits of the product, which aren’t many.

 

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]

Using IronPython for Application Testing

Understanding Why You Want to Use IronPython for Testing

Every testing technique you’ve ever used has some drawback. For example, if you include debug statements in your code, you must ensure that you perform a release build to remove the statements before you release the code. Otherwise, the application will run slowly. In addition, using debug statements can cause the application to perform differently from the way it performs when you use it in a production environment, which makes it possible that the very tests that you depend on to check the application will actually hide problems from view.

Using IronPython for testing has a considerable number of benefits over other testing tools. The biggest benefit is that you don’t have to do anything special to the application. The test harness you create exists outside the application and doesn’t affect the application in any way. All the test harness does is monitor application behavior and report on it to you. As a result, if the test harness reviews every aspect of the application and verifies that it runs correctly, the application will run correctly in the production environment, too, because nothing will have changed.

As you’ve seen throughout the book, IronPython is an interpreted environment. That means you don’t have to create the test harness in one piece — you can create it a little at a time as you try things out with the application. In fact, the very nature of IronPython makes it possible for you to play “what if” analysis on your application. You can see just how bad you can make the application environment and try things that users do, such as create unreasonable execution conditions.

Using an IronPython script for testing means that all the testing code is in one place. If you decide that you need to add another test, you don’t have to delve into the inner workings of the application to add it and then create another build. Instead of using this time-consuming process, you simply add a few more lines to an external script using any text editor that you like. There’s nothing complicated about the process — anyone knowledgeable about your application should be able to do it without any problem.

The external nature of IronPython also makes it impossible for your test code to add problems (such as errors, performance issues, or reliability concerns) to the application. In some cases, adding test code actually introduces an application error, making it hard to know whether the error is in the test harness or the application. If there’s a problem in the IronPython test harness, you’ll see an IronPython error telling you about it. In short, you have separation between the test harness and the application, which ensures one won’t affect the other.

There are a few downsides to working with IronPython as a testing tool. The most important of these issues is that IronPython treats your application like a series of black boxes. It provides input to a method and expects a certain output. However, IronPython can’t see into the method to test individual elements within it.

IronPython also can’t see private members of your application, so it can’t test absolutely every aspect of your application. If a private member is causing a problem, you need to use some other tools to find it. Of course, you can use IronPython to infer certain issues in private methods based on their effect on public methods, but this kind of logic can prove more troublesome than direct testing.

Considering the Test Environment

Before you begin writing your test harness, you need to consider the test environment. The test environment determines how you test the application, be it a DLL or a desktop application with user access. The following list provides some criteria you need to consider as part of the test environment.

  • Code access: You must define how the test harness will access the code. It’s important to determine whether the harness will test absolutely every method, property, event, and other application element individually, whether it will test elements in combination, or whether it will use a combination of individual and combined tests.
  • Test ranges: A test harness must test both the possible and the impossible. For example, you might design a method to accept positive numbers from 0 through 5. However, the test harness must also test numbers greater than 5 and less than 0. In addition, it must test unexpected input, such as a string.
  • User emulation: When working with some applications, you must determine how to emulate user activity. For example, you might write down a series of steps that the user will take to perform a certain activity and then execute those steps in your test harness. Of course, users are unpredictable; your script must also perform some haphazard and unpredictable steps and provide unexpected input. If you find that users are doing something you never expected, you must add it to the test harness.
  • Security testing: If you don’t try to break down the walls you erected for your application, someone else will most certainly sign up for the job. Because IronPython tends to treat everything as public, it actually makes a great tool for testing security. You’ll find no artificial walls to keep things neat and tidy. Security is never neat or tidy — it’s all about someone ripping away the veneer of the façade you called security when you put the application together. IronPython lets you test your application brutally, the same way someone else will.
  • System characteristics: Even though you can’t write code to ensure that your application will run on every machine in the solar system, you can do things such as add random pauses in your code to mimic activity on an overloaded system. You can also execute your application and its test harness on a number of different machine configurations to verify that the application will run as expected.

There are probably other criteria that you need to consider for your individual testing scenario. Take time to brainstorm scenarios, worst-case situations, and truly horrifying events, and then test for them. The following sections provide some additional insights about the test environment and the issues you must consider.

Defining Access

The matter of access is an essential part of testing. The word “access” has all kinds of meanings and connotations. Of course, there’s the access of your test harness to the code within the application. The black box nature of IronPython prevents access in depth, but careful programming can provide access to unprecedented amounts of information within your application and make testing relatively complete.

You must also consider the access the user has to the application as part of the test harness. For example, if you use external configuration files, you can count on some number of users accessing them. Even if you work out methods that are seemingly impossible to overcome, a user or two will find a way to overcome them. Anything you can devise will be overcome by someone (it’s always easier to destroy than to create). Consequently, you must consider all forms of user access as part of your test harness — if for no other reason than to determine how bad things can get when a user meddles.

It’s also important to consider external access. Whenever a system has access to the network or the Internet, you must consider the potential for outside sources to access your application (even if your application isn’t designed for such access). Many vendors of shrink-wrapped software have gained notoriety for not taking this kind of access into consideration. The thought was that the application didn’t access the outside source, so there wasn’t any need to consider the outside source during testing. It turns out that any outside access opens avenues of influence and attack for all the applications on a system, so you must test this kind of access.

Access is a two-way street. As part of your testing harness, you must consider application access to external resources. For example, you must consider what happens when an application attempts to access a particular file on disk and can’t find it. Even more important, you need to consider resources on the network or on the Internet. There are many forms of access that your test harness must consider as it tests the various methods inside the application. It isn’t always possible to test simply for strict inputs or outputs; you must test inputs and outputs within the confines of an environment defined by various kinds of access.

Considering a Few Things IronPython Can’t Test

Earlier, you learned that IronPython tests application elements using a black box approach — given a particular input, what should the element provide as output? However, there are other limitations you need to consider in the way IronPython performs testing. For example, IronPython can’t perform stress testing. If you want to test your application in a memory-starved environment, then you need to combine IronPython with another tool. For example, you might want to read the article at http:// msdn.microsoft.com/magazine/cc163983.aspx about a load-generating tool you can build yourself. Web application load testing requires other techniques that you can learn about at http:// support.microsoft.com/kb/231282. If you need to stress test applications in combination with a particular server, check out the site at http://blogs.msdn.com/nickmac/archive/2004/10/06/ server-stress-tools.aspx.

IronPython can perform diagnostic testing of your application with ease, but it doesn’t make a good environment for performance testing. As with stress testing, you need to combine IronPython with another tool to check application performance in various conditions. In fact, you may very well need to combine IronPython, your stress testing tool, and your performance testing tool to obtain statistics for a range of test scenarios and environments.

The point of this section is that while IronPython is a good scripting tool or a good diagnostic tool, it can’t do everything. In many cases, you must combine IronPython with one or more additional tools to obtain the desired information about your application. Your test plan should include all of these contingencies, and you should consider them before you create your test harness.

Creating the Test Harness

An advantage to working with IronPython is that you need not create the test harness in one sitting. You can use an iterative technique to create the test harness. It’s possible to start with a small nugget of tests that you know you must perform, and then add to that nugget as other issues come to light. Eventually, you end up with a full-blown test suite.

Most .NET developers won’t initially understand the benefits of using an interpreter for testing, but the realization will grow with time that interpreters make things easy. If you get an idea, you don’t have to run a complete test or compile anything. All you need to do is open up the IronPython console, load the assembly you want to test, and then try out various tests until you come up with a perfect combination of items to use. At this point, you can click the system menu in the IronPython console, choose Edit ➪ Mark, highlight the text you want to copy from your experiments, and press Enter to copy it to the clipboard. Now you can paste the text you’ve created into your test harness and comment it. In fact, the IronPython console (and all consoles for that matter) provides a number of commands, as shown in Figure 18-1.

As an alternative, if you already have the beginnings of a test-harness check, but want to add to it, you can always paste the text directly into the IronPython console using the Paste command shown in Figure 18-1. The interpreter will automatically execute any statements that you paste into it, so you’ll be ready to start typing new code after you paste it.

Modularity is the name of the game when it comes to a test harness. Try to place the individual tests into separate files so that you can reuse the code later. Simply have a centralized file where you call each of the tests in turn. The tests will output the information you need to screen, so the developer using the test harness need not even know that there are multiple files involved.

Use the text-editing tools to copy and paste text as needed.
Figure 18-1: Use the text-editing tools to copy and paste text as needed.

Testing DLLs

DLLs present one of the easier ways to begin using IronPython to test applications. In fact, you’ve already performed a kind of testing in Chapters 16 and 17 when you created the extensions and then used their content as part of an IronPython application. All that a test harness will do is formalize the testing process so that the output you receive speaks directly about the functionality of the DLL under test, rather than its use in an application. The following sections describe how to perform a test on a DLL using IronPython.

Creating the Test DLL

The DLL used for testing purposes is extremely simple so that the section doesn’t focus more on an interesting DLL than it does on testing techniques. All that this DLL provides is an account where you make an initial deposit to create the account and then make deposits, withdrawals, and transfers. The DLL includes a number of features so that you can try things out, but the code definitely isn’t production quality. For one thing, most of the error-checking code is left out to keep the code clear so you can easily see what will happen next. Listing 18-1 shows the DLL code used for this example.

Listin g 18-1: Defining a DLL to test

[code]
public class Accounts
{
// Contains the current account amount.
private Int32 Total;
public Accounts()
{
// Sets a default acccount amount.
Total = 5000;
}
public Accounts(Int32 Initial)
{
// Set a user supplied initial amount.
Total = Initial;
}
// Provides access to the account total.
public Int32 GetTotal
{
get { return Total; }
}
// Adds a deposit to the account.
public Int32 Deposit
{
set { Total += value; }
}
// Subtracts a withdrawal.
public Int32 Withdrawal
{
set { Total -= value; }
}
public void Transfer(Accounts Account2)
{
// Place the money in the second account in the first account.
this.Total += Account2.Total;
// Withdraw the money from the second account.
Account2.Total = 0;
}
}
[/code]

The example includes two constructors (something you didn’t try in Chapters 16 or 17). The developer can create an account with a default value of 5000 or provide some other initial amount. In either case, you end up with a new Accounts object that has Total defined.

The GetTotal property is read-only and lets the developer obtain the total in the count from Total. Using a property enables you to perform checks before allowing people to have the information. For example, you could place a security code in this property to ensure that only authorized personnel received the information. If a developer were to take this approach, you’d need to write a test to check the GetTotal property using an account other than the developer account.

The Deposit and Withdrawal properties are write-only. The caller doesn’t receive anything back from them. You could use a method to perform the task as well. Using a property makes the test code easier to read, but that’s the only advantage. In both cases, the properties change the value of Total. Of course, you can perform checks in the properties, such as verifying that a withdrawal won’t result in an account with a value less than 0.

The Transfer() method moves all the money from one account to the other. Typically, you’d provide some type of transaction support in a method of this type, but the example doesn’t include it. This is one situation where IronPython can test the method’s inputs and outputs, but can’t test the internal workings of the method. You’d need another tool to test issues such as whether the transaction support actually worked as intended.

Creating the DLL Test Script

It’s time to build an IronPython script to test the DLL shown in Listing 18-1. In this case, the test script is a bit short and doesn’t test every contingency (such as starting with a negative amount in the account), but it demonstrates how you’d create a test script for a DLL. Listing 18-2 contains the code needed for this example.

Listin g 18-2: Developing a DLL test harness

[code]
# Creates a new heading.
def CreateHeading(Test, Title):
print ‘n########################################‘
print ‘Test ID = ‘, Test
print ‘Test Title = ‘, Title
# Displays the values.
def ShowValues(Expected, Received):
print ‘Expected Value = ‘, Expected
print ‘Received Value = ‘, Received
if Expected == Received:
print ‘Test Passed’
else:
print ‘Test Failed’
# Ends the test.
def CreateFooter():
print ‘########################################‘
# Print out statements of everything the test is doing.
print ‘Beginning Test’
print ‘Loading clr’
import clr
print ‘Loading test module’
clr.AddReferenceToFile(‘TestDLL.DLL’)
from TestDLL import *
CreateHeading(‘0001’, ‘Creating Account1’)
Account1 = Accounts()
ShowValues(5000, Account1.GetTotal)
CreateFooter()
CreateHeading(‘0002’, ‘Making a Deposit’)
Account1.Deposit = 1000
ShowValues(6000, Account1.GetTotal)
CreateFooter()
CreateHeading(‘0003’, ‘Making a Withdrawal’)
Account1.Withdrawal = 500
ShowValues(5500, Account1.GetTotal)
CreateFooter()
CreateHeading(‘0004’, ‘Creating Account2’)
Account2 = Accounts(3000)
ShowValues(3000, Account2.GetTotal)
CreateFooter()
CreateHeading(‘0005’, ‘Transferring Money’)
Account1.Transfer(Account2)
print ‘nAccount1 = 8500’
ShowValues(8500, Account1.GetTotal)
print ‘nAccount2 = 0’
ShowValues(0, Account2.GetTotal)
CreateFooter()
# Pause after the debug session.
raw_input(‘nPress any key to continue…’)
[/code]

Let’s begin with the three functions at the beginning of the script: CreateHeading(), ShowValues(), and CreateFooter(). It may seem a bit silly at first to create these functions, but they provide a method for changing the output of the tests quickly, should you need to do so. In addition, you don’t want to write the same print statements hundreds of times as you create your script. It’s far easier to simply call the functions.

The CreateHeading() and CreateFooter() functions don’t have much logic in them — they simply display information onscreen. The ShowValues() function does have a bit of logic. In this case, it simply compares the expected value to the result and displays the appropriate output text. However, you could perform any number of checks required by your application. For example, if you’re working with strings, you might need to check the string length and determine precisely how it differs from another string.

Notice that the __main__() code begins with print ‘Loading clr‘. It’s important to describe every event that occurs in the test script. Otherwise, you won’t know where a script has failed during testing. Make sure you describe the mundane acts of loading and unloading modules, as well as the actual tests.

The first test begins with a call to CreateHeading() with the test number and title. The code then performs a test, Account1 = Accounts() in this case, calls ShowValues() to test the result, and finishes with CreateFooter(). Almost all of the tests follow the same pattern.

The final test is a little different than the rest. To perform the test correctly, you must evaluate the content of both Account1 and Account2. This is a case where you can infer what is happening inside a method with the test code. The method, Transfer(), could perform the task correctly with Account1, but not with Account2, which would tell you something about the content of the method and where to look for the problem.

This final bit of script also shows the flexibility of using the three functions presented earlier. By separating the individual tasks into three parts, you can call the ShowValues() function multiple times as needed. You might also consider creating a second form of ShowValues() to accept a comparison string for output (the print ‘nAccount1 = 8500‘ part of the script).

Performing the DLL Test

It’s time to run the DLL test. If you configured your project using the techniques in Chapters 16 and 17, you should be able to click Start Debugging (or press F5) to start the build process. During the build process, the compiler checks your DLL for major errors.

After the DLL is built, the IronPython script runs. Remember that this script is running outside of the IDE, so nothing it does will actually affect the performance of your code. The diagnostic tests will run and provide the information shown in Figure 18-2.

Notice that the use of formatting, test numbers, titles, comparison values, and so on makes the test results extremely easy to read. Of course, a large DLL could overwhelm the capacity of the console to display information. In this case, you could just as easily send the output to a text file, HTML page, or an XML file. The point is that the script makes it possible to view diagnostics about your application almost immediately after you build it.

Testing Applications

You can use IronPython for more than DLL testing — you can also use it to test your applications. Applications are more of a challenge than DLLs because you have to find a way to emulate user input. Of course, many developers just aren’t as creative as users. A developer would never think about putting text where a number is expected. Many developers discover, to their chagrin, that users will also try implanting scripts and doing other weird things to the application that aren’t easy to test. Some users will even try odd character combinations looking for hidden application features or just to see what will happen. Tests will only work as well as your ability to outguess the user. The following sections show how to test a simple Windows Forms application.

The output shows a list of all of the tests run by the IronPython script on the DLL.
Figure 18-2: The output shows a list of all of the tests run by the IronPython script on the DLL.

Creating the Test Application

The test application is very simple, but it does include some internal code you can use for testing purposes. The following sections describe the test application.

Defining the Form

A Windows Forms application need not be complex to test it using IronPython. All you really need are a few controls and some buttons with code for their event handlers. Figure 18-3 shows the simple form used for this example.

As with Windows Forms you use in a DLL, you must make an important change to test an application using IronPython. All the controls you want to access must have their Modifiers property set to Public. The default setting of Private prevents you from accessing them directly in IronPython.

Building the Code

You can see that the form in Figure 18-3 has three Button controls in it. Each of the controls has a Click() event handler associated with it, as shown in Listing 18-3.

The simple form used for this example provides enough inputs to test.
Fi gure 18-3: The simple form used for this example provides enough inputs to test.

Listin g 18-3: Defining an application to test

[code]
private void btnQuit_Click(object sender, EventArgs e)
{
Close();
}
public void btnAdd_Click(object sender, EventArgs e)
{
txtResult.Text = (Int32.Parse(txtValue1.Text) +
Int32.Parse(txtValue2.Text)).ToString();
}
public void btnSubtract_Click(object sender, EventArgs e)
{
txtResult.Text = (Int32.Parse(txtValue1.Text) –
Int32.Parse(txtValue2.Text)).ToString();
}
[/code]

The btnQuit_Click() event handler is as you might expect. It simply closes the form using the Close() method. You won’t test this functionality using the IronPython script.

The btnAdd_Click() event handler converts the values of txtValue1.Text and txtValue2.Text to Int32 values using Int32.Parse(). It then adds the numbers together, converts the result to a string using ToString(), and places it into txtResult.Text. Because IronPython needs to test this event handler, the visibility is set to public. If you don’t change the visibility of the event handler, IronPython won’t be able to access it. The btnSubtract_Click() event handler works the same as the btnAdd_Click() event handler, except that it subtracts the two numbers.

Creating the Application Test Script

As long as you’re willing to make the required visibility changes to your application, you can use IronPython to test it. Creating a test project for an application works precisely the same as creating a test project for a DLL. Here’s the short list of changes you must perform:

  1. Change the build output location for both the Debug and Release builds to the solution folder.
  2. Add IPY.EXE as an existing project to your solution.
  3. Set the ipy project as the startup project so that the IDE executes it instead of the Windows Forms application.
  4. Configure the ipy project to start your script and to use the appropriate working directory.
  5. Add a new IronPython script to the solution folder.

This test script uses the three functions described in Listing 18-2 to provide output. It also adds the following two output functions:

[code]
# Verify the type.
def CheckType(Object, Type):
if Object.GetType().__str__() == Type:
print ‘Test Passed’
else:
print ‘Test Failed’
# Show initial values.
def ShowInit(Value1, Value2):
print ‘Value1: ‘, Value1
print ‘Value2: ‘, Value2
[/code]

The CheckType() function compares the type of an object you create against an expected type. If the type is incorrect, then it displays a failed message. You can use this function when creating a form or other object that could fail for any number of reasons.

The ShowInit() function displays the initial values for a binary operation or perhaps just two values used for some other task. You could probably create a version of the function that accepts any number of arguments in the form of an array. The point is that you can create some specialized functions to display data for a particular test and then find that you can use it for other purposes later.

As previously mentioned, this test script also uses the three functions found in Listing 18-2. Listing 18-4 shows the actual test script for this application. It doesn’t provide a complete test but does provide enough information that you could easily complete it if you wanted.

Listin g 18-4: Developing an application test harness

[code]
# Print out statements of everything the test is doing.
print ‘Beginning Test’
print ‘Loading clr’
import clr
print ‘Loading System assembly support’
import System
print ‘Creating a blank event argument.’
EventArg = System.EventArgs()
print ‘Loading test module’
clr.AddReferenceToFile(‘TestApplication.EXE’)
from TestApplication import *
CreateHeading(‘0001’, ‘Creating a test form’)
MyForm = Form1()
CheckType(MyForm, ‘TestApplication.Form1’)
CreateFooter()
CreateHeading(‘0002’, ‘Testing a default add’)
MyForm.btnAdd_Click(object, EventArg)
ShowInit(MyForm.txtValue1.Text, MyForm.txtValue2.Text)
ShowValues(‘2’, MyForm.txtResult.Text)
CreateFooter()
CreateHeading(‘0003’, ‘Testing a default subtract’)
MyForm.btnSubtract_Click(object, EventArg)
ShowInit(MyForm.txtValue1.Text, MyForm.txtValue2.Text)
ShowValues(‘0’, MyForm.txtResult.Text)
CreateFooter()
CreateHeading(‘0004’, ‘Testing add with one change’)
MyForm.txtValue1.Text = ‘5’
MyForm.btnAdd_Click(object, EventArg)
ShowInit(MyForm.txtValue1.Text, MyForm.txtValue2.Text)
ShowValues(‘6’, MyForm.txtResult.Text)
CreateFooter()
# Pause after the debug session.
raw_input(‘nPress any key to continue…’)
[/code]

The test script begins by loading the required code for the test, beginning with clr. Because this test has to work with event handlers, it needs to load the System assembly and create a System.EventArgs object, EventArg. Because the event handlers in this application don’t actually use the event arguments, EventArg is actually a default object with no content. The call simply won’t succeed without it, however, so you must create it.

After the script finishes the prerequisites, it performs the first test, which is to create the Windows Forms object, Form1, as MyForm. The creation process could fail; you want to verify that MyForm isn’t null, so that’s the first test that relies on the CheckType() function. You don’t have to show the form to test it, so the code doesn’t call ShowDialog(). If you do decide to show the form, you’ll actually need someone to work with it. The script is suspended during the time the form appears onscreen.

The next step is to perform some tasks with the form. The code performs a default add and subtract. The two value fields, MyForm.txtValue1.Text and MyForm.txtValue2.Text, contain default values that you can use for testing. Actually, it’s good application design to always include default values for the user so that the user has some idea of what kind of content to provide.

The MyForm.btnAdd_Click() and MyForm.btnSubtract_Click() event handlers perform the actual addition and subtraction. In order to call these two methods, you must supply both a sender object and event arguments. The sender object can simply be an object because the code doesn’t use it.

The final test in the example is to change one of the values and perform another addition. To perform this task, the script changes the value of MyForm.txtValue1.Text and calls MyForm.btnAdd_Click(). Normally, you’d provide a wealth of additional tests to check various values and see how they react with the code. For example, you might provide some negative values to ensure that the event handlers work properly with them. You might also test incorrect input, such as providing a string. The point is that you can completely automate any level of testing using this IronPython script technique.

Performing the Application Test

At this point, you have an application to test and the script to test it. It’s time to run the application. One of the problems you could encounter is not making something public (such as an object, control, or property) that you need to test (the default is to create private objects, controls, and properties). Unfortunately, the need to make class members public is one of the problems of using IronPython for desktop application testing. It’s not a big problem, but you need to consider it. When working with an extremely large application, changing the required member visibility could prove problematic. In addition, making some members public could pose security risks.

Let’s hope everything works as anticipated when you run the test. Figure 18-4 shows typical output from this application.

As with the DLL testing script, this script outputs text that’s easy to read and results that are easy to decipher. You know immediately whether certain tests have failed and precisely what inputs were used to conduct the test. As with DLL testing, you may need to use some other form of output, such as an XML file, when performing testing on complex applications because the content won’t fit entirely onscreen.

Desktop applications can prove difficult to test, but the results are worth it.
Fi gure 18-4: Desktop applications can prove difficult to test, but the results are worth it.

Performing Command Line Tests

For many developers, testing must be formal or it really isn’t testing. Actually, ad hoc testing is sometimes better because you get to play with the application while you test it. Testing in an ad hoc manner at the command line is possible in IronPython because it’s an interpreted environment. In fact, we’ve been performing ad hoc testing throughout the book. Every time you reviewed the content of an application, no matter what type it was, using the dir() function, you were performing a kind of ad hoc testing because you were reviewing the content of the application.

The test demonstrated that the DLL had added the overrides correctly and that you should be able to access them from an application. In addition, you discovered that IronPython views the content of the DLL in a slightly different manner than another environment might view them.

Let’s look at a specific test example, the TestDLL.DLL file. For the purposes of this example, you want to use the dir() function to determine whether the Accounts class contains everything it should (and nothing it shouldn’t), as shown in Figure 18-5. Notice that there’s no mention of Total in this list, but you can see all of the properties and methods described in Listing 18-1.

Make sure you check the actual content of the DLL against the expectations you have for it.
Fi gure 18-5: Make sure you check the actual content of the DLL against the expectations you have for it.

If you remember from Chapters 16 and 17, the __doc__() function is undefined for an assembly that you import into IronPython, but the help() function does produce a result. One of the next checks you should perform manually is to verify that the assembly provides the kind of information you expect from help. Figure 18-6 shows the output of the help() function for the Accounts class. Notice that it contains all of the information you expect, including the fact that there are two forms of __new__(), the constructor, and the read/write state of the various properties.

Of course, you’ll want to perform other sorts of manual testing that could eventually appear in your test script. For example, you might decide to check whether the Accounts class will let you create an account with a negative starting amount (it will).

It would be also helpful to know whether someone could circumvent some of the properties in the Accounts class. You wouldn’t want someone to use code such as Account2 = Account2 + 20 to overcome the protections in the Deposit property. In this case, the test displays an error. Another check might include adding two accounts together, such as Acccount3 = Account1 + Account2.

By now, you should have the point of using manual testing. You can creatively think of ways that someone might try to overcome protections in your code. It probably isn’t possible to find every avenue of entry into a DLL, but testing in this way helps you think through more potential problems that other forms of testing allow. Interactively probing your code is a unique method of testing the impossible.

Verify that the help() function doesn’t show any surprises about your assembly.
Fi gure 18-6: Verify that the help() function doesn’t show any surprises about your assembly.

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]

Extending IronPython Using Visual Basic.NET

Considering C# and VISUAL BASIC.NET Extension Similarities

Visual Basic.NET does have some distinct advantages over C# when building an extension. The most important of these distinctions is that Visual Basic.NET does more for you in the background. For example, Visual Basic.NET automatically creates a namespace for you — it isn’t something you have to think about. Visual Basic.NET also performs some type conversions automatically, so you don’t have to think about type conversions as much either. When you do need to perform a type conversion, you use the CType() function, which makes the kind of conversion a little more apparent.

You can easily use either C# or Visual Basic.NET to perform simple tasks. For example, either language works fine for creating a math library or for working with files. C# probably has an advantage in working with low-level extensions, especially those that interact with the Win32 API. On the other hand, the tendency of Visual Basic.NET to hide some of the gory details of programming works to your advantage when working with higher-level programming requirements, such as database access. Consequently, this chapter describes the requirements for creating a database extension.

Creating the Simple Visual Basic.NET Extension

The best place to begin learning how to create extensions is to create a very simple one. The sections that follow explore a simple Visual Basic.NET extension. This project creates a simple math library. In the process, it demonstrates some unique principles of creating extensions using Visual Basic.NET.

Creating the Project

A Visual Basic.NET extension project is nothing more than the typical class library. The following steps help you create the project for this example. You can use the same steps when working with the other examples — simply change the project name.

  1. Choose File ➪ New ➪ Project. You’ll see the New Project dialog box shown in Figure 17-1.

    Create a new project to hold your Visual Basic.NET extension.
    Figure 17-1: Create a new project to hold your Visual Basic.NET extension.
  2. Choose the Visual Basic folder in the Installed Templates list.
  3. Select .NET Framework 3.5 or an earlier version of the .NET Framework if you’re using Visual Studio 2010. Don’t select the .NET Framework 4.0 entry because IronPython won’t load extensions based on the .NET Framework 4.0. The list of templates changes when you change the .NET Framework version.
  4. Select the Class Library template.
  5. Check Create Directory for Solution if it isn’t already checked. When working with extensions, creating a solution directory provides a place for putting solution-level objects.
  6. Type Calcs in the Name field and click OK. Visual Studio creates a class library project for you.
  7. Right-click Class1.vb in Solution Explorer and choose Rename from the context menu. Visual Studio makes the filename editable.
  8. Type Calcs.VB for the new filename and press Enter. Visual Studio displays a dialog box that asks whether you’d like to rename all of the Class1.vb references to match the new filename.
  9. Click Yes. The project is ready for use.

Developing the Visual Basic.NET Extension

The Visual Basic.NET extension code for this example is relatively simple. Listing 17-1 shows the constructor, operator overrides, and methods used for this example.

Listin g 17-1: A simple calculations extension

[code]
Public Class Calcs
Private Data As Int32
Public Sub New(ByVal Value As Int32)
Me.Data = Value
End Sub
Public Overrides Function ToString() As String
Return Data.ToString()
End Function
Public Shared Operator +(ByVal Value1 As Calcs, _
ByVal Value2 As Calcs) As Calcs
Return New Calcs(Value1.Data + Value2.Data)
End Operator
Public Shared Operator -(ByVal Value1 As Calcs, _
ByVal Value2 As Calcs) As Calcs
Return New Calcs(Value1.Data – Value2.Data)
End Operator
Public Shared Operator *(ByVal Value1 As Calcs, _
ByVal Value2 As Calcs) As Calcs
Return New Calcs(Value1.Data * Value2.Data)
End Operator
Public Shared Operator /(ByVal Value1 As Calcs, _
ByVal Value2 As Calcs) As Calcs
Return New Calcs(Value1.Data / Value2.Data)
End Operator
Public Function Inc() As Calcs
Return New Calcs(Me.Data + 1)
End Function
Public Function Dec() As Calcs
Return New Calcs(Me.Data – 1)
End Function
End Class
[/code]

The code begins with a constructor that accepts an Int32 value as input. The example doesn’t include a default constructor because IronPython needs to assign a value to the object during the instantiation process. A default constructor would still need to assign a value to the private Data member, so it’s just better to assign a valid value to Data at the outset.

The ToString() override comes next. The default behavior for ToString() is to display the name of the class. You must override this behavior to display the value of Data. Notice that you must access Data as Me.Data — the copy of Data associated with this particular instance of the Calcs class.

The four Operator methods are defined as Shared, rather than Overrides. The Operator methods act as static class members so that you can use them naturally in IronPython. The input arguments for each method are the objects you create within IronPython. Consequently, there isn’t any concept of numeric type for Value1 or Value2 (you could theoretically use the same methods for any numeric value). The actual math operation occurs on the Data member of each object.

IronPython doesn’t support the ++ or — operators that are supported by Visual Basic for increment and decrement. Consequently, the class provides an Inc() and Dec() method. Notice that these methods aren’t defined as Shared because they work with a single object. You need to consider the differences between binary (those that work with two objects) and unary (those that work with a single object) operators when creating your extension. Binary operators are always declared as Shared, while unary operators appear as a standard method.

At this point, you can compile the class if desired. Start a copy of the IronPython console and type the following commands to load the extension.

[code]
import clr
clr.AddReferenceToFile(‘Calcs.DLL’)
import Calcs
dir(Calcs.Calcs)
[/code]

The dir() function shows the content of the Calcs extension as shown in Figure 17-2. Notice that Inc() and Dec() appear as you expect. However, there aren’t any entries for +, -, *, and / methods. These operators still work as you expect, but IronPython shows a Python equivalent for the operators in the form of __add__(), __radd__(), __sub__(), __rsub__(), __mul__(), __rmul__(), __div__ (), and __rdiv__(). These methods don’t appear unless you define the operators in your class.

If you’re looking at the class in the IronPython console, you might want to give it a quick try before you close up the console and move on to the next part of the example. Try this code and you’ll see an output of 15 from the __add__() method. Figure 17-2 shows the results of the calculation.

[code]
Value1 = Calcs.Calcs(10)
Value2 = Calcs.Calcs(5)
print Value1.__add__(Value2)
[/code]

The dir() function shows the content of the Calcs class.
Figure 17-2: The dir() function shows the content of the Calcs class.

Adding the IronPython Project

At this point, you have a Visual Basic.NET extension (or module) to use with IronPython. Of course, you’ll want to test it. The easiest way to do this is to add the IronPython project directly to the current solution. The following steps describe how to perform this task.

  1. Right-click the solution entry in Solution Explorer and choose Add ➪ Existing Project from the context menu. You’ll see the Add Existing Project dialog box shown in Figure 17-3.

    Locate IPY.EXE and add it to your solution.
    Figure 17-3: Locate IPY.EXE and add it to your solution.
  2. Locate IPY.EXE on your hard drive and highlight it. Click Open. You’ll see a new project entry added to the solution.
  3. Right-click the ipy entry in Solution Explorer and choose Set as Startup Project from the context menu. This step ensures that choosing one of the startup options from the Debug menu starts the IronPython application.
  4. Right-click the ipy entry in Solution Explorer and choose Properties from the context menu. You’ll see the General tab of the ipy Properties window shown in Figure 17-4.

    Configure the IronPython application to work with Calcs.DLL.
    Figure 17-4: Configure the IronPython application to work with Calcs.DLL.
  5. Type -D TestCalcs.py in the Arguments field.
  6. Click the ellipses in the Working Directory field to display the Browse for Folder dialog box. Locate the output folder of the Calcs.DLL (or other extension) file. Click OK. The IDE adds the correct directory information to the Working Directory field.
  7. Open Windows Explorer. Locate the CalcsCalcsbinDebug folder. Right-click in the right pane and choose New ➪ Text Document from the context menu. Name the file TestCalcs.py and press Enter. Click Yes if asked if you want to rename the file extension.
  8. Right-click the solution item in Solution Explorer and choose Add ➪ Existing Item from the context menu to display the Add Existing Item dialog box shown in Figure 17-5.
  9. Locate the TestCalcs.py file in the solution and click Add. Visual Studio adds TestCalcs.py to the Solution Items folder in Solution Explorer and automatically opens the file for you. You’re ready to add test code for the application.
Add the TestCalcs.py file to the solution.
Figure 17-5: Add the TestCalcs.py file to the solution.

Creating the IronPython Application

It’s time to write code to test Calcs.DLL. Listing 17-2 shows the code you’ll use for testing purposes.

Listin g 17-2: Testing the extension using IronPython

[code]
# Add a reference to the CLR
import clr
# Obtain access to the extension.
clr.AddReferenceToFile(‘Calcs.DLL’)
import Calcs
# Create an instance of the class and fill it with data.
Value1 = Calcs.Calcs(10)
# Print the original value, then decrement and increment it.
print ‘Original Value1 Content: ‘, Value1
print ‘Value1 + 1: ‘, Value1.Inc()
print ‘Value1 – 1: ‘, Value1.Dec()
# Create a second value and display it.
Value2 = Calcs.Calcs(5)
print ‘nOriginal Value2 Content: ‘, Value2
# Use the two values together in different ways.
print ‘nValue1 + Value2 = ‘, Value1 + Value2
print ‘Value1 – Value2 = ‘, Value1 – Value2
print ‘Value1 * Value2 = ‘, Value1 * Value2
print ‘Value1 / Value2 = ‘, Value1 / Value2
# Pause after the debug session.
raw_input(‘nPress any key to continue…’)
[/code]

The code begins by importing the Common Language Runtime (CLR). It then uses the AddReferenceToFile() method to create a reference to Calcs.DLL. The final step is to import the Calcs code.

Before the code can use the Calcs code, it must create an instance of it, Value1. Notice that the code calls the Calcs.Calcs() constructor with an initial value. Any time you want to assign a value to Value1, you must use the Calcs.Calcs() constructor. If you were to assign a value using Value1 = 15, it would change the type of Value1. A consequent addition, such as Value1 + Value2, would produce the following error:

[code]
Traceback (most recent call last):
File “<stdin>”, line 1, in <module>
TypeError: unsupported operand type(s) for +: ‘int’ and ‘Calcs’
[/code]

One way to overcome this problem would be to override the = operator.

After the code creates Value1, it demonstrates the use of the Inc() and Dec() methods. These two methods simply add or remove 1 from the value of Value1. If you want to change the actual value of Value1, you need to make Value1 equal to the output of the method like this:

[code]
Value1 = Value1.Inc()
[/code]

The next step is to create Value2, a second Calcs object you can use for binary operations. The code outputs the initial value of Value2. The remainder of the example demonstrates the use of the various operators. As you can see, they work precisely as you would expect. You could even use them to create a third value like this:

[code]
Value3 = Value1 + Value2
[/code]

Figure 17-6 shows the output from this example. Except for the absence of the ++ and — operators, everything works much as you would expect.

Using Visual Basic.NET for User Interface Support

It’s certainly possible to create message boxes and even Windows Forms applications using IronPython. The biggest issue is that IronPython lacks support for the designers that make the task of writing Windows Forms code so easy. You have to be able to picture the form you want in your mind and then use trial and error to get it to appear in the application. Consequently, most developers will probably want to use a language such as Visual Basic.NET to create their Windows Forms applications and then make those forms accessible from IronPython as part of an extension.

Here are the results of using the Visual Basic.NET extension within IronPython.
Figure 17-6: Here are the results of using the Visual Basic.NET extension within IronPython.

The examples in the sections that follow aren’t all that complicated, but they do demonstrate the principles required to build your own library of message boxes and Windows Forms classes. By the time you finish these examples, you’ll have everything needed to create your own user interface library for use in IronPython.

Creating the User Interface Library Module

From an IronPython perspective, user interface elements come in two forms: messages boxes and Windows Forms. Obviously, Visual Basic.NET can create a host of user interface presentations, but if you start at this basic level, you’ll find the task of creating a user interface library module easier. The following sections describe how to create both a message box class and a Windows Forms class that you place in a single DLL for use with your IronPython application. Of course, a production DLL could have hundreds of different forms, depending on the user interface requirements for the application.

Defining Simple Message Boxes

Message boxes (created using the MessageBox class) are extremely useful for displaying short messages and getting canned responses. Depending on the buttons you provide, a user could tell you that the application should retry an operation or answer yes to simple questions. If you need a little more input, you can always rely on an input box (created with the InputBox() method of the Interaction class). Of course, an input box is still limited to a single field, but even so, it does extend the kinds of input you can receive from the user.

Listing 17-3 demonstrates both the MessageBox.Show() and InputBox() methods. In addition, you’ll see how to implement the __doc__() method that most IronPython developers rely upon to obtain information about your extension.

Listin g 17-3: Working with simple message boxes

[code]
Imports System.Windows.Forms
Public Class Dialogs
Public Function ShowMessage(ByVal Msg As String) As String
Return MessageBox.Show(Msg).ToString()
End Function
Public Function ShowMessage(ByVal Msg As String, _
ByVal Title As String) As String
Return MessageBox.Show(Msg, Title).ToString()
End Function
Public Function ShowMessage(ByVal Msg As String, ByVal Title As String, _
ByVal Buttons As Int16) As String
Return MessageBox.Show(Msg, Title, CType(Buttons, MessageBoxButtons) _
).ToString()
End Function
Public Function ShowMessage(ByVal Msg As String, ByVal Title As String, _
ByVal Buttons As Int16, ByVal Icon As Int16 _
) As String
Return MessageBox.Show(Msg, Title, CType(Buttons, MessageBoxButtons), _
CType(Icon, MessageBoxIcon)).ToString()
End Function
Public Function ShowMessage(ByVal Msg As String, ByVal Title As String, _
ByVal Buttons As Int16, ByVal Icon As Int16, _
ByVal DefaultButton As Int16) As String
Return MessageBox.Show(Msg, Title, CType(Buttons, MessageBoxButtons), _
CType(Icon, MessageBoxIcon), _
CType(DefaultButton, MessageBoxDefaultButton) _
).ToString()
End Function
Public Function GetInput(ByVal Msg As String, ByVal Title As String)
Return InputBox(Msg, Title, “Type a value”)
End Function
Public Function __doc__() As String
Return “This is a help string”
End Function
End Class
[/code]

Before you can compile this code, you need to add a reference to System.Windows .Forms.DLL. Right-click Dialogs in Solution Explorer and choose Add Reference from the context menu. You’ll see the Add Reference dialog box shown in Figure 17-7. Highlight the System.Windows.Forms entry and click OK. At this point, you also need to add an Imports System.Windows.Forms entry to your project and you’re ready to work with message boxes.

Add the System.Windows.Forms.DLL entry to your project.
Figure 17-7: Add the System.Windows.Forms.DLL entry to your project.

The code begins by creating a series of ShowMessage() methods. The first is relatively simple and the complexity increases with each ShowMessage() method entry. Notice that the ShowMessage() method uses Int16 input values to select the buttons, icon, and default button. You could also use enumerations to provide input values. The one thing you don’t want to do is ask the IronPython developer to provide a MessageBoxButtons, MessageBoxIcon, or MessageBoxDefaultButton value, because then the IronPython developer would need to import all the required .NET Framework functionality, reducing the usefulness of your extension. The CType() function helps you convert the Int16 values into the appropriate enumeration value. Interestingly enough, there are 21 forms of the MessageBox .Show() method, even though the example shows only five of them.

The GetInput() method shows just one of several InputBox() method variations you can use. In this case, the IronPython developer supplies the prompt (or message) and title to display onscreen. The GetInput() method supplies a default InputBox() value. Normally, you want to supply a value so that the user knows to type something and what you want the user to type. Even if the required input seems obvious to you, many users won’t know what to provide.

The __doc__() provides a help string for the IronPython developer. The example shows something quick, but in reality, you’d provide complete documentation for your class. The output string can use all the standard formatting characters. You could even read the content in from an external source, such as a file, to make it easy to provide updates without having to recompile the extension. Using an external file would also allow the IronPython developer to personalize the content.

Defining Complex Forms

A Windows Forms class can contain anything you want. It can even call other forms as needed. In fact, anything you can do with a Visual Basic.NET Windows Forms application is doable with IronPython. Of course, you do need to maintain interaction with the IronPython application. The following steps describe how to create a simple Windows Forms class for your extension.

  1. Right-click Dialogs in Solution Explorer and choose Add ➪ New Item. Select the Windows Forms entry in the Installed Templates list. You see the Add New Item dialog box shown in Figure 17-8.

    Add a Windows Form to your project.
    Figure 17-8: Add a Windows Form to your project.
  2. Highlight the Windows Form entry. Type TestForm.VB in the Name field and click Add. Visual Studio adds the new form to your project and automatically opens it for editing.
  3. Create the form just as you normally would for any static application. Figure 17-9 shows the form used for this example. It’s simple, but it contains multiple data entry fields and multiple exit options.

The form shown in Figure 17-9 is a little deceptive. Before you assume anything about this form, it does have a few differences from the forms you’ve created for your static applications.

The Windows Form can contain any level of complexity you desire.
Figure 17-9: The Windows Form can contain any level of complexity you desire.
  • Buttons that close the form, rather than do something within the form, must have the DialogResult property set to a unique value or you won’t be able to tell which button the user clicked. For this example, the DialogResult for btnOK is OK, while the DialogResult for btnCancel is Cancel.
  • Getting information from the form you create to the IronPython application can prove problematic. You could contrive all sorts of odd methods for accomplishing the task, but the simplest method is to set the Modifiers property for the individual controls (txtName and txtColor) to Public. In this case, using Public doesn’t create a problem because IronPython sets everything to public. In all other respects, there’s no difference between this form and any other form you’ve created in the past.

To make things simple, this example doesn’t use any code-behind for the form itself. Any codebehind works as you’d expect. There isn’t any difference between calling the form from IronPython than calling it from within your Visual Basic.NET application.

Accessing the User Interface Library Module from IronPython

It’s time to use the extension you’ve created with an IronPython application. The following sections describe an alternative way to set up your project so that you don’t have to create the IronPython file using Windows Explorer and show how to use the extension.

An Alternative Method for Adding the IronPython Project

There are a number of ways to configure a test setup for your extensions. The “Adding the IronPython Project” section shows one technique. The technique shown in that section works well when you want to maintain separate builds of your extension. For example, you might want to maintain separate debug and release builds.

Unfortunately, that earlier method is a bit clumsy — you have to create the IronPython file using Windows Explorer. The technique in this section avoids that problem. In addition, this technique shows how to maintain just one build — the build you’re currently using for debugging, testing, or experimentation. Use the following steps to create a centralized test configuration:

  1. Right-click Dialogs in Solution Explorer and choose Properties from the context menu. Select the Compile tab. You’ll see the Properties window shown in Figure 17-10.

    Configure the build to use a central output location.
    Figure 17-10: Configure the build to use a central output location.
  2. Click Browse next to the Build Output Path field to display the Select Output Path dialog box shown in Figure 17-11. Because you’ll add the IronPython test file at the solution level, you need to send the output to the solution level as well.
  3. Select the first Dialogs entry in the list and click OK. Visual Studio adds an absolute path to the Output Path field that you must change for every machine that uses the application. As an alternative, you could type .. (two periods and a backslash) in the field to place the output in the solution folder.
  4. Select the next configuration in the Configuration field.
  5. Perform Steps 2 through 4 for each configuration. Make sure each configuration uses the same output directory. Normally, your project will contain only Debug and Release configurations.
  6. Right-click the solution entry in Solution Explorer and choose Add ➪ Existing Project from the context menu. You’ll see the Add Existing Project dialog box shown in Figure 17-3.
  7. Locate IPY.EXE on your hard drive and highlight it. Click Open. You’ll see a new project entry added to the solution.

     Modify the output path as required for your application.
    Figure 17-11: Modify the output path as required for your application.
  8. Right-click the ipy entry in Solution Explorer and choose Set as Startup Project from the context menu.
  9. Right-click the ipy entry in Solution Explorer and choose Properties from the context menu. You’ll see the General tab of the ipy Properties window shown in Figure 17-4.
  10. Type -D DialogTest.py in the Arguments field.
  11. Click the ellipses in the Working Directory field to display the Browse for Folder dialog box. Locate the solution folder for the project (the first Dialogs folder). Click OK. The IDE adds the correct directory information to the Working Directory field.
  12. Right-click the solution entry in Solution Explorer and choose Add ➪ New Item from the context menu. You see the Add New Item dialog box shown in Figure 17-12.

    Add the IronPython test file to your project.
    Figure 17-12: Add the IronPython test file to your project.
  13. Type DialogTest.py in the Name field and click Add. Visual Studio adds the new file to the Solution Items folder in Solution Explorer and opens the file automatically for editing.

Performing the Message Box and Form Tests

The example is ready except for the test code. Listing 17-4 shows the IronPython code you need for this example.

Listin g 17-4: Testing the message boxes and forms

[code]
# Define the message box tests.
def TestMessages():
# Create a message box object.
MyDialog = Dialogs.Dialogs()
# Show the help information.
print ‘Dialogs Class Help Information.’
print MyDialog.__doc__()
# Test a simple message box.
print ‘nTesting a simple message box.’
print ‘Simple message box output: ‘,
print MyDialog.ShowMessage(‘Hello’)
# Perform a more complex test.
print ‘nA more complex message box.’
print ‘Complex message box output: ‘,
print MyDialog.ShowMessage(‘Hello Again’, ‘Title 2’, 3, 64, 256)
# Get some user input.
print ‘nUsing an InputBox.’
print ‘InputBox Output: ‘,
print MyDialog.GetInput(‘Type Your Name:’, ‘User Name Entry’)
# Define the form test.
def TestForm():
# Create the form instance.
MyForm = Dialogs.TestForm()
# Display the form and test the dialog result.
print ‘nThe form example.’
if MyForm.ShowDialog().ToString() == ‘OK’:
# Display the results.
print ‘The user clicked OK.’
print ‘User Name: ‘, MyForm.txtName.Text
print ‘Favorite Color: ‘, MyForm.txtColor.Text
# Display an alternate result.
else:
print ‘The user clicked cancel.’
# Import the Common Language Runtime.
import clr
# Access the extension.
clr.AddReferenceToFile(‘Dialogs.DLL’)
import Dialogs
# Test the message box code.
TestMessages()
# Test the form code.
TestForm()
# Pause after the debug session.
raw_input(‘nPress any key to continue…’)
[/code]

The code begins by importing CLR support and then uses the AddReferenceToFile() to add a reference to the Dialogs.DLL. The next step is to import the Dialogs namespace for use. The __main__() function calls two functions, TestMessages() and TestForm(), to test the content of the Dialogs namespace. It then pauses so you can see the results.

The TestMessages() function begins by creating an instance of Dialogs.Dialogs, MyDialog. It then calls the MyDialog.__doc__() method to output the help information provided by the Dialogs class. Normally you’d use this method at the interactive console, but it’s good to see how the method works.

The next step is to test the MyDialog.ShowMessage() method. To keep you from clicking all afternoon, the test code uses just two forms of the method. The first form shows the simplest dialog box, while the second shows the most complex. The most complex dialog box (shown in Figure 17-13) contains a message, title, icon, and three buttons. Notice that the second button, rather than the first button, is selected by default. Normally, a message box selects the first button by default.

The complex message box can convey quite a bit of information for such a simple call.
Figure 17-13: The complex message box can convey quite a bit of information for such a simple call.

The next step is to display an input box. In this case, the MyDialog.GetInput() method displays an input box that contains a simple prompt and a title, as shown in Figure 17-14. Notice the default message in the input box. The input box automatically highlights this default entry so that the first thing the user types will erase the default content. The output from the MyDialog .GetInput() method is the text that the user types in the input box.

 Input boxes are good for small amounts of custom user input.
Figure 17-14: Input boxes are good for small amounts of custom user input.

The TestForm() function begins by creating an instance of the Dialogs.TestForm class, MyForm. The code then displays the dialog box shown in Figure 17-9 using the MyForm.ShowDialog() method. Notice that the example code adds a call to ToString(), so that the entire method call is MyForm.ShowDialog().ToString(). This is a technique for converting the System.Windows .Forms.DialogResult to a simple string that you can compare with the desired output, which is ‘OK‘ in this case.

When the call succeeds (the user clicks OK), the code prints the user’s name and favorite color. Notice that the code directly accesses both txtName.Text and txtColor.Text to obtain the required information. When the call fails (the user clicks Cancel), the code outputs a simple failure message. Figure 17-15 shows typical output from this example.

The IronPython output shows the results of the various dialog and form selections.
Figure 17-15: The IronPython output shows the results of the various dialog and form selections.

Using Visual Basic.NET for Database Support

Visual Basic.NET makes database management easy. Of course, there are all the handy designers that Visual Basic.NET makes available. The features of Server Explorer help as well. However, the fact that Visual Basic.NET tends to hide some of the details is what helps the most. The following sections provide a simple database management example that you could easily expand to help IronPython work with all sorts of data.

Obtaining and Configuring the Database

This example relies on an old standby, the Northwind database. Microsoft has passed this database by for significantly more complex examples, but Northwind remains unsurpassed in its ability to create useful examples with very little code, so it’s the database of choice for this chapter. You can download the Northwind database from http://www.microsoft.com/downloads/details .aspx?FamilyID=06616212-0356-46A0-8DA2-EEBC53A68034.

Make sure you have a database manager installed on your system. The Northwind database works just fine with versions of SQL Server as old as SQL Server 2000, but you should at least try a newer version, even if it’s SQL Server 2008 Express. The following steps tell you how to install the Northwind database.

  1. Double-click the SQL2000SampleDb.msi. You’ll see the normal Welcome dialog box for installing Microsoft products. Click Next. You’ll see the licensing agreement.
  2. Click I Agree after reading the license agreement, and then click Next. You’ll see an Installation Options dialog box. There aren’t any actual installation options.
  3. Click Next. You’ll see a Confirm Installation dialog box.
  4. Click Next. The installer installs the files into the C:SQL Server 2000 Sample Databases folder on your machine (you aren’t given a choice about the installation folder). After the installation is complete, you’ll see an Installation Complete dialog box.
  5. Click Close. The Northwind database and its associated script are now loaded on your machine.
  6. Open a command prompt in the C:SQL Server 2000 Sample Databases folder.
  7. Type OSQL -E -i InstNwnd.SQL and press Enter (the command line switches are case sensitive — make sure you type the command correctly). The OSQL utility will start building and installing the Northwind database. This process can take a while to complete — get a cup of coffee and enjoy. When the process is complete, you see a command prompt with a bunch of numbers on it and no error message, as shown in Figure 17-16.
The output from the OSQL utility doesn’t tell you much except if it encountered errors.
Figure 17-16: The output from the OSQL utility doesn’t tell you much except if it encountered errors.

Creating the Database Support Module

Creating a database support module is a multi-step process. At a minimum, you must first create a connection to the database and then work with that connection using code. The example that follows isn’t very complex. All that this example will do is retrieve some information from the database in the interest of keeping things simple. Even so, the basics shown in the example provide enough information for you to start creating database extensions of your own.

Creating a Connection to the Database

The first step in working with the Northwind database is to create a connection to it. The following steps describe how to perform this task.

  1. Right-click on the Data Connections entry in Server Explorer and choose Add Connection from the context menu. You may see the Choose Data Source dialog box shown in Figure 17-17. If not, you’ll see the Add Connection dialog box shown in Figure 17-18 and will need to proceed to Step 3.
  2. Highlight the Microsoft SQL Server entry. Select the .NET Framework Data Provider for SQL Server entry in the Data Provider field. Click Continue. You’ll see the Add Connection dialog box shown in Figure 17-18.

    Select the SQL Server data source to make the Northwind connection.
    Figure 17-17: Select the SQL Server data source to make the Northwind connection.
  3. Select or type the server name in the Server Name field. You can type a period (.) for the default server. The Add Connection dialog box automatically enables the Select or Enter a Database Name field.
  4. Select the Northwind database in the Select or Enter a Database Name field.
  5. Click Test Connection. You see a success message box (click OK to dismiss it).
  6. Click OK. Visual Studio displays the new connection in Server Explorer, as shown in Figure 17-19.
    The Add Connection dialog box lets you create and test a connection to the Northwind database.
    Figure 17-18: The Add Connection dialog box lets you create and test a connection to the Northwind database.

    The new connection appears in Server Explorer where you can work with it directly.
    Figure 17-19: The new connection appears in Server Explorer where you can work with it directly.
  7. Choose Data ➪ Add New Data Source. You’ll see the Data Source Configuration Wizard dialog box shown in Figure 17-20.

    Use the Data Source Configuration Wizard to create a coded connection.
    Figure 17-20: Use the Data Source Configuration Wizard to create a coded connection.
  8. Highlight Database and click Next. You’ll see the Choose Database Model page.
  9. Highlight the Dataset option and click Next. You’ll see the Choose Your Data Connection page. Notice that the Northwind database connection already appears in the connection field. The connection name will have your machine name, followed by the database name, followed by .dbo, such as main.Northwind.dbo. If it doesn’t, make sure you select it from the list. If the connection doesn’t appear in the list, click Cancel and start over with Step 1 because your connection wasn’t successful.
  10. Select the Northwind connection and click Next. The wizard will ask how you want to save the connection. There isn’t a good reason to change the default name provided.
  11. Click Next. You see the Choose Your Database Objects page shown in Figure 17-21.
  12. Check the Customers table entry, as shown in Figure 17-21. The example relies on the Customers table and none of the other database content. Click Finish. The new data source appears in the Data Sources window, as shown in Figure 17-22. If you can’t see this window, choose Data ➪ Show Data Sources.

Adding Database Manipulation Code

After all the work you performed to obtain access to the data, the actual database manipulation code is relatively easy. Listing 17-5 shows the small amount of code used to actually retrieve a particular record from the database based on the CustomerID field. Of course, you can add any level of complexity required.

Select the Customers table for this example.
Figure 17-21: Select the Customers table for this example.
The data source is ready to use in the example extension.
Figure 17-22: The data source is ready to use in the example extension.

Listin g 17-5: Retrieving data from the database

[code]
Public Function GetData(ByVal Customer As String) As _
NorthwindDataSet.CustomersRow
‘ Obtain access to the table.
Dim MyData As NorthwindDataSetTableAdapters.CustomersTableAdapter = _
New NorthwindDataSetTableAdapters.CustomersTableAdapter()
‘ Create a DataSet.
Dim DS As NorthwindDataSet.CustomersDataTable = _
New NorthwindDataSet.CustomersDataTable()
‘ Fill the DataSet with data.
MyData.Fill(DS)
‘ Find a particular record using the Customer ID.
Return DS.FindByCustomerID(Customer)
End Function
[/code]

The code begins by creating a TableAdapter object. Because the example relies on the Data Source Configuration Wizard, it has a specific TableAdapter to use in the form of the NorthwindDataSetTableAdapters.CustomersTableAdapter, MyData object. MyData provides the means to select information from the table. In addition, it can update, delete, and insert records. Essentially, MyData is the database connection.

The next step is to create a DataTable object. Again, the example has a specific version, NorthwindDataSet.CustomersDataTable class, DS object. DS contains all the data selected from the database through the TableAdapter object.

In order to get data from the database into the DataTable object, the code calls the MyData.Fill() method. Until the code calls this method, DS contains all of the information about the Customers table, but none of the records.

Finally, the code calls the DS.FindByCustomerID() method to find the record requested by the caller. The input argument to this method, Customer, is a string that contains the CustomerID field value. The output from the call is a NorthwindDataSet.CustomersRow object, which is a specialized form of the DataRow. Interestingly enough, IronPython can use the DataRow directly without having to translate it in any way.

Accessing the Database Module through IronPython

The example extension has a method, GetData(), that accepts a CustomerID as input and provides a NorthwindDataSet.CustomersRow as output. All you need now is some IronPython code to make the request and display the result. Listing 17-6 shows a typical example.

Listin g 17-6: Displaying a record onscreen

[code]
# Import the Common Language Runtime.
import clr
# Access the extension.
clr.AddReferenceToFile(‘Northwind.DLL’)
import Northwind
# Create an instance of the Northwind access object.
MyData = Northwind.DBAccess()
# Fill a row with data.
Row = MyData.GetData(‘ALFKI’)
# Display the data on screen.
print ‘All the data for Customer ID ALFKI’
print ‘nCustomer ID: ‘, Row.CustomerID
print ‘Company Name: ‘, Row.CompanyName
print ‘Contact Name: ‘,
if Row.IsContactNameNull():
print ‘Nothing’
else:
print Row.ContactName
print ‘Contact Title: ‘,
if Row.IsContactTitleNull():
print ‘Nothing’
else:
print Row.ContactTitle
print ‘Address: ‘,
if Row.IsAddressNull():
print ‘Nothing’
else:
print Row.Address
print ‘City: ‘,
if Row.IsCityNull():
print ‘Nothing’
else:
print Row.City
print ‘Region: ‘,
if Row.Is_RegionNull():
print ‘Nothing’
else:
print Row._Region
print ‘Postal Code: ‘,
if Row.IsPostalCodeNull():
print ‘Nothing’
else:
print Row.PostalCode
print ‘Country: ‘,
if Row.IsCountryNull():
print ‘Nothing’
else:
print Row.Country
print ‘Phone: ‘,
if Row.IsPhoneNull():
print ‘Nothing’
else:
print Row.Phone
print ‘Fax: ‘,
if Row.IsFaxNull():
print ‘Nothing’
else:
print Row.Fax
# Pause after the debug session.
raw_input(‘nPress any key to continue…’)
[/code]

This listing looks like a lot of code, but the process is relatively simple. The example begins as usual by gaining access to CLR, using the AddReferenceToFile() method to create a reference to the extension, and creating an instance of the extension class.

At this point, the code calls MyData.GetData() with a CustomerID of ‘ALFKI‘. The output is placed in Row. If you use the dir() function on Row, you see it provides a lot more than a listing of fields that appear as part of the output. Figure 17-23 shows the attributes Row provides.

The output fields come in two types. The first are fields that the row must contain. These fields always contain data. The second are optional fields that might not contain data. If you try to print these fields, you’ll get an error. Consequently, the next section of code displays the mandatory fields first.

Row contains more than just fields.
Figure 17-23: Row contains more than just fields.

Notice the if…else structures that appear next. Every optional field includes an IsFieldNameNull() method. Before you print these optional fields, use the null check, such as Row.IsContactNameNull(), to verify that the field contains data. In this case, the code simply prints ‘Nothing‘ when the field is null.

You need to consider one other issue when working through your database access methods. Notice that the _Region field has an underscore in front of it. This underscore doesn’t appear in the database or in the Visual Basic.NET code — IronPython adds it for some reason. If you suddenly find that some fields aren’t accessible, even though you’re using the right name, check for an underscore. Figure 17-24 shows the output from this example.

The extension provides data to IronPython to output.
Figure 17-24: The extension provides data to IronPython to output.

 

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]

Extending IronPython Using C#

Understanding the Requirements for an Extension

It’s important to understand that an extension, any extension, probably ties your code to Windows. Whenever you use an extension with IronPython, you rely on something other than the Python libraries to perform a task, which means you lose the platform independence for which Python is so famous. In short, extensions provide considerable flexibility and help you provide additional capabilities for IronPython, but this flexibility isn’t without cost. Every time you make a design decision of this sort, you must pay a price in the following:

  • Reduced reliability: Due to increased failure points.
  • Weakened security: More languages mean more places where someone could leave a security hole.
  • Impaired speed: Marshaling data between language barriers takes time.
  • Fewer platforms: In order to use an extension, you must find a platform that supports both IronPython and the extension language.

Writing an extension isn’t always straightforward. It isn’t as simple as writing some class library code and putting it in a DLL. In fact, you must spend considerable effort thinking about how an extension should be designed to make it useable. The following list considers just a few of the most important factors for your extension.

  • Python language requirements: IronPython may not support every feature that the static language supports. For example, you may find that IronPython doesn’t support a particular static language operator, such as the ++ operator.
  • IronPython developer mentality: An extension that performs tasks in a way that runs completely counter to the way that an IronPython developer normally does them isn’t very useful, because the IronPython developer will have to think too hard about using the extension. The best kind of extension is one that feels natural to the IronPython developer.
  • Flexibility: An extension should provide some significant advantage in flexibility. When you write an extension, write it with the benefit to the IronPython developer in mind, not simply because the functionality the extension provides is interesting.

The one factor that you don’t need to consider is whether something is doable. Normally, if you can perform a task with the static language you want to use to build the extension, then you can do it with IronPython as well. Sometimes, you have to massage the data or present the technique in a way that doesn’t match your normal methodology, but you can normally perform the task with a bit of effort.

Considering IronPython and Static Language Differences

IronPython is a dynamic language (a language that does things like decide variable type at run time, which is contrasted with a static language that decides everything during compile time). As such, it has some significant advantages for the human developer that a static language can’t provide. It’s true that the concept of language is foreign to the computer, but the human developer relies on certain characteristics of language to accomplish tasks quickly and with few errors. Consequently, as part of defining the reason to use an extension, you must consider the differences between IronPython and the static language of your choice.

Defining Why You Use a Static Language with IronPython

Typically, you use a static language with IronPython to gain a specific advantage. For example, IronPython doesn’t create graphical user interfaces very well, so using a static language to perform this task could provide a significant advantage in development time. In addition, you could probably reuse code that you already have on hand, which may reduce debugging time as well. Look for the advantages that you can gain when using a static language with IronPython. If you have problems describing the material benefit of an extension, then perhaps you really should look at another solution.

Make sure you consider the strengths of the static language when making your selections. For example, C# is often the best choice for Win32 API interaction because it supports unsafe pointers — a requirement for certain specialized Win32 API tasks. Of course, you should make sure that the use of the Win32 API is actually required. Perhaps a third-party library already has the solution you require and with a lot less work. Visual Basic.NET is often the best choice for database work because it takes care of so many tasks in the background for the developer. You don’t have to worry so much about coercing data types because Visual Basic addresses the need for you in the background.

Sometimes the use of a static language is practical. For example, you might have an overwhelming number of developers on your team who know C# or Visual Basic.NET, but know nothing about IronPython. In general, this is one of the poorest reasons to use a static language with IronPython, but the reality of development today is that you often use the tools you have on hand to accomplish the task. No one can afford to have developers sitting on their hands simply because the dynamic language is the best choice for a particular job.

Understanding Line Noise

There are good reasons to avoid using a static language with IronPython. You can write most code in IronPython using far fewer lines than a static language requires. Fewer lines of code translate into higher developer productivity and sometimes into fewer coding errors as well.

The additional code that a static code developer must write is often referred to as line noise. The code doesn’t substantially translate into useful output, but the static language requires it. For example, IronPython doesn’t require that you declare the type of a variable — you simply leave this task to IronPython.

While the extra code in a static language does tend to reduce the potential for unintended output, it can also make the code harder to read. With every benefit, there’s a corresponding negative. When you decide to use an extension with IronPython, you need to consider when it’s appropriate to work through the extra code and cumbersome features of static languages and when IronPython is truly the better choice.

Let’s look at a quick example. Say you want to create an array of names in a function and pass them back to a caller. Here’s the C# code to perform the task.

[code]
public String[] GetNames()
{
String[] Result = new String[4];
Result[0] = “John”;
Result[1] = “Amy”;
Result[2] = “Jose”;
Result[3] = “Carla”;
return Result;
}
public void ShowNames()
{
String[] TheNames = GetNames();
foreach (String Name in TheNames)
{
Console.WriteLine(Name);
}
}
[/code]

The code in GetNames() creates an array of String, fills it with names, and returns those names to the caller, ShowNames(). At this point, ShowNames() uses a foreach loop to display each name individually. Now take a look at the same functionality written in IronPython.

[code]
def GetNames():
return “John”, “Amy”, “Jose”, “Carla”
def ShowNames():
for Name in GetNames():
print Name
[/code]

The code performs the same task in both cases, but as you can see, the IronPython code is significantly shorter. In addition, the IronPython code is actually easier to read.

Considering Scoping Issues

One of the most important differences between IronPython and static languages such as C# is that IronPython doesn’t have the concept of scope within classes. Everything in an IronPython class is public, so you always have access to every element. Of course, this presents a dilemma for languages that do support scope. When creating an IronPython extension, your static language scope declarations will change as follows:

  • Public members remain public.
  • Protected members become public.
  • Protected Internal members become public.
  • Private members remain private and don’t appear at all to IronPython.
  • Internal members become private and don’t appear at all to IronPython.

Creating the Simple C# Extension

The example in the following sections provides a simple set of calculations. Think of it as the basic four-function calculator with a bit extra added. The example doesn’t do anything fancy, but it does demonstrate techniques you need to build any C# extension for IronPython. The rest of the examples in this chapter build on this example, so you should at least scan the techniques presented in the sections that follow.

Creating the Project

A C# extension project in Visual Studio is nothing more than the typical class library. The following steps help you create the project for this example. You can use the same steps when working with the other examples — all you need to do is change the project name.

  1. Choose File ➪ New ➪ Project. You see the New Project dialog box shown in Figure 16-1.

    Create a new project to hold your C# extension.
    Figure 16-1: Create a new project to hold your C# extension.
  2. Choose the Visual C# folder in the Installed Templates list.
  3. Select .NET Framework 3.5 or an earlier version of the .NET Framework. Don’t select the .NET Framework 4.0 entry. The list of templates changes when you change the .NET Framework version.
  4. Select the Class Library template.
  5. Type Calcs in the Name field and click OK. Visual Studio creates a class library project for you.
  6. Right-click Class1.cs in Solution Explorer and choose Rename from the context menu. Visual Studio makes the filename editable.
  7. Type Calcs.CS for the new filename and press Enter. Visual Studio displays a dialog box that asks whether you’d like to rename all of the Class1.cs references to match the new filename.
  8. Click Yes. The project is ready for use.

At the time of this writing, IronPython doesn’t support extensions written using the .NET Framework 4.0. You must create your extensions using the .NET Framework 3.5 or earlier. Otherwise, the extension will simply fail to load and IronPython won’t provide anything in the way of an explanation (at least, nothing useable). If you suspect that you’ve targeted the wrong .NET Framework version, choose Project ➪ ProjectName Properties. Select the Application tab of the Properties window and change the entry in the Target Framework field to .NET Framework 3.5, as shown in Figure 16-2. The IDE may ask permission to modify features in your setup and require that you restart your project to see the effects of the change.

Modify the Target Framework field to a version of the .NET Framework that works with IronPython.
Fi gure 16-2: Modify the Target Framework field to a version of the .NET Framework that works with IronPython.

Developing the C# Extension

The C# extension does have a few tricks to it, but generally speaking, if you know how to create a class library, you already know how to create the code for a C# extension. Listing 16-1 shows the code for the example extension.

Listin g 16-1: A simple calculations extension

[code]
public class Calcs
{
private Int32 Data;
public Calcs(Int32 Value)
{
this.Data = Value;
}
public override string ToString()
{
return Data.ToString();
}
public static Calcs operator +(Calcs Value1, Calcs Value2)
{
return new Calcs(Value1.Data + Value2.Data);
}
public static Calcs operator -(Calcs Value1, Calcs Value2)
{
return new Calcs(Value1.Data – Value2.Data);
}
public static Calcs operator *(Calcs Value1, Calcs Value2)
{
return new Calcs(Value1.Data * Value2.Data);
}
public static Calcs operator /(Calcs Value1, Calcs Value2)
{
return new Calcs(Value1.Data / Value2.Data);
}
public Calcs Inc()
{
return new Calcs(this.Data + 1);
}
public Calcs Dec()
{
return new Calcs(this.Data – 1);
}
}
[/code]

In most cases, you want to create a constructor that accepts the kind of data you want to manipulate with the extension. In this case, the constructor accepts an Int32 value. Interestingly enough, the constructor is the only place where you normally reference the data type of the data directly. In all other cases, you work with the data type indirectly by using the extension class.

Another issue is displaying the data in IronPython. The default implementation of the ToString() method displays the class name, which isn’t helpful. Consequently, you must override the default implementation of ToString() and provide your own output. In this case, the method simply returns the current value of the private variable Data as a string.

This example deals with operators. Of course, there are two kinds of operators, unary and binary. The method you implement for each kind of operator is different.

To create a binary operator, you must consider that the operator will work with two instances of the Calcs class. In short, the operator works with the base class and you must declare it as static. In this example, the + operator is binary, so the code declares it as static. The method also accepts the two instances of the Calcs class as input. In order to return output, the method must create a new instance of the Calcs class with the sum of the two input values. Notice that the method never defines what kind of data it works on, simply that the data is contained in an instance of the Calcs class.

Creating a unary operator is different because you’re working with a single instance of the Calcs class in this instance. To create a unary operator, you simply declare the method as a non-static member of the class, as shown for the Inc() and Dec() methods. In this case, because you’re working with a single value, the code uses this.Data (the internal representation of the data value of the single value) to perform the math. You may wonder why the code simply doesn’t create a ++ operator method. A ++ operator method would look like this and wouldn’t work in a unary manner within IronPython.

[code]
public static Calcs operator ++(Calcs Value1)
{
return new Calcs(Value1.Data + 1);
}
[/code]

If you compiled the class now, you could view it in the IronPython console. The following code provides the steps for loading the extension into memory.

[code]
import clr
clr.AddReferenceToFile(‘Calcs.DLL’)
import Calcs
dir(Calcs.Calcs)
[/code]

Figure 16-3 shows the output of the dir(Calcs.Calcs) call. Notice that Inc() and Dec() appear as you expect. However, there aren’t any entries for +, -, *, and / methods. These operators still work as you expect, but IronPython shows a Python equivalent for the operators in the form of the __add__(), __radd__(), __sub__(), __rsub__(), __mul__(), __rmul__(), __div__(), and __rdiv__(). These methods don’t appear unless you define the operators in your class.

If you’re looking at the class in the IronPython console, you might want to give it a quick try before you close up the console and move on to the next part of the example. Try this code and you’ll see an output of 15 from the __add__() method.

[code]
Value1 = Calcs.Calcs(10)
Value2 = Calcs.Calcs(5)
print Value1.__add__(Value2)
[/code]

The dir() function shows the content of the Calcs class.
Figure 16-3: The dir() function shows the content of the Calcs class.

Adding the IronPython Project

At this point, you have a C# extension (or module) to use with IronPython. Of course, you’ll want to test it. The easiest way to do this is to add the IronPython project directly to the current solution. The following steps describe how to perform this task.

  1. Right-click the solution entry in Solution Explorer and choose Add ➪ Existing Project from the context menu. You see the Add Existing Project dialog box shown in Figure 16-4.

    Locate IPY.EXE and add it to your solution.
    Figure 16-4: Locate IPY.EXE and add it to your solution.
  2. Locate IPY.EXE on your hard drive and highlight it. Click Open. You see a new project entry added to the solution.
  3. Right-click the ipy entry in Solution Explorer and choose Set as Startup Project from the context menu. This step ensures that choosing one of the startup options from the Debug menu starts the IronPython application.
  4. Right-click the ipy entry in Solution Explorer and choose Properties from the context menu. You’ll see the General tab of the ipy Properties window shown in Figure 16-5.

    Configure the IronPython application to work with Calcs.DLL.
    Figure 16-5: Configure the IronPython application to work with Calcs.DLL.
  5. Type -D TestCalcs.py in the Arguments field.
  6. Click the ellipses in the Working Directory field to display the Browse for Folder dialog box. Locate the output folder of the Calcs.DLL (or other extension) file. Click OK. The IDE adds the correct directory information to the Working Directory field.
  7. Open Windows Explorer. Locate the CalcsCalcsbinDebug folder. Right-click in the right pane and choose New ➪ Text Document from the context menu. Name the file TestCalcs.py and press Enter. Click Yes if asked if you want to rename the file extension.
  8. Right-click the solution item in Solution Explorer and choose Add ➪ Existing Item from the context menu to display the Add Existing Item dialog box shown in Figure 16-6.
  9. Locate the TestCalcs.py file in the solution and click Add. Visual Studio adds TestCalcs.py to the Solution Items folder in Solution Explorer and automatically opens the file for you. You’re ready to add test code for the application.
Add the TestCalcs.py file to the solution.
Figure 16-6: Add the TestCalcs.py file to the solution.

Creating the IronPython Application

Now that you have a file to use for the IronPython application, it’s time to add some code to it. The example code fully exercises everything you can do with the C# extension. Listing 16-2 shows the code you add to the TestCalcs.py file.

Listin g 16-2: Testing the extension using IronPython

[code]
# Add a reference to the CLR
import clr
# Obtain access to the extension.
clr.AddReferenceToFile(‘Calcs.DLL’)
import Calcs
# Create an instance of the class and fill it with data.
Value1 = Calcs.Calcs(10)
# Print the original value, then decrement and increment it.
print ‘Original Value1 Content: ‘, Value1
print ‘Value1 + 1: ‘, Value1.Inc()
print ‘Value1 – 1: ‘, Value1.Dec()
# Create a second value and display it.
Value2 = Calcs.Calcs(5)
print ‘nOriginal Value2 Content: ‘, Value2
# Use the two values together in different ways.
print ‘nValue1 + Value2 = ‘, Value1 + Value2
print ‘Value1 – Value2 = ‘, Value1 – Value2
print ‘Value1 * Value2 = ‘, Value1 * Value2
print ‘Value1 / Value2 = ‘, Value1 / Value2
# Pause after the debug session.
raw_input(‘nPress any key to continue…’)

[/code]

The example begins by importing support for the Common Language Runtime (CLR). It then uses the AddReferenceToFile() method to reference the Calcs.DLL file and imports the code into IronPython. These steps are similar to those that you used to test the DLL initially.

The next step is to create an instance of the Calcs class, Value1. The code references Calcs twice — once for the namespace and a second time for the class itself. The next few code steps display the value of Value1 and show how to use the Inc() and Dec() methods. If you set Value1 equal to the output of Inc() or Dec(), it truly would increment or decrement the value of Value1. Because IronPython doesn’t support the ++ operator, however, you can’t use the ++ operator in your extension. On the other hand, you could implement the += and -= operators.

You can’t really test binary operators without a second variable, so the code creates a second instance of Calcs, Value2. The example then shows how the +, -, *, and / operators work. Figure 16-7 shows the output from this example.

Here are the results of using the C# extension within IronPython.
Figure 16-7: Here are the results of using the C# extension within IronPython.

Using C# for User Interface Support

It’s a painful process because you don’t have access to any designers, but the process is definitely doable. You may very well decide to use IronPython directly for all your graphics needs, simply to avoid using another language. However, C# or Visual Basic.NET make better choices for creating a user interface because you do get access to the designer support that these languages provide. With this in mind, the following sections describe how you can add graphic support to IronPython using a C# extension.

Defining a Library of Dialog Boxes

If you’re using IronPython as your main application language and relying on a static language for ancillary support, such as the user interface requirements, it makes sense to create all the dialog boxes you require and place them in a library. Of course, if the application is relatively complex, you might use several physical DLLs to perform the task or rely on a single DLL, but rely on multiple projects to accommodate a team of developers The point is that you need to plan how to store the dialog boxes in a manner that makes it efficient to work on the project.

There’s a tendency by some developers to create generic dialog boxes and then manipulate them in code. This technique does work well when you use the dialog boxes in the static language. However, the approach can become counterproductive when using the dialog boxes in IronPython. The IronPython code can become so complicated that it becomes unreliable and hard to maintain. In general, use specific dialog boxes whenever possible, which won’t require many (or any) changes.

IronPython doesn’t have a representation of every C# or Visual Basic.NET feature. For example, in the section “Developing the C# Extension” section earlier in this chapter, you’ll discover that IronPython doesn’t support the C# ++ operator, but it does support the += operator. It’s best to perform data manipulation in the static language environment when possible or pass the raw data to IronPython in a form it can readily use. For example, you might pass a list of field values to IronPython as a dictionary.

Marshaling data between languages can reduce application performance. You may find situations where you need to process data in a thread to maintain acceptable performance for the user. However, before you take time to create a complex threading solution, ask users to try the application in a test environment to determine whether the performance is acceptable.

Creating the Dialog Box Library in C#

Your dialog box library can support dialog boxes at two levels. It’s possible to meet some IronPython needs using a simple message box or prompt box. Because these solutions are already programmed for you, supporting them through the static language, where the features are easily accessed, is a good way to save on development and debugging time. You can customize the implementation of these standardized features to make them easy to use within IronPython — reducing the need to import a lot of managed assemblies into IronPython.

Of course, many user-interface needs require something more advanced than a simple message box. The following sections describe how to create simple message boxes and complex Windows Forms in C# that you can use in your IronPython application. The goal is to use the right kind of interface element for a given task and to make the interface element easy to access and process from within IronPython. The section “Creating the Simple C# Extension” earlier in this chapter describes how to set up the solution used for this example.

Defining Simple Message Boxes

This example is interesting because it shows how you can create overrides of your methods. The MessageBox.Show() method has 21 overrides in C#. Of course, you might not need all those overrides and the example shows only five of them. Before you can work with message boxes in a C# class, you need to add a reference to the System.Windows.Forms.DLL and add the following using statement.

[code]
using System.Windows.Forms;
[/code]

Now that you have the prerequisites in place, it’s time to look at some code. Listing 16-3 shows the code used to create this example.

Listin g 16-3: Creating a simple message box class

[code]
public class Dialogs
{
public Dialogs()
{
}
public String ShowMessage(String Msg)
{
return MessageBox.Show(Msg).ToString();
}
public String ShowMessage(String Msg, String Title)
{
return MessageBox.Show(Msg, Title).ToString();
}
public String ShowMessage(String Msg, String Title, Int16 Buttons)
{
return MessageBox.Show(Msg, Title,
(MessageBoxButtons)Buttons).ToString();
}
public String ShowMessage(String Msg, String Title, Int16 Buttons,
Int16 Icon)
{
return MessageBox.Show(Msg, Title,
(MessageBoxButtons)Buttons, (MessageBoxIcon)Icon).ToString();
}
public String ShowMessage(String Msg, String Title, Int16 Buttons,
Int16 Icon, Int16 DefaultButton)
{
return MessageBox.Show(Msg, Title,
(MessageBoxButtons)Buttons, (MessageBoxIcon)Icon,
(MessageBoxDefaultButton)DefaultButton).ToString();
}
}
[/code]

The code begins with the usual constructor. The constructor doesn’t really need to do anything in this case. Of course, you could set up the constructor to accept some of the required inputs, such as the message and message box title, but sending the information with the ShowMessage() method works just fine, too. The constructor could also set up default settings, if desired, that the developer could override with specific versions of ShowMessage().

The ShowMessage() method declarations come next. The methods are relatively simple. Each one calls a different override of the MessageBox.Show() method. Notice that you must coerce the MessageBoxButtons, MessageBoxIcon, MessageBoxDefaultButton values from the inputs. You could ask the caller to provide the actual enumerated values, but that approach would reduce the benefit of using this approach for working with message boxes, because the developer would need to load the required .NET assemblies anyway.

Even when working with simple message boxes, you can encounter a few problems. For example, the enumerations provided in the static environment make it simple to select a particular button combination or icon. IntelliSense displays the list of values from which you can choose. However, IronPython doesn’t provide IntelliSense, so there isn’t any simple method of selecting a button combination or icon from a list. The example uses numbers, which works fine for the button combinations because they’re numbered 0 through 5. However, the icons have values of 0, 16, 32, 48, and 64, which are hardly easy to remember. The default button values are equally odd at 0, 256, and 512. Tables 16-1 through 16-3 show the values for the message box enumerations. In a production environment, you’d probably create text equivalents for the developer, which you could translate in the extension, or provide some type of enumeration for the developer.

Message Box Button Combinations
Table 16-1: Message Box Button Combinations

 

Table 16-2: Message Box Icon Combinations
Table 16-2: Message Box Icon Combinations
Table 16-2: Message Box Icon Combinations
Table 16-2: Message Box Icon Combinations (continued)
Message Box Default Button Options
Table 16-3: Message Box Default Button Options

Using Enumerations with IronPython

There’s a way around the issue of enumerated values in .NET calls. You can simply choose to create your own enumeration. For example, let’s say you want to overcome the problem of working with the MessageBoxButtons enumeration. In this case, you create an enumeration and a new override of the ShowMessage() method as shown here.

[code]
public enum ButtonTypes
{
OK,
OKCancel,
AbortRetryIgnore,
YesNoCancel,
YesNo,
RetryCancel
}
public String ShowMessage(String Msg, String Title, ButtonTypes Buttons)
{
return MessageBox.Show(Msg, Title,
(MessageBoxButtons)Buttons).ToString();
}
[/code]

Notice that you must still use coercion to make the MessageBox.Show() call. However, the IronPython developer now has an enumeration to use when making the call. Here’s a typical call from within IronPython.

[code]
MyDialog.ShowMessage(‘Hello’, ‘Title’, MyDialog.ButtonTypes.OKCancel)
[/code]

The resulting message box would contain ‘Hello‘ as the message, ‘Title‘ as the message box title, and two buttons, OK and Cancel.

Considering Developer Help

As your extensions gain in complexity, you need to start providing some help to the IronPython developer. Most IronPython developers will spend part of their time in the interpreter trying things out. The developer will look to your documentation for help in using the extension you create. There are two forms of help, as shown here.

[code]
help(MyDialog.ShowMessage)
MyDialog.ShowMessage.__doc__()
[/code]

It turns out that IronPython automatically provides a form of the help() function help for you as shown in Figure 16-8. In this case

IronPython provides a kind of help for you automatically.
Figure 16-8: IronPython provides a kind of help for you automatically.

Unfortunately, IronPython doesn’t provide the __doc__() method by default. You must define it for yourself as part of the class you create. Here’s a simple __doc__() method you can use with the example. Of course, a production version would contain far more information.

[code]
public String __doc__()
{
return “This is a help string”;
}
[/code]

When you try this method out at the Python prompt, you see the outline shown in Figure 16-9. You can use all of the normal formatting characters to make the help provided by the __doc__() method look nice. For that matter, you could store the information externally and simply read it in as needed.

 You must define your own version of the __doc__() method.
Figure 16-9: You must define your own version of the __doc__() method.

Defining Complex Forms

At some point, simple message boxes simply won’t do the job for you. After all, you’ll want forms that contain a number of fields that you can use to process complex information from the user. In this case, you must create a standard Windows form for your extension. To accomplish this task, you begin by adding the form using the following steps.

  1. Right-click Dialogs in Solution Explorer and choose Add ➪ New Item. Select the Windows Forms entry in the Installed Templates list. You see the Add New Item dialog box shown in Figure 16-10.

    Add a Windows Form to your project
    Figure 16-10: Add a Windows Form to your project
  2. Highlight the Windows Form entry. Type TestForm.CS in the Name field and click Add. Visual Studio adds the new form to your project and automatically opens it for editing.

At this point, you can create the form just as you normally would for any static application. Figure 16-11 shows the form used for this example. It’s simple, but it contains multiple dataentry fields and multiple exit options.

The Windows Form can contain any level of complexity you desire.
Figure 16-11: The Windows Form can contain any level of complexity you desire.

Before you assume anything about this form, note that it does differ in a few ways from the forms you’ve created for your static applications. The first difference is that the buttons that close the form, rather than do something within the form, must have the DialogResult property set to a unique value or you won’t be able to tell which button the user clicked. For this example, the DialogResult for btnOK is OK, while the DialogResult for btnCancel is Cancel.

The second difference involves a problem with getting information from the form you create to the IronPython application. You could contrive all sorts of odd methods for accomplishing the task, but the simplest method is to set the Modifiers property for the individual controls (txtName and txtColor) to Public. In this case, using Public doesn’t create a problem because IronPython sets everything to public. In all other respects, there’s no difference between this form and any other form you’ve created in the past.

To make things simple, this example doesn’t use any code-behind for the form itself. Any codebehind works as you’d expect. There isn’t any difference between calling the form from IronPython than calling it from within your C# application.

Accessing the Dialog Box Library from IronPython

At this point, you have a nice collection of dialog box and form classes to use in an IronPython application. Of course, a production application would probably have quite a few more forms in it, but you have enough for testing and experimentation purposes. The following sections describe how to use these classes.

An Alternative Method for Adding the IronPython Project

There are a number of ways to configure a test setup for your extensions. The section “Adding the IronPython Project” earlier in this chapter shows one technique. The technique works well when you want to maintain separate builds of your extension. However, you might want to maintain just one build — the build you’re currently using for debugging, testing, or experimentation. Use the following steps to create a centralized test configuration.

  1. Right-click Dialogs in Solution Explorer and choose Properties from the context menu. Select the Build tab. You see the Properties window shown in Figure 16-12.
  2. Click Browse next to the Output Path field to display the Select Output Path dialog box shown in Figure 16-13. Because you’ll add the IronPython test file at the solution level, you need to send the output to the solution level as well.

    Configure the build to use a central output location.
    Figure 16-12: Configure the build to use a central output location.
  3. Select the first Dialogs entry in the list and click OK. Visual Studio adds an absolute path to the Output Path field that you must change for every machine that uses the application. As an alternative, you could type ..(two periods and a backslash) in the field to place the output in the solution folder.
  4. Select the next configuration in the Configuration field.
  5. Perform Steps 2 through 4 for each configuration. Make sure each configuration uses the same output directory. Normally, your project will only contain Debug and Release configurations.

    Modify the output path as required for your application.
    Figure 16-13: Modify the output path as required for your application.
  6. Right-click the solution entry in Solution Explorer and choose Add ➪ Existing Project from the context menu. You see the Add Existing Project dialog box shown in Figure 16-4.
  7. Locate IPY.EXE on your hard drive and highlight it. Click Open. You’ll see a new project entry added to the solution.
  8. Right-click the ipy entry in Solution Explorer and choose Set as Startup Project from the context menu.
  9. Right-click the ipy entry in Solution Explorer and choose Properties from the context menu. You see the General tab of the ipy Properties window shown in Figure 16-5.
  10. Type -D DialogTest.py in the Arguments field.
  11. Click the ellipses in the Working Directory field to display the Browse for Folder dialog box. Locate the solution folder for the project (the first Dialogs folder). Click OK. The IDE adds the correct directory information to the Working Directory field.
  12. Right-click the solution entry in Solution Explorer and choose Add ➪ New Item from the context menu. You see the Add New Item dialog box shown in Figure 16-14.

    Add the IronPython test file to your project.
    Figure 16-14: Add the IronPython test file to your project.
  13. Type DialogTest.py in the Name field and click Add. Visual Studio adds the new file to the Solution Items folder in Solution Explorer and opens the file automatically for editing.

Performing the Message Box and Form Tests

It’s finally time to test the message boxes and forms you’ve created. The code in this section performs a few simple tests and demonstrates how to obtain output from the message boxes and forms you’ve created. You can use this code as a starting point for more complex processing in your own application. Listing 16-4 shows the test code for this application.

Listin g 16-4: Testing the extension using IronPython

[code]
# Define the message box tests.
def TestMessages():
# Create a message box object.
MyDialog = Dialogs.Dialogs()
# Test a simple message box.
print ‘Testing a simple message box.’
print ‘Simple message box output: ‘,
print MyDialog.ShowMessage(‘Hello’)
# Perform a more complex test.
print ‘nA more complex message box.’
print ‘Complex message box output: ‘,
print MyDialog.ShowMessage(‘Hello Again’, ‘Title 2’, 3, 64, 256)
# Define the form test.
def TestForm():
# Create the form instance.
MyForm = Dialogs.TestForm()
# Display the form and test the dialog result.
print ‘nThe form example.’
if MyForm.ShowDialog().ToString() == ‘OK’:
# Display the results.
print ‘The user clicked OK.’
print ‘User Name: ‘, MyForm.txtName.Text
print ‘Favorite Color: ‘, MyForm.txtColor.Text
# Display an alternate result.
else:
print ‘The user clicked cancel.’
# Import the Common Language Runtime.
import clr
# Access the extension.
clr.AddReferenceToFile(‘Dialogs.DLL’)
import Dialogs
# Test the message box code.
TestMessages()
# Test the form code.
TestForm()
# Pause after the debug session.
raw_input(‘nPress any key to continue…’)
[/code]

The test code begins by importing CLR and gaining access to the Dialogs namespace. This example demonstrates one of the benefits of using a namespace, easy access to multiple classes. It’s a good way to organize a library of forms to make them easy to access and to avoid naming conflicts.

The TestMessages() function contains the code to test the Dialogs.Dialogs class. This code begins by creating a Dialogs.Dialogs instance, MyDialog. In this case, the application begins by creating a simple message box and displaying it onscreen. This message box lacks a title and contains only an OK button. When the user clicks OK, the program prints the dialog result to screen.

The second test is a little more complex. This time the code relies on the most complex form of the ShowMessage() method to display a dialog box that contains a message, title, icon, and multiple buttons as shown in Figure 16-15. Notice that the figure shows that the message box also has the middle button selected by default. Pressing Enter will automatically select this default option. Normally, message boxes select the first button as the default. Depending on which button the user clicks, the application will display a message with the appropriate dialog result. You could also use this dialog result as part of an if…else statement to choose an appropriate course of action.

A more complex message box includes multiple buttons, a title, and an icon.
Figure 16-15: A more complex message box includes multiple buttons, a title, and an icon.

The TestForm() method begins by creating an instance of Dialogs. TestForm, MyForm. The dir() function will show you that MyForm now has access to all of the functionality normally associated with a Windows Forms class, but without importing any of the bulk associated with the System.Windows .Forms assembly. As with any Windows Form, you call ShowDialog() to display the form. However, the result of displaying the form is going to be something that IronPython can’t use directly. The way to overcome this problem is to call ShowDialog().ToString(). In this case, the output is a string that describes which button the user has clicked.

This portion of the example shows how to process the form data locally. When the user clicks OK, the dialog result is ‘OK‘ and the if statement succeeds. The code accesses the MyForm.txtName.Text and MyForm.txtColor.Text properties to determine what the user has typed. When the if statement fails, the code displays a message telling you that the user clicked Cancel. Figure 16-16 shows typical output from this example.

Here are the results of using the C# extension within IronPython.
Figure 16-16: Here are the results of using the C# extension within IronPython.

Using C# for Win32 Support

The Python language doesn’t really support much in the way of platform-specific functionality and that’s by design. One of the tenets of cross-platform compatibility is not to make an issue out of the platform on which the code runs. However, in some cases, you really do need to access the platform and discover things about it. For example, you might want to know more about the environment in which your application is executing, such as the size of the console window. You might even want to clear the console window (a feature that is missing from the IronPython console, without which your sessions can appear messy). An application may need to know something about the security in place for the current session. In short, you might have many reasons for wanting to know something more, but Python (and by extension, IronPython) largely lacks the functionality to provide this information.

The example in the following sections plays to a strength of C#, which is to interact with the Windows platform through a feature called Platform Invoke (P/Invoke). This example goes outside the managed .NET environment and relies on the Win32 API to access Windows functionality that you can’t access through .NET.

Creating the P/Invoke Code

Before you can write any P/Invoke code, you need to add the following using statement.

[code]
using System.Runtime.InteropServices;
[/code]

This statement provides access to the various special programming features that C# provides for accessing the Win32 API.

If you haven’t worked with the Win32 API in the past, you might find the use of structures, enumerations, and pointers confusing. In reality, all these events take place somewhere in the background when you execute any application. At some point, your managed code ends up interacting with the Win32 API to perform tasks because the basic Windows DLLs still rely on the Win32 API. Normally, CLR hides all these details from view so you don’t need to worry about them. Listing 16-5 shows the Win32 API access code — the lower-level code that does all the hard work for this example.

Listin g 16-5: Win32 API access code and structures

[code]
// This special class contains an enumeration of
// standard handles.
class StdHandleEnum
{
public const int STD_INPUT_HANDLE = -10;
public const int STD_OUTPUT_HANDLE = -11;
public const int STD_ERROR_HANDLE = -12;
};
// The GetStdHandle() function returns a handle to any
// standard input or output.
[DllImport(“kernel32.dll”, SetLastError=true)]
public static extern IntPtr GetStdHandle(int nStdHandle);
// This sructure contains a screen coordinate.
[StructLayout(LayoutKind.Sequential, Pack=1)]
public struct COORD
{
public short X;
public short Y;
}
// Obtains the current display mode–fullscreen or fullscreen hardware.
[DllImport(“Kernel32.DLL”)]
public static extern bool GetConsoleDisplayMode(ref UInt32 lpModeFlags);
// An enumeration used to determine the current display mode.
public enum ConsoleDispMode
{
CONSOLE_WINDOWED = 0, // Only implied by function.
CONSOLE_FULLSCREEN = 1, // The console is fullscreen.
CONSOLE_FULLSCREEN_HARDWARE = 2 // The console owns the hardware.
}
// Obtains the size of the largest console window possible.
[DllImport(“Kernel32.DLL”)]
public static extern COORD
GetLargestConsoleWindowSize(IntPtr hConsoleOutput);
// Returns the console mode information.
[DllImport(“Kernel32.DLL”)]
public static extern bool GetConsoleMode(
IntPtr hConsoleHandle,
ref UInt32 lpMode);
public enum ModeFlags
{
// Input mode flags
ENABLE_PROCESSED_INPUT = 0x0001,
ENABLE_LINE_INPUT = 0x0002,
ENABLE_ECHO_INPUT = 0x0004,
ENABLE_WINDOW_INPUT = 0x0008,
ENABLE_MOUSE_INPUT = 0x0010,
// Output mode flags
ENABLE_PROCESSED_OUTPUT = 0x0001,
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
}
[/code]

Many of the Win32 API functions require you to know specific integer or hexadecimal values. Even C++ developers can’t remember these numbers. Normally, a C++ developer relies on define statements that put the numbers into human-readable form. The P/Invoke code used in this chapter does the same thing, but sometimes it places the numbers in an enumeration to make them even easier to use. The StdHandleEnum class provides a list of standard handles (pointers) for Windows devices: input, output, and error. However, these aren’t the actual handles.

In order to get the standard Windows handle, an application must call the GetStdHandle() function. This function is in kernel32.dll. The [DllImport()] attribute tells the compiler where to look for an external Win32 API function that you want to use in your code. In this case, the attribute also tells the compiler that you want any error information that the Win32 API can provide. The use of extern before the function name tells the compiler that the requested DLL contains a function of the name that follows. You can now call this function directly and CLR will automatically perform any required marshaling for you.

Many of the Win32 API calls provide coordinates — x and y locations that tell where something is or how large it is. The COORD structure provides a means of transferring this kind of information between the .NET environment and the Win32 API environment. Windows uses a very basic view of structures. Unfortunately, .NET often causes problems by trying to optimize the data structures and causes P/Invoke calls to fail even though they should succeed. The [StructLayout()] attribute tells the compiler how to create a data structure in memory, which overrides the normal optimization process.

You may create applications that need to run in full-screen mode, if for no other reason than that they require the additional screen real estate to present information to the user. The GetConsoleDisplayMode() function tells you what mode the console is currently in. If the console is in the wrong mode, you can ask the user to change the mode or simply stop the application before the screen mode causes any problems. This function returns flags, not an enumerated value. At least one of the flags is always set, but the return value can have multiple flags set. The ConsoleDispMode enumeration makes it easier to work through the flag settings and provide usable output. The section “Defining the GetCurrentDisplayMode() Method” later in this chapter provides more information about this function.

In some cases, you need to know the largest size console window that the system will support. The GetLargestConsoleWindowSize() function provides this information. You can use other Win32 API functions to adjust the size of the window to meet application requirements (which is a topic for another book). The section “Defining the GetConsoleWindowSize() Method” provides more information about this function.

It’s also handy to know what kinds of operations the console window will support. For example, it’s good to know whether the console window will respond to the mouse. The GetConsoleMode() function provides this kind of information. The output is in the form of flags that you must interpret in your code. The GetConsoleMode() function is special in that the output you receive depends on the kind of device handle you provide. The output differs when you provide an input handle, versus an output handle. The section “Defining the GetConsoleInfo() Method” provides additional information about how this technique works.

Developing the IronPython Callable Methods

The P/Invoke code shown in Listing 16-5 does expose the Win32 API calls needed to perform certain tasks with IronPython. Theoretically, you could rely on just the code in Listing 16-5 to gain the access you require in IronPython. However, the task would be difficult because you’d need to work through the required bit manipulations. It’s better to place the code you need to access the Win32 API in easily called methods, which is the purpose of the code in the following sections.

Defining Common Variables and the Constructor

Win32 API calls often reuse information. It’s not uncommon for functions to ask for the same information over and over. For example, any function that works with a window will probably need the handle for that window. With this requirement in mind, Listing 16-6 shows the common variables and the constructor used for this example.

Listin g 16-6: Common variables and constructor

[code]
UInt32 DisplayMode = 0; // The current display mode.
IntPtr hOut; // Handle to the output device.
IntPtr hIn; // Handle to the input device.
UInt32 ConsoleMode = 0; // The console mode information.
public ConMode()
{
// Obtain a handle to the console screen and console input.
hIn = GetStdHandle(StdHandleEnum.STD_INPUT_HANDLE);
hOut = GetStdHandle(StdHandleEnum.STD_OUTPUT_HANDLE);
}
[/code]

The common variables include the current display mode (such as windowed), the console mode information (such as whether it accepts mouse input), and the handles for the input and output devices. These variables represent common pieces of information that the developer requires for multiple calls.

The constructor initializes the input and output handles using the GetStdHandle() function. The input argument simply tells Windows which handle you want. The output is an IntPtr, a special kind of variable that points to something. An IntPtr is a safe pointer, meaning you can use it without problems in a managed language. C# also supports unsafe pointers that you should use only as a last resort.

Defining the GetCurrentDisplayMode() Method

Sometimes you need to know whether the console is presented in a windowed or full-screen mode. A windowed console can get covered up and needs to share resources with other windows. In addition, the text in a windowed console can be small and hard to read. On a positive note, using a windowed console makes it easier to share data between applications. In most cases, the user will prefer that you use a windowed console to make it easier to multitask between applications. Listing 16-7 shows how to detect the current console display mode.

Listin g 16-7: Obtaining the current display mode

[code]
public OutputMode GetCurrentDisplayMode()
{
// Get the current display mode.
if (GetConsoleDisplayMode(ref DisplayMode))
// Determine if the console is in windowed mode.
if (DisplayMode == (UInt32)ConsoleDispMode.CONSOLE_WINDOWED)
return OutputMode.Windowed;
else
{
// If the console is fullscreen mode, determine which
// of the potential conditions are true.
switch (DisplayMode)
{
case (UInt32)ConsoleDispMode.CONSOLE_FULLSCREEN:
return OutputMode.Fullscreen;
case (UInt32)ConsoleDispMode.CONSOLE_FULLSCREEN_HARDWARE:
return OutputMode.HadwareAccess;
case (UInt32)ConsoleDispMode.CONSOLE_FULLSCREEN +
(UInt32)ConsoleDispMode.CONSOLE_FULLSCREEN_HARDWARE:
return OutputMode.FullscreenHardwareAccess;
}
}
// Return a default value.
return OutputMode.Unknown;
}
[/code]

The code begins by calling GetConsoleDisplayMode() to obtain the display mode as a numeric value. The information is returned in DisplayMode, not as a return value from the function call. The function itself returns a success value that indicates the call was successful. The first if statement says that if the call is successful, then DisplayMode will contain the console display mode, and that the application should proceed to process it. Because DisplayMode provides a return value, you must include the ref keyword when passing it to the Win32 API.

Now that the code has a display mode value, it needs to process it. If a console is in windowed mode, all the code has to do is return a value that says it’s windowed. However, full-screen mode requires some additional processing. When a console is in full-screen mode, it can also have access to the hardware. This is virtual hardware access, but it still feels to the application as if the access is direct. Consequently, the code must now determine whether the console is simply in full-screen mode or it’s in full-screen mode with hardware access.

The call could fail, but it’s unlikely to. Even so, the GetCurrentDisplayMode() handles the potential problem by providing the OutputMode.Unknown return value. This value simply says that the method couldn’t determine the current console display mode.

Defining the GetConsoleWindowSize() Method

Sometimes an application needs to know the maximum windowed console that a machine can accommodate. You might need additional room to display complex textual information. The Win32 API returns this information in a COORD structure that simply states the number of rows and columns of text that a console can support at maximum size. The following code shows the GetConsoleWindowSize() method used to obtain this information.

[code]
public COORD GetConsoleWindowSize()
{
// Determine the largest screen size possible.
return GetLargestConsoleWindowSize(hOut);
}
[/code]

This method is easy. All it does is call the GetLargestConsoleWindowSize() function with the output handle. Make sure you provide the output handle, and not the input handle, when making this call. The X and Y members of COORD contain the maximum screen size on return from the call.

Defining the GetConsoleInfo() Method

Consoles can support a number of input and output methods. For example, a console can support the mouse, which may make it easier for the user to interact with your character-mode application. If a console provides support for echo, it re-displays commands sent to it from batch files and other forms of automation. Consequently, you might find it useful to know just what the console will do for you. Listing 16-8 shows how to determine the input and output handling that a console provides.

Listin g 16-8: Obtaining the console characteristics

[code]
public struct ConsoleData
{
public Boolean Echo;
public Boolean LineInput;
public Boolean MouseInput;
public Boolean ProcessedInput;
public Boolean WindowInput;
public Boolean ProcessedOutput;
public Boolean LineWrap;
}
public ConsoleData GetConsoleInfo()
{
// Create the required structure.
ConsoleData Output = new ConsoleData();
// Retrieve the input information.
if (GetConsoleMode(hIn, ref ConsoleMode))
{
if ((ConsoleMode & (UInt32)ModeFlags.ENABLE_ECHO_INPUT) ==
(UInt32)ModeFlags.ENABLE_ECHO_INPUT)
Output.Echo = true;
else
Output.Echo = false;
if ((ConsoleMode & (UInt32)ModeFlags.ENABLE_LINE_INPUT) ==
(UInt32)ModeFlags.ENABLE_LINE_INPUT)
Output.LineInput = true;
else
Output.LineInput = false;
if ((ConsoleMode & (UInt32)ModeFlags.ENABLE_MOUSE_INPUT) ==
(UInt32)ModeFlags.ENABLE_MOUSE_INPUT)
Output.MouseInput = true;
else
Output.MouseInput = false;
if ((ConsoleMode & (UInt32)ModeFlags.ENABLE_PROCESSED_INPUT) ==
(UInt32)ModeFlags.ENABLE_PROCESSED_INPUT)
Output.ProcessedInput = true;
else
Output.ProcessedInput = false;
if ((ConsoleMode & (UInt32)ModeFlags.ENABLE_WINDOW_INPUT) ==
(UInt32)ModeFlags.ENABLE_WINDOW_INPUT)
Output.WindowInput = true;
else
Output.WindowInput = false;
}
// Retrieve the output information.
if (GetConsoleMode(hOut, ref ConsoleMode))
{
if ((ConsoleMode & (UInt32)ModeFlags.ENABLE_PROCESSED_OUTPUT) ==
(UInt32)ModeFlags.ENABLE_PROCESSED_OUTPUT)
Output.ProcessedOutput = true;
else
Output.ProcessedOutput = false;
if ((ConsoleMode & (UInt32)ModeFlags.ENABLE_WRAP_AT_EOL_OUTPUT)
== (UInt32)ModeFlags.ENABLE_WRAP_AT_EOL_OUTPUT)
Output.LineWrap = true;
else
Output.LineWrap = false;
}
// Return the results.
return Output;
}
[/code]

This is one of the few situations in the chapter where you need to send a number of pieces of information back to IronPython. The ConsoleData structure contains an entry of each piece of information that the GetConsoleInfo() provides. An IronPython application can set the output of the call to a variable and then use the variable content to determine precisely how the console is configured.

The GetConsoleInfo() method is a little more complicated than the other calls in the extension. This method relies on the GetConsoleMode() function to obtain console information. However, notice that the method calls the GetConsoleMode() function twice, once with the input handle and again with the output handle. This method demonstrates how the use of the wrong handle could cause problems because the output from the GetConsoleMode() function differs with the handle you provide as input.

The return value from the GetConsoleMode() function is a series of flags. Notice how the code uses if statements to determine whether each flag is set. When a flag is set, the feature is enabled and the code sets that value in the ConsoleData data structure, Output, to true. The method ends by returning the fully completed ConsoleData data structure to the caller.

Writing an IronPython Application to Use P/Invoke

If you’ve been following along with the example, you know it’s finally time to use the ConMode class with IronPython. It’s now possible to determine the display mode, the size of the console window, and the capabilities it provides. Listing 16-9 shows the code used for testing this extension.

Listin g 16-9: Testing the Win32 API extension

[code]
# Import the Common Language Runtime.
import clr
# Access the extension.
clr.AddReferenceToFile(‘Win32API.DLL’)
import Win32API
# Create an instance of the class.
TestWin32 = Win32API.ConMode()
# Check the display mode.
print ‘The display mode is: ‘,
print TestWin32.GetCurrentDisplayMode()
# Obtain the largest possible window size.
print ‘nThe largest possible window size is: ‘
Size = TestWin32.GetConsoleWindowSize()
print ‘tColumns: ‘, Size.X
print ‘tRows: ‘, Size.Y
# Display the console characteristics.
print ‘nThe console has these characteristics:’
Chars = TestWin32.GetConsoleInfo()
print ‘tEcho Enabled: ‘, Chars.Echo
print ‘tLine Input Enabled: ‘, Chars.LineInput
print ‘tMouse Input Enabled: ‘, Chars.MouseInput
print ‘tProcessed Input Enabled: ‘, Chars.ProcessedInput
print ‘tWindow Input Enabled: ‘, Chars.WindowInput
print ‘tConsole Can Produce Processed Output:’, Chars.ProcessedOutput
print ‘tConsole Uses Line Wrap: ‘, Chars.LineWrap
# Pause after the debug session.
raw_input(‘nPress any key to continue…’)
[/code]

The code begins by importing CLR support. It then creates a reference to Win32API.DLL and imports the Win32API namespace into the IronPython environment. The next step is to create an instance of the Win32API.ConMode class, TestWin32.

At this point, the code begins checking each console feature in turn, beginning with the console display mode, which doesn’t require any additional processing. The GetConsoleWindowSize() method call requires that the code display the Size.X (columns) and Size.Y (rows) values separately.

The GetConsoleInfo() method call comes next. This particular call requires a little more processing because it returns more information. The output of the call appears in Chars as a ConsoleData data structure. As you can see, the code simply displays the true or false value of each of the data structure members. Figure 16-17 shows the output from this example.

One of the most important issues when making Win32 API calls from IronPython is to ensure that the C# extension processes the data in an easy-to-use manner. In addition, you should provide a consistent method for returning the data from the C# extension to IronPython, such as using data structures (as shown in the example).

 

 

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.