Sol's Graphics for Beginners

(ch29src.zip)

(prebuilt win32 exe)

29 - Transitions

In this last chapter of part 'D' we'll plug in some transitions to the game. Most of the changes are small and simple, but unfortunately there's a lot of them.

Let's make the ball look like it really falls off the game board. To do this, we'll need yet another new variable. Add it to main.cpp and gp.h, with the extern-prefixes:

// State start tick
int gStateStartTick;

We'll set this variable at the end of the changestate() function (in main.cpp), so that we can calculate how long a certain state has been running:

gStateStartTick = SDL_GetTicks();

While we're in the changestate() function, remove the case where, if we're in the STATE_FALLOFF, we're setting the next state to STATE_ENTRY.

In game.cpp, we'll do something that we should have done long time ago. We'll split rendergame() into two functions. Everything from the beginning of rendergame() until the "Lock surface if needed" comment shall become gamephysics(), and everything after that will be the new rendergame().

Make the gamephysics() function declaration as follows:

void gamephysics(int tick)

Move the current tick-requesting code from the beginning of the gamephysics function down to the rendergame-function, and call the gamephysics with the current tick:

void rendergame()
{
  // Ask SDL for the time in milliseconds
  int tick = SDL_GetTicks();

  gamephysics(tick);

Compile and run to verify that things still work. You'll find that it's impossible to fall off. We'll change that in a bit.

First, we only want to run the game physics if we're actually in the INGAME state. Change the call to gamephysics to:

if (gGameState == STATE_INGAME)
  gamephysics(tick);

After this, if you compile and run, the ball will freeze if you fall off the board.

Go to the player's object-drawing code, and change it to:

// draw the player object
switch (gGameState)
{
case STATE_FALLOFF:
    break;
default:
  drawball((int)(gXPos + gCameraX), (int)(gYPos + gCameraY), 
            RADIUS, BALLCOLOR, BALLLOCOLOR, BALLHICOLOR, 
            gRoll/10.0f, (float)atan2(gYRoll, gXRoll));
  break;
}

We'll be adding more stuff here later on. If we weren't, we might as well just check if we're in the FALLOFF state or not. Compiling and running will show you that the ball now disappears if you go fall off.

Next, find the place where we're filling the background, and paste the following between the background and the level drawing code:

if (gGameState == STATE_FALLOFF)
{
  float scale = (tick - gStateStartTick) / 1000.0f;
  if (scale > 1) 
  {
    scale = 1;
    gNextState = STATE_ENTRY;
  }
  scale = 1 - scale;

  drawball((int)(gXPos + gCameraX), (int)(gYPos + gCameraY),
          (int)(RADIUS * scale), BALLCOLOR, BALLLOCOLOR, BALLHICOLOR, 
          0, 0);
}

Compile, run, and fall off the board.

Now the ball becomes smaller for one second, and the state goes forward. There's several things that we need to improve here: The dots on the ball don't shrink, the size change is too linear, and it would be nicer to fall down the background tunnel instead of just scaling down.

First, go to ball.cpp. We'll need to calculate the appropriate dot size for the ball's dots, so add the following line to the beginning of the drawball function:

int dotradius = (r / (RADIUS / 2));

The original dot radius was 2, so this code will give us dots with the radius 2 with the default radius.

Next, replace all the values of 2 with the newly calculated dotradius:

for (i = 0; i < BALLVTXCOUNT; i++)
{
  if (gRVtx[i].z < 0)
    drawcircle((int)(gRVtx[i].x * (r - dotradius) + x),
                (int)(gRVtx[i].y * (r - dotradius) + y),
                dotradius, color0);
}

for (i = 0; i < BALLVTXCOUNT; i++)
{
  if (gRVtx[i].z >= 0)
    drawcircle((int)(gRVtx[i].x * (r - dotradius) + x),
                (int)(gRVtx[i].y * (r - dotradius) + y),
                dotradius, color1);
}

Compile, run, fall. Better.

Let's get back in game.cpp, in our falling ball rendering code. What we want is to make the ball move towards the center of the "well" as it shrinks.

What this means in practise is that we need to calculate the position of the well, as well as the original position of the ball, and interpolate between these. The interpolation bit is easy. Calculating the center of the well is a bit more involved.

Change the falling ball-drawing code to:

int wellxpos = (int)(WIDTH - WIDTH * ((WIDTH / 2) - gCameraX) 
                / (gLevelWidth * TILESIZE));
int wellypos = (int)(HEIGHT - HEIGHT * ((HEIGHT / 2) - gCameraY) 
                / (gLevelHeight * TILESIZE));
drawball((int)(wellxpos * (1 - scale) + (gXPos + gCameraX) * scale), 
          (int)(wellypos * (1 - scale) + (gYPos + gCameraY) * scale),
          (int)(RADIUS * scale), BALLCOLOR, BALLLOCOLOR, BALLHICOLOR, 
          0, 0);

The well position calculation is almost the same as what we're doing each frame to calculate how the background should be drawn. We could save a bit and store the result of those calculations in temporary variables.

Compile and run.

Now the ball is falling towards the center, but the motion is too linear. When you drop an object, it tends to accelerate. Multiply the scale value with itself before we use it in the drawball():

scale = scale * scale;

Compile, run and fall off again. Slightly better.

It would be nicer if the ball would continue on its motion vector, in addition to falling off, but we won't go there for the time being.

Next we'll do the same steps with the entry of the ball. Go to main.cpp, changestate, and remove the next state setting, from STATE_ENTRY case, changing it to:

case STATE_ENTRY: 
  reset();
  break;

Back in game.cpp, go to the player's object drawing code, and add the following case:

case STATE_ENTRY:
  {
    float scale = (tick - gStateStartTick) / 1000.0f;
    if (scale > 1) 
    {
      scale = 1;
      gNextState = STATE_READY;
    }
    scale = 1 - scale;
    float pscale = scale * scale;

    drawball((int)(gXPos + gCameraX), (int)(gYPos + gCameraY), 
            (int)(RADIUS + pscale * 200), BALLCOLOR, BALLLOCOLOR, BALLHICOLOR, 
            0.4f * scale, scale * PI);
  }
  break;

Here we're basically doing the same thing as with the ball's falloff code, except that we're starting off with a huge ball, and scaling it down.

Compile and run. Play with it for a while. Press on some direction arrow while the ball is falling, and you'll notice that we have a slight problem. The game's physics will run for one full second the instant the game actually runs, and if you have some direction arrow pressed, we'll warp someplace.

We're setting gLastTick and gLevelStartTick at the end of the reset(). We'll need to set those values at the beginning of ingame; in main.cpp, add the following case in the changestate() function:

case STATE_INGAME:
  gLastTick = SDL_GetTicks(); 
  gLevelStartTick = SDL_GetTicks(); 
  break;

Make sure you add it to the second switch-structure. Don't remove the lines from reset(), as that would mess up the status messages.

Next, we have the READY state. Wipe the STATE_READY case from changestate().

In game.cpp, near the end of render(), just before we're unlocking the screen, paste the following:

if (gGameState == STATE_ENTRY || gGameState == STATE_READY)
{
  drawstring(200, 140, "Get ready!");
}

if (gGameState == STATE_READY)
{
  drawstring(230, 160, "Go!");
  if (tick - gStateStartTick > 500)
    gNextState = STATE_INGAME;
}

if (gGameState == STATE_FALLOFF)
{
  drawstring(200, 140, "Fall off!");
}

Compile and run.

Final thing to add is the handling of the ENDLEVEL state. As before, go to changestate() in main.cpp, and remove the STATE_ENDLEVEL block from the second switch-structure.

Back in game.cpp, add the following block before we're unlocking the screen in render():

if (gGameState == STATE_ENDLEVEL)
{    
  int eventpos = tick - gStateStartTick;
  char temp[80];
  drawstring(160, 120, &quot;Level finished!&quot;);
  if (eventpos > 500)
  {
    float seconds = (gLastTick - gLevelStartTick) / 1000.0f;
    int bonus = (int)(gLevelTime - secondsleft) * 25;
    if (bonus < 0)
      bonus = 0;
    sprintf(temp, &quot;Time:%0.2fs Bonus:%d&quot;, seconds, bonus);
    drawstring(160, 140, temp);
    if (eventpos > 1000)
    {
      sprintf(temp, &quot;Coins:%d%%&quot;, gCollectiblesTaken * 100 / gCollectibleCount);
      drawstring(160, 160, temp);
      if (eventpos > 2000 && (eventpos % 2000 > 500))
        drawstring(160, 200, &quot;Hit space for next level!&quot;);
    }
  }
}

Compile and run. Of course, pressing space at the end of the level won't do anything yet.

In main.cpp, main() function and main loop, we're checking for SDL_KEYUP events. Add the following cases after the SDLK_DOWN case:

case SDLK_RETURN:
case SDLK_SPACE:
  if (gGameState == STATE_ENDLEVEL)
  {
    gNextState = STATE_ENTRY;
  }
  break;

Now you can enter the next level by hitting space (or return), even before the ENDLEVEL state has shown all of the score information.

There's still tons of little things we could do, like make the texts slide in and out, cause the score to increase as the bonus is shown, underline the bonus system by showing how time left turns into points, etc.

That's all for now. There's still plenty to do, like menus.

(don't tell anyone, but apparently I wrote a bunch of chapters and never published them. They're probably a bit rough)

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

Any comments etc. can be emailed to me.