Welcome back to The XNA Tutorial, sorry it’s been so long! More than 5000 words later, you’d think I was done. You’d think that was all that there is... but no! Our Pong clone is looking awesome (well, as awesome as three white oblongs on a black background can look) and it’s time to add some new features.XNA Tutorial – Part 2
Here’s a roundup of what we’ll be adding to our game in this tutorial:
- A screen manager.
- A system for keeping and displaying the score for each player.
- The ability to play sounds.
- A main-menu-style screen.
We’ve got to start somewhere and I guess the first option is the best. A screen management system is an incredibly important element in most games and, yes, we probably should have dealt with this earlier but better to learn to walk before trying to run.
Rather, than go in to great detail about screen management now, let’s get set up. You’re going to need some things before we can start. First, if you’re for some reason starting this tutorial without having completed Part 1, and you don’t have XNA or Visual Studio installed, then go back to Part 1 of the tutorial and follow the setup instructions at the beginning. Second, if you have XNA installed but didn’t complete Part 1 of the tutorial, or would just like an organised copy, then download the file PongTutorial.zip in Part 1 of the tutorial (I’ve also made this available at the bottom of this tutorial – it is named ‘Part1-PongTutorial.zip’).
Finally, you need to open up the PongTutorial solution in Visual Studio. Now we’re ready to start coding again!
Screen Management Is Boring
Yes, screen management is boring. However, it’s a necessary evil and gives greater definition to the structure of our game. Let’s start by creating a new class. I’ll say now, I’m not going to reiterate how to perform some of the simple tasks that I went through in Part 1 so if you’re not sure how to do something, open up Part 1, hit Ctrl+F and search for how to do it. Now, create a new class in the normal manner and name it ‘ScreenManager.cs’. Naturally, you will have a file that looks like this:
A quick, reminder – don’t forget to add in your ‘using’ statements! Moving on, this class is going to need to inherit from the DrawableGameComponent class, just like our GameScreen class did. So change the class definition to this:Code:using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace PongTutorial { class ScreenManager { } }
Simple enough. Now let’s create a constructor for the class. In the body of the class, insert the following:Code:class ScreenManager : DrawableGameComponent
Now, although we need to continue building this class, there is another class we need to exist first. Create a new class and name it ‘Screen.cs’ then add in the normal ‘using’ statements. The Screen class and the ScreenManager class have a mutual dependence upon each other in that one is effectively useless without the other, but together they work in harmony.Code:public ScreenManager(Game1 g) : base(g) { }
Unfortunately, this is where things start to get a bit tricky. In Part 1 of the tutorial, we made our GameScreen class the main platform on which our game was built. This will no longer be the case as the ScreenManager now needs to be the bottom layer of our game. Annoying? Yes, but revising and improving code is just as important as writing it in the first place.
I like metaphors and analogies so here’s one to explain the ScreenManager: Imagine that you have a TV show with one filming set (GameScreen) and a static camera. Now imagine you introduce a few more film sets in to your show. Humbug! Now you need two more cameras! Well, the ScreenManager class is the equivalent of giving your camera wheels. The ScreenManager allows you to decide which set (or sets) are in action at any given time and gives you an easy way of changing the state of different screens.
Right, let’s move on. Now we have our Screen class, we first need to make it abstract, so do so in the normal manner (as covered in Part 1). Next, we need to add the following members to the body of the class:
The first member is likely to be familiar. As for the second member, the name makes it fairly self explanatory – it literally refers to whether the screen is currently active or inactive. By this, I mean whether the screen is being updated and drawn.Code:protected Game1 GAME_REF; public bool Active;
Next we need to add some methods. Add the following underneath the members:
As you’ve probably already noticed, the first member and both methods of this class are present in the GameScreen class. This means that we need to make some changes to our GameScreen class. First DELETE this line from the class:Code:public abstract void Update(GameTime gameTime); public abstract void Draw(GameTime gameTime);
Then change the class declaration so it looks like this:Code:private Game1 GAME_REF;
As you can see we’ve removed our GAME_REF member and changed our class so it inherits from the Screen class instead of the DrawableGameComponent class.Code:class GameScreen : Screen
Now, you’ll also need to remove the call to the base constructor from the constructor declaration (it should be on the second line. You’ll also need to remove the calls to the base class methods in the Update and Draw method. Again, you should know how to do both these things, but if you don’t I can simplify it slightly more: go through the class and remove any line with the word base in it.
Finally we need to make one less obvious change. Find the line in the Draw method that refers to the method Clear called from the Graphics Device object and change it so it appears as follows:
Those of you who’ve been paying close attention will realise that, now that we’re not inheriting from DrawableGameComponent, the handy class that gives us access to much of the Game1 class, we no longer have direct access to the GraphicsDevice. However, we can still access that through our GAME_REF so it’s not all that bad :-)Code:GAME_REF.GraphicsDevice.Clear(Color.Black);
Now we have one last change to make. Open up the Game1 class and find the line where we add our GameScreen as a component. As GameScreen no longer inherits from DrawableGameComponent, it can no longer legally be a member of the Components list. However, don’t delete the line just yet. Instead, change it to the following:
Awesome! With our revised code we now have a game that’s just as well built as before, but now we have the ability to insert more screens as we see fit. Sadly, if you press the debug button now, you’ll likely be presented with an empty screen. This is because our ScreenManager class is still virtually empty.Code:Components.Add(new ScreenManager(this));
The ScreenManager needs to be able to store screens in a way that allows us to manipulate them easily. An array seems like a good choice for this but it’s better to use a List. The List class is a collection type that is just **** awesome. Seriously, if you’ve had any experience with C# and haven’t used the List type then go and read up on it now because when it comes to games, it’s the type of choice for dealing with your game entities, plus it has endless applications and will probably be useful in nearly every program you every create.
Right, now that you’re convinced, add the following lines above the constructor in the ScreenManager class:
...and inside the constructor, add the following:Code:List<Screen> screens; public List<Screen> Screens { get { return screens; } }
In this second piece of code, we initialise our List, we add a GameScreen, then, we make it the active screen. Currently, our GameScreen isn’t going to do anything. When it was a DrawableGameComponent, the Draw and Update methods were called implicitly by the Game1 class. Now, they need to be called explicitly within the ScreenManager. First, override the Update and Draw methods in the ScreenManager class. We did this in Part 1 so naturally you’re a pro at it now ;-) Second, add the following code to the top of the Update method:Code:screens = new List<Screen>(); screens.Add(new GameScreen(g)); screens[0].Active = true;
...and third, add this (almost identical) piece of code to the top of the Draw method:Code:foreach (Screen s in screens) if (s.Active) s.Update(gameTime);
Great, now just hit the Debug button to make sure that it compiles and hey presto! We have the exact same game we had at the end of Part 1 of the tutorial! Admittedly, this is not very impressive but most of the progress we’ve made has been under the surface rather than having an effect on presentation or gameplay. Anyhow, it’s time to move on and introduce another screen!Code:foreach (Screen s in screens) if(s.Active) s.Draw(gameTime);
I’m a Compulsive Liar
Yes, I tricked you again, it’s what I do; you should seriously be used to this by now. Instead of creating another screen, we are going to try adding some sound to our game. To start, we need some sort of class to manage the playing of sounds (you can totally see where this is going) so create a new class name SoundManager.cs and if necessary open it up from the Solution Explorer.
Playing sounds is fairly simple but it’s best to have some measures in control. First, make the class abstract. Once more, you should be a pro at this by now. Add the usual ‘using’ statements and also add this one:
This library’s purpose is given away by its name. Now we need to add some members to the class:Code:using Microsoft.Xna.Framework.Audio;
There’s a new type here, but I’ll explain in a minute. Next add the following two methods:Code:private static bool isBusy = false; private static SoundEffectInstance effectInstance;
There we have it, our SoundManager class is complete. So let’s review: we’ve encountered two new types, an enum, and some methods and properties associated with them. Our new types are SoundEffect and SoundEffectInstance. SoundEffect is to audio what Texture2D is to graphics. We use the ContentManager in the same way as with Texture2D objects to load a sound into a SoundEffect object. SoundEffectInstance is a type that allows more control over a SoundEffect object, such as looping, applying various changes, and state-checking (which is what we’ve used it for). Our PlaySound method will allow us to play sounds effectively, and the overall setup of the class will help us to make sure that the individual sounds don’t clash when playing (as only one sound is able to play at a time). Anyhow, I’ll talk more on that later.Code:public static void PlaySound(SoundEffect effect) { if (!isBusy) { effectInstance = effect.CreateInstance(); effectInstance.Play(); } } public static void Update(GameTime gameTime) { if (effectInstance != null && effectInstance.State == SoundState.Playing) { isBusy = true; } else { isBusy = false; } }
Next, we need to make sure our SoundManager is updated, but first there’s another change we need to make because I forgot to do so earlier :-D Open up the GameScreen class and move the following line to the top of the Update method in the ScreenManager class:
We need to do this because we want our InputManager to update all the time, not just when the GameScreen is active. Next add the following line underneath the above code:Code:InputManager.Update();
Cool, now both our Manager classes update. Cool, now we can start adding the playing of sounds in to our game. First we need a sound asset though. At the bottom of this post you’ll find a zip file named Part2-Resources.zip that you’ll need to download (it also includes all the resources from Part 1). Save it somewhere, then open it and unpack a file named boop.wav. Next, add it to the Content folder of our project, in the Solution Explorer, just like we did with the ball and paddle graphics in Part 1 of the tutorial.Code:SoundManager.Update();
Now, open up our GameBall class and add a ‘using’ statement for the audio library, as we did in our SoundManager class. Next, add the following member to the existing ones at the top of the class:
Next we need to make a change to the parameters of the constructor. Change it so it reads as follows:Code:SoundEffect collisionSound;
Of course, we need to assign our new member with this new parameter so, if you haven’t already done so intuitively, add the following to the bottom of the constructor:Code:public GameBall(Texture2D sprite, Vector2 position, GameComputer computer, GamePlayer player, SoundEffect effect)
Now we need to go and make the appropriate changes in the GameScreen class. Open it up and add a ‘using’ statement for the audio library, then change the line where the GameBall is instantiated to this:Code:this.collisionSound = effect;
As you can see, all we’ve done is added the new parameter to the constructor so that we load in our sound correctly. Awesome, now we can start making the sound play!Code:ball = new GameBall( ballTexture, new Vector2( (float)(container.X + container.Width / 2 - ballTexture.Width / 2), (float)(container.Y + container.Height / 2 - ballTexture.Height / 2) ), computer, player, GAME_REF.Content.Load<SoundEffect>("boop") );
Go back in to the GameBall class and find the Collision method, we’re going to make a few changes:
We’ve add one new line of code in four different places. This is so that, whenever a collision occurs, our ‘boop’ sound will play. Time to debug the code! Once again, if any errors appear, refer to the instructions in Part 1 for help. Now, hit the Debug button and check that our sounds play correctly. If all goes well it should play every time the ball bounces. Brilliant! Time to move on to adding a menu screen to our game :-)Code:public void Collision() { //Check if ball has hit the top or bottom of the screen. if (position.Y <= GameScreen.Container.Y) { directionUp = false; SoundManager.PlaySound(collisionSound); } if (position.Y >= GameScreen.Container.Y + GameScreen.Container.Height - sprite.Height) { directionUp = true; SoundManager.PlaySound(collisionSound); } //Update/create collision rectangles playerRect = new Rectangle((int)player.Position.X, (int)player.Position.Y, player.Sprite.Width, player.Sprite.Height); computerRect = new Rectangle((int)computer.Position.X, (int)computer.Position.Y, computer.Sprite.Width, computer.Sprite.Height); ballRect = new Rectangle((int)this.position.X, (int)this.position.Y, this.sprite.Width, this.sprite.Height); //Check for collision - If so, change direction. if (ballRect.Intersects(playerRect)) { directionLeft = false; SoundManager.PlaySound(collisionSound); } if (ballRect.Intersects(computerRect)) { directionLeft = true; SoundManager.PlaySound(collisionSound); } //Check for ball leaving screen. if (position.X - sprite.Width <= GameScreen.Container.X || position.X >= GameScreen.Container.X + GameScreen.Container.Width) this.Reset(); }
We Don’t Need A Main Menu...
Yes, admittedly, we don’t need a main menu, but let’s just put one in anyway. After all, this is a learning exercise. Right then, first create a new class file named MenuScreen.cs in the usual manner. Second, add the usual ‘using’ statements. Third, change the class declaration so that the class inherits from Screen (as with the GameScreen class).
First we’ll add a member so add the following to the body of the class:
Second, open up the Resources zip file from earlier and add the file ‘mainmenu.png’ to the Content folder in the usual way. Third, add the following constructor to the class:Code:Texture2D titleSprite;
Cool, now override the Draw and Update method just as we did in the GameScreen class. Once you’ve done this add the following to the Draw method:Code:public MenuScreen(Game1 g) { GAME_REF = g; GAME_REF = g; titleSprite = GAME_REF.Content.Load<Texture2D>("mainmenu"); titleContainer = new Rectangle( (int)(0 + (g.GraphicsDevice.Viewport.Width / 2) - (titleSprite.Width / 2)), (int)(0 + (g.GraphicsDevice.Viewport.Height / 2) - (titleSprite.Height / 2)), titleSprite.Width, titleSprite.Height ); }
This will make our graphic draw in the centre of the screen (we’ll load the graphic from the Content folder later). Now, we have a problem, we have no way of telling the ScreenManager to change screens. Now there are two ways we can tackle this: we can provide a reference to the ScreenManager to every screen (which seems a bit overkill), or we can provide access to it through our Game1 class (which all screens already have access to). Obviously we’re going with the second option. To do this, once again we must revise our code. Open up the Game1 class and add the following member and property to the top of the class:Code:GAME_REF.sb.Begin(SpriteBlendMode.AlphaBlend); GAME_REF.GraphicsDevice.Clear(Color.Black); GAME_REF.sb.Draw(titleSprite, titleContainer, Color.White); GAME_REF.sb.End();
Next change the contents of the Initialize method, to this:Code:ScreenManager sManager; public ScreenManager screenManager { get { return this.sManager; } }
But, oh no! Now we’ll have accessibility issues! These occur due to the class used in a property declaration being less accessible than the property itself. Open up the ScreenManager class and change the class declaration to the following:Code:sManager = new ScreenManager(this); Components.Add(sManager);
Next, open up the Screen class and change its class declaration to the following:Code:public class ScreenManager : DrawableGameComponent
Cool, now we have easy access to the screen manager from our screens. Now we can continue with our MenuScreen. We now need to add some logic to allow the game to start when the space bar has been pressed. Insert the following code in to the Update method of the MenuScreen class:Code:public abstract class Screen
Here we’re activating the GameScreen and deactivating the MenuScreen when the user presses the space bar. Now we need to make a quick change so that the menu screen appears before the game screen does. Open up the ScreenManager class and change the entire contents of the constructor to the following:Code:if (InputManager.KeySpace) { GAME_REF.screenManager.Screens[1].Active = true; this.Active = false; }
Cool, now we can see our new screen in action. Hit the Debug button and, if all goes well, you should be presented with the main menu screen. Hit space bar and the game should begin. Awesome! Our game now has a little more substance to it. Currently though, our game never ends and there is no way for users to return to the title screen. This is because our game has no scoring system and so there is no end point at which either the player or computer wins. I guess it’s time to ad one in.Code:screens = new List<Screen>(); screens.Add(new MenuScreen(g)); screens.Add(new GameScreen(g)); screens[0].Active = true; screens[1].Active = false;
And The Winner Is...
Well, I should hope this won’t take to long. After all, the revisions we’ve made throughout this tutorial have made our code much more robust. We’ll start by creating a class for managing the scores, which we will name – you guessed it! – ScoreManager.cs so create this file in the usual way. Next add the following properties:
Cool now let’s initialise these in the GameScreen class. Add the following to the GameScreen constructor:Code:public static int PlayerScore { get; set; } public static int ComputerScore { get; set; }
Awesome now we need to make sure the score increases each time the ball goes behind a paddle. Open up the GameBall class and find the Reset method. Change the method so it appears as follows:Code:ScoreManager.PlayerScore = 0; ScoreManager.ComputerScore = 0;
As you can see we’ve added a parameter to the method and added an if statement to the bottom that increases the score based on the value of the parameter. For this work we need to pass the correct parameter in calls to this method.Code:private void Reset(int p) { //Reset the position to the centre of the screen. position = new Vector2( (float)(GameScreen.Container.X + GameScreen.Container.Width / 2 - sprite.Width / 2), (float)(GameScreen.Container.Y + GameScreen.Container.Height / 2 - sprite.Height / 2) ); //Flip directions. directionLeft = !directionLeft; directionUp = !directionUp; if (p == 1) ScoreManager.PlayerScore++; else if (p == 2) ScoreManager.ComputerScore++; }
To do this, find the ‘if’ statement that contains the call to the Reset method near the bottom of the Collision method and change it so it reads as follows:
Now we’ve made it so that, when the ball passes off the screen, the correct player scores a point. Next we need to display the score on the screen. To do this we need to use a type we haven’t seen before – the SpriteFont. This class provides an interface between installed system fonts and the texture drawing components of our game. First we need to create a SpriteFont file:Code:if (position.X - sprite.Width <= GameScreen.Container.X) this.Reset(2); if (position.X >= GameScreen.Container.X + GameScreen.Container.Width) this.Reset(1);
1. Right-click the Content folder in the Solution Explorer.
2. Highlight ‘Add’, then select ‘New Item...’
3. Select ‘Sprite Font’ and name the file ‘font.spritefont’.
Our new file should open up automatically, and you should be presented with an XML-style layout. If you’re not familiar with XML, it’s basically just a way of representing data. The two items we are concerned with are ‘FontName’ and ‘FontSize’.
Now, for those of you who are familiar with XML, spritefonts are just basic XML files which can be serialised in to SpriteFont objects. The loading of the font and conversion into Texture2D is performed by the Content Processor and the object itself so we don’t have to worry about it.
Now, in our file you’ll see the name ‘Kootenay’ between the FontName tags. Change it so the line appears as follows:
Now change the line with the FontSize tags below, so that it appear as follows:Code:<FontName>Courier New</FontName>
That’s all! Now close the spritefont file and open up the GameScreen file. Add the following member to the existing ones in this class:Code:<Size>32</Size>
...then add the following to the bottom of the constructor:Code:private SpriteFont font;
Nice, now we have an easy way of printing the score to the screen. To do this we once again use the SpriteBatch, but instead of the Draw method, we call the DrawString method. Simple enough right? Of course it is, I’m not even kidding, the only complicated part of the next bit is getting the positioning spot on and even that’s not so hard. Add the following above the call to the End method, near the bottom of the Draw method:Code:font = g.Content.Load<SpriteFont>("font");
Now these DrawString calls probably look scarier than they are. We’re just telling the SpriteBatch to draw the scores on the correct sides of the screen, and at the top, and to keep the scores centred at their positions.Code:GAME_REF.sb.DrawString(font, ScoreManager.PlayerScore.ToString(), new Vector2( 0 + Container.Width / 2 - font.MeasureString(ScoreManager.PlayerScore.ToString()).X - 20, 10), Color.White); GAME_REF.sb.DrawString(font, ScoreManager.ComputerScore.ToString(), new Vector2( 0 + Container.Width / 2 + 20, 10), Color.White);
We’re almost at the end! One last thing to do, make sure that the game ends when one of our players hits 10 points. Add the following method to the bottom of the ScreenManager class:
Last, add a call to the method in the Update method for the class. I’m pretty sure I don’t need to show you how to do this ;-)Code:public void CheckForWinner() { if (ScoreManager.ComputerScore == 10 || ScoreManager.PlayerScore == 10) { screens[1] = new GameScreen((Game1)Game); screens[0].Active = true; screens[1].Active = false; } }
And that’s all for this time! Seriously, that’s all I’m going to show you. Sure we could add more stuff to the game but that’s your job. Now I’ll leave you with a list of things for you to try adding to the game yourself:
- A Win/Lose screen that appears at the end of the game.
- A line down the middle of the screen to separate the playing field.
- A sound that plays when the player or computer scores a point.
- A sound that plays when the game ends.
- ADVANCED: An options screen that can be accessed from the Main Menu, in which the user can turn sounds On or Off.
Enjoy yourselves, and I’ll be back soon with some additional material to help you build your own creations!
So, until next time...
Last edited by Roger; 01-28-2011 at 09:56 PM.
Thanks you so much!
I try it
There are currently 1 users browsing this thread. (0 members and 1 guests)
Bookmarks