email sol follow sol rss feed of the blog wishlist Sol::Tutorials

Sol's Graphics for Beginners

(ch26src.zip)

(prebuilt win32 exe)

26 - Wall Madness

Feel free to make a copy of the project, or continue from the last one. When did you last back up your project?

The original name for this chapter was 'Some New Tiles', but, as it turned out, most of this chapter will be spent on just one tile type, which are the walls.

We'll be adding three different new tile types: A slowdown tile, a 'slippery' tile, as well as the wall. Let's get the easy bits over with first. Download tiles2.bmp (right-click, save as) and save it to the project directory again. This version of the tile graphics contains the three new tile types.

First, go to main.cpp, find the tiles.bmp loading code and load tiles2.bmp instead.

Next, open up gp.h and find the leveldataenum. Add the following enumerations:


  LEVEL_WALL = 24,
  LEVEL_SMOOTH = 25,
  LEVEL_ROUGH = 26

Make sure the line with the earlier last enumeration (LEVEL_DOWN) ends with a comma ( , ).

Next up is the game.cpp file. Go to the reset() function, where we're going through the level data, and add the following cases:


      case '#':
        gLevel[p] = LEVEL_WALL;
        break;
      case '~':
        gLevel[p] = LEVEL_ROUGH;
        break;
      case '=':
        gLevel[p] = LEVEL_SMOOTH;
        break;  

Finally, go down to the render() function, find where we're drawing the level, and add the following cases:


        case LEVEL_WALL:
          tile = 3;
          break;
        case LEVEL_ROUGH:
          tile = 8;
          break;
        case LEVEL_SMOOTH:
          tile = 9;
          break;

Now the new tile types are loaded and drawn, but they function currently exactly the same way as normal floor tiles do.

You can add the new tiles to your existing maps, or you can download this bunch of modified maps. The first level in the modified maps also includes an area where we can play around with the physics a bit.

Leaving the toughest to the last, let's add functionality to the slowdown and slippery tiles.

First, go to gp.h, and add the following constant:


// Slowdown due to rough tile
#define SLOWDOWNROUGH 0.95f

Back to game.cpp. We're interested in the current tile in several places in the future, so let's store that information in a variable. In render(), go to the beginning of the physics loop (where we're checking for gKeyLeft etc), and paste the following right after the loop starts:


      int currenttile = (((int)gYPos) / TILESIZE) * gLevelWidth + ((int)gXPos) / TILESIZE;

We were calculating the current tile in the switch-case where we're checking for LEVEL_DROP and LEVEL_END and things of that sort; you can now remove the calculation and change the 'switch' statement to:


    switch (gLevel[currenttile])

Next we'll adjust how the motion vector is added to the player's position based on the tile type. Find the old code that multiplies the motion vector with SLOWDOWN and is then added to the current position, and replace it with the following:


    switch (gLevel[currenttile])
    {
    case LEVEL_SMOOTH:
      gXPos += gXMov;
      gYPos += gYMov;
      break;
    case LEVEL_ROUGH:
      gXMov *= SLOWDOWNROUGH;
      gYMov *= SLOWDOWNROUGH;
      gXPos += gXMov;
      gYPos += gYMov;
      break;
    default:
      gXMov *= SLOWDOWN;
      gYMov *= SLOWDOWN;
      gXPos += gXMov;
      gYPos += gYMov;
    }  

Compile, run, and try to run over the rough and smooth tiles.

Rolling on the rough tiles will slow down a lot, while the smooth tiles won't slow down at all.

The effect of the smooth tiles is only obvious when moving at very low speeds. We'll do something about this in the next chapter.

Next up, the walls.

In order to collide with the walls, we'll need to check whether there's a wall tile above, below, left, right, or any of the four diagonal directions, and we need to check if we've crossed any of the boundaries of our current tile far enough to cause the collision.

In order to save a lot of hassle during gameplay, we'll create a new table that stores the wall information for each tile.

First, add the following to main.cpp, as well as to gp.h (again with the extern-prefix in the header):


// Level collision data
unsigned char *gColliders; 

Later in main.cpp, add the following in the beginning of the init() function to start off with a empty set of colliders:


  gColliders = NULL;

Next, we'll add the precalculation code. Go to game.cpp, find reset(), and after allocating memory for the level data, add the following lines:


  delete[] gColliders;
  gColliders = new unsigned char[gLevelWidth * gLevelHeight];
  memset(gColliders, 0, sizeof(unsigned char) * gLevelWidth * gLevelHeight);

Next, go downwards in the reset(), into the loop where we're looking for collectibles and the level start tile, and add the following:


    if (gLevel[i] == LEVEL_WALL)
    {
      int ypos = i / gLevelWidth;
      int xpos = i % gLevelWidth;

      if (ypos > 0)
      {
        if (xpos > 0)
          gColliders[i - gLevelWidth - 1] |= COLLIDE_SE;
        
        gColliders[i - gLevelWidth] |= COLLIDE_S;

        if (xpos < gLevelWidth - 1)
          gColliders[i - gLevelWidth + 1] |= COLLIDE_SW;
      }
      if (xpos > 0)
        gColliders[i - 1] |= COLLIDE_E;
      
      if (xpos < gLevelWidth - 1)
        gColliders[i + 1] |= COLLIDE_W;

      if (ypos < gLevelHeight - 1)
      {
        if (xpos > 0)
          gColliders[i + gLevelWidth - 1] |= COLLIDE_NE;
        
        gColliders[i + gLevelWidth] |= COLLIDE_N;

        if (xpos < gLevelWidth - 1)
          gColliders[i + gLevelWidth + 1] |= COLLIDE_NW;
      }
    }

Compiling this would give you a bunch of errors on the COLLIDE_xx constants. Go to gp.h, and paste the following after the level data enumeration:


// Tile collision directions
enum colliderenum
{
  COLLIDE_N  = 0x01,
  COLLIDE_NW = 0x02,
  COLLIDE_W  = 0x04,
  COLLIDE_SW = 0x08,
  COLLIDE_S  = 0x10,
  COLLIDE_SE = 0x20,
  COLLIDE_E  = 0x40,
  COLLIDE_NE = 0x80
};

What we're doing above is that for each wall, we're adding collision data for the surrounding tiles. For instance, we're setting COLLIDE_N bit on for the tile below the wall tile. Most of the code is due to the fact that we have to be careful not to access any level data outside the actual map.

Next we'll want to check whether the ball collides with the walls. We'll be interested in the exact position inside the current tile. Add the following after we've increased the gRoll with the current motion vector:


    // Collision with tiles
    currenttile = (int)(gYPos / TILESIZE) * gLevelWidth + (int)(gXPos / TILESIZE);
    float xposintile = gXPos - (int)(gXPos / TILESIZE) * TILESIZE;
    float yposintile = gYPos - (int)(gYPos / TILESIZE) * TILESIZE;

Since we've just adjusted the gYPos and gXPos variables, we'll need to recalculate the current tile.

Let's add code to force the ball outside the walls if collided. Add the following code right after what you just pasted:


  // Check collision with tile edges

  if (gColliders[currenttile] & COLLIDE_E && xposintile > (TILESIZE - RADIUS))
  {
    gXPos -= xposintile - (TILESIZE - RADIUS);
  }

  if (gColliders[currenttile] & COLLIDE_W && xposintile < (RADIUS))
  {
    gXPos += (RADIUS) - xposintile;
  }

  if (gColliders[currenttile] & COLLIDE_S && yposintile > (TILESIZE - RADIUS))
  {
    gYPos -= yposintile - (TILESIZE - RADIUS);
  }

  if (gColliders[currenttile] & COLLIDE_N && yposintile < (RADIUS))
  {
    gYPos += (RADIUS) - yposintile;
  }  

Compile and run.

The ball now stops at the wall edges, but hitting the corners is a bit woozy. The corner checks are a bit more involved because we're dealing with a sphere instead of a square. Paste the following after the edge checks:


  if (gColliders[currenttile] & COLLIDE_NE && 
    ((xposintile - 32) * (xposintile - 32) + 
      (yposintile * yposintile)) < (RADIUS * RADIUS))
  {      
    float dist = (float)sqrt((xposintile - 32) * (xposintile - 32) + 
                      yposintile * yposintile);
    if (dist > 0)
    {
      gXPos += (RADIUS - xposintile) / dist;
      gYPos += (RADIUS - yposintile) / dist;
    }
  }

  if (gColliders[currenttile] & COLLIDE_NW && 
    ((xposintile) * (xposintile) + 
      (yposintile * yposintile)) < (RADIUS * RADIUS))
  {      
    float dist = (float)sqrt(xposintile * xposintile + 
                      yposintile * yposintile);
    if (dist > 0)
    {
      gXPos += (RADIUS - xposintile) / dist;
      gYPos += (RADIUS - yposintile) / dist;
    }
  }

  if (gColliders[currenttile] & COLLIDE_SE && 
    ((xposintile - 32) * (xposintile - 32) + 
      ((yposintile - 32) * (yposintile - 32))) < (RADIUS * RADIUS))
  {      
    float dist = (float)sqrt((xposintile - 32) * (xposintile - 32) + 
                      (yposintile - 32) * (yposintile - 32));
    if (dist > 0)
    {
      gXPos += (RADIUS - xposintile) / dist;
      gYPos += (RADIUS - yposintile) / dist;
    }
  }

  if (gColliders[currenttile] & COLLIDE_SW && 
    ((xposintile) * (xposintile) + 
      ((yposintile - 32) * (yposintile - 32))) < (RADIUS * RADIUS))
  {      
    float dist = (float)sqrt(xposintile * xposintile + 
                        (yposintile - 32) * (yposintile - 32));
    if (dist > 0)
    {
      gXPos += (RADIUS - xposintile) / dist;
      gYPos += (RADIUS - yposintile) / dist;
    }
  }

Here we first check if the square of the distance to the corner is smaller than the square of the ball's radius. This way we're saving a couple of square roots.

Next, we're calculating the exact distance, and then we're moving the ball out from the tile. The greater-than-zero check is there to avoid division by zero errors.

Now we're colliding with the walls, but the collision response is pretty bad. I'll save you several iterations that I went through before ending up with the following solution.

I originally considered easy solutions, such as:

In the above cases, the ball's collisions are one-dimensional; i.e. all we needed to do was to reverse the motion vector in the collision direction.

Unfortunately, things are not quite that simple.

Corner cases are one quite problematic; the motion on one axis turns into motion on another. And of course we don't always approach the corners directly.

Finally, consider this case: we're trying to move up and right, and we're following a wall, but a corner is coming. Eventually, we'll be hitting the wall on the right, and at that point we'll have two collisions at the same time. We'll be colliding both with the wall above as well as with the wall to the right, but we should only bounce to the left.

After trying easy solutions for a while, I gave up and picked up 2d vectors. First off, we'll need to figure out the collision normal (i.e. the vector by which we're colliding), calculate dot product with our current motion vector and adjust the motion vector based on the result.

Warning:

Some actual math ahead.

Normals? Dot product? Let's slow down a bit.

Let's start with the simple case:

On the left case, before the collision the motion vector is 1,1 and after it it should be -1,1. The normal vector for the wall points to the left, so it's -1,0.

Dot product for two vectors is d = motionx * normalx + motiony * normaly. In our case:


  d = 1 * -1 + 0 * 1 = -1.

Now, if we multiply the normal with the dot product and substract it from the motion vector twice, we get:


  x = originalx - d * normalx * 2 = 1 - (-1 * -1 * 2) = -1
  y = originaly - d * normaly * 2 = 1 - (-1 * 0 * 2) = 1

which is what we wanted.

In the corner case:

motion changes from 0,-1 to 1,0. Normal is 1,1. (Yes, that's not a normalized normal. Doesn't matter. Get over it).


  d = 0*1 + -1*1 = -1
  x = originalx - d * normalx * 2 =  0 - (-1 * 1 * 2) = 2
  y = originaly - d * normaly * 2 = -1 - (-1 * 1 * 2) = 1

What happened? Well, unlike with the simple cases, the normal vector is not one unit length; it's length is actually sqrt(2). If we normalize the vector (so that it's length is 1), we get:


  d = 0 * 0.71 + -1 * 0.71 = -0.71
  x =  0 - (-0.71 * 0.71 * 2) = 1
  y = -1 - (-0.71 * 0.71 * 2) = 0

(1 / sqrt(2) is about 0.71)

So, back to code. First, we need to harvest the collision vectors. Add the following variables after the xposintile/yposintile calculations, just before we check collisions with the tile edges:


    int collision = 0;
    float normalx = 0;
    float normaly = 0;

Go through all of the collision cases and set 'collision' to one in every case.

Go through all of the collision cases again, and instead of adjusting the ball's position, adjust the normal.

Then, after the collision cases (but before the check with the level borders), add the following:


    if (collision)
    {
      gXPos += normalx;
      gYPos += normaly;
    }

Compile and run. The application should work like before.

Add the following code inside the if (collision) block:


    // Normalize (i.e. make unit length) the collision normal
    float len = sqrt(normalx*normalx + normaly*normaly);
    normalx /= len;
    normaly /= len;

    // Calculate dot product between the wall collision and motion vector
    float dot = gXMov * normalx + gYMov * normaly;
  
    // Adjust the motion vector based on the collision
    gXMov -= dot * 1.5f * normalx;
    gYMov -= dot * 1.5f * normaly;

Here we first make sure that the normal is unit length, calculate the dot product, and adjust the motion vector.

Only one more problem to solve. If you try to slide along a wall, you keep hitting the corners. Add the following code between the wall and corner case checks:


    if (collision)
    {
      gXPos += normalx;
      gYPos += normaly;

      // re-calculate the positions so that we don't collide with
      // corners unneccessarily after colliding with the walls.
      xposintile = gXPos - (int)(gXPos / TILESIZE) * TILESIZE;
      yposintile = gYPos - (int)(gYPos / TILESIZE) * TILESIZE;
    }

After this, we can slide along walls without problems.

Now that that's over with, we can start 27 - Tuning Gameplay.

Having problems? Improvement ideas? Just want to discuss this tutorial? Try the forums!

Any comments etc. can be emailed to me.

Site design & Copyright © 2017 Jari Komppa
Possibly modified around: June 01 2010