Introduction to SDL_ttf

A beginner’s introduction to the SDL_ttf library, and how we can use it to add text rendering to our application.
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
aSDL9d.jpg
Ryan McCombe
Ryan McCombe
Posted

In this lesson, we’ll learn how to render text in our SDL2 programs using SDL2_ttf.

This lesson assumes we already have SDL2 and SDL2_ttf installed and set up in our project.

If this is not the case, refer to our earlier lessons where we set up the development environment for Windows or Mac:

Let's start with a basic event loop and window class, similar to what we covered in previous lessons:

#include <SDL.h>

#include "TextWindow.h"

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);

  TextWindow Window;

  SDL_Event Event;
  while (true) {
    while (SDL_PollEvent(&Event)) {
      if (Event.type == SDL_QUIT) [[unlikely]] {
        SDL_Quit();
        return 0;
      }
    }
  }

  return 0;
}
#pragma once
#include <SDL.h>
#include <SDL_ttf.h>
#include <iostream>
#include <string>

using std::cout, std::endl;

class TextWindow {
 public:
  TextWindow() {
    SDLWindow = SDL_CreateWindow(
      "Hello Window",
      SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED,
      600, 150, 0
    );

    SDLWindowSurface = SDL_GetWindowSurface(SDLWindow);
    FillWindowSurface();
    SDL_UpdateWindowSurface(SDLWindow);
  }

 private:
  void FillWindowSurface() {
    SDL_FillRect(
      SDLWindowSurface,
      nullptr,
      SDL_MapRGB(SDLWindowSurface->format, 240, 240, 240)
    );
  }

  SDL_Window* SDLWindow{nullptr};
  SDL_Surface* SDLWindowSurface{nullptr};
};

This should be familiar from our previous lessons on creating windows and application loops using SDL2:

Initialising SDL_ttf

Before we can start using SDL_ttf, we need to initialize it. We do this by calling the TTF_Init function:

TTF_Init();

This function returns 0 if the initialization was successful, and -1 otherwise. In the event of an error, we can also call TTF_GetError() to retrieve a text description of what went wrong.

Let's add this code to the bottom of our TextWindow constructor:

if (TTF_Init() < 0) {
  cout << "Error from TTF_Init: "
       << TTF_GetError() << endl;
}

When we no longer need SDL_ttf, we can unload it by calling TTF_Quit(). Let's do that in a destructor:

~TextWindow() {
  TTF_Quit();
}

Loading Fonts

To render text, we need to have a font we want to use. Fonts are typically stored in the TrueType Font (.ttf) format. These files are widely available online, with many free options available. In this example, we’ll use Roboto, a free font available from Google Fonts.

Once we move the font into our project directory, we can load it using the TTF_OpenFont function, passing in the path to the font file, and the size we want to use:

Font = TTF_OpenFont("Roboto-Medium.ttf", 50);

This function returns a pointer to a TTF_Font, which we will need to use later when rendering our text.

Let's add this variable to our class, as well as a function we can call from our constructor to populate it. The rest of our code remains unchanged:

class TextWindow {
public:
  TextWindow() {
    // ...
    LoadFont();
  }

private:
  void LoadFont() {
    Font = TTF_OpenFont("Roboto-Medium.ttf", 50);
  }
  TTF_Font* Font;
};

If TTF_OpenFont fails, it will return a nullptr. Additionally, we can inspect the error by calling TTF_GetError().

Let's enhance our LoadFont function accordingly:

void LoadFont() {
  Font = TTF_OpenFont("Roboto-Medium.ttf", 50);
  if (!Font) {
    cout << "Failed to load font: "
         << TTF_GetError() << endl;
  }
}

When we no longer need our font, we can clean it up by passing the pointer to TTF_CloseFont. Let's do that in our destructor:

~TextWindow() {
  TTF_Quit();
  TTF_CloseFont(Font);
}

Rendering Text

With our font available, we can use it to create new SDL_Surface objects containing our desired text.

SDL_ttf provides a lot of options for doing this, each implemented as a separate function. The function we want depends on two things:

  • The character encoding we need. In most cases, we will want UTF-8 encoding.
  • The quality of text we want.

Why would we ever want low-quality text?

Fonts store their visual data - the shape of each character - in what are known as vector images. Vector images are versatile - they’re the reason we can scale fonts up or down without losing quality.

But, we can’t render vectors directly on our screen. To render something on our screen, we need to have raster (that is, bitmap) data. That is, we need to define what color each pixel needs to be.

Rasterization (converting vectors to rasters) is not a trivial process. This is especially true if we want our text to have smooth, anti-aliased edges. Blitting those soft edges onto non-solid background colors adds yet more work.

Calls to SDL_ttf’s render functions can degrade performance, particularly if we’re calling them frequently, or with large amounts of text.

To account for this, SDL_ttf gives us 4 options for how we want to render our text, each implemented as a separate function:

  • LCD (eg TTF_RenderUTF8_LCD) does not antialias the text and requires an additional argument where we specify a solid background color. This has the best possible rendering and blitting performance
  • Solid (eg TTF_RenderUTF8_Solid) does not antialias the text, and does not require us to specify the background color. The resulting surface can be blitted onto any background color. This has the same rendering performance as LCD, but worse blitting performance.
  • Shaded (eg TTF_RenderUTF8_Shaded) antialiases the text and requires an additional argument where we specify a solid background color. This has the worst possible rendering performance, but the same high blitting performance as LCD
  • Blended (eg TTF_RenderUTF8_Blended) antialias the text and does not require us to specify the background color. The resulting surface can be blitted onto any background color. This has the worst possible rendering and blitting performance.

In this example, we’ll use the highest quality text rendering, Blended.

Therefore, in this case, the function we will use is TTF_RenderUTF8_Blended. This function accepts 3 arguments - the TTF_Font pointer we got when we loaded our font, the text we want to render, and the color we want the text to be:

SDL_Color Color = {20, 20, 20};
TTF_RenderUTF8_Blended(Font, "Hello World!", Color);

This function will return a pointer to a new SDL_Surface, which we can treat like any other surface. Most importantly, we can blit it onto our window surface so we can see the text.

Let's implement this as a public function:

class TextWindow {
// ...
public:
  void SetText(std::string Text) {
    SDL_Color Color = {20, 20, 20};

    // Our function parameter is a std::string, but SDL_ttf
    // requires a C-style string be passed, so we use
    //  std::string's c_str method to get the C-string
    SDL_Surface* TextSurface {
      TTF_RenderUTF8_Blended(Font, Text.c_str(), Color)
    };

    // Check to see if the surface was successfully created
    if (!TextSurface) {
      cout << "Failed to render text: "
           << TTF_GetError() << endl;
    }

    // Fill the window surface with our solid color
    // This ensures any previous text is erased
    FillWindowSurface();

    // Blit our text onto our window surface
    SDL_BlitSurface(
      TextSurface,
      nullptr,
      SDLWindowSurface,
      nullptr
    );

    // Release our text surface as we no longer need it
    SDL_FreeSurface(TextSurface);

    // Send our new frame to the screen
    SDL_UpdateWindowSurface(SDLWindow);
  }
// ...
};

If any of the steps here are unclear, our earlier lessons on SDL windows, images, and surface blitting are likely to be helpful:

Finally, over in our main function, we can call this method, and see our results:

#include <SDL.h>
#include "TextWindow.h"

int main(int argc, char** argv) {
  // ...
  TextWindow Window;
  Window.SetText("Hello world!");
  // ...
}
Image showing Hello world text rendered in the top left of the window

Font / Glyph Caching

Most production-grade applications that require a lot of dynamic text typically include a caching step in their text rendering process.

For example, any time we rasterize a character (a glyph), we can store the resulting raster data in case we need it again later. Raster data is stored in a texture - a bitmap image.

This means as our program runs, it is gradually building up a large texture containing all the glyphs we’ve generated. This is sometimes called a glyph atlas.

Any time we try to rasterize a glyph, we can first check to see if we already have that glyph stored in the atlas, at the required size and color. If we do, we can reuse it and avoid the performance hit.

If we don’t, we can rasterize it, and add the new glyph to the atlas.

An example of an SDL implementation of this is the unofficial extension SDL_FontCache

Positioning Text

We can use the 2nd and 4th arguments of SDL_BlitSurface to position the text within the window.

These two arguments are pointers to SDL_Rect. The first rectangle corresponds to the source surface (ie, the text) and the second rectangle corresponds to the destination (ie, the window surface).

The width and height of any surface or rectangle are available using the w and h fields respectively. Here, we define a rectangle that has the same dimensions as the source surface.

SDL_Rect Source { 0, 0, TextSurface->w, TextSurface->h };

Here, we define a rectangle that starts 70 pixels from the left edge, 40 pixels from the top edge, and has the same width and height as the Source rectangle:

SDL_Rect Destination { 70, 40, Source.w, Source.h };

Using these two rectangles in our call to SDL_BlitSurface allows us to position our text:

SDL_Rect Source { 0, 0, TextSurface->w, TextSurface->h };
SDL_Rect Destination { 70, 40, Source.w, Source.h };
SDL_BlitSurface(
  TextSurface, &Source, SDLWindowSurface, &Destination
);
Image showing our hello world text rendered with its position being controlled

We could extend this further to implement more complex positioning. For example, we could write code that uses the w and h of the text surface, the w and h of the window surface, and some maths to ensure our text is always centered, in the top right, or anywhere else we need it to be

We cover surface blitting in more detail in this lesson:

Complete Code

The final code is available here:

#include <SDL.h>
#include "TextWindow.h"

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);

  TextWindow Window;
  Window.SetText("Hello world!");

  SDL_Event Event;
  while (true) {
    while (SDL_PollEvent(&Event)) {
      if (Event.type == SDL_QUIT) [[unlikely]] {
        SDL_Quit();
        return 0;
      }
    }
  }

  return 0;
}
#pragma once
#include <SDL.h>
#include <SDL_ttf.h>

#include <iostream>
#include <string>

using std::cout, std::endl;

class TextWindow {
 public:
  TextWindow() {
    SDL_Init(SDL_INIT_VIDEO);

    SDLWindow = SDL_CreateWindow(
      "Hello Window",
      SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED,
      600, 150, 0
    );

    SDLWindowSurface = SDL_GetWindowSurface(SDLWindow);
    FillWindowSurface();
    SDL_UpdateWindowSurface(SDLWindow);

    if (TTF_Init() < 0) {
      cout << "Error calling TTF_Init: "
           << TTF_GetError() << endl;
    }

    LoadFont();
  }

  ~TextWindow() {
    TTF_Quit();
    TTF_CloseFont(Font);
  }

  void SetText(std::string Text) {
    SDL_Color Color { 20, 20, 20 };
    SDL_Surface* TextSurface {
      TTF_RenderUTF8_Blended(Font, Text.c_str(), Color)
    };
    if (!TextSurface) {
      cout << "Failed to render text: "
           << TTF_GetError() << endl;
    }

    FillWindowSurface();

    SDL_Rect Source { 0, 0, TextSurface->w, TextSurface->h };
    SDL_Rect Destination { 70, 40, Source.w, Source.h };
    SDL_BlitSurface(
      TextSurface, &Source, SDLWindowSurface, &Destination
    );
    SDL_FreeSurface(TextSurface);
    SDL_UpdateWindowSurface(SDLWindow);
  }

 private:
  void FillWindowSurface() {
    SDL_FillRect(
      SDLWindowSurface,
      nullptr,
      SDL_MapRGB(SDLWindowSurface->format, 240, 240, 240)
    );
  }

  void LoadFont() {
    Font = TTF_OpenFont("Roboto-Medium.ttf", 50);
    if (!Font) {
      cout << "Failed to load font: "
           << TTF_GetError() << endl;
    }
  }

  SDL_Window* SDLWindow{nullptr};
  SDL_Surface* SDLWindowSurface{nullptr};
  TTF_Font* Font;
};

Was this lesson useful?

Next Lesson

Maths and Geometry Primer

Learn the basics of maths and geometry, so we can represent concepts like positions and distances between objects in our worlds.
3D art showing a teacher in a classroom
Ryan McCombe
Ryan McCombe
Posted
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access

This course includes:

  • 27 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Maths and Geometry Primer

Learn the basics of maths and geometry, so we can represent concepts like positions and distances between objects in our worlds.
3D art showing a teacher in a classroom
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved