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

Sol's Graphics for Beginners

(ch16.cpp)

(prebuilt win32 exe)

16 - Level Data From File

(Last revised 6. April 2005)

Feel free to make a copy of the project, or continue from the last one. (Backups, backups!)

In this chapter we'll continue where we were left off in chapter 12, and we'll add a bunch of game mechanisms. These include adding a time limit, scoring, several levels, loading of said levels from file, and new tile types. Along the way we'll also change the way the goal tile works.

If you have looked at the tile graphics, you may have wondered about the four tiles with an arrow on top of them. Let's implement those first.

Add the following constant (under the definition of THRUST):


// Sliding tile thrust power
#define SLIDEPOWER 0.075f

Add the following lines to the leveldataenum enumeration:


  LEVEL_LEFT = 10,
  LEVEL_RIGHT = 11,
  LEVEL_UP = 12,
  LEVEL_DOWN = 13

Make sure that the LEVEL_COLLECTIBLE line, which comes before LEVEL_LEFT, ends with a comma ( , ).

Next, in render(), inside the physics loop, where we're checking the collisions with the tiles, add the following cases:


    case LEVEL_LEFT:
      gXMov -= SLIDEPOWER;
      break;
    case LEVEL_RIGHT:
      gXMov += SLIDEPOWER;
      break;
    case LEVEL_UP:
      gYMov -= SLIDEPOWER;
      break;
    case LEVEL_DOWN:
      gYMov += SLIDEPOWER;
      break;

Finally, still in render(), find the background-filling code, and add the following cases to the switch-case structure:


      case LEVEL_UP:
        drawtile(j * TILESIZE, i * TILESIZE, 4);
        break;
      case LEVEL_RIGHT:
        drawtile(j * TILESIZE, i * TILESIZE, 5);
        break;
      case LEVEL_DOWN:
        drawtile(j * TILESIZE, i * TILESIZE, 6);
        break;
      case LEVEL_LEFT:
        drawtile(j * TILESIZE, i * TILESIZE, 7);
        break;

That's it. If you want, you can edit the level data to add these new tiles to play with them. Whenever the player is on top of an arrow tile, the player will be pushed to the direction of the arrow. The SLIDEPOWER has been chosen so that the player can get across the tile in the wrong direction, if wanted. You can easily adjust the amount of "push" the tiles have by adjusting that value.

Next, let's add the concept of time. We'll give the player 10 seconds to solve the level. Add the following constant (after TILESIZE):


// How many seconds does the player have to solve the level
#define TIMELIMIT 10

Then, add the following variable: (after gLastTick)


// Level start tick
int gLevelStartTick;

Next, initialize the variable at the end of reset():


  gLevelStartTick = SDL_GetTicks(); 

And let's draw the time below the score. Paste the following after the drawstring() near the end of the render() function:


  int secondsleft = TIMELIMIT - (gLastTick - gLevelStartTick) / 1000;
  if (secondsleft < 0)
    secondsleft = 0;
  sprintf(scorestring, "Time:%d", secondsleft);
  drawstring(5, 22, scorestring);

If you compile and run, you will see that the time counts down from 10 down to zero.

(Note that if you wait for about 25 days, the value will loop over, since the gLastTick will continue to grow until the integer overflows).

Let's add scoring and change the way the goal tile works. Add the following new variable (after the gKey variables):


// Player's score
int gScore;

To make sure the score starts from zero, add the following line somewhere in init():


  gScore = 0;

Next, find the code in render(), in our physics loop, and replace the LEVEL_END handling with the following:


    case LEVEL_END:
        gScore += 100;
        if (gCollectibleCount > 0)
        {
          int secondsleft = TIMELIMIT - (gLastTick - gLevelStartTick) / 1000;
          if (secondsleft < 0)
            secondsleft = 0;
          gScore += (secondsleft * 20 * gCollectiblesTaken) / gCollectibleCount;
        }
        reset();
      break;

After this, whenever the player reaches the goal tile, the score goes up by 100 points. If there's still time left, the score goes up by 20 points for every second that's left. Or, it would do so, if the player had collected all of the collectibles. The time bonus depends on the amount of collectibles taken; if the player collects half the collectibles, the award is half of the maximum. No collectibles, no bonus.

Let's make the player see the score next. Near the end of render() we're printing the score. Replace the sprintf line with the following:


  sprintf(scorestring, "Score:%d", gScore);

After these changes the player has several strategies to consider. One strategy is to head for the goal as fast as possible. Another is to collect all the collectibles. Third possible strategy is just to crawl to the goal to see the next level.

Next level? Right. The topic of this chapter is loading level from file, so let's do so.

First, download this test level (right click, save-as), and save it to the main directory (the one where the .bmp:s got saved earlier), or create a file called level0.txt and copy-paste the following into it:


....v..........
...>__o__o___E.
..>___.........
.>__o________<_
._____________o
._...._____o___
_____._________
_____.__o__._._
_____._____.___
__S__.^.___.___

Let's go through the level format just in case. Each line of the text file contains 15 characters, and there are 10 lines in the file. Each character represents a tile.

The underscore ( _ ) is part of the platform. 'S' stands for start, 'E' for end - ie. the goal. The period ( . ) stands for the bottomless pit. Little o:s are the collectibles. The four "arrows" ('<', 'v', '>' and '^') stand for the arrow tiles.

Go to reset(), and paste the following after we've set the gKey variables to zero (but before we start scanning through the level data):


  FILE *f = fopen("level0.txt", "rb");
  if (f == NULL)
    exit(0);

  int p = 0;
  while (p < LEVELWIDTH * LEVELHEIGHT && !feof(f))
  {
    int v = fgetc(f);
    if (v > 32)
    {
      switch (v)
      {
      case '.':
        gLevel[p] = LEVEL_DROP;
        break;
      case '_':
        gLevel[p] = LEVEL_GROUND;
        break;
      case 'S':
        gLevel[p] = LEVEL_START;
        break;
      case 'E':
        gLevel[p] = LEVEL_END;
        break;
      case 'o':
        gLevel[p] = LEVEL_COLLECTIBLE;
        break;
      case '>':
        gLevel[p] = LEVEL_RIGHT;
        break;
      case '<':
        gLevel[p] = LEVEL_LEFT;
        break;
      case 'v':
        gLevel[p] = LEVEL_DOWN;
        break;
      case '^':
        gLevel[p] = LEVEL_UP;
        break;
      }
      p++;
    }
  }
  fclose(f);

Note that the above code ignores all characters below the ASCII code 33. This means that all the control characters (namely, newline and carrier return) are ignored. The code also only cares about the first 150 meaningful characters, so, if we wanted to, we could write comments about the level after the level data. (Another 'feature'! Don't trust this 'feature' though, as it will get broken later on).

After this change, when you compile and run, you should see the test level with all of the arrow tiles and everything.

Loading data from disk is very powerful feature indeed. Let's add multiple levels!

Add the following global variable: (after the gLevelStartTick):


// Current level
int gCurrentLevel;

Reset the level to zero in init() (after reseting the score)


  gCurrentLevel = 0;

In render(), in the code where we're checking for collisions with tiles, increase the level number before calling reset() when the player reaches the end:


        gCurrentLevel++;

The above changes let us track the current level number.

Finally, we need to load a different level file in reset() depending on the current level value.

In reset(), we're opening 'level0.txt' and then we check if the result was NULL, calling exit() if so. Replace those lines with the following:


  char name[80];
  FILE * f;
  do
  {
    sprintf(name, "level%d.txt", gCurrentLevel);
    f = fopen(name, "rb");
    if (f == NULL)
    {
      if (gCurrentLevel == 0)
        exit(0);
      gCurrentLevel = 0;
    }
  }
  while (f == NULL);

The above code generates the filename based on the level number. If the level file is not found, we wrap back to the level 0. If the level 0 file is not found, we call exit().

To clean up a bit, you can remove the level data from the source file. Leave the array itself there, though.

That's it! Next, you can create any number of levels (just follow the naming convention with the filenames), or download this zip file which contains a bunch of levels.

That completes this part of the tutorial. You might say that the game is 90% complete at this point, which is, in a way, true. The other 90% still remains, though. To scratch the surface..

  • Lives. Currently the player can retry the current level infinitely. Wouldn't 3 (or 5) tries be enough?
  • Menus. The game just jumps into the game.
  • Start, goal and death sequences.
  • High scores.
  • Audio. The game is currently totally silent.
  • New tile types. Things like a 'slowdown' tile, 'speedup' tile (that would increase the speed regardless of the direction), tiles which can be collided with, pressure plates that change the state of some other tiles, etc.
  • Levels that are bigger than the screen, and scroll.
  • Better graphics.
  • Animated, translucent ball.
  • Different kinds of collectibles.
  • Game difficulty tuning. Possibly through a better physics model.
  • Lots and lots of levels.
  • Content development tools - for instance, cheat keys to skip levels and to reload the current level.
  • Testing, testing, testing, testing..

The tutorial continues with a slight detour in Part C..

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