[Solved] C# Script for multiple UI Sliders, allow decrease only, if max has reached

Hi, i need to limit UI Sliders.

Now i want the Slider to Stop, if the Limit is 0, but being able to slide back and decrease their value.

like:

Min = 0;
Max = 20;
Limit = Max-SliderValues;

SliderValues = Slider1.value+Slider2.value+Slider3.value+slider4.value;

if the slider values are like this:
Slider1.value = 10;
Slider2.value = 2;
Slider3.value = 2;

than for e.g.
Slider4.value = max 6;

i stumbled over normalizedValue, but i don’t have a clue how to use it to get the desired result.

You want each slider mutually limited by the sum of all sliders, such as “all sliders must be less than max in total?”

If so, in your update loop:

  • read each slider value (from an array of these Sliders)

  • if one Slider is different from the previous frame (keep an array of previous values), then:
    ------- tally all the others besides the one that changed
    ------- calculate the maximum for this slider
    ------- if its value has gone above the max, set it back to the max

If two or more are above the limit, this would obviously only limit the first one it encounters.

One slider can reach max and if so, no slider can be moved until the maxed slider has been decreased. I’ll give an example:

Workers in Sum: 100;
Assigned workers: Mining+Fishing+Crafting+Gym;
Workers available : 100-Assigned workers;

You have 4 Sliders: Mining, Fishing, Crafting, Gym

Each slider decreases Worker Available and increases Assigned Workers.
The theoretical maximum is 100, practical Workers available.
The Sliders need to stop when WA = 0, but the Problem is, with the decreasing Value, the Slider stops at half and all Sliders will jump back to 0 if you try to push them further as the value allows.

Perfect. The solution I listed above would do that.

Great! I will try this and keep you updated.

Hey @Ghost2K , hope you don’t mind but I was curious about a system to do this so I did it. :slight_smile:

I was mostly curious about how one might “pull the other sliders down” when one starts to try to exceed the max, so I have a boolean called ReduceOthers that optionally does this.

Here’s the script:

(BUGGY CODE REMOVED - SEE NEW POST BELOW!)

(Thanks to @Cannist for finding issues!)

I also attached a complete .unitypackage with a little mini test scene all set up to demo it.

That’s very impressive! That’s what i’'m looking for, thank you very much. Would i be okay if i mention you in the credits? I’m folling you on Twitter. You can find infos from our game there.

I’ve adjusted the code a little bit to fit and it’s working. I’d never thought that something like this would need such a complicated solution. I guess i underestimated Unity. ^^ I really thought that there would be a simple soulution for that, somewhere hidden.

Fun fact: The Sliders are most difficult thing i’ve worked on so far and i have coded a full functional souls like levelsystem, item system that fully custumizable and other stuff, but these things? :smiley:

Of course! I would be honored. I’m happy you found it useful.

This is why software task estimation is so difficult! People ask “How hard would it be to do X,” and really the only way you can confidently answer is if you have already done it and you can hand it to them right there. Otherwise any guess about time is always just that: a guess.

Little story: a year or two ago we had a bug reported in our Facebook share, one of the letters in the Spanish-localized message was a wrong accent, like over the e or something, I forget. It was assigned to an engineer, he found it, fixed it, closed it.

The next day QA reported back that it still broken so the engineer looked again. He had forgotten one other place, so the engineer re-fixed it. Another day goes by and the problem still exists. A few more days go by and finally as we’re nearing release I got called in to take a look.

Sure enough, 100% of the code and data files did NOT have the issue. I binary-ransacked the entire project and this character sequence just did not exist. And yet when you shared on Facebook, there it was: wrong character.

I spent nearly a full day investigating and cycling, and it didn’t help that a full build required over two hours of time, so iteration was slow.

Finally it turned out that Facebook was caching the original string we had uploaded, and you had to call a special URL on the Facebook page to flush the cache and take fresh text.

You can find issues like that almost ANYWHERE in software, and at the end of the day the only thing you can do is keep deleting stuff until you have the one core thing that fails. In this case I watched the CORRECT character go out to Facebook and yet it was wrong, so I know it was outside the software.

All told it was ONE WEEK to fix ONE CHARACTER.

In the end of course all joked that “it’s Facebook’s fault,” but sure enough, there it was deep inside their documentation, just a single like such as “To flush your URL responder, call this function.” Hey, they documented it! We never found it.

In the spirit of peer-review, might I suggest two improvements to the code, @Kurt-Dekker ?

  1. This is a minor thing. You could avoid the inner loop that computes othersTotal by computing the currentTotal once before the loop and then just subtract Sliders[j].value from that where needed. Half of the time when you access othersTotal currently you’re interested in the currentTotal anyway. This will not have a significant performance impact unless you have an unusably huge number of sliders but I think it would be a bit neater.

                                if (overAmount > othersTotal)
                                {
                                    overAmount = othersTotal;
                                }

By doing this in line 82 you check if the slider has been advanced so far that we needed to reduce all others below zero for compensation and then essentially restrict that to 0. But unless I am mistaken you still end up with too big a value for the current slider. I think you’d have to add the following line into the of body:

Sliders[i].value = MaximumTotal;

Or am I missing something? It feels like you would have probably noticed this during testing if it was the case.

  1. (Yes, I said only two, but I saw an other thing right now.) It might be good to record changes the code makes to the other sliders during adjustment in LastSliderValues. Currently you only record the change to the one slider that we leave untouched. I would assume that this causes the adjustment code to trigger again in the next Sliders.Count frames. It does not have a visible effect because no further adjustments are needed, but theoretically it could mess things up if the user manages to also apply a change to the sliders within that (arguably very short) time window.

Hey THANKS! Good catches there… I just went over it a bit more and found some other issues. Fixing. To see it fail right now, set the max to 0.5f and drag on slider up, and the others go negative. DOH!

OP: I’ll post an updated code snippet in a bit… testing a bit more first.

@Kurt-Dekker : I will mention you under special thanks. :slight_smile: I can send you a PM for the testphase if want. I should start in a month. Your story is hard, thats issues i’ve encountered on other occasions. I tried to fix a broken project, but i couldn’t find the issue. The Unity profiler was bugged and showed wrong things. We could fix it after upgrading Unity to 2017.

1 Like

I’ve set max Value on the Slider Object = MaximumTotal in the code and it works great! I appreciate your effort, thank you very much!

1 Like

UPDATE: Thanks to @Cannist for finding serious issues in the original code. It would fail badly (producing NaNs) if your max was less than one slider value span.

Also essential is that when reducing, if it cannot reduce, then it must fall back on “limit” of the slider attempting to move.

I have also indicated the limited testing, which is with values 0.0 to 1.0 for the sliders. Good luck if your base number is nonzero… you have become a TestPilot!

I have taken that code down, fixed the issues (I think!), and here is the updated script. Be careful, some of the internal if/then logic and assignation logic is quite different.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class MutuallyLimitedSliders : MonoBehaviour
{
    [Tooltip( "Sliders will be in total limited to never exceed this.")]
    public    float        MaximumTotal;

    [Tooltip( "Set this to reduce other sliders instead of limiting the moving one.")]
    public    bool        ReduceOthers;

    [Header( "CAVEAT: only tested on 0.0 to 1.0 sliders!")]

    [Tooltip( "Sliders to observe and mutually limit.")]
    public    Slider[]    Sliders;

    void Reset()
    {
        MaximumTotal = 1.0f;
    }

    float LastMaximumTotal;
    float[] LastSliderValues;

    void SetSliderGuarded( Slider slider, float value)
    {
        if (value < slider.minValue) value = slider.minValue;
        if (value > slider.maxValue) value = slider.maxValue;
        slider.value = value;
    }

    void Update ()
    {
        // if you change the maximum, trigger the full restart checks
        if (MaximumTotal != LastMaximumTotal)
        {
            LastSliderValues = null;
            LastMaximumTotal = MaximumTotal;
        }

        // is this our first time or did the count of sliders change?
        if (LastSliderValues == null || LastSliderValues.Length != Sliders.Length)
        {
            LastSliderValues = new float[ Sliders.Length];

            float currentTotal = 0.0f;
            for (int i = 0; i < Sliders.Length; i++)
            {
                LastSliderValues[i] = Sliders[i].value;
                currentTotal += Sliders[i].value;
            }

            if (currentTotal > MaximumTotal)
            {
                Debug.LogWarning( "Total already greater than max at start!");

                if (ReduceOthers)
                {
                    for (int i = 0; i < Sliders.Length; i++)
                    {
                        float reducedValue = (Sliders[i].value * MaximumTotal) / currentTotal;
                        SetSliderGuarded( Sliders[i], reducedValue);
                    }
                }
            }
        }

        // check and limit
        {
            bool adjusted = false;
            for (int i = 0; i < Sliders.Length; i++)
            {
                if (Sliders[i].value != LastSliderValues[i])
                {
                    if (!adjusted)
                    {
                        // tally others
                        float othersTotal = 0.0f;
                        for (int j = 0; j < Sliders.Length; j++)
                        {
                            if (i != j)
                            {
                                othersTotal += Sliders[j].value;
                            }
                        }

                        // limit?
                        if (othersTotal + Sliders[i].value > MaximumTotal)
                        {
                            adjusted = true;        // an adjustment has happend, don't do any other this frame

                            bool doLimiting = true;

                            if (ReduceOthers)
                            {
                                // we will bring all the others down to fit, if possible
                                float overAmount = (othersTotal + Sliders[i].value) - MaximumTotal;

                                // we need to reduce the others by overAmount... can we?
                                if (overAmount < othersTotal)
                                {
                                    doLimiting = false;

                                    // proportionally reduce the others
                                    for (int j = 0; j < Sliders.Length; j++)
                                    {
                                        if (i != j)
                                        {
                                            float reducedValue = Sliders[j].value - (Sliders[j].value * overAmount) / othersTotal;
                                            SetSliderGuarded( Sliders[j], reducedValue);
                                        }
                                    }
                                }
                                else
                                {
                                    // we cannot... therefore we must limit this one
                                    doLimiting = true;
                                    // meanwhile drive all the others to their minvaule and retotal
                                    othersTotal = 0.0f;
                                    for (int j = 0; j < Sliders.Length; j++)
                                    {
                                        if (i != j)
                                        {
                                            Sliders[j].value = Sliders[j].minValue;
                                            othersTotal += Sliders[j].value;
                                        }
                                    }
                                }
                            }

                            // we either ar not reducing, OR we were unable to reduce, so fall back to limit
                            if (doLimiting)
                            {
                                // we will prevent this slider from causing the total to exceed
                                float limited = MaximumTotal - othersTotal;
                                SetSliderGuarded( Sliders[i], limited);
                            }
                        }
                    }

                    LastSliderValues[i] = Sliders[i].value;
                }
            }
        }
    }
}

Also attached is an updated scene package, with some debugging text on each slider.

5922950–632948–MutuallyLimitedSliders.unitypackage (11.1 KB)

Thank you very much, i will test this tomorrow. :slight_smile:

You’re welcome. :slight_smile:

I think there is still an issue with your new version if the min values are not all 0. In line 102 you check if adjustment is possible withif (overAmount < othersTotal).
However, othersTotal is the wrong thing to look at here, you would need the reducable total, i.e. the sum of the differences between current value and min value. Unfortunately this also means that the reduction code can go wrong as you might not be able to reduce a slider as much as you try to. SetSliderGuarded prevents from actually setting an invalid value, but your logic does not adapt if such an error would occur. It might be easier to reduce the sliders proportionally to their (value - minValue) instead of their absolute value.

You’re right and that’s why I put line 14 in. :slight_smile:

Agreed. The way to do it is wrap each slider in an get/set that makes it always start at 0 but I leave that as an exercise to the reader for today.

Thank you for your review. Code is never finished, it just (sometimes) ships. :slight_smile:

Well, I got curious as well about the decrease proportional to the value instead of (value - minValue) and here is my take on it:

    public void Update()
    {
        // the index of the slider that the user increased
        int userChangedIndex = -1;


        if (LastSliderValues == null)
        {
            LastSliderValues = new float[Sliders.Length];
            for (int i = 0; i < Sliders.Length; i++)
            {
                LastSliderValues[i] = Sliders[i].minValue;
            }
        }

        // if you decrease the maximum, trigger the full restart checks
        if (MaximumTotal < LastMaximumTotal)
        {
            LastMaximumTotal = MaximumTotal;
        }
        else
        {
            // Try to find a slider that was increased
            for (int i = 0; i < Sliders.Length && userChangedIndex < 0; i++)
            {
                if (Sliders[i].value > LastSliderValues[i])
                    userChangedIndex = i;
            }

            // exit early if no increase was observed
            if (userChangedIndex < 0)
                return;
        }


        // Essentially overAmount = currentTotal - MaximumTotal
        // We'll add the current total piece-wise in the loop below
        float overAmount = -MaximumTotal;
        // The number of sliders that is adjustable, i.e. can be reduced in their value.
        // This does not count the slider the user changed.
        int adjustableSliders = 0;
        // The sum of the values of the adjustable sliders.
        // Needed to determine the proportional decrease.
        float totalOfAdjustableSliders = 0f;
        for (int i = 0; i < Sliders.Length; i++)
        {
            overAmount += Sliders[i].value;
            if (i != userChangedIndex && Sliders[i].value > Sliders[i].minValue)
            {
                adjustableSliders++;
                totalOfAdjustableSliders += Sliders[i].value;
            }
        }

        // In each iteration we'll try to distribute the whole remaining overAmount
        // proportionally. However, that can fail if a slider (with a non-zero minValue)
        // cannot be reduced enough. In that case some overAmount will be left after the
        // iteration step but those sliders will not be adjustable anymore.
        // Theoretically the loop condition could be
        //    (overAmount > 0f && adjustableSliders > 0)
        // but floating point inaccuracies might make the float comparison fail.
        // So we observe if the number of adjustable sliders change and exit the loop if they do not.
        int lastAdjustableSliders = 0;
        while (overAmount > 0f && adjustableSliders != lastAdjustableSliders)
        {
            lastAdjustableSliders = adjustableSliders;
            float reduceByFactor = overAmount / totalOfAdjustableSliders;

            // try to reduce each adjustable slider proportionally
            // some sliders we might only be able to reduce by less than that
            // but then they will stop being adjustable
            for (int i = 0; i < Sliders.Length; i++)
            {
                if (i != userChangedIndex && Sliders[i].value > Sliders[i].minValue)
                {
                    // Slider is adjustable
                    float reduceBy = Sliders[i].value * reduceByFactor;
                    if (Sliders[i].minValue < Sliders[i].value - reduceBy)
                    {
                        // we can adjust by the desired amount
                        Sliders[i].value = Sliders[i].value - reduceBy;
                        overAmount -= reduceBy;
                        totalOfAdjustableSliders -= reduceBy;
                    }
                    else
                    {
                        // we can only adjust down to min value
                        // and the slider is not adjustable anymore
                        overAmount -= Sliders[i].value - Sliders[i].minValue;
                        totalOfAdjustableSliders -= Sliders[i].value;
                        Sliders[i].value = Sliders[i].minValue;
                        adjustableSliders--;
                    }
                }
            }

        }

        // We have adjusted all the sliders we can.
        // If there is still some overAmount left we
        // need to limit the slider changed by the user.
        if (overAmount > 0f && userChangedIndex >= 0)
            Sliders[userChangedIndex].value = Sliders[userChangedIndex].value - overAmount;


        // Finally record last slider values
        for (int i  = 0; i < Sliders.Length; i++)
        {
            LastSliderValues[i] = Sliders[i].value;
        }
    }

Annoyingly (but not completely unexpected), as soon as you cannot disperse the correction in a single loop anymore you need to account for float inaccuracies in the termination condition…
Also, trying this out by dragging sliders around I have to say the reduction proportionate to the value feels wrong if the slider starts at non-zero. If I’d do this for real I’d probably have the graphical slider actually start at zero but then prevent it from going below a configured minimum.

Thanks for this fun discussion. :slight_smile:

2 Likes

Absolutely! In my experience, I would force all these sliders to be 0.0 to 1.0 and then have some other notion of scaling external to the entire UI system, just to keep all the business logic of the game in code, rather than half of it in code and half of it in UI. I’m a huge fan of putting zero logic into UI, which is why I made my Datasacks package, which encourages virtually zero logic into the UI, and encourages you to put all the logic in your game code.

Thinking further on it, in OP’s original example of Mining, Fishing, Crafting, Gym, he has X discrete workers, so it is likely he wants something else on top of this anyway that forces each slider to discrete integer positions rather than allowing 2.5 workers on Gym and 1.5 workers on Crafting etc. Again, such a thing would probably be best in code so all that logic is centralized.

1 Like

Thanks to both of you this is fantastic and just what i was looking for saved me an age… awesome!!

1 Like