Hi all! This lesson is about drawing bitmap fonts on a surface in SDL. They are called bitmap fonts because our fonts will be stored in one big bitmap that contains all of them. Here's what one of our font map looks like:



Our font system will consist of 2 files: font.cpp and font.h. The lesson itself will also have the file lesson4.cpp. I decided to put the font routines into seperate files because this way it's much easier to use the same code in many programs - just copy the 2 files to your new project, add an '#include "font.h"' line and you're all set.

Note: I won't talk about what code goes into which file. You can put all the code into one big file if you want (if you like to follow this tutorial while coding your font routines). I will only talk about the functions. If you want to look at the code in many files, look at the source. Also this code could really easilly be put into a class. I won't use classes this time.

All our fonts will be stored inside one structure called SDLFont. It looks like this:

// Structure to hold our font
struct SDLFont
{
  SDL_Surface *font;    // The SDL Surface for the font image
  int width;            // Width of the SDL Surface (=height)
  int charWidth;        // Width of one block character in the font
  int *widths;          // Real widths of all the fonts
  unsigned char *data;  // The raw font data
};

The SDL_Structure font will hold the font image map. Width is the width of the font (for easy access) and since the font image is a square, the width = height. charWidth is the with of one "character cell" on the font (width/16). Since mostly all of the characters in the font have different real widths, then the array widths will hold the widths of all the characters. Data will hold the raw image data for the SDL_Surface.

Now here's one function that you should remember from the previous lessons (if you have read them). It simply blits one part of a surface onto an other part of some other surface. It will be used when drawing the font.

void fontDrawIMG(SDL_Surface *screen, SDL_Surface *img, int x, 
                           int y, int w, int h, int x2, int y2)
{
  SDL_Rect dest;
  dest.x = x;
  dest.y = y;
  SDL_Rect src;
  src.x = x2;
  src.y = y2;
  src.w = w;
  src.h = h;
  SDL_BlitSurface(img, &src, screen, &dest);
}

Before using a font, we need to initalize it. We have the function initFont for that. It returns a new SDLFont structure. initFont takes the directory of the font as a parameter (the font consists of 3 files that must be one directory). It also takes the rgb color value of the font and an alpha value. If you aren't sure what RGB means, check this page: http://whatis.techtarget.com/definition/0,,sid9_gci212900,00.html. Alpha tells us how much % stuff will shine through the font. For the r, g, b and a values, give floating point numbers from 0 to 1. You may need to experiment with the different settings.

SDLFont *initFont(char *fontdir,float r, float g, float b, float a)

So, if you would want to use a fonts you'd do:

SDLFont *font;
font = initFont("data/fonts/arial", 1, 1, 0, 0.5);

That code would create you a half transparent arial font (if, of course you have an arial font in "data/fonts/arial") with the color yellow.

Anyway, back to initFont. We have some variables at the top of initFont. Most of them are temporary. We'll only return the tempFont variable with the function.

SDLFont *initFont(char *fontdir,float r, float g, float b, float a)
{
  // some variables
  SDLFont *tempFont;               // a temporary font
  FILE *fp;                        // file pointer
  char tempString[100];            // temporary string
  unsigned char tmp;               // temporary unsigned char
  int width;                       // the width of the font
  SDL_Surface *tempSurface;        // temporary surface

The next lines make the string tempString equal the path of 'font.ini'. If for example the parameter fontdir would be "data/font", then after this operation tempString would be "data/font/font.ini". We read in the width of the font from the ini file. On error we return 0.

  // find out about the size of a font from the ini file
  sprintf(tempString,"%s/%s",fontdir,"font.ini");
  fp = fopen(tempString, "rb");
  if( fp == NULL )
  {
    return 0;
  }
  fscanf(fp, "%d", &width);
  fclose(fp);

Now we will create our font. We first 'new' the tempFont structure. After that we allocate width*width*4 bytes of space for the image data. There are width*width pixels in our font image and since the image will have 4 channels - RGBA, we multiply it by 4. We then give the font structure the width of the font and the width of one character cell (width/16).

  // let's create our font structure now
  tempFont = new SDLFont;
  tempFont->data = new unsigned char[width*width*4];
  tempFont->width = width;
  tempFont->charWidth = width/16;

Now we must read in the font data. For that we make tempString equal the path to font.raw - the raw grayscale image data of the font. We then load in the font one pixel at a time. When reading in the font image, we store 255 in the rgb channels of the image multiplied by the r, g and b function parameters (to get the correct color). And in the alpha channel of the new SDL surface we store the brightness of the pixel (grayscale color) multiplied by the function parameter a (as in alpha). We store 255*{r,g,b} instead of the font brightness*{r,g,b} in the rgb channels to get a good smooth font at the edges. If we can't read in the font data (some error occurred), we'll return 0.

  // open the font raw data file and read everything in
  sprintf(tempString,"%s/%s",fontdir,"font.raw");
  fp = fopen(tempString, "rb");
  if( fp != NULL )
  {
    for(int i=0;i<width*width;i++)
    {
      tmp = fgetc(fp);
      tempFont->data[i*4] = (unsigned char)255*(unsigned char)r;
      tempFont->data[i*4+1] = (unsigned char)255*(unsigned char)g;
      tempFont->data[i*4+2] = (unsigned char)255*(unsigned char)b;
      tempFont->data[i*4+3] = (unsigned char)(((float)tmp)*a);
    }
  } else {
    return 0;
  }
  fclose(fp);

Now we will create a new SDL_Surface. This process can be used elsewhere as well, not only in the creation of THIS SDL_Surface. Most of the following code comes from the SDL_CreateRGBSurfaceFrom and SDL_CreateRGBSurface SDL Documentation pages. Check them out for more info on the subject. The r,g,b,a masks tell SDL the byte order of the r,g,b,a values. But what's with the #ifs? From the SDL Doc: SDL interprets each pixel as a 32-bit number, so our masks must depend on the endianness (byte order) of the machine. We create the real surface with the SDL_CreateRGBSurfaceFrom function. As you might have noticed, there's also a SDL_CreateRGBSurface function, that creates a new surface, but doesn't add any data to it. Now, to SDL_CreateRGBSurfaceFrom we pass the image data, the width and the height of the file, the number of bits per pixel, the number of bytes for one row of the image and the rgba masks. We also convert the surface to the display format for faster blitting.

  // now let's create a SDL surface for the font
  Uint32 rmask,gmask,bmask,amask;
  #if SDL_BYTEORDER == SDL_BIG_ENDIAN
  rmask = 0xff000000;
  gmask = 0x00ff0000;
  bmask = 0x0000ff00;
  amask = 0x000000ff;
  #else
  rmask = 0x000000ff;
  gmask = 0x0000ff00;
  bmask = 0x00ff0000;
  amask = 0xff000000;
  #endif
  tempFont->font = SDL_CreateRGBSurfaceFrom(tempFont->data, 
        width, width, 32, width*4, rmask, gmask, bmask, amask);
  tempFont->font = SDL_DisplayFormatAlpha(tempSurface);
  SDL_FreeSurface(tempSurface);

Now let's also load in the widths of the fonts. The code should be easy to follow.

  // let's create a variable to hold all the widths of the font
  tempFont->widths = new int[256];

  // now read in the information about the width of each character
  sprintf(tempString,"%s/%s",fontdir,"font.dat");
  fp = fopen(tempString, "rb");
  if( fp != NULL )
  {
    for(int i=0;i<256;i++)
    {
      tmp = fgetc(fp);
      tempFont->widths[i]=tmp;
    }
  }
  fclose(fp);

And finally, we return the tempFont.

  /// return the new font
  return tempFont;
}

And that's it with initFont. Now let's go on with drawString. drawString takes the surface to draw to, the font to draw with, the position where to draw and the string to draw as arguments. It looks like this:

void drawString(SDL_Surface *screen, SDLFont *font, int x, int y, 
                                                   char *str, ...)

Since drawString uses the "variable number arguments" thingy, we are gonna have to "dechyper" it. That's done with the following lines. (Take a look at the Grounds Up lesson 8 for more info on functions with a variable number of arguments)

{
  char string[1024];                  // Temporary string

  va_list ap;                // Pointer To List Of Arguments
  va_start(ap, str);         // Parses The String For Variables
  vsprintf(string, str, ap); // Converts Symbols To Actual Numbers
  va_end(ap);                // Results Are Stored In Text

Now let's get the lenght of the string. We'll also have one more variable xx that will hold the width of the string that has so far been printed - every time we print a character, we increment xx with the width of the character.

  int len=strlen(string);  // Get the number of chars in the string
  int xx=0; // This will hold the place where to draw the next char

Now we'll loop through all the chars in the string and draw each character onto the screen in each loop. (And remember, the fontDrawIMG function looks like this: void fontDrawIMG(screen, img, x, y, w, h, x2, y2); )

  for(int i=0;i<len;i++)// Loop through all the chars in the string
  {

We draw onto the screen SDL_Surface a part of the font SDL_Surface.

    fontDrawIMG(
      screen,
      font->font,

We draw the char at pos [x+xx,y]. x+xx = this function's parameter x + the width of all the characters before this one, so we wouldn't overlap any of the previous characters in the string when drawing the next one. y = this function's y parameter

      xx+x,
      y,

For the width of the to-be-drawn character we take it's real width + 2

      font->widths[string[i]]+2,

And for the height we take the height of the character (height/width of the font/16)

      font->charWidth,

Now comes the tricky part. The font image DOES consist of a 16x16 grid of characters. From left to right in the image, the ascii values of the characters increase: The character at block 0x0 in the font image is the character with the ascii code 0, the character at the pos 1x0 has the ascii code 1, the char at the pos 15x0 has the ascii code 15. And that's the end of the first row Now in the second row on the image, the first character (0x1) has the ascii value 16, the fourth character on the second row of the image (3x1) has the ascii value 19. To calculate the ascii value of the character 3x1, we use the really simple equation: row*[number of thing in one row (=16)]+column pos. So the position 3x1 has the ascii value 1*16+3 = 19. The character in the image at the position 8x12 has the ascii value 12*16+8=200, and so on. But this isn't much of use to us since we KNOW the ascii value of a character, but we NEED to find out it's position on the image. First we'll get the column on the image. For that we'll divide the ascii value with 16 (the number of columns) and get it's remainder (we'll use the modulus operator). We'll do this equation to get the column: [ascii value]%16. Now to get to the position of the column in pixels, we multiply the ascii value by the width of one column ([font image width]/16) And so the equation to get the first pixel of the block becomes: [font image width]%16*[1/16th of the font image width] Now, since all the letters are centered in each cell (cell = 1/16th of the image), we need to get the center of the cell. This is done by adding half the width of the cell to the number we got before. And the only thing left to do is to subtract half of the character's real width and we get the x position from where to draw our character on the font map :)

      (string[i]%16*font->charWidth)+((font->charWidth/2)
                               -(font->widths[string[i]])/2),

To get the row of the character in the image, we divide the ascii value of the character by 16 and get rid of all the numbers after the point (.) (if we get the number 7.125257.., we remove the .125157... and end up with 7) We then multiply the result with the height of one cell and voila - we get the y position!

      (((int)string[i]/16)*font->charWidth)
    );

Now we increase the xx width counter by the width of the drawn character.

    xx+=font->widths[string[i]];
  }
}

And that's it with drawString :). We have 2 more font functions: stringWidth and freeFont. As you may have guessed, stringWidth returns the width of the string. It's really easy to follow, so I won't talk about it much. It starts the same way as drawString.

int stringWidth(SDLFont *font,char *str,...)
{
  char string[1024];         // Temporary string

  va_list ap;                // Pointer To List Of Arguments
  va_start(ap, str);         // Parses The String For Variables
  vsprintf(string, str, ap); // Converts Symbols To Actual Numbers
  va_end(ap);                // Results Are Stored In Text

Now we just count up the width of all the characters

  int xx=0;
  int len=strlen(string);
  for(int i=0;i<len;i++)
  {
    // Add their widths together
    xx+=font->widths[string[i]];
  }

and then return the sum

  return xx;
}

And the final font function, freeFont, MUST be called before you exit your program. It frees up the memory used by the font.

void freeFont(SDLFont *font)
{
  delete [] font->widths;
  delete [] font->data;
  SDL_FreeSurface(font->font);
  delete font;
}

And that's the end with the font functions. I also created 2 overloaded inline functions for initFont that may make your life a bit easier. They call the

SDLFont *initFont(char *fontdir,float r, float g, float b, float a)

version of the initFont function. The first one doesn't have the alpha parameter - it's defaulted to 1. The second one only has the fontdir parameters. The color is white. The reason why they are inline is simple - speed.

inline SDLFont *initFont(char *fontdir, float r, float g, float b) 
{ 
  return initFont(fontdir, r,g,b,1); 
}

inline SDLFont *initFont(char *fontdir) 
{ 
  return initFont(fontdir, 1,1,1,1); 
}

Now let's look at the main program code. We have coulpe of global variables: the screen surface, 2 fonts, the location of the scrolling text that is in this demo and the scrolling text itself.

SDL_Surface *screen;   // The screen surface
SDLFont *font1;        // 2 fonts
SDLFont *font2;
int y=480;             // Position of the scrolling text

char *string="Cone3D GFX with SDL Lesson 4"; // The scrolling text

DrawScene is the function where we do all the drawing.

void DrawScene()
{

The first thing we do is fill the entire screen with the color black. You can use the function SDL_FillRect to fill any part of any surface with any color. The first parameter to SDL_FillRect is the screen to color, the second is a SDL_Rect that specifies the area of the surface to fill. The final parameter is the color to draw. You can give the color to the function with a hex value (0xRRGGBB) (replace RR with a 2 digit hex value representing the red color component, GG with a 2 digit hex value representing the green color component and BB with a 2 digit hex value representing the blue color component). Try one of these pages for a rgb to hex convertor:

http://mediagods.com/tools/rgb2hex.html
http://javascript.internet.com/equivalents/rgb-to-hex.html
http://www.univox.com/home/support/rgb2hex.html

For fun you may change the hex number below to test different background colors.

  // Clear the entire screen with black
  SDL_FillRect(screen,NULL,0x000000);

We draw the scrolling text at it's scroll position on the center of the screen.

  drawString(screen,font1,320-stringWidth(font1,string)/2,
                                                 y,string);

At the top-left of the screen we draw a little message saying where the scrolling message is at the moment.

  drawString(screen,font2,1,1,"Scroll location: %d",y);

And we'll also draw the url to this site on the bottom right of the screen.

  drawString(screen,font2,639-stringWidth(font2,
  "http://cone3d.gamedev.net"),480-16,"http://cone3d.gamedev.net");

And finally we flip the screen buffers.

  SDL_Flip(screen);
}

Now in main we start with the usual stuff

int main(int argc, char *argv[])
{
  // Initalize SDL
  if ( SDL_Init(SDL_INIT_AUDIO|SDL_INIT_VIDEO) < 0 )
  {
    printf("Unable to init SDL: %s\n", SDL_GetError());
    exit(1);
  }
  atexit(SDL_Quit);

  // Load a bmp file as the icon of the program
  SDL_WM_SetIcon(SDL_LoadBMP("icon.bmp"),NULL);

  // Initalize the video mode
  screen=SDL_SetVideoMode(640,480,32,
     SDL_SWSURFACE|SDL_HWPALETTE|SDL_FULLSCREEN);
  if ( screen == NULL )
  {
    printf("Unable to set 640x480 video: %s\n", SDL_GetError());
    exit(1);
  }

We load in the fonts. Notice that I'm using 2 different functions here. For fun you may try different color/alpha falues here. Also know that you SHOULD do stuff like "if(font1==NULL) {printf("error");}" here.

  // Load in the fonts
  font1 = initFont("data/font1");
  font2 = initFont("data/font2",1,1,0);

Now we do the usual loop

  // Loop a bit
  int done=0;
  while(done == 0)
  {
    SDL_Event event;

    while ( SDL_PollEvent(&event) )
    {
      // If someone closes the prorgam, then quit
      if ( event.type == SDL_QUIT )  {  done = 1;  }

      if ( event.type == SDL_KEYDOWN )
      {
        // If someone presses ESC, then quit
        if ( event.key.keysym.sym == SDLK_ESCAPE ) { done = 1; }
      }
    }

Draw the scene

    // Draw the scene
    DrawScene();

Ans scroll the text.

    // Scroll the text
    y-=1;if(y<-32) y=480;
  }

And now, if somebody happends to make the program exit, we free the fonts.

  // Let's clean up...
  freeFont(font1);
  freeFont(font2);

  return 0;
}

And that's it with this lesson. You can download the source here. You can also find more fonts here.

 

Posted by 싸이on
,