This tutorial will show you how to implement a simple Pong-like game for Windows using Microsoft XNA 4.0. The finished product will look like the image below. You will control the hand at the bottom of the screen using the left and right keys on your keyboard. The ball will move about the screen, bouncing off the sides of the window. The goal is to keep the ball from falling past your hand.
Microsoft is no longer maintaining XNA, but you can still use it in a modern IDE like Visual Studio 2013. However, you will need to install a number things to get this to work:
If you do not have access to Visual Studio 2010, you might try MonoGame. It does not have a content pipeline converter (software that converts content like sound files into xnb files) so you will not be able to use the .wav files used in this tutorial. All the code in this tutorial (minus SoundEffect code) will work fine in a MonoGame project.
Although this tutorial shows screen shots from VS 2010, there is no big difference when using VS 2013.
Let's first add a ball that will bounce off the walls. We'll start by adding the basketball.png file (one of the graphics you downloaded earlier) to your PongContent project. This project is used to hold all the images, sounds, etc. that are used in your game. Then we'll add the code to load the image and draw it.
If you click on the image file, you will see its properties in the Properties window
as shown below. Note that the image is given an Asset Name called
basketball. The asset name will be used later in the tutorial.
public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; // Ball sprite Texture2D ballSprite; // Ball location Vector2 ballPosition = Vector2.Zero; // Store some information about the sprite's motion. Vector2 ballSpeed = new Vector2(150, 150);
In the LoadContent() method, load the basketball image into ballSprite. LoadContent() is called just once when your application begins, and this is where you will load all images and sounds that are used in your game.
protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); ballSprite = Content.Load<Texture2D>("basketball"); }
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); // Draw the sprite spriteBatch.Begin(); spriteBatch.Draw(ballSprite, ballPosition, Color.White); spriteBatch.End(); base.Draw(gameTime); }
Close the window before proceeding.
In order for the ball to move around the screen, you will next modify the Update() method. This function is called approximately 60 times a second, and it's used to modify the position of the sprites which are drawn on the screen in the Draw() method. This function is always called immediately before Draw() is called.
Remember that in the Draw() method, the ballPosition variable was used to position the ball in the window. Modify Update() so the ballPosition changes based on how much time has elapsed since the last time the Update() method was called. Since Update() is called approximately 60 times a second, we will multiply ballSpeed by 1/60. Since ballSpeed is initially 150, the ball will be moving approximately 150 * 1/60 = 2.5 pixels each time Update() is called.
We also need to ensure that the ball does not fly off the screen. The maxX and maxY variables will be the maximum values that the ball can travel in the x and y direction before needing to head back the other direction. A pair of if statements are used to check if the ball has traveled out of the window's bounds, and if so, to make the ball go in the opposite direction.
protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); // Move the sprite by speed, scaled by elapsed time ballPosition += ballSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds; int maxX = GraphicsDevice.Viewport.Width - ballSprite.Width; int maxY = GraphicsDevice.Viewport.Height - ballSprite.Height; // Check for bounce if (ballPosition.X > maxX || ballPosition.X < 0) ballSpeed.X *= -1; if (ballPosition.Y > maxY || ballPosition.Y < 0) ballSpeed.Y *= -1; base.Update(gameTime); }
Close the window before proceeding.
IsMouseVisible = true;
We will now add a paddle (the hand) which can be controlled by the left and right arrow keys on the keyboard.
// Paddle sprite Texture2D paddleSprite; // Paddle location Vector2 paddlePosition;
paddleSprite = Content.Load<Texture2D>("hand");
protected override void Initialize() { base.Initialize(); // Set the initial paddle location paddlePosition = new Vector2( graphics.GraphicsDevice.Viewport.Width / 2 - paddleSprite.Width / 2, graphics.GraphicsDevice.Viewport.Height - paddleSprite.Height); }Make sure base.Initialize() is called before accessing paddleSprite, or it will be null, and your program will crash!
// Update the paddle's position KeyboardState keyState = Keyboard.GetState(); if (keyState.IsKeyDown(Keys.Right)) paddlePosition.X += 5; else if (keyState.IsKeyDown(Keys.Left)) paddlePosition.X -= 5;
spriteBatch.Draw(paddleSprite, paddlePosition, Color.White);
GraphicsDevice.Clear(Color.White);
We will now add the logic needed to detect when the ball and the hand collide. We'll make the ball bounce off the hand, but if the ball makes it to the bottom of the screen before the hand touches it, we'll restart the hand at the top of the screen.
// Check for bounce if (ballPosition.X > maxX || ballPosition.X < 0) ballSpeed.X *= -1; if (ballPosition.Y < 0) ballSpeed.Y *= -1; else if (ballPosition.Y > maxY) { // Ball hit the bottom of the screen, so reset ball ballPosition.Y = 0; ballSpeed.X = 150; ballSpeed.Y = 150; }
// Ball and paddle collide? Check rectangle intersection between objects Rectangle ballRect = new Rectangle((int)ballPosition.X, (int)ballPosition.Y, ballSprite.Width, ballSprite.Height); Rectangle handRect = new Rectangle((int)paddlePosition.X, (int)paddlePosition.Y, paddleSprite.Width, paddleSprite.Height); if (ballRect.Intersects(handRect)) { // Increase ball speed ballSpeed.Y += 50; if (ballSpeed.X < 0) ballSpeed.X -= 50; else ballSpeed.X += 50; // Send ball back up the screen ballSpeed.Y *= -1; }
Note that rectangular bounds checking is not always the most ideal type of collision detection. If you examine the picture below, you'll see that with rectangular collision detection, the ball and hand sprites are intersecting, but there is actually a significant distance between the actual ball and hand.
Although rectuangular bounds checking may not always be ideal, it is the easiest to implement and the fastest to check. You can read more about 2D collision detection methods here.
Press F5 and varify that the collision detection is working. The ball should sail up when the hand hits it. If the ball hits the bottom of the window, it should immediately restart at the top of the window.
Also try hitting the ball with the side of the hand instead of the top. What happens? The ball seems to get "stuck" bouncing up and down very quickly because the collision detection keeps detecting an intersection which changes the direction of the ball up and down and up and down. This is probably not the behavior you want.
To fix this problem, we need to only execute our speed and direction-changing code once, and that's when the ball is on it's way down (ballSpeed.Y > 0). If the ball is on it's way up (ballSpeed.Y < 0), then we don't want to execute this code at all. Make the following fix to the rectange intersection if statement.
if (ballRect.Intersects(handRect) && ballSpeed.Y > 0)
Now re-run your program, and the ball should shoot back up if you hit it with the top of the hand or from the side.
Our game could be a lot more fun to play with the addition of sound effects. Let's add a sound effect that plays when the hand hits the ball and another that plays when we miss the ball.
SoundEffect swishSound; SoundEffect crashSound
swishSound = Content.Load<SoundEffect>("swish"); crashSound = Content.Load<SoundEffect>("crash");
if (ballRect.Intersects(handRect) && ballSpeed.Y > 0) { swishSound.Play(); ... }
else if (ballPosition.Y > maxY) { // Ball hit the bottom of the screen, so reset ball crashSound.Play(); ... }
If you've gone through this tutorial and can't get your program to work, or if you are in a time crunch and just want to examine the final product, you can download the complete solution in pong.zip.
There are a number of improvements we could make to this game. For example, we could keep track of the score or add a menu that can start a game. To be a true pong game, we should have two paddles (hands), not just one!
From a coding standpoint, it would be helpful to re-write the project so that it makes better use of object-oriented programming methodology. For example, the ball and the paddle logic should ideally be encapsulated into classes.
The XNA project in PongImproved.zip has a Paddle and Ball class. Unzip the project and load it into Visual Studio. The Paddle and Ball classes inherit from Microsoft.Xna.Framework.DrawableGameComponent which is a component that has its own Update() and Draw() methods so the ball and paddle can update their movements and draw themselves. The Game1 class only needs to declare and instantiate the objects and add them to the list of GameComponents so the objects' Update() and Draw() methods are called:
private Ball ball; private Paddle paddle; ... public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; ball = new Ball(this); paddle = new Paddle(this); Components.Add(ball); Components.Add(paddle); }
This simplifies how the ball and paddle collision detection is performed:
// Check for bounce if (ball.X > maxX || ball.X < 0) ball.ChangeHorzDirection(); if (ball.Y < 0) ball.ChangeVertDirection(); else if (ball.Y > maxY) { // Game over - reset ball crashSound.Play(); ball.Reset(); } // Collision? Check rectangle intersection between ball and hand if (ball.Boundary.Intersects(paddle.Boundary) && ball.SpeedY > 0) { swishSound.Play(); // Increase speed and go back up the screen ball.ChangeVertDirection(); ball.SpeedUp(); }
The improved project also has some additional functionality:
Using this project as a starting point, see if you can make some enhacements (keeping track of the score, adding an opponent, etc.).
Home | Page last modified: |