My most inefficient way of implementing running texts. How to optimize?

This is the “running text” that I’m talking about:

I’m inefficiently implementing that running text for a few hours, and I am starting to get discouraged of refactoring the code. I bloated it up, and I know I made it completely unreadable.

When I think of the running text, I came up with using just 4 variables in the beginning. 1 that remembers the beginning of a sentence, and the other 2 remembers the end of each line, where the dialog has 2 lines of sentences that can be used. For the text drawing, I need to use a Graphics object obtained from BufferStrategy, in order to draw the custom fonts that I have packaged into my JAR file.

For example, “Happening” and “tomorrow” are two words in one sentence.

Using lots of if…else conditional checks, I managed to have it line wrap if it can’t fit in one line. But just that feature alone made me implement a lot of variables. So, I thought there might be a better way of efficiently implementing this.

'Twas just 4 variables, now it’s like 10 of them. And I feel as if I’m doing something that I don’t know how to explain.

Here’s the code, using Pastebin: http://pastebin.java-gaming.org/acdd63b9b8c

How do you optimize? And if possible, can anyone provide tips on efficient ways of implementing the running text?

Thanks in advance.

Optimizing is unnecessary. Drawing a few lines of text, no matter how, has practically zero effect on performance.

???

Does that mean the variables I decalred and used are okay, as long as the functionality works as intended? Soesn’t that makes the code bloated too much?

It seems to me that your task is the job for java.awt.font.LineBreakMeasurer.

I did not take a good look at your code, but I see no reason why you would need so much state(class variables). So you probably can optimize your code in a way, so that the logic is more clear and easier to understand. Wouldn’t you only need just one state variable? Namely the current char of the sentence you are drawing?

Seriously, “allocating” (likely on the stack) local variables and such is nearly the cheapest operation one can do. The drawString() calls and deriveFont() will dwarf the running time of anything else there. Don’t worry about it.

I just wrote this. Give it a try. It’s small and works well.

public static void printText() {
		final String message = "Hello there";
		
		new Thread(new Runnable() {

			String printedMessage;
			
			@Override
			public void run() {
				for (int i = 0; i < message.length() + 1; i++) {
					
					printedMessage = message.substring(0, i);
					
					//print it
					System.out.println(printedMessage);
					
					try {
						Thread.sleep(200);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
			
		}).start();
	}

Sleeping isn’t really an optimal solution for creating the typing effect. See this thread: http://www.java-gaming.org/topics/would-there-be-any-thread-sleep-problems/28774/msg/262512/view.html#msg262512

Maybe it will help you:

package com.company;

import javax.swing.*;

public class Main
{

    public static void main( String[] args )
    {
        JFrame frame = new TextLayoutLineBreakerMeasurer();
        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

        frame.setSize( 200, 200 );
        frame.setVisible( true );
    }
}

package com.company;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;

public class TextLayoutLineBreakerMeasurer extends JFrame
{
    String m = "HAPPENING TOMORROW!!";

    int i;

    final javax.swing.Timer t;

    public TextLayoutLineBreakerMeasurer() throws HeadlessException
    {
        t = new javax.swing.Timer( 200,
                new ActionListener()
                {
                    public void actionPerformed( ActionEvent e )
                    {
                        if( ++i == m.length() )
                        {
                            t.stop();
                        }
                        repaint();
                    }
                }
        );
        t.start();
    }

    public void paint( Graphics g )
    {
        String s = m.substring( 0, i );
        if( s.isEmpty() )
        {
            return;
        }
        Graphics2D graphics2D = ( Graphics2D ) g;
        GraphicsEnvironment.getLocalGraphicsEnvironment();
        Font font = new Font( "Arial", Font.PLAIN, 24 );
        AttributedString messageAS = new AttributedString( s );
        messageAS.addAttribute( TextAttribute.FONT, font );
        AttributedCharacterIterator messageIterator = messageAS.getIterator();
        FontRenderContext messageFRC = graphics2D.getFontRenderContext();
        LineBreakMeasurer messageLBM = new LineBreakMeasurer( messageIterator,
                messageFRC );

        Insets insets = getInsets();
        float wrappingWidth = getSize().width - insets.left - insets.right;
        float x = insets.left;
        float y = insets.top;

        while( messageLBM.getPosition() < messageIterator.getEndIndex() )
        {
            TextLayout textLayout = messageLBM.nextLayout( wrappingWidth );
            y += textLayout.getAscent();
            textLayout.draw( graphics2D, x, y );
            y += textLayout.getDescent() + textLayout.getLeading();
            x = insets.left;
        }
    }
}

Thanks. I see a lot of unfamiliar classes being used, but the font setup and attributes portion looks easy to understand.

What is Inset? What does it do?

Insets

Here is another example.
It uses double buffering.


import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.awt.Font;
import java.util.ArrayList;

public class Text extends JPanel {
    static final int CHAR_WIDTH = 7;
    static final int CHAR_HEIGHT = 14;
    
    public static void main(String[] args) {
        JFrame frame = new JFrame();
        Text text = new Text();
        frame.add(text);
        text.setPreferredSize(new Dimension(800, 600));
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
        text.runningText("This is a running text. Words that don't fit completely on a line will automatically be moved to the next line.", 30, 30, 15, 100);
    }
    
    private void runningText(String str, int x, int y, int maxWidth, int delay) {
        new Thread() {
            @Override
            public void run() {
                String[] lines = toLines(str, maxWidth);
                int maxLineLength = maxLength(lines);
                
                Graphics2D g = image.createGraphics();
                
                g.setFont(new Font("monospaced", Font.PLAIN, 12));
                g.setColor(Color.BLACK);
                g.drawRect(x-5, y-5, maxWidth*CHAR_WIDTH + 10, lines.length*CHAR_HEIGHT + 10);
                
                for(int j = 1; j < str.length(); j++) {
                    g.setColor(Color.WHITE);
                    g.fillRect(x, y, maxLineLength*CHAR_WIDTH, lines.length*CHAR_HEIGHT);
                    g.setColor(Color.BLACK);
                    int count = 0;
                    loop:
                    for(int i = 0; i < lines.length; i++) {
                        for(int k = 0; k < lines[i].length(); k++) {
                            g.drawString(lines[i].substring(k, k+1), x+k*CHAR_WIDTH, y+(i+1)*CHAR_HEIGHT);
                            count++;
                            if(count == j) break loop;
                        }
                    }
                    repaint();
                    try {
                        Thread.sleep(delay);
                    } catch(Exception e) {
                    }
                }
                
                g.dispose();
            }
        }.start();
    }
    
    BufferedImage image = null;
    
    public Text() {
        initImage();
    }
    
    public void initImage() {
        int w = getWidth();
        int h = getHeight();
        if(w <= 0 || h <= 0) {
            w = 800;
            h = 600;
        }
        image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = image.createGraphics();
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, w, h);
        g.dispose();
    }
    
    @Override
    public void setPreferredSize(Dimension d) {
        super.setPreferredSize(d);
        initImage();
    }
    
    static String[] toLines(String str, int maxLength) {
        ArrayList<String> lines = new ArrayList<>();
        String[] words = str.split("\\s");
        String line = "";
        int length = 0;
        for(String word: words) {
            if(length+word.length()+1 > maxLength) {
                lines.add(line);
                line = "";
                length = 0;
            }
            if(length > 0) {
                line += " ";
                length += 1;
            }
            line += word;
            length += word.length();
        }
        if(line.length() > 0) lines.add(line);
        return lines.toArray(new String[lines.size()]);
    }
    
    static int maxLength(String[] lines) {
        int length = 0;
        for(String line: lines) length = Math.max(length, line.length());
        return length;
    }

    @Override
    public void paint(Graphics _g) {
        Graphics2D g = (Graphics2D)_g;
        g.scale(3,3);
        g.drawImage(image, 0, 0, null);
    }
}

If we deal with Swing it is enough to add JPanel.setDoubleBuffered( true ).

I’ve been trying to find a way how to line wrap if there exists a word that is MAX_STRING_LENGTH long, where MAX_STRING_LENGTH = 18 (defined by how many letters the dialogue box can fit in).

	private String[] toLines(String all) {
		ArrayList<String> lines = new ArrayList<>();
		String[] words = all.split("\\s");
		String line = "";
		int length = 0;
		for (String w : words) {
			if (w.length() >= MAX_STRING_LENGTH) {
				line += w;
				length += w.length();
				lines.add(line);
			}
			else if (length + w.length() + 1 > MAX_STRING_LENGTH) {
				lines.add(line);
				line = "";
				length = 0;
			}
			if (length > 0) {
				line += " ";
				length += 1;
			}
			line += w;
			length += w.length();
		}
		if (line.length() > 0) lines.add(line);
		return lines.toArray(new String[lines.size()]);
	}

The problem that I’m having with words of length 18 or more is the way line wrapping works in the dialogue boxes.

Look at where the red box is boxing around. You can spot the letter “a” which is the first letter of the third word, “abcdefghijklmnopqr abcdefghijklmnopqr abcdefghijklmnopqr”.

Couldn’t find the right way to make it stop adding text to the already-36-characters-long “line” variable (not the “lines” ArrayList variable.).

I think you should check max height of the text that can be inserted in the window.

Also, IIRC, if the word > MAX_STRING_LENGTH it should be split on 2 parts:
first part =< MAX_STRING_LENGTH, the remainder is moved on the next line.

Do you really intend on using words of 18 letters or more? It’s nice to have a complete solution for these kinds of edge cases, but it may have little to no practical impact.

It may not have impact overall, but there is a bug that affects all of the words. For some reasons, I wasn’t able to get a few short words on the first line when the first line isn’t full. Same goes for the second line, it wraps short words over to the next dialog.

Update:

So I managed to fix most of the issues regarding extreme and common cases of my dialogue.

Here’s the code in its entirely: http://pastebin.java-gaming.org/dd6bb5c978e

I had to abuse the use of exception handling in order to get them working as intended. I don’t know if this counteracts with the standard practices.