GeonBit.UI: UI extension for MonoGame

Hi all,

I’m working on a hobby game engine and my UI system is ready and fully functional, so I exported it as an independent extension. Here’s a quick preview:




Its under the MIT license and all the textures are CC and other permissive styles, so feel free to use it for whatever you want.

Here’s the git repo:

A quick demonstration video:

And installation video tutorial:

Any feedback, ideas etc will be appreciated.

Cheers! :slight_smile:

7 Likes

that’s one of the coolest and most complete UI’s I’ve seen.

Incidentally I’ve started working on another one of my own yesterday, but seeing yours kinda makes me want to try to see if I can’t use it. I’ll check it out

Ok, I’ve tried it out in my project and integration was pretty easy, well done.

I think the overall structure, documentation etc. is very positive, so I want to say that upfront.
Here is a quick integration, which i recorded:

So with that out the way let’s get to some criticism I have

  • it’s pretty neat that the UI Panels always stay in the window, however, this breaks when setting UserInterface.SCALE to something not 1.
  • Speaking of scale - one can scale the paragraphs etc. but it has no effect on Checkbox. This wouldn’t be a problem but the default checkbox text is scaled larger than the default-everything-else text
  • The slider goes almost over the end to the left but only to 90% to the right, when the step count is not even.

Garbage is a big problem. I think that’s fine for the typical target of this GUI, which i presume is simplistic 2d games, but unfortunately it disables it for my high-performance applications.
Even if the UI does nothing but exist - that means I don’t update anything on there - it generates loads and loads of garbage.
Here is a quick gif of the memory in garbage collector with and without UI (outside of the captured screen).

EDIT:

I’ve been using it a bit more and I think it’s a pleasure to use and looks good out of the box. I think I’ll definitely stick to it for a while.
One problem is that in order to determine the style for each entity some strings are added together each frame, which is not great for garbage collection, since these have to be discarded every time. I think I’ll look into finding a better solution for the string problem.

1 Like

Hi @kosmonautgames, thank you for the very useful feedback!

Thanks :slight_smile:

This is weird I just tested it again and it appears to be working (I tried the example code, there’s a step there with draggables entities + zooming buttons at the corner). Could you please paste the code of the panels / entities you had trouble with, or maybe if its not a hassle record a quick video showing the problem?

Yeah I made checkboxes text a big larger in the default themes, I thought it was better looking (could be wrong though heh). Anyway to scale checkbox text you need to access the child text entity, eg checkbox.TextParagraph.Scale = 0.7f;. Or if you want to change the CheckBox default text size in the theme itself, you can edit the XML files, specifically Styles/CheckBoxParagraph-Default.xml.

PS setting checkbox.Scale should have scaled the box itself, but there was a small bug there which I noticed thanks to you and fixed :slight_smile:

good catch, there were actually few problems with sliders Min value and Steps Count. I think its fixed (in the dev branch), but if you can paste the code to the slider you made I can validate that.

You are right I didn’t optimize it too much regarding garbage etc because its aimed for simple games. I didn’t quite understand the performance video you showed tbh, but anyway if you got cool tricks that I can try to reduce garbage let me know, I’m opened for ideas :slight_smile:

Thanks again for the feedback, it was super helpful!

EDIT: Just saw your edit right after submitting lol. Cool, if you find a neat way to optimize the style strings etc you can open pull request. Also you might want to take a look at the dev branch, I fixed few issues there and its pretty stable (but not fully tested so I didn’t merge yet)

            UserInterface.SCALE = 0.7f;
            UIManager.ShowCursor = false;
            panel = new Panel(new Vector2(500,600), PanelSkin.Simple, /*this does nothing when without a parent */Anchor.TopRight);
            panel.Draggable = false;
            UIManager.AddEntity(panel);
            panel.SetPosition(Anchor.Auto, new Vector2(1700,100));

So the problem evolves around SetPosition.

First of all, the SetPosition does not correspond to screen resolution, but instead to screenresolution / SCALE.
Which I can accept, that’s fine, I can convert my values.

So then I want my default panel to be at the very right end, so let’s say if my screen resolution is 100x100 and my panel is 20x20 i want the position of the panel to be <80, something>.

With default scale, I can simply overshoot and say
SetPosition(100, something) and it will stick to the right and find itself at (80,0). Good.

But if I scale to 0.5 for example and overshoot some random value and say
SetPosition(300, something) my panel will be out of the screen.

In the first example given above it will look like this, for a screen resolution of 1280x800

now what’s interesting is that if I enable dragging my panel will find itself somewhere in the middle of the screen instead of to the right, because, I suppose (haven’t checked) the SetPosition will be clamped to fit the 1280x800 resolution, which is in the middle of the screen after the scaling.
I guess the scale is applied after the first clamping/fitting to the screen is applied.

Afterwards I can drag it to the right if I want and it won’t go further.

In summary:
If I have SCALE < 1 my panel won’t be at the right end if i enable dragging and it won’t stick to the right end if I don’t enable drag.

First few notes regarding your code, that may not be related to the problem but can make your life easier :slight_smile:

  1. You don’t need to set panel.Draggable = false, its false by default.
  2. When you call panel.SetPosition(Anchor.Auto, new Vector2(1700,100), you are overriding the original anchor TopRight with Auto. If you want the panel to stay relative to the TopRight corner and just set the offset, you can use SetOffset() instead.

So in short you can change your code to this:

UserInterface.SCALE = 0.7f;
UIManager.ShowCursor = false;
panel = new Panel(new Vector2(500,600), PanelSkin.Simple, Anchor.TopRight, offset: new Vector2(0, 100));
UIManager.AddEntity(panel);

And get the same result without the need to calculate screen width etc. See image below:

Now about the dragging clap etc, I will try to reproduce and debug it tomorrow or the day after it, whenever I’ll have some time. If you want you can open a ticket on the git or I’ll just update you here, both options are ok.

Thanks :slight_smile:

I might give this a shot looks nice.

Is it possible to straight add it to your own solution and link it to your own project without building the dll separately and importing it ?

.

I wrote my own but its just so damn ugly and bloated and awful looking, i dunno i just hate it every time i rewrote it ended up looking worse maybe it just does too much. But it does scaling fully even the text aligns properly when resizing a window.

So anyways i just wanted to comment on scaling, as some food for thought.

You can maintain scale, if you scale the positions and the width heights by a ratio found from a original design time resolution vs any new resolution or window bounds. That’s one way to do it linearly but.

The drawback to that is you cant change the resolution while you are designing the game without extra ugly stuff which i think is still left in mine.

What i started to do though is use my own user positional floats that range from 0 to 1.0 for all button positions plus width height floats ect basically (a floating rectangle class) that relates to a game windows full width or height no matter what it may be. The methods that set positions of course then really accept screen percentages as positions.

As well i keep internal screen button positions. Such that as the users positional floats never need to change as the buttons actual private integer screen drawing positions and width heights can be changed by the floating positions * the current game resolutions width height.

This is triggered thru the WindowClientSizedChange event when it fires. It then activates my pre-registered buttonmanager class’s OnResize method. Which then simply loops all the created items and scales there actual screen positions.

The drawback of that is then what relates to what if you add a button to a panel. While the panel relates the the whole screen, The button relates to the panel so then you need a extra step when you resize the screen. As it becomes a translation as well so you have to make it clear to the user that this is the case, or automate the buttons placement, or sliders, textbox’s ect… This also means that clicking on things such as text boxes must check for the actual screen positions to see if they are clicked or active as well.

This is amazing! The UI components have been my biggest stumbling block thus far. And all the other UI libraries I have integrated with have not left me feeling very happy with the product. This is definitely the best looking, most complete, UI library I have seen, without over complicating things. 10/10!

Unfortunately no… I opened a thread about it here if you are interested: XML Content - use custom types from the same project and not external dll

tl;dr when you compile you also build the content, and the content must have the compiled data structures for the XML serialization. So there’s a chicken and egg case here.

If the main issue with your own UI is the textures, you can just copy them from GeonBit.UI since they are all CC and other permissive licenses (all the texture sources are listed in credits, you can check them out). I already did the tedious job of breaking everything into files.

But if its something deeper than just textures, maybe GeonBit is the answer :wink:

Very interesting points, thank you! It shouldn’t be too hard to turn everything from absolute (pixels) into relative (percents), I just need to edit the function that calculate destination rectangles in the base entity class + few other places.

However, since I personally prefer pixel-based sizes for my own projects + the anchors system should solve most of the positioning issues, I don’t want to change GeonBit to completely use percents instead of pixels.

For that reason I think your suggestion would be a really good feature in the form of a configurable flag (for example a public bool in the global Interface Manager) that can shift units from pixels to percents and vice-versa.

To keep it simple I will not allow mixing, either everything is in pixels or percents.

Do you think its an important feature I should add? Will it be useful for you?

Thank you very much :slight_smile:

And kosmonautgames I haven’t forgot about you, yesterday I solved part of the dragging issue but still got few things to check…

Actually willmotil might be a good candidate to maybe help with the garbage.
I’ve tried to change the dictionaries to work with stringbuilder instead of string, and I’ve added helper functions for the enum to string conversion, but there are troubles with stringbuilder being keys in dictionaries it seems

Maybe I’ll try something else if I have time

Bummer. Well another approach is to ditch the strings completely and use enums instead (dictionary per state containing dictionaries per property). I sort of like the flexibility and simplicity of strings as keys for styling but at the bottom line if you’re adding new styles you are editing the code anyway, so you might as well just update an enum too.

Anyway regarding the dragging and positioning problem I think its fixed (merged with the master branch), I tried to reproduce the way you did and it seems ok, but if I misunderstood you and the bug still there let me know.

Thanks :slight_smile:

No it’s only important i suppose if it solves a problem that otherwise can’t be solved.
Or it makes using the project faster or simpler to use.

It is my code is huge and ugly and i just cant make it look pretty. Every time i rewrote it i just wanted to delete the whole class when i was done i haven’t tried in a long time and i was just about to when i saw your post. Maybe i wasn’t using the right pattern or I just suck at doing ui i guess.

Ill check it out.

Edit: much nicer then my messy class.

Might be boxing stuff it shouldn’t. Might be on the MonoGame side. Could be totally unrelated. SrtingBuilder is a class so it should work with a enum, though enums are special things in c#.

Not sure if any of this matters much unless your typing in a textbox or something which is probably no big deal anyways.

The bigger deal stuff is more monogame call related things, like when you have a fps or scroller and holding a key down to move creates garbage and your always holding a key down lol wasd ackk.

Then there is numerical to text displayed stuff which has special considerations for dynamically changing real time numerical displayed text, which you already know about.

Anyways just glancing it over… if you really wanted to not generate text garbage when typing

In InputHelper.cs → NewKeyTextInput → string lastCharPressedStr = key.ToString();
That’s probably generating garbage for every keystroke. There is probably other stuff.

In paragraphs.cs → WrapText is using string concatenation that always creates garbage that gets collected but only when it is actually called. like if the user is typing in a text box depending on how its displayed that could generate a ton of it ect…

Basically you would Probably would have to replace everything that uses strings to stringbuilder.
The above call alone could fill a post on how to do it with a stringbuilder though it would be easier. i dunno why that was the accepted suggestion on stackoverflow.

Provided MonoGame calls themselves are not generating garbage just pressing a key. Aside from boxing unboxing no no’s. Text is pretty much the single most garbage generating thing in most peoples apps and the hardest to eliminate collections caused by them but usually the severity of this depends on if its dynamically being altered and updated in real time or not.

So the idea is to not do it if and when its avoidable or reduce it to only updating strings when necessary.

Do users of a app created with this need to install the fonts ?

I would typically just allow the font to be set that is to be used in load content to the ui or a reference to it that the programmer adds to his project.

That could be a problem if users must install the fonts. Has this been tested ?

Anyways i cant get it to execute getting errors ill try again later.

Users of the application do not need the font, no. It is converted to a texture atlas when imported through the content pipeline as spritefont.

Only people who want to modify source code and recompile will need the font installed

Very good points, thanks! I will try it out next time I’ll work on it (unless one of you already have it implemented and want to pull request?)

@kosmonautgames is right you only need the fonts if you want to compile the code. But anyway if you want to build a MonoGame extension or a game engine or whatever else that users will need to compile and you don’t want to distribute fonts you can always change the default themes to use one of the default monospace fonts that come with Windows / Unix (fonts should be monospace).

Are you still working on this and updating problems?

Yeah recently I made a pretty big update and also added a NuGet package.

Are you thinking about using GeonBit.UI?

Well , I look at the 5 minute instal video and I tried for an hour and failed to use it. I think you need a foolproff tutorial for it.

There are people like me who does not know the difference between building and running a project or between debug and relase.

I will try nuget install but i think i will succed to fail at that too.

I was just rereading this i don’t know if you ever saw my wrapped StringBuilder class i posted a while back.

I should have posted it before. It might be of some use to you, in case your still dealing with garbage, even under stringbuilder.

In most of my screen shots i post there is basically no garbage. Primarily because of two things.

  1. MonoGame itself is awesome and generates little to no collectable garbage except in a select few edge cases.
  2. The below class… While it doesn’t address any problem with monogame, it instead address a problem with c# numerics tostring or tochar conversions creating garbage.

This is a thin wrapper on stringbuilder that basically augments stringbuilder to prevent garbage created by numeric to text conversion by c#. It simply does it instead of letting c# do it and create garbage. A couple extra temp stringbuilders handle edge case copying by holding onto references until they can be cleared during certain copy operations.

All the methods are proven so far at least in my tests, except the vector versions that are new and untested, so i commented them out. Some edge cases that are commented “//just append it” are not handled but they are pretty rare.

using System;
using System.Text;
using System.Collections.Generic;
using Microsoft.Xna.Framework;

namespace MyText
{
    public sealed class MgStringBuilder
    {
        private static char decimalseperator = '.';
        private static char minus = '-';
        private static char plus = '+';

        private static StringBuilder last;
        private StringBuilder sb;
        public StringBuilder StringBuilder
        {
            get { return sb; }
            private set { sb = value; last = sb; }
        }

        public int Length
        {
            get { return StringBuilder.Length; }
            set { StringBuilder.Length = value; }
        }
        public int Capacity
        {
            get { return StringBuilder.Capacity; }
            set { StringBuilder.Capacity = value; }
        }
        public void Clear()
        {
            Length = 0;
            sb.Length = 0;
        }

        // constructors
        public MgStringBuilder()
        {
            StringBuilder = StringBuilder;
            if (sb == null) { sb = new StringBuilder(); }
            if (last == null) { last = new StringBuilder(); }
        }
        public MgStringBuilder(int capacity)
        {
            StringBuilder = new StringBuilder(capacity);
            if (sb == null) { sb = new StringBuilder(); }
            if (last == null) { last = new StringBuilder(); }
        }
        public MgStringBuilder(StringBuilder sb)
        {
            StringBuilder = sb;
            if (sb == null) { sb = new StringBuilder(); }
            if (last == null) { last = new StringBuilder(); }
        }
        public MgStringBuilder(string s)
        {
            StringBuilder = new StringBuilder(s);
            if (sb == null) { sb = new StringBuilder(); }
            if (last == null) { last = new StringBuilder(); }
        }

        public static void CheckSeperator()
        {
            decimalseperator = Convert.ToChar(System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator);
        }

        // operators
        public static implicit operator MgStringBuilder(StringBuilder sb)
        {
            return new MgStringBuilder(sb);
        }
        //public static implicit operator StringBuilder(MgStringBuilder msb)
        //{
        //    StringBuilder b = new StringBuilder().Append(msb.sb);
        //    return b;
        //}
        public static implicit operator StringBuilder(MgStringBuilder msb)
        {
            return msb.StringBuilder;
        }

        public static MgStringBuilder operator +(MgStringBuilder sbm, MgStringBuilder s)
        {
            sbm.StringBuilder.Append(s);
            return sbm;
        }

        public void AppendAt(int index, StringBuilder s)
        {
            int len = this.StringBuilder.Length;
            int reqcapacity = (index + s.Length + 1) - this.StringBuilder.Capacity;
            if (reqcapacity > 0)
                this.StringBuilder.Capacity += reqcapacity;

            for (int i = 0; i < s.Length; i++)
            {
                this.StringBuilder[i + index] = (char)(s[i]);
            }
        }
        public void Append(StringBuilder s)
        {
            int len = this.StringBuilder.Length;
            int reqcapacity = (s.Length + len) - this.StringBuilder.Capacity;
            //int reqcapacity = (s.Length + len +1) - this.StringBuilder.Capacity;
            if (reqcapacity > 0)
                this.StringBuilder.Capacity += reqcapacity;

            this.StringBuilder.Length = len + s.Length;
            for(int i = 0;i< s.Length;i++)
            {
                this.StringBuilder[i + len] = (char)(s[i]);
            }
        }
        public void Append(string s)
        {
            this.StringBuilder.Append(s);
        }
        public void Append(bool value)
        {
            this.StringBuilder.Append(value);
        }
        public void Append(byte value)
        {
            // basics
            int num = value;
            if (num == 0)
            {
                sb.Append('0');
                return;
            }
            int place = 100;
            if (num >= place * 10)
            {
                // just append it
                sb.Append(num);
                return;
            }
            // part 1 pull integer digits
            bool addzeros = false;
            while (place > 0)
            {
                if (num >= place)
                {
                    addzeros = true;
                    int modulator = place * 10;
                    int val = num % modulator;
                    int dc = val / place;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                place = (int)(place * .1);
            }
        }
        public void Append(short value)
        {
            int num = value;
            // basics
            if (num < 0)
            {
                // Negative.
                sb.Append(minus);
                num = -num;
            }
            if (value == 0)
            {
                sb.Append('0');
                return;
            }

            int place = 10000;
            if (num >= place * 10)
            {
                // just append it, if its this big, this isn't a science calculator, its a edge case.
                sb.Append(num);
                return;
            }
            // part 1 pull integer digits
            bool addzeros = false;
            while (place > 0)
            {
                if (num >= place)
                {
                    addzeros = true;
                    int modulator = place * 10;
                    int val = num % modulator;
                    int dc = val / place;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                place = (int)(place * .1);
            }
        }
        public void Append(int value)
        {
            // basics
            if (value < 0)
            {
                // Negative.
                sb.Append(minus);
                value = -value;
            }
            if (value == 0)
            {
                sb.Append('0');
                return;
            }

            int place = 1000000000;
            if (value >= place * 10)
            {
                // just append it
                sb.Append(value);
                return;
            }
            // part 1 pull integer digits
            int n = (int)(value);
            bool addzeros = false;
            while (place > 0)
            {
                if (n >= place)
                {
                    addzeros = true;
                    int modulator = place * 10;
                    int val = n % modulator;
                    int dc = val / place;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                place = (int)(place * .1);
            }
        }
        public void Append(long value)
        {
            // basics
            if (value < 0)
            {
                // Negative.
                sb.Append(minus);
                value = -value;
            }
            if (value == 0)
            {
                sb.Append('0');
                return;
            }

            long place = 10000000000000000L;
            if (value >= place * 10)
            {
                // just append it,
                sb.Append(value);
                return;
            }
            // part 1 pull integer digits
            long n = (long)(value);
            bool addzeros = false;
            while (place > 0)
            {
                if (n >= place)
                {
                    addzeros = true;
                    long modulator = place * 10L;
                    long val = n % modulator;
                    long dc = val / place;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                place = (long)(place * .1);
            }
        }
        public void Append(float value)
        {
            // basics
            if (value < 0)
            {
                // Negative.
                sb.Append(minus);
                value = -value;
            }
            if (value == 0)
            {
                sb.Append('0');
                return;
            }

            int place = 100000000;
            if (value >= place * 10)
            {
                // just append it, if its this big its a edge case.
                sb.Append(value);
                return;
            }
            // part 1 pull integer digits
            int n = (int)(value);
            bool addzeros = false;
            while (place > 0)
            {
                if (n >= place)
                {
                    addzeros = true;
                    int modulator = place * 10;
                    int val = n % modulator;
                    int dc = val / place;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                place = (int)(place * .1);
            }

            // ok lets try again
            // nd > 0 wont let us see the decimal
            float nd = value - (float)(n);
            if (nd > -1 && nd < 1)
            {
                sb.Append(decimalseperator);
            }
            addzeros = true;
            //nd = value;
            float placed = .1f;
            while (placed > 0.00000001)
            {
                if (nd > placed)
                {
                    float modulator = placed * 10;
                    float val = nd % modulator;
                    float dc = val / placed;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                placed = placed * .1f;
            }
        }
        public void Append(double number)
        {

            // basics
            if (number < 0)
            {
                // Negative.
                sb.Append(minus);
                number = -number;
            }
            if (number == 0)
            {
                sb.Append('0');
                return;
            }

            long place = 10000000000000000L;
            if (number >= place * 10)
            {
                // just append it, if its this big its a edge case.
                sb.Append(number);
                return;
            }
            // part 1 pull integer digits
            long n = (long)(number);
            bool addzeros = false;
            while (place > 0)
            {
                if (n >= place)
                {
                    addzeros = true;
                    long modulator = place * 10L;
                    long val = n % modulator;
                    long dc = val / place;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                place = (long)(place * .1);
            }

            // the decimal part
            double nd = number - (double)(n);
            if (nd > 0 && nd < 1)
            {
                sb.Append(decimalseperator);
            }
            addzeros = true;
            //nd = number;
            double placed = .1;
            while (placed > 0.0000000000001)
            {
                if (nd > placed)
                {
                    double modulator = placed * 10;
                    double val = nd % modulator;
                    double dc = val / placed;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                placed = placed * .1;
            }
        }
        //public void Append(Vector2 value)
        //{
        //    Append("(");
        //    Append(value.X);
        //    Append(",");
        //    Append(value.Y);
        //    Append(")");
        //}
        //public void Append(Vector3 value)
        //{
        //    Append("(");
        //    Append(value.X);
        //    Append(",");
        //    Append(value.Y);
        //    Append(",");
        //    Append(value.Z);
        //    Append(")");
        //}
        //public void Append(Vector4 value)
        //{
        //    Append("(");
        //    Append(value.X);
        //    Append(",");
        //    Append(value.Y);
        //    Append(",");
        //    Append(value.Z);
        //    Append(",");
        //    Append(value.W);
        //    Append(")");
        //}
        public void Append(Color value)
        {
            Append("(");
            Append(value.R);
            Append(",");
            Append(value.G);
            Append(",");
            Append(value.B);
            Append(",");
            Append(value.A);
            Append(")");
        }

        public void AppendTrim(float value)
        {
            // basics
            if (value < 0)
            {
                // Negative.
                sb.Append(minus);
                value = -value;
            }
            if (value == 0)
            {
                sb.Append('0');
                return;
            }

            int place = 100000000;
            if (value >= place * 10)
            {
                // just append it, if its this big its a edge case.
                sb.Append(value);
                return;
            }
            // part 1 pull integer digits
            int n = (int)(value);
            bool addzeros = false;
            while (place > 0)
            {
                if (n >= place)
                {
                    addzeros = true;
                    int modulator = place * 10;
                    int val = n % modulator;
                    int dc = val / place;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                place = (int)(place * .1);
            }

            // ok lets try again
            float nd = value - (float)(n);
            sb.Append(decimalseperator);
            addzeros = true;
            //nd = value;
            float placed = .1f;
            while (placed > 0.001)
            {
                if (nd > placed)
                {
                    float modulator = placed * 10;
                    float val = nd % modulator;
                    float dc = val / placed;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                placed = placed * .1f;
            }
        }
        public void AppendTrim(double number)
        {
            // basics
            if (number < 0)
            {
                // Negative.
                sb.Append(minus);
                number = -number;
            }
            if (number == 0)
            {
                sb.Append('0');
                return;
            }
            long place = 10000000000000000L;
            if (number >= place * 10)
            {
                // just append it, if its this big its a edge case.
                sb.Append(number);
                return;
            }
            // part 1 pull integer digits
            long n = (long)(number);
            bool addzeros = false;
            while (place > 0)
            {
                if (n >= place)
                {
                    addzeros = true;
                    long modulator = place * 10L;
                    long val = n % modulator;
                    long dc = val / place;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                place = (long)(place * .1);
            }

            // ok lets try again
            double nd = number - (double)(n);
            sb.Append(decimalseperator);
            addzeros = true;
            //nd = number;
            double placed = .1;
            while (placed > 0.001)
            {
                if (nd > placed)
                {
                    double modulator = placed * 10;
                    double val = nd % modulator;
                    double dc = val / placed;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                placed = placed * .1;
            }
        }
        //public void AppendTrim(Vector2 value)
        //{
        //    Append("(");
        //    AppendTrim(value.X);
        //    Append(",");
        //    AppendTrim(value.Y);
        //    Append(")");
        //}
        //public void AppendTrim(Vector3 value)
        //{
        //    Append("(");
        //    AppendTrim(value.X);
        //    Append(",");
        //    AppendTrim(value.Y);
        //    Append(",");
        //    AppendTrim(value.Z);
        //    Append(")");
        //}
        //public void AppendTrim(Vector4 value)
        //{
        //    Append("(");
        //    AppendTrim(value.X);
        //    Append(",");
        //    AppendTrim(value.Y);
        //    Append(",");
        //    AppendTrim(value.Z);
        //    Append(",");
        //    AppendTrim(value.W);
        //    Append(")");
        //}

        public void AppendLine(StringBuilder s)
        {
            sb.AppendLine();
            Append(s);
        }
        public void AppendLine(string s)
        {
            sb.AppendLine();
            sb.Append(s);
        }
        public void AppendLine()
        {
            sb.AppendLine();
        }

        public void Insert(int index, StringBuilder s)
        {
            this.StringBuilder.Insert(index, s);
        }
        public void Remove(int index, int length)
        {
            this.StringBuilder.Remove(index, length);
        }

        public char[] ToCharArray()
        {
            char[] a = new char[sb.Length];
            sb.CopyTo(0, a, 0, sb.Length);
            return a;
        }
        public override string ToString()
        {
            return sb.ToString();
        }
    }
}
2 Likes

Sorry to hear that, I hope the NuGet will work out for you :slight_smile:
If you have any specific problems don’t hesitate to ask.

Cool stuff!

I think I solved most of the GC related issues in GeonBit.UI (Its still not 100% optimal but much better than before). I’m not doing any numeric-to-string conversions there (if I remember correctly) but your string builder might help me in another project I’m working on.

Do you have it on a git repo I can check out or should I just grab it from here?
Thanks :slight_smile:

No repo its just a stand alone class. Works as is… copy paste.
(change the namespace and its good to go).

sorry for the wall of text to explain it.

I didn’t think it was good enough yet for a repo, It looks ugly to me and i haven’t given a lot of thought to localization concerns i haven’t really cleaned it up since i made it, it really does need some more work, but…honestly it works very well for eliminating garbage.

All numeric conversion you do in c# creates garbage that gets collected.
The character conversion in c# for numerics to characters itself creates collections.
I actually posted this problem on user voice like a year ago.

this (1).ToString(); or (char)(1); is going to get garbage collected.
Including anything like the framerate positions ect…
score.ToString() no matter if it goes to StringBuilder string char whatever.
Its going to make the gc go “chomp chomp chomp boom collect”.
It’s easy enough to test / prove that yourself.

The class just gives you a way to never have to do that when using stringbuilder.

Basically its just a wrapper around StringBuilder but lets you append numbers separately so that they wont create garbage. I basically calculate the character itself by modulus for each decimal place from the value directly. mgSb.Append(1); won’t get collected were sb.Append(1); will.
I put just enough into it so it basically functions like stringbuilder on its own but you can pull out the stringbuilder by like StringBuilder sb = mgSb;.

I stuck the operator overloads in that i thought were safe. Which is enough to just pass it into any monogame method directly that takes a string builder so basically in most cases you can use it in place of stringbuilder.

I suppose you could use it with stringbuilder like…

sb.Append( mgsb.Append( frameRate ) );
but you can append text to it too so that’s kind of pointless.
You can if you like just rip out the conversion functions from it and make numeric converters for sb directly but it’s going to end up looking like the above anyways.

I basically just replace all my own string builder calls with the wrapper as there is no point in casting it back to a stringbuilder until i pass it to a monogame method and that is done implicitly via the operator.

like spriteBatch.DrawString( mgSbInstance, pos, color, ect…

1 Like