How to do running text efficiently?

Started by
7 comments, last by larsbutler 10 years ago

This is what a running text look like. The term itself could mean anything else, so I decided to put a GIF here for context.

1rYszXm.gif

And this is what I'm currently doing, using the code given below:

cI3JRSZ.png

I have created an inflexible running text code. It's not only bloated with variables, but also this is the only way I could think up of for the past few hours. But, it runs very fine. And now I want to optimize it.


	private String[] tokens;
	private int tokenPointer;
	private int beginningPointer;
	private int firstLinePointer;
	private int secondLinePointer;
	private int totalStringPointer;
	private boolean firstLineFull;
	private boolean secondLineFull;
	private Map<Integer, Boolean> dialogs;
	private boolean next;
	private boolean nextTick;
	private int stringPointer;
	private byte tickSpeed;
	private byte arrowTickSpeed;
	private String dialogueText;
	private boolean showDialog;

	public void renderTextGraphics(Graphics g) {
		if (this.dialogueText != null && !this.dialogueText.isEmpty()) {
			//The game uses 8f FONT when shown on the screen. It is scaled by GAME_SCALE.
			//Text are drawn with positive X = RIGHT, positive Y = UP. Not the other way around.
			g.setColor(Color.black);
			g.setFont(Art.font.deriveFont(Font.PLAIN, 24f));
			if (this.totalStringPointer <= this.dialogueText.length()) {
				//Handles one word sentences only.
				if (this.stringPointer > this.dialogueText.length()) {
					this.stringPointer = this.dialogueText.length();
					this.next = true;
					g.drawString(this.dialogueText.substring(this.beginningPointer, this.beginningPointer + this.stringPointer), Dialogue.getDialogueTextStartingX(), Dialogue.getDialogueTextStartingY());
					return;
				}
				
				if (this.totalStringPointer == this.dialogueText.length()) {
					this.next = true;
					g.drawString(this.dialogueText.substring(this.beginningPointer, this.beginningPointer + this.firstLinePointer), Dialogue.getDialogueTextStartingX(), Dialogue.getDialogueTextStartingY());
					g.drawString(this.dialogueText.substring(this.beginningPointer + this.firstLinePointer, this.dialogueText.length()), Dialogue.getDialogueTextStartingX(), Dialogue.getDialogueTextSecondLineStartingY());
					return;
				}
				
				if (this.firstLineFull && this.secondLineFull) {
					this.next = true;
					g.drawString(this.dialogueText.substring(this.beginningPointer, this.beginningPointer + this.firstLinePointer), Dialogue.getDialogueTextStartingX(), Dialogue.getDialogueTextStartingY());
					g.drawString(this.dialogueText.substring(this.beginningPointer + this.firstLinePointer, this.beginningPointer + this.secondLinePointer), Dialogue.getDialogueTextStartingX(), Dialogue.getDialogueTextSecondLineStartingY());
					return;
				}
				
				//Handles more than one word.
				String text = this.tokens[this.tokenPointer];
				if (this.firstLinePointer + text.length() <= MAX_STRING_LENGTH) {
					if (this.stringPointer > text.length()) {
						this.firstLinePointer += this.stringPointer;
						this.secondLinePointer = this.firstLinePointer;
						if (this.tokenPointer < this.tokens.length - 1) {
							this.tokenPointer++;
							this.stringPointer = 0;
						}
						//Short sentences.
						if (this.totalStringPointer >= this.dialogueText.length() - 1) {
							g.drawString(this.dialogueText.substring(this.beginningPointer, this.beginningPointer + this.firstLinePointer), Dialogue.getDialogueTextStartingX(), Dialogue.getDialogueTextStartingY());
							return;
						}
					}
				}
				else {
					this.firstLineFull = true;
				}
				if (this.firstLineFull) {
					if (this.secondLinePointer + text.length() < MAX_STRING_LENGTH * 2) {
						if (this.stringPointer > text.length()) {
							this.secondLinePointer += this.stringPointer;
							if (this.tokenPointer < this.tokens.length - 1) {
								this.tokenPointer++;
								this.stringPointer = 0;
							}
						}
					}
					else {
						this.secondLineFull = true;
					}
					g.drawString(this.dialogueText.substring(this.beginningPointer, this.beginningPointer + this.firstLinePointer), Dialogue.getDialogueTextStartingX(), Dialogue.getDialogueTextStartingY());
					if (!this.secondLineFull) {
						if (this.secondLinePointer + text.length() < MAX_STRING_LENGTH * 2) {
							g.drawString(this.dialogueText.substring(this.beginningPointer + this.firstLinePointer, this.beginningPointer + this.secondLinePointer + this.stringPointer), Dialogue.getDialogueTextStartingX(), Dialogue.getDialogueTextSecondLineStartingY());
						}
					}
					else {
						g.drawString(this.dialogueText.substring(this.beginningPointer + this.firstLinePointer, this.beginningPointer + this.secondLinePointer), Dialogue.getDialogueTextStartingX(), Dialogue.getDialogueTextSecondLineStartingY());
						this.next = true;
					}
				}
				else {
					g.drawString(this.dialogueText.substring(this.beginningPointer, this.beginningPointer + this.firstLinePointer + this.stringPointer), Dialogue.getDialogueTextStartingX(), Dialogue.getDialogueTextStartingY());
				}
			}
			else {
				this.next = true;
				g.drawString(this.dialogueText.substring(this.beginningPointer, this.beginningPointer + this.firstLinePointer), Dialogue.getDialogueTextStartingX(), Dialogue.getDialogueTextStartingY());
				g.drawString(this.dialogueText.substring(this.beginningPointer + this.firstLinePointer, this.beginningPointer + this.secondLinePointer), Dialogue.getDialogueTextStartingX(), Dialogue.getDialogueTextSecondLineStartingY());
				return;
			}
		}
	}

For the background context: The problem for me is the way the running text works.

For each game update ticks, it draws a new letter at the end of the string. Once it reaches the end of the dialog box, it should automatically move itself to the next line. In order to accomplish this, I need to use three pointers. One for keeping track of the beginning of a string, because the area of the dialog box is limited, and dialogues are rarely just 2 lines at the most. The other two pointers keep track of each line, checking to make sure the text are staying in bounds of the dialog box.

When the text is more than 1 dialog box big, I swap the ones keeping track of the beginning of a sentence and the end of the sentence, so the beginning is now 1 sentence ahead, and erase the end of the old sentence so that it can keep track of the new sentence.

I thought using 3 variables is enough for me, but it turns out I need like 10 of them just to get the auto-newline part implemented.

--------------------------------------------------

I don't know how to optimize it more as I have been thinking alone on this part. Could anyone provide tips on how I should "optimize" the codes a bit? Thanks in advance.

EDIT: Apparently, there are still bugs in that portion of code. I am really thinking I'm not implementing the running text as well as I thought. Ugh!

Advertisement

I don't know how to optimize it more as I have been thinking alone on this part. Could anyone provide tips on how I should "optimize" the codes a bit? Thanks in advance.

Why do you feel you need to "optimize" this code? Is it not performing fast enough? Have you profiled the code to see where the bottleneck is, and do you know how fast the code needs to run so that it meets your needs?

If you mean that the algorithm is messy and you'd like to clean it up, that's different. Refactoring will help with this. http://www.cs.unc.edu/~stotts/723/refactor/chap1.html

Start by moving the algorithm to its own class. Then move any common code to its own method so there is no duplication. Then find any spots that are hard to understand, and rename variables and create methods with descriptive names to help you understand the code when you revisit it 3 months from now. Finally, rearrange things until there is as much duplication as possible, then replace the duplicate with a single method.

During this process you may find that some code isn't needed or there is a simpler way to get the same results. By making the code easy to understand, you'll make it possible to optimize it later if you really need to.

I think, therefore I am. I think? - "George Carlin"
My Website: Indie Game Programming

My Twitter: https://twitter.com/indieprogram

My Book: http://amzn.com/1305076532

Oh, it's called refactoring? Understood.

Isn't using lots of state variables messy and bloatwise?

It does the job easily, it is just messy when you at it.

Yes, you're talking about refactoring, or cleaning up the code. Some book talk about how code just "smells" funny, which is actually a good way to explain it. There isn't anything wrong with it, and there are no bugs, but...

http://sourcemaking.com/refactoring

Check this out for examples of refactoring you can do, but most of it self explanatory. The goal of refactoring is clean code that works. If it looks messy, refactor until its clean, but don't go so far that you have so many levels of indirection that you can't figure out what is going on.

I think, therefore I am. I think? - "George Carlin"
My Website: Indie Game Programming

My Twitter: https://twitter.com/indieprogram

My Book: http://amzn.com/1305076532

Okay, I see it now. The main problem lingers though, since I want to reduce some of the if... conditions.

So since you are looking at replacing the algorithm (NOT refactoring, which leaves the algorithm fundamentally intact) I immediately wonder why you are reformatting everything each time. That seems like the first issue in the code.

Why not just format the whole thing once, then advance which letters are visible? This is especially true with automatic word wrapping, it is painful to watch a word start get spelled out on a line only to watch it pop to the next row mid-word as it exceeds the line length. This gives you a single pass to format everything, and a much simpler pass to advance the length of the string.

It depends how simple you want the text display.

You can have a lot of fun if you want. I like to use a very old technique. I have a byte array for the text and a byte array for the attributes. I use a fixed size font (which makes formatting a lot easier) and pass both the char and the attribute into a shader.

Think of it a bit like an old char mapped array.

The attribute controls how the char is displayed. It can be as simple as colour, or as complex as you like.

For example you could map the attribute to a 1D texture, anything over 128 could be transparent, then it could fade in, go through a bright flash and settle at the display colour.

When you update the text. If you are not adding any text you just run through the attributes and set up rules for how they change. Could be as simple as decrement until zero, or you could have ranges, anything you can think off.

When all attributes are stable (none changed in the current update), you can check to see if you need to change the text.

I know it's a very old idea, but a lot of old ideas are still good ideas.


Why not just format the whole thing once, then advance which letters are visible? This is especially true with automatic word wrapping, it is painful to watch a word start get spelled out on a line only to watch it pop to the next row mid-word as it exceeds the line length. This gives you a single pass to format everything, and a much simpler pass to advance the length of the string.

Hm, never thought of that approach before. Going to work that out for a bit.

Some good suggestions here. The only thing I can add here in terms of code is improvement is to mention the "DRY" principle (Don't Repeat Yourself) [1]. According to the definition, DRY applies mostly to higher-level architecture and design, but it applies to the small details of code as well.

For example, I see a lot of this repeated in your code:


g.drawString(this.dialogueText.substring(this.beginningPointer, this.beginningPointer + this.firstLinePointer), Dialogue.getDialogueTextStartingX(), Dialogue.getDialogueTextStartingY());

Try and see if you can eliminate some of the duplication.

[1] - http://en.wikipedia.org/wiki/Don't_repeat_yourself

This topic is closed to new replies.

Advertisement