9
\$\begingroup\$

Can you use any function to render more than a line of text in a surface?

If you can't do this, what whould be the way to go? I am doing the following: Create a surface (not sure if should be a software or a hardware one) and then create as many surfaces as lines my text have, then blitting all the surfaces, and freeing all the surfaces. But this turns to be very slow. This is the code:

void Text::updateSurface() {
    if (surface != NULL) {
        SDL_FreeSurface(surface);
    }

    TTF_Font * font = TTF_OpenFont(fontPath.c_str(),size);
    if (font == NULL) {
        ERROR("Null font"<<TTF_GetError());
    }

    int maxWidth = 0;
    int totalHeight = 0;

    std::vector<SDL_Surface *> surfaces;
    for (size_t i=0; i<lines.size(); i++) {
        SDL_Surface * partialSurface = TTF_RenderText_Solid(font, lines[i].c_str(), color);
        if (partialSurface == NULL) {
            ERROR("surface == NULL: "<<TTF_GetError());
        }
        surfaces.push_back(partialSurface);
        totalHeight += partialSurface->h;
        maxWidth = std::max(maxWidth, partialSurface->w);
    }

    TTF_CloseFont(font);

    surface = SDL::allocateSurface(maxWidth,totalHeight);
    if (backgroundColor != NULL) {
        SDL_FillRect(surface, NULL, SDL_MapRGB(surface->format,backgroundColor->r, backgroundColor->g, backgroundColor->b));
    }
    totalHeight = 0;
    for (size_t i = 0; i < surfaces.size(); i++) {
        SDL_Rect dst = {0, totalHeight, 0, 0};
        switch(aligment) {
        case ALIGMENT_RIGHT:
            dst.x = maxWidth - surfaces[i]->w;
            break;
        case ALIGMENT_CENTER:
            dst.x = (maxWidth - surfaces[i]->w) / 2;
            break;
        case ALIGMENT_LEFT:
        default:
            break;
        }
        totalHeight += surfaces[i]->h;
        SDL_BlitSurface(surfaces[i],NULL,surface,&dst);
        SDL_FreeSurface(surfaces[i]);
    }
}

This is the code for SDL::allocateSurface(...)

static SDL_Surface * allocateSurface(int width, int height, int bpp = -1, Uint32 flags = 0)

SDL_Surface * SDL::allocateSurface(int width, int height, int bpp, Uint32 flags) {
    if (bpp == -1) {
        bpp = SDL::bpp; //SDL_GetVideoInfo()->vfmt->BitsPerPixel;
    }

    if (flags == 0) {
        flags = SDL_HWSURFACE;
    }

    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

    return SDL_CreateRGBSurface(flags, width, height, bpp, rmask, gmask, bmask, amask);
}
\$\endgroup\$
8
  • \$\begingroup\$ Why multiple surfaces? You can render the text all to one single surface. \$\endgroup\$ Commented Dec 26, 2012 at 1:47
  • \$\begingroup\$ How? That question was the first line in my post :S \$\endgroup\$
    – José D.
    Commented Dec 26, 2012 at 2:09
  • \$\begingroup\$ You'll need to get the Size of each line of text via TTF_SizeText(text,*w, *h) then using the information from their, you can create your own line breaks :). \$\endgroup\$
    – dan369
    Commented Dec 26, 2012 at 2:22
  • \$\begingroup\$ Still I need one surface for each line, for the rendering. Am i wrong? \$\endgroup\$
    – José D.
    Commented Dec 26, 2012 at 2:31
  • \$\begingroup\$ Well i'd think you'd need to have 2 surfaces. 1 for your current text line and then another for your result. You'd re-use your textLine surface to loop through each line and then render each line to your result image. But, to make things easier, there is this library you could use;code.bluedinosaurs.com/lib/NFont \$\endgroup\$
    – dan369
    Commented Dec 26, 2012 at 4:59

4 Answers 4

10
\$\begingroup\$

Don't render text directly with SDL_TTF. It's not efficient, at all. Use SDL_TTF to generate a glyph atlas, or use a tool like AngelCode BMFont. You can get glyph metrics (Width, Height, x advance, etc.) using TTF_LineSkip to find the vertical distance, and TTF_GlyphMetrics.

At this point, you can render text yourself. By putting the font glyphs into an atlas, and storing glyph metrics, you can easily render multiple lines, render each glyph in a different style or even font, do various effects, etc. By batching your glyph draws, you can be way more efficient than using SDL_TTF naively. There's no need to copy whole lines into temporary SDL buffers which are copied to your real output.

It's harder, but worth it. Easy toy render APIs are just that: toys.

\$\endgroup\$
3
  • \$\begingroup\$ I've seen this method used before but won't that ruin the Kerning of the font? If you're rendering a large block of text (hence the need for multi-lines) readability would be important. \$\endgroup\$
    – Cramer
    Commented Oct 5, 2013 at 5:08
  • \$\begingroup\$ You can query the kerning information between two glyphs and store that in an additional table. Most fonts in games are designed to be big, bold, and very easy to read at a single glance, and so only need very basic size metrics and no kerning, but it's definitely possible to have it in a pre-cached glyph map. \$\endgroup\$ Commented Oct 5, 2013 at 5:20
  • 5
    \$\begingroup\$ At this point, rendering fonts is not a huge computational task, especially if you are only doing a 2D game and you are basically describing rewriting the font system but use bitmaps rather than vectors. And if you don't need complicated metrics its easier just to pre-render the fonts at compile time \$\endgroup\$
    – CobaltHex
    Commented Feb 25, 2014 at 17:59
13
\$\begingroup\$

There is one more possibility, if performance doesn't matter much, and you are willing to use the newer version SDL2_TTF(available from the official hg.libsdl.org mercurial repository). SDL2_TTF has new functions including TTF_RenderText_Blended_Wrapped, where you can specify a wraplength parameter, which specifies in pixels the maximal width( the surface width also gets padded to this value ). It also supports newline characters, and will break the line at \n characters.

\$\endgroup\$
2
  • \$\begingroup\$ Is it possible to use this function to break on newline characters without breaking on a maximal width? \$\endgroup\$ Commented Feb 16, 2021 at 11:26
  • 1
    \$\begingroup\$ @0liveradam8 I think if you specify wrapLength as 0 there will be no wrapping, so for that you'll just want to use a really big value as the wrapLength. As long as the texture doesn't reach that size it's not going to wrap due to pixel length. I would set that to something like the max texture size the gpu will support if using gpu acceleration, so that it doesn't cause technical issues if it becomes too big. \$\endgroup\$
    – akaltar
    Commented Feb 17, 2021 at 8:56
1
\$\begingroup\$

Here's the exact code I use for generating a multi-line speech bubble. It's not the best, but it's worked for me so far. I use increments of 5 throughout the function since the sprites for the corners and edges of my speech bubbles are 5x5 pixels.

in-game example:

in-game implementation of function below

void speechbubble(SDL_Rect origin, int numlines, std::string quote1, std::string quote2, std::string quote3, SDL_Surface* screen)
{
const char* m_quote1 = quote1.c_str();
const char* m_quote2 = quote2.c_str();
const char* m_quote3 = quote3.c_str();
SDL_Surface *spritesheet = load_image("images/spritesheet.bmp");

SDL_Rect smallclip[8];
SDL_Rect m_origin = {origin.x + 5, origin.y + 5, origin.w - 5, origin.h - 5};

smallclip[0].x=0;smallclip[0].y=224;    //top left corner
smallclip[1].x=5;smallclip[1].y=224;    //top right corner
smallclip[2].x=10;smallclip[2].y=224;   //bot left corner
smallclip[3].x=15;smallclip[3].y=224;   //bot right corner
smallclip[4].x=0;smallclip[4].y=229;    //top surface
smallclip[5].x=5;smallclip[5].y=229;    //bot surface
smallclip[6].x=10;smallclip[6].y=229;   //left surface
smallclip[7].x=15;smallclip[7].y=229;   //right surface

for(int i =0;i<8;i++)
{
    smallclip[i].w = 5;
    smallclip[i].h = 5;
}

Uint32 white = SDL_MapRGB(screen->format,255,255,255);

SDL_FillRect(screen,&m_origin,white);

for(int i = 0; i<((origin.w-1)/5);i++)  //top and bottom surfaces
{
    apply_surface(origin.x+5 + (i*5), origin.y, spritesheet, screen, &smallclip[4]);
    apply_surface(origin.x+5 + (i*5), origin.y+origin.h, spritesheet, screen, &smallclip[5]);
}
apply_surface(origin.x+origin.w-5, origin.y, spritesheet, screen, &smallclip[4]);
apply_surface(origin.x+origin.w-5, origin.y+origin.h, spritesheet, screen, &smallclip[5]);

for(int i = 0; i<((origin.h-1)/5);i++)  //left and right surfaces
{
    apply_surface(origin.x, origin.y+5 + (i*5), spritesheet, screen, &smallclip[6]);
    apply_surface(origin.x+origin.w, origin.y+5 + (i*5), spritesheet, screen, &smallclip[7]);
}
apply_surface(origin.x, origin.y+origin.h-5, spritesheet, screen, &smallclip[6]);
apply_surface(origin.x+origin.w, origin.y+origin.h-5, spritesheet, screen, &smallclip[7]);

apply_surface(origin.x, origin.y,spritesheet,screen,&smallclip[0]);                     //apply the corners
apply_surface(origin.x+origin.w, origin.y,spritesheet,screen,&smallclip[1]);
apply_surface(origin.x, origin.y+origin.h,spritesheet,screen,&smallclip[2]);
apply_surface(origin.x+origin.w, origin.y+origin.h,spritesheet,screen,&smallclip[3]);

SDL_Surface* text_quote1 = NULL;
SDL_Surface* text_quote2 = NULL;
SDL_Surface* text_quote3 = NULL;

TTF_Font *font1 = TTF_OpenFont("fonts/CONTRA__.ttf", 14);
SDL_Color color_black = {0,0,0};
SDL_Color color_white = {255,255,255};

if(font1 != NULL)
{
    text_quote1 = TTF_RenderText_Shaded(font1, m_quote1, color_black, color_white);
    if(m_quote2 != NULL) text_quote2 = TTF_RenderText_Shaded(font1, m_quote2, color_black, color_white);
    if(m_quote3 != NULL) text_quote3 = TTF_RenderText_Shaded(font1, m_quote3, color_black, color_white);
}

apply_surface(origin.x+5,origin.y+5,text_quote1,screen);
if(m_quote2 != NULL) apply_surface(origin.x+5,origin.y+19,text_quote2,screen);
if(m_quote3 != NULL) apply_surface(origin.x+5,origin.y+38,text_quote3,screen);
}
\$\endgroup\$
0
\$\begingroup\$

Try this, instead of TTF_RenderFont use TTF_RenderText_Blended (just my personal preference) then render the text surface line, then blit onto the main surface and free the text. This way you only have one surface every being rendered on screen.

Heres the method:

void printMessageAt(const string& msg, Uint32 x, Uint32 y, TTF_Font *myFont, SDL_Surface *screen) {
    SDL_Rect dest = {x,y,0,0};
    SDL_Color color = {0, 0, 0, 0};

    // This creates the text surface
    SDL_Surface *stext = TTF_RenderText_Blended(myFont, "The line of text to render", color);

    // If it worked, then blit it to the main screen
    // If you wrap this function
    if (stext) {
        SDL_BlitSurface( stext, NULL, screen, &dest );
        // Very important for performance to free the text surface
        SDL_FreeSurface(stext);
    }
    else {
        throw
        string("Couldn't allocate text surface in printMessageAt");
    }
}

You can call this multiple times for multiple lines of text. Hope this helps, it essentially relies on the special use of the SDL_BlitSurface command.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .