Open craftworkgames opened 7 years ago
I've been thinking about this a bit more. Get text to draw across multiple lines using either a BitmapFont
or SpriteFont
.
Let's say we wanted to render "Hello World" and wrap it over 2 lines.
The first way is to insert new line characters where we want to break the text.
_spriteBatch.DrawString(_font, "Hello\nWorld", position: Vector2.Zero, color: Color.White);
The other way is to draw each line separately, positioned based on the line height.
_spriteBatch.DrawString(_font, "Hello", position: Vector2.Zero, color: Color.White);
_spriteBatch.DrawString(_font, "World", position: new Vector2(0, _font.LineHeight), color: Color.White);
Both of these approaches are relatively the same performance wise and don't cause any garbage collection. The second approach has a little extra memory overhead passing some extra variables around but also has the advantage that it's more customizable (e.g. each word could be different color).
The complexity, and garbage collection issues kick in when we are trying to split up the string into words and sentences, then calculating the size of each word to determine where in the string we want to split it across multiple lines.
One approach to solving this issue is to make a kind of "retained mode" or stateful class that keeps track of the sentences in the string. This way the string only needs to be processed when the bounding rectangle changes rather than every render pass. An example of this would be how a "label" in a GUI system might work.
Another advantage of this approach is that we're decoupling the string splitting responsibility from the BitmapFont
class and therefore we could use the same approach with a SpriteFont
just as easily. The code would still need to know how to call the MeasureString
method on each type of font but apart from that the rest of the code is identical.
The rendering code simply becomes a single string containing new line characters at the appropriate breakpoints or several strings positioned appropriately.
Lastly, there's a couple of other things to think about:
My feeling is that these things would be required for rendering labels in a GUI system. It's an open question what goes in the GUI system and what stays in the core library. It seems it might be useful to have some sort of simple "text sprite" thing without the overhead of a complete GUI for rendering text within the game (maybe over the head of your character for example).
Personally, I feel the direction we should go with that is to use a class for it, something like WrappedText. This class should be capable of a few things.
I would actually recommend keeping this class outside of the GUI system altogether, for maximum usage for our users. There will be a lot that want this functionality, but already have their own GUI system so they'd rather not switch for just that. Not to mention I don't think it's the job of the GUI system to do so. The setup shouldn't be any different than using a string anyways.
To put it further, I'm willing to do this WrappedText class if we're in agreement on doing it.
How do we deal with text alignment within the bounding rectangle? Is it the responsibility of this class?
I think it should be. I suggest two enumerations: VerticalAlignment
and HorizontalAlignment
. HorizontalAlignment
would have Left
, Centre
, and Right
while VerticalAlignment
would have Top
, Centre
, and Bottom
. This would cover all possible combinations of text alignment within a rectangle by using a combination of these two enumerations.
How do we deal with the "clipping rectangle" (sometimes known as the scissor rectangle) for text that renders outside the bounding rectangle? Is this the responsibility of this class?
Since SpriteBatch
is probably going to be the renderer of choice here and since SpriteBatch
doesn't allow for changing the rendering state mid batch it's probably best to fake the clipping by not drawing anything outside the given rectangle.
Personally, I feel the direction we should go with that is to use a class for it, something like WrappedText.
Yep, I think we are on the same page here. I'm not sure what we should call the class yet WrappedText
is not bad but let's leave the final naming until after we've fleshed out exactly what it is.
Pre-wrapping text before it is drawn. This will make a little bit of garbage but it will do so exactly once instead of every frame.
Pre-wrapping will happen only once if the size of the bounding rectangle doesn't change. In a lot of cases this will be true, but I think we probably need to support changes in size as well. Either way it shouldn't be a big deal.
Accepting a mask so that things wrap more naturally.
Can you explain how the mask works a little more?
Accepting an update to the substring being drawn (for stuff like displaying text on screen one letter at a time like is acceptable in most RPGs)
This is an interesting one. I'm sure there's also an interesting solution, but let's be careful not to blow out the scope of this class. Drawing the substring probably falls outside the responsibility of this class and lands closer to the responsibility of the renderer. Although, it might also be somewhere in between.
I would actually recommend keeping this class outside of the GUI system altogether, for maximum usage for our users. There will be a lot that want this functionality, but already have their own GUI system so they'd rather not switch for just that. Not to mention I don't think it's the job of the GUI system to do so. The setup shouldn't be any different than using a string anyways.
Yes, I agree there's definitely a place for this outside the GUI system. I'm thinking it's more akin to a type of Sprite
that can be positioned, rotated, scaled, and rendered in much the same way. It's also somewhat related to #305 in the sense the it's another type of scene node in a scene graph.
Of course, once we have it, using it in the GUI system also makes sense.
I think it should be. I suggest two enumerations: VerticalAlignment and HorizontalAlignment.
Sure, makes perfect sense.
Since SpriteBatch is probably going to be the renderer of choice here
SpriteBatch
will be the renderer in the first cut since we already have it but I'm trying to get into the habit of decoupling rendering from the model. That way we can support different renderers as needed.
and since SpriteBatch change the rendering state mid batch it's probably best to fake the clipping by not drawing anything outside the given rectangle.
Yeah, I agree that's a nice way to go. I guess that means we'd need an overload of DrawString
that takes a clipping rectangle or something.
Yep, I think we are on the same page here. I'm not sure what we should call the class yet WrappedText is not bad but let's leave the final naming until after we've fleshed out exactly what it is.
Yeah, I agree. The name's pretty unimportant anyways. Can always change.
Pre-wrapping will happen only once if the size of the bounding rectangle doesn't change. In a lot of cases this will be true, but I think we probably need to support changes in size as well. Either way it shouldn't be a big deal.
Ahh true enough. I agree it should support changes in size, but I have questions about how it would be used like that, but it's trivial anyways if we do that at the start.
Can you explain how the mask works a little more? This works with the substring thing, but I'll explain the problem,
Okay let's say you have room for 14 characters before you wrap. You're displaying a chatbox of a character talking to you, and your string is "Hey there, how are you?" If you don't use a mask, what would happen is that when it reaches "how", it'll display "ho" on the first line, but when the w is added, "how" suddenly breaks the word wrap, and the whole word jumps to the bottom, looking horrifically unnatural and painful to look at. A mask would be the whole line it's supposed to display, so that it'll display exactly where it should be in the final product without jumping. This may not actually be needed if we batch them in the class, now that I think about. The mask would have been necessary to not have that jumping in the old style of word wrapping. As for this, I'll have to think more about it, but I think the substring thing below would fix that problem.
https://dl.dropboxusercontent.com/u/11911775/pics/1480379553515.webm Video demonstration.
This is an interesting one. I'm sure there's also an interesting solution, but let's be careful not to blow out the scope of this class. Drawing the substring probably falls outside the responsibility of this class and lands closer to the responsibility of the renderer. Although, it might also be somewhere in between.
I see what you're getting at, but if we do this outside of the class, would it not make a lot of garbage though? That's my main thought for having it in this class. Lots of games that display text in an auto-wrapped format use that sort of thing as well, so I don't feel like it's outside the scope of what this should do, but I may also be in the minority. We should get some more opinions on it.
Yeah, I agree that's a nice way to go. I guess that means we'd need an overload of DrawString that takes a clipping rectangle or something.
To be honest, I don't understand this clipping rect thing. Could you please explain it a bit more, as I can't imagine at all why we would do that clipping at draw time.
Ah right. I understand where you're coming from now.
The way I imagine it working is that you're pre-calculating the positions of all the characters in the chatbox by giving it the whole string. Then you tell the renderer to only render part of the string.
What this essentially boils down to in code is the algorithm:
I think that approach can be implemented fairly easily with no garbage and keeps the responsibilities separated.
What about something similar to libGDX GlyphLayout ?
This will allow stuff like styling markup thing Markup
I plan to port libGDX to c# once i have some free time, so i might be able to make it myself then share it over here
Thanks @Scellow. Definitely worth looking at. I don't know how many times I've used LibGDX for inspiration. In many ways, LibGDX and MonoGame.Extended are similar libraries.
Yeah, @craftworkgames, that's where I'm coming from, and I think I also get how you want to split the logic from the renderer too. The only real question is how to get x characters from the string without making garbage. My personal example for my game was to wrap it in classes and draw each character individually, but that's also because I allow for things like images and colors in my strings. I'm not sure that'd be the best approach for this. Sure, it's garbage free, but there's a significant performance hit I believe.
https://dl.dropboxusercontent.com/u/11911775/pics/coloricontest.png
The only real question is how to get x characters from the string without making garbage.
That's actually pretty easy. The renderer simply loops over all the characters in the string and draws them, so telling it to only draw the first 13 characters is a pretty small change. We wouldn't need to do any string splitting at all.
That said if we want to support arbitrary substrings, it might not be so simple.
If we want to support different colored text and other markup type things, we'd have to preprocess the string into different "runs" like the GlyphLayout thing does.
So just draw them one at a time like I've been doing? I figured that'd be slow, but I guess that's what it does anyways via spritefonts and bitmapfonts, ehh? I'm also for the markup. Considering most of what you use wrapped text for, the more robust we are, the better, right? It does definitely complicate things for us on the coding side though, but we're here to uncomplicate things for our end users by doing the complicated things ourselves, right? So count my vote in for that!
I figured that'd be slow, but I guess that's what it does anyways via spritefonts and bitmapfonts, ehh?
Drawing lots of sprites is what a SpriteBatch
is good at. As long as you're sensible about keeping the sprites packed into a single texture and you're not changing render states mid sentence you're all good to render them one at a time.
It does definitely complicate things for us on the coding side though
When you break complicated things down into a series of simple things, good stuff happens 😉
You mentioned renderers, which ones do you have in mind? I can imagine Spritefont, BitmapFont, and (since we have nuclex already right?) Vectorfont or whatever they call it.
@JustinHughart SpriteBatch
can render somewhere at ~4000 sprites in one draw call if using the same texture for all those sprites. Each SpriteBatch.Draw()
is not an actual draw but an enqueue for a draw command. Those draw commands are merged into one draw command if the same texture is used. It's been the norm to have every glyph from a string of characters to be drawn as sprites.
As renderers ago, it's going to be pretty much SpriteBatch
for now.
Ahh, I didn't know they merged commands with it. That's good and interesting to know. Also I understand that we're likely to use Spritebatch, but are we planning on supporting both Spritefont and Bitmapfont directly? I mean, we have to make something to actually draw it, right? Or is that outside the scope of this conversation?
You mentioned renderers, which ones do you have in mind?
Spritefont
and BitmapFont
are not renderers. They are the models.
The current renderer uses SpriteBatch
via an extension method so it's effectively decoupled from the model.
@LithiumToast has also been working on a renderer called DynamicBatch2D which could be used.
But the point is that if we decouple the responsiblities the renderer can be anything. Someone might want to write a renderer that draws the text to a texture floating in 3D space for example.
SpriteFont
is akward because the source rectangles for each glyph are not accessible currently without creating another copy of the Dictionary<char, Glyph>
. See https://github.com/MonoGame/MonoGame/blob/develop/MonoGame.Framework/Graphics/SpriteFont.cs#L82
EDIT: It's for this reason that SpriteFont
can not be used in any other renderers other than SpriteBatch
for now.
I've removed the word wrapping overloads from the
DrawString
methods in theBitmapFontExtensions
class.There's a few key reasons I decided to remove the word wrapping overloads:
Without word wrapping the
BitmapFont
class has a very clear scope. Parity withSpriteFont
.The wrapping code is quite complex and as a result caused a number of bugs (e.g.
MeasureString
wasn't taking into account wrapped lines).Splitting strings into words and lines over and over again during rendering causes a lot of garbage collection. There are other ways to deal with this problem, but they make it even more clear it shouldn't be the responsibility of the
BitmapFont
class.For reference (and for anyone still wanting to use the old code), here's a copy of the method before I started messing with it. Calls to
DrawInternal
should be replaceable by calls toDrawString
with care.