Last active
March 2, 2022 23:45
-
-
Save codebreadGist/8f12f90664f8a66db37b6f0f3f690f1e to your computer and use it in GitHub Desktop.
Scrolling, word wrapping text with various colors and font styles (Monogame)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Monogame/XNA doesn't allow you to alter the style of a string in the middle of drawing it, so instead you have to draw each | |
individual piece of a string if you want to bold or italicize a word. This involves knowing how long each bit of string is | |
and calling DrawString() for each one, all while making sure they line up correctly. Add in the ability to word wrap and | |
scroll the dialogue one letter at a time and it becomes a pretty painful problem to solve. | |
To start, I wrote a function that can take a dialogue box width and a string with special formatting strings embedded in it. | |
The string used for the text above is this: | |
"I can't {red}shake {blue}the {green}feeling {normal}that {italics}something {normal}belongs here... | |
Something extraordinarily {bold}ordinary." | |
No line breaks or anything like that, just the font styles wrapped in curly braces. | |
Step by step, the function works as follows: | |
1. Split the string into subsections on '{' to get the subsections | |
string[] sections = text.Split('{'); | |
2. For each section, split on '}' to get the subsection and font style as a 2 element array. Add these to a List<string[]> | |
subsections | |
for (int i = 0; i < sections.Length; i++) | |
{ | |
subsections.Add(sections[i].Split('}')); | |
if (subsections[i].Length == 2 && subsections[i][1] == "") // Remove any element that only has a style and no text | |
subsections.RemoveAt(i); | |
else if (subsections[i].Length > 1) //I want the text to be the first element, and the style second | |
Array.Reverse(subsections[i]); | |
} | |
Now I have a List that looks like this: | |
{ | |
["I can't"] | |
["shake", "red"] | |
["the", "green"] | |
... | |
["something", "italics"] | |
... | |
} | |
3. Loop through each subsection and change your font/color based on the second element in each array. I used a switch | |
statement here, catching the "italics", "bold", and "normal" specifically, and then casting anything else to a Color. | |
Of course, you only want to do this if the array has a second element (you'll notice above my first element doesn't because | |
I didn't give it a formatting string) | |
for (int i = 0; i < subsections.Count; i++) | |
{ | |
//Set the next font | |
if (subsections[i].Length > 1) | |
{ | |
switch (subsections[i][1]) | |
{ | |
case "bold": | |
currentColor = Color.Black; | |
currentFont = bold != null ? bold : dialogueFontBold; | |
break; | |
case "italics": | |
currentFont = italics != null ? italics : dialogueFontItalic; | |
currentColor = Color.Black; | |
break; | |
case "normal": | |
currentFont = dialogueFont; | |
currentColor = Color.Black; | |
break; | |
default: | |
currentFont = dialogueFont; | |
System.Drawing.Color tempColor = System.Drawing.Color.FromName(subsections[i][1]); | |
currentColor = new Color(tempColor.R, tempColor.G, tempColor.B); | |
break; | |
} | |
} | |
... | |
... | |
} | |
4. In the same loop, I split the first element (the dialogue) on every space, creating an array of words. I then measure | |
the length of a single space character using the currentFont that I set above and loop through the array of words that I | |
just created. For each word I measure it and add it to a StringBuilder. As I go I keep track of the total "lineWidth" and | |
do some different things based on how big that number gets. I also keep track of the total length of the entire dialogue | |
string, "stringNum", so I can use it for scrolling dialogue later. | |
for (int i = 0; i < subsections.Count; i++) | |
{ | |
//Set the next font | |
... | |
... | |
StringBuilder sb = new StringBuilder(); | |
List<string> words = subsections[i][0].Split(' ').ToList(); | |
float spaceWidth = currentFont.MeasureString(" ").X; | |
lineWidth = 0; | |
for (int j = 0; j < words.Count; j++) | |
{ | |
if (words[j] != "") | |
{ | |
Vector2 size = currentFont.MeasureString(words[j]); | |
if (words[j].Contains("\n")) | |
{ | |
lineWidth = 0; | |
} | |
if (lineWidth + size.X < maxLineWidth) | |
{ | |
sb.Append(words[j] + " "); | |
lineWidth += size.X + spaceWidth; | |
} | |
else | |
{ | |
words[j] = "\n" + words[j]; | |
subsections.Insert(i + 1, new string[] { | |
SplitList(words, j), subsections[i].Length > 1 ? subsections[i][1] : "normal" }); | |
subsections[i][0] = String.Join(" ", words); | |
break; | |
} | |
} | |
} | |
stringNum += sb.Length; | |
} | |
That last part in the "else" statement is probably confusing, because it was confusing to me when I wrote it. I'll break | |
it down for you. If the current subsection we're looping through looks like this: | |
["how are you doing", "bold"] , then the word array looks like this: | |
["how", "are", "you", "doing"] | |
If the word "you" ends up going beyond the bounds of the dialogue box, it will end up in that else block with a line | |
break added. | |
["how", "are", "\nyou", "doing"] | |
Then I call a function that I wrote called SplitString, which takes in a List<string> and the index to split it on. It | |
will remove all of the elements at the index and after the index passed in, and return the removed elements as a new | |
concatenated String. I then insert that new string into the "subsections" List at the next index, and I use the current | |
index's font style for it. In other words we go from this: | |
["hello", "normal"] | |
["how are you doing", "bold"] | |
["today?", "normal"] | |
to: | |
["hello", "normal"] | |
["how are", "bold"] | |
["\nyou doing", "bold"] | |
["today?", "normal"] | |
and then the "words" loop breaks and keeps going, with that new addition next in line. It catches on the first "if" | |
statement and resets the lineWidth, and all is good in the world. | |
5. Now, once ALL of that is done I end up with the "subsections" List broken up based on style and also new lines so | |
word wrap will work. At this point I save the "subsections" List and move onto the Draw Loop. | |
scrollTextSections = subsections; //scrollTextSections is a List<string[]>defined at class level. | |
At this point I should mention that you could easily draw the dialogue now if you didn't want scrolling text. At the end of | |
each iteration of the "subsection" loop, after the "words" loop, you could call DrawString() and use the default | |
position + linewidth, currentFont, and currentColor to get the desired, non-scrolling effect. In order to get the vertical | |
positioning correct you would have to keep track of the amount of times you had a new line break and multiply that by the | |
height of a newline character for the currentFont you're using. For a more in-depth look at that, keep reading. | |
6.The scrolling is the most difficult part, as I have to keep track not only of where in the dialogue the "scroll index" is, | |
but I have to know where it is relative to each subsection as well. | |
DISCLAIMER: This code sucks a lot, and a lot of it is just redoing what I did above a second time in order to get the | |
correct font, color, linewidth and new lines added. I can't do it before getting my scrollTextSections list though, because | |
I can't be making edits to the "subsections" list as I scroll through the dialogue. | |
6.1. To start, I set some base stuff: | |
SpriteFont currentFont = dialogueFont; | |
Color currentColor = Color.Black; | |
float lineHeight = currentFont.MeasureString("\n").Y; | |
int numberOfNewLines = 0; | |
Vector2 posOffset = new Vector2(0, 0); | |
float lineWidth = 0f; | |
currentFullStringIndex = 0; //int defined at class level | |
scrollDialogue = ""; //String defined at class level | |
6.2. Loop through scrollTextSections and get the currentFont and currentColor, same as Step 3 above. | |
6.3. In the same loop, set some local variables and split the current iteration's string into a "words" array. Then loop | |
through the words and set the line width as you go, while appending to a StringBuilder. This is similar to Step 4 above, | |
but reduced a bit: | |
StringBuilder sb = new StringBuilder(); | |
List<string> words = scrollTextSections[i][0].Split(' ').ToList(); | |
float spaceWidth = currentFont.MeasureString(" ").X; | |
for (int j = 0; j < words.Count; j++) | |
{ | |
if (words[j] != "") | |
{ | |
Vector2 size = currentFont.MeasureString(words[j]); | |
if (words[j].Contains("\n")) | |
{ | |
lineWidth = 0; | |
numberOfNewLines++; | |
posOffset.X = 0; | |
} | |
if (lineWidth + size.X < maxLineWidth || size.X > maxLineWidth) | |
{ | |
sb.Append(words[j] + " "); | |
lineWidth += size.X + spaceWidth; | |
} | |
} | |
} | |
It's a lot cleaner now because of the "\n" new line breaks I added in Step 4 above. This means that lineWidth should NEVER | |
go beyond maxLineWidth. You'll notice that I'm also keeping track of the "numberOfNewLines" added, as well as resetting my | |
"posOffset.X" back to 0 when this happens. Both of these are used to draw the subsections in their correct spot on the | |
screen, relative to the other subsections. | |
6.4. Still in the "scrollTextSections" loop, I create a String that holds the current subsections dialogue, just for | |
readability: | |
String dialogue = scrollTextSections[i][0]; | |
6. 5. Scroll the dialogue! | |
if (scrollDialogueNum < stringNum && scrollDialogueNum >= currentFullStringIndex && | |
scrollDialogueNum - currentFullStringIndex < dialogue.Length) | |
{ | |
scrollDialogue = dialogue.Substring(0, scrollDialogueNum - currentFullStringIndex); | |
scrollDialogueNum++; | |
s.DrawString(currentFont, scrollDialogue, pos + posOffset, currentColor, 0, Vector2.Zero, 1f, SpriteEffects.None, 0); | |
posOffset.X = lineWidth; | |
posOffset.Y = lineHeight / 2 * numberOfNewLines; | |
} | |
else if (scrollDialogueNum >= currentFullStringIndex + dialogue.Length) | |
{ | |
s.DrawString(currentFont, dialogue, pos + posOffset, currentColor, 0, Vector2.Zero, 1f, SpriteEffects.None, 0); | |
posOffset.X = lineWidth; | |
posOffset.Y = lineHeight / 2 * numberOfNewLines; | |
} | |
currentFullStringIndex += sb.Length; | |
Yowza. So, to break this down... | |
scrollDialogueNum: The current character index we're on, relative to the FULL dialogue string | |
stringNum: The full dialogue string's length, in characters | |
currentFullStringIndex: This keeps track of where we are in our Draw Loop. If we have 6 elements in "scrollTextSections", | |
that means we're calling DrawString 6 times, one for each element. At the end of each of those iterations I add the | |
length of the current element's WORDS to "currentFullStringIndex". Notice I say words instead of string. I don't want to | |
add the length of any "\n", which is why I use the StringBuilder's length. (StringBuilder's ignore linebreaks for their | |
length variable) | |
scrollDialogue: The string we're drawing based on how far the text as scrolled so far | |
Alright, so the first "if" statement is making a few checks. The first is to make sure we haven't reached the end of the | |
scrolling yet. The second, | |
scrollDialogueNum >= currentFullStringIndex | |
is to make sure that I only enter the "if" block when we're supposed to be drawing the next letter. Remember, this will | |
get hit 6 times per Draw Loop if I'm drawing 6 subsections, so I only want to increment scrollDialogueNum if we're drawing | |
the word that the text is currently scrolling through. The last check, | |
scrollDialogueNum - currentFullStringIndex < dialogue.Length | |
is making sure I stay within the bounds of each subsection when I draw. If scrollDialogueNum is at 30, but I'm drawing | |
the first subsection (meaning currentFullStringIndex is 0), that means I'm going to try drawing dialogue[30]. If the first | |
subsection's dialogue is, "I can't", as it is in the gif above, this would throw an exception. | |
If all of these things pass, we move inside: | |
scrollDialogue = dialogue.Substring(0, scrollDialogueNum - currentFullStringIndex); | |
scrollDialogueNum++; | |
s.DrawString(currentFont, scrollDialogue, pos + posOffset, currentColor, 0, Vector2.Zero, 1f, SpriteEffects.None, 0); | |
posOffset.X = lineWidth; | |
posOffset.Y = lineHeight / 2 * numberOfNewLines; | |
I set scrollDialogue to be equal to the current subsection's dialogue, but only up until the current scrolling letter. | |
To better understand this, imagine we have this string and each word is a subsection: "Super Daryl Deluxe" If | |
"scrollDialogue = 15" and we're currently drawing the third subsection (so currentFullStringIndex = 12, the length | |
of the first and second subsections combined), that means I'm on the letter 'U' in "Deluxe", because I want to draw | |
the 15th character. But since this subsection is only "Deluxe", 15 is out of bounds. Subtract the length of the previous | |
two subsections (scrollDialogueNum - currentFullStringIndex) and you get 3, which is the index of 'U' in "Deluxe". | |
So now we have the correct substring of the word we're trying to draw. I draw it using "pos", which is the position i pass | |
into the function, and then I add the posOffset to it. Afterward I set the "posOffset" variable to be the current lineWidth | |
(size of the current subsection) for X and the amount of new lines for Y. I cut the lineHeight in half because if you | |
measure just a "\n", as I did, it gives you the height of two lines. | |
Lastly, to break down the second part of 6.5 above: | |
else if (scrollDialogueNum >= currentFullStringIndex + dialogue.Length) | |
{ | |
s.DrawString(currentFont, dialogue, pos + posOffset, currentColor, 0, Vector2.Zero, 1f, SpriteEffects.None, 0); | |
posOffset.X = lineWidth; | |
posOffset.Y = lineHeight / 2 * numberOfNewLines; | |
} | |
currentFullStringIndex += sb.Length; | |
The check is saying: okay you failed the "if" statement above, which means maybe you're done scrolling, or maybe you're | |
trying to draw a word you've already scrolled through. If either of those is the case, draw the entire word. Finally, | |
increment the "currentFullStringIndex " by the length of the current StringBuilder. | |
----------------------------- | |
Alright, that's everything. I'll admit this code is pretty messy and there's probably a better way to get the same results, | |
but I have seen a lot of people asking about word wrap, or scrolling dialogue, or changing font styles mid-string with very | |
few solutions posted, and I've never seen them combined as one. Hopefully this helps someone out. | |
Credits: | |
- For the word wrapping part I had found a post a year or so ago that helped me: http://stackoverflow.com/a/15987581 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment