formatting a string in java2d, adding new lines

Hello.
Characters in my 2d game can say stuff and then text appears above their heads, like in a comic. So far I’ve been only able to figure out how to center text, but not how to divide it into multiple lines if text is too long. I’m using Java2d, drawString(). So I guess I could count the words, calculate their width and then do multiple drawString() since “\n” dosen’t work in Java2d, but I guess this has all been automized somewhere. Also it would be nice if I can get it justified. Can you show me where to look?
Thank you.

Take a look at LineBreakMeasurer - http://java.sun.com/javase/6/docs/api/java/awt/font/LineBreakMeasurer.html

Thank you, it is just what I needed.
API is very functional but that makes it complex also, much to be done for a simple thing such as breaking string for writing across multiple lines.

Anyway I figured out most of it, but still what I wrote doesn’t work as expected. Here’s the code. I want to start showing text 100 pix left of player and then warp it into new line after 100 pix right of player. The left border works just fine, but I don’t understand what is wrong with the right one, it depends where player is!? When I move player left (towards left side of screen) then warping border shirinks, comes closer to player, shrinkin the text, and when I move player to right then it expands, goes away from player thus streatching the text. Please someone tell me what I’m missing


...
// getPosX(), getPosY() return (x,y) coordinates of Player, RADIUS is radius of visible part of image of player (player image resembles to a circle)
...
            AttributedString text = new AttributedString(say);
            AttributedCharacterIterator char_iterator= text.getIterator();
            LineBreakMeasurer measurer = new LineBreakMeasurer(char_iterator, g2d.getFontRenderContext());
            
            Point2D.Float pen = new Point2D.Float(getPosX()-100, getPosY()-RADIUS-g2d.getFontMetrics().getHeight());
            float wrappingWidth = getPosX()+100;
            
            while(measurer.getPosition() < char_iterator.getEndIndex()) {
                TextLayout layout = measurer.nextLayout(wrappingWidth);
                pen.y += layout.getAscent();

                layout.draw(g2d, pen.x, pen.y);
                pen.y += layout.getDescent() + layout.getLeading();
            }

EDIT: sorry everyone, stupid me, I figured it out … I thought that nextLayout() takes x coordinate as argument, didn’t noticed it was actually width, even if variable was clearly named to imply that :slight_smile:
Now I’m working to get the text centered… any ideas? :slight_smile:

EDIT2: got it, for centered text use this while in loop for drawing:


...
pen.x += (wrappingWidth - layout.getBounds().getWidth()) / 2;
...

ok I’ve got a problem with this… I can’t figure out how to get the text printed always above my player. Like if there is one line of text then print it right above him, if there are more then print the last line right above him. I should move pen.y up but I can’t know how much lines of text there will be at the end. LineBreakMeasurer class doesn’t seem to have any indication about that. One thing I can do is create a measurer object, iterate over it just for counting lines, then recreate measurer object and iterate for drawing them… but this is ultra stupid. There’s got to be a way how to do this normally, anyone? Thank you.

LineBreakMeasurer can give you a TextLayout object, which can give you all sorts of nifty information, including the text’s bounding box. Collect all of the TextLayouts prior to rendering them, then do your y-coordinate subtraction as appropriate.

I would need to store layouts in array, and either way I must first finish iteration over LineBreakMeasurer… what you suggest is as good as what I’m doing now, iterating over it and count the lines, then recreate LineBreakMeasurer and iterate again to display the text.
Shame that there isn’t something like “countLayouts(width)” to see how much layouts would you get.

So did you get this all figured out, if so can I see your finished code. I did the same thing you are doing or did, and it seems I over-complicated the matter a lot if I am reading this right… Ill go find it and post what I did.

well don’t know exactly what do you need, but this draws text above some 2d point, and breaks it into multiple lines if length exceeds TEXT_AREA_WIDTH value… like in a comic when characters talk.
There are 2 problems I haven’t studied yet: First one is that you need to know how much lines you have in order to move text up (normally it’s written downwards) so, as I said, I iterate over LineBreakMeasurer twice, first is for counting the lines and second is for drawing the text. For now this seems to be the only way to do this. Second problem is that in first usage it stalls my simple game, noticeable glitch ~50ms, after that all fine, so you need to warm it up. Call it once when stall isn’t noticeable before you start using it. If someone could explain this to me I would appreciate it :slight_smile:


    /** draws text above player, if is too wide splits it into multiple
     * lines, last line is always same amount of pixels above player
     * @param g2d - graphics to draw to
     * @see it seems this has something heavy to create and requires
     * one warmup before it is used, or it will stall the game ~50ms
     * first time it is used
     * @author Kova
     */
    void drawWhatIsSaid(Graphics2D g2d) {
        if (System.nanoTime() < say_time) {
            final int TEXT_AREA_WIDTH = 200;
            final boolean TEXT_CENTERED = true;
            
            AttributedString text = new AttributedString(say);
            AttributedCharacterIterator char_iterator= text.getIterator();
            LineBreakMeasurer measurer = new LineBreakMeasurer(char_iterator, g2d.getFontRenderContext());
            Point2D.Float pen = new Point2D.Float(getPosX()-TEXT_AREA_WIDTH/2, getPosY()-RADIUS-g2d.getFontMetrics().getHeight());
            
            int i=0; // TODO change this stupid counting of lines and do it the right way
            while(measurer.getPosition() < char_iterator.getEndIndex()) {
                measurer.nextLayout(TEXT_AREA_WIDTH);
                i++;
            }
            measurer = new LineBreakMeasurer(char_iterator, g2d.getFontRenderContext());
            while(measurer.getPosition() < char_iterator.getEndIndex()) {
                pen.x = getPosX()-TEXT_AREA_WIDTH/2;
                pen.y = getPosY()-RADIUS-g2d.getFontMetrics().getHeight();                
                TextLayout layout = measurer.nextLayout(TEXT_AREA_WIDTH);
                pen.y -= --i*layout.getBounds().getHeight();
                pen.y += layout.getAscent();
                if (TEXT_CENTERED)
                    pen.x += (TEXT_AREA_WIDTH - layout.getBounds().getWidth()) / 2;
                layout.draw(g2d, pen.x, pen.y);
                pen.y += layout.getDescent() + layout.getLeading();
            }
    
        }
    }

Hrm thats way different than what I did. Gotta swithc partitins twice just to get it so I havn’t bothered yet. I supose I can do it right now. Strange glichy things though.

here is mine which works 100% of the time and is not very re-usable. I should re-write it someday so its more flexible.

import java.awt.*;
import java.util.Iterator;
import java.awt.image.BufferedImage;
 
/**
    AlertWindow is the window that pops up when you pick up a new Animal.
*/
public class AlertWindow {
    public static final int PORTRAIT_X_OFFSET = 30;
    public static final int PORTRAIT_Y_OFFSET = 22;
 
    public static final int PORTRAIT_STRING_X_OFFSET = 74;
    public static final int NORMAL_STRING_X_OFFSET = 30;
 
    public static final int START_STRING_Y_OFFSET = 25;
 
    public static final int STRING_OFFSET_BETWEEN_LINES = 15;
    public static final int FONT_SIZE = 12;
    public static final int BOX_IMAGE_TRANSPARENCY_OFFSET = 27;
 
    private static String text;
    private static BufferedImage portrait;
    private static BufferedImage image;
    public static boolean drawing;
 
    static int x, y, boxWidth;
 
 
    /**
        This sets the background box image, in case I ever want to change it
 
        @param i BufferedImage for background box
    */ 
 
    public static void setImage(BufferedImage i)   {
        image = i;
        x = (RunnerApp.WIDTH / 2) - (getWidth() / 2);
        y = (RunnerApp.HEIGHT / 2) - (getHeight() / 2);
        boxWidth = (RunnerApp.WIDTH / 2) + (getWidth() / 2)
            - BOX_IMAGE_TRANSPARENCY_OFFSET;
    }
 
    /**
        Gets the current background box image
 
        @return BufferedImage returns box image
    */
    public static BufferedImage getImage()  {
        return image;
    }
 
    /**
        Sets the animal portrait of the window
 
        @param np BufferedImage to reprent animal
    */
    public static void setPortrait(BufferedImage np)   {
        portrait = np;
    }
 
    /**
        Portrait of animal
 
        @return BufferedImage Portrait of animal
    */
    public static BufferedImage getPortrait()  {
        return portrait;
    }
 
    /**
        Returns what the animal says
 
        @return String animal quote
    */
    public static String getText()    {
        return text;
    }
 
    /**
        Sets what the animal says.
 
        @param t String for what the animal will say
    */
    public static void setText(String t)   {
        text = t;
    }
 
    /**
        Returns the height of the window
 
        @return Height of window
    */
    public static int getHeight()  {
        if (image == null)   {
            return 0;
        }   else    {
            return image.getHeight();
        }
    }
 
    /**
        Returns the width of the window
 
        @return Width of window
    */
    public static int getWidth()  {
        if (image == null)   {
            return 0;
        }   else    {
            return image.getWidth();
        }
    }
 
    /**
        Draw the window IF drawing is true
 
        @param g Graphics area to draw in
    */
    public static void draw(Graphics g)    {
        if (!drawing)   return;
 
        g.drawImage(image, x, y, null);
        g.drawImage(portrait, x + PORTRAIT_X_OFFSET, 
                    y + PORTRAIT_Y_OFFSET, null);
 
        Font font = new Font("Helvetica Neue", Font.PLAIN, FONT_SIZE);
        g.setFont(font);
        g.setColor(Color.WHITE);
        FontMetrics metrics = g.getFontMetrics(font);
 
        int height = metrics.getHeight();
        int stringX, stringY;
        stringX = x + PORTRAIT_STRING_X_OFFSET;
        stringY = y + START_STRING_Y_OFFSET;
 
        int lastWhitespace = 0;
        int lastNewlineMarker = 0;
 
        for (int i = 0; i < getText().length(); i++)  {
            String string = getText().substring(lastNewlineMarker, i);
            if (getText().charAt(i) == ' ')   {
                lastWhitespace = i;
            }
            if (x + PORTRAIT_STRING_X_OFFSET 
                  + metrics.stringWidth(string) >= boxWidth)   {
                    String s = getText().substring(lastNewlineMarker,
                                                    lastWhitespace);
                    lastNewlineMarker = lastWhitespace;
                    g.drawString(s, stringX, stringY);
 
                    stringY += STRING_OFFSET_BETWEEN_LINES;
 
            }
            if (i == getText().length() - 1)   {
                g.drawString(getText().substring(lastNewlineMarker, 
                    getText().length()), stringX, stringY);
            }
        }
    }
}

And a link to the much more readable rafb paste.

http://rafb.net/p/9n4nH073.html

That code is very specific, a great feature is that my artist decided to leave random bits of transparency around the talky box window thing. So drawing it looked werid. And I was too lazy to trim it so I just added an offset. Thats the last one in the list.

Otherwise I think its fairly straightforward.