Jump to content

25 posts in this topic Last Reply

Highlighted Posts

Posted:
Last Online:  
 

Hi,

I've got a basic mod harness up and running, thanks to tutorials by @boformer, and now I'm trying to add draggable UI button.

As far as I can tell, the basic pattern for adding a button looks something like this:

            UIButton myButton = someParent.AddUIComponent<UIButton>();

            myButton.normalBgSprite = "someId";
            myButton.hoveredBgSprite = "someId";
            myButton.pressedBgSprite = "someId";
            myButton.disabledBgSprite = "someId";
            myButton.focusedBgSprite = "someId";
            myButton.disabledTextColor = new Color32(128, 128, 128, 255);
            myButton.relativePosition = new Vector3(5f, 0f);
            myButton.size = new Vector2(50f, 50f)
            myButton.name = "uniqueId";
            myButton.text = "some text";
            myButton.textScale = 1.3f;
            myButton.textVerticalAlignment = UIVerticalAlignment.Middle;
            myButton.textHorizontalAlignment = UIHorizontalAlignment.Center;

My first issue is what should the parent object (someParent) be? Ideally I'd like to make the button draggable so users can put it wherever they want.

The next issue is how to tell the button what sprites to use? I've looked at some other mods, like this, but can't work out what the relationship is between the .png filenames and the identifiers used in the code. If possible, I'd like to have custom background and foreground sprites for the various states.

As for listening to button events, those seem to follow a pattern like so:

            myButton.eventClicked += (component, click) =>
            {
                // change focus state?
                anotherButton.Unfocus();
                myButton.Focus(); // doesn't this auto-unfocus anything else with same parent?
                // then your code here
            };

Again, I've been picking at some other mods so there might be better ways to do things? Essentially the button will toggle a 'build bar' (or whatever correct terminology is for things like the roads menu), so I assume I can use the visibility of the build bar to keep track of UI state?

Share this post


Link to post
Share on other sites
Posted:
Last Online:  
 

The parent depends on when you want your button to be visible. If all the time, you can use this -

UIButton button = UIView.GetAView().AddUIComponent<UIButton>();

If the parent should be some kind of panel, use this:

UIComponent parent = UIView.Find("TSBar");

You can find the name of the desired panel with scene explorer (CTRL+R) in ModTools.

 

About the sprites - If you are using visual studio make sure that the PNG file is imported as Embedded Resource (found it myself the hard way). You can load it from external file as well, but that requires additional loading. If you are still not sure about the file name, decompile the dll afterwards. If you look at the screenshot, the file is not actually called "sprites.png" but has some prefixes.

All in all, I suggest decompiling as many mods as possible in dnSpy to look how they are made.

EmbeddedResource.png

  • Like 1

Share this post


Link to post
Share on other sites
  • Original Poster
  • Posted:
    Last Online:  
     

    Thanks both! I now have a basic button working :D

    Is it possible to use a sprite atlas for background but something else for foreground? I want to use the vanilla sprites for background but have custom foreground. It seems wasteful to duplicate the background sprites :/ I was thinking maybe set the button foreground sprites to null, then add a child UISprite to the UIButton for use as the foreground so it could use different sprite sheet?

    Also, I notice that  there's lots of duplicate properties on the UIBUtton, for example clickSound and m_clickSound - what's the difference and which should I be setting?

    Share this post


    Link to post
    Share on other sites
    Posted:
    Last Online:  
     

    I would recommend you to take a look at the latest version of @Elektrix's mod. It's not on GitHub, so you have to decompile it. He's using an UISprite child component so he can use a different atlas:

    image.png.a824aafe8dbf34a497d5f1ce5dedd469.png

    image.png.9522b3cb13579b5e994f5e182ee9f4d2.png

     

    About clickSound and m_clickSound. I would also recommend you to take a look at the decompiled source of UIComponent.

    m_clickSound is protected so it might be a better idea to use the public clickSound:

    image.png.1d2d2df9af96fc70114523a2b8aec759.png

    • Thanks 1

    Share this post


    Link to post
    Share on other sites
  • Original Poster
  • Posted:
    Last Online:  
     

    Awesome!

    I have a basic C# question now...

    I'm implementing my button as a UI class, inspired by UltimateEyeCandy:

    class UIChameleonButton : UIButton
    {
       // ...
    
       public override void Start()
       {
          base.Start();
          // i'll set up child sprite here
       }
    
       // ...
    
       protected override void OnMouseDown(...)
       {
          // set pressed state of child sprite
         
          base.OnMouseDown(...)
       }
      
       protected override void OnMouseUp(...)
          // etc...  (I guess there's some sort of state change event that would work better)
       }
    }

    Is there a way that I can make the atlas, etc., of the child sprite component accessible from the main button instance, to help decruft external code? So, essentially, adding a property to the UIChameleonButton that acts upon the internal child sprite component when doing get/set? This is probably really basic stuff, but I'm still cargo coding when it comes to C# :/

    Share this post


    Link to post
    Share on other sites
    Posted:
    Last Online:  
     

    Not tested, but this might work for you. With a little trick, you don't need the UISprite ;)

    public class ButtonWithSeparateFgAtlas : UIButton
    {
        private UITextureAtlas m_ForegroundAtlas;
    
        public UITextureAtlas foregroundAtlas
        {
            get
            {
                if (m_ForegroundAtlas == null)
                {
                    m_ForegroundAtlas = GetUIView()?.defaultAtlas;
                }
                return m_ForegroundAtlas;
            }
            set
            {
                if (value != m_Atlas)
                {
                    m_Atlas = value;
                    Invalidate();
                }
            }
        }
    
        protected override UITextureAtlas.SpriteInfo GetForegroundSprite()
        {
            if (foregroundAtlas == null)
            {
                return null;
            }
            if (!isEnabled)
            {
                return foregroundAtlas[disabledFgSprite];
            }
            if (hasFocus)
            {
                return foregroundAtlas[focusedFgSprite] ?? foregroundAtlas[normalFgSprite];
            }
            if (m_IsMouseHovering)
            {
                return foregroundAtlas[hoveredFgSprite] ?? foregroundAtlas[normalFgSprite];
            }
            return foregroundAtlas[normalFgSprite];
        }
    }

     

    Share this post


    Link to post
    Share on other sites
  • Original Poster
  • Posted:
    Last Online:  
     

    After some digging using ILSpy, I've hit a bit of a brick wall.

    I found that the method UIButton.OnRebuilRenderData() is where the background and foreground are rendered. However, on digging in to the UIInteractiveComponent.RenderForeground() there's a reference to the atlas. "Just override the method", I thought... However, in that method there's UISprite.RenderOptions which is an internal struct only available within the assembly.

    I could possibly swap the atlases before calling RenderForeground(), but that seems icky.

    Another option would be to somehow merge the two atlases in to one atlas, but then I'm immediately going to hit issues with different sprite sizes (probably going to hit those issues anyway).

    Any ideas?


      Edited by aubergine18  

    formatting

    Share this post


    Link to post
    Share on other sites
    Posted:
    Last Online:  
     

    Ah well, you are right. I think I lead you into the wrong direction.

    It might be better to just define your own sprite with all foreground and background states.

    Or just use a single UISprite as a child and forget the different states.

    Or nest another UiButton with only a foreground set. The events will travel upwards to the parent button.

    ---

    Alternatively, here is a more or less ugly hack, but I'm not sure if this is the only reference to atlas that must be patched:

            protected override void RenderForeground()
            {
                var bgAtlas = m_Atlas;
                m_Atlas = foregroundAtlas;
                base.RenderForeground();
                m_Atlas = bgAtlas;
            }

     

    Share this post


    Link to post
    Share on other sites
  • Original Poster
  • Posted:
    Last Online:  
     

    I've decided to use a child sprite as it solves a lot of the issues I was having.

    However, now another issue... When ```Start()``` is called on my UI component, it seems other elements of the vanilla game UI aren't yet initialised (as far as I can tell, still digging). So I can't anchor to the other elements at that time because they don't seem to be there yet.

    I've seen another mod use ```Update()``` override to do it's initialisation, I assume by that time all the ```Start()``` methods on everything have been called. However, it seems really crufty way to do it as that will get called every... tick? Even if there's a flag set to minimise code execution, it's still crufty way to deal with initialisation.

    Any ideas how to make my mod detect when all the vanilla stuff is ready without using ```Update()``` ?

    Share this post


    Link to post
    Share on other sites
    Posted:
    Last Online:  
     

    There is one problem related to this: it seems that time to time the UI buttons get despawned. I don't know much about this issue. But when I looked in the Crossings mod, in the Update() method there is a check whether the button still exists and if not, it is respawned. It seems pretty ugly, I don't do it in myself, but I can't tell much about it.

    Share this post


    Link to post
    Share on other sites
    Posted:
    Last Online:  
     
    5 hours ago, aubergine18 said:

    However, now another issue... When ```Start()``` is called on my UI component, it seems other elements of the vanilla game UI aren't yet initialised (as far as I can tell, still digging). So I can't anchor to the other elements at that time because they don't seem to be there yet.

    Hmmm, I need the full code to help you with that. When are you initializing your UI? OnLevelLoaded?

    Share this post


    Link to post
    Share on other sites
  • Original Poster
  • Posted:
    Last Online:  
     

    @boformer Yes, OnLevelLoaded().

    I've been looking at a number of other mods and there's all sorts of hacks in there, such as using Awake() (Hide It), OnGui(), OnEnabled()OnBeforeSimulationFrame() (TMPE), Update() (Hide It), etc.

    @Strdate Regarding button despawns, I assume it's possibly something to do with load-save-load cycle?

    Share this post


    Link to post
    Share on other sites
    Posted:
    Last Online:  
     

    If the "clean" solution doesn't work, use Update() or something like that. Just start with an if-clause and return right away if the anchoring has been done before.

    Share this post


    Link to post
    Share on other sites
    Posted:
    Last Online:  
     
    2 hours ago, aubergine18 said:

    Regarding button despawns, I assume it's possibly something to do with load-save-load cycle?

    Well I am not sure, the buttons are created every time the game is loaded.

    Share this post


    Link to post
    Share on other sites
    Posted:
    Last Online:  
     

    A little extra: It helps to check if the buttons already exists in OnLevelLoaded. That way you can support savegame loads from the pause menu.

    • Like 2

    Share this post


    Link to post
    Share on other sites
  • Original Poster
  • Posted:
    Last Online:  
     

    When savegame is loaded from pause menu, does it trigger an OnLevelUnloading() prior to the OnLevelLoaded()?

    (due to issues with many mods, I always exit to desktop then reload cities.exe before attempting a savegame load)

    Share this post


    Link to post
    Share on other sites
  • Original Poster
  • Posted:
    Last Online:  
     

    BTW, some images of my "chameleon" button in action:

    • When dragged over map, it uses same bg sprites as the info views toggle button (floating.gif).
    • When dragged over TSBar it uses same sprites as road menu toggle button (tsbar.gif).


    (Note: Those aren't animated gifs, just seems to be default image format when saving from windows screen cap tool.)

     

    floating.gif

    tsbar.gif


      Edited by aubergine18  

    formatting

    Share this post


    Link to post
    Share on other sites
    Posted:
    Last Online:  
     
    8 minutes ago, aubergine18 said:

    When savegame is loaded from pause menu, does it trigger an OnLevelUnloading() prior to the OnLevelLoaded()?

    OnLevelUnloading() should be triggered, but on OnReleased(). It makes sense to make it as fail-safe as possible. I think the UI is not destroyed when you load from pause menu, just take that into account.

    Btw, what kind of mod are you working on?

    Share this post


    Link to post
    Share on other sites
  • Original Poster
  • Posted:
    Last Online:  
     

    I'm working on a new UI for TM:PE, but hoping to spawn lots of little libraries that might be useful for other mods.

    The current thing is the "chameleon button" which should be useful to many mods:

    • Works pretty much the same way as UIButton, but...
    • Right-click + drag to move button wherever you want
    • It changes its background sprite, size, etc., to fit in with other buttons in the area you drag it to (see images above)
    • WIP: It "sticky snaps" to panels such as TSBar, InfoPanel (at bottom of screen), etc. so users don't need to fiddle around trying to align it nicely - it does it for them
    • Next: Hoping to also make it possible to drag it _in to_ the MainToolStrip

    Once I've got that done, next thing will be a library to make adding custom tool strips (tool strip = things like roads menu) easier and quicker. For example it will simplify adding tabs, icons, option buttons, etc. (There might already be an API to do that sort of thing, but I'm not aware of it).

    What I'm hoping to achieve, in TM:PE at least, is to replace a bunch of UI code with something more maintainable, whilst also making TM:PE fit in with the vanilla game interaction model more closely. So instead of various floating panels that all have different designs and quirks, there will be familiar tool strips (like roads menu, but for speeds, lane arrow, etc).

    Share this post


    Link to post
    Share on other sites
    Posted:
    Last Online:  
     

    That's a good idea! The TM GUI would really benefit from that. Make sure to post some progress pics!

    Share this post


    Link to post
    Share on other sites
  • Original Poster
  • Posted:
    Last Online:  
     

    I have another stupid question to ask :)

    I currently have hard-coded values for the locations of the various panels on screen, used to determine which background sprite to use on the button when it's in that area. This will obviously not work for different screen resolutions, so I need to do something more reliable.

    When user first moves button, I want to populate a list of some kind with probably five Vector4's (representing 5 key regions where the button can be dragged to).

    I can then check to see which panel the button is in. I ideally want element references to be based on an enum, something like:

    public enum UIScreenRegion { None, Floating, InfoPanel, TSBar, MainToolStrip, ThumbnailBar };
    
    private UIScreenRegion m_currentRegion = UIScreenRegion.None;
    
    private (something) m_regionRects
    
    protected void OnMoved()
    {
        // work out new position of button
        var ratio = UIView.GetAView().ratio;
        position = new Vector3(position.x + (p.moveDelta.x * ratio), position.y + (p.moveDelta.y * ratio), position.z);
    
    
        if (m_regionRects is empty)
        {
            // populate rects, eg:
            m_regionRects[UIScreenRegion.TSBar] = (get tsbar .absolute)
        }
    
        // do simple collision check on rects to find which one button is in (already got code for this)
        ....
      
        // constrain
        ConstrainToRect()
    }
    
    public ConstrainToRect()
    {
       Vector4 rect = m_regionRects[m_currentRegion]
       // etc...
    }

    Which type of list would you recommend?

    Share this post


    Link to post
    Share on other sites
    Posted:
    Last Online:  
     

    How about a Dictionary<UiScreenRegion, Quad2>?

    A Quad2 can be used for collision checks (Intersect method)

    • Like 1

    Share this post


    Link to post
    Share on other sites
  • Original Poster
  • Posted:
    Last Online:  
     

    Looking at Quad2 it seems it's aimed at 3D objects (I assume the 4 vector2's define the origin, x, y and z?)

    Is that a bit  overkill for 2D UI stuff?

    Share this post


    Link to post
    Share on other sites
    Posted:
    Last Online:  
     

    Those are just the corners of the quad. Vector2 is 2d. It's a bit more advanced as it also supports non-rect quads and rotated quads. Still pretty fast. Of course you can roll out your own solution.

    Share this post


    Link to post
    Share on other sites

    Sign In or register to comment...

    To comment in reply, you must be a community member

    Sign In  

    Already have an account? Sign in here.

    Sign In Now

    Create an Account  

    Sign up to join our friendly community. It's easy!  

    Register a New Account


    ×

    Thank You for the Continued Support!

    Simtropolis depends on donations to fund site maintenance costs.
    Without your support, we just would not be in our 24th year online!  You really help make this a great community. *:thumb:

    But we still need your support to stay online. If you're able to, please consider a donation to help us stay up and running. This helps sustain a platform where we can share our community creations for years to come.

    Make a Donation, Get a Gift!

    Expand your city with the best from the Simtropolis Exchange.
    Make a Donation and get one or all three discs today!

    STEX Collections

    By way of a "Thank You" gift, we'd like to send you our STEX Collector's DVD. It's some of the best buildings, lots, maps and mods collected for you over the years. Check out the STEX Collections for more info.

    Each donation helps keep Simtropolis online, open and free!

    Thank you for reading and enjoy the site!

    More About STEX Collections