In the previous chapters, you have learned a lot about the various aspects of developing games using XNA Game Studio. You have also learned about the content pipeline, which is a critical component of the XNA platform. Until now, all of the code examples have been for the PC. In this chapter, that changes. Now we will build games specifically for the Zune device.
If you don't have a Zune yet, don't worry! You will also learn how to create copies of your Zune project for the PC and arrange the project so that it compiles differently based on the target platform.
First, you need to understand the work flow of Zune game development. Because there is currently no emulator for the Zune device, you will need to get used to waiting a little longer each time you run or debug the application on the device. Other caveats exist, as you'll learn in this chapter.
In the course of mastering the work flow of development for the Zune, you will build a small test "game" that operates on both the Zune and the PC. At the end of the chapter, using those concepts, you will build a more complicated game called OutBreak, which will be your first Zune project worthy of being called a game!
The deployment process is very straightforward and simple. To deploy a Zune game project to the Zune device, the following conditions must be met:
Before continuing with this chapter, make sure you have set up your development environment, which includes Visual Studio 2008 or Visual C# 2008 Express or later and XNA Game Studio 3.0 or later. Chapter 1 covers downloading and installing Visual Studio and XNA Game Studio.
Note You can download the latest version of XNA Game Studio from the XNA Creators Club web site (http://creators.xna.com). If you have a previous version of XNA Game Studio installed, including any community technology preview (CTP) versions, you must first uninstall those products before installing the new one.
The Zune firmware update process is done through the Zune client software, available at http://www.zune.net. At the time of this writing, the appropriate configuration is Zune firmware version 3.0 and XNA Game Studio 3.0.
Caution XNA Game Studio 3.0 supports development on 64-bit platforms for the Zune. If you are running a 64-bit operating system, you must download the 64-bit version of the Zune software to install the proper drivers for the device.
To verify the Zune's firmware version and to update it if necessary, plug in your Zune device after installing the Zune client software and click the Settings button in the upper-right corner of the screen, as shown in Figure 4-1.
Figure 4-1. Click the Settings button on the Zune screen to check your Zune's firmware version.
From the Settings screen, click the Device tab. On the left side of that tab, click Device Update. If your firmware is up-to-date, you will see a screen similar to Figure 4-2. If your device firmware is not up-to-date, you will see an Update button, which you can click to start the firmware update process. When this process completes, your Zune will be ready for game development.
Figure 4-2. The Device Update screen shows the status of your Zune's firmware.
Caution If you are using multiple Zunes, ensure that each of them has the latest firmware installed before attempting to use them in the development process.
To allow Visual Studio to deploy to your Zune, all you need to do is launch XNA Game Studio Device Center and add the device. This process is identical to that of adding an Xbox 360 console to Device Center.
XNA Game Studio Device Center is a utility that allows you to manage the devices to which you want to deploy. Zune development with XNA, unlike Xbox 360, does not require a Creators Club membership, so the procedure is straightforward.
To register your device with Device Center, follow these steps:
Figure 4-3. XNA Game Studio Device Center
Tip You can connect multiple Zunes to different USB ports on your computer and add them to XNA Game Studio Device Center.
Figure 4-4. The XNA Game Studio Devices dialog box
Figure 4-5. The Select Your Zune dialog box when two Zunes are connected to the computer
Figure 4-6. Testing connectivity between Visual Studio and the Zune
Figure 4-7. The Zune device has been added successfully.
Figure 4-8. XNA Game Studio Device Center with two Zunes added and one set as the default, which is where games will be deployed
At this time, your Zune is registered with XNA Game Studio Device Center. When you plug it in next time, it will be available for you to use; you don't need to repeat this process.
If you later want to remove a Zune device from Device Center, all you need to do is right-click the device in Device Center and select Remove, as shown Figure 4-9. After confirmation, the Zune will be unregistered. You will need to add it again later if you wish to use it again.
Figure 4-9. Removing a Zune from Device Center
Tip A great feature that has been added in XNA Game Studio 3.0 is the ability to take a screen capture from a Zune that is running a game. While the game is running, simply right-click the Zune in XNA Game Studio Device Center and click Take Screen Capture. The result will be a PNG file at 240 by 320 pixels. See Figure 4-12, at the end of the following exercise, for an example.
What's a programming book without the requisite Hello World example? In this short exercise, you will create a Zune game that displays some text on the screen. You will confirm that your configuration is ready to rock, and you will also see how to deploy a game to the Zune.
HelloZune
as the project name, as shown in Figure 4-10. Click OK.Figure 4-10. Creating the HelloZune project
Content
node in the Solution Explorer and choose Add New Item. Select the Sprite Font template and name it Tahoma
(or another font name that you know exists on your computer), as shown in Figure 4-11. You don't need to include the .spritefont
extension; it will be added automatically. Click Add.Figure 4-11. Adding a sprite font
Game1.cs
. Immediately after the declaration of the spriteBatch
variable and before the Game1
constructor, declare a SpriteFont
variable to reference the font you just added, as follows:
SpriteFont tahomaFont;
LoadContent
method, replace the // TODO:
line with the code to load the font from the content pipeline:
tahomaFont = Content.Load<SpriteFont>("Tahoma");
Draw
method, replace the // TODO:
line with the code to draw some text on the screen using the default sprite batch. This code draws the text "Hello Zune" in black Tahoma at point (0, 0) (Vector2.Zero
):
spriteBatch.Begin();
spriteBatch.DrawString(tahomaFont, "Hello Zune", Vector2.Zero, Color.Black);
spriteBatch.End();
Figure 4-12. The Hello Zune game in action
This simple "game" has shown you the basics of creating and deploying a very basic XNA game to your Zune device. Later in this chapter, in the "Your First Real Zune Game: OutBreak" section, you will create a slightly more complicated game with movement, game logic, input, and more.
What is the practical difference between running with and without debugging, and why does the Zune reboot itself after your game exits?
The Zune reboot behavior is by design, and exists mainly to clean up resources and reload the firmware so that no leaks or possible exploits exist. You may notice that the games that come with the Zune 3.0 firmware do not reboot after exiting them. This is because they are signed by Microsoft and trusted by the firmware. There is currently no way to reproduce this behavior when creating your own game; the Zune will always reboot itself when your game exits.
Of course, a constantly rebooting Zune device can be a hindrance to the game-development process, when you are frequently testing code. Thankfully, when you run with debugging, the Zune will exit gracefully to the XNA Game Studio Connect screen, without rebooting fully.
As you've seen, when you are ready to deploy and test a game, or you can run it with debugging simply by pressing F5. You can also click the Debug icon in Visual Studio (it looks like a play icon).
For most purposes, running with debugging is preferred, because it is considerably more practical and provides more value to you during the development stage. Running with debugging has the following benefits:
The drawbacks to running with debugging are as follows:
Depending on your needs at the time, you may prefer to run without the overhead of the debugger. Running without debugging has the advantage of providing truer, more consistent performance. Because there is no debugger overhead, you will have the maximum possible headroom available to run the game.
Running without debugging has the following drawbacks:
Tip You can prevent the Zune software from launching when a device is initially plugged in by selecting Settings Device Sync Options in the Zune software and clearing the check box titled "Start the Zune software when I connect a device."
Suppose that you're developing a Zune game and make a minor change that doesn't really warrant a full deployment to the Zune—you know it will work, and you don't want to wait on the deployment process. For example, perhaps you want to test some new colors or change some text. You might wish there was a parallel Windows version of this game that you could use just to check that the game still runs after these minor changes. Thankfully, XNA Game Studio provides an automated way to do this.
The Create a Copy option, available from the project's context menu in the Solution Explorer, creates a new project file that references the same source code and content as the original project. This way, any changes you make are reflected in the output of either project. The next exercise demonstrates how to create a copy of a Zune game for Windows, using the Hello Zune game we created in Exercise 4-1.
Tip The Create a Copy option can be used to create copies of your game to and from PC, Xbox, and Zune projects. This means that if you start with a Windows project, you can create a Zune copy of it later this way.
EXERCISE 4-2. A WINDOWS COPY OF HELLO ZUNE
Creating a Windows copy of a Zune game is easy. In this example, we'll use the HelloZune
project we created in Exercise 4-1.
HelloZune
project .Figure 4-13. Creating a copy of the project for Windows
Windows Copy of HelloZune
, as shown in Figure 4-14. Open Game1.cs
, and you will see that it contains the code we wrote in Exercise 4-1; this is the exact same file.Figure 4-14. The Zune project and the Windows copy in the Solution Explorer
Windows Copy of HelloZune
project in the Solution Explorer and select Set as Startup Project.Figure 4-15. Hello Zune running on the PC
Now that we have some simple elements of the game development work flow established, it's time to learn more about this very cool device and how to create games that perform well on it.
Current iterations of the Zune, both flash-based and hard-drive-based, share the following system specifications:
The two major areas of note in this list are the processor speed and RAM. While these specs appear to be limiting at first glance, the computing capability of the Zune is actually quite powerful compared to similar devices on the market at this time. The XNA Framework is incredibly efficient, although there is an inaccurate perception that managed code runs noticeably slower than unmanaged code.
Although the XNA Framework is great at optimizing and running complicated code, you should still make it part of your active mindset to write "performant" code (a term borrowed from a .NET architect friend of mine named Glen Jones).
Realistically, it is safe and perfectly fine to use things like generic collections and foreach
loops, although these elements of programming have much faster alternatives. For example, working with arrays will always be faster than working with generic collections. Usually, for
loops are faster than foreach
loops.
Writing performant code is about balance. Don't be afraid to harness the power of the Zune; too much consideration for performance can end up costing you a lot of time. I suggest that you start with what you are comfortable coding. If a generic collection suits your needs, use one. Later, during the optimization stages of development, take a scalpel to your control flow and data types to determine where you are taking the biggest performance hit, and work backward in layers of abstraction.
Be careful not to pass enormous constant value types around all the time. I had one experience in mobile application development where the previous team had megabytes worth of constant values being passed around in nearly every function call. This would bring the application to its knees just a few layers into the call stack.
In general, you should write modular, cohesive code; use libraries; and adopt smart design patterns, which is a topic for another book. Optimization is important but not crucial in the opening phases of game development, so don't worry about it until it becomes a problem (especially since you are developing for the exact same hardware per user).
Here are a few pointers for improving performance on the Zune from the get-go:
Dereference frequently used variables: Let's say you are frequently accessing a game object's property in a block of code; for example,
battleship.Guns[0].AmmunitionCount
. You can dereference this easily into an integer like this:
int firstGunAmmo = battleship.Guns[0].AmmunitionCount;
Dereference this variable at the beginning of the code block, and use
firstGunAmmo
for the remaining calculations, instead of repeating the longer line of code. This makes your code more readable, and it also means that the property is retrieved only once. This reduces the number of calculations.Use textures whose width and height are a power of 2: Most professional games use textures that have dimensions that are powers of 2, such as 4 × 4, 16 × 16, 32 × 16, and so on. Textures not fitting this format will still be drawn, of course, but it is computationally more expensive to rotate them due to anti-aliasing.
Be smart about content loading: Loading content on the Zune is one of the most computationally expensive operations you can perform. Loading all the content up-front, at the game's start, will eliminate the stalls and freezes associated with on-demand content loading. However, when done correctly, on-demand content loading can actually work in your favor. For example, if you have a level-based game where each level requires a substantial quantity of asset data to be loaded, adding the new assets between levels (and showing a loading screen at that time) could improve your game's overall performance, since assets will not be loaded until they are needed.
Input handling on the Zune device can be slightly confusing. The part of the XNA Framework that allows you to access the state of the buttons and touchpad is the same that you would use for an Xbox 360 controller; there is currently no specific Zune input state class. The Zune controls are referenced in code by their analogs on the Xbox 360 controller.
Note On first-generation Zunes, there is no touch-sensitive pad. There is only a click wheel that works like a directional pad. This means that you should always support directional pad input, and support touch sensitivity as an added feature that can be disabled or enabled (if your game calls for it).
The Zune pad is a large, touch-sensitive area on the Zune. It operates exactly like a thumbstick on the Xbox 360 controller.
First, you should be aware of a couple of interesting caveats to the Zune pad's use that can cause frustration:
In code, the Zune pad is accessed like the Xbox 360's left thumbstick, and it outputs a 2D vector. The magnitude of this vector is always between zero and one, and the origin of the vector is in the center of the touchpad. Think of it as a simple 2D Cartesian coordinate plane, as illustrated in Figure 4-16.
In Figure 4-16, you see that the center of the Zune pad is at (0, 0). The x and y values can run anywhere from −1 to 1. The arrow shown in the figure is a vector representative of a touch at the absolute upper-right corner of the Zune pad, which translates in code to a Vector2
object equivalent to <1, 1>
.
The value sent by the Zune pad changes based on where you touch it. Because the component values of a Vector2
object are of type float
, the vector returned can look something like <-0.084, .587>
. Because these values are always between −1 and 1, you can use the output of the Zune pad as ratios to modify other values using multiplication.
Figure 4-16. How the Zune pad input control works
For example, in the next exercise, we will add the following code to Game1.cs
:
Texture2D characterTex;
SpriteFont normalFont;
Vector2 characterPosition = new Vector2(120, 160);
string displayText = "";
Vector2 displayTextPosition = new Vector2(5, 270);
const float sensitivity = 3.0f;
The characterPosition
value will be updated in the Update
method, based on the value of the Zune pad, and used later in the Draw
method. It is initialized to a hard-coded Vector2
representing the center of the screen. Remember that the screen is 240 pixels wide by 320 pixels high. These values are simply those values divided by two.
The sensitivity
variable is used to determine how far the character will move per unit on the Zune pad. You can modify this value to see the results. Increasing it causes the character to move more, while decreasing it causes the character to move less.
The Update
method will include code to use the Zune pad input, as follows:
GamePadState inputState = GamePad.GetState(PlayerIndex.One);
Vector2 zunePadValue = inputState.ThumbSticks.Left;
zunePadValue.Y = -zunePadValue.Y;
characterPosition = characterPosition + (sensitivity * zunePadValue);
displayText = "Zune Pad:
" + zunePadValue.ToString();
This code first gets the state of the Zune inputs using GamePad.GetState
. (Note that, technically, only one player is active on the Zune at a time, so you should always use PlayerIndex.One
). The value of the left thumbstick is then retrieved and stored in the zunePadValue
variable. Remember that the Zune pad state is retrieved using the left thumbstick value, because that is how it is mapped in the framework.
Notice the line that negates the Y
component of the zunePadValue
variable. The XNA coordinate system has y increasing as it goes downward, whereas a positive y value from the Zune pad indicates that it was pressed upward. Failing to negate this value gives an "inverted y axis" kind of feel, and is less intuitive in 2D games than it might be in 3D games.
The code then updates the character position variable. It uses the current character position as a base, and adds to that the vector returned by the Zune pad state multiplied by the sensitivity
variable. Thus, if the character is currently at (0, 0) and the Zune pad is pressed at (0.5, 0.5), the new value of the character position will be (0, 0) + 2.0(0.5, −0.5) = (0, 0) + (1, −1) = (1, −1). This works because the user is touching the Zune pad above and to the right of center. Intuitively, the user would expect the on-screen object to move up and to the right. You can see that this calculation fulfills that assumption by moving the character from (0, 0) to (1, −1). Remember that in XNA, when the y coordinate is negative, that means up, not down.
In the next exercise, you will gain some practical insight into how this works.
EXERCISE 4-3. ZUNE PAD INPUT HANDLING
In this example, you will see how the Zune pad can be used to control on-screen elements in a smooth, responsive manner. You will be modifying a position value and drawing a texture at that position. You can follow along with the source code provided in Chapter 4 /Exercise 3/ZunePadExample
. We'll be picking up the pace a bit in this exercise, because I assume you have followed the previous exercises and now have experience creating simple Zune Game projects in Visual Studio.
ZunePadExample
.Content
project, add the asset character.png
from the Chapter 4 /Exercise 3/Artwork
folder.Normal
to the Content
project. This is what we will use to view the value returned by the Zune pad. The default font face for XNA games is called Kootenay, which is a royalty-free font included with XNA Game Studio.Game1.cs
. Above the constructor, where the private variables are declared, add the following lines of code (which were discussed in the text preceding this exercise):
Texture2D characterTex;
SpriteFont normalFont;
Vector2 characterPosition = new Vector2(120, 160);
string displayText = "";
Vector2 displayTextPosition = new Vector2(5, 270);
const float sensitivity = 3.0f;
LoadContent
method and add the following two lines in place of the TODO
comment to initialize the game content:
characterTex = Content.Load<Texture2D>("character");
normalFont = Content.Load<SpriteFont>("Normal");
Update
method and add the following lines that access the Zune pad, in place of the TODO
comment (this code was also discussed in the text before this exercise):
GamePadState inputState = GamePad.GetState(PlayerIndex.One);
Vector2 zunePadValue = inputState.ThumbSticks.Left;
zunePadValue.Y = -zunePadValue.Y;
characterPosition = characterPosition + (sensitivity * zunePadValue);
displayText = "Zune Pad:
" + zunePadValue.ToString();
Draw
method and add the following lines that draw the character and the text on the screen, again, in place of the TODO
comment:
spriteBatch.Begin();
spriteBatch.Draw(characterTex, characterPosition, Color.White);
spriteBatch.DrawString(normalFont, displayText, displayTextPosition,
Color.Black);
spriteBatch.End();
Figure 4-17. The Zune pad example in action
As you play around with your game, observe that the closer to the center of the pad you touch, the slower the character moves around the screen. The closer to the edge you press, the faster it moves. Be careful not to lose your character someplace off-screen, because there are no constraints in place to prevent it from going outside the play area. (Note that you could implement constraints easily by checking the position in an if
statement before updating the character position in the Update
method.)
Older Zunes do not have a touchpad. They just have a four-directional click wheel with a center button. Newer Zunes with the touchpad are clickable as well. It is always a good idea to support the directional click buttons in case the end user doesn't have the touchpad. The games that come with the Zune 3.0 firmware, such as Hexic and Texas Hold 'em, have an option to disable touch. Disabling touch essentially causes the input subsystem to ignore touch and use only the directional click buttons.
The directional click buttons are used like normal controller buttons in XNA. They are accessed in code in the same way as the DPad
collection of buttons for the Xbox 360. The following code snippet shows an example of how the DPad
buttons are used:
GamePadState inputState = GamePad.GetState(PlayerIndex.One);
bool isUpPressed = inputState.DPad.Up == ButtonState.Pressed;
bool isDownPressed = inputState.DPad.Down == ButtonState.Pressed;
bool isLeftPressed = inputState.DPad.Left == ButtonState.Pressed;
bool isRightPressed = inputState.DPad.Right == ButtonState.Pressed;
The four Boolean variables defined here will indicate if the button is currently being pressed.
One thing to watch out for is the problem of multiple presses. Since the Update
method is called many times per second (and this is usually where you handle input), you can't effectively compare the button state to ButtonState.Pressed
as a trigger. Depending on how long the user holds down the button, you might have tens or hundreds of triggers, figuratively shooting far more bullets than you intend. After we take a look at accessing other buttons, we will build an input subsystem that checks for a new button press, rather than simply polling the button state.
Because the Zune is a device with a very small form factor and an extremely limited number of inputs, we've already covered the majority of available input devices (the Zune pad and the directional click buttons). There are only three means of input left, and they are all buttons.
"But wait, I see only two other buttons," you say? Perhaps you forgot that you can actually press down the Zune pad. This is usually associated with the primary input activity for most games, covering actions such as fire, OK, select item, and so on. On the Xbox controller, the primary input (when it's not one of the triggers) is usually the A button, so naturally, pressing the Zune pad down is the same as pressing the A button (which is the same as pressing the middle button on first-generation Zunes).
The button marked with the play/pause icon is mapped to B, and the button marked with a back arrow is mapped to the Back button (which has a similar icon in the Xbox 360 controller).
All of these buttons can be accessed through the GamePadState
's Buttons
collection and compared to the ButtonState
enumeration values to determine if they are being pressed.
Table 4-1 lists each of the available controls on the Zune, how they map to Xbox 360 controls, and how they are used in code in the XNA Framework.
Table 4-1. Zune Control Mapping
Zune Control | Xbox 360 Control | Associated GamePadState Property | Output Type |
Zune pad | Left thumbstick | ThumbSticks.Left |
Vector2 |
Click wheel | Directional pad | DPad |
Group of ButtonStates |
Center button | A | Buttons.A |
ButtonState |
Play/Pause | B | Buttons.B |
ButtonState |
Back | Back | Buttons.Back |
ButtonState |
In this section, you will learn how to separate input handling from your main game logic in a way that both Windows and Zune games can use. This way, you can use the same input class library for Windows and Zune games, without needing to modify code for different versions of the same game.
The input handler class saves you a lot of time because it handles both keyboard and Zune pad input. If you are running the game on Windows, you can use either the keyboard or an attached Xbox 360 controller. If you are running the same code on the Zune, keyboard input will be ignored (since there is no keyboard on the Zune). The extraneous code does not present a problem, since the XNA Framework will safely ignore it, but you can use compiler directives to split out the platform-specific code if it really bothers you.
Another great feature of this input handler class is that it is extensible. You can always extend this class for a new game and add different properties suited to whatever game you are building. Additionally, this class provides support for "new" button presses. Earlier, I mentioned the problem of polling buttons for their pressed state and how that can result in many actions being triggered. Using this library, you can quickly and easily determine if a button was pressed, rather than checking to see if the button is down.
We'll look at two versions of the input handler: one that is specific to the Zune and does not include keyboard handling, and one that does handle keyboard input, so it can support Windows games as well as Zune games.
A Zune-Specific Input Handler
Listing 4-1 shows the Zune-specific version of the input handler, in which the implementation of InputState
does not deal with keyboard input.
Note This code is based on the input handler class included with most of the samples on the XNA Creators Club web site, including the Game State Management sample, which you can find at http://creators.xna.com/en-us/samples/gamestatemanagement.
Listing 4-1. Input State Handling, Centralized and Abstracted, for the Zune
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
namespace InputHandler
{
/// <summary>
/// Helper for reading input from gamepad. This class tracks both
/// the current and previous state of the Zune Pad and provides some
/// properties to abstract specific presses.
/// </summary>
public class InputState
{
#region Fields
public GamePadState CurrentGamePadState;
public GamePadState LastGamePadState;
#endregion
#region Initialization
/// <summary>
/// Constructs a new input state.
/// </summary>
public InputState()
{
CurrentGamePadState = new GamePadState();
LastGamePadState = new GamePadState();
}
#endregion
#region Properties
/// <summary>
/// Checks for a Middle Button press (A by default)
/// </summary>
public bool MiddleButtonPressed
{
get
{
return IsNewButtonPress(Buttons.A);
}
}
/// <summary>
/// Checks for a press of Up (DPadUp by default)
/// </summary>
public bool UpPressed
{
get
{
return IsNewButtonPress(Buttons.DPadUp);
}
}
/// <summary>
/// Checks for a press of Down (DPadDown by default)
/// </summary>
public bool DownPressed
{
get
{
return IsNewButtonPress(Buttons.DPadDown);
}
}
/// <summary>
/// Checks for a press of Right (DPadRight by default)
/// </summary>
public bool RightPressed
{
get
{
return IsNewButtonPress(Buttons.DPadRight);
}
}
/// <summary>
/// Checks for a press of Left (DPadLeft by default)
/// </summary>
public bool LeftPressed
{
get
{
return IsNewButtonPress(Buttons.DPadLeft);
}
}
/// <summary>
/// Checks for a press of the Play button (B by default)
/// </summary>
public bool PlayPressed
{
get
{
return IsNewButtonPress(Buttons.B);
}
}
/// <summary>
/// Checks for a press of the Back button
/// </summary>
public bool BackPressed
{
get
{
return IsNewButtonPress(Buttons.Back);
}
}
#endregion
#region Methods
/// <summary>
/// Reads the latest state of the gamepad.
/// </summary>
public void Update()
{
LastGamePadState = CurrentGamePadState;
CurrentGamePadState = GamePad.GetState(PlayerIndex.One);
}
/// <summary>
/// Checks if a button was newly pressed during this update.
/// </summary>
/// <param name="button">The button to check</param>
/// <returns>True if the button is down; false otherwise</returns>
public bool IsNewButtonPress(Buttons button)
{
return (CurrentGamePadState.IsButtonDown(button) &&
LastGamePadState.IsButtonUp(button));
}
/// <summary>
/// Checks if a button is pressed down.
/// </summary>
/// <param name="button">The button to check</param>
/// <returns>True if the button is down; false otherwise</returns>
public bool IsButtonDown(Buttons button)
{
return CurrentGamePadState.IsButtonDown(button);
}
#endregion
}
}
This input handler has three methods—IsButtonDown, IsNewButtonPress
, and Update
— which make up the most important part of this class. The method IsButtonDown
is not directly used in determining button presses, but it is useful for whatever program may be consuming it.
The magic happens in the IsNewButtonPress
method:
/// <summary>
/// Checks if a button was newly pressed during this update.
/// </summary>
/// <param name="button">The button to check</param>
/// <returns>True if the button is down; false otherwise</returns>
public bool IsNewButtonPress(Buttons button)
{
return (CurrentGamePadState.IsButtonDown(button) &&
LastGamePadState.IsButtonUp(button));
}
The InputState
class maintains two copies of a GamePadState
variable. The first copy, CurrentGamePadState
, stores the state of the Zune's input. The current value stored here is updated every time the Update
method of this class is called. The second, LastGamePadState
, gives access to the buttons' state before the most recent update. By comparing the two game pad states, we can determine whether a button was pressed—that is to say, that the button was previously up and is now down. In the IsNewButtonPress
method, you can see how this logic comes into play. The Boolean expression returned in this method gives a true or false indication of whether the button in question was previously up, but is now down. The IsNewButtonPress
forms the foundation of the C# properties defined further up in the class, which are named according to specific controls. Let's take the MiddleButtonPressed
property as an example:
/// <summary>
/// Checks for a Middle Button press (A by default)
/// </summary>
public bool MiddleButtonPressed
{
get
{
return IsNewButtonPress(Buttons.A);
}
}
This property has a simple getter that returns a call to IsNewButtonPress
with the specified button. The power of this simplistic design lies in the fact that you can add as many properties as you like, and that you can change which buttons map to the named controls just by changing the button checked in the argument to IsNewButtonPress
. You can extend this class and define your own properties, which enables you to reuse the class over and over. Imagine you have a game where the middle button should cause a projectile to fire. You might define a new property like this:
/// <summary>
/// Checks to see if the user pressed Fire.
/// </summary>
public bool Fire
{
get
{
return IsNewButtonPress(Buttons.A);
}
}
This tells the game that a press of A means fire. Perhaps later you want to change Fire
to mean the user pressed B. To do that, you would just change the code of this property to check Buttons.B
instead.
Furthermore, since the same buttons are often used differently given other situations, you can add new properties that reference the same button to take advantage of this flexibility. Pressing the middle button could also indicate a selection of an on-screen element, but you wouldn't want to code this using the word Fire
; that could become very unreadable. All you would need to do is add a new property:
/// <summary>
/// Checks for a menu selection.
/// </summary>
public bool Select
{
get
{
return IsNewButtonPress(Buttons.A);
}
}
To ensure that the InputState
class always has the latest values, the Update
method is employed. This method should be called as one of the first few line items of the game's Update
method. This way, whenever the game is updated, the input state is updated also. The input state class's Update
method performs only one major task: it sets the last input state to the current one, and then retrieves the current input state using GamePad.GetState
.
How can this code be ported to support Windows as well? Technically, Windows games are supported by this code, but you would need to connect an Xbox 360 controller to deliver any input to the game. A better idea is to add keyboard support.
An Input Handler for Zune and Windows Games
Listing 4-2 shows a version of the input handler in Listing 4-1 that supports keyboard input. Note the emphasized lines, which have been added in this revision.
Listing 4-2. An Input Handler Class That Handles Both Windows and Zune Input
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
namespace InputHandler
{
/// <summary>
/// Helper for reading input from gamepad. This class tracks both
/// the current and previous state of the Zune Pad and the keyboard,
/// and provides some properties to abstract specific presses.
/// </summary>
public class InputState
{
#region Fields
public GamePadState CurrentGamePadState;
public GamePadState LastGamePadState;
public KeyboardState CurrentKeyboardState;
public KeyboardState LastKeyboardState;
#endregion
#region Initialization
/// <summary>
/// Constructs a new input state.
/// </summary>
public InputState()
{
CurrentGamePadState = new GamePadState();
LastGamePadState = new GamePadState();
CurrentKeyboardState = new KeyboardState();
LastKeyboardState = new KeyboardState();
}
#endregion
#region Properties
/// <summary>
/// Checks for a Middle Button press (A by default)
/// </summary>
public bool MiddleButtonPressed
{
get
{
return IsNewButtonPress(Buttons.A) ||
IsNewKeyPress(Keys.LeftControl);
}
}
/// <summary>
/// Checks for a press of Up (DPadUp by default)
/// </summary>
public bool UpPressed
{
get
{
return IsNewButtonPress(Buttons.DPadUp) ||
IsNewKeyPress(Keys.Up);
}
}
/// <summary>
/// Checks for a press of Down (DPadDown by default)
/// </summary>
public bool DownPressed
{
get
{
return IsNewButtonPress(Buttons.DPadDown) ||
IsNewKeyPress(Keys.Down);
}
}
/// <summary>
/// Checks for a press of Right (DPadRight by default)
/// </summary>
public bool RightPressed
{
get
{
return IsNewButtonPress(Buttons.DPadRight) ||
IsNewKeyPress(Keys.Right);
}
}
/// <summary>
/// Checks for a press of Left (DPadLeft by default)
/// </summary>
public bool LeftPressed
{
get
{
return IsNewButtonPress(Buttons.DPadLeft) ||
IsNewKeyPress(Keys.Left);
}
}
/// <summary>
/// Checks for a press of the Play button (B by default)
/// </summary>
public bool PlayPressed
{
get
{
return IsNewButtonPress(Buttons.B) ||
IsNewKeyPress(Keys.LeftAlt);
}
}
/// <summary>
/// Checks for a press of the Back button
/// </summary>
public bool BackPressed
{
get
{
return IsNewButtonPress(Buttons.Back) ||
IsNewKeyPress(Keys.Back);
}
}
#endregion
#region Methods
/// <summary>
/// Reads the latest state of the gamepad and the keyboard
.
/// </summary>
public void Update()
{
LastGamePadState = CurrentGamePadState;
CurrentGamePadState = GamePad.GetState(PlayerIndex.One);
LastKeyboardState = CurrentKeyboardState;
CurrentKeyboardState = Keyboard.GetState();
}
/// <summary>
/// Checks if a button was newly pressed during this update.
/// </summary>
/// <param name="button">The button to check</param>
/// <returns>True if the button is down; false otherwise</returns>
public bool IsNewButtonPress(Buttons button)
{
return (CurrentGamePadState.IsButtonDown(button) &&
LastGamePadState.IsButtonUp(button));
}
/// <summary>
/// Checks if a key was newly pressed during this update.
/// </summary>
/// <param name="button">The key to check</param>
/// <returns>True if the key is down; false otherwise</returns>
public bool IsNewKeyPress(Keys key)
{
return (CurrentKeyboardState.IsKeyDown(key) &&
LastKeyboardState.IsKeyUp(key));
}
/// <summary>
/// Checks if a button is pressed down.
/// </summary>
/// <param name="button">The button to check</param>
/// <returns>True if the button is down; false otherwise</returns>
public bool IsButtonDown(Buttons button)
{
return CurrentGamePadState.IsButtonDown(button);
}
/// <summary>
/// Checks if a key is pressed down.
/// </summary>
/// <param name="button">The key to check</param>
/// <returns>True if the key is down; false otherwise</returns>
public bool IsKeyDown(Keys key)
{
return CurrentKeyboardState.IsKeyDown(key);
}
#endregion
}
}
Notice the addition of the IsNewKeyPress
method. This does the same thing as IsNewButtonPress
, except it works with keys instead of buttons. Two new KeyboardState
variables have been added, and they are used in exactly the same way as their GamePadState
counterparts.
Now turn your attention to the properties. In addition to checking for button presses, the code is also checking for the possibility of a key press. The OR (||
) operator is used here, because the user could technically use one or the other. If AND (&&
) were used, the Zune would never fire any inputs, because keys do not exist (therefore are never pressed) on the Zune device. However, at runtime, the Zune will operate happily even with the keyboard code in there. This is partly because both KeyboardState
variables are initialized to a new KeyboardState
and never change from that value.
You can take the code from Listing 4-2 and create a new game library project with it if you wish.
Let's run through a quick exercise using the code from Listing 4-2 to handle input in a clean and efficient manner. In Exercise 4-4, you will use the InputState
class in a simple game that runs on both the Zune and Windows.
EXERCISE 4-4. A ZUNE APP WITH WINDOWS COMPATIBILITY
In this exercise, you will learn to use the InputState
class to build a simple game that changes colors of the screen based on which button is pressed (either on the Zune or Windows). In this application, we will change the game screen color like so: Down = black, Up = red, Right = green, and Left = blue.
InputStateTestGame
.InputState
to the project and add the code from Listing 4-2, except you don't need the statements to use System.Collections.Generic
and System.Text
, and the namespace should be InputStateTestGame
. So, the class should begin as follows:
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
namespace InputStateTestGame
{
/// <summary>
...
Normal.spritefont
.Game1.cs
, just after the declaration of the spriteBatch
variable:
SpriteFont arialFont;
InputState inputState = new InputState();
Color backgroundColor = Color.DarkGray;
LoadContent
method and load the Arial sprite font:
arialFont = Content.Load<SpriteFont>("Normal");
HandleInput
in the Game1.cs
file, below the Draw
method. This class handles changing the background color based on which button was pressed. Note the effective utilization of the InputState
class, and how easy it is to get input, regardless of whether it's a Zune or Windows implementation.
private void HandleInput()
{
if (inputState.DownPressed)
backgroundColor = Color.Black;
if (inputState.UpPressed)
backgroundColor = Color.DarkRed;
if (inputState.RightPressed)
backgroundColor = Color.DarkGreen;
if (inputState.LeftPressed)
backgroundColor = Color.DarkBlue;
}
Update
method in the Game1.cs
file and add the following lines to update the input state class and handle the input. Add this code in place of the TODO
comment.
inputState.Update();
HandleInput();
Draw
method in the Game1.cs
file and locate the method that clears the device. Replace Color.CornflowerBlue
with backgroundColor
. The modified line should look like this:
graphics.GraphicsDevice.Clear(backgroundColor);
TODO
comment in the Draw
method, add the following lines to draw the text:
string helperText = "Up: Red
" +
"Down: Black
" +
"Right: Green
" +
"Left: Blue
";
spriteBatch.Begin();
spriteBatch.DrawString(normalFont, helperText, Vector2.Zero, Color.White);
spriteBatch.End();
The carriage return/new line sequence,
, is used to create a multiline string. The helper text is drawn at (0, 0)—the top-left corner—in white.
InputStateTestGame
project in the Solution Explorer and choosing Create Copy of Project for Windows.Figure 4-18. The color-changing application on the Zune after pressing Down to change the screen to black
Figure 4-19. The color-changing application on Windows after pressing the left arrow key to change the screen to blue (although the screenshot is in grayscale)
As you've seen, handling input on the Zune and adding support for Windows is easy with our InputState
class. It's a good idea to take this code and put it in a usable library of useful code for Zune games, which you can include in any other games you create. This is one example of many reusable components we will be creating throughout the course of the book.
While we are slowly turning the Zune into a mobile gaming platform, that was not its original intent. The Zune is, and will remain, a music player. After all, that's what it was designed to be from the very beginning, and that's what it does best!
When a Zune game is launched, it executes under the .NET Framework in "developer mode." Your game and its actions exist only for the duration that it runs. Zune games have limited access to the local file system. They cannot access the firmware or do anything outside the protected bubble in which they execute. This protection is by design, and is what causes the Zune to reboot after a game exits. Rebooting the Zune causes it to reload its firmware and start anew, eliminating memory leaks, orphan threads, and other things that can create problems.
Developer mode exists mainly to protect the contents of the Zune from the crazy things developers can do. As a result, Zune games can play only those songs that are not restricted by a DRM system.
Let's take a look at some of the components involved in playing music in Zune games.
Three major components of the XNA Framework allow you to play music: the Song, MediaLibrary
, and MediaPlayer
classes. These classes reside in the Microsoft.Xna.Framework.Media
namespace, so don't forget to add this namespace to your list of using
directives.
The Song Class
The Song
class gives you access to a lot of useful data for a particular song. You can access common song attributes, such as the album name, artist, track number, track name, duration, genre, and more. It also has an IsProtected
attribute, which allows you to check for DRM protection before attempting to play a song.
Songs are usually acquired by indexing into the collection of songs available on an instance of the MediaLibrary
class, like so:
Song firstSong = mediaLibrary.Songs[0];
The MediaLibrary Class
The MediaLibrary
class gives you access to the library of music on the Zune. There are useful properties available on instances of the MediaLibrary
class that allow you to get music by album, genre, artist, or even playlist. The Songs
property gives you a collection of all the songs on the Zune.
Creating a new instance of the MediaLibrary
class is very easy. The default constructor of this class takes no arguments, so all you need to do on the Zune is instantiate a simple variable like so:
MediaLibrary library = new MediaLibrary();
The other constructor takes a MediaSource
argument, which is useful on PC or Xbox games where you want to use Windows Media Connect libraries or libraries on the local file system.
Note To enumerate available media sources on other platforms (PC or Xbox 360), you can use MediaSource.GetAvailableMediaSources()
. This method returns a list of available media sources on the device or computer.
The MediaLibrary
class also allows you to access pictures on the device in an easy way. Instances of the MediaLibrary
class have a property called Pictures
. Indexing into this collection returns a Picture
object, which in turn has a method called GetTexture. GetTexture
returns a Texture2D
object, which you can draw on the screen to create an interesting, personalized experience. Likewise, the Album
object has a method called GetAlbumArt
, which you can use to get a Texture2D
object representing the album art.
The MediaLibrary
class is very comprehensive, and these are just a few examples of the unique behaviors you can achieve with it.
The MediaPlayer Class
You cannot create your own instance of the static MediaPlayer
class. You can use only a handful of static methods to control which song is playing. This actually saves you some code in the long run. The code to play a song (held in a Song
object) is straightforward:
MediaPlayer.Play(song);
You can also use this method to play a SongCollection
object, a special collection of songs that can be obtained in a number of ways. Usually, the Songs
property of certain objects in a MediaLibrary
is of type SongCollection
, so you could also use code like this to play an entire artist catalog:
MediaPlayer.Play(mediaLibrary.Artists[artist_index].Songs);
Other useful static methods of the MediaPlayer
class include Stop, Pause, Resume, MoveNext, MovePrevious
, and GetVisualizationData
. There are also some static properties that you can use to inquire about or alter the current state of the media player, such as Volume, PlayPosition, IsMuted, IsVisualizationEnabled, IsShuffled, IsRepeating
, and State
. The State
property can be one of three values as defined in the MediaState
enumeration: Playing, Paused
, or Stopped
.
Note By default, accessing MediaLibrary.Songs
gives you a collection of all songs on the Zune sorted alphabetically by track name. You can achieve some randomization by telling MediaPlayer
to play this entire collection and then setting MediaPlayer.IsShuffled
to true
. However, if you use MoveNext
and MovePrevious
, you must watch out for DRM-protected files.
You can set the volume of the media player by assigning a value between 0.0 and 1.0 to MediaPlayer.Volume
. This value is logarithmic and represents a decibel value, so 0.5 is not really half as loud as 1.0; instead 0.5 is nearly inaudible. It's also important to note that the overall volume you hear is controlled by the Zune's master volume, which you cannot access through code unless you use the Guide
class to show the built-in Zune menu, as described next. In effect, altering MediaPlayer.Volume
will result in a volume less than or equal to the Zune master volume, so it's really useful only when you want to decrease (or reset) the volume.
Note Even when MediaPlayer.Volume
is set to maximum (1.0f
), it is still distinctly quieter than the maximum volume when the Zune is just playing songs. It is about half as loud.
Another option for playing music is to use the built-in guide menu. On the Xbox 360, the guide is the menu that pops up when you press the Xbox logo button. That interface allows you to sign in to profiles, play music, and so on. Like the Xbox guide, the Zune guide can be hooked into via XNA. It brings up a Zune-specific menu for altering volume, playing music, and more. It's a very quick and convenient way to add music functionality to your game without needing to create a custom music browser.
The guide can be shown by calling Guide.Show()
. Note that there is no guide interface for PC games, so you can't compile code for dual Windows/Zune projects unless you use conditional preprocessor directives, like this:
#if ZUNE
Guide.Show();
#endif
Your game will continue running its Update
loop while the guide is shown. To prevent this, you can put your entire Update
loop in an if
statement that references the Guide.IsVisible
property (which will not compile under Windows):
// in the Update method
if (Guide.IsVisible == false)
{
// do all your updates
}
This is a quick and effective way to pause your game if the guide is shown.
There are some drawbacks to using the guide:
Draw
method to clear to black when the guide is shown.Note The guide itself will not show up in screenshots taken from the XNA Game Studio Device Center.
If time allows, it is preferable to write and use your own audio/music engine, rather than use the guide.
As an example of using these main components for playing music, we'll build a simple media player application for the Zune. In the exercise, we'll first declare some variables in the Game1
class to represent the core things we need for the music player:
input
: An instance of our InputState
class, making it easier for us to capture inputalbumArtTex
: A reference to the current album artnoArtTex
: The default texture when there is no album art or no song is playingalbumArtPosition
: A point on the screen where the album art will be drawncurrentSongIndex
: The index in the media library of the song currently being playedregularText
and boldText
: Strings drawn on the screen in their respective fontsboldTextPosition
and regularTextPosition
: Points where these strings are drawnfont
and boldFont
: Differently sized and styled versions of the Kootenay fontWe'll add a RefreshAlbumArt
method to handle drawing the album art on the Zune screen, as shown in Listing 4-3.
Listing 4-3. The RefreshAlbumArt Method
private void RefreshAlbumArt()
{
if (MediaPlayer.State == MediaState.Playing ||
MediaPlayer.State == MediaState.Paused)
{
Song currentSong = MediaPlayer.Queue.ActiveSong;
if (currentSong.Album.HasArt)
{
albumArtTex = currentSong.Album.GetAlbumArt(this.Services);
}
else
albumArtTex = noArtTex;
}
else
{
albumArtTex = noArtTex;
}
albumArtPosition = new Vector2(120 - albumArtTex.Width / 2, 100);
}
The RefreshAlbumArt
method is responsible for setting the Texture2D
object drawn on screen. If the currently playing song has album art, the Texture2D
representing that art is extracted using the Album.GetAlbumArt
method (passing in the Game
class's Services
property). Then, based on the art obtained, the position to draw the art is modified so that it is centered horizontally.
In the Update
method, we'll add some input handling that advances tracks, updates the album art, and updates the status text for the currently playing song, as shown in Listing 4-4.
Listing 4-4. Update Method for the Media Player Example
protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
input.Update();
if (input.PlayPressed)
{
switch (MediaPlayer.State)
{
case MediaState.Paused:
MediaPlayer.Resume();
break;
case MediaState.Playing:
MediaPlayer.Pause();
break;
case MediaState.Stopped:
PlayNextSong();
break;
}
}
if (input.RightPressed)
{
PlayNextSong();
}
if (input.LeftPressed)
{
PlayPreviousSong();
}
switch (MediaPlayer.State)
{
case MediaState.Stopped:
boldText = "Stopped";
regularText = "Press Play to play a song.";
break;
case MediaState.Playing:
Song currentSong = MediaPlayer.Queue.ActiveSong;
boldText = "Artist: " + currentSong.Artist.Name +
"
Album: " + currentSong.Album.Name;
regularText = currentSong.Name;
break;
case MediaState.Paused:
boldText = "Paused";
regularText = "";
break;
}
base.Update(gameTime);
}
First, the input engine is updated. Without calling input.Update
, the game will never know when any buttons are pressed. Then we check to see if Play, Right, or Left are pressed and act accordingly. The Play button behaves differently depending on the state of the media player. If no media is playing, it starts playing the current song. If media is currently playing, it pauses the media player. If the media player is paused, pressing Play will resume from the current track. Pressing Right causes the track to advance using the PlayNextSong
method, shown in Listing 4-5. Pressing Left causes the track to move back using the PlayPreviousSong
method, also shown in Listing 4-5.
After handling input, we must update the text displayed on the screen to match what is playing. Again, this is dependent on the state of the media player. If the player is stopped, we simply offer a directive to press Play. If media is playing, we set the bold text and regular text to the artist, album, and track name. If the media is paused, we simply display "Paused" on the screen.
Listing 4-5. The PlayNextSong and PlayPreviousSong Methods
private void PlayNextSong()
{
for (int songIndex = currentSongIndex + 1; songIndex <=
mediaLibrary.Songs.Count; songIndex++)
{
if (songIndex >= mediaLibrary.Songs.Count)
songIndex = 0;
if (mediaLibrary.Songs[songIndex].IsProtected == false)
{
currentSongIndex = songIndex;
MediaPlayer.Play(mediaLibrary.Songs[currentSongIndex]);
break;
}
}
RefreshAlbumArt();
}
private void PlayPreviousSong()
{
for (int songIndex = currentSongIndex - 1; songIndex >= −1; songIndex--)
{
if (songIndex<0)
songIndex = mediaLibrary.Songs.Count - 1;
if (mediaLibrary.Songs[songIndex].IsProtected == false)
{
currentSongIndex = songIndex;
MediaPlayer.Play(mediaLibrary.Songs[currentSongIndex]);
break;
}
}
RefreshAlbumArt();
}
The PlayNextSong
and PlayPreviousSong
methods are very similar in structure. The purpose of the for
loops is to obtain the next available unprotected track; it will keep looking until it finds one. When an unprotected track is found, the MediaPlayer
plays it. In both methods, the album art is automatically refreshed by the call to RefreshAlbumArt
.
Note You can also use MediaPlayer.MoveNext()
and MediaPlayer.MovePrevious()
to advance the current track in the song collection. However, currently, if the next song in the queue is DRM-protected, this method will throw an exception. The purpose of the custom PlayNextSong
and PlayPreviousSong
methods is to provide checks for DRM protection so no attempt to play a protected song is made. It is safe to use the built-in methods if the MediaPlayer
is playing a SongCollection
that does not contain any protected tracks.
In the Draw
method, we will clear the screen to black, draw the status text blocks, and draw the album art, as shown in Listing 4-6.
Listing 4-6. Draw Method for the Media Player Example
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
try
{
spriteBatch.Draw(albumArtTex, albumArtPosition, Color.White);
spriteBatch.DrawString(fontBold, boldText, boldTextPosition,
Color.White);
spriteBatch.DrawString(font, regularText, regularTextPosition,
Color.White);
}
catch
{
}
spriteBatch.End();
base.Draw(gameTime);
}
The try-catch
block is used because some track names, artist names, and album names may contain special characters that are not compiled by the content pipeline. The DrawString
method draws a string of text character by character, so when an undrawable character is encountered, an exception will be thrown, and you will see a partial string of text. You can modify which characters are compiled into the sprite font by opening the .spritefont
file and looking for the CharacterRegions
tag. In that code, you can modify the Start
and End
elements to ensure all possible special characters are included in the sprite font. See if you can arrive at a more elegant solution than the try-catch
approach.
Also notice that the albumArtTex
object is drawn first. This ensures that the text will be displayed over the album art. If there is no album art, albumArtTex
has been set to the default "no album art" texture by the RefreshAlbumArt
method, so this is safe.
Now let's put this together into a media player.
EXERCISE 4-5. A SIMPLE MEDIA PLAYER
The Zune firmware sports a world-class music player with a lot of great features. So, why build another one? This exercise will help you understand how the main media components of the XNA Framework work together to make a basic music player. In turn, this understanding will prove valuable when you want to implement music in your games.
SimpleMusicPlayer
. The sample code can be found in the Exercises/Chapter 4 /Example 5
folder.Content
project. Call one Kootenay
and change its font size to 10
. Call the other KootenayBold
, change its font size to 10
, and change its style to Bold
. This can be done by editing the associated .spritefont
files.NoArt.png
file to the Content
project from the Examples/Chapter 4 /Exercise 5/Artwork
folder.InputState
class from earlier in the chapter (Listing 4-1), into the mix. You can do this by right-clicking the project, choosing Add Existing Item, and navigating to the Examples/Chapter 4 /Exercise 4/InputHandler
folder. Since you are copying code rather than using a game library, don't forget to change the namespace in InputState.cs
to SimpleMusicPlayer
. If you've created your own library for input handling already, then you are ahead of the game (in a good way). Just add a reference to your own library and be sure to include the namespace.Game1.cs
where the variables are declared:
MediaLibrary mediaLibrary;
InputState input;
Texture2D albumArtTex, noArtTex;
Vector2 albumArtPosition;
int currentSongIndex = −1;
string regularText, boldText;
Vector2 boldTextPosition, regularTextPosition;
SpriteFont font, fontBold;
Initialize
method, initialize the media library and current song. The vectors are initialized to points on the screen that look good placement-wise. The volume is set to the maximum (1.0f
).
protected override void Initialize()
{
input = new InputState();
mediaLibrary = new MediaLibrary();
MediaPlayer.Volume = 1.0f;
regularText = "";
boldText = "";
boldTextPosition = new Vector2(5, 5);
regularTextPosition = new Vector2(5, 35);
base.Initialize();
}
LoadContent
method, load the two sprite fonts and the default "no album art" texture. Then call RefreshAlbumArt()
.
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>("Kootenay");
fontBold = Content.Load<SpriteFont>("KootenayBold");
noArtTex = Content.Load<Texture2D>("NoArt");
albumArtTex = noArtTex;
RefreshAlbumArt();
}
RefreshAlbumArt
private method shown in Listing 4-3.UnloadContent
method and tell the MediaPlayer
to stop playing. When the game exits, you want the audio to stop, especially if you are debugging and the Zune doesn't reboot.
protected override void UnloadContent()
{
MediaPlayer.Stop();
}
Update
method and modify it so it looks like Listing 4-4.PlayNextSong
and PlayPreviousSong
private methods shown in Listing 4-5.Draw
method and modify it so it looks like Listing 4-6.Figure 4-20. The SimpleMusicPlayer application running. The song in this screenshot harkens back to my college days. I played drums in a band called Morningside. We had no album art, and this song was called simply 7. I love it because it opens with a wicked drum solo.
We're going to shift gears a bit now and work on the very first full-fledged Zune game of the book, called OutBreak. OutBreak will roll together a lot of the techniques covered thus far, resulting in a game you will actually enjoy playing. You will learn a few new techniques to spice up your graphics as well.
In this example, you'll see the guide menu used to provide musical accompaniment. Simple game state management is also addressed, along with some other new concepts such as collision detection and text centering.
OutBreak is a paddle game, where the object is to clear the board of all the visible blocks. By block, I mean the object the player is trying to strike with the ball.
Before writing any code, let's take a look at some basic design elements that will lead us to implementing the game logic.
During one interview a few years ago for a C# position, I was asked whether my coding style was maverick or meticulous. At the time, I wasn't sure what was meant by the question. Now I know that mavericks sit down and code, with little or no design planning, figuring things out as they go. Meticulous coders are structured almost to the point of refusing to write code until they have a signed-off design document. For the purposes of this example, we'll find some middle ground between the two and outline some basic rules that our game must follow.
The Rules
The general rules of this implementation of OutBreak are simple:
You could alter these rules a little to suit your wishes without changing too much code. No numbers are directly specified (for example, the maximum number of lives), because these are defined in code as constants.
Game States
The mechanism for dealing with game state is very simple: we use an enumeration to manipulate the state, and we check the game state at various times using a switch
statement. We define four possible game states for the game:
Intro
: The first screen of the game appears, as shown in Figure 4-21.Figure 4-21. OutBreak in the Intro state
Ready
: A countdown screen appears between rounds, as shown in Figure 4-22.Figure 4-22. OutBreak in the Ready state
Playing
: The game is active, as shown in Figure 4-23.Figure 4-23. OutBreak in the Playing state
GameOver
: The game has ended, as shown in Figure 4-24.Figure 4-24. OutBreak in the GameOver state
These four game states compose everything you'll see in the game. The next step is to figure out how to transition between these states. Figure 4-25 shows a traditional state diagram that will help you determine when to move between these game states. Each circle is a game state, and the arrows flowing between them represent the conditions that must be met for the game to transition to another state.
Figure 4-25. States and transitions for the OutBreak game
Figure 4-25 shows that the game state flow is very simple. The game displays the intro screen first. Then the game alternates between the countdown and playing screens, until the player has lost all her lives. When there are no lives left, the game moves to the GameOver
state. The player can then restart the game by pressing a button, which changes the game state back to Intro
, and the state flow resumes from the Intro
state.
Are you ready to write your very first real, playable Zune game? This example incorporates a lot of what you have already learned. Some important concepts demonstrated here are usage of the media player (via the guide), collision detection, drawing game objects, handling input, randomization, writing clean code using regions and classes, and maintaining a parallel copy of the game for Windows.
The structure of the Game1.cs
file in this game has been modified slightly from the standard layout of what's included by default in an XNA game. I made extensive use of regions to separate code clearly, and I trimmed out some of the unused namespaces. Reading through this example should closely resemble your thought process while building the game, although you will likely see some code you don't understand yet. This is where the use of regions comes in handy. Regions are just ways of grouping code together; they aren't compiled, and are there for your convenience as a programmer. They provide easy reference to portions of code and help keep your code organized.
By the end of this chapter, you'll have a functioning game that runs on both Zune and Windows. If at any time you feel you are lost or don't have the correct code, check the supplied source code in the Examples/
Chapter 4 /Exercise 6/OutBreak
folder.
Setting Up the Project
To get started, set up your project as follows:
OutBreak
.Game1.cs
and arrange it into regions. The rest of this example will refer to certain areas of the code by region name, so that you are adding and modifying code in the correct places. Create regions as follows:
XNA Methods
.spriteBatch
and Graphics
) in a region called Private Fields
.Game1
constructor in a region called Constructor(s)
.Public Enumerations, Public Constants, Helper Methods
, and Game State Transitions
.When collapsed, your code should look like Figure 4-26.
Figure 4-26. The overall structure of the OutBreak Game1.cs file
Tip Use #region Region Name
to mark the start of a region, and mark the end with #endregion
. You can also select a block of code (of any size), right-click the selection, choose Surround With, then #region
, which will automatically stick the selected code in a new region.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
Content
project by right-clicking it and choosing Add Existing Item. Navigate to the Examples/
Chapter 4 /Exercise 6/Artwork
folder and add all six files in that folder: ball.png, paddle.png, block.png, gameover.png, outbreak_background.png
, and outbreak_bg_dark.png
.Content
project. Call it Normal.spritefont
. This sprite font should be Kootenay, size 14. At this point, your Solution Explorer window should look like Figure 4-27.Figure 4-27. The project structure of the OutBreak game
Customizing the Input State Class
Once again, we'll use the InputState
class we built earlier in the chapter. Add the InputState.cs
file from Exercise 4-4 to the project. Change the namespace in this file to OutBreak
. Add or modify the following properties to support our specific input actions in the InputState.cs
file, as shown in Listing 4-7.
Listing 4-7. Input Handler Class for OutBreak (InputState.cs)
/// <summary>
/// Checks for a Middle Button press (A by default)
/// </summary>
public bool MiddleButtonPressed
{
get
{
return IsNewButtonPress(Buttons.A) ||
IsNewKeyPress(Keys.Space) || IsNewKeyPress(Keys.Enter);
}
}
/// <summary>
/// Checks to see if Left is down.
/// </summary>
public bool LeftIsDown
{
get
{
return IsButtonDown(Buttons.DPadLeft) ||
IsKeyDown(Keys.Left);
}
}
/// <summary>
/// Checks to see if Right is down.
/// </summary>
public bool RightIsDown
{
get
{
return IsButtonDown(Buttons.DPadRight) ||
IsKeyDown(Keys.Right);
}
}
/// <summary>
/// Checks for a press of the Play button (B by default)
/// </summary>
public bool PlayPressed
{
get
{
return IsNewButtonPress(Buttons.B) ||
IsNewKeyPress(Keys.Enter) || IsNewKeyPress(Keys.Space);
}
}
These four properties support proper input from the Zune and the keyboard, providing a consistent and intuitive user experience, regardless of the platform. The Enter key or spacebar on the keyboard equate to pressing Play or the middle button on the Zune in this particular game. The left and right arrow keys will correspond to directional pad Left and Right. We added the two properties because we want to check the current state of the button so that the paddle moves smoothly, rather than just on a new key or button press.
Creating a Class for Block Objects
Next, we will create a game object class to represent a single block in the game. The block has only a few properties: color, position, and visibility. This class is more or less the same as a struct
, as it just has some basic properties and a constructor to populate them. The next chapter will cover game component objects in great detail, but for now, let's keep it simple. Add a new class file to the project called Block.cs
and fill it with the code shown in Listing 4-8.
Listing 4-8. The Block Class for OutBreak (Block.cs)
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace OutBreak
{
public class Block
{
public Vector2 Position;
public Color Color;
public bool IsVisible;
public Block(Vector2 position, Color color, bool visible)
{
Position = position;
Color = color;
IsVisible = visible;
}
}
}
Defining Enumerations
Enumerations are important and useful, because they are readable and easily accessible using IntelliSense. This game has two enumerations: one for the game state and one that you can use to determine which type of game element the ball struck last. Add the two enumerations shown in Listing 4-9 in the Enumerations
region you defined earlier in the Game1.cs
file, outside the Game1
class.
Listing 4-9. Enumerations for OutBreak (in Game1.cs)
public enum LastObjectBounced
{
Paddle,
Top,
Wall,
Right,
Block,
None
}
public enum GameState
{
Intro,
Ready,
Playing,
GameOver
}
The reason we have a LastObjectBounced
enumeration is that sometimes, in paddle games, the ball can get "stuck" inside a paddle or other game object due to collision detection if it doesn't know what it hit last. Our collision detection logic, which you will see later, reverses the direction of the ball based on its collision status with some other object. Knowing that the ball hit a block last, for example, allows us to ensure the vector is reversed again if the ball still collides with the same object, even after having its direction changed.
The game uses a vector to indicate the direction of the ball. That vector is added to the ball's current position to update its location on the screen. As a result, the game logic moves the ball in increments of greater than 1 pixel, so the ball could feasibly land inside a block or paddle, rather than hitting its border precisely. Since we just flip the direction vector using the Vector2.Reflect
method, the ball could still be inside that game object, and on the next update, it would reverse direction again because the collision is still true. The LastObjectBounced
enumeration allows us to check for new collisions and ignore ones that could produce this behavior.
The GameState
enumeration defines our four game states, outlined during the design phase of the game.
Defining Private Fields
We will use quite a few private fields to make this game work correctly. Private fields are member variables that are used to store shared data beyond the scope of method calls, such as the score, object positions, and more. Expand the Private Fields
region in the Game1
class and modify its contents to look like Listing 4-10.
Listing 4-10. Private Fields for OutBreak (in Game1.cs)
// Default XNA fields
private GraphicsDeviceManager graphics;
private SpriteBatch spriteBatch;
// Game element textures
private Texture2D blockTex, paddleTex, ballTex;
// Background textures
private Texture2D introBackgroundTex, darkBackgroundTex, gameOverTex;
// Font
private SpriteFont normalFont;
// Gameplay objects
private List<Block> blocks;
private InputState input;
private GameState gameState;
private LastObjectBounced lastObjectBounced;
private TimeSpan screenTimer;
// Gameplay helper variables
private int paddleSpeed;
private int score;
private int numLives;
private int scoreMultiplier;
private bool isPaused;
// Game math objects
Vector2 initialBallDirection;
Vector2 ballDirection;
Vector2 ballPosition;
Vector2 paddlePosition;
// Text display position vectors
Vector2 textReadyLivesPosition;
Vector2 textCountdownPosition;
Vector2 textFinalScorePosition;
Vector2 textPlayingScorePosition;
The fields are organized by their general purpose. The default XNA fields are the ones provided by default. The Texture2D
objects declare our drawable textures, and the SpriteFont
object represents our font.
The game-play objects include a linear list of Block
objects, the current game state, an instance of the input state class, the last collision object, and a TimeSpan
object used to determine when the countdown screen (Ready
state) is shown. The screenTimer
object lets us count down on the countdown screen.
The game-play helper variables are value types that represent simple numbers and Boolean values, such as the score, the current number of lives, and whether the game is paused by invoking the guide menu.
The math objects are all Vector2
objects that we can use to calculate the current position and direction of moving game objects, such as the paddle and ball.
Finally, the Vector2
objects at the bottom are initialized to specific point locations where text is drawn on the screen. These variables are included to reduce hard-coding.
Setting Up Constant Values
Constant values are included to ensure that you have only one place to go to modify certain aspects of the game that should not change. That place is the Public Constants
region. Expand that region and add the code shown in Listing 4-11.
Listing 4-11. Constant Values for OutBreak (in Game1.cs)
// Gameplay constants
public const int MAX_LIVES = 3;
public const int BASE_SCORE = 2;
public const int SCREEN_DISPLAY_TIME = 3;
public const int PADDLE_SPEED = 4;
public const int PADDLE_Y = 280;
public const int BALL_Y = 270;
public const int BLOCK_WIDTH = 30;
public const int BLOCK_HEIGHT = 10;
These constants can be changed at design time to modify the associated game-play elements. The suggested values here happen to work well for the game. SCREEN_DISPLAY_TIME
is a value in seconds that determines how long the countdown screen is shown before moving into the Playing
state.
Initializing the Game
The constructor doesn't need to change, so we can leave that region alone. Expand the XNA Methods
region and find the Initialize
method. As you learned early in the book, this is where you set up any values that need to be initialized before the game runs. Modify the Initialize
method so it looks like Listing 4-12.
Listing 4-12. Initialize Method for OutBreak (XNA Methods Region in Game1.cs)
protected override void Initialize()
{
// Set the screen and buffer size
graphics.PreferredBackBufferWidth = 240;
graphics.PreferredBackBufferHeight = 320;
graphics.ApplyChanges();
// Initialize game objects
input = new InputState();
screenTimer = new TimeSpan();
blocks = new List<Block>();
isPaused = false;
initialBallDirection = new Vector2(0.5f, −7.0f);
paddleSpeed = PADDLE_SPEED;
// Initialize the screen element positions
textReadyLivesPosition = new Vector2(10, 160);
textCountdownPosition = new Vector2(10, 220);
textFinalScorePosition = new Vector2(120, 250);
textPlayingScorePosition = new Vector2(10, 295);
base.Initialize();
}
The first three lines set the viewport width and height to the Zune screen dimensions of 240 by 320 and apply those changes. This forces the back buffer to these dimensions, and it also sets the Windows game's window size.
The next block of code instantiates the game objects. The initialBallDirection
vector is copied to the ballDirection
vector every time a new round starts; this is the default trajectory of the ball. With each update, it moves 0.5 pixel to the right and 7 pixels up, as we have defined it.
The final block of code sets up points that represent locations on the screen for text to be drawn.
Implementing Game State Transitions
The methods in Listing 4-13 are used by other methods in the game logic to transition the game correctly to other states. These methods do more than just altering the value of the gameState
variable: they reset other values, rebuild collections, and so forth, depending on the state to which the game is transitioning. Near the bottom of the code, find the Game State Transitions
region, expand it, and add the methods shown in Listing 4-13.
Listing 4-13. Game State Transition Methods for OutBreak (in Game1.cs)
private void MoveToIntroState()
{
score = 0;
scoreMultiplier = 1;
numLives = MAX_LIVES;
ballDirection = initialBallDirection;
lastObjectBounced = LastObjectBounced.Paddle;
ResetBlocks();
ResetPositions();
gameState = GameState.Intro;
}
private void MoveToReadyState(GameTime gameTime)
{
screenTimer = gameTime.TotalGameTime;
scoreMultiplier = 1;
ResetPositions();
gameState = GameState.Ready;
}
private void MoveToPlayingState()
{
ResetPositions();
gameState = GameState.Playing;
}
private void MoveToGameOverState()
{
gameState = GameState.GameOver;
}
private void MoveToLevelComplete()
{
gameState = GameState.GameOver;
}
These methods work as follows:
MoveToIntroState
: Resets the game entirely. The score, lives count, multiplier, directions, last collision object, game object positions, and collection of blocks are all reset to default values here. This method could theoretically be called at any time during the game to start completely afresh.MoveToReadyState
: Accepts a GameTime
argument because it is time-sensitive. It needs to know when it was invoked so that the countdown can begin from the current time. It also resets the game object positions and the score multiplier.MoveToPlayingState
: Occurs between the countdown screen and the playing screen. To be safe, this method resets game object positions before transitioning.MoveToGameOverState
: Changes the game state to GameOver
.MoveToLevelComplete
: Same as MoveToGameOverState
, although it is really just a stub to provide future support for a game that has more than one level. This method is called when there are no more blocks on the screen.Implementing Reset Helper Methods
In the game state transitions code, we called two methods that haven't appeared in our code yet: ResetBlocks
and ResetPositions
. These are helper methods that take care of resetting more complicated elements in the game. Find the Helper Methods
region and add the two methods shown in Listing 4-14.
Listing 4-14. Helper Methods for OutBreak (in Game1.cs)
private void ResetBlocks()
{
// Random RGB color values
Random rnd = new Random();
float red, green, blue;
Color randomColor;
int numRows = 5;
int numColumns = 8;
int x = 0;
int y = 0;
blocks.Clear();
for (int row = 0; row<numRows; row++)
{
for (int col = 0; col<numColumns; col++)
{
red = (float)rnd.NextDouble();
green = (float)rnd.NextDouble();
blue = (float)rnd.NextDouble();
randomColor = new Color(red, green, blue);
blocks.Add(new Block(new Vector2(x, y), randomColor, true));
x += BLOCK_WIDTH;
}
x = 0;
y += BLOCK_HEIGHT;
}
}
private void ResetPositions()
{
try
{
// Put the paddle and ball in the middle of the screen.
int screenWidth = GraphicsDevice.Viewport.Width;
paddlePosition = new Vector2(screenWidth / 2 - paddleTex.Width / 2,
PADDLE_Y);
ballPosition = new Vector2(screenWidth / 2 - ballTex.Width / 2, BALL_Y);
ballDirection = initialBallDirection;
}
catch
{
throw new Exception("Don't call ResetPositions until after
the content is loaded.");
}
}
The ResetBlocks
method clears out the block list and runs a nested for loop for an 8×5 grid of blocks. In this loop, a brand-new color and position are assigned to the block. This is an example of a classic row/cell type of algorithm most people learn in introductory programming classes.
The ResetPositions
method is interesting because it dynamically positions elements on the screen based on the size of the texture supplied. Centering objects is accomplished by taking half the screen width and subtracting half the texture width. This method affects only the position of the paddle and the ball. Because this method is dependent on the textures being loaded, the method cannot be used correctly until after LoadContent
loads the textures. Speaking of the LoadContent
method, let's take a look at that next.
Loading the Game Content
Expand the XNA Methods
region and find the LoadContent
method. Add the code shown in Listing 4-15 to initialize textures and fonts.
Listing 4-15. LoadContent Method for OutBreak (XNA Methods Region in Game1.cs)
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
blockTex = Content.Load<Texture2D>("block");
paddleTex = Content.Load<Texture2D>("paddle");
ballTex = Content.Load<Texture2D>("ball");
introBackgroundTex = Content.Load<Texture2D>("outbreak_background");
darkBackgroundTex = Content.Load<Texture2D>("outbreak_bg_dark");
gameOverTex = Content.Load<Texture2D>("gameover");
normalFont = Content.Load<SpriteFont>("Normal");
// Move to the New Game state.
// Requires that the textures be loaded so we can center objects.
MoveToIntroState();
}
This is standard content loader code, with the addition of the call to MoveToIntroState
. It would seem that we should call that method in the Initialize
method, but it makes more sense (and is required) to call it here, because the textures must be loaded in order to dynamically position the ball and paddle based on their size. This is the last chance to call such methods before the game loop starts running.
Handling Input
Having the InputState
class certainly reduces the amount of code we need to write, but we are creating a real game at this point. As such, it makes sense to have a method dedicated to handling the input. First of all, the input
instance of the InputState
class must be updated directly from the game's Update
method. After that, we can call the HandleInput
method. Find the Helper Methods
region and add the method shown in Listing 4-16 to handle various types of input.
Listing 4-16. HandleInput Method for OutBreak (Helper Methods Region in Game1.cs)
private void HandleInput(GameTime gameTime)
{
switch (gameState)
{
case GameState.Intro:
// Wait for the play button to be pressed
if (input.PlayPressed)
{
MoveToReadyState(gameTime);
}
break;
case GameState.Playing:
// Check Left
if (input.LeftIsDown)
{
// Check boundary to the left
if (paddlePosition.X > 0)
paddlePosition.X -= paddleSpeed;
}
// Check Right
if (input.RightIsDown)
{
if (paddlePosition.X < GraphicsDevice.Viewport.Width -
paddleTex.Width)
paddlePosition.X += paddleSpeed;
}
// Bring up the Guide
#if ZUNE
if (input.PlayPressed)
{
Guide.Show();
}
#endif
break;
case GameState.GameOver:
if (input.MiddleButtonPressed || input.PlayPressed)
MoveToIntroState();
break;
default:
break;
}
}
This is your first real exposure to referencing the game state. Input should behave differently based on the current game state. For example, there's no point handling Left and Right input if the game is just sitting idly on the intro screen. To accomplish this, we have a switch
statement that looks at the value of gameState
.
On the intro screen, we only care if the user presses Play, which transitions the game to the Ready
state.
When playing the game (in the Playing
state), we want to handle three types of input: pressing Left, pressing Right, or pressing the Play button. When checking if Left is down, we move the paddle left only if it's not already all the way to the left, which we determine by comparing the paddle position's X
component to the left boundary (zero). When checking if Right is down, we move the paddle to the right only if the current paddle's position (plus the entire width of the paddle) is less than the right boundary (the screen width). This keeps the paddle constrained equally on both sides of the playing area. When Play is pressed, the guide menu appears. Note the use of conditional preprocessor directives here. Guide.Show
will not compile in the Windows version of the game, so we wrap this bit of code in #if ZUNE
.
Note The XNA, ZUNE
, and WINDOWS
build constants are predefined for you in XNA game projects.
Finally, in the GameOver
state, pressing the middle button or Play will result in the game resetting to the Intro
state. No other game states require input to be handled, but it's easy to support it by adding another case label.
Handling Collisions
In this game, we will use an incredibly simple method of checking for collisions. This method just checks to see if one rectangle intersects another. A bounding box is a rectangle around an object that represents its borders. The bounding boxes are set up and then checked to see if they intersect. If they do, some action is taken, depending on which objects are colliding.
Collision detection is really the meat of the game, as it drives most of the decisions made, such as win or lose, success or failure, life or death, and so on. As such, HandleCollisions
is a rather beefy method. Find the Helper Methods
region and add the HandleCollisions
method shown in Listing 4-17.
Listing 4-17. HandleCollisions Method for OutBreak (Helper Methods Region in Game1.cs)
private void HandleCollisions(GameTime gameTime)
{
Rectangle ballBoundingBox = new Rectangle((int)ballPosition.X,
(int)ballPosition.Y, ballTex.Width, ballTex.Height);
Rectangle blockBoundingBox = new Rectangle();
Rectangle paddleBoundingBox = new Rectangle((int)paddlePosition.X,
(int)paddlePosition.Y, paddleTex.Width, paddleTex.Height);
float ballCenterX = 0.0f;
float paddleCenterX = 0.0f;
float distance = 0.0f;
float ratio = 0.0f;
// Check collisions with blocks
foreach (Block block in blocks)
{
blockBoundingBox.X = (int)block.Position.X;
blockBoundingBox.Y = (int)block.Position.Y;
blockBoundingBox.Width = BLOCK_WIDTH;
blockBoundingBox.Height = BLOCK_HEIGHT;
if (block.IsVisible)
{
if (ballBoundingBox.Intersects(blockBoundingBox))
{
// The ball hit a block - increment score
block.IsVisible = false;
if (lastObjectBounced == LastObjectBounced.Block)
scoreMultiplier *= 2;
score += BASE_SCORE * scoreMultiplier;
// Do a quick search to see if there are any blocks left
bool allBlocksGone = true;
foreach (Block b in blocks)
{
if (b.IsVisible)
{
allBlocksGone = false;
break;
}
}
if (allBlocksGone)
{
MoveToLevelComplete();
}
else
{
// Reflect in Y direction
ballDirection = Vector2.Reflect(ballDirection,
Vector2.UnitY);
lastObjectBounced = LastObjectBounced.Block;
}
}
}
}
// Check collisions with the paddle
if (ballBoundingBox.Intersects(paddleBoundingBox))
{
if (lastObjectBounced != LastObjectBounced.Paddle)
{
// Bounce the direction vector in the Y direction
ballDirection = Vector2.Reflect(ballDirection, Vector2.UnitY);
// Determine how far from the center of the paddle the ball hit
ballCenterX = ballPosition.X + (float)(ballTex.Width / 2.0f);
paddleCenterX = paddlePosition.X + (float)(paddleTex.Width / 2.0f);
distance = ballCenterX - paddleCenterX;
ratio = distance / (paddleTex.Width / 2);
ballDirection.X = ballDirection.X + (ratio * 2);
scoreMultiplier = 1;
lastObjectBounced = LastObjectBounced.Paddle;
}
}
// Check left & right collisions
if (ballPosition.X <= 0 ||
ballPosition.X >= GraphicsDevice.Viewport.Width - ballTex.Width)
{
if (lastObjectBounced != LastObjectBounced.Wall)
{
// Bounce the direction vector in the X direction
ballDirection = Vector2.Reflect(ballDirection, Vector2.UnitX);
lastObjectBounced = LastObjectBounced.Wall;
}
}
// Check top collision (screen boundary)
if (ballPosition.Y <= 0)
{
if (lastObjectBounced != LastObjectBounced.Top)
{
ballDirection = Vector2.Reflect(ballDirection, Vector2.UnitY);
lastObjectBounced = LastObjectBounced.Top;
scoreMultiplier = 1;
}
}
// Check bottom collision (player loses a life)
if (ballPosition.Y >= GraphicsDevice.Viewport.Height)
{
numLives--;
if (numLives<1)
MoveToGameOverState();
else
{
MoveToReadyState(gameTime);
}
}
}
As you can see, this method is quite complicated, so we'll break it down so that it makes more sense. The next chapter will discuss collision detection in greater detail, so the explanation here is brief.
The first three lines of the method set up the bounding boxes for the objects we want to check for collisions: the ball, the paddle, and a block. The next four float
variables are used when the ball hits the paddle to determine how far from the center of the paddle the ball hit. This allows the code to slightly alter the ball's direction based on where it struck the paddle.
Next is a foreach
loop that loops through all the Block
objects to determine which of them collide with the ball. This loop does a couple of things. If the ball collides with the current Block
, that Block
is made invisible and the score is updated. If no blocks are left, the level is completed. Otherwise, the ball's direction vector is flipped, and the lastObjectBounced
variable is set to LastObjectBounced.Block
. This foreach
loop handles collisions between the ball and all Block
objects.
After the foreach
loop are four if
statements that check other collisions:
if
statement checks for a collision between the paddle and the ball. If the ball and paddle collide, the ball direction vector is flipped in the y direction, and then has some modification applied to the vector's X
component based on where the ball landed. Doing this adds a special dynamic to the game that is very intuitive for the player. If you want to "slow down" or "speed up" the ball, you can do this by positioning the paddle differently.if
statement checks for left and right wall collisions. If the ball hits the left or right wall, the ball's direction vector is flipped in the x direction.if
statement checks to see if the ball has hit the top of the screen. If so, the vector is flipped in the y direction without assigning any points.if
statement checks to see if the ball has hit the bottom of the screen without touching the paddle. In this case, a life is lost and the game transitions to the Ready
state. If there are no lives left, the game transitions to the GameOver
state.Putting It Together: The Update Method
All of the methods we need are in place to get the game updating correctly. Expand the XNA Methods
region, find the Update
method, and modify it to look like Listing 4-18.
Listing 4-18. Update Method for OutBreak (XNA Methods Region in Game1.cs)
protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
#if ZUNE
isPaused = Guide.IsVisible;
#endif
if (!isPaused)
{
input.Update();
HandleInput(gameTime);
switch (gameState)
{
case GameState.Intro:
// No processing for this game state
break;
case GameState.Ready:
if (gameTime.TotalGameTime.Subtract(screenTimer).Seconds >=
SCREEN_DISPLAY_TIME)
gameState = GameState.Playing;
break;
case GameState.Playing:
HandleCollisions(gameTime);
ballPosition = Vector2.Add(ballPosition, ballDirection);
break;
}
}
base.Update(gameTime);
}
The first thing you notice here is another conditional preprocessor directive to update the value of the isPaused
variable based on the visibility of the guide. The reason this depends on the platform is that the guide does not exist for Windows games. On Windows, isPaused
will always be false
, because there is no code to alter it. On the Zune, however, this value determines two things: whether the game updates during game play and what is drawn behind the guide in the Draw
method.
Accordingly, the meat of the logic is wrapped in an if
statement so that it executes only when the game is not paused. Without this bit of code, the game will continue to run in the background while the guide is displayed, which would not be a good thing.
If the game should be updating (meaning it's not paused), then the input engine is explicitly updated and the input is handled via a call to HandleInput
. Then the game processing for each game state comes into play. In the Ready
state, the timer is updated; if a number of seconds equal to SCREEN_DISPLAY_TIME
has elapsed, the game transitions into the Playing
state. During the Playing
state, collisions are handled, and the ball's position is added to its direction vector to move it on the screen.
Notice how compact the Update
method is. We extracted most of the core game logic into different methods so that this one stays neat and tidy, in case you need to add more game states or other engines to update.
Putting It Together: The Draw Method
The final piece of this exercise is the Draw
method, which is responsible for drawing the game elements at their most recent locations. There is not, and there should not be, any game-play logic in this method. Expand the XNA Methods
region, find the Draw
method, and alter it to look like Listing 4-19.
Listing 4-19. Draw Method for OutBreak (XNA Methods Region in Game1.cs)
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
switch (gameState)
{
case GameState.Intro:
spriteBatch.Draw(introBackgroundTex, Vector2.Zero, Color.White);
break;
case GameState.Ready:
spriteBatch.Draw(darkBackgroundTex, Vector2.Zero, Color.White);
spriteBatch.DrawString(normalFont,
"Lives: " + numLives + "
Score: " + score,
textReadyLivesPosition, Color.White);
int remainingSeconds = SCREEN_DISPLAY_TIME -
(gameTime.TotalGameTime.Seconds - screenTimer.Seconds) + 1;
spriteBatch.DrawString(normalFont,
"Get Ready... " + remainingSeconds,
textCountdownPosition, Color.White);
break;
case GameState.Playing:
if (!isPaused)
{
spriteBatch.Draw(paddleTex, paddlePosition, Color.White);
spriteBatch.Draw(ballTex, ballPosition, Color.White);
foreach (Block block in blocks)
{
if (block.IsVisible)
spriteBatch.Draw(blockTex, block.Position, block.Color);
}
spriteBatch.DrawString(normalFont,
"score: " + score + " (x" + scoreMultiplier + ")",
textPlayingScorePosition, Color.White);
}
break;
case GameState.GameOver:
spriteBatch.Draw(gameOverTex, Vector2.Zero, Color.White);
string scoreText = "Your Score: " + score;
Vector2 textOrigin = normalFont.MeasureString(scoreText) / 2;
spriteBatch.DrawString(normalFont, scoreText,
textFinalScorePosition, Color.White, 0.0f, textOrigin, 1.0f,
SpriteEffects.None, 0.5f);
break;
}
spriteBatch.End();
base.Draw(gameTime);
}
The Draw
method is sensitive to the current game state. The game state determines exactly what is drawn on the screen. In any case, the screen is always cleared to black.
In the Intro
state, the intro graphic is drawn—nothing too fancy is happening here.
In the Ready
state, the darker background is drawn, followed by two text elements. The first element contains the current score and number of remaining lives. The second text element is the countdown text, which looks like "Get Ready ...x," where x is the number of seconds remaining until the round starts.
In the Playing
state, the paddle and ball are drawn. Then all of the blocks are drawn, but only if that block is visible. The score is displayed in real time at the bottom of the screen.
Finally, in the GameOver
state, the "Game Over" graphic is drawn, along with the score. The score is centered using the MeasureString
method, which we will discuss further in the next chapter. Notice that the code to draw the score text uses a different method signature for DrawString
than you have seen so far. This allows us to specify the text origin as well as the position.
After the switch
statement, the sprite batch is closed, and the entire screen is rendered to the device.
Testing OutBreak
Whew! All of the code is now in place for the game. You should have a good understanding of the design behind OutBreak and the purpose of each of the methods. Now, let's take it for a test drive.
First, test it on the PC. Right-click the Windows copy project and choose Set as Startup Project. It doesn't matter which Zune you have set for deployment; if deployment fails, the Windows game can still launch. Rebuild the solution and correct any compilation errors that arise. If you have trouble, compare your code with the complete sample code provided for this game. Press F5 to run the game.
The Enter and spacebar keys are used for play, as well as the middle button press, as we defined in the InputState
class. Use the left and right arrow keys to move the paddle. Test the game play on your PC and enjoy.
Next, test it on your Zune. Right-click the Zune project and choose Set as Startup Project. Ensure you have the correct Zune set as the default device in XNA Game Studio Device Center. Press F5 to deploy and run the game. Use the Zune directional pad to move the paddle to the right and left. During game play, press the Play button to bring up the guide, and notice how the game pauses.
Congratulations—you have just built your first Zune game that also runs on Windows!
The following questions will help you apply what you've learned in this chapter to your own games. The answers are supplied in Appendix C.
GamePadState
enumeration?GetAlbumArt
return?By now, you should be feeling very confident about writing games for your Zune. This chapter took you through several different exercises that demonstrated how to handle input, play music, debug, and create Windows copies of Zune games. You were even introduced to collision detection, vector math, simple game state management, and more.
In the next chapter, you will learn how to make Zune game development an even easier experience using advanced game state management, game components, and different types of collision detection.
18.225.8.35