Rendering Text with SDL_ttf

Learn to render and manipulate text in SDL2 applications using the official SDL_ttf extension
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
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

In this lesson, we’ll see how we can render text within our programs. We’ll use the official SDL_ttf extension we installed earlier in the course.

We’ll build upon the concepts we introduced in the previous chapters. Our main.cpp looks like below.

The key thing to note is that we have created a Text class, and instantiated an object from it called TextExample. This object is being asked to Render() onto the window surface every frame:

#include <iostream>
#include <SDL.h>
#include "Text.h"

class Window {/*...*/}; int main(int argc, char** argv) { SDL_Init(SDL_INIT_VIDEO); SDL_Event Event; bool shouldQuit{false}; Window GameWindow; Text TextExample{"Hello World"}; while (!shouldQuit) { while (SDL_PollEvent(&Event)) { if (Event.type == SDL_QUIT) { shouldQuit = true; } } GameWindow.Render(); TextExample.Render(GameWindow.GetSurface()); GameWindow.Update(); } SDL_Quit(); return 0; }

Our Text class currently looks like this, but we’ll expand it throughout this lesson:

#pragma once
#include <SDL.h>
#include <SDL_ttf.h>

class Text {
 public:
  Text(std::string Content) {}

  void Render(SDL_Surface* DestinationSurface) {
    // ...
  }
};

We will also need a TrueType (.ttf) file stored in the same location as our executable. The code examples and screenshots in this lesson use Roboto-Medium.ttf, available from Google Fonts.

Initializing and Quitting SDL_ttf

To use SDL_ttf in our application, we #include the SDL_ttf.h header file. We then call TTF_Init() to initialize the library, and TTF_Quit() to close it down:

#include <SDL.h>
#include <SDL_ttf.h>
#include "Text.h"

class Window {/*...*/}; int main(int argc, char** argv) { SDL_Init(SDL_INIT_VIDEO); TTF_Init(); SDL_Event Event; bool shouldQuit{false}; Window GameWindow; Text Example{"Hello World"};
while (!shouldQuit) {/*...*/} TTF_Quit(); SDL_Quit(); return 0; }

Error Checking

TTF_Init() returns 0 if it succeeds, and a negative error code if it fails. We can check for this error state, and call SDL_GetError() for an explanation:

if (TTF_Init() < 0) {
  std::cout << "Error initializing SDL_ttf: "
    << SDL_GetError();
}

Loading and Freeing Fonts

To create text in our application, we first need to load a font. To do this, we call TTF_OpenFont(), passing the path to our font file, and the font size we want to use:

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

This function creates a TTF_Font object and returns a pointer to it. To prevent memory leaks, we pass this pointer to TTF_CloseFont() when we no longer need it.

To simplify memory management, we’ll also prevent our Text objects from being copied by deleting their copy constructor and copy assignment operator. Our updated Text class looks like this:

#pragma once
#include <SDL.h>
#include <SDL_ttf.h>

class Text {
 public:
  Text(std::string Content) : Font {
    TTF_OpenFont("Roboto-Medium.ttf", 50)
  } {

  }

  void Render(SDL_Surface* DestinationSurface) {

  }

  ~Text() {
    TTF_CloseFont(Font);
  }
  Text(const Text&) = delete;
  Text& operator=(const Text&) = delete;

private:
  TTF_Font* Font;
};

TTF_WasInit()

If we call TTF_CloseFont() when SDL_ttf is not initialized, our program can crash. Issues like this are most common during the "cleanup" phase of our application, where we’re trying to shut everything down cleanly.

In our example, the TextExample object’s destructor isn’t called until main() ends. However, before main() ends, we call TTF_Quit(), meaning by the time ~TextExample() calls TTF_CloseFont(), SDL_ttf has already been shut down.

We can test for this scenario using TTF_WasInit(). The TTF_WasInit() function returns a positive integer if SDL_ttf is currently initialized, so we can make our class more robust like this:

// Text.h
class Text {
  // ...

  ~Text() {
    if (TTF_WasInit()) {
      TTF_CloseFont(Font);
    }
  }
  
  // ...
};

Calling TTF_Init() and TTF_Quit() Multiple Times

As an aside, SDL_ttf keeps track of how many times TTF_Init() has been called, and will only shut down when the same number of calls to TTF_Quit() have been made.

This gives us an alternative approach to working with the library. Instead of calling TTF_Init() when our application starts, we can call it every time it is needed - for example, in the constructors of objects that use it.

We then ensure each of those initializations is matched with a TTF_Quit() call, such as in the destructors of those same objects.

The positive integer returned by TTF_WasInit() represents how many times TTF_Init() has been called, reduced by how many times SDL_Quit() has been called. If we manage the lifecycle of all our objects perfectly, the number will be 0 when our application closes.

Error Checking

If TTF_OpenFont() failed, it will return a nullptr. We can check for this, and log out the error for an explanation:

// Text.h
class Text {
 public:
  Text(std::string Content) : Font {
    TTF_OpenFont("Roboto-Medium.ttf", 50)
  } {
    if (!Font) {
      std::cout << "Error loading font: "
        << SDL_GetError();
    }
  }
  
  // ...
};

Rendering Text

From a high level, the process of rendering text is very similar to rendering images:

  • We generate an SDL_Surface containing the pixel data representing our text
  • We blit that surface onto another surface, using functions like SDL_BlitSurface()

There are many options we can use to generate our surface. We’ll explain their differences in the next lesson. For now, we’ll use TTF_RenderUTF8_Blended(). This function requires three arguments:

  • The font we want to use, as an SDL_Font pointer
  • The text we want to render, as a C-style string
  • The color we want to use, as an SDL_Color

For example:

TTF_RenderUTF8_Blended(
  Font,
  "Hello World",
  SDL_Color{255, 255, 255} // White
)

SDL will render our text onto SDL_Surface, containing the pixel data we need to display the text. It will return a pointer to that surface, or a nullptr if the process failed. We can therefore check for errors in the usual way:

SDL_Surface* TextSurface{
  TTF_RenderUTF8_Blended(
    Font, "Hello World", {255, 255, 255}
  )
};

if (!TextSurface) {
  std::cout << "Error creating TextSurface: "
    << SDL_GetError();
}

Let’s add this capability to our class as a private CreateSurface() method. We’ll additionally:

  • Add a TextSurface member to store a pointer to the generated surface
  • Call SDL_FreeSurface() from the destructor to release the surface when we no longer need it.
  • Call SDL_FreeSurface() before we replace the existing TextSurface with a new one. This is not currently necessary as we’re only calling CreateSurface() once per object lifecycle, but we’ll expand our class later.
  • Call CreateSurface() from our constructor to ensure our Text objects are initialized with a TextSurface
// Text.h
class Text {
 public:
  Text(std::string Content) : Font {
    TTF_OpenFont("Roboto-Medium.ttf", 50)
  } {
    if (!Font) {
      std::cout << "Error loading font: "
        << SDL_GetError();
    }
    CreateSurface(Content);
  }

  void Render(SDL_Surface* DestinationSurface) {

  }

  ~Text() {
    SDL_FreeSurface(TextSurface);
    TTF_CloseFont(Font);
  }
  Text(const Text&) = delete;
  Text& operator=(const Text&) = delete;

private:
  void CreateSurface(std::string Content) {
    SDL_Surface* newSurface = TTF_RenderUTF8_Blended(
      Font, Content.c_str(), {255, 255, 255}
    );
    if (newSurface) {
      SDL_FreeSurface(TextSurface);
      TextSurface = newSurface;
    } else {
      std::cout << "Error creating TextSurface: "
        << SDL_GetError() << '\n';
    }
  }

  TTF_Font* Font{nullptr};
  SDL_Surface* TextSurface{nullptr};
};

Surface Blitting

After creating an SDL_Surface containing our rendered text, we can display it on the screen using a process called blitting. Blitting involves copying the pixel data from one surface (our text surface) to another (the window surface).

We covered blitting in much more detail when working with images earlier in the chapter.

In our current setup, the Text::Render() method receives the window surface as a parameter. By blitting our TextSurface onto this window surface, we draw the text onto the window:

// Text.h
class Text {
 public:
  // ...

  void Render(SDL_Surface* DestinationSurface) {
    SDL_BlitSurface(
      TextSurface, nullptr,
      DestinationSurface, nullptr
    );
  }
  
  // ...
};
Screenshot of the program running

Source Rectangle

As with any call to SDL_BlitSurface(), we can pass pointers to the 2nd and 4th arguments. These are pointers to SDL_Rect objects representing the source and destination rectangles respectively.

When working with text, it is fairly unusual that we would specify a source rectangle. The text we generated fills the entire SDL_Surface created by SDL_ttf. However, we can still crop it if we have some reason to:

// Text.h
class Text {
 public:
  // ...

  void Render(SDL_Surface* DestinationSurface) {
    SDL_BlitSurface(
      TextSurface, &SourceRectangle,
      DestinationSurface, nullptr
    );
  }
  
  // ...

private:
  void CreateSurface(std::string Content) {
    SDL_Surface* newSurface = TTF_RenderUTF8_Blended(
      Font, Content.c_str(), {255, 255, 255}
    );
    if (newSurface) {
      SDL_FreeSurface(TextSurface);
      TextSurface = newSurface;
    } else {
      std::cout << "Error creating TextSurface: "
        << SDL_GetError() << '\n';
    }
    
    SourceRectangle.w = TextSurface->w - 50;
    SourceRectangle.h = TextSurface->h - 20;
  }

  TTF_Font* Font{nullptr};
  SDL_Surface* TextSurface{nullptr};
  SDL_Rect SourceRectangle{0, 0, 0, 0};
};
Screenshot of the program running

Destination Rectangle

More commonly, we’ll want to provide a destination rectangle with x and y values, controlling where the text is placed within the destination surface:

// Text.h
class Text {
 public:
  // ...
  void Render(SDL_Surface* DestinationSurface) {
    SDL_BlitSurface(
      TextSurface, nullptr,
      DestinationSurface, &DestinationRectangle
    );
  }

private:
  // ...
  SDL_Rect DestinationRectangle{50, 50, 0, 0}; 
};
Screenshot of the program running

As we covered in our image rendering lessons, SDL_BlitSurface() will update the w and h values of this rectangle with the size of our output within the destination surface.

If these values are smaller than the width and height of our source surface (or source rectangle, if we’re using one) that indicates that our text did not fit within the destination surface at the location we specified:

// Text.h
class Text {
 public:
  Text(std::string Content) : Font {
    TTF_OpenFont("Roboto-Medium.ttf", 150)
  } {
    if (!Font) {
      std::cout << "Error loading font: "
        << SDL_GetError();
    }
    CreateSurface(Content);
  }

  void Render(SDL_Surface* DestinationSurface) {
    SDL_BlitSurface(
      TextSurface, nullptr,
      DestinationSurface, &DestinationRectangle
    );
    if (DestinationRectangle.w < TextSurface->w) {
      std::cout << "Clipped horizontally\n";
    }
    if (DestinationRectangle.h < TextSurface->h) {
      std::cout << "Clipped vertically\n";
    }
  }

  // ...
};
Clipped horizontally
Clipped horizontally
Clipped horizontally
...
Screenshot of the program running

Scaling Text

As with any surface, we can add scaling to the blitting process using SDL_BlitScaled(). However, we should avoid doing this where possible. Scaling an image causes a loss of quality, and this can be particularly noticeable with text.

Let’s temporarily update Render() and CreateSurface() to increase the size of our text using scaled blitting, and note the quality loss:

// Text.h
class Text {
 public:
  Text(std::string Content) : Font {
    TTF_OpenFont("Roboto-Medium.ttf", 25)
  } {
    if (!Font) {
      std::cout << "Error loading font: "
        << SDL_GetError();
    }
    CreateSurface(Content);
  }

  void Render(SDL_Surface* DestinationSurface) {
    SDL_BlitScaled(
      TextSurface, nullptr,
      DestinationSurface, &DestinationRectangle
    );
  }

  ~Text() {
    SDL_FreeSurface(TextSurface);
    if (TTF_WasInit()) {
      TTF_CloseFont(Font);
    }
  }
  Text(const Text&) = delete;
  Text& operator=(const Text&) = delete;

private:
  void CreateSurface(std::string Content) {
    SDL_Surface* newSurface = TTF_RenderUTF8_Blended(
      Font, Content.c_str(), {255, 255, 255}
    );
    if (newSurface) {
      SDL_FreeSurface(TextSurface);
      TextSurface = newSurface;
    } else {
      std::cout << "Error creating TextSurface: "
        << SDL_GetError() << '\n';
    }
    
    DestinationRectangle.w = TextSurface->w * 4;
    DestinationRectangle.h = TextSurface->h * 4;
  }

  TTF_Font* Font{nullptr};
  SDL_Surface* TextSurface{nullptr};
  SDL_Rect DestinationRectangle{50, 50, 0, 0};
};
Screenshot of the program running

Instead, we can just render the text at the correct size we want, by setting the font size. Our constructor is currently setting the font size to 20. Let’s change that to be a constructor argument:

// Text.h
class Text {
 public:
  Text(std::string Content, int Size = 25)
  : Font {
    TTF_OpenFont("Roboto-Medium.ttf", Size)
  } {
    if (!Font) {
      std::cout << "Error loading font: "
        << SDL_GetError();
    }
    CreateSurface(Content);
  }
  // ...
};

The TTF_SetFontSize() function also lets us change the size of a font we’ve already loaded. Let’s use this in a new SetFontSize() method, allowing consumers to change the font size without needing to create an entirely new object:

// Text.h
class Text {
 public:
  // ...
  void SetFontSize(int NewSize) {
    TTF_SetFontSize(Font, NewSize);
  }
  // ...
};

Let’s increase our font size, and notice the quality improvement over using SDL_BlitScaled():

// main.cpp

class Window {/*...*/}; int main(int argc, char** argv) { SDL_Init(SDL_INIT_VIDEO); TTF_Init(); SDL_Event Event; bool shouldQuit{false}; Window GameWindow; Text TextExample{"Hello World", 100};
while (!shouldQuit) {/*...*/} TTF_Quit(); SDL_Quit(); return 0; }
Screenshot of the program running

We cover more techniques related to scaling in the next lesson.

Rasters and Vectors

Font files, such as the .ttf format, store their characters in a vector format. These formats often involve a set of instructions on how to draw the lines and shapes of each character. A font’s representation of a character is often called a glyph.

However, to store text in an SDL_Surface, and ultimately show it on our screen, the data needs to be in a raster format. This is the image format we’re likely most familiar with - grids of small, rectangular pixels.

The process of converting vectors and other arbitrary data types to raster images is called rasterization.

The main advantage of a vector format is that it allows any algorithm that understands the format, such as TTF_RenderUTF8_Blended(), to generate raster data for a specific character by executing the instructions provided by the font.

And, like calling a function, that algorithm can include parameters such as the size we want the raster to be. This allows a font (or any other vector graphic) to be rendered at any size without losing quality.

Complete Code

Complete versions of the files we created in this lesson are available below:

#include <iostream>
#include <SDL.h>
#include <SDL_ttf.h>
#include "Text.h"

class Window {
public:
  Window() {
    SDLWindow = SDL_CreateWindow(
      "My Program", SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED, 600, 300, 0);
  }

  void Render() {
    SDL_FillRect(
      GetSurface(), nullptr, SDL_MapRGB(
        GetSurface()->format, 50, 50, 50
      )
    );
  }

  void Update() {
    SDL_UpdateWindowSurface(SDLWindow);
  }

  SDL_Surface* GetSurface() {
    return SDL_GetWindowSurface(SDLWindow);
  }

  ~Window() { SDL_DestroyWindow(SDLWindow); }
  Window(const Window&) = delete;
  Window& operator=(const Window&) = delete;

private:
  SDL_Window* SDLWindow;
};

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  TTF_Init();
  SDL_Event Event;
  bool shouldQuit{false};

  Window GameWindow;
  Text TextExample{"Hello World", 100};

  while (!shouldQuit) {
    while (SDL_PollEvent(&Event)) {
      if (Event.type == SDL_QUIT) {
        shouldQuit = true;
      }
    }
    GameWindow.Render();
    TextExample.Render(GameWindow.GetSurface());
    GameWindow.Update();
  }

  TTF_Quit();
  SDL_Quit();
  return 0;
}
#pragma once
#include <SDL.h>
#include <SDL_ttf.h>

class Text {
 public:
  Text(std::string Content, int Size = 25)
  : Font {
    TTF_OpenFont("Roboto-Medium.ttf", Size)
  } {
    if (!Font) {
      std::cout << "Error loading font: "
        << SDL_GetError();
    }
    CreateSurface(Content);
  }

  void SetFontSize(int NewSize) {
    TTF_SetFontSize(Font, NewSize);
  }

  void Render(SDL_Surface* DestinationSurface) {
    SDL_BlitSurface(
      TextSurface, nullptr,
      DestinationSurface, &DestinationRectangle
    );
  }

  ~Text() {
    SDL_FreeSurface(TextSurface);
    if (TTF_WasInit()) {
      TTF_CloseFont(Font);
    }
  }
  Text(const Text&) = delete;
  Text& operator=(const Text&) = delete;

private:
  void CreateSurface(std::string Content) {
    SDL_Surface* newSurface = TTF_RenderUTF8_Blended(
      Font, Content.c_str(), {255, 255, 255}
    );
    if (newSurface) {
      SDL_FreeSurface(TextSurface);
      TextSurface = newSurface;
    } else {
      std::cout << "Error creating TextSurface: "
        << SDL_GetError() << '\n';
    }
  }

  TTF_Font* Font{nullptr};
  SDL_Surface* TextSurface{nullptr};
  SDL_Rect DestinationRectangle{0, 0, 0, 0};
};

Summary

In this lesson, we've explored the fundamentals of rendering text in SDL2 using the SDL_ttf extension. We've covered:

  • Initializing and quitting SDL_ttf
  • Loading and freeing fonts
  • Creating text surfaces
  • Rendering text to the screen
  • Handling potential errors in text rendering
  • Basic text positioning and scaling

In the next lesson, we’ll expand on these concepts further, covering more advanced use cases.

Was this lesson useful?

Next Lesson

Text Performance, Fitting and Wrapping

Explore advanced techniques for optimizing and controlling text rendering when using SDL_ttf
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
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
Rendering Images and Text
  • 53.GPUs and Rasterization
  • 54.SDL Renderers
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:

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

Text Performance, Fitting and Wrapping

Explore advanced techniques for optimizing and controlling text rendering when using SDL_ttf
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved