Sol's Graphics for Beginners

(ch24src.zip)

(prebuilt win32 exe)

24 - Moving Camera

Feel free to make a copy of the project, or continue from the last one.

Before we can have levels that are larger than the screen, we need to implement some way of seeing a larger area than the screen. To do this, we'll implement a "camera" of sorts - that is, we'll have one coordinate pair (gScreenX and gScreenY) with which we'll track what the player sees.

Basically we'll just add those variables, and then add the contents of said variables to everything we render. That means that we need to add clipping to the tiles. Let's start with that.

I could go through the clipping step by step, but we've already covered that in 06 - Primitives and Clipping. Thus I'll just give you the finished implementation of the clipping-enabled drawtile function.

Go to graphics.cpp, and replace drawtile() function with this:

void drawtile(int x, int y, int tile)
{
  // Lock surface if needed
  if (SDL_MUSTLOCK(gTiles))
    if (SDL_LockSurface(gTiles) < 0) 
      return;

  int i, j;
  for (i = 0; i < TILESIZE; i++)
  {
    // vertical clipping: (top and bottom)
    if ((y + i) >= 0 && (y + i) < HEIGHT)
    {
      int len = TILESIZE;
      int xofs = x;
      int tilexofs = 0;

      // left border
      if (xofs < 0)
      {
        tilexofs -= xofs;
        len += xofs;
        xofs = 0;
      }

      // right border
      if (xofs + len >= WIDTH)
      {
        len -= (xofs + len) - WIDTH;
      }
      int ofs = (i + y) * PITCH + xofs;

      // note that len may be 0 at this point, 
      // and no pixels get drawn!

      int tileofs = (i + tile * TILESIZE) * 
                    (gTiles->pitch / 4) + tilexofs;
      for (j = 0; j < len; j++)
      {
        ((unsigned int*)gScreen->pixels)[ofs] = 
          ((unsigned int*)gTiles->pixels)[tileofs];
        ofs++;
        tileofs++;
      }
    }
  }

  // Unlock if needed
    if (SDL_MUSTLOCK(gTiles)) 
        SDL_UnlockSurface(gTiles);
}

The above is practically the merging of the clipping in drawrect() and the old drawtile() with one important difference.

When drawing the rectangle, all the pixels on a single span had the same value. When rendering tiles, every pixel we're drawing has a different value, and thus we need to track the x-offset when clipping on the left border. We're using the tilexofs variable to do this.

Later on, when we're moving around, you can try what happens when the tilexofs is removed. The program won't crash, but you'll get an interesting graphics glitch.

Moving on, let's add the camera coordinate pair. Add the following in main.cpp, and also in gp.h (with the extern-prefixes):

// Camera position
float gCameraX;
float gCameraY;

The rest of the changes will be done to game.cpp. First, go to the start of the reset() function, and add the following lines to reset the camera position:

gCameraX = WIDTH / 2;
gCameraY = HEIGHT / 2;

Next, in the rendergame() function, find the line where we're incrementing the gRoll variable. Above it, add the following:

gCameraX = (WIDTH / 2) - gXPos;
gCameraY = (HEIGHT / 2) - gYPos;

Then, go down to the comment 'fill background'. After that comment we have the level-rendering rendering loop, where we're calling drawtile() with different parameters depending on the level data. We need to add the gCameraX and gCameraY to all of the coordinates. Instead, let's refactor the code a bit. Replace the switch-case structure with this:

if (gLevel[i * LEVELWIDTH + j] != 0)
{
  int tile = 0;

  switch (gLevel[i * LEVELWIDTH + j])
  {
  case LEVEL_START:
    tile = 2;
    break;
  case LEVEL_END:
    tile = 1;
    break;
  case LEVEL_UP:
    tile = 4;
    break;
  case LEVEL_RIGHT:
    tile = 5;
    break;
  case LEVEL_DOWN:
    tile = 6;
    break;
  case LEVEL_LEFT:
    tile = 7;
    break;
  case LEVEL_GROUND:
  case LEVEL_COLLECTIBLE:
  default:
    // tile = 0;
    break;
  }
  drawtile((int)(j * TILESIZE + gCameraX), 
            (int)(i * TILESIZE + gCameraY), tile);
}

Compile and run.

The game now looks somewhat odd, since only the level tiles are obeying the camera position. Below the level rendering loop, we're drawing the collectibles. Add gCameraX and gCameraY to the X and Y-values of the collectibles' drawcircle() function calls, like so:

drawcircle((int)(gCollectible[i].mX + 2 + gCameraX),
        (int)(gCollectible[i].mY + 2 + gCameraY),
        gCollectible[i].mRadius,
        0);
drawcircle((int)gCollectible[i].mX + gCameraX,
        (int)gCollectible[i].mY + gCameraY,
        gCollectible[i].mRadius,
        gCollectible[i].mColor);

After the collectibles we have the player's object; add the camera variables to it's coordinates as well:

// draw the player object
drawball((int)(gXPos + gCameraX), (int)(gYPos + gCameraY), 
          RADIUS, BALLCOLOR, BALLLOCOLOR, BALLHICOLOR, 
          gRoll / 10.0f, (float)atan2(gYMov, gXMov));

Compile and run. Mission accomplished, but we'll do a couple more changes.

First off, colliding with the "screen borders" don't make much sense anymore. Find the comment that says "Collision with the screen borders", and replace the collision code - everything between the comment and the collectibecollision() call - with the following:

// Collision with the level borders
if (gXPos > WIDTH || gXPos < 0 || gYPos > HEIGHT || gYPos < 0)
{
  reset();
}

With the above call in place, the player will fall off the track whenever they move outside the level.

Now the camera is fixed to the player's position. This is practical, but it feels a bit too static. Find the code where we're updating the camera position, and replace it with this:

float targetx = (WIDTH / 2) - gXPos;
float targety = (HEIGHT / 2) - gYPos;
gCameraX = (targetx + gCameraX * 19) / 20;
gCameraY = (targety + gCameraY * 19) / 20;

Compile and run. Now the camera lags behind the player; the amount of lag can be adjusted by changing the ratio of the new and old values. (Remember, the above code will be run 100 times per second, regardless of the frame rate).

The main negative side with the lagging camera is that the player is able to move too far to the edges of the screen. We can fight this by reducing the lag, but there's a better solution. Instead of making the camera's desired target the ball itself, we can predict where the camera should be in a while, and aim for that.

float targetx = (WIDTH / 2) - (gXPos + gXMov * 25);
float targety = (HEIGHT / 2) - (gYPos + gYMov * 25);

The combined effect of these two changes is subtle, but the camera feels more natural this way. You can adjust the multipliers to adjust the amount of the game field the player sees ahead.

Finally, currently when you start a level, the camera moves from the center position to the desired position. Let's remove this movement by starting the camera at the desired position.

Remove the gCameraX and gCameraY resetting lines from the beginning of the reset() function, and add the following lines to the end of the function:

gCameraX = (WIDTH / 2) - gXPos;
gCameraY = (HEIGHT / 2) - gYPos;

When you compile and run, the camera starts off from where we'd expect it to.

Now we're finally ready for 25 - Bigger Levels.

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

Any comments etc. can be emailed to me.