12
\$\begingroup\$

About / Background

This is indirectly a follow up from: General Game Loop 3.0, if you prefer to test against it, feel free to. That code is very outdated now though. A test suite is given at the end.

For the actual class to direct feedback against: MyText.java, a short description of it follows beneath, as well as a test suite for it.

The idea is to build a wrapper around java.awt.Graphics.drawString(String str, int x, int y);, making it more ease and more effective to use in game development.

Remarks

There is in an Interface that is being implemented by MyText that holds everything in common for rendering. I avoided it here as feedback there is not really needed at the current state.

These fields are loaded from a configuration file that I also choose to omit from this feedback session, as it just adds another layer of complexity to it.

private String delimeter = "...";
private Font font = new Font("Verdana", 0, 12);
private Color color = Color.BLACK;

I've chosen to omit all @Javadoc as of now as well. The idea is that code should be clear enough even without.

Currently it is missing one feature, that is "rainbow" colored font, will later on add support for something like [red]some red text[/red] back to origina- [blue]aha and blue again[/blue].

Feedback target

  • This class is fairly massive, might be the case of splitting it into three classes? One that handles lines, one with fixed width and one with fixed height and width? However, the later class is not getting much cleaner than this one. Right now it is very easy to switch between them during development.

  • private Image stringToText(String string) is a mess, are there easier/more clean roads to take to achieve what it is supposed to do?

  • Usability - Is this something (with @Javadoc) that you could happily work with when developing games, if not, what is missing?

MyText

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class MyText
{
    private String string;
    private Image image;
    private final int x;
    private final int y;        
    private int w;
    private int h;
    private final Font font;
    private final Color color;

    private String delimeter = "...";

    private MyText (TextBuilder textBuilder) {
        string = textBuilder.string;
        font = textBuilder.font;
        color = textBuilder.color;

        x = textBuilder.x;
        y = textBuilder.y;

        w = textBuilder.w;
        h = textBuilder.h;

        delimeter = textBuilder.delimeter;

        image = stringToText (textBuilder.string);      
    }   

    public int getX () {
        return x;
    }

    public int getY () {
        return y;
    }

    public int getW () {
        return w;
    }

    public int getH () {
        return h;
    }

    public void render(Graphics g) {
        g.drawImage(image, x, y, null);     
    }

    public void invalidate (String s) {
        if (!string.equals(s)) {
            image = stringToText (s);
        }
    }

    private Image stringToText(String string) {
        BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = img.createGraphics();
        g.setFont(font);
        FontMetrics fm = g.getFontMetrics();

        List<String> lines = new ArrayList<>(Arrays.asList(string.split("\\r?\\n")));

        // break longest lines based on width
        if (w > 0 && w < Integer.MAX_VALUE) {
            for (int i = 0; i < lines.size(); i++) {
                if (fm.stringWidth(lines.get(i)) > w) {
                    List<String> words = new ArrayList<>(Arrays.asList(lines.get(i).split(" ")));
                    String new_line = words.get(0);

                    // word is too long
                    if (fm.stringWidth(new_line) > w) {
                        return null;
                    }

                    int j = 1;
                    for (; j < words.size(); j++) {
                        if (fm.stringWidth(" " + new_line + words.get(j)) < w) {
                            new_line += " " + words.get(j);
                        } else {
                            lines.set(i, new_line); 
                            break;
                        }
                    }

                    String leftOver = "";

                    for (; j < words.size(); j++) {
                        leftOver += words.get(j) + " ";
                    }
                    leftOver = leftOver.substring(0, leftOver.length()-1);

                    lines.add(i+1, leftOver);
                }
            }
        }

        // update width to the longest string
        w = 0;
        for (int i = 0; i < lines.size(); i++) {
            w = Math.max(w, fm.stringWidth(lines.get(i)));
        }

        // add delimiter if string passed height, find how many lines fit
        if (h < fm.getHeight() * lines.size()) {
            int index = lines.size();

            while (index-1 > h / fm.getHeight()) {
                index--;
            }

            String new_line = lines.get(index);

            while (fm.stringWidth(delimeter) + fm.stringWidth(new_line) > w) {
                // delimiter too long to fit at all
                if (fm.stringWidth(delimeter) > w) {
                    break;
                }

                new_line = new_line.substring(0, new_line.lastIndexOf(' '));
            }

            new_line += delimeter;

            lines.remove(index);
            lines.add(index, new_line);

            for (int i = lines.size()-1; i > index ; i--) {
                lines.remove(lines.size()-1);
            }
        }

        // update height to the total lines
        h = fm.getHeight() * lines.size();

        // clean
        g.dispose();

        // render new image
        img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        g = img.createGraphics();
        g.setFont(font);
        fm = g.getFontMetrics();

        g.setColor(color);
        for (int i = 0; i < lines.size(); i++) {
            g.drawString(lines.get(i), 0, fm.getHeight()*(i+1)-4);
        }
        g.dispose();

        return img;
    }

    public static class TextBuilder
    {
        private final String string;
        private final int x;
        private final int y;
        private int w = Integer.MAX_VALUE;
        private int h = Integer.MAX_VALUE;
        private String delimeter = "...";
        private Font font = new Font("Verdana", 0, 12);
        private Color color = Color.BLACK;

        public TextBuilder (String string, int x, int y) {
            this.string = string;
            this.x = x;
            this.y = y;
        }

        public TextBuilder setMaxWidth (int w) {
            this.w = w;
            return this;
        }

        public TextBuilder setMaxBounds (int w, int h) {
            this.w = w;
            this.h = h;         
            return this;
        }

        public TextBuilder setDelimeter (String delimeter) {
            this.delimeter = delimeter;
            return this;
        }

        public TextBuilder fontSize (String name, int style, int size) {
            this.font = new Font (name, style, size);
            return this;
        }

        public TextBuilder color (Color color) {
            this.color = color;
            return this;
        }

        public MyText build () {
            return new MyText (this);
        }
    }
}

Explanation

public void render(Graphics g);

Invoked from the render loop, per the test case.

public void invalidate (String s)

Let's say you have a dynamic variable, health, when you take damage you want to update the String contained in the MyText, it is done via this method. It keeps all other values.

Test Case

This is just here so you can try the code without having to write a test application yourself.

import java.awt.Canvas;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.image.BufferStrategy;

import test.MyText.TextBuilder;

public class MyMain extends Canvas implements Runnable
{
    MyText aLine = new TextBuilder ("Hello", 100, 100).build();
    MyText aList = new TextBuilder ("Hello, Hello, Hello, Hello, Hello, Hello, Hello, Hello", 200, 100).setMaxWidth(50).build();
    MyText aBox = new TextBuilder ("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's. Wtf is this all about.", 300, 100).setMaxBounds(100, 100).build();

    public static void main (String[] args) {
        new MyMain ();  
    }

    public MyMain () {
        Frame frame = new Frame ();
        frame.add (this);
        frame.setPreferredSize(new Dimension(500,300));
        frame.pack ();
        frame.setLocationRelativeTo (null);
        frame.setVisible (true);

        // off we go
        Thread thread = new Thread(this);
        thread.start ();
    }

    @Override
    public void run() {
        while (true) {
            while (true) {
                BufferStrategy bufferstrategy = getBufferStrategy ();

                if (bufferstrategy == null) {
                    createBufferStrategy(3);
                    break;
                }

                Graphics g = bufferstrategy.getDrawGraphics();      
                g.clearRect(0, 0, 500, 300);

                /* Test Cases Begin */      
                aLine.render(g);
                aList.render(g);
                aBox.render(g);
                /* Test Cases End */

                g.dispose();
                bufferstrategy.show();
            }
        }
    }
}
\$\endgroup\$
1
\$\begingroup\$

delimeter should be spelled delimiter.


for (int i = 0; i < lines.size(); i++) {
    if (fm.stringWidth(lines.get(i)) > w) {
        List<String> words = new ArrayList<>(Arrays.asList(lines.get(i).split(" ")));
        String new_line = words.get(0);

        // word is too long
        if (fm.stringWidth(new_line) > w) {
            return null;
        }

        int j = 1;
        for (; j < words.size(); j++) {
            if (fm.stringWidth(" " + new_line + words.get(j)) < w) {
                new_line += " " + words.get(j);
            } else {
                lines.set(i, new_line); 
                break;
            }
        }

        String leftOver = "";

        for (; j < words.size(); j++) {
            leftOver += words.get(j) + " ";
        }
        leftOver = leftOver.substring(0, leftOver.length()-1);

        lines.add(i+1, leftOver);
    }
}

This slab of code has only 1 output variable, lines. You could move it to a separate function to try and keep stringToText understandable.

    w = 0;
    for (int i = 0; i < lines.size(); i++) {
        w = Math.max(w, fm.stringWidth(lines.get(i)));
    }

With Java 8 you could turn this into a stream, calling .max()


        while (index-1 > h / fm.getHeight()) {
            index--;
        }

Don't loop for something that you can calculate.

index = ((int) Math.floor(h / fm.getHeight())) + 1; will do, although you might not need the int cast. Use with either the conditional, or with Math.min.

\$\endgroup\$
0
\$\begingroup\$

Could have collection of lines, then create Guava Function or similar which creates the intermediate List collection, the one currently done within the lengthy method. Use Guava apply or transform, filter methods with the function and lines collection input. That should help reduce the stringToText method length.

Could apply an OOP design pattern, pipes & filters, etc, refactoring code into additional classes. Instead of lengthy procedural logic, divide amongst constructors.

Replace for loop with iterator usage for code prettiness and to prevent off by one errors.

Could use a recursive approach.

public String insertLinefeeds(String input) 
{       
    final List<String> words = ImmutableList.copyOf( input.split("\\s") );

    final Iterator<String> wordsIter = words.iterator();

    String lineOut = "";

    while( wordsIter.hasNext() )
    {
        String wordIn = wordsIter.next();

        // method providing access to Font Metric Width measurement check

        if( !new FontMetricWidthFunction().apply( wordIn ) )
            throw new RuntimeException("Word too long: " + wordIn);

        if( (new FontMetricWidthFunction().apply( lineOut + " " + wordIn)) )
        {
           lineOut += wordIn + " ";

        }
        else
        {
            String lineRemaining = "";

            while( wordsIter.hasNext() ) lineRemaining += 
                    wordsIter.next() + " "; 

            if( lineRemaining.length() < 2 )
            {
                // last word in this line remains

                // insert line break for this line

                lineOut += "\n";

                // skip recursion since line feed, last word only needed

                return lineOut + wordIn + "\n";

            }
            else
            {
                // words remain

                // insert line break for this line

                lineOut += "\n";

                // continue inserting breaks as needed for remaining words                  

                lineOut += insertLinefeeds(wordIn + " " + lineRemaining);

            }//if

        }//if

    }//while

    return lineOut + "\n";

}//method

String testStr = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's. Wtf is this all 

insertLinefeeds(testStr) for 30 character width

Lorem Ipsum is simply dummy 
text of the printing and 
typesetting industry. Lorem 
Ipsum has been the 
industry's. Wtf is this all 
about.
\$\endgroup\$
  • \$\begingroup\$ The for loops I am using the indexes in, I don't really think it is any more neat or pretty using them int i = 0; for (String s : lines) {...i++;}. Can you give a more concrete example of how to use make use of Guava's apply or transform in this case? Also "... refactoring code into additional classes...", if I knew how to refactor the code I would do it, so again, any concrete example? \$\endgroup\$ – Emz Dec 30 '15 at 5:02
  • \$\begingroup\$ Collection framework iteration can replace unsightly, lengthy loop, conditional logic, especially using Guava. One can shrink the code into a readable series of steps. Appears List collections are being edited currently using loops, conditional logic. Can create some code later if still wish. Have some Java at catjcode.com/websvn (this repo) with several old projects using Guava function, Collections2, transform, filter, etc instead of lengthy loops. Also, have you considered StringBuilder? There may be some useful methods with it instead of String. \$\endgroup\$ – kph0x1 Dec 30 '15 at 5:28
  • \$\begingroup\$ StringBuilder could make it a bit more effective yes, however a very minor, no usefulness to my knowledge, as all I am doing is .append(). Your link does not work either. \$\endgroup\$ – Emz Dec 30 '15 at 16:58
  • \$\begingroup\$ Had revised comment link shortly after posting; should be cooperative now. \$\endgroup\$ – kph0x1 Dec 30 '15 at 18:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.