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();
}
}
}
}
2 Answers 2
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
.
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.
-
\$\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– Emz2015年12月30日 05:02:43 +00:00Commented Dec 30, 2015 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– kph0x12015年12月30日 05:28:17 +00:00Commented Dec 30, 2015 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– Emz2015年12月30日 16:58:35 +00:00Commented Dec 30, 2015 at 16:58 -
\$\begingroup\$ Had revised comment link shortly after posting; should be cooperative now. \$\endgroup\$kph0x1– kph0x12015年12月30日 18:12:34 +00:00Commented Dec 30, 2015 at 18:12