2. Implement the Game
In this section we start with the default game loop code generated when we created the HelloXNA
project to create a simple space ship shooter game. Our game will be a
simple top-down shooter where alien space ships attempt to strike the
hero ship. The hero ship can shoot missiles at the alien ships to cause
them to explode. Score is based on the number of alien ships blown up.
Now that we have an understanding of the game play,
let's get started. We alternate between coding and background detail as
we develop the game to explain key concepts. In the Game1.cs code file
we declare four variables of type Texture2D named HeroShip, SpaceShip, Missile, and BackgroundImage that are initialized in the LoadContent method:
HeroShip = this.Content.Load<Texture2D>("Sprites/heroship");
SpaceShip = this.Content.Load<Texture2D>("Sprites/spaceship");
Missile = this.Content.Load<Texture2D>("Sprites/missile");
BackgroundImage = this.Content.Load<Texture2D>("Textures/background");
Since these assets are loaded by a ContentManager object, we do not need to worry about releasing the objects in the UnloadContent method. The ContentManager will take care of that for us when it goes out of scope.
Now that we loaded the content, we next cover modifications to the Update() and Draw() methods. We cover Draw first because we want to draw the background on the screen. To actually do the drawing to the graphics device you use a SpriteBatch object.
By default, the XNA Game Studio project template creates a member named spritePatch of type SpriteBatch. Within the Draw method, you first call spriteBatch.Begin, then spriteBatch.Draw any number of times, and then spriteBatch.End.It
is more efficient for the graphics hardware to draw to the back buffer
as a batch and then draw to the back buffer to the device as opposed to
drawing items one at a time directly to the graphics device.SpriteBatch.Begin method sets up the back buffer and the call to SpriteBatch.End indicates that the batch is ready to be drawn to the device.
spriteBatch.Begin();
spriteBatch.Draw(BackgroundImage,graphics.GraphicsDevice.Viewport.Bounds, Color.White);
spriteBatch.End();
Figure 3 shows the background image in the Emulator.
The background is static for our simple game so we are finished with it. We covered Begin and End for the SpriteBatch class but not the Draw method, which has several overloads that look intimidating at first, taking rectangle objects, rotation, scale, etc.
All overloads of the Draw method have a couple of parameters in common in that all of the overloads take a Texture2D(the image to draw) and a position to draw it either in the form of a Vector2 or a Rectangle object. For the background image shown in Figure 3, we draw with this overload:
spriteBatch.Draw(BackgroundImage,graphics.GraphicsDevice.Viewport.Bounds, Color.White);
BackgroundImage is the Texture2D to be drawn. For the place to draw the texture, we pass in the Rectangle size of the entire screen, which is defined by graphics.GraphicsDevice.Viewport.Bounds.
We could hard-code the values to be the shipping screen resolution of
800 × 480. However, we do not want to hardcode this value, because at
some point in the future an additional screen resolution will be
introduced that is 480 × 320. The last parameter in this Draw method overload allows you to tint Texture2D in a particular color. Color.White represents no tint at all.
Now let's shift over to the Update method.
It is in the Update method where calculations are performed to
determine where the alien ship, space ship, and missile should be
drawn. In addition to the variable representing the sprite image, we
also need variables to track the position and speed of the objects as
well. First let's have a quick discussion on vectors.
When Windows Phone 7 initially hits the market, the
screen resolution is 800 pixels in height and 480 pixels in width. In
XNA Framework, the 2D coordinate system puts 0,0 in the upper left hand
corner of the screen with the positive X direction flowing to the right
and positive Y direction flowing down the screen, as shown in Figure 4.
A Vector2 consists of an X and Y component that represents a direction and magnitude from the origin. As an example, since the screen is 480 pixels wide, the XVector2 would have zero for the Y component, and 480 for the X component like this (480,0). Likewise, a Vector2 that represents the Y axis would have zero for the X component and 800 for the Y component like this (0,800). axis represented as a
A Vector2 can represent either a position or a speed. As a position, you set the X and Y
component to be the values you want to place an object on the Cartesian
plane. As an example, to place the upper left-hand corner of a sprite
or image in the middle of the screen, you would set the sprites
position to this value (240,400), which is half the screen width and
height respectively.
When you position a sprite, it is always relative to
the upper left-hand corner of the sprite, which is considered (0,0) in
terms of height and width for the sprite. If you don't take this into
account and think you are positioning the center of a sprite, it will
be off by half the height of the sprite toward the bottom and half the
width of the sprite to the right.
|
|
Position is pretty straightforward in a 2D plane. Representing Speed as a Vector2 is similar except that the Vector2
is interpreted by its magnitude or length and direction. Imagine an
arrow with its tail at position (0,0) and its head at (X,Y) such as
(10,10) on a 2D plane, as illustrated in Figure 5.
If the imaginary Vector2 (0,10) represents a speed, it means that the object is moving straight down at 10 pixels per frame. If the Vector2 (10,10) represents a speed, it means that for each frame, the object is moving 10 pixels to the right and 10 pixels down. Figure 6 depicts a ball that has a position of (300, 160). A speed Vectors2 (50,50) is applied to the ball in a given frame.
Applying a speed of (50,50) redirects the ball to a
southeastern direction in the plane, giving a new position of (350,
210) for the ball. We know this intuitively because X is positive to
the right and Y is positive in the down position, so the Vector2
(50,50) moves the ball 50 pixels to the right and 50 pixels down. Note
that we could just as easily have a speed of (-50, 50), which would
intuitively move the ball to the southwest direction.
Now that you have a basic understanding of position
and speed as Vector2 objects, let's go back to the XNA Game Studio
project. We will perform speed and position calculations in the Update
method. We also need to check to see if an object has either collided
with another object/sprite, or if the object collided with an edge of
the screen.
If it an object collides with an object we can do
several things. If the object is a golf club and the other object
represents a golf ball, a developer would want to simulate the club
connecting with the ball and send it flying in the opposite direction
based on the ball and club Vector2 position, the angle of impact.
Another example action upon a collision is that if the collision is
between an alien space ship and a missile fired from the player's space
ship, the game could simulate an explosion and the alien space ship
disappears from the screen.
Speaking of the player space ship, the other key component handed in the Update
method is user input, which is also calculated using Vector2 objects.
For Windows Phone 7 user input can be in the form of a screen tap, a
gesture, or the accelerometer as the major game input methods.
With the background on the screen coordinate system
and Vector2D objects out of the way, you should have a pretty good idea
of what the code will look like in the Update method in pseudo code:
Handle User Input
Apply User Input to objects
Update Position of objects
Detect Object and Edge Collisions
Determine the final state and position of all objects
A key point to consider is that if the Update method takes too long the Game loop will decide to skip a call to the Draw method, resulting in dropped frames. Keep this in mind when writing code in the Update method.
Armed with an understanding of what we need to do in the Update
method in general, let's dive into coding the game logic. We need to
add a couple of member variables to represent the position and speed
for our game objects, which include the hero ship, the enemy space
ship, and the missile. We need to track each object's position, and
speed so we declare six additional variables:
//Define Speed and Position vectors for objects that move
Vector2 HeroShipPosition;
Vector2 HeroShipSpeed;
Vector2 SpaceShipPosition;
Vector2 SpaceShipSpeed;
Vector2 MissilePosition;
Vector2 MissileSpeed;
We initialize these values in a separate method named InitializeObjects executed in LoadContent just after we load the related sprite images.
private void InitializeObjects()
{
//Initialize Positon and Speed
SpaceShipPosition = new Vector2(
graphics.GraphicsDevice.Viewport.Width / 2 - SpaceShip.Width / 2, -SpaceShip.Height);
SpaceShipSpeed = new Vector2(0, 2); // 2 pixels / frame "down"
//Center hero ship width wise along the X axis
//Place hero ship with 20 pixels underneath it in the Y axis
HeroShipPosition = new Vector2(
graphics.GraphicsDevice.Viewport.Width / 2 - HeroShip.Width / 2,
graphics.GraphicsDevice.Viewport.Height - HeroShip.Height - 20f);
HeroShipSpeed = Vector2.Zero;
//Center Missile on Space Ship and put it 50 pixels further down
//off screen "below" hereoship
MissilePosition = HeroShipPosition +
new Vector2(HeroShip.Width / 2 - Missile.Width / 2, HeroShip.Height + 20f);
MissileSpeed = new Vector2(0, −6); // 6 pixels / frame "up"
}
I mentioned this earlier but it is worth restating:
when you draw a sprite, the position provided to the Draw method is the
origin of the sprite when drawn meaning that the provided position
becomes the upper left-hand corner of the sprite. In order to draw the
alien spaceship and hero spaceship centered width-wise on the screen,
we subtract half of the sprite's width from the position so that the
middle of the sprite is centered on the screen.
I already covered how to draw in the Draw method,
and we are already drawing the background, which was easy to position
since it fills the entire screen. We haven't written the code to draw
the hero ship, the alien spaceship, or the missile. Since we now have
position information for these objects to reference in the form of the
initialized variables, let's update the Draw method. We add these three lines of code to the Draw method right after we draw the background image:
spriteBatch.Draw(SpaceShip, SpaceShipPosition, Color.White);
spriteBatch.Draw(Missile, MissilePosition, Color.White);
spriteBatch.Draw(HeroShip, HeroShipPosition, Color.White);
Figure 7 shows the results.
We are pretty much down with the Draw method for our simple game and will focus on the Update method to make the game interactive. The first thing we do is add the basic update formula for all three objects to the Update method:
HeroShipPosition += HeroShipSpeed;
SpaceShipPosition += SpaceShipSpeed;
MissilePosition += MissileSpeed;
If you run the game right now, you will see the
enemy spaceship move down the screen and the missile move up the screen
at a slightly faster rate. Figure 8 shows a snapshot of the movement.
While pretty cool, it doesn't do much. The sprites
pass through each other and then fly off to infinity in the up
direction for the missile and the down direction for the alien
spaceship. We can make a couple of modifications to make it somewhat
more interesting as well as explain a couple of concepts.
The first modification that we make is edge detection. In the Update Method we type CheckScreenBoundaryCollision(gameTime); and then right-click on it and select Generate => Method Stub.
In the newly generated method, we check to see if
the missile, which has a default speed of −4 pixels / frame in the Y
direction, has flown off the top of the screen, which would be a
negative Y value. Since the alien space ship drops straight down at 2
pixels per frame, we check to see if it has a Y value greater than the
screen height. Here is the method:
private void CheckScreenBoundaryCollision(GameTime gameTime)
{
//Reset Missile if off the screen
if (MissilePosition.Y < 0)
{
MissilePosition.Y = graphics.GraphicsDevice.Viewport.Height -
HeroShip.Height;
}
//Reset enemy spaceship if off the screen
//to random drop point
if (SpaceShipPosition.Y >
graphics.GraphicsDevice.Viewport.Height)
{
SpaceShipPosition.Y = -2*SpaceShip.Height;
}
}
Notice that we avoid using actual number values. As an example, we could use 480 instead of graphics.GraphicsDevice.Viewport.Height,
but when the second screen resolution becomes available the game will
break, because screen height in landscape mode will be 320.
The second modification to the game is to detect
collisions between the alien spaceship and the missile. We create a new
method, CheckForCollisions(gameTime); as before and edit the
generated stub. There are many different algorithms available to detect
for collisions with different levels of accuracy. The most accurate
method is to compare for equality each point of the first image with
every possible point in the other image. While most accurate, it is
also the most CPU intensive and time consuming.
A simple method to check for collisions is to use
bounding boxes. The idea is to wrap an object in a rectangle and check
for collisions. The rectangle is based on the maximum length and width
of the object. This can be inaccurate for irregular shapes, but costs
much less in terms of CPU and time. In our simple game we generate two Rectangle objects that wrap the alien spaceship and the missile and then call the Intersects method to detect a collision. Here is the code:
private void CheckForCollisions(GameTime gameTime)
{
//Alien and Missile
Rectangle AlienRec = new Rectangle((int)SpaceShipPosition.X,
(int)SpaceShipPosition.Y,SpaceShip.Width, SpaceShip.Height);
Rectangle MissileRec = new Rectangle((int)MissilePosition.X,
(int)MissilePosition.Y,Missile.Width, Missile.Height);
if (AlienRec.Intersects(MissileRec))
{
SpaceShipPosition.Y = -2*SpaceShip.Height;
MissilePosition.Y = graphics.GraphicsDevice.Viewport.Height - HeroShip.Height;
}
}
We create the two Rectangle objects and then check for collision by calling if (AlienRec.Intersects(MissileRec)) and then update position similar to when there is a screen edge collision.
We now have an application that shows a dropping
alien spaceship intersected by a missile over and over again. While not
the most functional game, specifically because it doesn't incorporate
user input or interactivity at all, it allows us to demonstrate key
concepts for XNA Game Studio without inundating you with new concepts. Listing 2 shows the full code for our incomplete game.
Example 2. Modified Game1.cs
using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input.Touch; using Microsoft.Xna.Framework.Media;
namespace HelloXNA { /// <summary> /// This is the main type for your game /// </summary> public class Game1 : Microsoft.Xna.Framework.Game
{ GraphicsDeviceManager graphics; SpriteBatch spriteBatch;
//Define Texture2D objects to hold game content Texture2D HeroShip; Texture2D SpaceShip; Texture2D BackgroundImage; Texture2D Missile;
//Define Speed and Position vectors for objects that move Vector2 HeroShipPosition; Vector2 HeroShipSpeed; Vector2 SpaceShipPosition; Vector2 SpaceShipSpeed; Vector2 MissilePosition; Vector2 MissileSpeed;
public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content";
// Frame rate is 30 fps by default for Windows Phone. TargetElapsedTime = TimeSpan.FromTicks(333333);
}
/// <summary> /// Allows the game to perform any initialization it needs to before starting to run. /// This is where it can query for any required services and load any non-graphic /// related content. Calling base.Initialize will enumerate through any components /// and initialize them as well. /// </summary> protected override void Initialize() { // TODO: Add your initialization logic here
base.Initialize(); }
/// <summary> /// LoadContent will be called once per game and is the place to load /// all of your content. /// </summary> protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice);
HeroShip = this.Content.Load<Texture2D>("Sprites/heroship"); SpaceShip = this.Content.Load<Texture2D>("Sprites/spaceship"); Missile = this.Content.Load<Texture2D>("Sprites/missile"); BackgroundImage = this.Content.Load<Texture2D>("Textures/background");
InitializeObjects(); }
private void InitializeObjects() { //Initialize Positon and Speed SpaceShipPosition = new Vector2( graphics.GraphicsDevice.Viewport.Width / 2 - SpaceShip.Width / 2, -SpaceShip.Height); SpaceShipSpeed = new Vector2(0, 2); // 2 pixels / frame "down"
//Center hero ship width wise along the X axis //Place hero ship with 20 pixels underneath it in the Y axis HeroShipPosition = new Vector2( graphics.GraphicsDevice.Viewport.Width / 2 - HeroShip.Width / 2, graphics.GraphicsDevice.Viewport.Height - HeroShip.Height - 20f); HeroShipSpeed = Vector2.Zero;
//Center Missile on Space Ship and put it 50 pixels further down //off screen "below" hereoship MissilePosition = HeroShipPosition + new Vector2(HeroShip.Width / 2 - Missile.Width / 2, HeroShip.Height + 20f); MissileSpeed = new Vector2(0, −6); // 6 pixels / frame "up"
}
/// <summary> /// UnloadContent will be called once per game and is the place to unload /// all content. /// </summary> protected override void UnloadContent() { // TODO: Unload any non ContentManager content here }
/// <summary> /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit();
// TODO: Add your update logic here CheckScreenBoundaryCollision(gameTime); CheckForCollisions(gameTime);
HeroShipPosition += HeroShipSpeed; SpaceShipPosition += SpaceShipSpeed; MissilePosition += MissileSpeed;
base.Update(gameTime); }
private void CheckForCollisions(GameTime gameTime) { //Alien and Missile Rectangle AlienRec = new Rectangle((int)SpaceShipPosition.X, (int)SpaceShipPosition.Y, SpaceShip.Width, SpaceShip.Height); Rectangle MissileRec = new Rectangle((int)MissilePosition.X, (int)MissilePosition.Y, Missile.Width, Missile.Height);
if (AlienRec.Intersects(MissileRec)) { SpaceShipPosition.Y = −2 * SpaceShip.Height; MissilePosition.Y = graphics.GraphicsDevice.Viewport.Height - HeroShip.Height; } }
private void CheckScreenBoundaryCollision(GameTime gameTime) { //Reset Missile if off the screen if (MissilePosition.Y < 0) { MissilePosition.Y = graphics.GraphicsDevice.Viewport.Height - HeroShip.Height; }
//Reset enemy spaceship if off the screen //to random drop point if (SpaceShipPosition.Y > graphics.GraphicsDevice.Viewport.Height) { SpaceShipPosition.Y = −2 * SpaceShip.Height; } }
/// <summary> /// This is called when the game should draw itself. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue);
// TODO: Add your drawing code here spriteBatch.Begin(); spriteBatch.Draw(BackgroundImage, graphics.GraphicsDevice.Viewport.Bounds, Color.White); spriteBatch.Draw(SpaceShip, SpaceShipPosition, Color.White); spriteBatch.Draw(Missile, MissilePosition, Color.White); spriteBatch.Draw(HeroShip, HeroShipPosition, Color.White); spriteBatch.End();
base.Draw(gameTime); } } }
|