Make a Game in 60 Minutes
This tutorial helps you learn about the process of game construction, while guiding you through writing a relatively complete game.
- Introduction
- Before you begin: Getting the project ready
- Step 1: Ship Shape
- Step 2: We need rocks. Lots of them.
- Step 3: Boom! You're dead.
- Step 4: Revenge of the ship
- Step 5: Space, the final frontier
- Wrap-Up
Introduction
Understanding the basics of game coding is the single most-difficult step for a beginning game programmer. While it is easy to find samples that show completed games, tips and tricks, or tutorials that show you how to do specific techniques, there are very few that help you through the process of game construction. This objective of this tutorial is to help you learn about the process of game construction, while guiding you through writing a relatively complete game. In addition, this tutorial will use only assets that are found by default in the Spacewar Starter Kit with XNA Game Studio Express, eliminating the need to download and install additional content.
The game we will implement will be a simple clone of the popular Asteroids® game by Atari®, ironically influenced by the original Spacewar game as well. The place of Asteroids in video game history is well known, and you are encouraged to read the interesting history of the game on Wikipedia. This tutorial assumes you have a general idea of how the Asteroids game works.
A lot of initial work in this tutorial is already done for you. In fact, this tutorial picks up at the end of the third tutorial in the XNA Game Studio Express documentation under Going Beyond: XNA Game Studio Express in 3D. Once you have completed those three tutorials, you will have a moveable spaceship with sounds and rendering in 3D space. In another 60 to 90 minutes of coding time, you will have a relatively complete Asteroids-style game.
| Note |
|---|
| If you get lost at any time in this tutorial, look inside the Solved_Steps folder. You will find code that you can copy to you main code area to help you get back on track, corresponding to the end of each step. |
Before you begin: Getting the project ready
You
can begin by downloading the code for "Make a Game in 60 Minutes" from
the XNA Creators Club Online tutorial section. The tutorial starts off
exactly where you left off with Tutorial 3, except a few changes have
been made to the code to improve readability and improve the execution
efficiency of the program. This includes the separation of the Ship information from the Game1
class. Feel free to take the time to examine the difference between the
code at the end of Tutorial 3 and the code that begins this tutorial.
They are identical in terms of functionality. Understanding the
differences isn't critical to successfully completing this series of
tutorials, but it's still important that you're comfortable with what
the code is doing.
Step 1: Ship Shape
Our first step is to get the ship flying around the screen from a top-down point of view. We will accomplish this by simply changing the camera's angle and distance. Finally, we will adjust the rotation mechanics on the user input, so that it matches the behavior we want.
Let's reverse the camera position along the Z axis by simply altering the value from negative 5000 to positive 25000. The cameraPosition member is declared near the start of the Game1 class. Now our cameraPosition declaration will look like this:
Vector3 cameraPosition = new Vector3(0.0f, 0.0f, 25000.0f); |
Unfortunately, if you run the tutorial with only that change, the ship doesn't show up. Why? Because the "projection matrix" of the camera isn't correct. The formal term that describes the problem is "bounding frustum culling" (also called "viewing frustum culling"). Look in the XNA Game Studio Express documentation for the BoundingFrustum class, which includes a key diagram to help you learn more about frustums and how they relate to the camera. A camera's near and far plane is set in a specific way to (usually) address performance concerns. In this case, the camera's original near plane is 1 and the far plane is at 10,000. When the camera was set at 5,000 units, like in Figure 1, the ship was in the camera's view space.
Figure 1. Original Camera Setting and View Space
That's perfectly fine when the spaceship was located 5,000 units away from the camera. But when you moved the camera starting point to 25,000, the camera's view space was in the wrong place, as in Figure 2, leaving the ship too far away to be seen.
Figure 2. New Camera Position with Incorrect View Space
Let's correct the viewing space problem now. Inside the Initialize method of the Game1 class, you will see the method that creates the projectionMatrix:
projectionMatrix = Matrix.CreatePerspectiveFieldOfView( |
What we need to do is change the near and far clipping planes of the frustum so that the ship is back in the viewing space. We determine the near and far clipping planes by simple math. The camera is 25,000 units away from the ship, so we set the near plane 5,000 units "closer" to the camera, and the far plane 5,000 units "further away", like this:
projectionMatrix = Matrix.CreatePerspectiveFieldOfView( |
This corrects the viewing space so that the ship is inside it, as in Figure 3.
Figure 3. Corrected Viewing Space
Running the program now will show you the view facing the back end of the ship, rather than facing the nose end. If you fly the ship toward or away from you, you will see the ship disappear as it moves outside the frustum after a few seconds. Now let's alter the orientation of the ship and how it responds to our input.
In the Ship
class, we will change the default orientation of the ship so that it
starts from a "top-down" perspective. If the ship is initially facing
away from us, a 90-degree rotation along the x-axis will give us the
top-down view. Don't forget, we're looking at the ship right down the
z-axis, so from our perspective, changes in X are "left/right" and
changes in Y are "up/down". Thus, rotating the ship on the x-axis flips
the ship around as if we were spinning it on the wings. In the XNA
Framework, angular measurements are given in radians, which means we're
rotating the ship Pi/2 radians.
public Matrix RotationMatrix = Matrix.CreateRotationX(MathHelper.PiOver2); |
Now, every time we change the ship's rotation (in the ship's Rotation
property "set" method), we alter the rotation matrix to include this
default rotation, plus the rotation amount along the z-axis supplied by
the player's controller. You could just as well rotate along any other
axis, provided you
- Position the camera properly, and
- Perform your translation and rotation calculations in relation to the correct axis.
Failing to properly calculate translation and rotation movement can yield some surprising, if not frustrating, results.
if (rotation != newVal) |
You
should notice that you ship appears to be flying slowly now. That's
because our view is much further away than it used to be. Just under
the declaration of Velocity, add a floating-point constant that you can use to adjust the ship's velocity:
private const float VelocityScale = 5.0f; //amplifies controller speed input |
At the end of the ship's Update method, insert the VelocityScale value to give the ship a little extra speed (more accurately, it increases the number of units per frame in the game):
Velocity += RotationMatrix.Forward * VelocityScale * |
Running with these changes will now give you a top-down view of the ship, which you can fly around on the screen. If you fly off the screen, press the "warp" button. We recommend changing the original use of the A button to another button, as you will be using the A button to fire in a later step. If you don't get the right results, don't panic. Just reach into the Solved_Steps->Step1 directory, select all the files, copy, then paste them into your development directory, overwriting your current Game1.cs and Ship.cs files.
Step 2: We need rocks. Lots of them.
We've got a ship in the game, so let's add asteroids to it now. For the sake of simplicity, we are only going to track each asteroid's position, direction, and speed. Let's create a simple class that has only those three members. Right-click on the MyFirstGame project in Solution Explorer, click Add, and then click New Item. Then select a Class file. Name it Asteroid.cs. (Don't forget to add a using statement for Microsoft.Xna.Framework). Because this class is "lightweight," we will change it from a class to a struct (literally, change the word "class" to "struct" in the file). There are many nuances about when to use and not use a struct (called a "value type" in C# parlance), which are beyond the scope of this document, many of the issues relate to performance and garbage collection (GC). In a blog post by the Compact Framework team (http://blogs.msdn.com/netcfteam/archive/2006/12/22/managed-code-performance-on-xbox-360-for-xna-part-2-gc-and-tools.aspx) they say this about value types:
- "Games typically have lots of small objects that represent game state. The obvious optimization here is to reduce live object count. You can do that by defining those data structures as structs which are value types (to use more general terminology). Value types stay off the GC heap... of course that assumes that your structs don't get boxed in to objects, which can often happen unknowingly in your code."
In this case, we will use a value type for the Asteroid (and later for bullets) to reduce garbage collection events, as well as to keep the implementation simple.
Let's add these three members to the struct:
public Vector3 position; |
Inside our Game1 class, we will create a simple array that contains asteroids. Let's add some additional members to our Game1 class to render the asteroids. After the declaration for the ship (Ship ship = new Ship();), add the following:
Model asteroidModel; |
There's
something new in each of these four lines, so let's look at each one.
The first line is an object that holds on to a lot of information that
describes the actual asteroid model loaded by the Content Pipeline
processor. We'll do that shortly. The second line retains state
information related to specific lighting and effect transformations on
the asteroid. Because we aren't adding any special lighting effects, we
will set up a default effect on the model and leave it. The third line
is a simple array of asteroids, but you will notice the introduction of
the GameConstants class, which will generally hold values
that we might want to change as we develop and test the game. We will
talk more about that shortly. The final line creates a random number
generator, which we will use for a few purposes in the game.
Let's look at this new GameConstants
class briefly. One nice design trick for simple games like this is to
gather game parameters, which you might want to customize, into a
single location. Simply create a new class called GameConstants and add these constants to the class (we will use the PlayfieldSize constants later):
//camera constants |
As you might guess from the addition of the camera constants, we will want to modify the CameraPosition declaration in the Game1 class to look like this now:
Vector3 cameraPosition = new Vector3(0.0f, 0.0f, GameConstants.CameraHeight); |
… and the projectionMatrix to look like this:
projectionMatrix = Matrix.CreatePerspectiveFieldOfView( |
Let's turn our attention back to the Asteroid
class again. In order to render the asteroid, we need to add an
asteroid model to the Content Pipeline. You already have a
Content/Models directory in your game, since it's storing your ship
model. Add the "asteroid1.x" model to that directory by right-clicking
on the directory, clicking Add, and then clicking Existing Item.
Then navigate to your Spacewar directory. (Remember, you had to do this
when you did "Tutorial 1: Displaying a 3D Model on the Screen" from the
"Going Beyond: XNA Game Studio Express in 3D" series). Select the
"asteroid1.x" file from your Content/Models directory (you might need
to select files of type "Content Pipeline Files" to see it) and add it
to your Models directory. In addition to adding this model, you will
also need to manually copy the asteroid's texture file,
"asteroid1.tga", from the Content/Textures directory in your Spacewar
game to the Content/Textures area in MyFirstGame. Just manually copy
it, do not use the "Add->Existing Item…" approach. Also, be very
careful about the copying process. A common beginner's mistake is to
copy a Texture file into a Model directory. This is a bad thing.
Now we will visit the LoadGraphicsContent method in the Game1 class. This is where we will load the mesh model for our asteroid that we just added. Just below the line where you added the "p1_wedge" model, load the asteroid model and transforms:
|
Next, we will populate the asteroidList with several asteroids at the end of the Initialize method in the Game1 class (before the base.Initialize()
call). When we create an asteroid, we will give it a starting speed and
random direction. For now, let's start the asteroids from the center of
the screen. We will create a separate method called ResetAsteroids, which will populate the list of asteroids.
private void ResetAsteroids() |
| Note |
|---|
You will need to add two floating-point constants (code given below), AsteroidMinSpeed and AsteroidMaxSpeed, to the GameConstants class yourself. In this example, 100.0 is the minimum speed, and 300.0 is the maximum. |
public const float AsteroidMinSpeed = 100.0f; |
Then add the call to ResetAsteroids() just before the call to base.Initialize() in the Initialize method.
The direction values of the asteroids are using a basic trigonometric function to determine the X and Y components of the direction, based on the starting angle. We don't modify the Z value because the game only plays in two dimensions.
Now that we've created the asteroids, we need to render them. You should recognize that this should go in the Draw() method. Indeed, we will simply look through the asteroidList
and render each asteroid in the same manner as the ship. So let's add
this code after the completion of the rendering of the ship in the Draw() method.
for (int i = 0; i < GameConstants.NumAsteroids; i++) |
If we run this code as-is right now, we will see the ship and single asteroid rendered in the center. There are actually 10 asteroids there, but they're stacked one on top of the other.
Our next step is to give the asteroids some motion. This is accomplished in the Update() method by simply iterating over the list and updating their position. Let's do that just after we update the ship's velocity:
for (int i = 0; i < GameConstants.NumAsteroids; i++) |
One thing we've added is a time delta. This is a small efficiency trick. We calculate the timeDelta
value once per update, rather than repeatedly calling the property to
check for the total seconds passed. This will be the first line of the Update() method (in the Game1 class):
float timeDelta = (float)gameTime.ElapsedGameTime.TotalSeconds; |
You will notice that we are calling each asteroid's Update() method in this loop, so we will need to add that method to the Asteroid
struct (inside the structs braces, not outside it). Thanks to the
expressiveness of the XNA Framework Math library, this can be written
in a very simple manner:
public void Update(float delta) |
| Note |
|---|
You will need to add the floating-point constant, AsteroidSpeedAdjustment, to the GameConstants class. In our case, we use a default value of 5.0. |
If everything went well, you will see the asteroids all flying away from the ship in random directions until they all disappear from the screen.
What's wrong with this picture?
Let's keep the asteroids
in the game by wrapping the asteroid around the screen. This is
accomplished by allowing the asteroids to drift off the screen, then
shifting them to the other side once they've disappeared from the
screen. The values of playfield size constants were made from some
rough approximations based on the actual viewing space. A properly
designed game will carefully calculate the field of view area and
determine the limits based on asteroid model sizes, etc. In our case,
we will use the PlayfieldSize constants in a simple fashion to determine the "wraparound" trigger areas. After we update the asteroid's position in the Asteroid class' Update method, we then determine if we need to move the asteroid around:
if (position.X > GameConstants.PlayfieldSizeX) |
Now you should see your asteroids calmly wrapping around the screen as they drift through space. Perfect! Well, almost. This game won't be very interesting if we start all the asteroids in the center, since that would result in a collision with the ship. Let's add some code to start the asteroids on the left or right edge of the screen.
Choosing
where to start the asteroid is a little tricky. For the x value of the
asteroid's position, we must first choose to start on the left or right
side of the screen. We use the random number generator to pick either a
0 or 1. If it's 0, we will start on the left side; a 1 will be on the
right. To do this, we call random.Next(2), which
generates a number between 0 and up to, but not including, the passed
value (so it only returns a 0 or 1). For the y value of the asteroid's
position, we simply choose a random number that is within the
playfield's y range. This means we will replace the line that assigns
the asteroid position a value of Vector3.Zero with the following code:
if (random.Next(2) == 0) |
Note Don’t forget to declare the two floating-point values, xStart and yStart, just before the for loop!
It might look a little confusing, but run through the math calculations a couple times to get comfortable with what is going on. At this point, you have a ship in the center of the screen, with several asteroids starting on the sides, moving in random directions and speeds. Once again, if you get lost, copy the files from Solved_Steps->Step2 and drop them in your development directory. Just remember that you need to do the content copying (from the Spacewar game) on your own.
Step 3: Boom! You're dead.
Now, let's add a few more content items to our game, which we will use in Steps 3 and 4. You will add one model and three sounds to the game:
- Add "pea_proj.x" (the bullet model) to your Content->Models section in your project. To do this, right-click Models, click Add, and then click Existing Item. Don't forget you might need to change the Files of Type drop-down to Content Pipeline Files. The model is located in your Spacewar directory under Content/Models (the same place the asteroid model was lurking). You will also need to copy (again, don't use Add and then Existing Item here) the "pea_proj.tga" file from the Spacewar game's Content/Texture location to your Content/Textures location)
- Just like you did in Tutorial 3 ("Making Sounds with XNA Game Studio Express and XACT"), reach into the Content/Audio/Waves directory in your Spacewar game and copy weapons/explosion3.wav, explosions/explosion2.wav, and weapons/tx0_fire1.wav into your Content/Audio/Waves directory. Then run XACT and add those waves to your XACT sound bank. Don't forget to also add them to the list of cues.
We have something visually interesting now. We have a ship, with sound effects, that we can move around. We also have asteroids happily flying around on the screen. Unfortunately, we can't shoot the asteroids. On the other hand, the asteroids also can't hurt us...yet. So let's add some collision detection between the ship and the asteroids. In a later step, we'll get even by shooting back.
With the XNA Framework, simple collision detection is easy. In this step, we will be using a BoundingSphere, which is an object that creates the smallest-sized sphere (by default) that can enclose the target model. The BoundingSphere contains many different intersection tests, including the ability to detect intersections with planes, rays, boxes, and, of course, other spheres (among other things). Hence, we will put an invisible bubble around each object we want to test, and then determine if they intersect each other.
One trick to remember in gameplay is that you should consider
different rules for collisions, depending on the context. In this case,
we will deliberately create a bounding sphere around the ship that is
smaller than the ship. Why? This is a little game programming trick.
Most models are uneven in shape, but a BoundingSphere only
takes into account the point farthest from the model's center when
creating the sphere's radius. This results in collisions that often
appear like they weren't near the player's ship. In addition, creating
a slightly smaller sphere gives a little more "forgiveness" in case a
player gets too close to an asteroid. So let's create two constants in
the GameConstants class that sets bounding sphere sizes for the asteroids and ship:
public const float AsteroidBoundingSphereScale = 0.95f; //95% size |
Now, let's create the actual bounding sphere around the ship, just after we update the asteroid positions in the Update method of the Game1
class. Then we create a loop that visits each asteroid. Inside this
loop, we create a temporary bounding sphere around the asteroid and
determine whether the ship and asteroid sphere are intersecting. If the
two spheres intersect, we play an explosion sound and break out of the
loop:
BoundingSphere shipSphere = new BoundingSphere( |
Running this program now gives us some great feedback. First, the collision check seems to work pretty well. Second, we hear a collision sound. Third, the sound doesn't seem right. Why? Because as the asteroid and ship move through each other, the collision check is constantly firing every frame, with the XACT engine trying to play the explosion in every frame, causing garbled sound. We can solve this problem by removing the colliding objects from the updating and rendering. In a real game, this means the ship explodes and you lose a life. In our tutorial, we will simply remove the ship and the offending asteroid from the display and then update.
Our code starts getting a little more complex now, but we will
leverage some handy secrets in XNA Game Studio Express to make it easy.
To start, we will need to create a Boolean flag that tells us if the
ship is alive or dead. This will go in the Ship class, right after the declaration of VelocityScale:
public bool isActive = true; |
Before we test for a ship and asteroid collision, we need to verify the shipAlive flag is true. This is done by wrapping the collision code you already wrote in an if statement. This is easy with XNA Game Studio Express. Highlight the entire block of code that does the collision check (the BoundingSphere declaration and the loop right after it), then right-click the selected code and click Surround With, then select the if statement (not the #if statement) from the list. You will see your code is now wrapped in the loving arms of an if statement, and eagerly awaits you entering the Boolean condition. Now all you have to do is replace true with ship.isActive. Finally, we set ship.isActive to false after we play the explosion sound.
This fixes the explosion sound, but both the ship and the offending
asteroid are still visible in the game. Let's first remove the ship.
Since we have set the flag in the Update() method, that still leaves us the responsibility to not draw the ship anymore. So we once again wrap a chunk of code in the Draw() method with the if
statement. By now you should be familiar with what portion of the code
draws the ship. Select the line of code that draws the ship,
right-click, click Surround With, and insert an if (ship.isActive) test.
Running the code now should let you merrily smash your ship into an asteroid, with an accompanying explosion and the disappearance of your ship.
Finally, we need to remove the colliding asteroid. This requires a flag just like the ship. Each asteroid needs an isActive
flag that tells us whether we should draw or update the asteroid. This
is accomplished in five steps, which you should attempt to do on your
own:
- Create an
isActiveflag inside theAsteroidclass, similar to what you did with the ship - Set the
isActiveflag to true when you create each asteroid in theLoadGraphicsContentmethod in theGame1class - In the code where we draw the asteroids, surround the drawing code in an if statement. This happens inside the loop where we iterate through each asteroid
- Similarly, we now need to do the same thing in the update section, checking to see whether an asteroid is active before we execute a collision test with the ship
- If a ship does collide with an asteroid, set that asteroid's active state to false just after we play the explosion sound.
If you did all the steps correctly, you should have an
almost-functional game! Collisions, sounds, moving ships. It's all
starting to come together! This leads us to the next question: What to
do once we blow up the ship? Easy: Press the "Warp" button. In Tutorial
2 ("Making Your Model Move Using Input"), you wrote some code that
reset the ship back to the center. It's still there and still useful
(except back then it was the A button)! Now go ahead and add a shipAlive=true statement in the code block for pressing the B button. (Hint: Look in the UpdateInput method in the Game1 class). Also, if you haven't changed the warp button from A to B, now is the time to do it. Instant life-regeneration!
Our next step will add bullets to the game, so we can shoot back. The good news is: all the work you've done up to now will make the bullet work seem easy.
This step had a lot of code and content changes, so don't panic if something went wrong and the game isn't quite working right. As always, if you get lost, just copy the files from Solved_Steps->Step3 and you're ready to get started with Step 4. Just remember that you still need to do the content changes on your own.
Step 4: Revenge of the ship
In many ways, a bullet in the game is like an asteroid: it travels in a direction and collides with things. We're going to treat bullets just a little differently though, giving the game a little fine-tuning in the process.
Conveniently, the Bullet class structure is exactly like the Asteroid class structure, so all you need to do is copy the Asteroid class and rename the file to Bullet.cs and struct name to Bullet. In addition, we will want to add these new constants to the GameConstants class for later use:
public const int NumBullets = 30; |
We're going to think ahead a little bit right now, though. How long
do we want the bullets to fly around in space? Do we want them to wrap
around the screen? Maybe only live for a certain number of seconds or
travel a certain distance? Do we want the bullets to be able to collide
with both asteroids and the ship? Any of these approaches are
legitimate ways to make the game physics behave. In this case, though,
we are simply going to let the bullets disappear once they go off the
screen. This means the Update() method in the Bullet class will flag the bullet as inactive once it drifts off the view.
This is a simple check, similar to what was done with the Asteroid class:
public void Update(float delta) |
Just like the Asteroid class, we are now done with the actual Bullet
class. However, we have to do several things to make the bullets
actually work in the game. We've done this all before with the
asteroids, but let's review the basic steps:
- Load the model into the Content Pipeline and set the effect transforms.
- Create a list to track all bullets in the game.
- Create a bullet when a player presses a specific button, and make a firing sound.
- Draw the bullet in-flight.
- Test the asteroids and bullets for collisions. If they collide, make an explosion sound and remove the colliding bullet and asteroid.
So let's begin by creating the needed instance variables. Underneath the same place that we created the asteroidList and asteroidModel variables, let's create a list to hold the bullets and a model to hold our bullet's shape.
Model bulletModel; |
Then in the LoadGraphicsContent() method, we create the bulletList and assign the pea_proj model to bulletModel. Remember, you added pea_proj.x to the Content/Models directory in step 3:
|
Unlike the asteroids, we don't create bullets inside LoadGraphicsContent. Instead, we create a bullet every time a user presses a button (in our case, the A button on the controller). So let's add a new condition to the UpdateInput() method at the very end:
//are we shooting? |
There's an interesting trick in the above code that needs explaining. When we calculate the initial position of the bullet, we want the bullet to appear as if it's firing out the nose of the ship. Thus, we begin by determining where the bullet is starting from, which is the ship's center. Then we translate the bullet 200 additional units in the direction of the bullet (200 is the rough approximation of the distance from the ship's center to the nose of the ship).
This kind of "motion offset" is very common in game development. One "extra credit" feature you can do is to add the ship's current velocity to the bullet's velocity.
At this point in time, it's actually possible to run your game and press the fire (A)
button, but you won't yet be able to see the bullets (because we
haven't' drawn them, but you knew that). When you press the fire
button, you might have observed that the sound behaves just like the
original problem we had with the asteroid/ship explosions; we are
triggering the sound too many times. In fact, you probably noticed that
you can hold the fire button down and it will fire a continuous
"stream" of bullets (until all the bullet "slots" are used). We're
going to make a simple fix to the UpdateInput() method to fire the bullet only once every time the button is pressed.
The problem with UpdateInput is that we're failing to track the user's previous input state. Let's create a variable that does this. Just after the ContentManager declaration (near the beginning of the Game1 class), add this variable:
GamePadState lastState = GamePad.GetState(PlayerIndex.One); |
Then, at the end of the UpdateInput method, save the user's game pad state:
lastState = currentState; |
Now all we need to do is change the if statement for the "fire" effect to verify that the button wasn't held down the last time we updated:
if (ship.isActive && currentState.Buttons.A == ButtonState.Pressed && |
When you run the program, you will now hear a firing sound for every time you individually press the A button. Now that you see how to do this, add the same check to your hyperspace button for consistency reasons. The next step is to draw the bullet as it is flying around the screen. Conveniently, this code is identical to the code that draws the asteroids, except we replace the word "asteroid" with "bullet":
for (int i = 0; i < GameConstants.NumBullets; i++) |
Then we will again do exactly the same thing in the Update
method. Just after the part where we update the asteroid positions (but
before we do the asteroid/ship collision test), we can add the code to
update the bullets:
for (int i = 0; i < GameConstants.NumBullets; i++) |
If we run the code at this point in time, we actually have an
"almost working" game! All that is left is testing for collisions
between the bullet and the asteroids. This process is really quite
easy; all you need to do is loop through each asteroid, checking to see
if a bullet is colliding with it. If so, deactivate both the colliding
bullet and asteroid and continue through the list of asteroids until
you're done. The code is almost literally a copy of the ship/asteroid
collision code, except instead of if (shipAlive)... we
have a loop through each asteroid. One thing to note: Do this collision
check before checking to see if the ship collides with an asteroid;
that way, the player gets credit for a "kill" before getting destroyed!
//bullet-asteroid collision check |
If everything went well, you can now fly a ship around, shoot asteroids, and collide with asteroids. Congratulations, you have written your first XNA Framework game! But wait...the blue background looks, well, nothing at all like a good Asteroids game. We need a space background and, of course, a way to keep score. Let's cover that in our last step.
Just like step 3, this step had a lot of code changes. As always, if you get lost, just copy the files from Solved_Steps->Step4, and you're ready to get started with Step 5!
Step 5: Space, the final frontier
Our last step will be to add finishing touches to the game to make it both visually appealing and to give it more of a game feel. We will do this in two parts. The first part will have us quickly adding a 2D background texture to the game to give it a nice space appearance. The second part will be adding a simple scoring mechanism to the game. When it comes to doing either step, the first thing to remember is that all 2D items are drawn as sprites. A background and score are no different in terms of how they are drawn, but as you will learn, it does matter when they are drawn.
For our first step, we need to create a texture for the starry
background, plus create a sprite batch object so that we can draw the
background. We begin by adding the stars and spriteBatch declarations in the same place we declared our Asteroid and Bullet models:
Texture2D stars; |
Just after we create the bulletModel and bulletTransforms objects, we load the texture and create the sprite batch processor:
|
Lastly, at the beginning of the Draw() method, just after we call Clear
on the graphics device, we draw our star background. It's important we
draw the background at the beginning instead of the end; otherwise, we
will be obscuring everything we've already drawn (asteroids, and so on)
by laying the background on top of the previously drawn objects.
spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.None); |
Now add the B1_stars.tga file into the Content->Textures area in your project (right-click Textures, click Add, and then click Existing Item. Then select the file in the Content/Textures area where your Spacewar game is located. Don't forget you might need to change the Files of Type drop-down to Content Pipeline Files. When you run your game now, you should see a pretty star field in the background, with all your gameplay in the foreground.
All that is left is keeping score in the game. This is accomplished in a few simple steps:
- Create a sprite font and add it to the Content Pipeline processing.
- Load the sprite font with the rest of your content.
- Set the display string and call the
DrawStringmethod.
Creating the sprite font is simple. The first thing to do is create a new folder under the Content folder called Fonts. Then right-click the Fonts folder, click Add, and then click New Item. In the menu, you will see several available templates. You will want to pick Sprite Font. The default file name for this file is SpriteFont1.spritefont. While you could leave it that way, we recommend you give it the same name as the font you want to use. Since we will be using the Lucida Console font, you will want to name the file Lucida Console.spritefont. Feel free to experiment with different fonts later, once you're comfortable with this process. Once you create the file, it will open to allow you to edit the different font parameters. Just accept the settings and close it for now.
Before we go on, it's important to understand that fonts are very technical pieces of art. The people and companies that create them pour an enormous amount of work in them. In many cases, fonts are protected under copyright and licensing terms that widely vary. Just because a font is installed on your computer doesn't mean you automatically have the right to redistribute the font to anybody else. Keep this in mind if you ever decide to share games that you write.
Now that we've created the sprite font, let's add some code in the Game1 class so that we can display something. Just after we declared the SpriteBatch object, let's add a few more declarations:
SpriteFont lucidaConsole; |
The first declaration will hold our sprite font when we load it. The second is a simple counter for our score. Finally, the scorePosition object will let us position the score in screen coordinates. You could just as well move the scorePosition into the GameConstants class, but due to compilation rules regarding the Vector2 class, you can't make it a const value.
Loading the sprite font is a one-line addition to the end of the LoadGraphicsContent method, just after we instantiate SpriteBatch:
|
All that's left is to display the score on the screen. This is
pretty simple, provided you respect the rules of drawing order. So far,
we have four very distinct drawing steps in the Draw
method. We draw the background and then the game elements (ship, then
asteroids, then bullets). If we drew the background after the game
elements, all we would see if the star field, because drawing the star
field last covers the entire screen space. This same issue applies for
the game score. We want to draw the game score last so that it appears
overlaid on the rest of the game.
Hopefully by now, you will realize that the score will be drawn just before the base.Draw call is made in the Draw method. The actual code to draw the string is simply a sprite batch Begin/End pair, with the call to DrawString in between:
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, |
When you run the game now, you should see a score displayed in the upper-left corner. You will also notice that the game elements appear to render underneath the score, giving the effect we want. Now let's think about how we want to score the game.
Good gameplay isn't just about "running and gunning," it's about
forcing the player to make decisions and tradeoffs in order to achieve
one or more goals. In this game, we're going to penalize the player for
each round they fire (offensive actions come at a cost) and for any
time they press the "warp" button (defensive actions come at a cost).
We will also penalize the player for dying. In most video games, you
are given a limited number of lives, and you subtract a "life" when the
player's avatar gets destroyed. However, in this game, a multi-life
system isn't implemented (you should do that as "extra credit"), so
we're simply going to take away points. We will also reward points for
each asteroid destroyed. Let's first set up some scoring values in the GameConstants class:
public const int ShotPenalty = 1; |
Now all we need to do is alter the scores in the appropriate places. For instance, the shot penalty would be added to the UpdateInput method, just after we registered that the player fired a bullet, most likely just after the soundBank.PlayCue("tx0_fire1"); line:
score -= GameConstants.ShotPenalty; |
We will need to do similar approaches in three other areas, which you should accomplish on your own:
- When the ship is determined to have collided with an asteroid (subtract DeathPenalty from score).
- When a bullet is determined to have collided with an asteroid (add KillBonus to score).
- When the player presses the warp button (subtract WarpPenalty from score).
Once you have completed these steps, you will have an almost fully functional game. Congratulations! Again, if things don't work right, just copy over the files from Solved_Steps/Step5 and you will be all set!
Wrap-Up
The initial goal of this tutorial was to show you that the tools, materials, and knowledge to write a game are right at your fingertips, and to guide you through the process of writing your first game. By now, you've learned how to:
- Change camera views to achieve different rendering perspectives
- Write simple collision-detection routines
- Create a game environment where many things appear to be happening at once
- Integrate 2D and 3D rendering
- Render text in your game
- Create a feel of "gameplay" where the player has both benefits and penalties with their decisions
Hopefully, you have also enjoyed the process of making the game. After all, making a game should be just as much fun as playing one! But this is only the beginning. While the game you've made is interesting, there are MANY things you can still do to make the game more engaging and enjoyable. We end this tutorial by offering you several suggestions (but by no means a complete list) on how you can take your game to the next level:
- Wrap the ship around on the screen.
- Vibrate the controller when a ship collides with an asteroid.
- Split the big asteroids into successively smaller ones.
- Add explosion effects when a bullet hits an asteroid.
- Add engine particle effects as the ship flies around.
- Add a smart "UFO" that attacks the player's ship.
- Add a "high score" capability to the game.
- Determine when the playing field is cleared and start a new level, perhaps with more or faster asteroids.
Good luck, and good gaming!