In this and the next chapter, we cover programming with the XNA Framework. With Windows Phone 7.5, XNA Framework development was limited to game development for the most part. You can share the nonvisual APIs between the XNA Framework and Silverlight, but you could not share rendering. In Windows Phone 7.5.5, this changes with the XNA Framework shared graphics manager, which supports blended rendering of XNA Framework and Silverlight content. We cover blended rendering at the end of this chapter.
If you jumped straight to this chapter in the book, first please review the extensive introduction to XNA Game Studio development in Chapter 1. Chapter 3 focuses on user input. It also contains several examples based on XNA Game Studio development related to touch and accelerometer input. In Chapter 7, we discussed how to work with the XNA Media Library APIs, playing music, displaying image art, and the like. I recommend reviewing those chapters prior to diving in here.
In general, it is impossible to cover the full range of game development in just two chapters, so we take an optimized approach that builds on existing samples available at Microsoft’s AppHub website, which is located at http://create.msdn.com with the primary goal of helping Silverlight developers learn enough XNA to bring it into their Silverlight applications as well as get started with game development if desired. There are quite a few samples based on the XNA Framework that we leverage in this chapter to jumpstart game development. Why reinvent a game screen or menu framework when there are tried and true samples that we can leverage while covering key concepts of game development?
Note The AppHub samples are available under the Microsoft Permissive License (Ms-PL) license, which you can download and review at http://create.msdn.com/downloads/?id=15.
I’m not a lawyer, so I won’t provide an opinion on the license—but please do download and read it.
A hot topic of interest to indie game developers is access to Xbox LIVE gamer services. Xbox LIVE gamer services provide support for matchmaking, Achievements, Leaderboards, and the like to Xbox games. You can find more information here:
http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.gamerservices.aspx
On Windows Phone, a subset of Xbox LIVE gamer services is available to Xbox LIVE publishers. Example Xbox LIVE game publishers are Microsoft Game Studios, Electronic Arts, and Gameloft. Generally, a game developer works with an existing Xbox LIVE game publisher to ship a game that includes Xbox LIVE gamer services. It is difficult for a new game developer to immediately become an Xbox LIVE game publisher. It is a high bar; just look at the names of existing publishers.
So for an indie game developer, create an awesome game or possibly a fully fleshed-out concept as the first step to gain access to Xbox LIVE gamer services. The next step is to get your game in front of an existing Xbox LIVE game publisher. Microsoft Game Studios currently has an e-mail alias to which you can send proposals: [email protected]
. The other way to get noticed is to submit your finished game to AppHub and climb the popularity charts. An existing proven success as an indie publisher will help you get noticed.
Don’t lose hope if you have your heart set on Xbox LIVE gamer services. Initially, the services were not available to Xbox Indie game publishers as part of create.msdn.com, but the services were eventually made available to indie game publishers on big Xbox. One can hope that in the future more Xbox LIVE gamer services become available to indie developers on Windows Phone 7.5 as well.
In the next section we will begin a journey of taking the HelloXNA project from Chapter 1 and turn it into a full-fledged game with a menu system, score keeping, options, tombstone support, and the like—all of the things you need to consider when building a game for Windows Phone 7.5.
Back in Chapter 1, we covered XNA Game Studio development fundamentals in the HelloXNA
sample. The sample covered game loop basics with a very simple UI that had a hero ship that shot a missile at the alien attacker ship. As you recall, XNA is not event driven. Rather, it checks status on objects and input (polls) in a type loop that tries to ensure that the screen is drawn at 30 frames/second. Frame rate can drop if a game spends too much time “thinking” when updating objects. Here is a summary of how this works:
while (Game.IsRunning)
{
Update(); //Perform calculations, update objects, handle input
Draw(); //Draw background, objects, effects
}
The HelloXNA
sample from Chapter 1 isn’t really much of a “game,” because there wasn’t any interactivity. It essentially loaded up content and initialized state in the Game.LoadContent
method. The Update
method did some screen bounds checking and reset objects when they flew off the screen, and then the objects were rendered in the Draw
method. In the HelloXNA
sample, you cannot move the hero ship, and the one attacker appears on the screen at the same place. In this section we will turn it into a “real” game with a menu, score, movable hero ship, and more worthy opponents in the alien attackers.
In the next section, we turn the HelloXNA
game into a real game in the Chapter 8 AlienShooter
sample project. The AlienShooter
game will include a main menu, options menu, and actual game play. We introduce a Game Management sample that provides a ScreenManager
class, a Menu
class, and GameScreen
objects to manage game functions. The game is modularized into individual screen classes, so you can have a screen that shows the main menu, a screen that shows the options for the game, and so on, as well as a screen that actually handles game play. By following these best practices you give your game a professional look and feel, and make it more maintainable. In subsequent sections we cover how to add basic animations and effects to provide more interesting game play.
This section covers how to add the game management and screens to the AlienShooter sample project. We start by creating the Chapter 8 code solution with a project named AlienShooter
based on the XNA Game Studio 4.0 Windows Phone 7.5 Game (4.0) project template. The best way to start creating a “real” game is to base it on the Game State Management sample, which includes a main menu, an options screen, some gameplay, and a pause menu. It displays a loading screen between menus and gameplay with a popup message box to confirm that a user intends to quit. You can download the sample here:
http://create.msdn.com/en-US/education/catalog/sample/game_state_management
Tip Right-click on the .zip and select “unblock” to ensure that security checks don’t prevent you from being able to compile and run the application.
When you deploy the GameStateManagementSample (Phone) sample, notice that the game does not show up in the Games Hub, but is instead in the application list. The Genre
attribute on the App element in the WMManifest.xml file determines where an application appears. The default is Genre="Apps.Normal"
so that the application appears in the Application List. Change the genre attribute to Genre="Apps.Games"
to have the game show up in the Games Hub.
Run the GameStateManagementSample (Phone)
sample project to see the nice animations and transitions available with this sample. Let’s get started by transitioning the AlienShooter
sample to this format and then we will add in the code from Chapter 1’s sample named HelloXNA
. From there we will refactor to make the game more interactive.
Tip Close the Toolbox tool window to give yourself more space when working with XNA Game Studio games, since there aren’t components relevant in the Toolbox tool window.
With both the AlienShooter
and GameStateManagementSample (Phone)
sample projects open, we first create two new folders in AlienShooter named Screens
and GameManagement
. Copy the contents of the ScreenManager
folder in the GameStateManagementSample (Phone)
sample project to the GameManagement
folder in the AlienShooter
project. Copy the Screens
folder content to the Screens
folder content in the AlienShooter
project.
Once everything is copied over, select the option to Show All Files at the top of the Solutions Tool Window in Visual Studio. The next step is to right-click the files in Visual Studio and select Include In Project to add them to the AlienShooter
project. Be sure to fix up the namespaces by changing them from GameStateManagement
to AlienShooter.GameManagement
for the GameManagement
folder content and to AlienShooter.Screens
for the Screens
folder content. You will also need to add a using AlienShooter.GameManagement;
statement to all of the code files in the Screens folder as well. After everything is fixed up, the application should compile without any errors or warnings.
Next open the GameStateManagementSample (Phone) Content
project folder and, using Windows Explorer, copy over all of the content files to the AlienShooterContent
project folder except for the Content.contentproj
file. As before, select the option to Show All Files in Visual Studio 2010, and then include the files into the AlienShooterContent
project folder. We decide to split the content between GameManagement
content and actual game content. Create a new folder in the AlienShooterContent
project named GameManagement
and copy menufont.spritefont
and blank.png
into the AlienShooterContent
project GameManagement
folder. The rest of the content is related to game play, which we will eventually replace anyway.
Next we go through the GameStateManagementSample (Phone)
project’s Game1.cs
and modify the AlienShooter Game1.cs
file to match the game state sample’s Game.cs
file. First, rename the Game1 class to AlienShooterGame
and rename the code file Game1.cs
to AlienShooterGame.cs
. You will notice right away that the default spriteBatch
private variable is not present in the game state sample, which suggests that the Game1.Draw
and Game1.Update
methods must not perform any game logic in this code file. Instead, there is a private field variable named screenManager
of type ScreenManager
that is added. Add a using AlienShooter.GameManagement;
statement to the top of AlienshooterGame.cs
as well.
We next move to the Constructor AlienShooterGame()
and modify it to match the game state sample’s GameStateManagementGame
constructor as shown in Listing 8-1.
public AlienShooterGame()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
TargetElapsedTime = TimeSpan.FromTicks(333333);
// You can choose whether you want a landscape or portrait
// game by using one of the two helper functions defined below
InitializePortraitGraphics();
// InitializeLandscapeGraphics();
// Create the screen manager component.
screenManager = new ScreenManager(this);
Components.Add(screenManager);
// Attempt to deserialize the screen manager from disk. If that
// fails, we add our default screens.
if (!screenManager.DeserializeState())
{
// Activate the first screens.
screenManager.AddScreen(new BackgroundScreen(), null);
screenManager.AddScreen(new MainMenuScreen(), null);
}
}
The code file defines two helper functions that set whether the game is a portrait or landscape game. Listing 8-2 has the code for the two helper functions.
private void InitializePortraitGraphics()
{
graphics.PreferredBackBufferWidth = 480;
graphics.PreferredBackBufferHeight = 800;
}
private void InitializeLandscapeGraphics()
{
graphics.PreferredBackBufferWidth = 800;
graphics.PreferredBackBufferHeight = 480;
}
The next bit of code in Listing 8-1 for the AlienShooterGame
constructor creates the ScreenManager
and then adds it to the Components
collection for the Microsoft.Xna.Framework.Game
base class. Component classes inherit from the Microsoft.Xna.Framework.DrawableGameComponent
class, which attempts to modularize game development by providing the ability to have objects Update
and Draw
themselves without forcing all of that logic into the default Game.Draw
and Game.Update
methods. The ScreenManager
class inherits from DrawableGameComponent
and handles all of the actual Draw
and Update
logic for the game. I cover the ScreenManager
class in detail in just a bit, so let’s continue updating our AlienShooterGame
(formerly Game1.cs
) code file to get our game up and running. The last section of the AlienShooterGame
constructor attempts to load the ScreenManager
instance from disk. If it cannot, it loads the default screens to start the game.
The code overrides Game.OnExiting
to serialize game state to disk in this event handler to set up state for the next time the game launches.
protected override void OnExiting(object sender, System.EventArgs args)
{
// serialize the screen manager whenever the game exits
screenManager.SerializeState();
base.OnExiting(sender, args);
}
Unlike in Chapter 1, where all of our code was packed into Game1.cs
, AlienShooterGame.cs
does not perform any code in Update
or in Draw
. For our new version of the application, we use the ScreenManager
to do the work for us, so we remove the LoadContent
, UnloadContent
, and Update
methods from AlienShooterGame.cs
. We modify Game.Draw
to set the background color to Color.Black
from Color.CornflowerBlue
but otherwise this completes our edits for this file.
In order to test our work, we need to modify where GameManagement
content is loaded from; remember that we placed that content inside a GameManagement
folder in the AlienShooterContent
project. Open up the ScreenManager
class and find the LoadContent
override. Modify the load locations like this:
font = content.Load<SpriteFont>("GameManagement/menufont");
blankTexture = content.Load<Texture2D>("GameManagement/blank");
Once these edits are completed, test and run and the AlienShooter
project. It should work just like the GameStateManagementSample (Phone) sample
project with a bit of code and content reorganization under the covers. You can choose to follow these steps by hand in your project or simply grab the AlienShooter
project and rename the namespaces, game class, and screen classes to match your application. In the next section we go through the Game Management–related classes located in the GameManagement
folder to help you understand what’s going on under the covers so that you can further customize the game to meet your needs.
The GameManagement
folder contains three class files, GameScreen.cs
, InputState.cs
, and ScreenManager.cs
. The ScreenManager
class orchestrates loading GameScreen
objects and transitions between GameScreen
objects for you. The GameScreen
abstract base class contains all of the underlying plumbing code to plug into the Game Management framework and is the base class for objects declared in the Screens
folder. The InputState
class collects Keyboard
, Gamepad
, and TouchPanel
inputs. For Windows Phone 7.5, TouchPanel
input is primary, though some devices support a keyboard as well. Figure 8-1 shows the class diagram for the Game Management classes.
The ScreenManager
class is a DrawableGameComponent,
while the GameScreen
base class is not. The ScreenManager
manages one or more screen classes that inherit from GameScreen
. ScreenManager
maintains a stack of screens, calls their Update
and Draw
methods at the appropriate times, and automatically routes input to the top most active screen.
You will notice that DrawableGameComponent
looks very similar to the Game
class. There are method overrides available for LoadContent
, UnloadContent
, Update
, and Draw
, much like the Game
class. Under the covers the XNA Framework keeps the main Game
class and available DrawableGameComponents
in sync with respect to the Update
and Draw
methods, so that the developer can just focus on each individual component and screen.
In the ScreenManager.Draw
and ScreenManager.Update
methods, each screen is given an opportunity to process input, and either Update
or Draw
itself if it is the active screen. The ScreenManager
’s framework provides nice transition support for fade-in and fade-out of screens as the user navigates the game. If you like the way things work as is, you can just focus on your menu, options, game play screen, and so on, and the framework takes care of the rest.
Now that we have covered the game management plumbing provided by the ScreenManager
class, we will switch focus to the UX aspects of screens and menu. There are several screens in the project that were copied over from the Game Management sample that provide the starter UI. We go through these screens in this section.
The BackgroundScreen
class sits behind all of the other menu screens. It draws a background image that remains fixed in place regardless of whatever transitions that other screens may implement. The content that is loaded is the background.png. Swap the background.png file with your default background image.
The MainMenuScreen
class is the first screen that is loaded by the game. It displays the main menu items of Play and Options. The OptionsMenuScreen
class is displayed over the main menu screen. It provides the user a chance to configure the game. Figure 8-2 shows the default Main Menu and Options Menu screens.
The menu items have a nice Metro transition when flying in and out. Individual menu items are still graphic items with additional logic to detect a touch and fire an event. The MenuEntry
class provides menu item functionality. Here is the constructor for the OptionsMenuScreen
class:
public OptionsMenuScreen()
: base("Options")
{
// Create our menu entries.
ungulateMenuEntry = new MenuEntry(string.Empty);
languageMenuEntry = new MenuEntry(string.Empty);
frobnicateMenuEntry = new MenuEntry(string.Empty);
elfMenuEntry = new MenuEntry(string.Empty);
SetMenuEntryText();
// Hook up menu event handlers.
ungulateMenuEntry.Selected += UngulateMenuEntrySelected;
languageMenuEntry.Selected += LanguageMenuEntrySelected;
frobnicateMenuEntry.Selected += FrobnicateMenuEntrySelected;
elfMenuEntry.Selected += ElfMenuEntrySelected;
// Add entries to the menu.
MenuEntries.Add(ungulateMenuEntry);
MenuEntries.Add(languageMenuEntry);
MenuEntries.Add(frobnicateMenuEntry);
MenuEntries.Add(elfMenuEntry);
}
The MenuEntries
collection in the OptionsMenuScreen
constructor is a protected member of the MenuScreen
base class for the MainMenuScreen
and OptionsMenuScreen
classes. The MenuScreen.Draw
and MenuScreen.Update
methods measure and position menu entries for drawing to the screen. The MenuScreen
class includes the sliding transitions logic for menu items in case you want to modify it.
Click on a menu item to view the transition to a new screen. The loading screen inherits from the GameScreen
class coordinates transitions between the menu system and the game itself. Normally one screen will transition off at the same time as the next screen is transitioning on, but for larger transitions that can take a longer time to load their data; we want the menu system to be entirely gone before we start loading the game. This is done as follows:
This concludes the overview of the Game Management ScreenManager
, GameScreen
, MenuScreen
, and MenuEntry
classes. In the next section we dive in and start to customize the template game for the AlienShooter game.
In this section, we begin customizing the game management starter project with content and game logic. We start by adding the content from the original game in Chapter 1 to AlienShooter
. We copy over the Sprites
and Textures
folder and remove background.png
, which is the generic XNA Framework sample image.
When you create a new XNA project, it always creates two projects, one project for your game and the other for your content. The Content project provides compile-time processing for game assets, including detecting missing assets, asset conversion, compression, and preprocessing. The key component to the Content project is the Content Pipeline.
The content pipeline does the work of preparing assets for use within an XNA Framework game. Assets are processed in two steps. The first step is importing the content. For the majority of formats, XNA has Content Importer
classes to convert content into a common intermediate format. The second step converts the imported content into a final compiled, compressed, format via a Content Processor
.
Importing content is as simple as adding new items to the Content project. In most cases, the content pipeline automatically detects the asset type and assigns the correct Content Importer
and Content Processor
. It is also possible to create custom Content Importer
and Content Processor
classes for assets not supported by the built-in classes.
In the Chapter 1 HelloXNA
sample we demonstrate how to load content a single texture at a time using this code from the LoadContent
method:
HeroShip = this.Content.Load<Texture2D>("Sprites/heroship");
SpaceShip = this.Content.Load<Texture2D>("Sprites/spaceship");
Missile = this.Content.Load<Texture2D>("Sprites/missile");
Each content item is loaded into a single texture. When drawn, each item requires its texture to be loaded by the GPU and rendered. The items just defined are drawn to the screen with these three lines of code in the Draw
method:
spriteBatch.Draw(SpaceShip, SpaceShipPosition, Color.White);
spriteBatch.Draw(Missile, MissilePosition, Color.White);
spriteBatch.Draw(HeroShip, HeroShipPosition, Color.White);
Loading each texture is a performance hit for the GPU. A better way is to load a single texture that contains all of the images, and then just tell Draw
which area of the texture to draw for that item. This type of texture is called a sprite sheet and is very common to use in game development, especially when creating sprite animation, which we will cover later in the chapter. There are two challenges with using sprite sheets: you have to take all of your individual images and mash them into a larger image, and you have to remember which part of the single large texture contains the particular image you want to draw to the screen.
Luckily AppHub comes to the rescue again with a ready-to-go custom Content Importer and Content Processor that can take a collection of images and automatically turn them into a single sprite sheet, solving the first challenge. For the complete background on the sample, download it here:
http://create.msdn.com/en-US/education/catalog/sample/sprite_sheet
The sample also includes a runtime class to include in your game named SpriteSheet, which
allows either named or indexed access to images so that you do not have to remember the exact pixel location, solving the second challenge listed previously.
We implement the SpriteSheet sample in AlienShooter by copying over the SpriteSheetPipeline and SpriteSheetRuntime (Phone) projects from the AppHub SpriteSheetSample
and adding them to the Chapter 8 Solution. Right-click the References
folder in the AlienShooterContent
project, select Add Reference, choose the Projects tab, and select the SpriteSheetPipeline
project. Next, right-click on the References folder in the AlienShooter
project and select Add Reference, choose the Projects tab, and select the SpriteSheetRuntime (Phone)
project.
In the AlienShooterContent
project, navigate to the Sprites
folder and exclude from the project these assets: heroship.tga
, missile.tga
, and spaceship.tga
. We don’t want to add the textures twice, so they should not be included as individual assets any more. Next, right-click on the Sprites
folder and select Add New Item and choose XML File with a name of AlienShooterSpriteSheet.xml
. Listing 8-3 shows the edited XML file.
<?xml version="1.0" encoding="utf-8" ?>
<XnaContent>
<Asset Type="System.String[]">
<Item>Sprites/heroship.tga</Item>
<Item>Sprites/missile.tga</Item>
<Item>Sprites/spaceship.tga</Item>
</Asset>
</XnaContent>
If you don’t edit the file correctly, you will get a compile error if the SpriteSheetPipeline
Content Processor cannot find an asset listed in the XML file. Notice in this example that the folder location is included relative to the AlienShooterContent
project to correctly identify the asset location. Figure 8-3 shows the Solution tool window with the AlienShooterContent
project expanded and the Properties dialog for the AlienShooterSpriteSheet.xml
.
Notice in Figure 8-3 that the individual sprite images are hidden and not part of the project in the Sprites folder, though they need to be accessible in the file system to the SpriteSheetProcessor
Content Processor so that it can turn them into a single sprite sheet. The only content that is part of the Sprites
folder is the AlienShooterSpriteSheet.xml
XML file. Also in Figure 8-3, notice that the custom SpriteSheetProcessor
Content Processor is configured for the XML file. Let’s now draw-update the GamePlay screen to start to render our new AlienShooter
assets. A using SpriteSheetRuntime
statement is added to the top of the GamePlay.cs
code file. Three new private fields are added at the top of the GamePlayScreen
class:
SpriteSheet alienShooterSpriteSheet;
Texture2D backgroundTexture;
Rectangle screenRect;
The alienShooterSpriteSheet
variable will let us render the entire sprite sheet to the screen for debug purposes. The backgroundTexture
variable represents our game background as with the HelloXNA sample and the screenRect
variable holds a variable that points to the Rectangle
object that is the size of the draw area.
The LoadContent
method is updated to load the alienShooterSpriteSheet
SpriteSheet
and backgroundTexture
Texture2D
variables:
alienShooterSpriteSheet = content.Load<SpriteSheet>("Sprites/AlienShooterSpriteSheet");
backgroundTexture = content.Load<Texture2D>("Textures/background");
Most of the code in the Update
and HandleInput
methods is commented out since we no longer want the placeholder code. The placeholder draw code is modified in the Draw
method, and the following two additional items are added to draw the backgroundTexture
and the alienShooterSpriteSheet
to the screen:
spriteBatch.Begin();
//Draw background
spriteBatch.Draw(backgroundTexture, screenRect, Color.White);
//Draw Sprite Sheet
spriteBatch.Draw(alienShooterSpriteSheet.Texture,
new Rectangle(screenRect.Width / 2, screenRect.Height / 2,
alienShooterSpriteSheet.Texture.Width / 2,
alienShooterSpriteSheet.Texture.Height / 2),
Color.White);
spriteBatch.End();
Everything should compile, and the game now shows AlienShooter
assets on the GamePlayScreen,
as shown in Figure 8-4.
Notice how tightly the Hero Spaceship, Missile, and Alien Ship are automatically packed in Figure 8-4 by the custom Content Processor. Just to close the loop, the code to draw an individual image from the AlienShooterSpriteSheet
is very simple. Here is the code to just draw the Hero Spaceship at a point in the middle of the screen:
spriteBatch.Draw(alienShooterSpriteSheet.Texture,
new Vector2((screenRect.Width / 2)-
alienShooterSpriteSheet.SourceRectangle("heroship").Width / 2,
(screenRect.Height / 2)-
alienShooterSpriteSheet.SourceRectangle("heroship").Height / 2),
alienShooterSpriteSheet.SourceRectangle("heroship"),
There is a verbose computation to create a Vector2
for the screen center minus the center of the Hero Ship, but otherwise it is the standard Draw(texture, Vector, SourceRectangle, Color)
method. Notice how easy it is to identify the Rectangle
on the alienShooterSpriteSheet
where the Hero Ship resides. Just pass in the asset name and the SpriteSheet
class will locate it. More on drawing later in the chapter, but I wanted to close out how to completely work with the SpriteSheet
class.
When you download the SpriteSheet
Sample from AppHub, it includes an example extension on how to add a compression option to the Content Processor. Unfortunately the extension does not work for the XNA Game Studio 4.0 Reach profile. It just works for the HiDef profile.
Tip The Reach profile limits XNA Game Studio to a subset of functions supported on Windows Phone, Xbox, and Windows for maximum compatibility. The HiDef profile supports only Xbox and Windows. It includes support for custom HLSL shaders as well as additional features.
We dive into creating a more robust game below, but first let’s cover some of the other constructs for a real game such as displaying game status text and menus, which is covered in the next section.
Displayed text, such as game status and menus, is an important component of game development. Users want a nice clean menu system to navigate. Game status is critical to game play; it makes the game interesting such as how much life is left, high score, rounds of ammunition available, and the like.
There are two methods to display text in the XNA Framework. The first is via the SpriteFont
class. The SpriteFont
class leverages the built-in Font classes on your PC to create the font texture to display letter character images when rendered. The SpriteFont
class is covered in the next section.
The other method to display text in the XNA Framework is with customized bitmap fonts. This method is more advanced, which means it is highly customizable but also requires additional work. To demonstrate working with fonts a new project named FontsSample
is added to the Chapter 8 solution.
The SpriteFont
class takes a built-in font and rasterizes it based on configuration parameters. To add a text Font for rendering in XNA Game Studio, right-click on the FontsSampleContent
project and select Add Item Sprite Font, and enter a name. Usually, you name the Sprite Font item the same as the Font Name. If you include multiple sizes append the size to the end, which will make more sense in a bit.
While we take fonts for granted when working in Microsoft Word or in Outlook, fonts are licensed content. You must have redistribution rights to include a font in your game. Search the Web for “purchase fonts online” and quite a few sites show up. Some include free fonts with others for purchase.
Luckily XNA Game Studio 4.0 includes a set of OpenTypefonts you can use in your games. Figure 8-5 shows the fonts that are licensed by Microsoft for your use. The image is taken from the AppHub education catalog.
For our sample, name the new Sprite Font item SegoeKeycaps24
and click Add. A new item named SegoeKeycaps16.spritefont
is added to the FontSampleContent
project. What may surprise you is that the SegoeKeycaps16.spritefont
file is an XML file. Listing 8-4 shows the contents of the newly added file.
<?xml version="1.0" encoding="utf-8"?>
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
<Asset Type="Graphics:FontDescription">
<FontName>Segoe UI Mono</FontName>
<Size>14</Size>
<Spacing>0</Spacing>
<UseKerning>true</UseKerning>
<Style>Regular</Style>
<!– <DefaultCharacter>*</DefaultCharacter> –>
<CharacterRegions>
<CharacterRegion>
<Start> </Start>
<End>~</End>
</CharacterRegion>
</CharacterRegions>
</Asset>
</XnaContent>
The XML file describes the parameters for what should be rendered when text is drawn using this SpriteFont. For our example, change the FontName
element to Segoe Keycaps
. Change the Size to 16 and leave everything else at the default values.
You can edit the Spacing
element to increase the space between characters, as well as modify the UseKerning
item to modify how the font is laid out. Kerning adjusts the spacing between characters to be more pleasing to the eye. An example of kerning is when a capital “W” overhangs a neighboring “e” slightly when typing the word “Well.” Without kerning, letters do not overhang or underhang as the case may be. The Style
element indicates whether it should be Regular
, Bold
, or Italic
.
The last element to discuss is the CharacterRegions
element, which controls what letters are available in the font. The default range is 32 (ASCII space) to 126 (ASCHII ‘~’). This reduces the number of characters rasterized as part of the content import process.
To draw the SegoeKeycaps16
Sprite Font, add a new private member named segoeKeycaps16 of type SpriteFont
to the Game1
class in the FontSample
project. Next add this line of code to the Game1.LoadContent
method:
segoeKeycaps16 = Content.Load<SpriteFont>("SegoeKeycaps16");
We used the SpriteBatch.Draw
method to draw textures to the screen. The SpriteBatch.DrawString
method has six overloads to provide flexibility in drawing text to the screen. Here are a couple of examples:
spriteBatch.DrawString(segoeKeycaps24, text, new Vector2(24, 130), Color.Yellow);
spriteBatch.DrawString(segoeKeycaps24, text, new Vector2(24, 450), Color.Orange,
-.25f * (float)Math.PI, Vector2.Zero, 3, SpriteEffects.None, 0);
You can tinge the font color by adjusting the fourth parameter with a color value such as Color.Yellow
. The second DrawString
call above applies a negative 45 degree rotation in the fifth parameter and a 300 percent scale in the sixth parameter, with an orange tinge applied to the Sprite Font. Another font is added to the project named Miramonte
with Italics style, and a few more DrawString
calls are in the code, but Figure 8-6 shows the results.
The scaled font renders pretty well but you can see some stretching in the letter “e” in Figure 8-6 that could be more obvious with other fonts. In the next section we cover how to create a bitmap font.
Bitmap fonts are not dependent on a built-in font. Instead, the fonts are loaded from a texture similar to what is shown in Figure 8-7.
The example in Figure 8-7 was copied from MSDN by zooming in the browser by 200% and taking a screenshot, but you can create a custom bitmap font texture using any drawing program by following the conventions specified in the documentation.
The character order matters, with a space as the upper-left character. You can use a solid white or black background color for monochrome characters. In the previous example, black is used as the background color. Multicolored characters are supported with an alpha channel.
The space between characters must be filled with Magenta color (Red:255 Green:0 Blue:255 Alpha:255). The FontTextureProcessor
class is applied to the texture instead of the default processor. The FontTextureProcessor
will pack the characters in as close as possible so you don’t have to worry about exact spacing between characters.
A new project named BitmapFontSample
is added to the Chapter 8 solution. The bitmap font texture in Figure 8-7 is added to the BitmapFontSampleContent
project with a name of BitmapFont.bmp
. The Content Processor is configured to Sprite Font Texture - XNA Framework
from the default of Texture - XNA Framework
.
The SpriteFont
class is still used as the member type for the bitmapFont object and the bitmap font is loaded in LoadContent
just like for a regular SpriteFont
content:
SpriteFont bitmapFont;
…//Load Content
bitmapFont = Content.Load<SpriteFont>("BitmapFont");
Drawing is the same as well with the DrawString
method:
spriteBatch.Begin();
spriteBatch.DrawString(bitmapFont, text, new Vector2(24, 70), Color.White);
spriteBatch.End();
In summary, the primary differences when using a custom bitmap font is that you must draw out the characters correctly in the proper order and you configure the Content Processor
to Sprite Font Texture - XNA Framework
for the bitmap font texture.
The MenuScreen
class is part of the GameStateManagementSample (Phone) sample
project. It is the base class for the MainMenu.cs
and OptionsMenu.cs
screen objects that are part of the AlienShooter game project.
The MenuScreen
class takes advantage of a helper class named MenuEntry
, which draws the text and publishes a Selected event. The MenuEntry
class does not detect the touch. Instead, the MenuScreen
does most of the work to draw, detect a touch, and associate the touch with the correct MenuEntry
item. A good way to understand how this works is to look at the constructor for the MainMenuScreen
class:
public MainMenuScreen()
: base("Main Menu")
{
// Create our menu entries.
MenuEntry playGameMenuEntry = new MenuEntry("Play Game");
MenuEntry optionsMenuEntry = new MenuEntry("Options");
// Hook up menu event handlers.
playGameMenuEntry.Selected += PlayGameMenuEntrySelected;
optionsMenuEntry.Selected += OptionsMenuEntrySelected;
// Add entries to the menu.
MenuEntries.Add(playGameMenuEntry);
MenuEntries.Add(optionsMenuEntry);
}
The MainMenuScreen
class creates the menu entries and associates event handlers with the menu entries. The base class MenuScreen
handles the animation and positioning. A developer can customize the animation and layout by modifying the base MenuScreen
class.
One way to customize the menu screens is to change the Font in the AlienShooterContent
project. Open the /GameManagement/menufont.spritefont
file and change the FontName
element to Quartz MS listed in Figure 8-5 and change the Size
to 24. The font has a science fiction look to it, which suites a game named AlienShooter pretty well.
We also modify the GamePlayScreen
class to have Score and Lives text across the top with a SlateBlue
color background. This is achieved via the SpriteBatch.DrawString
method as well as adjusting the background color to SlateBlue
and drawing the backgroundTexture
34 pixels lower via the added backgroundPosition
Vector2 object. Figure 8-8 shows both the updated menu screen and game play screen.
When you run the project on a device, the animations and transitions look pretty nice. These can certainly be customized as well once you understand how to create animations and transitions in XNA Game Studio. We cover both topics in detail to create the actual game play.
In this section we focus on building out the game. Remember in Chapter 1 that we drew all of the objects directly in the Game1.cs
class. In this chapter we will build up an object hierarchy to add better organization to the game structure. We start with an explanation of how sprite animation works so that we can then encapsulate the work in a GameObject
class.
In this section we cover spite animation, which is the technique that brings a 2D game to life. Sprite animation is a matter of showing frames or individual sprites at set intervals to give the illusion of motion, no different than flipping a drawing book that has pictures of a stick man running. Each picture is static, but when drawn in the correct order with the right position, the stick man appears animated when you flip through the images.
A new project named SpriteAnimation
is added to the Chapter 8 solution. Project references to the SpriteSheetRuntime
and SpriteSheetPipeline
are added to the SpriteAnimation
and SpriteAnimationContent
projects respectively in order to take advantage of the automatic sprite sheet creation and easy sprite access via file name within the sprite sheet without having to remember coordinates or the math to track frames.
The sprite sheet is created from the AlienShooter textures. For the hero ship, we want to add rocket engine exhaust animations as well as for the missile. For the alien spaceship, a bit of glow is added underneath that shimmers and some tilt to the left and right is added to the ship. To create these effects I fired up Paint.NET and created 10 sprites for each object, varying the patterns enough to make cartoonish flame for the hero ship and missile as well as the glow and tilt for the spaceship. Figure 8-9 shows the individually edited sprite files in the file system.
The files are added to the Sprite Sheet XML file as shown in Listing 8-5.
<?xml version="1.0" encoding="utf-8" ?>
<XnaContent>
<Asset Type="System.String[]">
<Item>Sprites/heroship0.tga</Item>
<Item>Sprites/heroship1.tga</Item>
<Item>Sprites/heroship2.tga</Item>
<Item>Sprites/heroship3.tga</Item>
<Item>Sprites/heroship4.tga</Item>
<Item>Sprites/heroship5.tga</Item>
<Item>Sprites/heroship6.tga</Item>
<Item>Sprites/heroship7.tga</Item>
<Item>Sprites/heroship8.tga</Item>
<Item>Sprites/heroship9.tga</Item>
<Item>Sprites/spaceship0.tga</Item>
<Item>Sprites/spaceship1.tga</Item>
<Item>Sprites/spaceship2.tga</Item>
<Item>Sprites/spaceship3.tga</Item>
<Item>Sprites/spaceship4.tga</Item>
<Item>Sprites/spaceship5.tga</Item>
<Item>Sprites/spaceship6.tga</Item>
<Item>Sprites/spaceship7.tga</Item>
<Item>Sprites/spaceship8.tga</Item>
<Item>Sprites/spaceship9.tga</Item>
<Item>Sprites/missile0.tga</Item>
<Item>Sprites/missile1.tga</Item>
<Item>Sprites/missile2.tga</Item>
<Item>Sprites/missile3.tga</Item>
<Item>Sprites/missile4.tga</Item>
<Item>Sprites/missile5.tga</Item>
<Item>Sprites/missile6.tga</Item>
<Item>Sprites/missile7.tga</Item>
<Item>Sprites/missile8.tga</Item>
<Item>Sprites/missile9.tga</Item>
</Asset>
</XnaContent>
The Content Processor is configured to SpriteSheetProcessor, which instructs the Content Pipeline to collect the individual files and mash them together into a single texture, as shown in Figure 8-10.
Personally, I would find it tedious to hand-create a sprite texture as shown in Figure 8-10, and prefer drawing individual images and letting the SpriteSheet
sample code do the hard work for me. Once you see how easy it is to animate sprites using this method, I think you will agree.
Drawing the frames so that it animates between the images is pretty straightforward. In the Game1 class for the SpriteAnimation sample, the following private fields are declared:
SpriteSheet SpriteAnimationSpriteSheet;
int spriteIndex = 0;
Rectangle screenRect;
TimeSpan timeToNextFrame = new TimeSpan();
TimeSpan frameTime = TimeSpan.FromMilliseconds(50d);
The spriteIndex
variable is used to append a number from 0 to 9 to the sprite names of heroship
, missile
, and spaceship
. Note in Figure 8-9 and Listing 8-5 how the sprite images are named. Incrementing spriteIndex
steps over to the next sprite by name shown visually in Figure 8-10.
The timeToNextFrame
field is used to sum elapsed time for the game. The frameTime
field stores how often the spriteIndex
should change over to the next frame. Here is the code from Game1.Update that performs this calculation:
protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
// TODO: Add your update logic here
// Add elapsed game time between calls to Update
// Once enough time has passed, i.e. timeToNextFrame > frameTime
//increment sprite index.
timeToNextFrame += gameTime.ElapsedGameTime;
if (timeToNextFrame > frameTime)
{
if (spriteIndex < 9)
spriteIndex++;
else spriteIndex = 0;
frameElapsedTime = TimeSpan.FromMilliseconds(0d); }
base.Update(gameTime);
}
Essentially, the frameTime
variable is how long any given frame is displayed. If you want the frames to animate more slowly, increase the value of the frameTime
variable, currently set to 50.
Drawing a frame is pretty straightforward as well. Here is the code to draw the center of the hero ship at the center of the screen:
spriteBatch.Draw(SpriteAnimationSpriteSheet.Texture,
new Vector2((screenRect.Width / 3) -
SpriteAnimationSpriteSheet.SourceRectangle("spaceship" + spriteIndex.ToString()).Width / 2,
(screenRect.Height / 3) -
SpriteAnimationSpriteSheet.SourceRectangle("spaceship" + spriteIndex.ToString()).Height / 2),
SpriteAnimationSpriteSheet.SourceRectangle("spaceship" + spriteIndex.ToString()),
Color.White);
The previous code takes the entire sprite sheet and uses this parameter for the source rectangle to select which sprite to draw:
SpriteAnimationSpriteSheet.SourceRectangle("spaceship" + spriteIndex.ToString())
You can only see the actual animation by running the code, but Figure 8-11 provides a snapshot.
We copy over all of the heroship
, alienship
, and missile
images and SpriteSheet
XML file to the AlienShooterContent
content project’s Sprites
folder so that we can leverage the assets we created in our primary game.
Now that you understand how to animate sprites, we can move forward and create a GameObject
class that handles the animation logic, as well as other object state allowing the code to focus more on game play and let the GameObject
class handle rendering and state management.
Remember from our ScreenManager
coverage that the actual game functionality exists in the GameplayScreen
class. The rest of this chapter focuses on the code in the GameplayScreen
class that manages the game objects as well as the game object code itself. The goal is that you come away with a solid understanding of how to go about creating your own games.
We create a class named GameObject
to be our base class for game assets, which includes the hero ship, the missiles, and the alien ships. The GameObject
class handles the animation, Update, and Drawing for each object. We copied over the assets and sprite sheet logic from the SpriteAnimation
sample project. Here is the GameObject
constructor:
public GameObject(SpriteSheet loadedTexture, string spriteName, Rectangle screenRect)
{
SpriteAnimationSpriteSheet = loadedTexture;
SpriteCenter = new Vector2(
SpriteAnimationSpriteSheet.SourceRectangle(spriteName + 0).Width / 2,
SpriteAnimationSpriteSheet.SourceRectangle(spriteName + 0).Height / 2);
//Used to access sprite in SpriteSheet
//Assume starts at 0 so SpriteName+0 is first Sprite frame for animation
//NumberOfAnimationFrames is how many sprite frames are available
SpriteName = spriteName;
_screenRect = screenRect;
//Default initialization
FrameTime = TimeSpan.FromMilliseconds(100d);
NumberOfAnimationFrames = 10;
Position = Vector2.Zero;
ElapsedFrameTime = TimeSpan.FromMilliseconds(0d);
Velocity = Vector2.Zero;
Rotation = 0f;
Alive = false;
}
The constructor for GameObject takes the following parameters:
SpriteSheet loadedTexture
string spriteName
Rectangle screenRect
Just as with the SpriteAnimation
sample, all of the individual object frames are combined into a single texture shared by all of the objects, which is more efficient than loading individual textures and switching textures when rendering. The loadedTexture
parameter represents the single texture and is passed in to the constructor. The spriteName
parameter is used by the animation code so that the correct object frames can be found in the loadedTexture
. This code assumes a naming convention starting at spriteName+0
through spriteName+NumberOfAnimationFrames
, which is hard-coded to 10 frames for all objects. The screenRect
parameter is used to check when objects collide with screen bounds.
The GameObject
class has quite a few public properties declared that are used to animate the sprite and to hold information on Sprite such as the center point, position, velocity and rotation. Here are the declarations:
public SpriteSheet SpriteAnimationSpriteSheet { get; set; }
public string SpriteName { get; private set; }
public int NumberOfAnimationFrames { get; set; }
public TimeSpan FrameTime { get; set; }
public TimeSpan ElapsedFrameTime { get; set; }
public Vector2 SpriteCenter { get; set; }
public bool Alive { get; set; }
public Vector2 Position { get; set; }
public Vector2 Velocity { get; set; }
public float Rotation { get; set; }
There is an additional property related to collision detection that we will cover next.
The BoundingRect
property of type Rectangle
is used to return the Rectangle
area that contains the sprite on screen. This property is used for collision detection. If you have ever played a video game where the objects seemed to touch but nothing happen, it is a result of imperfect collision detection, as shown in Figure 8-12 where a regular shaped object, like a rectangle, is used to define the area of an irregularly shaped object.
There are more than a few algorithms for sprite collision detection that can be found in books focused on the topic that range in accuracy from bounding rectangles to resource-intensive point by point comparison. Probably the best answer lies somewhere between those extremes, such as using more than one bounding box or only performing point-by-point comparison on sides that could collide, and so on. For our purposes, we check for intersection using the BoundingRect
property defined in GameObject
:
public virtual Rectangle BoundingRect
{
get
{
return new Rectangle((int)Position.X, (int)Position.Y,
SpriteAnimationSpriteSheet.SourceRectangle(SpriteName + 0).Width,
SpriteAnimationSpriteSheet.SourceRectangle(SpriteName + 0).Height);
}
}
One trick to that you can use to adjust collision detection is to call the Inflate(horizontalAmount,verticalAmount)
method to individually increase or decrease (with a negative value) the sides of the Rectangle
to better match the object shape.
The rest of the GameObject
class contain its individual methods to reset its position if the object flies off the screen, update the object state, and draw the object to screen. Methods are marked with the virtual
keyword so that they can be overridden in inherited classes as needed:
public virtual void ResetGameObject()
{
Position = Vector2.Zero;
Velocity = Vector2.Zero;
Alive = false;
}
public virtual void Update(GameTime GameTime)
{
if (Alive)
{
Position += Velocity;
//Check screen bounds
if ((Position.X < 0) ||
(Position.X > _screenRect.Width) ||
(Position.Y < 0) ||
(Position.Y > _screenRect.Height))
ResetGameObject();
//Update animation
UpdateAnimation(GameTime);
}
}
private void UpdateAnimation(GameTime gameTime)
{
ElapsedFrameTime += gameTime.ElapsedGameTime;
if (ElapsedFrameTime > FrameTime)
{
if (_spriteIndex < NumberOfAnimationFrames - 1)
_spriteIndex++;
else _spriteIndex = 0;
ElapsedFrameTime = TimeSpan.FromMilliseconds(0d);
}
}
public virtual void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
if (Alive)
{
spriteBatch.Draw(SpriteAnimationSpriteSheet.Texture, Position - SpriteCenter,
SpriteAnimationSpriteSheet.SourceRectangle(SpriteName + _spriteIndex.ToString()),
Color.White);
}
}
The ResetGameObject
is called when an object is “destroyed” or flies off the screen. The method is pretty simple in just setting Position
and Velocity
to a zero vector and Alive
to false.
The Update
method checks to see if the object is Alive
before updating Position
by adding Velocity
to it and then checking screen bounds. The Update
method also calls the UpdateAnimation
method, which leverages the code we developed in the SpriteAnimation
sample. Finally, the Draw
method simply applies the same logic we used in the SpriteAnimation
sample to draw the correct frame to screen as part of the animation sequence.
Now that we have our base class out of the way, we move on to cover the enemy alien class and then move on to the user-controlled hero ship and missile classes.
The AlienGameObject
class that represents the frenzied aliens is remarkably simple. The class inherits from the GameObject
class and uses the inherited constructor with some modifications:
public AlienGameObject(SpriteSheet loadedTexture,
string spriteName, Rectangle screenRect)
: base(loadedTexture, spriteName, screenRect)
{
Alive = true;
ResetGameObject();
}
The Alive
property is set to true because we want enemies to keep dropping. There is a customized ResetGameObject
method that overrides the base class version as well:
public override void ResetGameObject()
{
//Randomize animation
_spriteIndex = (int)(randomNumber.NextDouble() * NumberOfAnimationFrames);
//Randomize initial position
Position = new Vector2(randomNumber.Next(_screenRect.Width), 35);
//Apply default alien speed
Velocity = new Vector2(randomNumber.Next(alienVelocityArc), alienSpeed);
Alive = true;
}
The alien spaceship has a unique shape. For better collision detection we override
the BoundingRect
property and call Rectangle.Inflate(0,2);
to inflate the Rectangle
on the Y axis resulting in much better collision detection for this game:
public override Rectangle BoundingRect
{
get
{
Rectangle rect = new Rectangle((int)Position.X, (int)Position.Y,
SpriteAnimationSpriteSheet.SourceRectangle(SpriteName + 0).Width,
SpriteAnimationSpriteSheet.SourceRectangle(SpriteName + 0).Height);
rect.Inflate(0,20);
return rect ;
}
}
That’s it for this class. All of the lively action is a result of the animation code handled by the base class and the ResetGameObject
method. This method does a couple of things:
Position
Vector2
to start the drop.Figure 8-13 shows an army of aliens invading.
You will want to play on a real device to get a full sense of the game, but as you can see in Figure 8-13, the aliens strike different drop lines and fall fairly randomly for such little code. The animation code helps to keep things lively as well. In the next section, we cover the hero ship class, which takes accelerometer and touch input to move and fire missiles at the invasion.
We create another class named UserGameObject
that inherits from the GameObject
class. This class represents the player, which is the Hero Ship in AlienShooter
, which makes it unique compared to the other classes since it takes user input in the form of accelerometer and touch. The accelerometer input will move the ship left or right by tilting the phone. It has a window so that if the phone is relatively flat the ship does not move. Touching the screen will fire missiles.
By default, the Game Management sample does not include support for accelerometer input. The GamePlayScreen class has a HandleInput
method that takes a parameter of type InputState
, which is a class that captures keyboard, controller, and touch input. It does not capture accelerometer input by default. In the source code for the book, you will find a modified InputState class, as well as a new class named AccelerometerState
, which is shown in Listing 8-6.
public class AccelerometerState
{
public double X;
public double Y;
public double Z;
public DateTimeOffset Timestamp;
}
The modifications to the InputState
class are pretty straightforward:
//robcamer - Add a private instance of AccelerometerState
public AccelerometerState CurrentAccelerometerState { get; private set; }
//robcamer - Add private field for the accelerometer
private Accelerometer accelerometer;
The constructor for the InputState
class is modified to instantiate the previous two private variables, as well as add an event handler to the Accelerometer
:
CurrentAccelerometerState = new AccelerometerState();
//Robcamer - initialize accelerometer
accelerometer = new Accelerometer();
accelerometer.ReadingChanged +=
new System.EventHandler<AccelerometerReadingEventArgs>(accelerometer_ReadingChanged);
accelerometer.Start();
The values of CurrentAccelerometerState
are updated in the accelerometer_ReadingChanged
event handler:
void accelerometer_ReadingChanged(object sender, AccelerometerReadingEventArgs e)
{
CurrentAccelerometerState.X = e.X;
CurrentAccelerometerState.Y = e.Y;
CurrentAccelerometerState.Y = e.Z;
CurrentAccelerometerState.Timestamp = e.Timestamp;
}
Now that we have the modifications in place for the InputState
class, two private fields related to user input are added to the UserGameObject class.
private AccelerometerState _accelerometerState;
private Vector2 _leftRightVector = new Vector2(5, 0);
The _accelerometerState
and _leftRightVector
fields are modified in the UserGameObject
.HandleInput
method.
public void HandleInput(InputState input)
{
//Must check for TouchLocationState as wel as Count
//Otherwise, FireMissile will be called twice
//Once for 'Pressed' and once for 'Released'
if ((input.TouchState.Count > 0) &&
input.TouchState[0].State == TouchLocationState.Pressed)
{
FireMissile();
}
_accelerometerState = input.CurrentAccelerometerState;
if (_accelerometerState.X > .1)
{
Velocity = _leftRightVector;
}
if (_accelerometerState.X < -.1)
{
Velocity = -_leftRightVector;
}
//near Zero tilt left or right so
//set velocity to zero
if ((_accelerometerState.X < .1) &&
(_accelerometerState.X > -.1))
Velocity = Vector2.Zero;
}
If the _accelerometerState
X
component is greater than .1, a positive or negative velocity is applied to the Velocity
vector via the leftRightVector
variable in the proper tilt direction. Likewise, if the user holds the phone close to neutral, Velocity
is set to zero. If you don’t like how it responds, play with the .1 value to see what feels good to you.
The other part of the HandleInput
session is to detect if the screen is touched. Each touch fires to TouchState
values, one for TouchLocationState.Pressed
and one for TouchLocationState.Released
. If the screen is touched the FireMissile
method is fired.
The UserGameObject class manages a collection of Missile
objects since they are closely associated with the hero ship:
public List<MissileGameObject> Missiles;
public int MaxNumberofMissiles;
The Missile
related properties are public properties because the GameplayScreen
will need to check collisions between missiles and enemy alien ships. Also, the number of available missiles could be adjusted dynamically as part of game play for a “blitz” mode, where the hero ship can fire more than three at a time as an example. The Missiles
collection is instantiated in the UserGameObject
constructor:
MaxNumberofMissiles = 3;
Missiles = new List<MissileGameObject>();
for (int i=0;i < MaxNumberofMissiles; i++)
Missiles.Add(new MissileGameObject(loadedTexture,"missile",screenRect));
When the screen is touched, the FireMissile
method code searches for an available missile; only three are available by default. It then sets the missile property to Alive
and the Missile
Position
property to the same Position
value for the hero ship / UserGameObject
class. Note that once the Missile
is set to Alive
, it automatically starts moving based on the default logic implemented in the GameObject
class.
The UserGameObject
class overrides the Update
method with custom code to position the spaceship correctly with user input. It also manages the Missiles
collection by calling Update
for missiles where Alive
is true; that is, they are in flight. Here is the UserGameObject.Update
method:
public override void Update(GameTime gameTime)
{
base.Update(gameTime);
if (Position.X < SpriteCenter.X)
Position = new Vector2(SpriteCenter.X,Position.Y);
if (Position.X > (_screenRect.Width - SpriteCenter.X))
Position = new Vector2(_screenRect.Width-SpriteCenter.X,Position.Y);
for (int i = 0; i < MaxNumberofMissiles; i++)
{
if (Missiles[i].Alive == true)
Missiles[i].Update(gameTime);
}
}
The Draw
method is overridden as well:
public override void Draw(GameTime gameTime, Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch)
{
for (int i = 0; i < MaxNumberofMissiles; i++)
{
if (Missiles[i].Alive == true)
Missiles[i].Draw(gameTime, spriteBatch);
}
base.Draw(gameTime, spriteBatch);
}
The Draw
method checks to see if each Missile
is Alive
and then calls Draw
for the Missile
object. The last item in the UserGameObject
class to cover is the LoadContent
method. In this method, the ContentManager
instance is passed in so that the UserGameObject
can make a sound effect when a missile is fired from the hero ship. Here is the method:
public void LoadContent(ContentManager content)
{
_missileLaunchSoundEffect =
content.Load<SoundEffect>("SoundEffects/MissileLaunch");
}
XNA Game Studio has very rich audio mixing capabilities to support Dolby-quality sound. For our game the SoundEffect
class provides a quick and easy way to play audio during a game with the Play
method. We add three sound effects to the AlienShooterContent project in the SoundEffects
folder:
Explosion.wma
HeroShipDamage.wma
MissileLaunch.wma
One item to note is that I recorded the sounds using the Windows 7 Recorder tool, which generates a .wma
file. When added to the Content project, the XNA Framework automatically chose the Song - XNA Framework
Content Processor. This format cannot be played by the SoundEffect class. Simply change the Content Processor to Sound Effect - XNA Framework
and the audio plays fine.
Let’s now move on to a discussion of the Missile
class.
The Missile
class is pretty straightforward, shown in Listing 8-7.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SpriteSheetRuntime;
using Microsoft.Xna.Framework;
namespace AlienShooter.GameObjects
{
class MissileGameObject : GameObject
{
public MissileGameObject(SpriteSheet loadedTexture,
string spriteName, Rectangle screenRect)
: base(loadedTexture, spriteName, screenRect)
{
ResetGameObject();
}
public override void ResetGameObject()
{
Position = new Vector2(-SpriteCenter.X, _screenRect.Height + SpriteCenter.Y);
Velocity = new Vector2(0,-5);
Alive = false;
}
}
}
The next class we cover is the GameStatusBoard
class.
The GameStatusBoard
class keeps track of and displays the score and lives available during the game. This class also plays sound effects when an enemy ship is destroyed and when the hero ship takes a hit from an enemy ship. The GameStatusBoard
class also vibrates the phone when an alien ship hits the hero ship using the VibrateController
class. Figure 8-14 shows the status board in action.
In the following two sections, we cover keeping score and tracking lives functionality in the GameStatusBoard
class.
Each time a hero ship missile intercepts an alien space ship, 5 points are added to the GameStatusBoard.Score
property. The Score property is modified in the GamePlay screen, which we cover later.
Within the GameStatusBoard
class, updating the score results in a SoundEffect.Play call for the Explosion.wma
file:
public int Score
{
get { return _score; }
set
{
_score = value;
_alienExplosionSoundEffect.Play();
}
}
The GameStatusBoard.Update
method has a switch
statement to display a message based on current score:
switch (Score)
{
case 50: _displayMessage = true;
_message = "Nice Start!";
break;
case 60: _displayMessage = false;
break;
case 100: _displayMessage = true;
_message = "Keep It Up!";
break;
case 120: _displayMessage = false;
break;
default: break;
}
When the _displayMessage
field is true based on score, GameStatusBoard.Draw
displays the message in the Status Board with this call:
if (_displayMessage)
spriteBatch.DrawString(_gameFont, _message, new Vector2(175, 0),
_livesTextColor);
That’s it for score keeping. We next cover tracking hero ship lives and game over functionality.
Be default, when playing the Alien Shooter game, you get three lives to start. Each time an alien ship intersects the hero ship, a life is deducted. At two lives a message of “Warning!”
is displayed. At one life, a message of “Danger!”
is displayed. Finally, when zero lives are the state, a “Game Over!”
message is displayed at top and in the middle of the screen. Also, each time a life is deducted, the phone is briefly vibrated. Here is the declaration and instantiation of the VibrateController
class:
private VibrateController _vibrateController = VibrateController.Default;
private TimeSpan _vibrateTimeSpan = TimeSpan.FromMilliseconds(400);
Here is the Lives
property where the sound is played and phone vibrated when the Lives
property is modified:
public int Lives
{
get { return _lives; }
set
{
_lives = value;
_heroShipDamageSoundEffect.Play();
_vibrateController.Start(_vibrateTimeSpan);
}
}
Here is the code from the GameStatusBoard.Update
method that determines what message to display, and in what color:
switch (_lives)
{
case 3: _livesTextColor = Color.LightGreen;
break;
case 2: _livesTextColor = Color.Yellow;
_displayMessage = true;
_message = "Warning!";
break;
case 1: _livesTextColor = Color.Red;
_displayMessage = true;
_message = "Danger!";
break;
case 0: _livesTextColor = Color.Red;
_displayMessage = true;
_message = "Game Over!";
GameOver = true;
break;
}
Here is the corresponding code from the GameStatusBoard.Draw
method that determines when to display a message about the hero ship help on the screen:
if (_displayMessage)
spriteBatch.DrawString(_gameFont, _message, new Vector2(175, 0),
_livesTextColor);
if (GameOver)
spriteBatch.DrawString(_gameFont, _message, new Vector2(175, 370),
_livesTextColor);
Having an informative game status board is an important component of any game development effort. This section covered how simple it is to provide the basics. In the next section, we cover the overall logic in the GamePlayScreen
class that pulls together all of the game objects we just covered.
When you click New Game in the main menu, the GameplayScreen
is the screen that loads and the GameplayScreen
class is where all of the game action occurs. The following sections cover how the GameplayScreen
class manages the game objects, collision detection, and scoring.
Now we are ready to modify the GameplayScreen
class to manage the game objects. We declare the object instances needed:
//Game objects
GameStatusBoard statusBoard;
List<AlienGameObject> enemies;
int maxEnemies = 5;
UserGameObject heroShip;
int maxMissiles = 3;
//Indicates to draw game over frame;
bool drawGameOverFrame = false ;
The maxMissiles
and maxEnemies
variables are not constants, because we may want to change them dynamically during the game as part of the game play. Otherwise, one UserGameObject
named heroShip
and a List
of AlienGameObject
s are the other key components of the game. Another potential modification would be to increase the number of AlienGameObject
s in the game as the score gets higher to make it more interesting. Otherwise, a player will get bored if nothing changes.
Next we load and initialize assets in LoadContent()
:
public override void LoadContent()
{
if (content == null)
content = new ContentManager(ScreenManager.Game.Services, "Content");
gameFont = content.Load<SpriteFont>("gamefont");
alienShooterSpriteSheet = content.Load<SpriteSheet>("Sprites/AlienShooterSpriteSheet");
backgroundTexture = content.Load<Texture2D>("Textures/background");
backgroundPosition = new Vector2(0, 34);
//Get a pointer to the entire screen Rectangle
screenRect = ScreenManager.GraphicsDevice.Viewport.Bounds;
//Initialize Enemies collection
enemies = new List<AlienGameObject>();
for (int i = 0; i < maxEnemies; i++)
{
enemies.Add(new AlienGameObject(alienShooterSpriteSheet, "spaceship", screenRect));
}
//Initialize Player Object
heroShip = new UserGameObject(alienShooterSpriteSheet, "heroship", screenRect, maxMissiles);
heroShip.Position = new Vector2(screenRect.Width / 2, 720);
heroShip.LoadContent(content);
//Initialize Status Board
statusBoard = new GameStatusBoard(gameFont);
statusBoard.LoadContent(content);
// A real game would probably have more content than this sample, so
// it would take longer to load. We simulate that by delaying for a
// while, giving you a chance to admire the beautiful loading screen.
Thread.Sleep(1000);
// Once the load has finished, we use ResetElapsedTime to tell the game's
// timing mechanism that we have just finished a very long frame, and that
// it should not try to catch up.
ScreenManager.Game.ResetElapsedTime();
}
Breaking out our game assets into objects greatly unclutters the code in the GameplayScreen
class. As an example, initializing the enemies
object is a matter of passing in the texture information for the animations:
//Initialize Enemies collection
enemies = new List<AlienGameObject>();
for (int i = 0; i < maxEnemies; i++)
{
enemies.Add(new AlienGameObject(alienShooterSpriteSheet, "spaceship", screenRect));
}
Adding support for the heroShip
and the AlienGameObject
to the GameplayScreen.Update
method is pretty straightforward now that we have nice objects that manage work for us:
public override void Update(GameTime gameTime, bool otherScreenHasFocus,
bool coveredByOtherScreen)
{
if (IsActive)
{
if (!statusBoard.GameOver)
{
CheckForCollisions();
heroShip.Update(gameTime);
statusBoard.Update(gameTime);
for (int i = 0; i < maxEnemies; i++)
{
enemies[i].Update(gameTime);
}
}
}
base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
}
If StatusBoard.GameOver
is not true, and the hero ship has lives available, the game action continues. Otherwise, the code is straightforward, calling Update
for each object. Notice the call to the CheckForCollisions
method. We cover collision detection in the next subsection.
Adding support for user input is just as easy by adding a call to heroShip.HandleIpnut(input)
just after the else
in the GameplayScreen.HandleInput
method. For the GameplayScreen.Draw
method, the Draw
method is called for each object. If StatusBoard.GameOver
is true, the Draw
method is not called for the enemies any further, and the game is over.
public override void Draw(GameTime gameTime)
{
ScreenManager.GraphicsDevice.Clear(ClearOptions.Target,
Color.SlateBlue, 0, 0);
// Our player and enemy are both actually just text strings.
SpriteBatch spriteBatch = ScreenManager.SpriteBatch;
spriteBatch.Begin();
//Draw Background
spriteBatch.Draw(backgroundTexture, backgroundPosition, Color.White);
//Draw Status Board
statusBoard.Draw(gameTime, spriteBatch);
//Draw Hero Ship
heroShip.Draw(gameTime, spriteBatch);
//Draw enemies
if (!statusBoard.GameOver)
{
for (int i = 0; i < maxEnemies; i++)
{
enemies[i].Draw(gameTime, spriteBatch);
}
}
spriteBatch.End();
// If the game is transitioning on or off, fade it out to black.
if (TransitionPosition > 0)
ScreenManager.FadeBackBufferToBlack(1f - TransitionAlpha);
}
In the GameplayScreen.Update
method, there is a call to the CheckForCollisions
method. This method detects collisions between inflight missiles and enemy alien ships (a score) as well as collisions between enemy alien ships and the hero ship (lose a life). Here is the code for the CheckForCollisions
method:
private void CheckForCollisions()
{
//Checking for two major collisions
//1 - Has an in flight missile intersected an alien spaceship - score 5 pts
for (int i = 0; i < heroShip.MaxNumberofMissiles; i++)
if (heroShip.Missiles[i].Alive)
for (int j = 0; j < maxEnemies; j++)
if ((enemies[j].Alive) &&
(enemies[j].BoundingRect.Intersects(heroShip.Missiles[i].BoundingRect)))
{
statusBoard.Score += 5;
enemies[j].ResetGameObject();
heroShip.Missiles[i].ResetGameObject();
}
//2 - Has an alien spaceship intersected the hero ship? - deduct a life
for (int j = 0; j < maxEnemies; j++)
if ((enemies[j].Alive) && (enemies[j].Position.Y > 600) &&
(enemies[j].BoundingRect.Intersects(heroShip.BoundingRect)))
{
statusBoard.Lives -= 1;
for (int i = 0; i < maxEnemies; i++)
enemies[i].ResetGameObject();
for (int i = 0; i < heroShip.MaxNumberofMissiles; i++)
heroShip.Missiles[i].ResetGameObject();
}
}
For detecting a hit by a missile from an alien ship, each missile’s bounding box must be compared with each enemy alien ship’s bounding box. The same goes for detecting a collision between an enemy ship and the hero ship to lose a life. Each one must be compared every frame for the most part. This means that every time two objects are compared, which could be every frame, two new bounding boxes must be constructed. Remember that the bounding box includes a position for the bounding box as well as height and width. Here is the code to return a bounding box in the GameObject
base class as a refresher:
public virtual Rectangle BoundingRect
{
get
{
return new Rectangle((int)Position.X, (int)Position.Y,
SpriteAnimationSpriteSheet.SourceRectangle(SpriteName + 0).Width,
SpriteAnimationSpriteSheet.SourceRectangle(SpriteName + 0).Height);
}
}
In a mobile application you want to try to minimize memory allocations to reduce garbage collection activity. In a mobile game, it is especially important to watch memory allocations. As an example, when an enemy is destroyed by a hit, the AlienGameObject
for that enemy has its Alive
property set to false. We could instead set the object to null and then instantiate a new object but that just wastes CPU cycles on garbage collection.
Another way to minimize CPU cycles is to do work only if needed. Notice in the CheckforCollisions
method that the if
statements are structured to only perform work and get a BoundingRect
when needed. As an example, an enemy alien ship can only intersect the hero ship after it has fallen about two-thirds of the way down the screen, so a check is made to perform the collision calculations only if the alien ship is below 600 pixels on the screen:
if ((enemies[j].Alive) && (enemies[j].Position.Y > 600) &&
(enemies[j].BoundingRect.Intersects(heroShip.BoundingRect)))
Part of game development is always looking for ways to do things smartly. For me, it is one of the most enjoyable parts of the effort. Now that we have covered XNA Framework development, we move on to the new shared rendering capability available in Windows Phone 7.5 that allows XNA Framework and Silverlight composited rendering.
With Windows Phone 7 RTM, developers had to choose a rendering method, either Silverlight or the XNA Framework. Starting with Windows Phone OS 7.1, you can combine Silverlight and XNA Framework rendering into a single application.
When taking advantage of XNA Framework shared graphics, you create a Silverlight application, with an App.xaml
, page.xaml
, navigation, and so on. The full Silverlight application programming model is available to you with the additional capability of rendering XNA Framework content such as a 3D model. This enables developers to have Silverlight-rendered content in an XNA Framework game, as well as bring XNA Framework content, such as a 3D model, into a Silverlight application. This is an incredibly exciting capability now available to developers, enabling unique user interfaces such as actually seeing a 3D model of a product for a mobile marketplace, 3D augmented reality, and much more.
As background, Silverlight runs in “retained mode” on the GPU, where it has a nice object model that is processed to determine the look and layout, and is then rendered by the GPU. XNA Framework runs in immediate mode on the GPU, directly accessing the GPU buffers to render. When thinking about rendering both XNA Framework and Silverlight content, performance invariably comes to mind—how are GPU resources managed with both runtimes trying to render?
In “shared graphics” mode, the XNA Framework handles all rendering. You can declare Silverlight controls within the XAML markup for a page with shared graphics enabled, but the markup does not render automatically—the XNA Framework is in control to maximize memory. To render Silverlight content, you load it into a UIElementRenderer
class, which takes the UIElement
content and generates a texture based on current look and layout processing. This ensures that only the needed Silverlight content is actually rendered within the XNA Framework.
The actual rendering is performed within the XNA Framework game loop, provided by the GameTimer
class so that the Silverlight UI loaded into the UIElementRender
is sampled 30 times per second. This means that as the Silverlight UI layout and look changes, via animation or otherwise, the modifications are picked up each time the UIElementRender
texture is generated and drawn in the game loop.
Performance is maximized by evicting all Silverlight resources from memory when using XNA Framework rendering. Silverlight content renders only when specified by the UIElementRenderer
. A related performance topic is XNA Framework content loading, which can take some time. XNA Framework content is not dumped from memory when, say, navigating to a Silverlight-based high score page and then navigating back to the game page. Users would not want to sit through reloading content if that was the case.
As you can see, while the performance hit to use shared graphics is not zero, it is very small. The product team has also taken steps to maximize memory usage efficiency and not to exact a usability penalty on users for reloading content.
With the background out of the way, we next cover the project templates and then dive into the various classes mentioned in this section in detail so that you can start using shared graphics in your own applications.
With the updated Windows Phone Developer Tools for Mango, two project templates are available: the Windows Phone Rich Graphics Application in the Silverlight for Windows Phone section, and the Windows Phone Silverlight and XNA Application project template. Both are essentially the same project template to get you started, but let’s first provide background on how shared rendering works by first covering the SharedGraphicsDeviceManager
class.
In the previous part of this chapter and in Chapter 1, we covered XNA Framework development, including the GraphicsDeviceManager
class, which managed the device adapter, orientation, screen size, as well as the low-level graphics management under the covers. The SharedGraphicsDeviceManager
class does the same for a composite rendering application, bringing XNA Framework rendering to Silverlight.
This line of code in OnNavigatedTo
on a Silverlight XAML page will enable shared rendering:
SharedGraphicsDeviceManager.Current.GraphicsDevice.SetSharingMode(true);
This line of code in OnNavigatedFrom
on a Silverlight XAML page will disable shared rendering:
SharedGraphicsDeviceManager.Current.GraphicsDevice.SetSharingMode(false);
You might ask at this point, “What about the game loop?” We cover that in the next section.
Silverlight is an event-driven programming model, while the XNA Framework runs in an infinite loop, calling the Update
and Draw
methods until the user quits the game. To bridge the gap between the two programming models, a new class named GameTimer
has been added to the XNA Framework to enable the game loop within Silverlight.
The GameTimer
class has Start
and Stop
methods, just like the standard Timer
class, but it also includes the game loop heartbeat methods in Update
and Draw
events. These events will fire based on the value configured on GameTimer.UpdateInterval
. Generally games try to run at 60 frames per second. Many Windows Phone devices do not support rendering at that high a frame rate, so usually a value of 30 frames per second is configured in code like this:
timer.UpdateInterval = TimeSpan.FromTicks(333333);
Once the GameTimer Update
and Draw
events are wired up like this, you can start to code the XNA Framework rendering portion, just like in a regular XNA Framework application:
gameTimer.Update += OnUpdate;
gameTimer.Draw += OnDraw;’
In the next section I cover loading content and general initialization for XNA Framework development within the Silverlight application model.
There isn’t a LoadContent
method in the XNA Framework Shared Graphics project templates, because these are Silverlight applications. Instead, developers can load content in the OnNavigateTo event for the page. Declare a ContentManager
object in the XAML page and then obtain a reference to the App.Content
ContentManager
object for the application in the page constructor:
contentManager = (Application.Current as App).Content;
By having the ContentManager
declared at the App.xaml
class level, we keep all loaded XNA content in memory when navigating between the game page and a high score or setting page. This is what prevents having to reload the XNA Framework content when navigating the application. Having a reference to the App.Content
property in the page is simply a convenience for loading the content within the XAML page.
The other item in App.xaml
is the SharedGraphicsDeviceManager
declaration as part of the ApplicationLifetimeObjects
collection in XAML:
<xna:SharedGraphicsDeviceManager />
This makes the SharedGraphicsDeviceManager
object available to the entire project. By implementing the IApplicationService
interface, it allows the class to work well with the lifetime of the application and appropriately clean up resources as needed.
With the background out of the way, let’s get started with a simple demonstration of how this works in the next section with a basic shared graphics demonstration project.
In this sample you can start with either shared graphics project template, the Windows Phone Silverlight or the XNA Application template under the XNA Game Studio 4.0 folder in Visual Studio 2010, and name the project SharedGraphicsBasic
in the Chapter 8 solution.
On the MainPage.xaml
page, we replace the ugly Button
object with a TextBlock
and copy over the navigation code to get to the gameplay in GamePage.xaml
. The “game” will be based on the Chapter 3 sample named GesturesTouchPanelXNA. For now let’s get it working within this new framework. With the modifications to MainPage.xaml in place
, the rest of the modifications are on GamePage.xaml
.
We first copy over the StickMan.png
and Pescadero.spritefont
files to the SharedGraphicsBasicLibContent
project, which is the XNA Framework content project for the SharedGraphicsBasic
project. We next copy these field variables from GesturesTouchPanelXNA.Game1.cs
over to GamePage.xaml.cs
in the Chapter 8 SharedGraphicsBasic project:
GestureSample gestureSample;
string gestureInfo;
SpriteFont spriteFontSegoeUIMono;
Vector2 spriteFontDrawLocation;
Texture2D StickManTexture;
GameObject StickManGameObject;
We next copy over the GameObject.cs
code file to the SharedGraphicsBasic
project and then copy over the code from GesturesTouchPanelXNA Game1.cs
to SharedGraphicsBasic GamePage.xaml,
starting with the code in the Game1.Initialize
method to enable gestures over to GamePage’s
constructor:
TouchPanel.EnabledGestures = GestureType.DoubleTap | GestureType.Flick |
GestureType.FreeDrag | GestureType.Hold | GestureType.HorizontalDrag |
GestureType.None | GestureType.Pinch | GestureType.PinchComplete |
GestureType.Tap | GestureType.VerticalDrag | GestureType.DragComplete;.
Next is the code to load content and initialize variables in Game1.LoadContent
. GamePage.xaml
does not have a load content page, so instead we copy the code over to the SharedGraphicsBasic
.OnNavigateTo
method:
spriteFontSegoeUIMono = Content.Load<SpriteFont>("Pescadero");
spriteFontDrawLocation = new Vector2(40, 40);
//Load StickMan texture
StickManTexture = Content.Load<Texture2D>("StickMan");
//Create StickMan sprite object
StickManGameObject = new GameObject(StickManTexture);
//Position in the middle of the screen
StickManGameObject.Position = new Vector2(graphics.GraphicsDevice.Viewport.Width / 2,
graphics.GraphicsDevice.Viewport.Height / 2);
Next up are the Update
and Draw
events. In a regular XNA Framework project, Update
is a method override with this signature:
protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
// TODO: Add your update logic here
base.Update(gameTime);
}
In a SharedGraphics project with the GameTimer
events, the Update
event has this signature:
private void OnUpdate(object sender, GameTimerEventArgs e)
{
// TODO: Add your update logic here
}
As you can see, they have different parameter types, so modifications are required to the GameObject
class for the Update method to take the GameTimerEventArgs
event handler parameter instead of the GameTime
parameter, and we must also modify ElapsedGameTime
to just ElapsedTime
in the GameObject.Update
method.
Some additional modifications are required, such as changing the graphics
variable in the XNA Framework project to the SharedGraphicsDeviceManager.Current
variable in the Shared Graphics project. Other modifications are related to adding and resolving namespaces. Listing 8-8 shows the OnUpdate
and OnDraw
methods.
private void OnUpdate(object sender, GameTimerEventArgs e)
{
// TODO: Add your update logic here
ProcessTouchInput();
StickManGameObject.Update(e,
SharedGraphicsDeviceManager.Current.GraphicsDevice.Viewport.Bounds);
}
private void OnDraw(object sender, GameTimerEventArgs e)
{
SharedGraphicsDeviceManager.Current.GraphicsDevice.Clear(Color.CornflowerBlue);
// TODO: Add your drawing code here
spriteBatch.Begin();
// Draw gesture info
string output = "Last Gesture: " + gestureInfo;
// Draw the string
spriteBatch.DrawString(spriteFontSegoeUIMono, output, spriteFontDrawLocation,
Color.LightGreen, 0, Vector2.Zero, 1.0f, SpriteEffects.None, 0.5f);
//Draw the stickman
StickManGameObject.Draw(spriteBatch);
spriteBatch.End();
}
Once the modifications are complete and everything compiles, run the SharedGraphicsBasic
project and click the Load Game text. This brings up the same UI as shown in the GesturesTouchPanelXNA
project in Chapter 3, and everything works as expected. In the next section we replace the SpriteFont
display with Silverlight controls by using the UIElementRender
object.
In this section I cover how to use the UIElementRender
class, starting by first moving all of the modifications in the SharedGraphicsBasic
project over the new sample project for this section named UIElementRendererDemo
so that it has the exact same behavior. The functionality is working just like in the SharedGraphcisBasic
sample project, so now it is ready for additional modification to draw the gesture text with Silverlight instead of using the SpriteFont
class.
In the UIElementRendererDemo
project, open up GamePage.xaml
and remove the “No XAML content” comment. Add this XAML to the page just inside the <phone:PhoneApplicationPage>
tag:
<Grid>
<TextBlock Name="GestureStatus" Text="Gesture Type Here" />
</Grid>
Figure 8-15 shows the not very fancy XAML Expression Blend.
Switch back to Visual Studio and declare a UIElmentRenderer
object at the top of GamePage.xaml.cs
and comment out the gestureInfo
string declaration:
//Declare a UIElementRenderer
UIElementRenderer xamlRenderer;
Remember that we copied over all of the code to load the XNA Framework content into the OnNavigatedTo
method. We first comment out the code to load the SpriteFont
and then add the code to create the UIElementRender
just before the timer.Start()
code. Here is the full OnNavigatedTo
method:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
// Set the sharing mode of the graphics device to turn on XNA rendering.
SharedGraphicsDeviceManager.Current.GraphicsDevice.SetSharingMode(true);
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(SharedGraphicsDeviceManager.Current.GraphicsDevice);
// TODO: use this.content to load your game content here
//spriteFontSegoeUIMono = contentManager.Load<SpriteFont>("Pescadero");
spriteFontDrawLocation = new Vector2(40, 40);
//Load StickMan texture
StickManTexture = contentManager.Load<Texture2D>("StickMan");
//Create StickMan sprite object
StickManGameObject = new GameObject(StickManTexture);
//Position in the middle of the screen
StickManGameObject.Position = new
Vector2(SharedGraphicsDeviceManager.Current.GraphicsDevice.Viewport.Width / 2,
SharedGraphicsDeviceManager.Current.GraphicsDevice.Viewport.Height / 2);
//Create UIElementRenderer
xamlRenderer = new UIElementRenderer(
GestureStatus,
(int)GestureStatus.ActualWidth, (int)GestureStatus.ActualHeight);
// Start the timer
timer.Start();
base.OnNavigatedTo(e);
}
The UIElementRenderer
constructor takes the Xaml element, in this case the GestureStatus TextBlock
, and a height and width value that is used to generate the texture for rendering. You may be tempted to pass in GestureStatus.Width
and GestureStatus.Height
.
As described in Chapter 2, the layout system computes ActualHeight
and ActualWidth
for a control based on Margin
, Padding
, Height
and Width
settings, as well as the settings on the parent layout control. Therefore, it is important to pass in the ActualWidth
and ActualHeight
properties instead of Width
and Height
.
We could have configured GestureStatus.Text
in the Xaml to an empty string, which would then cause ActualHeight
and ActualWidth
to be zero as well. This results in a runtime error because you must have a positive integer for the UIElement’s
constructor’s Height
and Width
parameters. Note that it is not possible to change the UIElement’s
Height
and Width
properties at runtime. You must create a new UIElementRenderer
object or hard code the UIElementRenderer’s
Height
and Width
values to a value large enough to draw the desired content. This can result in overdraw if drawing a lot of empty space over and over, which has a performance cost. It is a trade-off to evaluate creating new UIElementRender
objects as needed or have one that is sized large enough that is drawn over and over.
Now on to configuring the current gesture type and drawing the string to the screen. The code in the ProcessTouchInput
method is updated to set the current GestureType
name to GestureStatus.Text
:
GestureStatus.Text = "Last Gesture: " + gestureSample.GestureType.ToString();
That’s it for the update code. Here is the code to draw the UIElementRenderer
in OnDraw
:
if (xamlRenderer != null && GestureStatus.Text != "")
{
xamlRenderer.Render();
spriteBatch.Draw(xamlRenderer.Texture, spriteFontDrawLocation, Color.White);
}
The code first checks to make sure that the UIElement
is created and that there is text to draw. UIElementRenderer.Render()
creates the texture to be drawn. This texture is based on the current status of the Sivlerlight UI, which could include Storyboard animations and other dynamic changes.
Calling UIElementRenderer.Render
takes a snapshot of the Silverlight UX at that moment, 30 times a second in the OnDraw
method so dynamic changes render smoothly as if just rendering within a plain Silverlight application. Next the code calls SpriteBatch.Draw
to render the texture. Figure 8-16 shows the UX.
Nothing fancy yet, so let’s make it more interesting. Let’s say you want to jazz up the text. This gets a bit more tricky in the XNA Framework. As shown in the earlier section titled “Bitmap Fonts,” you have to hand-draw each letter you need in order to create a more customized version of the text.
With shared graphics we can instead use Expression Blend to make things more interesting. In Expression Blend I changed the font to Algerian and increased the size to 36pt. In about a minute I created a simple animation that changes the color of the Font for the text every 100ms and runs forever or until stopped.
Once you are satisfied with the Storyboard, switch over to Visual Studio to create a MouseLeftButtonDown
event like this:
private void GestureStatus_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (AnimateTextBlcokFontColor.GetCurrentState() == System.Windows.Media.Animation.ClockState.Stopped)
AnimateTextBlcokFontColor.Begin();
else
AnimateTextBlcokFontColor.Stop();
}
When you run the application and click anywhere on the screen, you get a nice effect, which would have taken a lot more code if authored purely in the XNA Framework. Note that you can click anywhere to generate the tap event. Remember that for the current configuration on the TextBlock
, height and width are not configured, resulting in the TextBlock essentially filling the entire Xaml page, as well as the entire screen at runtime (most of the control is transparent, except the text). So when you run the project, anytime you touch the screen, you fire TouchPanel
events in the XNA Framework and the MouseLeftButtonDown event for the TextBlock.
To fix this, configure a height and width on the TextBlock so that it looks like this in Xaml:
<TextBlock Name="GestureStatus" Text="Gesture Type Here"
FontSize="48" FontFamily="Algerian" Width="468" Height="64"
MouseLeftButtonDown="GestureStatus_MouseLeftButtonDown" />
Running the application results in some interesting behavior. Now when you tap the screen, it no longer fires the MouseLeftButtonDown event; however, the touch area where you must tap to start/stop the Storyboard is not the same location where the TextBlock is drawn. Figure 8-17 illustrates the situation.
If you change the UIElementRender
to load the Grid
and reposition the GestureStatus TextBlock
to the top, it works fine but then you create overdraw for the entire page, which could impact performance. It may not matter depending on the application but is something to consider for a more demanding application.
Give the Grid
control a name of XamlGrid
and update the code to create the UIElementRenderer
control:
xamlRenderer = new UIElementRenderer(
XamlGrid,
(int)XamlGrid.ActualWidth, (int)XamlGrid.ActualHeight);
Next change the draw location in OnDraw Vector2.Zero
:
if (xamlRenderer != null && GestureStatus.Text != "")
{
xamlRenderer.Render();
spriteBatch.Draw(xamlRenderer.Texture, Vector2.Zero, Color.White);
}
Another option to work around this issue of input at one part of the screen but rendering at another part of the screen at runtime is to revert to drawing only the GestureStatus TextBlock
to remove the overdraw and position the control at design-time in Silverlight at the same location as where it will be drawn at run-time.
One option is to choose to just use XNA Framework touch input because we need to let the user manipulate the stickman via the TouchPanel
input. Remove the MouseLeftButtonDown
event so that the GestureStatus TextBlock Xaml
looks like this:
<TextBlock Name="GestureStatus" Text="Gesture Type Here"
FontSize="48" FontFamily="Algerian" />
To test for TouchPanel
touches, first create a bounding box for the xamlRenderer
in OnNavigateTo
:
xamlRendererRect =
new Microsoft.Xna.Framework.Rectangle(
(int)spriteFontDrawLocation.X, (int)spriteFontDrawLocation.Y,
(int)GestureStatus.ActualWidth,
(int)GestureStatus.ActualHeight);
Next we need to modify the ProcessTouchInput
method to check to see if the user touched the area where the UIElementRender xamlRenderer
will be drawn. We add this code just before the code checks to see if the stickman is touched:
if (xamlRendererRect.Contains(touchPoint))
{
if (AnimateTextBlcokFontColor.GetCurrentState() ==
System.Windows.Media.Animation.ClockState.Stopped)
AnimateTextBlcokFontColor.Begin();
else
AnimateTextBlcokFontColor.Stop();
}
When you run the application, it works as expected. Tap on the text to enable / disable the Storyboard
animation. Using the TouchPanel
is fine in this instance because the Xaml rendered by the UIElementRenderer
control is fine; it contains a single control that runs the animation. If there were multiple controls that needed individual input, you should stick to using Silverlight input for the Silverlight controls and position them correctly on the screen so that it matches the position at runtime.
As you can see, combining Silverlight and XNA Framework enables you to use the best of both worlds when rendering your user experience. This section took you through some of the considerations for taking advantage of the UIElementRenderer
and how layout affects its behavior. In the next section we cover how to use shared graphics across pages.
In this sample I demonstrate how to show XNA Framework graphics in the application background while navigating between pages. This technique would be useful if you wanted to have an animated application background, possibly in 3D, for your application.
We start with a standard Windows Phone project and add two additional pages, Page2.xaml
and Page3.xaml
. We then edit the pages in Expression Blend so that clicking text on MainPage.xaml
navigates to Page2.xaml
, and Page2.xaml
has a TextBlock
that navigates to Page3.xaml
. Navigation between pages uses the NavigateToPageAction
Behavior configured via Expression Blend.
For this sample, we need to have the XNA Framework drawing portion exist throughout the application lifetime, surviving page transitions. The application does not have any XNA Framework support, so we will now add support for shared graphics manually to help you see exactly how it works. First add references to these three assemblies:
Microsoft.Xna.Framework;
Microsoft.Xna.Framework.Graphics;
Microsoft.Xna.Framework.Interop
We need to create two classes for the sample. One will be a base page named RichBackgroundBasePage
, and the other will be a class named RichBackgroundRenderer
that will enable shared graphics, draw the background, and maintain state / content between page transitions. The application implements the IApplicationService
and IApplicationLifetimeAware
interfaces in order to maintain state across the application.
i’ve covered the IApplicationService
and IApplicationLifetimeAware
interfaces in Chapter 2 already, but to summarize these interfaces allow you to perform actions when the application loads and when the application exits. In IApplicationLifetimeAware.Started
shared graphics are enabled and a ContentManager
is created. Update
and Draw
methods are created as well but not a GameTimer, which we explain in a bit. Listing 8-9 has the full source code of RichBackgroundRenderer
.
using System;
using System.Windows;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
namespace SharedGraphicsBackground.helper
{
//Be sure to add this class to App.xaml in the <Application.ApplicationLifetimeObjects>
//element after the graphics adapter is created
public class RichBackgroundRenderer : IApplicationService, IApplicationLifetimeAware
{
public static RichBackgroundRenderer Current { get; private set; }
//Constructor
public RichBackgroundRenderer()
{
//There can be only one
if (null != Current)
throw new InvalidOperationException();
Current = this;
}
public void Update(TimeSpan elapsedTime, TimeSpan totalTime)
{
//Rich background cotent game loop update code goes here
}
public void Draw()
{
//Rich background content game loop draw code goes here
}
#region IApplicationService Implementation
void IApplicationService.StartService(ApplicationServiceContext context)
{
}
void IApplicationService.StopService()
{
}
#endregion
#region IApplicationLifetimeAware
//Implemented
void IApplicationLifetimeAware.Exited()
{
//clean up
SharedGraphicsDeviceManager.Current.GraphicsDevice.SetSharingMode(false);
}
void IApplicationLifetimeAware.Exiting()
{
}
//Implemented
void IApplicationLifetimeAware.Started()
{
SharedGraphicsDeviceManager.Current.GraphicsDevice.SetSharingMode(true);
//Load rich background content
ContentManager content = (Application.Current as App).Content;
}
void IApplicationLifetimeAware.Starting()
{
}
#endregion;
}
Now that we have our Lifetime aware class, we next create the base page class for our Silverlight pages.
The RichBackgroundBasePage
inherits from the PhoneApplicationPage
class. Its purpose is to put the XNA Framework shared graphics drawing code in a base class so that the XAML page that inherits from it just has the Silverlight page code.
The base page class manages a GameTimer
class instance and a UIElementRenderer
class instance. Same as in the previous shared graphics samples, the GameTimer
instance is started in the OnNavigateTo
method and stopped in the OnNavigateFrom
method.
In RichBackgroundBasePage.OnNavigatedTo
, the base page’s LayoutUpdated
event is wired to RichBackgroundBasePage.LayoutUpdated
. This allows the base page class to know when to create a new UIElementRenderer
if the size changes for the XAML being rendered by the UIElementRenderer
.
In the GameTimer’s
Draw
and Update
events, the RichBackgroundBasePage
obtains a reference to the RichBackgroundRenderer
via the static property Current
using this code to call Update
as an example:
RichBackgroundRenderer.Current.Update(e.ElapsedTime, e.TotalTime);
In RichBackgroundBasePage.OnDraw
the Silverlight UI is rendered using the uiElementRenderer
to essentially draw the entire Silverlight page. Listing 8-10 has the code.
using System;
using System.ComponentModel;
using Microsoft.Phone.Controls;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace SharedGraphicsBackground.helper
{
public class RichBackgroundBasePage : PhoneApplicationPage
{
GameTimer gameTimer;
SpriteBatch spriteBatch;
UIElementRenderer uiElementRenderer;
GraphicsDevice graphicsDevice;
public RichBackgroundBasePage()
{
if (!DesignerProperties.IsInDesignTool)
{
graphicsDevice = SharedGraphicsDeviceManager.Current.GraphicsDevice;
spriteBatch = new SpriteBatch(graphicsDevice);
// Create a timer for this page
gameTimer = new GameTimer();
gameTimer.UpdateInterval = TimeSpan.FromTicks(333333);
gameTimer.Update += OnUpdate;
gameTimer.Draw += OnDraw;
}
}
private void OnUpdate(object sender, GameTimerEventArgs e)
{
RichBackgroundRenderer.Current.Update(e.ElapsedTime, e.TotalTime);
}
private void OnDraw(object sender, GameTimerEventArgs e)
{
SharedGraphicsDeviceManager.Current.GraphicsDevice.Clear(
Microsoft.Xna.Framework.Color.CornflowerBlue);
// Render the Silverlight page to a texture
uiElementRenderer.Render();
RichBackgroundRenderer.Current.Draw();
// Draw the Silverlight page generated texture
spriteBatch.Begin();
spriteBatch.Draw(uiElementRenderer.Texture, Vector2.Zero,
Microsoft.Xna.Framework.Color.White);
spriteBatch.End();
}
private void OnLayoutUpdated(object sender, EventArgs e)
{
// As before, the rendered XAML must have a width and height greater than 0
if (ActualWidth > 0 && ActualHeight > 0)
{
//Check to see if the XAML to be rendered has changed size
if (uiElementRenderer == null ||
uiElementRenderer.Texture.Width != (int)ActualWidth ||
uiElementRenderer.Texture.Height != (int)ActualHeight)
{
//Only create a new UIElementRenderer if necessary
if (uiElementRenderer != null && uiElementRenderer.Texture != null)
uiElementRenderer.Texture.Dispose();
uiElementRenderer = new UIElementRenderer(
this, (int)ActualWidth, (int)ActualHeight);
}
}
}
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
if (!DesignerProperties.IsInDesignTool)
{
// Hook the LayoutUpdated to know when the page has calculated its layout
LayoutUpdated += OnLayoutUpdated;
gameTimer.Start();
}
base.OnNavigatedTo(e);
}
protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
{
if (!DesignerProperties.IsInDesignTool)
{
gameTimer.Stop();
}
base.OnNavigatedFrom(e);
}
}
}
In the next section we wire these classes into our standard Silverlight project manually.
We now have the infrastructure ready to modify the Silverlight application to add shared graphics. We need to create instances of the SharedGraphicsRenderer
and RichBackgroundRenderer
objects in App.xaml
. First bring in the necessary namespaces with this code added to the Application
element in App.xaml
:
xmlns:xna="clr-namespace:Microsoft.Xna.Framework;assembly=Microsoft.Xna.Framework.Interop"
xmlns:sgb="clr-namespace:SharedGraphicsBackground.helper"
The instances are created in the ApplicationLifetimeObjects collection:
<xna:SharedGraphicsDeviceManager />
<sgb:RichBackgroundRenderer />
Caution If you configure SetSharingMode
to True but do not call GameTimer.Start()
, your application will hang on the splash screen. Do not enable shared graphics unless you plan on using a GameTimer
as well.
A large amount of code must be added to App.xaml.cs to initialize the XNA Framework in Silverlight. The best way to proceed is to simply copy the App class from a Silverlight+XNA Framework project template application. In App.xaml.cs add these using clauses as well:
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
We next copy over the AppServiceProvider
class from a Silverlight+XNA Framework project template to the SharedGraphicsBackground
project as well. As background, the AppServicesProvider
class is part of the standards shared graphics project template. It provides access to pre-compiled XNA Framework assets to the ContentManager
instance.
Fix up the namespace if needed to match the target project. Compile the project and no errors should be present. If you try to run it the application will hang on the splash screen. We need to modify the pages to inherit from the RichBackgroundBasePage
to add a GameTimer
to the project.
Change the phone:PhoneApplicationPage
to sgb:RichBackgroundBasePage
after adding this namespace to each page:
xmlns:sgb="clr-namespace:SharedGraphicsBackground.helper"
Finally, edit each page’s code behind to inherit from RichBackgroundBasePage
instead of PhoneApplicationPage
class. With all of these modifications completed, compile and run the application. You will see that the system tray displays and the Silverlight page is drawn correctly and the pages navigate with the plain cornflower-blue background.
You now can modify the RichBackgroundRenderer
class to load up content, modify the Update
, and Draw methods to have XNA Framework content render in the background of the application. Since we started with a regular Silverlight application we don’t have a separate content project, which is added now.
To add a content project to the solution, right-click the Chapter 8 solution, select Add New Project, select the XNA Game Studio 4.0 folder and then Empty Content Project (4.0), enter the name SharedGraphicsBackground (Content)
, and click OK. If you try to add a project reference from the SharedGraphicsBackground
project to the newly created content project, the SharedGraphicsBackground (Content)
project is not visible in the project list. We must go through an intermediate library project.
Add an intermediary library project to the solution by right-clicking the Chapter 8 solution and selecting Add | New Project. Then select Windows Phone Game Library (4.0), enter the name SharedGraphicsBackgroundLib
, and click OK. Add a reference to the new library in the SharedGraphicsBackground
Silverlight project by right-clicking on References, selecting the SharedGraphicsBackgroundLib
project, and click OK.
The content project has not actually been linked to the SharedGraphicsBackground
project yet. To do so, right-click on the SharedGraphicsLib
project, select Add Content Project Reference, choose the SharedGraphicsBackground (Content)
project, and click OK. Next, add a folder named “textures” and add book.tga
texture to it. Load content as before using this line of code in the RichBackgroundRenderer.IApplicationLifetimeAware.Started
method:
bookTexture = content.Load<Texture2D>("textures/book");
In this section, we modify the RichBackgroundRenderer
class to draw an image of the book cover in the background and animate down the screen to demonstrate that the rendering survives page transitions. First we add these fields to the top of the class to support rendering:
GraphicsDevice graphicsDevice;
SpriteBatch spriteBatch;
//book graphic fields
Texture2D bookTexture;
Vector2 bookPosition;
Vector2 bookVelocity;
Next we modify the Update
and Draw
methods to perform the rendering and simple animation. Here is the code:
public void Update(TimeSpan elapsedTime, TimeSpan totalTime)
{
//Rich background content game loop update code goes here
bookPosition += bookVelocity;
if (bookPosition.Y > 800d)
{
bookPosition.X = graphicsDevice.Viewport.Width / 2 - bookTexture.Width / 2;
bookPosition.Y = -bookTexture.Height;
}
}
public void Draw()
{
//Rich background content game loop draw code goes here
spriteBatch.Begin();
spriteBatch.Draw(bookTexture, bookPosition, Color.White);
spriteBatch.End();
}
Figure 8-18 shows the animation flowing through after navigating to page 2.
While the animation is simple, it demonstrates how you can present unique user interfaces in Silverlight applications that can take advantage of XNA Framework graphics to provide interesting graphic effects that continue between Silverlight page navigation transitions.
This chapter covered extensively the process of building out the game play leveraging existing samples like the Game Screen Game Management and the Sprite Sheet processor samples to build out a 2D game. In the next chapter we focus on making enhancements to this game such as explosions for collisions as well as important housekeeping items such as adding support for saving and loading the game, as well as the very important tombstoning support.
This chapter also covered XNA Framework shared graphics available with Windows Phone OS 7.1. I demonstrated three scenarios, a page dedicated to XNA Framework rendering without any Silverlight rendering. I next demonstrated mixing Silverlight rendering with XNA Framework rendering. Finally I demonstrated XNA Framework rendering in the background that survives Silverlight page navigations.
In the next chapter you will update the AlienShooter game to include a particle system for smoke and explosions as well as other enhancements. Chapter 9 also covers 3D game development, including how it can be combined with augmented reality scenarios.