Sol's Graphics for Beginners

(ch15.cpp)

(prebuilt win32 exe)

15 - Slightly Improved Text

(Last revised 6. April 2005)

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

Let's first tackle with the problem of the fixed width font. If the font was really fixed width (such as the command prompt / console font), we wouldn't have a problem, but in our case every character has a different width.

In other words, we need to figure out the width of each character in the font, and then use that information to skip the empty horizontal space in the character data while printing.

First we need a couple of new arrays to store this new font information. Paste the following after the level data:

// Offset of character data per character
int gFontOfs[256];
// Width of character data per character
int gFontWidth[256];

These arrays are a bit oversized, since the font doesn't have that many characters. We could allocate the arrays when loading the font to avoid wasting space. On the other hand, we know that the font will never have over 256 characters, since our characters are 8 bit.

Next, paste the following code in init(), after the font image has been loaded:

if (SDL_MUSTLOCK(gFont))
  if (SDL_LockSurface(gFont) < 0) 
    return;

int i, j, k;
for (k = 0; k < gFont->h / gFont->w; k++)
{
  gFontOfs[k] = gFont->w;
  gFontWidth[k] = 0;
  for (i = 0; i < gFont->w; i++)
  {
    for (j = 0; j < gFont->w; j++)
    {
      if (((unsigned int*)gFont->pixels)[(i + k * gFont->w) * 
                                          (gFont->pitch / 4) + j] != 0) 
      {
        if (j < gFontOfs[k])
          gFontOfs[k] = j;

        if (j > gFontWidth[k])
          gFontWidth[k] = j;
      }
    }
  }
  gFontWidth[k] -= gFontOfs[k];
}

// Unlock if needed
  if (SDL_MUSTLOCK(gFont)) 
      SDL_UnlockSurface(gFont);

The 'k' variable loops through all of the characters in the font. The number of characters in the font is calculated by dividing the height of the image by the width.

The 'i' and 'j' variables loop through the font data. If a non-zero pixel is found, we check whether it is closer to the left or right border than any earlier pixel, and if so, this information is stored in gFontOfs (for left border) or gFontWidth (for right border).

After the whole character is looped through, the offset (leftmost pixel position) is substracted from the 'width' (rightmost pixel position) so that gFontWidth will contain the font width.

Again note that we're locking and unlocking the surface before accessing the pixel data directly.

Now that we have collected the data, we need to put it into use. This is relatively easy. First, in the drawcharacter() function, replace the lines starting with "int charofs..." and "for (j = 0;..." with the following:

int charofs = (i + character * gFont->w) * 
              (gFont->pitch / 4) + gFontOfs[character];
for (j = 0; j < gFontWidth[character]; j++)

If you compile and run, you'll see that the black areas between the characters are gone, but there's empty space there instead. Replace the drawstring() function with this:

void drawstring(int x, int y, char *s)
{
  while (*s != 0)
  {
    drawcharacter(x, y, *s);
    if (*s == 32)
      x += gFont->w / 2;
    else
      x += gFontWidth[*s - 33];
    s++;
  }
}

The value 32 is the ASCII code for space. Since the font doesn't contain a space (and what would the space's width be, since it doesn't contain any pixels?), we're moving the offset by half of the image size. For all other characters, the offset is moved based on our precalculated character widths.

Compile and run. Much better, but the text still obscures the background.

Copy the blending functions from Blending a Bit (blend_avg(), blend_mul() and blend_add()). Paste them before the drawcircle() function.

Next, we'll do a couple of changes to the drawcharacter() function. First, add a new parameter called 'blend', like so:

void drawcharacter(int x, int y, int character, int blend)

Next, replace the inner loop with this:

for (j = 0; j < gFontWidth[character]; j++)
{
  int pixel = ((unsigned int*)gFont->pixels)[charofs];
  if (pixel != 0)
  {
    int oldpixel = ((unsigned int*)gScreen->pixels)[screenofs];
    switch (blend)
    {
    case -1:
      pixel = blend_mul(pixel ^ 0xffffff, oldpixel);
      break;
    case 0:
      pixel = blend_avg(pixel, oldpixel);
      break;
    case 1:
      pixel = blend_add(pixel, oldpixel);
      break;
    }
    ((unsigned int*)gScreen->pixels)[screenofs] = pixel;        
  }
  screenofs++;
  charofs++;
}

Now, depending on the 'blend' value, the text is blended to the screen using either multiplicative, average or additive blend. The only really tricky bit is the fact that we're using the XOR operator (^) to inverse the bits of the color in the multiplicative blend. This is so that the brigther the pixel, the darker the multiplicative blend will become.

As an additional 'feature', if you use some other value than -1, 0 or 1 as blend, the pixels will not be blended at all, and will be drawn as solids instead. This is one of those rare cases where a 'bug' can be considered a 'feature' with clean conscience.

Go to drawstring(), and change the drawcharacter() call, adding the new parameter:

drawcharacter(x, y, *s, 0);

Compile and run. Okay, now the text doesn't obscure anymore.. but you can't really read the text either. Try out the other blend modes too (-1 and 1).

To solve this little problem, we'll replace the single drawcharacter() call with two:

drawcharacter(x + 1, y + 1, *s, -1);
drawcharacter(x, y, *s, 0);

Compile and run. Problem solved. For brigher text, change the second blend mode to additive (value 1).

Let's replace the "hello world!" with something more suitable, like score printout.

Go to 'render()', and replace the call to drawstring() with the following:

char scorestring[80];
sprintf(scorestring, "Score:%d", gCollectiblesTaken);
drawstring(5, 5, scorestring);

Since we don't have an actual score yet, we'll use the gCollectiblesTaken instead. Compile and run.

There's a couple of further improvements that could be done to this printer. First, the letters are now really close to each other; this may work for one font, but will look horrible on another. Adding some empty space between every character would help. This is easiest to implement in drawstring(). Another is the support for newline characters, also in drawstring(). Third, the width of the space is rather wide for this font, so that should probably be customized. We could also implement clipping.

For a very heavy-duty "printer", check out the freetype project. Our little bitmap printer will do for now, though.

Now that we're finally starting to look a bit like a game, let's add a couple more game-oriented things, most importantly the loading of 16 - Level Data From File.

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

Any comments etc. can be emailed to me.