Simple platformer game in XNA tutorial – part four “Simple collision detection”

image

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 Smiley, der blinker...

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.

image

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.

image

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:

image

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:

image

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.

Leave a Reply