AWT GUI Facade (7): Draw text

Graphic libraries usually provide methods to draw text on the screen. These handy methods are often quite slow to run because they recompute many parameters at each call. To save computational time, the flyweight pattern can be used to provide text parameters with low memory and cpu usage.

This post is part of the AWT GUI Facade series

Facade design

As usual, I start by adding new methods to the GUI Facade. There are many possibilities, here is an example that allows the drawing of messages of any size and color (NB: I only show new methods, I assume that the ones from the previous posts are still there):

  • The setColor() method sets the text color. It uses java.awt.Color to define the colors. I could create my own Color class, but it won’t lead to significant changes and java.awt.Color is always in the Java standard library;
  • The setTextSize() method sets the text height in pixels;
  • The getTextMetrics() method computes the bounding box of a text to print. It also uses a class from AWT, namely java.awt.Dimension;
  • The drawText() method draws text at coordinates (x,y). It also clips the text if it is too large. The clipping rectangle is defined by (x,y) and (width,height).

AWT Implementation

Implementing this facade with AWT is easy for setColor(), getTextMetrics() and drawText(). This is only a wrapping of existing methods:

@Override
public void setColor(Color color) {
    if (graphics == null)
        return;
    graphics.setColor(color);
}

@Override
public Dimension getTextMetrics(String text) {
    if (graphics == null)
        return new Dimension(0,0);
    FontMetrics fm = graphics.getFontMetrics();
    int textWidth = fm.stringWidth(text);
    int textHeight = fm.getHeight();
    return new Dimension(textWidth,textHeight);
}

@Override
public void drawText(String text, int x, int y, int width, int height) {
    if (graphics == null)
        return;
    FontMetrics fm = graphics.getFontMetrics();
    graphics.clipRect(x, y, width, height);
    graphics.drawString(text, x, y+fm.getAscent());
    graphics.setClip(null);
}

For the setTextSize() method, there is no existing method in AWT. A solution is to search for the font that has the required text height in pixels:

public void setTextSize(int size) {
    if (graphics == null)
        return;
    for (int i=2*size;i>=4;i--) {
        Font font = new Font("Arial",Font.PLAIN,i);
        graphics.setFont(font);
        FontMetrics fm = graphics.getFontMetrics();
        if (fm.getHeight() < size) {
            break;
        }
    }
}

This method is time-consuming and can be called many times at each frame rendering. Multiplied by the number of frames per second (usually 60), this can lead to a lot of unuseful computations since most of these calls will return the same values!

A first approach consists of manually caching the parameters (in this example the AWT font instance). This saves computation time but requires additional work for the user and the implementer of the facade.

A more efficient approach is based on the Flyweight pattern.

Flyweight Pattern

The Flyweight pattern allows you to get instances of a particular interface (or a parent class):

The Flyweight interface defines the type of object managed by the pattern. The FlyweightFactory class provides instances that implement this interface. This is similar to the Factory Method pattern, however, there is a fundamental difference: the objects returned by the pattern are not editable. No element in their interface should allow modifying their contents, or if it is the case, it is necessary to inform the user that the objects returned cannot be modified. This property is used to memorize all the objects already created, and to return the memorized objects when the parameters are the same. For example, if the following code is executed:

FlyweightFactory factory = new FlyweightFactory();
Flyweight item1 = factory.getFlyweight("Apple");
Flyweight item2 = factory.getFlyweight("Apple");

The first call to getFlyweight() creates a new instance that implements Flyweight with the “Apple” setting. On the second call, however, no object is created, and the same instance is returned. In the end, the variables item1 and item2 are identical and refer to the same object. If we ask a large number of times for objects with the same parameters, few objects are made, even if we have the illusion of having manufactured many objects.

Cache AWT Fonts

To use the Flyweight pattern to the AWT Fonts, I define a new class AWTFonts:

The getFont() of AWTFonts returns an AWT font with the required text height in pixels:

public Font getFont(Graphics graphics,int size) {
    Font font = fonts.get(size);
    if (font == null) {
        Font oldFont = graphics.getFont();
        for (int i=2*size;i>=4;i--) {
            font = new Font("Arial",Font.PLAIN,i);
            graphics.setFont(font);
            FontMetrics fm = graphics.getFontMetrics();
            if (fm.getHeight() < size) {
                break;
            }
        }
        fonts.put(size,font);
        graphics.setFont(oldFont);
    }
    return font;
}

The method starts by watching if the associative array contains a font of the required size (l. 2): if it is the case, there is nothing to do, the method returns the found font. In the opposite case, the method searches the font (l. 4-12), memorizes it in the associative array (l. 13), and then sends it back. The oldFont variable (l.4 & 14) only preserves the current font setting in graphics.

To benefit from this feature, I just create a single instance of AWFonts in the GUI Facade class, and use it in the setFontSize() method:

public void setTextSize(int size) {
    if (graphics == null)
        return;
    graphics.setFont(fonts.getFont(graphics,size));
}

Implementation with low-level graphic libraries

With low-level graphic libraries, like OpenGL, methods like setFont() or drawText() are not available. However, the Flyweight pattern is still relevant to these cases.

The common approach is these cases is to create a texture where you draw characters from a font. The main difficulty is that you can’t draw all the UTF-8 characters, you have to find an “on-demand” solution. The Flyweight pattern perfectly suits this problematic since objects are created when the user is asking for them. More specifically, the user asks for the properties of a character (like its location in the texture), for instance using a method “CharProperties getCharProperties(Char c, int fontSize)”.

Display the current day and gold count

To illustrate the use of these new facade capabilities, I update the render() method in the Game class:

public void render() {
    if (gui.beginPaint()) {
        gui.drawLayer(backgroundLayer);
        gui.drawLayer(groundLayer);

        gui.setColor(Color.white);
        gui.setTextSize(32);

        gui.drawText("Day 1", 2, 2, windowWidth-2, windowHeight-2);

        String text = "1000 Gold";
        Dimension dimension = gui.getTextMetrics(text);
        gui.drawText(text, windowWidth - dimension.width - 2, 2, dimension.width, dimension.height);

        gui.endPaint();
    }
}
  • The lines 6 and 7 set the color and the size of the text.
  • Line 9 draws a text in the top left corner (x=2,y=2), with a clipping box as large as the display.
  • Lines 11-13 draw a text in the top right corner. To compute the x coordinate, we need the width of the text to draw (l. 12). Then, the text is drawn (l. 13).

This lead to the following display:

The code of this post can be downloaded here:

To compile: javac com/learngameprog/awtfacade07/Main.java
To run: java com.learngameprog.awtfacade07.Main

Next post in this series

This entry was posted in Tutorial and tagged , , , . Bookmark the permalink.

Leave a Reply