XNA 4.0 Game Development by Example: Beginner's Guide
Create your own exciting games with Microsoft XNA 4.0
Dive headfirst into game creation with XNA
Four different styles of games comprising a puzzler, a space shooter, a multi-axis shoot 'em up, and a jump-and-run platformer
Games that gradually increase in complexity to cover a wide variety of game development techniques
Focuses entirely on developing games with the free version of XNA
Packed with many suggestions for expanding your finished game that will make you think critically, technically, and creatively
Fresh writing filled with many fun examples that introduce you to game programming concepts and implementation with XNA 4.0
A practical beginner's guide with a fast-paced but friendly and engaging approach towards game development
Read more about this book
(For more resources on XNA 4.0, see here.)
It was just another day at the bottom of the ocean until an explosion in one of the storage bays cracked the protective dome around Deep Sea Research Lab Alpha. Now the entire place is flooding, and the emergency pump system is a chaotic jumble of loose parts.
Designing a puzzle game
The Puzzler has always been a popular game genre. From old standbys like Tetris to modern crazes like Bejeweled, puzzle games are attractive to players because they do not require a long-term time investment or a steep learning curve.
The game mechanic is the heart of any good puzzle game. This mechanic is usually very simple, with perhaps a few twists to keep the players on their toes.
In Flood Control, the player will be faced with a board containing 80 pieces of pipe. Some will be straight pipes and some will be curved. The objective of the game is to rotate the pipes to form a continuous line to pump water from the left side of the board to the right side of the board.
Completing a section of pipe drains water out of the base and scores points for the player, but destroys the pipes used. New pipes will fall into place for the player to begin another row.
Time for action - set up the Flood Control project
Open Visual Studio Express Edition (If it is already open, select Close Solution from the File menu so you are starting with an empty slate).
In the Visual Studio window, open the File menu and select New Project...
Under Project Type, make sure XNA Game Studio 4.0 is selected.
Under Templates, select Windows Game (4.0).
Name the project Flood Control.
Click on OK.
Right-click on Flood ControlContent (Content) in the Solution Explorer window and select Add | New Folder. Name the folder Textures.
Add another folder under Flood ControlContent (Content) and name the folder Fonts.
Download the 0669_02_GRAPHICPACK.zip file from the book's code download link and extract the files to a temporary folder.
Back in Visual Studio, right-click on Textures in the Content project and click on Add | Existing Item. Browse to the folder where you extracted the 0669_02_GRAPHICPACK files and highlight all of them. Click on Add to add them to your project.
What just happened?
You have now set up a workspace for building Flood Control, and created a couple of folders for organizing game content. You have also imported the sample graphics for the Flood Control game into the project.
Introducing the Content Pipeline
The Flood ControlContent (Content) project inside Solution Explorer is a special kind of project called a Content Project. Items in your game's content project are converted into .XNB resource files by Content Importers and Content Processors.
If you right-click on one of the image files you just added to the Flood Control project and select Properties, you will see that for both the Importer and Processor, the Content Pipeline will use Texture - XNA Framework. This means that the Importer will take the file in its native format (.PNG in this case) and convert it to a format that the Processor recognizes as an image. The Processor then converts the image into an .XNB file which is a compressed binary format that XNA's content manager can read directly into a Texture2D object.
There are Content Importer/Processor pairs for several different types of content—images, audio, video, fonts, 3D models, and shader language effects files. All of these content types get converted to .XNB files which can be used at runtime.
In order to see how to use the Content Pipeline at runtime, let's go ahead and write the code to read these textures into memory when the game starts:
Time for action - reading textures into memory
Double-click on Game1.cs in Solution Explorer to open it or bring it to the front if it is already open.
In the Class Declarations area of Game1 (right below SpriteBatch spriteBatch;), add:
Texture2D playingPieces;Texture2D backgroundScreen;Texture2D titleScreen;
Add code to load each of the Texture2D objects at the end of LoadContent():
playingPieces = Content.Load<Texture2D>(@"TexturesTile_Sheet");backgroundScreen = Content.Load<Texture2D>(@"TexturesBackground");titleScreen = Content.Load<Texture2D>(@"TexturesTitleScreen");
What just happened?
In order to load the textures from disk, you need an in-memory object to hold them. These are declared as instances of the Texture2D class.
A default XNA project sets up the Content instance of the ContentManager class for you automatically. The Content object's Load() method is used to read .XNB files from disk and into the Texture2D instances declared earlier.
One thing to note here is that the Load() method requires a type identifier, specified in angled brackets (< >), before the parameter list. Known in C# as a "Generic", many classes and methods support this kind of type specification to allow code to operate on a variety of data types. We will make more extensive use of Generics later when we need to store lists of objects in memory. The Load() method is used not only for textures, but also for all other kinds of content (sounds, 3D models, fonts, etc.) as well. It is important to let the Load() method know what kind of data you are reading so that it knows what kind of object to return.
Sprites and sprite sheets
As far as XNA and the SpriteBatch class are concerned, a sprite is a 2D bitmapped image that can be drawn either with or without transparency information to the screen.
Sprites vs. TexturesXNA defines a "sprite" as a 2D bitmap that is drawn directly to the screen. While these bitmaps are stored in Texture2D objects, the term "texture" is used when a 2D image is mapped onto a 3D object, providing a visual representation of the surface of the object. In practice, all XNA graphics are actually performed in 3D, with 2D sprites being rendered via special configurations of the XNA rendering engine.
The simple form of the SpriteBatch.Draw() call when drawing squares only needs three parameters: a Texture2D to draw, a Rectangle indicating where to draw it, and a Color to specify the tint to overlay onto the sprite.
Other overloads of the Draw() method, however, also allow you to specify a Rectangle representing the source area within the Texture2D to copy from. If no source Rectangle is specified, the entire Texture2D is copied and resized to fit the destination Rectangle.
OverloadsWhen multiple versions of the same method are declared with either different parameters lists or different return values, each different declaration is called an "overload" of the method. Overloads allow methods to work with different types of data (for example, when setting a position you could accept two separate X and Y coordinates or a Vector2 value), or leave out parameters that can then be assigned default values.
By specifying a source Rectangle, however, individual pieces can be pulled from a large image. A bitmap with multiple sprites on it that will be drawn this way is called a "sprite sheet".
The Tile_Sheet.png file for the Flood Control project is a sprite sheet containing 13 different sprites that will be used to represent the pieces of pipe used in the game. Each image is 40 pixels wide and 40 pixels high, with a one pixel border between each sprite and also around the entire image. When we call SpriteBatch.Draw() we can limit what gets drawn from our texture to one of these 40 by 40 squares, allowing a single texture to hold all of the playing piece images that we need for the game:
The Tile_Sheet.png file was created with alpha-based transparency. When it is drawn to the screen, the alpha level of each pixel will be used to merge that pixel with any color that already occupies that location on the screen.
Using this fact, you can create sprites that don't look like they are rectangular. Internally, you will still be drawing rectangles, but visually the image can be of any shape.
What we really need now to be able to work with the playing pieces is a way to reference an individual piece, knowing not only what to draw to the screen, but what ends of the pipe connect to adjacent squares on the game board.
Alpha blendingEach pixel in a sprite can be fully opaque, fully transparent, or partially transparent. Fully opaque pixels are drawn directly, while fully transparent pixels are not drawn at all, leaving whatever has already been drawn to that pixel on the screen unchanged. In 32-bit color mode, each channel of a color (Red, Green, Blue, and Alpha) are represented by 8 bits, meaning that there are 256 different degrees of transparency between fully opaque (255) and fully transparent (0). Partially transparent pixels are combined with the current pixel color at that location to create a mixed color as if the pixels below were being seen through the new color.
Classes used in Flood Control
While it would certainly be possible to simply pile all of the game code into the Game1 class, the result would be difficult to read and manage later on. Instead, we need to consider how to logically divide the game into classes that can manage themselves and help to organize our code.
A good rule of thumb is that a class should represent a single thing or type of thing. If you can say "This object is made up of these other objects" or "This object contains these objects", consider creating classes to represent those relationships.
The Flood Control game contains a game board made up of 80 pipes. We can abstract these pipes as a class called GamePiece, and provide it with the code it needs to handle rotation and provide the code that will display the piece with a Rectangle that can be used to pull the sprite off the sprite sheet.
The game board itself can be represented by a GameBoard class, which will handle managing individual GamePiece objects and be responsible for determining which pieces should be filled with water and which ones should be empty.
The GamePiece class
The GamePiece class represents an individual pipe on the game board. One GamePiece has no knowledge of any other game pieces (that is the responsibility of the GameBoard class), but it will need to be able to provide information about the pipe to objects that use the GamePiece class. Our class has the following requirements:
Identify the sides of each piece that contain pipe connectors
Differentiate between game pieces that are filled with water and that are empty
Allow game pieces to be updated
Automatically handle rotation by changing the piece type to the appropriate new piece type
Given one side of a piece, provide the other sides of the piece in order to facilitate determining where water can flow through the game board
Provide a Rectangle that will be used when the piece is drawn, to locate the graphic for the piece on the sprite sheet
Identifying a GamePiece
While the sprite sheet contains thirteen different images, only twelve of them are actual game pieces (the last one is an empty square). Of the twelve remaining pieces, only six of them are unique pieces. The other six are the water-filled versions of the first six images.
Each of the game pieces can be identified by which sides of the square contain a connecting pipe. This results in two straight pieces and four pieces with 90 degree bends in them.
A second value can be tracked to determine if the piece is filled with water or not instead of treating filled pieces as separate types of pieces.
Time for action - build a GamePiece class - declarations
Switch back to your Visual C# window if you have your image editor open.
Right-click on Flood Control in Solution Explorer and select Add | Class...
Name the class GamePiece.cs and click on Add.
At the top of the GamePiece.cs file, add the following to the using directives already in the class:
using Microsoft.Xna.Framework.Graphics;using Microsoft.Xna.Framework;
In the class declarations section, add the following:
public static string[] PieceTypes = { "Left,Right", "Top,Bottom", "Left,Top", "Top,Right", "Right,Bottom", "Bottom,Left", "Empty"};public const int PieceHeight = 40;public const int PieceWidth = 40;public const int MaxPlayablePieceIndex = 5;public const int EmptyPieceIndex = 6;private const int textureOffsetX = 1;private const int textureOffsetY = 1;private const int texturePaddingX = 1;private const int texturePaddingY = 1;private string pieceType = "";private string pieceSuffix = "";
Add two properties to retrieve information about the piece:
public string PieceType{ get { return pieceType; }}public string Suffix{ get { return pieceSuffix; }}
What just happened?
You have created a new code file called GamePiece.cs and included the using statements necessary to access the pieces of the XNA Framework that the class will use.
Using DirectivesAdding the XNA Framework using directives at the top of the class file allows you to access classes like Rectangle and Vector2 without specifying their full assembly names. Without these statements, you would need Microsoft.Xna.Framework.Rectangle in your code every time you reference the type, instead of simply typing Rectangle.
In the declarations area, you have added an array called PieceTypes that gives a name to each of the different types of game pieces that will be added to the game board. There are two straight pieces, four angled pieces, and an empty tile with a background image on it, but no pipe. The array is declared as static because all instances of the GamePiece class will share the same array. A static member can be updated at execution time, but all members of the class will see the same changes.
Then, you have declared two integer constants that specify the height and width of an individual playing piece in pixels, along with two variables that specify the array index of the last piece that can be placed on the board (MaxPlayablePieceIndex) and of the fake "Empty" piece.
Next are four integers that describe the layout of the texture file you are using. There is a one pixel offset from the left and top edge of the texture (the one pixel border) and a single pixel of padding between each sprite on the sprite sheet.
Constants vs. Numeric literalsWhy create constants for things like PieceWidth and PieceHeight and have to type them out when you could simply use the number 40 in their place? If you need to go back and resize your pieces later, you only need to change the size in one place instead of hoping that you find each place in the code where you entered 40 and change them all to something else. Even if you do not change the number in the game you are working on, you may reuse the code for something else later and having easily changeable parameters will make the job much easier.
There are only two pieces of information that each instance of GamePiece will track about itself—the type of the piece and any suffix associated with the piece. The instance members pieceType and pieceSuffix store these values. We will use the suffix to determine if the pipe that the piece represents is empty or filled with water.
However, these members are declared as private in order to prevent code outside the class from directly altering the values. To allow them to be read but not written to, we create a pair of properties (pieceType and pieceSuffix) that contain get blocks but no set blocks. This makes these values accessible in a read-only mode to code outside the GamePiece class.
Read more