Friday, March 04, 2011

Week 2: With a Paddle

With all the installation and setup out of the way, this week I finally got to write some code.

I decided that the goal for this week was to get some initial classes laid out for the Breakout game and implement the basics of the Paddle.  Let's start with the class design.

When you create an XNA project, it auto-generates a Game class for you. This class is the core of your Game. In it you implement methods for loading content, updating state and drawing a frame.  However, I don't think it is the best place to put much game logic. I've always tried to avoid pointing too much custom code into classes that extend from framework classes since it makes it harder to test your own code in isolation and it means large amounts of you own code could break if the framework changes. The Game class will act mainly as glue between my game implementation and the XNA framework's game loop. To this end, I created a Breakout class which is instantiated by the generated Game class.  Here is what it looks like:

public class Breakout {
    private SpriteBatch spriteBatch;
    private Paddle paddle;
    private Ball ball;
    private Wall wall;
    private int screenHeight;
    private int screenWidth;

    public Breakout(SpriteBatch spriteBatch) {
        this.spriteBatch = spriteBatch;
        this.screenWidth = spriteBatch.GraphicsDevice.Viewport.Width;
        this.screenHeight = spriteBatch.GraphicsDevice.Viewport.Height;
        this.ball = new Ball();
        this.paddle = new Paddle(screenWidth, screenHeight);
        this.wall = new Wall();
    }
}

It's pretty straight forward, Breakout has a Paddle, a Ball and a Wall, just as you would expect.  The Breakout constructor is passed in a SpriteBatch instance which it will use to draw all the graphics for the game. The SpriteBatch is created in the Game class's Initialize method, where the Breakout object is also created, like so:



protected override void Initialize() {
    breakout = new Breakout(new SpriteBatch(GraphicsDevice));
    base.Initialize();
 }

The LoadContent, Update and Draw methods, which are called by the Game class, just pass the calls onto the respective methods of the Ball, Paddle and Wall, so each component of the game is responsible for loading, updating and drawing itself; all cleanly separated.  You could go even further down the dependency injection path and provide an abstraction layer for the SpriteBatch class, but I'm not going to bother at this point.

The next step was to implement the Paddle.  I started by applying my graphic design skills to come up with this fantastic piece of art:


There are two paddle images there, the bottom will be used for flashing when the ball hits it, but we'll ignore it for now.

It turns out that adding and loading graphics into an XNA game is remarkably simple. Visual Studio generates a GameContent project that is referenced by your main game project.  To add an image to this you simply drag and drop it into the project.  The image can then be loaded in the LoadContent method. This is how the paddle image is loaded by the Paddle class like this:

sprite = contentManager.Load<Texture2d>("paddle");

That is almost too good to be true. The last time I dabbled in Windows game development was back in 2000, I was using DirectX 5 and reading Windows Game Programming for Dummies. There is a big section in that book, and there was a big section of my code, that dealt with manually loading sprites from bmp files and handling colour palettes etc. Reading in encoded file formats was not even dealt with in the book because it was deemed to complex.  Having a single line of code do it for you is pretty great.  Makes me wonder what the catch is, or at the very least I need to read up on the XNA content pipeline some more to find out what formats it supports and what its limitations are.

Anyway, back to the Paddle class. The Paddle class has some state that it uses to keep track of the position and movement of the paddle, these are stored in instance variables as Vector2 objects. After the sprite is loaded in the LoadContent method we also initialise the position of the paddle so that it sits 10 pixels from the bottom of the screen in the middle of the X-axis. This is done like so:

int x = screenWidth - (screenWidth / 2) - (sprite.Width / 2);
 int y = screenHeight - PADDLE_OFFSET - PADDLE_HEIGHT;
 this.position = new Vector2(x, y);
 this.minPosition = new Vector2(0, y);
 this.maxPosition = new Vector2(screenWidth - sprite.Width, y);

x, which is the paddle's starting position on the horizontal axis, is calculated by finding the center of the screen and then moving to the left by half the width of the paddle's sprite. y, which is the vertical position just places to bottom of the sprite 10 pixels from the bottom of the screen. We also calculate the min and max horizontal positions of the paddle, this will be used later to make sure that the paddle doesn't go off the edge of the screen.

I think the most interesting part of the Paddle class is how it handles the movement of the paddle in response to the user input. The goal of the movement is that when the user presses the left or right arrow keys, the paddle will move to the left or right. If they hold the key down the paddle should accelerate so the longer you hold the key down the faster it goes.

To do this we just use the the classic equations of motion:

v = velocity
 u = initial velocity
 a = acceleration
 t = time
 s = distance travelled

 v = u + at
 s = vt

This is implemented in the Update method like so:

internal void Update(GameTime gameTime) {
    float time = (float)gameTime.ElapsedGameTime.TotalSeconds;
    KeyboardState keyboard = Keyboard.GetState();

    if (keyboard.IsKeyDown(Keys.Left) && !previousKeyboard.IsKeyDown(Keys.Left)) {
        velocity = new Vector2(-INITIAL_SPEED, 0);
    } else if (keyboard.IsKeyDown(Keys.Right) && !previousKeyboard.IsKeyDown(Keys.Right)) {
        velocity = new Vector2(INITIAL_SPEED, 0);
    } else if (keyboard.IsKeyDown(Keys.Left) && previousKeyboard.IsKeyDown(Keys.Left)) {
        velocity = velocity + (-ACCELERATION * time);
    } else if (keyboard.IsKeyDown(Keys.Right) && previousKeyboard.IsKeyDown(Keys.Right)) {
        velocity = velocity + (ACCELERATION * time);
    } else {
        velocity = Vector2.Zero;
    }

    position = position + (velocity * time);
    position = Vector2.Clamp(position, minPosition, maxPosition);
    
    previousKeyboard = keyboard;
 }

I've just used a constant acceleration in this initial version to keep it simple, but I might play around with it later if it doesn't feel right.

Right off the bat, I don't like those conditional statements, they are a bit hard to read. Essentially what they do is the first two check if the left or right key has been pressed for the first time, i.e. it wasn't pressed in the last frame, and the second two check if the left or right buttons have been held down. I'll refactor this out into a clean input class next week I think, but for now we'll have to deal with it.

So lets go through the code line by line.
  • On line 2, we grab the number of seconds that have elasped since the last frame. This becomes our t in the motion equations.
  • On line 3 we get the current keyboard state.
  • Lines 5 - 15 deal with calculating our new velocity.
    • Line 5 checks to see if the Left arrow key has been pressed for the first time, if so, on line 6 we set the next velocity to the negative of the initial speed. It's negative because we are going left.
    • Likewise, line 7 checks if we are going right for the first time and line 8 sets the velocity to the initial velocity, positive this time because we are going right.
    • On line 9 we check if the Left arrow key has been held down. If so, on line 10 we increase our velocity by the negative of the acceleration * time. This is the v = u + at in our motion equations.
    • Likewise on line 11 we check if Right has been held down and on line 12 we do the v = u + at again, this time in the positive direction.
    • And finally, if neither Left or Right are held down, then we set the velocity to 0 on line 14.
  • Once we have our velocity we can calculate our new position. We know distance travelled is simply v * t so we can just add that the current position on line 17.
  • Then on line 18, we make sure the position isn't outside the screen bounds
  • And finally save the keyboard state to check against in the next frame.
So that's it, pretty simple but it was pretty fun to do something based on actual physics formulae. I particularly like the way the XNA Vector2 structure, provides operator overloading here, it makes the code much more readable than having a bunch of nested methods calls like Add or Multiply. 

Once we have the new position we can draw the paddle in the Paddle class's Draw method like this:

internal void Draw(GameTime gameTime, SpriteBatch spriteBatch) {
    spriteBatch.Draw(sprite, position, source, Color.White);
}

All done.  The paddle now shows up on the screen and responds to the keyboard input. This is what it looks like (well slightly better than this low-res video at least):



As usual the code is up on Github if anyone is interested.  Next week I'll be cleaning up the input detection a bit and implementing the ball, so more graphics and 2D motion this time and maybe some collision detection.

And before I forget, I found these tutorials by George Clingerman really useful to get up to speed on 2D graphics in XNA, so thanks George!!

No comments: