Simple platformer game in XNA tutorial – part four “Simple collision detection”
Previously on xnafan.net
In the last part we looked at movement, and how to respond to keyboard input.
The current codebase lets the Jumper move all over the screen without any constraints at all - not exactly what we had in mind. This must be stopped! And it will ...
The agenda
- First we will take a look at a very simple implementation of how to detect collisions in XNA.
- Then we will add a boundingrectangle to our Sprite class, so everything we draw has knowledge of its outer bounds
- We will add code to test for collisions and stop the Jumper's movement if it hits something
How simple collision detection works
XNA offers us a very simple way of testing whether two items overlap. We define a Rectangle for each of two items, where the rectangles define the outer boundary of the items. Then we send one of the rectangles to the Intersects() method on the other, and it returns a bool telling us whether they intersect.
Example
We create three rectangles using the following code:
Rectangle _rectangle1 = new Rectangle(5, 5, 25, 20); Rectangle _rectangle2 = new Rectangle(25, 10, 15, 10); Rectangle _rectangle3 = new Rectangle(35, 25, 10, 5);
The parameters to the constructor are:
X and Y of upper left corner
width and height
Here I've illustrated where these three rectangles appear, and whether Intersects() returns true or false.
Adding a bounding rectangle to our Sprite class
Since a Rectangle can be constructed using the coordinates of the upper lefthand corner, plus width and height, it's easy to to create a bounding rectangle, because that's the information we've got in the Position property on the Sprite and the Width and Height of the Texture :).
The easy way of exposing a Rectangle property to the world is to implement a read-only property (a property with no set part) which, when accessed from outside, creates a new Rectangle based on the Position and Texture of the Sprite. Since we want to keep things simple in this tutorial, that's what we'll do! 😀
Add the following code to the Sprite class:
public Rectangle Bounds { get { return new Rectangle((int)Position.X, (int)Position.Y, Texture.Width, Texture.Height); } }
We need to cast the Position.X and Position.Y to int, since they are of type float, and will lose precision (the decimal part) when stored in an int. A lot of these operations which can introduce subtle, implicit errors are automatically checked by the compiler for us. By writing (int) in front of the value we want to convert, we can silence those warnings.
It's like saying to the compiler: "It's alright, I know what's going on and I explicitly allow it" 😉
Now whenever anybody wants to get the outer boundaries of our Sprite, they can retrieve the current position and size of our Sprite as a Rectangle through the Bounds property.
And remember that since Jumper and Tile inherit Sprite, they now also have a Bounds property.
Simple collision detection
Now, whenever we want to move something in our game, we have to test that object's bounding rectangle against the bounding rectangle of everything else, to see whether we hit something. In a large level we would have to cut the level up into smaller quadrants and store the objects in those quadrants somewhere for fast lookups. If we didn't, our game might slow down due to the large amounts of collision checks performed each Update(). Our gameboard is 15 * 10 tiles, and about one quarter of those will be blocked. This will give us about 15 * 10 /4 = 37.5 collisiondetections per movement we want to perform. That's not even close to being a problem :). When we refine our collision detection, we will get quite a few more checks per Update(), but still nothing to worry about.
Letting the Board tell whether is has room for something
Now we have to figure out where to put the code for detecting collisions.
We could put it in Jumper, but this would mean that we could only reuse that code in classes which inherit Jumper. We could also put it in Sprite, so all other subclasses of Sprite had access to it, but some of those classes might not need it. And the functionality is very closely related to the Board. So we'll add a method to Board to check for a given Rectangle whether there is room for it:
public bool HasRoomForRectangle(Rectangle rectangleToCheck) { foreach (var tile in Tiles) { if (tile.IsBlocked && tile.Bounds.Intersects(rectangleToCheck)) { return false; } } return true; }
Q: Why do I first check for IsBlocked, and then check Intersects afterwards, and not the other way around?
A: Because the first check takes less computations (a simple true/false lookup) than the other (mathematical comparisons with position, width, height). This way we don't calculate if there is no need
Â
Letting Jumper access the Board object through a static property
To give Jumper access to the Board object and all its tiles, we could just add a Board property to Jumper (or Sprite) and store a reference to the Board object here. This would enable Jumper to detect collisions. But I am going to show you a technique that requires less coding and enables any code in our project to access the Board.
In case you're still a bit wobbly on the concepts of class and object, think of the Class as the definition (the mold) for objects. There is only one class, but from that class you can instantiate ("new up") many objects, each with their own data.
Even though there may be many objects of any given class, there is only one class of that type. So we will create a property on the Board class instead of on the objects which we've done so far. This property will store a reference to the current Board object.
Since we can write "Board" anywhere, and gain access to the class, we will automatically have access to any public properties on the class.
Adding a static property to a class
To tell the compiler that we want a property (or method) stored on the class instead of on the objects created from the class, we add the keyword "static" to the property.
Go ahead and add a static Board property "CurrentBoard" to the Board class:
public static Board CurrentBoard { get; private set; }
We mark the property's setter private, so nobody can accidentally set a new Board outside the Board class. Right now we only want one board instance (object).
In the constructor of the Board class we add a final line to store the newly created Board object in the CurrentBoard property.
Board.CurrentBoard = this;
What's "this"?
"this" is a special word meaning "the object I am currently in. So what we're doing here is getting a reference to the Board object we're constructing through this and storing it on the class. Now we can access the Board object from Jumper :).
IMPORTANT! With the approach we've chosen here, the latest created Board object will always overwrite the previous (if any), so we should only construct the board once.
Now your Board class should look like this:
public class Board { public Tile[,] Tiles { get; set; } public int Columns { get; set; } public int Rows { get; set; } public Texture2D TileTexture { get; set; } private SpriteBatch SpriteBatch { get; set; } private Random _rnd = new Random(); public static Board CurrentBoard { get; private set; } public Board(SpriteBatch spritebatch, Texture2D tileTexture, int columns, int rows) { Columns = columns; Rows = rows; TileTexture = tileTexture; SpriteBatch = spritebatch; Tiles = new Tile[Columns, Rows]; InitializeAllTilesAndBlockSomeRandomly(); SetAllBorderTilesBlocked(); Board.CurrentBoard = this; } private void InitializeAllTilesAndBlockSomeRandomly() { for (int x = 0; x < Columns; x++) { for (int y = 0; y < Rows; y++) { Vector2 tilePosition = new Vector2(x * TileTexture.Width, y * TileTexture.Height); Tiles[x, y] = new Tile(TileTexture, tilePosition, SpriteBatch, _rnd.Next(5) == 0); } } } private void SetAllBorderTilesBlocked() { for (int x = 0; x < Columns; x++) { for (int y = 0; y < Rows; y++) { if (x == 0 || x == Columns - 1 || y == 0 || y == Rows - 1) { Tiles[x, y].IsBlocked = true; } } } } public void Draw() { foreach (var tile in Tiles) { tile.Draw(); } } }
Updating Jumper's Update()
Now we will add code to Jumper's Update() method to
- store where we were before we move
- move (already implemented)
- check whether we've collided
- if we collided, move back to where we came from
Our Update looks like this now:
public void Update(GameTime gameTime) { CheckKeyboardAndUpdateMovement(); SimulateFriction(); UpdatePositionBasedOnMovement(gameTime); }
but we no longer want to unconditionally UpdatePositionBasedOnMovement(). We want to Move If Possible :).
So cut the the last line of Update() into the clipboard, and instead write:
MoveIfPossible(gameTime);
Position your cursor inside the MoveIfPossible methodname and press ALT + SHIFT + F10, and you'll see this.
Click the "Generate method stub..." menuitem and you'll get a method like this:
Â
private void MoveIfPossible(GameTime gameTime) { }
...where you can paste your UpdatePositionBasedOnMovement(gameTime); methodcall.
This is my preferred method of coding: Write it like you would like to read it, and then have Visual Studio implement methods which support exactly that.
Now go ahead and
- store the position of Jumper before updating the Position
- check the board to see whether Jumper's new position is blocked
- restore Position to the previous position if Jumper can't go there
When you're done, check to see whether you have something along these lines:
private void MoveIfPossible(GameTime gameTime) { Vector2 oldPosition = Position; UpdatePositionBasedOnMovement(gameTime); if (!Board.CurrentBoard.HasRoomForRectangle(Bounds)) { Position = oldPosition; } }
Try it out - and don't be too sad that it isn't perfect ..yet!
Okay - now go ahead and change the LoadContent() method of SimplePlatformerGame to move the starting point for _jumper to 80 * 80, so he doesn't start out inside the border.
Here I use the shorthand notation for new Vector2(80,80) by multiplying a vector of (1,1) by 80:
_jumper = new Jumper(_jumperTexture, Vector2.One * 80, _spriteBatch);
Okay - now run the game, and check that the code works, but probably not exactly what you had hoped for.
Here's an explanation of what's happening.
The short of it is that because we're still "teleporting" from one position to the next, we are stopped too far out. Look at this illustration:
Jumper wants to update his position from an unblocked tile to a blocked tile. Since that isn't possible, Jumper doesn't move any closer to the wall. Jumper doesn't move till we release the arrow key, and speed decreases to the point where possible movement occurs in Update().
So we need to find out how far we can get in the direction we want. And that's what we'll do in part five of this tutorial
The updated class diagram
Here you can see that the Bounds property has been added to the Sprite class, and the HasRoomForRectangle method to the Board class:
What we've covered
You should take the following away from this chapter of the tutorial:
- You can store a reference to an object on the class using the static keyword. This enables code from anywhere to gain access to that object.
- You should think about which class you choose to add functionality to. Which class should have the responsibility. Think of the task of finding out whether there is room for a rectangle on the Board.
- Simple collision detection can be implemented using Rectangles.Intersects()
- Simple collision detection isn't perfect 😉
The code for the project so far
...is here :).
Next up...
In the next part of this tutorial, we will improve the collision detection to let us know exactly how far we can get before we must stop completely.
August 10th, 2018 at 05:55
See: "2D Collision Detection for Game Programmers: Focus on Circle Collisions" on Amazon.com for a detailed answer to this question.
This question is not easily answered. There are 3 types of collision algorithms that may be written: Static, Semi-Dynamic, and Dynamic. Most of the discussions here were for Static collisions. Static collisions are when the algorithm assumes that the two objects are static, or not moving, even it they are actually moving. Semi-Dynamic collision algorithms account for object A moving, but assumes object B is static. Dynamic collision algorithms take into account that both objects are moving.
Static collision algorithms return only if a collision has occurred. The programmer only has the location of the objects to use for collision response. This is limiting, but sufficient for many games. Games like "Space Invaders" may be written using the algorithm. It is important to note that the static algorithms can have issues with small fast moving objects. Care must be taken to insure these objects cannot skip over each other in a single frame.
Semi-Dynamic collision algorithms return if a collision has occurred, The mathematical intersection point, the intersection time, the collision point (Where the two objects touch), and the collision normal. These algorithms take more time to execute, but return more information to allow for a wide variety of collision responses. This is the recommended algorithm I would use for most games. Games like "Break Out" may be written using this algorithm.
Dynamic collision algorithms return if a collision has occurred, the mathematical intersection point, the point where object A is at collision, the point where object B is at collision, the collision point, the collision normal for object A, and the collision normal for object B. This algorithm is the slowest to execute, but gives all the collision response details possible. This algorithm would be suitable to a "Break Out" like game that has multiple ball and if the balls collide they bounce off each other. Typically "Break Out" like game do not have the balls interacting with each other, therefore, the balls can occupy the same location with no response. This is not realistic, but how it is usually done.
The process for building the algorithms is fairly simple, but filled with a lot of repeating detail that make it cumbersome. The algorithms can be broken down into the following steps:
1) Identify the 2 objects to test for a collision
2) Create the collision area.
3) Test if the control point collides with any of the objects that make up the collision area. Divide and conquer decisions should be used to maximize efficiency.
The collision area is a composite of the 2 objects colliding. To create the collision object take object A and transcribe object A around object B. In the case of a circle colliding with an AABB, the collision area will have 4 circles at its corners, and 4 segments connecting the circles at their outer tangent points.
There is insufficient space in this blog give the algorithms in detail, but in the book "2D Collision Detection Algorithms for Game Programmers: Focus on Circle Collisions" will explain all 3 collision types for circles colliding with Points, Lines, Horizontal Lines, Vertical Lines, Rays, Segments, Circles, Ellipses, Axis Aligned Bounding Boxes (AABB), Object Oriented Bounding Boxes (OOBB), Capsules, and Polygons. I highly recommend this book.
You may also be interested in my other books also, they may be found at Amazon.com:
1) "2D Collision Detection for Game Programmers: Focus on Point Collisions"
2) "2D Collision Detection for Game Programmers: Focus on Circle Collisions"
3) "2D Collision Detection for Game Programmers: Focus on Ellipse Collisions"
In print soon:
4) "2D Collision Detection for Game Programmers: Focus on Axis Aligned Bounding Box (AABB) Collisions"
5) "2D Collision Detection for Game Programmers: Focus on Object Oriented Bounding Box (OOBB) Collisions"
6) "2D Collision Detection for Game Programmers: Focus on Capsule Collisions"
7) "2D Collision Detection for Game Programmers: Focus on Polygon Collisions"
8) "2D Collision Detection for Game Programmers: Focus on Collision Response"
Good luck with your game.