XNA 4.0 Break Out Tutorials Collisions
XNA 4.0 Break Out Tutorials Collisions
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace BreakingOut { class Ball { Vector2 motion; Vector2 position; Rectangle bounds; float ballSpeed = 4; Texture2D texture; Rectangle screenBounds; public Rectangle Bounds { get { bounds.X = (int)position.X; bounds.Y = (int)position.Y; return bounds; } } public Ball(Texture2D texture, Rectangle screenBounds) {
public void Update() { position += motion * ballSpeed; CheckWallCollision(); } private void CheckWallCollision() { if (position.X < 0) { position.X = 0; motion.X *= -1; } if (position.X + texture.Width > screenBounds.Width) { position.X = screenBounds.Width - texture.Width; motion.X *= -1; } if (position.Y < 0) { position.Y = 0; motion.Y *= -1; } } public void SetInStartPosition(Rectangle paddleLocation) { motion = new Vector2(1, -1); position.Y = paddleLocation.Y - texture.Height; position.X = paddleLocation.X + (paddleLocation.Width - texture.Width) / 2; } public bool OffBottom() { if (position.Y > screenBounds.Height) return true; return false; } public void PaddleCollision(Rectangle paddleLocation) { Rectangle ballLocation = new Rectangle( (int)position.X, (int)position.Y, texture.Width, texture.Height); if (paddleLocation.Intersects(ballLocation)) { position.Y = paddleLocation.Y - texture.Height; motion.Y *= -1; } } public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(texture, position, Color.White); } } }
I added a new field bounds that describes the rectangle of the ball. I also added a read only accessor to get the rectangle. In the accessor I set the X part of the rectangle to the X part of the position of the ball, casting it to an integer. I do something similar for the Y part of the coordinates. I then return the rectangle.
In the constructor I create a Rectangle with the height and width of the ball texture. This way I don't have to recalculate the width and height of the bounds of the ball when it is needed for collision detection. It is a small optimization that will improve performance a little. Now, in the Game1 class we need to check for collision between the bricks and the ball. Modify the Update method to the following.
protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); paddle.Update(); ball.Update(); foreach (Brick brick in bricks) { brick.CheckCollision(ball); } ball.PaddleCollision(paddle.GetBounds()); if (ball.OffBottom()) StartGame(); base.Update(gameTime); }
I added an foreach loop that loops through all of the bricks. I then call the CheckCollision method of the brick passing in the ball to see if they collide. The next step will be in the CheckCollision method of the brick class. To start with update the code to the following.
public void CheckCollision(Ball ball) { if (alive && ball.Bounds.Intersects(location)) { alive = false; } }
So, you check to see if the brick is currently in play using the alive field. You then check if the Bounds property of the ball intersects with the location field. If it does I set the alive field of the brick to be false. If you run the game now the the ball will bounce around the screen and if it hits a brick it will remove the brick from play. The ball does not bounce off the brick though, it just keeps on going. Let's try the same idea when the ball collides with the paddle on the brick. What I will do is add a method to the Ball class called Deflection that will handle deflecting the ball in the right direction. Update the CheckCollision method of the Brick class to the following.
public void CheckCollision(Ball ball) { if (alive && ball.Bounds.Intersects(location)) { alive = false; ball.Deflection(this); } }
I just call the Deflection method of the ball passing in this, which is the current object that the ball collided with. Now, add this method the Ball class.
Now, that sort of works but it doesn't work quite right. First problem is when the ball hits the first few bricks it isn't deflecting properly. Part of the problem is it is hitting two bricks at the same time then hitting a third so all three bricks remove but the ball isn't deflecting properly. To start with if the ball collides with two bricks at the same time you only want it to deflect once. To handle that I will add in a field to the Ball class, collided. Add this field to the Ball class with the other fields.
bool collided;
Next step is to change the Update method to set collided to be false. This way it is updated each frame of the game. Change the Update method to the following.
public void Update() { collided = false; position += motion * ballSpeed; CheckWallCollision(); }
The next step is to update the Deflection method to only deflect if collided is false and then set collided to true. Change the Deflection method to the following.
public void Deflection(Brick brick) { if (!collided) { motion.Y *= -1; collided = true; } }
That is working a little better but if the ball hits the side of a brick it is deflecting vertically instead of horizontally. I'm going to leave it to you to smooth out the kinks with the ball deflecting. I will tell you that the best way to do it is the following. If the ball intersects with a brick you find where the ball will be in the next frame and construct a line segment. You then create line segments for each of the sides of the brick the ball is colliding with. Depending which line segments the ball line segment the intersects tells you how to deflect the ball. There is all kinds of source code on the Internet on intersecting line segments. What I am going to add in this tutorial is having the ball start in a random direction and have the ball speed up as the game progresses. First step is to have the ball start in a some what random direction. The ball will still start moving right and up but not always (1, -1). Change the SetInStartPosition method of the ball class to the following and the following constant with the ballSpeed field.
const float ballStartSpeed = 8f; public void SetInStartPosition(Rectangle paddleLocation) { Random rand = new Random(); motion = new Vector2(rand.Next(2, 6), -rand.Next(2, 6)); motion.Normalize(); ballSpeed = ballStartSpeed;
In the SetInStartPosition method I create a Random object for creating random numbers. I then create the motion vector. The X value of the motion vector will be between 2 and 5 and the Y value between -2 and -5. I then call the Normalize method on motion. What this does is make the motion vector a vector with a length of 1. This means that motion becomes a direction vector. You will find this a lot in game programming. You will often need the direction but with a length of 1. It helps things have a uniform motion. I then set ballSpeed to ballStartSpeed. The last step is to have the ball speed up over time. The simplest way to do this is to increase the ballSpeed field by a small amount each frame of the game. You can do that in the Update method. Change the Update method of the Ball class to the following.
public void Update() { collided = false; position += motion * ballSpeed; ballSpeed += 0.001f; } CheckWallCollision();
I'm going to end this tutorial here, and the series. It is a pretty complete game and I've told you how to change the few things that can be improved. I encourage you to visit the news page of my site, XNA Game Programming Adventures, for the latest news on my tutorials. Good luck in your game programming adventures! Jamie McMahon