Lets Write Pong


Let’s Write Pong

You got the concept all figured out and have all the files you need to get started. It’s time to do some actual coding. First take a look at the SpriteBatch class and see how you can easily manage all the sprites. The SpriteBatch class is not only useful to render sprites in the same format as the graphics were saved, but it is possible to scale them up or down, recolor them, and even rotate them around.

After putting the menu together you will add the paddles and move them with help of the input capabilities you already saw in Chapter 1. The ball itself moves with help of some simple variables, and every time you hit one of the paddles you bounce the ball back and play the PongBallHit.wav sound. If the ball goes out of the screen on the left or right side, the PongBallLost.wav sound is played and the player loses one life.

A few unit tests will be used to make sure that the menu and the basic game work. Then you will add additional unit tests to handle the complicated parts like hitting the ball from the sides of the paddle and fine-tuning the game play. For multiplayer support you will have a unit test to test out the controls, which are then wired up to the main menu option to support the multiplayer mode.

In the next part of this chapter you will then test the whole game on the Xbox 360 and think about more improvements you can make to the game.

Sprites

As you saw in Chapter 1 the SpriteBatch class is used to render your textures directly on the screen. Because you don’t have any helper classes yet you will render everything the same way. For more information about helper classes to make your everyday game programming life easier, check out Chapter 3. For your Pong game you use two layers: the Space Background, which is loaded from the PongBackground.dds texture, and the menu and game textures showing the menu texts and the game components (paddles and ball).

To load all textures you use the following lines. First of all you have to define the textures you are using here:

  Texture2D backgroundTexture, menuTexture, gameTexture; 

Then load everything in the Initialize method:

  // Load all our content backgroundTexture = content.Load<Texture2D>("PongBackground"); menuTexture = content.Load<Texture2D>("PongMenu"); gameTexture = content.Load<Texture2D>("PongGame"); 

And finally you can render the background with the SpriteBatch methods you learned about in Chapter 1:

  // Draw background texture in a separate pass, else it gets messed up // with our other sprites, the ordering does not really work great. spriteBatch.Begin(); spriteBatch.Draw(backgroundTexture,   new Rectangle(0, 0, width, height),   Color.LightGray); spriteBatch.End(); 

LightGray for the color means that you darken down the background a little bit for a better contrast to the foreground items (menu text and game elements). As you can see it is not just one line to render a sprite on the screen, and it will get a lot more complicated to render just parts of your sprite textures. Take a look at the rectangles you use for the game:

  static readonly Rectangle   XnaPongLogoRect = new Rectangle(0, 0, 512, 110),   MenuSingleplayerRect = new Rectangle(0, 110, 512, 38),   MenuMultiplayerRect = new Rectangle(0, 148, 512, 38),   MenuExitRect = new Rectangle(0, 185, 512, 38),   GameLifesRect = new Rectangle(0, 222, 100, 34),   GameRedWonRect = new Rectangle(151, 222, 155, 34),   GameBlueWonRect = new Rectangle(338, 222, 165, 34),   GameRedPaddleRect = new Rectangle(23, 0, 22, 92),   GameBluePaddleRect = new Rectangle(0, 0, 22, 92),   GameBallRect = new Rectangle(1, 94, 33, 33),   GameSmallBallRect = new Rectangle(37, 108, 19, 19); 

That is quite a lot of rectangles, but it is still simpler to just use these constant values than to import some xml data, for example. Static read-only is used instead of constants here because constants can’t be assigned to structs and static read-only behaves the same way as constants. You might ask how to get these values and how to make sure they are correct.

Unit Testing in Games

This is where unit testing comes into play. Unit testing for game programming basically means to split up your problems into small, easily manageable problems. Even for this very simple game it is still a good idea to write unit tests. Unit testing is perfect to align textures on the screen, test sound effects, and add collision testing. Originally I planned to write this chapter and the Pong game without using unit testing, but as soon as I programmed the game I couldn’t stop myself and before I knew it I had already written six unit tests, and I was convinced that this is the way to go and it would not make sense to delete the very useful tests.

For example, to test out the menu graphics rectangles you use the following unit test:

  public static void TestMenuSprites() {   StartTest(     delegate     {       testGame.RenderSprite(testGame.menuTexture,         512-XnaPongLogoRect.Width/2, 150,         XnaPongLogoRect);       testGame.RenderSprite(testGame.menuTexture,         512-MenuSingleplayerRect.Width/2, 300,         MenuSingleplayerRect);       testGame.RenderSprite(testGame.menuTexture,         512-MenuMultiplayerRect.Width/2, 350,         MenuMultiplayerRect, Color.Orange);       testGame.RenderSprite(testGame.menuTexture,         512-MenuExitRect.Width/2, 400,         MenuExitRect);    }); } // TestMenuSprites() 

Please note: This is not the final way you will do unit testing in this book. You just use the very basic idea here to execute unit tests. The delegate holds the code you will execute each frame in the Draw method.

You might ask yourself: What is StartTest? What about testGame or the RenderSprite method? Where do they come from? Well, this is one of the main differences between the old way of game programming and using an agile approach with unit testing. All these methods do not exist yet. Similar to how you planned your game, you also plan your unit tests by just writing down how you want to test something; in this case displaying the game logo and the three menu entries (Singleplayer, Multiplayer, and Exit).

After writing a unit test and all syntax errors are fixed you can immediately start testing by compiling your code - just press F5 and you will see a couple of errors. These errors have to be fixed step by step and then the unit test can be started. Static unit tests do not use Assert functions very often, but it is possible to add some code to throw exceptions if some values are not as expected. For your unit tests you will just test them by looking at the result on the screen and then modify the RenderSprite method until everything works the way you want.

The next chapter talks in greater detail about unit testing. For the Pong game, you just derive from the PongGame class and add a simple delegate to render custom code in your unit tests:

  delegate void TestDelegate(); class TestPongGame : PongGame {   TestDelegate testLoop;   public TestPongGame(TestDelegate setTestLoop)   {     testLoop = setTestLoop; } // TestPongGame(setTestLoop)   protected override void Draw(GameTime gameTime)   {     base.Draw(gameTime);     testLoop();   } // Draw(gameTime) } // class TestPongGame 

Now you can write the very simple StartTest method to create an instance of TestPongGame and then call Run to execute the Draw method with your custom testLoop code:

  static TestPongGame testGame; static void StartTest(TestDelegate testLoop) {   using (testGame = new TestPongGame(testLoop))   {     testGame.Run();   } // using } // StartTest(testLoop) 

The static instance testGame is used to make writing the unit tests easier, but it can be confusing if you use it somewhere else because it will only be valid after StartTest is called. In the next chapters you will see a better way to do all this.

Now two of the errors from the first version of the unit test are fixed; only the RenderSprite method is missing now. It is ok to add an empty method just to make the unit test work:

  public void RenderSprite(Texture2D texture, int x, int y,   Rectangle sourceRect, Color color) {   //TODO } // RenderSprite(texture, rect, sourceRect) public void RenderSprite(Texture2D texture, int x, int y,   Rectangle sourceRect) {   //TODO } // RenderSprite(texture, rect, sourceRect) 

After adding these two methods you can now execute the TestMenuSprites method. How to do that? With TestDriven.NET you can just right-click and select “Start Test,” but XNA Game Studio Express does not support plugins and you have to write your own way of unit testing by changing the Main method in the Program.cs file:

  static void Main(string[] args) {   //PongGame.StartGame();   PongGame.TestMenuSprites(); } // Main(args) 

As you can see I have also extracted the StartGame method to make the Main method easier to read and to make it easy to change unit tests. StartGame uses just the standard code:

  public static void StartGame() {   using (PongGame game = new PongGame())   {     game.Run();   } // using } // StartGame() 

If you press F5 now the unit test is executed instead of the normal game code. Because RenderSprite does not contain any code yet you will just see the space background that is drawn in the Draw method of PongGame. Now you add the code to make the menu work. You already know how to render sprites, but it is highly ineffective to start and end a SpriteBatch for every single RenderSprite call. Create a simple list of sprites you want to render each frame and add a new entry every time you call RenderSprite. Then at the end of the frame you will just draw all sprites:

  class SpriteToRender {   public Texture2D texture;   public Rectangle rect;   public Rectangle? sourceRect;   public Color color;   public SpriteToRender(Texture2D setTexture, Rectangle setRect,     Rectangle? setSourceRect, Color setColor)   {     texture = setTexture;     rect = setRect;     sourceRect = setSourceRect;     color = setColor;   } // SpriteToRender(setTexture, setRect, setColor) } // SpriteToRender List<SpriteToRender> sprites = new List<SpriteToRender>(); 

By the way: You added all this code, including the unit tests, to your PongGame class. Usually you would want to reuse code and extend games later and it is better to split everything up in multiple classes. To keep things simple and because you won’t use much of this code later, everything is just written down in the fastest way possible. Though this is clearly not the cleanest and most elegant way to code, it is usually the fastest and most efficient way to make your unit tests run. At a later point you can refactor your code to make it more elegant and reusable. Thanks to unit testing you always have a strong tool to make sure everything is still functioning after changing the layout of the code several times.

You might have seen that “Rectangle?” is used instead of just “Rectangle” for the sourceRect in the preceding code. “Rectangle?” means that this type is null-able and you can just pass in null for this argument, which makes it possible to create overloads of RenderSprite that don’t use sourceRect to just render the full texture:

  public void RenderSprite(Texture2D texture, Rectangle rect,   Rectangle? sourceRect, Color color) {   sprites.Add(new SpriteToRender(texture, rect, sourceRect, color)); } // RenderSprite(texture, rect, sourceRect, color) 

That’s pretty straightforward. The DrawSprites method, which is called at the end of the Draw method, is also not very complicated:

  public void DrawSprites() {   // No need to render if we got no sprites this frame   if (sprites.Count == 0)     return;   // Start rendering sprites   spriteBatch.Begin(SpriteBlendMode.AlphaBlend,     SpriteSortMode.BackToFront, SaveStateMode.None);   // Render all sprites   foreach (SpriteToRender sprite in sprites)     spriteBatch.Draw(sprite.texture,       // Rescale to fit resolution       new Rectangle(       sprite.rect.X * width / 1024,       sprite.rect.Y * height / 768,       sprite.rect.Width * width / 1024,       sprite.rect.Height * height / 768),       sprite.sourceRect, sprite.color);   // We are done, draw everything on screen with help of the end method.   spriteBatch.End();   // Kill list of remembered sprites   sprites.Clear(); } // DrawSprites() 

Though it is not very important for this game, at least on the Windows platform where you use 1024×768 as the default resolution anyway, you will rescale all sprites from 1024×768 to the current resolution. Please note that all textures for this game and all upcoming games in this book are usually stored in the 1024×768 resolution. The code in DrawSprites makes sure that all sprites are scaled correctly to the currently used resolutions. For example, on the Xbox 360 several resolutions are possible and will force the game to run in these resolutions, which you can’t know beforehand. For this reason Xbox 360 games should be resolution independent and allow HDTV-like 1080p (1920×1080) formats if possible.

Basically, DrawSprites checks if there are any sprites to render, else just exit the function. Then render all sprites with the default alpha blend mode and sorted from back to front without saving the states, which means that if you change any render state of XNA it will not be restored when End is called. Usually you will always want to use SaveStateMode.None because it is the fastest, and through your unit tests you make sure everything works out and is not changed in a way that messes up your rendering done after this method.

You might think “All that just to render the main menu graphics?” but if you press F5 now you can see the screen shown in Figure 2-5. Because you have implemented the basic code for your game, all the sprite rendering code, and everything you need for your unit tests, you have almost completed 50% of the work already. You now just need to add the game graphics, the controls, and some simple ball collision code and you are done.

image from book
Figure 2-5

Adding the Ball and Paddles

To add the ball, paddles, and all other game components you need you use another unit test called TestGameSprites:

  public static void TestGameSprites() {   StartTest(     delegate     {       // Show lives       testGame.ShowLives();       // Ball in center       testGame.RenderBall();       // Render both paddles       testGame.RenderPaddles();     }); } // TestGameSprites() 

This unit test represents the methodology of the agile development process even better than the last unit test. As you can see you just took a look at the design concept and implemented everything at a very high level. At the top you see the number of lives each player has. Then there is the ball in the middle and each player has his paddle. The screen border does not use any special graphics and you already have the background.

Just to make sure you understand the way these unit tests are done, add them and press F5 after adding this to the Main method and commenting out the old unit test:

  //PongGame.StartGame(); //PongGame.TestMenuSprites(); PongGame.TestGameSprites(); 

You will get three error messages because none of the three new methods of TestGameSprites is implemented yet. Now after you see these errors you know exactly that these are your next three steps, and if all of them are implemented and tested, this test is complete and you continue with the next part of your game. I cannot mention this often enough: This really makes the overall process much more straightforward and it seems like you planned everything from start to finish, but as you saw earlier you just wrote down one simple page of your game idea. Everything else was just designed and created as you go from the top-level design down to the implementation at the lowest level. Take a look at the three new methods:

  public void ShowLives() {   // Left players lives   RenderSprite(menuTexture, 2, 2, GameLivesRect);   for (int num = 0; num < leftPlayerLives; num++)     RenderSprite(gameTexture, 2+GameLivesRect.Width+       GameSmallBallRect.Width*num-2, 9,       GameSmallBallRect);   // Right players lives   int rightX = 1024-GameLivesRect.Width-GameSmallBallRect.Width*3-4;   RenderSprite(menuTexture, rightX, 2, GameLivesRect);   for (int num = 0; num < rightPlayerLives; num++)     RenderSprite(gameTexture, rightX+GameLivesRect.Width+       GameSmallBallRect.Width*num-2, 9,       GameSmallBallRect); } // ShowLives() 

ShowLives just shows the “Lives:” text for both players and adds the number of lives as small balls from the game texture. RenderBall is even simpler:

  public void RenderBall() {   RenderSprite(gameTexture,     (int)((0.05f+0.9f*ballPosition.X)*1024)     GameBallRect.Width/2,     (int)((0.02f+0.96f*ballPosition.Y)*768)     GameBallRect.Height/2,     GameBallRect); } // RenderBall() 

And finally you have RenderPaddles to show the left and right paddles at the current position:

  public void RenderPaddles() {  RenderSprite(gameTexture,    (int)(0.05f*1024)-GameRedPaddleRect.Width/2,    (int)((0.06f+0.88f*leftPaddlePosition)*768)    GameRedPaddleRect.Height/2,    GameRedPaddleRect);  RenderSprite(gameTexture,    (int)(0.95f*1024)-GameBluePaddleRect.Width/2,    (int)((0.06f+0.88f*rightPaddlePosition)*768)    GameBluePaddleRect.Height/2,    GameBluePaddleRect); } // RenderPaddle(leftPaddle) 

Before you even wonder about the all the floating-point numbers in RenderBall and RenderPaddles, these are the new variables you need for your game to keep track of the current ball and paddles positions:

  /// <summary> /// Current paddle positions, 0 means top, 1 means bottom. /// </summary> float leftPaddlePosition = 0.5f,   rightPaddlePosition = 0.5f; /// <summary> /// Current ball position, again from 0 to 1, 0 is left and top, /// 1 is bottom and right. /// </summary> Vector2 ballPosition = new Vector2(0.5f, 0.5f); /// <summary> /// Ball speed vector, randomized for every new ball. /// Will be set to Zero if we are in menu or game is over. /// </summary> Vector2 ballSpeedVector = new Vector2(0, 0); 

Now it might be a little bit clearer why you work with floating-point numbers in the render methods. This way you don’t have to deal with screen coordinates, multiple resolutions, and checking screen borders. Both the ball and the paddles just stay between 0 and 1. For the x coordinate 0 means you are at the left border, and 1 means you are at the right border of the screen. Same thing for the y coordinate and the paddles; 0 is the topmost position and 1 is the bottom of the screen. You also use a speed vector to update the ball position each frame, which is discussed in a minute.

The paddles are just rendered on the screen; you put the left paddle (red) to the left side and add 5% to make it more visible and add a little area behind it where the ball can move to lose a life for this player. The same thing happens for the right paddle (blue) at the right side at 95% (which is 0.95f) of the screen width. Take a look at the output you see after pressing F5 now (see Figure 2-6).

image from book
Figure 2-6

That does look like the game is almost done right now. Although unit testing is great and gives you great results in a quick manner, it does not mean you are done yet. The input and the collision testing still have to be done.

Handling Player Input

As you saw in Chapter 1, capturing the keyboard and gamepad input is quite easy in XNA. Writing an extra unit test just for that would be overkill; you already know how this works and you just want to test controlling the paddles here. You don’t even need a new unit test; you can just use the TestGameSprites test and maybe rename it to TestSingleplayerGame. The content of the unit test stays the same; you will now just change input handling and update the paddle positions in the Update method of PongGame:

  // Get current gamepad and keyboard states gamePad = GamePad.GetState(PlayerIndex.One); gamePad2 = GamePad.GetState(PlayerIndex.Two); keyboard = Keyboard.GetState(); gamePadUp = gamePad.DPad.Up == ButtonState.Pressed ||    gamePad.ThumbSticks.Left.Y > 0.5f; gamePadDown = gamePad.DPad.Down == ButtonState.Pressed ||     gamePad.ThumbSticks.Left.Y  < -0.5f; gamePad2Up = gamePad2.DPad.Up == ButtonState.Pressed ||     gamePad2.ThumbSticks.Left.Y  > 0.5f; gamePad2Down = gamePad2.DPad.Down == ButtonState.Pressed ||     gamePad2.ThumbSticks.Left.Y  < -0.5f; // Move half way across the screen each second float moveFactorPerSecond = 0.5f *   (float)gameTime.ElapsedRealTime.TotalMilliseconds / 1000.0f; // Move up and down if we press the cursor or gamepad keys. if (gamePadUp ||   keyboard.IsKeyDown(Keys.Up))   rightPaddlePosition -= moveFactorPerSecond; if (gamePadDown ||   keyboard.IsKeyDown(Keys.Down))   rightPaddlePosition += moveFactorPerSecond; // Second player is either controlled by player 2 or by the computer if (multiplayer) {   // Move up and down if we press the cursor or gamepad keys.   if (gamePad2Up ||     keyboard.IsKeyDown(Keys.W))     leftPaddlePosition -= moveFactorPerSecond;   if (gamePad2Down ||     keyboard.IsKeyDown(Keys.S))     leftPaddlePosition += moveFactorPerSecond; } // if else {   // Just let the computer follow the ball position   float computerChange = ComputerPaddleSpeed * moveFactorPerSecond;   if (leftPaddlePosition > ballPosition.Y + computerChange)     leftPaddlePosition -= computerChange;   else if (leftPaddlePosition < ballPosition.Y - computerChange)     leftPaddlePosition += computerChange; } // else // Make sure paddles stay between 0 and 1 if (leftPaddlePosition < 0)   leftPaddlePosition = 0; if (leftPaddlePosition > 1)   leftPaddlePosition = 1; if (rightPaddlePosition < 0)   rightPaddlePosition = 0; if (rightPaddlePosition > 1)   rightPaddlePosition = 1; 

You might see a few new variables here (multiplayer, gamePad, gamePad2, keyboard, and ComputerPaddleSpeed), but this section focuses on the code that changes the paddle positions for now. To make sure you always move the ball and the paddles at the same speed no matter how many frames you get, moveFactorPerSecond is calculated. moveFactorPerSecond will be 1 for 1 fps (frame per second), 0.1 for 10 fps, 0.01 for 100 fps, and so on.

Next you will change the right paddle position if up or down buttons or cursor keys are pressed. The left paddle is either controlled by player 2 using a second gamepad if available or the W and S keys. If multiplayer is not true you are in singleplayer mode and the left paddle is controlled by the computer, which just follows the ball limited by the ComputerPaddleSpeed, which is 0.5f. The ball will initially move much slower, but the speed gets increased at each contact and you can also hit the ball with the edge of the paddles to make it speed up. Then the computer will not catch up and you can win.

To make the new Update method work, add the new variables and constants:

  /// <summary> /// Ball speed multiplicator, this is how much screen space the ball /// will travel each second. /// </summary> const float BallSpeedMultiplicator = 0.5f; /// <summary> /// Computer paddle speed. If the ball moves faster up or down than /// this, the computer paddle can't keep up and finally we will win. /// </summary> const float ComputerPaddleSpeed = 0.5f; /// <summary> /// Game modes /// </summary> enum GameMode {   Menu,   Game,   GameOver, } // enum GameMode GamePadState gamePad, gamePad2; KeyboardState keyboard; bool gamePadUp = false,     gamePadDown = false,     gamePad2Up = false,     gamePad2Down = false; /// <summary> /// Are we playing a multiplayer game? If this is false, the computer /// controls the left paddle. /// </summary> bool multiplayer = false; /// <summary> /// Game mode we are currently in. Very simple game flow. /// </summary> GameMode gameMode = GameMode.Menu; /// <summary> /// Currently selected menu item. /// </summary> int currentMenuItem = 0; 

For the current test you just need the variables I mentioned before. However, take a look at all the rest of the variables you need for the game. BallSpeedMultiplicator determinates how fast the ball is and therefore how fast the game is. The game mode is used to handle all the three game modes you can be in. Either you just started the game and are in the menu or you are in the game. When you are in the game, but one player lost, the mode is changed to the game over state, showing the winner.

You don’t need this code yet, but it is the last part of input you have to handle, so take a look at the input for the menu:

  // Show screen depending on our current screen mode if (gameMode == GameMode.Menu) {   // Show menu   RenderSprite(menuTexture,       512-XnaPongLogoRect.Width/2, 150, XnaPongLogoRect);   RenderSprite(menuTexture,       512-MenuSingleplayerRect.Width/2, 300, MenuSingleplayerRect,       currentMenuItem == 0 ? Color.Orange : Color.White);   RenderSprite(menuTexture,       512-MenuMultiplayerRect.Width/2, 350, MenuMultiplayerRect,       currentMenuItem == 1 ? Color.Orange : Color.White);   RenderSprite(menuTexture,   512-MenuExitRect.Width/2, 400, MenuExitRect,   currentMenuItem == 2 ? Color.Orange : Color.White);   if ((keyboard.IsKeyDown(Keys.Down) ||     gamePadDown) &&     remDownPressed == false)   {     currentMenuItem = (currentMenuItem + 1)%3;   } // else if   else if ((keyboard.IsKeyDown(Keys.Up) ||     gamePadUp) &&     remUpPressed == false)   {     currentMenuItem = (currentMenuItem + 2)%3;   } // else if   else if ((keyboard.IsKeyDown(Keys.Space) ||     keyboard.IsKeyDown(Keys.LeftControl) ||     keyboard.IsKeyDown(Keys.RightControl) ||     keyboard.IsKeyDown(Keys.Enter) ||     gamePad.Buttons.A == ButtonState.Pressed ||     gamePad.Buttons.Start == ButtonState.Pressed ||     // Back or Escape exits our game     keyboard.IsKeyDown(Keys.Escape) ||     gamePad.Buttons.Back == ButtonState.Pressed) &&     remSpaceOrStartPressed == false &&     remEscOrBackPressed == false)   {     // Quit app.     if (currentMenuItem == 2 ||       keyboard.IsKeyDown(Keys.Escape) ||   gamePad.Buttons.Back == ButtonState.Pressed)     {       this.Exit();     } // if     else     {       // Start game .. handle game, etc. .. 

There are a few new variables like remDownPressed or gamePadUp, which are used to make handling the input a little easier. Check out the source code for additional details. The next chapter discusses the Input helper class in more detail, and that will simplify the process even more.

That’s all you need to know for handling the input in this Pong game. If you execute the unit test again you can still see the same screen as before, but you can now control the paddles.

Collision Testing

To move the ball away from the middle, the following method has to be called in your unit test TestSingleplayerGame. The ball moves to a random location (at least randomly in the four directions you have defined here):

  /// <summary> /// Start new ball at the beginning of each game and when a ball is /// lost. /// </summary> public void StartNewBall() {   ballPosition = new Vector2(0.5f, 0.5f);   Random rnd = new Random((int)DateTime.Now.Ticks);   int direction = rnd.Next(4);   ballSpeedVector =     direction == 0 ? new Vector2(1, 0.8f) :     direction == 1 ? new Vector2(1, -0.8f) :     direction == 2 ? new Vector2(-1, 0.8f) :     new Vector2(-1, -0.8f); } // StartNewBall() 

In the Update method the ball position gets updated based on the ballSpeedVector:

  // Update ball position ballPosition += ballSpeedVector *   moveFactorPerSecond * BallSpeedMultiplicator; 

If you start the game now with your unit test the ball will move away from the center and go out of the screen, which is not really cool. This is why you need collision testing. Take a look at the concept again (see Figure 2-7) and add some improvements for the collision code. This is one of the times it makes sense to go back to the concept and make improvements based on your new ideas and knowledge. There are three kinds of collisions that can happen:

image from book
Figure 2-7

  • Collision with Screen Border at the top and bottom screen borders

  • Collision with a Paddle to push the ball back to the other player

  • Lose life if collision with screen border behind paddle happens. Player will lose a life when this happens and the ball is reset with the help of StartNewBall.

You could continue using TestSingleplayerGame to check out the collisions, but it is far easier to construct a few tests and have every single issue tested. Again, unit testing is great for exactly this kind of problem. You have a clear idea on what you have to do now, but you don’t know yet how to do it. Just write a unit test and then work on the implementation:

  public static void TestBallCollisions() {   StartTest(     delegate     {       // Make sure we are in the game and in singleplayer mode       testGame.gameMode = GameMode.Game;       testGame.multiplayer = false;       testGame.Window.Title =         "Xna Pong - Press 1-5 to start collision tests";       // Start specific collision scene based on the user input.       if (testGame.keyboard.IsKeyDown(Keys.D1))       {         // First test, just collide with screen border         testGame.ballPosition = new Vector2(0.6f, 0.9f);         testGame.ballSpeedVector = new Vector2(1, 1);       } // if       else if (testGame.keyboard.IsKeyDown(Keys.D2))       {         // Second test, straight on collision with right paddle         testGame.ballPosition = new Vector2(0.9f, 0.6f);         testGame.ballSpeedVector = new Vector2(1, 1);         testGame.rightPaddlePosition = 0.7f;       } // if       else if (testGame.keyboard.IsKeyDown(Keys.D3))       {         // Thrid test, straight on collision with left paddle         testGame.ballPosition = new Vector2(0.1f, 0.4f);         testGame.ballSpeedVector = new Vector2(-1, -0.5f);         testGame.leftPaddlePosition = 0.35f;       } // if       else if (testGame.keyboard.IsKeyDown(Keys.D4))       {         // Advanced test to check if we hit the edge of the right paddle         testGame.ballPosition = new Vector2(0.9f, 0.4f);         testGame.ballSpeedVector = new Vector2(1, -0.5f);         testGame.rightPaddlePosition = 0.29f;       } // if       else if (testGame.keyboard.IsKeyDown(Keys.D5))       {         // Advanced test to check if we hit the edge of the right paddle         testGame.ballPosition = new Vector2(0.9f, 0.4f);         testGame.ballSpeedVector = new Vector2(1, -0.5f);         testGame.rightPaddlePosition = 0.42f;       } // if       // Show lifes       testGame.ShowLives();       // Ball in center       testGame.RenderBall();       // Render both paddles       testGame.RenderPaddles();     }); } // TestBallCollisions () 

The idea here is to press 1–5 to set up custom collision test scenes. For example, if 1 is pressed the ball is moved to (0.6, 0.9), which is near the center at the bottom of the screen. The ball speed is set to (1, 1) to make sure it moves to the screen border, where the ball should bounce off as described in the concept. If you press 4 or 5 the advanced paddle tests are started to check if you hit the edges of the right paddle, which requires a little bit more fine-tuning than the other quite simple collision tests. Collision testing is done in the Update method of PongGame.

Now you can start testing. Obviously if you just start the test now, it will not work because you have not implemented any collision testing yet.

Testing if you hit the top or bottom screen borders is the easiest test; all of the following code is added to the Update method just before you update the ball position for the next frame:

  // Check top and bottom screen border if (ballPosition.Y < 0 ||   ballPosition.Y > 1) {   ballSpeedVector.Y = -ballSpeedVector.Y;   // Move ball back into screen space   if (ballPosition.Y < 0)     ballPosition.Y = 0;   if (ballPosition.Y > 1)     ballPosition.Y = 1; } // if 

The important part here is to just invert the y part of the ball speed vector. Sometimes it can happen that in the next frame the moveFactorPerSecond is smaller than in this frame. Then the ball position can be still out of the screen border and you would invert the y part of the speed vector every frame. To fix this issue you make sure the ball position is always inside the screen and does not goes outside. The same adjustment is done for the paddles too.

The paddle collisions are a little bit more complicated. If you just want to test the top and bottom screen collision, just start the unit test with F5 now. To do the paddle collisions, bounding boxes are constructed to perform the intersection tests, which are available in the BoundingBox class of XNA. The BoundingBox struct uses Vector3 struct and works in 3D space, but you can just ignore the z value and always use 0 and use it just fine in 2D space of your Pong game:

  // Check for collisions with the paddles. // Construct bounding boxes to use the intersection helper method. Vector2 ballSize = new Vector2(   GameBallRect.Width / 1024.0f, GameBallRect.Height / 768.0f); BoundingBox ballBox = new BoundingBox(   new Vector3(ballPosition.X - ballSize.X / 2,   ballPosition.Y - ballSize.Y / 2, 0),   new Vector3(ballPosition.X + ballSize.X / 2,   ballPosition.Y + ballSize.Y / 2, 0)); Vector2 paddleSize = new Vector2(   GameRedPaddleRect.Width / 1024.0f,   GameRedPaddleRect.Height / 768.0f); BoundingBox leftPaddleBox = new BoundingBox(   new Vector3(-paddleSize.X/2,   leftPaddlePosition-paddleSize.Y/2, 0),   new Vector3(+paddleSize.X/2,   leftPaddlePosition+paddleSize.Y/2, 0)); BoundingBox rightPaddleBox = new BoundingBox(   new Vector3(1-paddleSize.X/2,   rightPaddlePosition-paddleSize.Y/2, 0),   new Vector3(1+paddleSize.X/2,   rightPaddlePosition+paddleSize.Y/2, 0)); // Ball hit left paddle? if (ballBox.Intersects(leftPaddleBox)) {   // Bounce of the paddle   ballSpeedVector.X = -ballSpeedVector.X;   // Increase speed a little   ballSpeedVector *= 1.05f;   // Did we hit the edges of the paddle?   if (ballBox.Intersects(new BoundingBox(     new Vector3(leftPaddleBox.Min.X - 0.01f,       leftPaddleBox.Min.Y - 0.01f, 0),     new Vector3(leftPaddleBox.Min.X + 0.01f,       leftPaddleBox.Min.Y + 0.01f, 0))))     // Bounce of at a more difficult angle for the other player     ballSpeedVector.Y = -2;   else if (ballBox.Intersects(new BoundingBox(     new Vector3(leftPaddleBox.Min.X - 0.01f,       leftPaddleBox.Max.Y - 0.01f, 0),     new Vector3(leftPaddleBox.Min.X + 0.01f,       leftPaddleBox.Max.Y + 0.01f, 0))))   // Bounce of at a more difficult angle for the other player   ballSpeedVector.Y = +2;   // Move away from the paddle   ballPosition.X += moveFactorPerSecond * BallSpeedMultiplicator; } // if 

The bounding boxes are constructed in a similar way as the rendering code is handled in RenderBall and RenderPaddles. The edge detection code makes it a little bit more complicated and it is just a quick and dirty way to speed up the ball if you hit it with the edge of your paddle. But these few lines make the game a lot more fun.

Exactly the same code you use for the left paddle is also used for the right paddle; you just have to replace all left paddle variables with right paddle variables and negate the move away code.

The final thing you have to do for your game to handle all the game play and the final collision test is to check when the ball gets behind the players paddle and the player loses a life. The code for that is quite simple. You can also directly handle if one of the players loses all his lives and the game is over then. Displaying the “Red Won” or “Blue Won” messages is done in the Draw method. If the user presses Space or Escape, he will return to the main menu and the game can start again:

  // Ball lost? if (ballPosition.X < -0.065f) {   // Play sound   soundBank.PlayCue("PongBallLost");   // Reduce number of lives   leftPlayerLives -- ;   // Start new ball   StartNewBall(); } // if else if (ballPosition.X > 1.065f) {   // Play sound   soundBank.PlayCue("PongBallLost");   // Reduce number of lives   rightPlayerLives -- ;   // Start new ball   StartNewBall(); } // if // If either player has no more lives, the other player has won! if (gameMode == GameMode.Game &&   (leftPlayerLives == 0 ||   rightPlayerLives == 0)) {   gameMode = GameMode.GameOver;   StopBall(); } // if 

Well, this was the hardest part for this game; the bounding box collision tests were a little bit complicated, but the rest of the game was very straightforward and could be implemented quite easily. You also learned a bit about unit testing and how to handle sprites in an effective manner. You can now test the game with your unit test by pressing F5 and fine-tune the collisions a bit (see Figure 2-8).

image from book
Figure 2-8

Adding Sound

To add sound to your game you would usually just drop a couple of .wav files into your project and play them back. In XNA, loading .wav files is not supported and the reason for that is that the Xbox 360 and Windows use different formats for sound and music. To overcome this issue Microsoft invented the XACT tool, which has been available in the DirectX SDK and the Xbox 360 SDK for quite some time now. XACT is short for “Microsoft Cross-Platform Audio Creation Tool.” XNA also makes use of this tool and Microsoft decided to make this the only way to play sound files.

Although XACT is a great tool for adding effects, making a lot of adjustments, and having all sound files managed in one place, it can overcomplicate things a little for a project like this one. There is a full chapter on XACT in this book; check it out in Chapter 9. For your Pong game you will just use two simple sound files:

  • PongBallHit.wav is played every time you collide with something (border or paddle) and it is also used for the menu as the sound for changing the selection.

  • PongBallLost.wav is used when a player lost a life because he didn’t catch the ball in time.

To add these files to your game, you have to create a new XACT project. You can find XACT at Start image from book All Programs image from book Microsoft XNA Game Studio Express image from book Tools.

In the new XACT project add a wave bank by clicking Wave Banks image from book Create Wave Bank and then add a sound bank by clicking Sound Banks image from book Create Sound Bank. Now drag the two .wav files into the new XACT wave bank (see Figure 2-9).

image from book
Figure 2-9

Next drag the two new wave bank entries over to the sound bank, and then drag them over to the cues. If you are confused or run into problems, check out Chapter 9 for more details.

These are the main components used in XACT for sounds:

  • Wave Bank stores all your .wav files; you can’t add anything but wav files here. No support for .mp3, .wma, or any format other than .wav. Importing compressed files is also not an option; ACPCM for Windows and XMA compression for the Xbox 360 is possible, but you have to follow the rules, which are described in Chapter 9.

  • Sound Bank is used to play back sounds later in the game with help of the cues. You can modify the sound settings here by changing the volume and pitch, adding categories, and attaching sound effects (RPC). You can also define multiple tracks here. Usually you will just set the volume here.

  • Sound Cues are used to play sounds. A sound cue has at least one sound bank entry attached, but you can assign multiple sound files to one cue and set rules - if one of the sounds should be played randomly, if you can only play one sound of this kind at a time, and rules for replacing sounds. The important thing here is the cue name, which is used in the game to access the cue and finally play the sound.

Figure 2-10 shows how your XACT project should look like now. You will set the volume of PongBallHit to –8 and the PongBallLost to –4; the default value of –12 is too silent and your hit sound is a little bit to loud, so reducing it sounds nicer in the game. The rest can use the default values and you can just save the project as PongSound.xap. Then add this file to your XNA Studio project and it will use the XNA content pipeline to automatically compile and build all files for both the Windows and Xbox 360 platform for you. Also make sure the two wav files are in the same directory as the PongSound.xap file, otherwise the content pipeline might not be able to find these files and it won’t be able to build your XACT project.

image from book
Figure 2-10

The code to play back the sounds is quite easy, and just to make sure the sounds work fine and the volume is correct, here is simple unit test for the sounds:

  public static void TestSounds() {   StartTest(     delegate     {       if (testGame.keyboard.IsKeyDown(Keys.Space))         testGame.soundBank.PlayCue("PongBallHit");       if (testGame.keyboard.IsKeyDown(Keys.LeftControl))         testGame.soundBank.PlayCue("PongBallLost");     }); } // TestSounds() 

Now all you have to do to implement the sounds to the game is to add a couple of lines at the locations where you want the sound to happen. First of all, add the hit sound to the menu and every time a ball collision occurs with the screen borders or the paddles (see the earlier collision tests).

Then add the lost sound like this to the part where you find out if a player lost a life:

  // Ball lost? if (ballPosition.X < -0.065f) {   // Play sound   soundBank.PlayCue("PongBallLost");   // Reduce life   leftPlayerLives -- ;   // Start new ball   StartNewBall(); } // if else if (ballPosition.X > 1.065f) {   // Play sound   soundBank.PlayCue("PongBallLost");   // Reduce life   rightPlayerLives -- ;   // Start new ball   StartNewBall(); } // if 

After adding these lines you can re-enable the TestSingleplayerGame unit test and see if the sounds are played correctly. For more complex games a better system for checking when to play sounds might be required, but most simple games will work fine just with the PlayCue method, which just plays the sound and holds the cue as long as it is needed. You can also get a sound cue and manage it yourself; this has the advantage of being able to stop it and resume it, and so on.




Professional XNA Game Programming
Professional XNA Programming: Building Games for Xbox 360 and Windows with XNA Game Studio 2.0
ISBN: 0470261285
EAN: 2147483647
Year: 2007
Pages: 138

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net