GeonBit.UI: UI extension for MonoGame

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

This UI looks very nice. I have been working on a UI for my own game as well. May I ask how you load your objects? I don’t have an editor yet and putting everything together in code leads to ugly mile long functions. That’s why I turned to XAML to load (or save) my layouts. That works so well that I also based the UI theme and even complete scenes for the 3D engine on XAML scripts embedded into the assembly.

Thanks :slight_smile: Unfortunately no, GeonBit.UI don’t support XAML atm. If you happen to write something like this (XAML for GeonBit.UI) let me know I’d love to integrate it.

I’m quite busy with my own UI, but what if I tell you that you only need to add about 10 lines of code in two static functions. That’s all. One function can load any valid XAML, the other one takes any serializable object and serializes it to XAML. You can use the second one to generate your first XAML files from the objects you have initialized the conventional way up to now. After that you can throw this code away and just load your objects from this XAML.

Look here: https://www.codeproject.com/Tips/188133/XamlServices-class-in-Net

The code I posted there works for files. That’s not practical. Change the loading method to use embedded resources and embed those XAML files into your assembly. You can’t save to an embedded resource, but that’s ok. You only need the save method as a crutch to get the first XAML files.

Hi Geon,

I’ve updated to the newest version (2.0.1) and the installation went fine with NuGet. I only had to change some stuff like UserInterface now being global, SCALE to GlobalScaleetc. and it ran with no problems.

I think some scale stuff is now handled differently, some buttons are now smaller than others, but I haven’t looked into why yet. Also some text now “runs away” at the beginning, but I’m sure that’s a mistake on my part (both things).

Either way, I think it’s easy to use and works fine.

One little thing: You use enum for almost everything, but when using UserInterface.Initialize(… theme) there is still a string (default “hd”), but without enums it’s hard to know what other themes there are if not looking at the documentation.

Also the shader files for HD and lowres are the same, maybe only having them once might prevent future bugs and save a little space and compilation time.

However, even though I can’t look at the files themselves, since they are “hidden” behind the compiled .dll now by default, i have huge troubles with garbage generation.

I have merely 3 buttons and 3 sliders, but the garbage generated is enormous, and that’s mainly due to using strings to read out configurations.

The last time I checked there was a lot of enums being stringed together and you then compare the string values and look for certain parts to continue.

If that is still the case, and performance profiling suggests it is (most garbage is created somewhere in GetStyleProperty), you could look into encoding these enum combinations differently, maybe in a struct or mini-class that combines several input parameters. Since enums are essentially ints you could also use some decimal or bit encoding / decoding.

1 Like

Glad to hear it went relatively smooth :slight_smile:

Could you elaborate more on the running text thing? Like, the text starts at wrong position and “jump” to its right place on first frame? If so with which entities? (had this problem in the past I though it was solved).

Also the sizing is a little odd, I did change the default themes font which might change texts a little, but not buttons… strange.

It was to allow adding new themes without changing the code, eg you can create your own folder of theme and just use it by name. However, point taken, maybe I’ll add a const for the two built-in themes or an enum and have function override.

That’s absolutely true, but I wanted to leave more freedom to custom themes. For example, maybe a theme where disabled entities are not greyed out but have different effect.

That’s very strange, I checked the demo again, with lots of entities etc, and the memory is constant with occasional GC calls:

I did found a serious memory leak with render targets which I just solved, but you’re not using them right?

There were other things as well last time we talked about it, most of them are solved. I added optimization against enums.toString() and some other places, but I have to admit I see no serious impact. :confused:

Can you elaborate more on your suggestion with the int encoding? Do you mean using a 2d array with indexes or something different?

Thanks for the useful (as always :slight_smile: ) feedback!

I will soon release a minor update that address some of the issues above, although I’m concerned that I didn’t see the GC madness you mentioned…

I know it’s not super helpful, but maybe you can spot a problem here:

I do nothing else with the UI. I commented out the text updates to make sure they are not part of the garbage.

All my toggles run away. Some buttons are larger than others, because I change their scale, I think previously it only changed text scale, so that is a mistake on me then.

What it looked like previously (same code) https://www.youtube.com/watch?v=jzW7Du0JfSk

Here is the code:

public void Initialize(GraphicsDevice graphics)
        {
            spriteBatch = new SpriteBatch(graphics);

            UserInterface.GlobalScale = 0.7f;
            UserInterface.ShowCursor = false;

            panel = new Panel(new Vector2(500,600), PanelSkin.Simple, Anchor.TopRight);
            panel.Draggable = false;
            UserInterface.AddEntity(panel);



            panel.AddChild(new Paragraph("Test UI"));
            panel.SetPosition(Anchor.TopRight, new Vector2(0,0));

            panel.AddChild(fps = new Paragraph("fps :"+GameStats.fps_avg));

            panel.AddChild(toggleShader = new CheckBox("Update Shader", Anchor.Auto, null, null, true));
            toggleShader.Scale = 0.5f;
            toggleShader.OnValueChange = (Entity entity) =>
            {
                CheckBox cb = (CheckBox) entity;
                GameSettings.g_UpdateShading = cb.Checked;
            };

            CheckBox seamFixToggle;
            panel.AddChild(seamFixToggle = new CheckBox("Fix Seams", Anchor.Auto, null, null, true));

            panel.AddChild(toggleShader = new CheckBox("Rotate Model", Anchor.Auto, null, null, true));
            toggleShader.Scale = 0.5f;
            toggleShader.OnValueChange = (Entity entity) =>
            {
                CheckBox cb = (CheckBox)entity;
                GameSettings.s_rotateModel = cb.Checked;
            };

            panel.AddChild(new LineSpace(1));
            
            Paragraph sliderValue = new Paragraph("Texture resolution: 512");
            panel.AddChild(sliderValue);
            

            Slider slider = new Slider(1, 12, SliderSkin.Default);
            slider.Value = 9;
            slider.StepsCount = 11;
            slider.OnValueChange = (Entity entity) =>
            {
                Slider slid = (Slider) entity;
                GameSettings.g_texResolution = (int) Math.Pow(2, slid.Value);
                //sliderValue.Text = "Texture resolution: " + GameSettings.g_texResolution;
            };
            panel.AddChild(slider);

            Paragraph environmentMap = new Paragraph("ambient intensity: 1.8");
            panel.AddChild(environmentMap);
            
            Slider envIntensitySlider = new Slider(0, 400, SliderSkin.Default);
            envIntensitySlider.StepsCount = 400;
            envIntensitySlider.Value = 180;
            envIntensitySlider.OnValueChange = (Entity entity) =>
            {
                Slider slid = (Slider)entity;
                GameSettings.g_EnvironmentIntensity = slid.Value/100.0f;
                //environmentMap.Text = "ambient intensity: " + GameSettings.g_EnvironmentIntensity;
            };
            panel.AddChild(envIntensitySlider);

            Paragraph seamFixSteps = new Paragraph("seam dilate pixels: 1");
            panel.AddChild(seamFixSteps);

            Slider seamFixStepsSlider = new Slider(0, 6, SliderSkin.Default);
            seamFixStepsSlider.StepsCount = 6;
            seamFixStepsSlider.Value = 1;
            seamFixStepsSlider.OnValueChange = (Entity entity) =>
            {
                Slider slid = (Slider)entity;
                GameSettings.g_SeamSearchSteps = slid.Value;
                //seamFixSteps.Text = "seam dilate pixels: " + GameSettings.g_SeamSearchSteps;
            };
            panel.AddChild(seamFixStepsSlider);


            seamFixToggle.OnValueChange = (Entity entity) =>
            {
                CheckBox cb = (CheckBox)entity;
                GameSettings.g_FixSeams = cb.Checked;
                seamFixStepsSlider.Disabled = !cb.Checked;
            };

        }

Oh wow when you said running text you actually meant running text :joy: Never seen it happen before its so strange… And I use a lot of checkboxes.

Thank you for the info, I’ll take a look at it as soon as I’ll have the time (I’m at work atm) and hopefully will figure out the source of this checkbox shenanigans.

So I finally had time to work on this… Unfortunately, try as I might, I co
uldn’t reproduce the running away paragraphs.

I took your code and dumped it inside the GeonBit.UI example project:

`#region File Description
//-----------------------------------------------------------------------------
// This program show GeonBit.UI examples and usage.
//
// GeonBit.UI is an export of the UI system used for GeonBit (an open source 
// game engine in MonoGame) and is free to use under the MIT license.
//
// To learn more about GeonBit.UI, you can visit the git repo:
// https://github.com/RonenNess/GeonBit.UI
//
// Or exaplore the different README files scattered in the solution directory. 
//
// Author: Ronen Ness.
// Since: 2016.
//-----------------------------------------------------------------------------
#endregion

// using MonoGame and basic system stuff
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System.Collections.Generic;

// using GeonBit UI elements
using GeonBit.UI.Entities;
using GeonBit.UI.Entities.TextValidators;
using GeonBit.UI.DataTypes;

namespace GeonBit.UI.Example
{
    /// <summary>
    /// GeonBit.UI.Example is just an example code. Everything here is not a part of the GeonBit.UI framework, but merely an example of how to use it.
    /// </summary>
    [System.Runtime.CompilerServices.CompilerGenerated]
    class NamespaceDoc
    {
    }

    /// <summary>
    /// This is the main 'Game' instance for your game.
    /// </summary>
    public class GeonBitUI_Examples : Game
    {
        // graphics and spritebatch
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        // all the example panels (screens)
        List<Panel> panels = new List<Panel>();

        /// <summary>
        /// 
        /// </summary>
      public  Button nextExampleButton;

        /// <summary>
        /// 
        /// </summary>
        public  Button previousExampleButton;

        // current example shown
        int currExample = 0;

        /// <summary>
        /// Create the game instance.
        /// </summary>
        public GeonBitUI_Examples()
        {
            // init graphics device manager and set content root
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Initialize the main application.
        /// </summary>
        protected override void Initialize()
        {         
            // create and init the UI manager
            UserInterface.Initialize(Content, BuiltinThemes.hd);
            UserInterface.UseRenderTarget = true;

            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // make the window fullscreen (but still with border and top control bar)
       /*     int _ScreenWidth = graphics.GraphicsDevice.Adapter.CurrentDisplayMode.Width;
            int _ScreenHeight = graphics.GraphicsDevice.Adapter.CurrentDisplayMode.Height;
            graphics.PreferredBackBufferWidth = (int)_ScreenWidth;
            graphics.PreferredBackBufferHeight = (int)_ScreenHeight;
            graphics.IsFullScreen = true;
            graphics.ApplyChanges();*/
            
            // init ui and examples
            InitExamplesAndUI();
        }
        
        /// <summary>
        /// Create the top bar with next / prev buttons etc, and init all UI example panels.
        /// </summary>        
        protected void InitExamplesAndUI()
        {

            UserInterface.GlobalScale = 0.7f;
            UserInterface.ShowCursor = false;

            Panel panel = new Panel(new Vector2(500, 600), PanelSkin.Simple, Anchor.TopRight);
            panel.Draggable = false;
            UserInterface.AddEntity(panel);


            CheckBox toggleShader = new CheckBox("aaa");


            panel.AddChild(new Paragraph("Test UI"));
            panel.SetPosition(Anchor.TopRight, new Vector2(0, 0));

            Paragraph fps;
            panel.AddChild(fps = new Paragraph("fps :"));

            panel.AddChild(toggleShader = new CheckBox("Update Shader", Anchor.Auto, null, null, true));
            toggleShader.Scale = 0.5f;
            toggleShader.OnValueChange = (Entity entity) =>
            {
                CheckBox cb = (CheckBox)entity;
            };

            CheckBox seamFixToggle;
            panel.AddChild(seamFixToggle = new CheckBox("Fix Seams", Anchor.Auto, null, null, true));

            panel.AddChild(toggleShader = new CheckBox("Rotate Model", Anchor.Auto, null, null, true));
            toggleShader.Scale = 0.5f;
            toggleShader.OnValueChange = (Entity entity) =>
            {
                CheckBox cb = (CheckBox)entity;
            };

            panel.AddChild(new LineSpace(1));

            Paragraph sliderValue = new Paragraph("Texture resolution: 512");
            panel.AddChild(sliderValue);


            Slider slider = new Slider(1, 12, SliderSkin.Default);
            slider.Value = 9;
            slider.StepsCount = 11;
            slider.OnValueChange = (Entity entity) =>
            {
                Slider slid = (Slider)entity;
                //sliderValue.Text = "Texture resolution: " + GameSettings.g_texResolution;
            };
            panel.AddChild(slider);

            Paragraph environmentMap = new Paragraph("ambient intensity: 1.8");
            panel.AddChild(environmentMap);

            Slider envIntensitySlider = new Slider(0, 400, SliderSkin.Default);
            envIntensitySlider.StepsCount = 400;
            envIntensitySlider.Value = 180;
            envIntensitySlider.OnValueChange = (Entity entity) =>
            {
                Slider slid = (Slider)entity;
                //environmentMap.Text = "ambient intensity: " + GameSettings.g_EnvironmentIntensity;
            };
            panel.AddChild(envIntensitySlider);

            Paragraph seamFixSteps = new Paragraph("seam dilate pixels: 1");
            panel.AddChild(seamFixSteps);

            Slider seamFixStepsSlider = new Slider(0, 6, SliderSkin.Default);
            seamFixStepsSlider.StepsCount = 6;
            seamFixStepsSlider.Value = 1;
            seamFixStepsSlider.OnValueChange = (Entity entity) =>
            {
                Slider slid = (Slider)entity;
                //seamFixSteps.Text = "seam dilate pixels: " + GameSettings.g_SeamSearchSteps;
            };
            panel.AddChild(seamFixStepsSlider);


            seamFixToggle.OnValueChange = (Entity entity) =>
            {
                CheckBox cb = (CheckBox)entity;
                seamFixStepsSlider.Disabled = !cb.Checked;
            };
        }

        /// <summary>
        /// Show next UI example.
        /// </summary>
        public void NextExample()
        {
            currExample++;
            UpdateAfterExapmleChange();
        }

        /// <summary>
        /// Show previous UI example.
        /// </summary>
        public void PreviousExample()
        {
            currExample--;
            UpdateAfterExapmleChange();
        }

        /// <summary>
        /// Called after we change current example index, to hide all examples
        /// except for the currently active example + disable prev / next buttons if
        /// needed (if first or last example).
        /// </summary>
        protected void UpdateAfterExapmleChange()
        {
            // hide all panels and show current example panel
            foreach (Panel panel in panels)
            {
                panel.Visible = false;
            }
            panels[currExample].Visible = true;

            // disable / enable next and previous buttons
            nextExampleButton.Disabled = currExample == panels.Count-1;
            previousExampleButton.Disabled = currExample == 0;
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // make sure window is focused
            if (!IsActive)
                return;

            // exit on escape
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            // update UI
            UserInterface.Update(gameTime);

            // call base update
            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            // draw ui
            UserInterface.Draw(spriteBatch);

            // clear buffer
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // finalize ui rendering
            UserInterface.DrawMainRenderTarget(spriteBatch);

            // call base draw function
            base.Draw(gameTime);
        }
    }
}

`
And it looks like it should, without running text:

I tried random stuff like resizing window etc, still not happening :confused:

Is your code public perhaps, somewhere I can view it? Or maybe you got more ideas I can try?

Thanks!

can you reproduce the garbage generation?

nope… only what I showed in previous post (garbage collection every ~15 seconds and looks pretty flat.

Hello Geon,

How easy is it to change the textures and the look of the UI when using GeonBit.UI?

I really like what I see here, it looks very polished, but I’m planning on making more of a dark/gritty game, and the feeling I get from the screenshots I see here is just too happy… it’s not very fitting for what I have in mind.

It’s very easy, all the texture files are just lying there in plain png files (inside GeonBit.UI/themes//textures) , and if you do drastic change to panels borders (for example create panel texture with a much thicker border than default) you can update the textures metadata XML file (in same folder) to make GeonBit use it properly.

Here’s an example of something awesome @matt did with it (hope it’s cool I’m showing it off Matt ;)) https://t.co/t5VR8EwfDy

Ofc he did custom shader work too to get that animated effect, but its a good example of how different UI themes can look.

PS if you edit the theme it’s recommended to rename the theme folder so next time you do nuget update the GeonBit.UI package won’t try to override your files.

1 Like

No problem @GeonBit. All the textures can be easily replaced. Like Ronen said - make a copy of a theme folder and start experimenting with the contents.

1 Like

Awesome! Thanks for the replies. :slight_smile:

Hey guys, I’ve been trying to use this with FNA, I’ve fixed all the compile time errors, but at runtime I’m having problems loading the xml data into the types defined in the DataTypes project (which I compiled linked with FNA btw), to be honest I don’t understand that custom pipeline thing, is it mandatory to convert those xml files into binaries (xnb I assume)? If not, are there any general directions on how to get things working? I thought that I could load the DataTypes.dll in the MonoGame pipeline tool somehow (compiled linked to the MonoGame framework obviously) and it would be able to convert those files and I would be able to use them in FNA, but that doesn’t seem to be the case so I’m extremely confused on how that pipeline thing is supposed to work, even in MonoGame. I’ve been able to use Ruminate and Gwen (by Benpye) but didn’t like them, the former is too basic and the latter has some issues. NuclearWinter I suppose is discontinued since the developers abandoned the game engine they were developing with it, and I’m looking for something open source so EmptyKeys isn’t an option, and also I don’t want “extravagant nonsense” :stuck_out_tongue: so Neoforce for example is a no-go for me. This one seems quite good and I really liked the code, only problem is getting it to work with FNA so if you guys could gimme a hand I’d appreciate. Thanks.

I never used FNA, but in MonoGame you can add references (dlls) to the content manager (click on the root directory and on the settings tab there’s a references button) to make it recognize classes so it can later serialize from XML. Tbh I’m not completely sure why it needs the reference but this is what you’re missing - the reference to DataTypes.dll from the content manager (at least that’s the case with MG).

If there’s an option to add reference to FNA content manager (does it have one?) this is probably what you need to do, if not, another option might be to install MG, compile the XMLs with it and add the compiled files to the content manager of FNA - as a port of xna it should be able to handle compiled content but as I said - I never worked with FNA so I’m guessing here.

Btw if you make it work on FNA I’d love to hear about it and what you needed to do to make it work - might as well update the docs with it :slight_smile:

I still don’t understand how that content pipeline thing works, however, in order to learn that I think a good way would be to see how it works in MonoGame, the problem is, I can’t make it work… when I try to run the solutions in the project (which should run the examples) I get a lot of these:

Processor ‘FontDescriptionProcessor’ had unexpected failure!
Importer ‘XmlImporter’ had unexpected failure!

However, I don’t see any code not even remotely resembling an importer of kinds… am I supposed to provide an importer or something? If I can get it working in MonoGame (and understand the black magic) I think I can make it work with FNA, but so far I’m quite confused :(. I’ve tried both the solution at the root and in the MG36 folder.
I did read the docs and in “Manual Install” I don’t know how to perform the step 2… I’ve read quite a bit of docs, even the CocosSharp, but I still have no idea what is this “pipeline manager”, I don’t think it’s the graphical asset converter tool, but I don’t know if it’s a project or what, maybe it’s something very obvious I’m missing because apparently everybody knows what it is :).

Well, apparently the so called “pipeline manager” is indeed the pipeline tool. I found a way to add references but it’s a bit obscure… I could link it to the DataTypes dlls and it worked fine to convert the types there into binaries, however, there are some files in the folders like styles and fonts which it won’t convert… anyway, after fiddling around I could get it working in FNA (kinda), it doesn’t properly because of those files I couldn’t convert into binaries, but at least it runs. I still can’t find a way to get it working on MonoGame (what’s kinda ironic provided it’s the supported framework :stuck_out_tongue: ).