0
\$\begingroup\$

I want to create a few values which will be dependent on each other. Specifically, I want total sum of few values always be 100 In unity inspector no matter how I tweak them.

E.G.:

int total = 100;

int one = 20;
int two = 30;
int three = 45;
int four = 5;

How can I make other values adjust automatically if I tweak one of them in Unity Inspector?

\$\endgroup\$
1
  • \$\begingroup\$ If you'd change the value of one item to 0, how would the system know which other value to alter? Would you want it to be random? Distributed to all others? \$\endgroup\$
    – user35344
    Mar 19, 2018 at 12:34

2 Answers 2

2
\$\begingroup\$

You can provide an OnValidate() method, which gets called in the Editor when the user changes an Inspector value.

const int TOTAL = 100;
public int one, two, three, four;

#if UNITY_EDITOR
// Save their previous values so we can identify which one changed.
int _oneCache, _twoCache, _threeCache, _fourCache;
_oneCache = -1;

void OnValidate() {
    // Skip this if we haven't cached the values yet.
    if(_oneCache >= 0) {

        // Find which value the user changed, and update the rest from it.
        if(_oneCache != one) {
            DistributeProportionately(ref one, ref two, ref three, ref four);
        } else if (_twoCache != two) {
            DistributeProportionately(ref two, ref one, ref three, ref four);
        } else if()
            // ... repeat for three, four...
        }
    }

    // Cache the old values for the next edit.
    _oneCache = one;
    _twoCache = two;
    _threeCache = three;
    _fourCache = four;   
}

void DistributeProportionately(ref changed, ref int a, ref int b, ref int c) {
    changed = Mathf.Clamp(changed, 0, TOTAL);
    int total = TOTAL - changed;

    int oldTotal = a + b + c;
    if(oldTotal > 0) {
        float fraction = 1f/oldTotal;
        a = Mathf.RoundToInt(total * a * fraction);
        b = Mathf.RoundToInt(total * b * fraction);
        c = Mathf.RoundToInt(total * c * fraction);
    } else {
        a = b = c = total / 3;
    }

    // Assign any rounding error to the last one, arbitrarily.
    // (Better rounding rules exist, so take this as an example only)
    c += total - a - b - c;
}

#endif

This can be done with a custom editor too without explicitly caching each value (instead you'd use an EditorGUI.BeginChangeCheck to identify the changed field), but for quick data validation I like to keep it in one file.

If you're using these to ensure a set of probabilities add up to 100%, then you might want to consider treating the user-facing values as relative weights instead, and keep the normalized values internal:

[SerializeField]
[HideInInspector]
float[] _normalizedProbabilities;

#if UNITY_EDITOR
[SerializeField]
float[] _probabilityWeights;

void OnValidate() {
    float totalWeight = 0f;
    foreach(var weight in _probabilityWeights)
        totalWeight += weight;

    if(totalWeight == 0f)
        return;

    _normalizedProbabilities = new float[_probabilityWeights.Length];
    for(int i = 0; i < _probabilityWeights.Length; i++)
        _normalizedProbabilities[i] = _probabilityWeights[i]/totalWeight;
}
#endif

This tends to be a lot simpler to manage.

\$\endgroup\$
1
\$\begingroup\$

Here is my solution for it, which also supports locking elements so that changing one, does not affect every other value.

private bool[] _lockStates;
public override void OnInspectorGUI()
{
    var data = (TrickChargeButtonsController) target;
    base.OnInspectorGUI();

    if (_lockStates == null || _lockStates.Length != data.normalizedValues.Length)
    {
        _lockStates = Enumerable.Range(0,data.normalizedValues.Length).Select( n=>false).ToArray();
    }
    
    if (GUILayout.Button("Normalize"))
    {
        for (int i = 0; i < data.normalizedValues.Length; i++)
        {
            data.normalizedValues[i] = (float)1.0f / (float)data.normalizedValues.Length;
        }
    }

    float totalLength = 1.0f;
    for (int i = 0; i < data.normalizedValues.Length;i++)
    {
        var lockedCount = _lockStates.Sum(isLocked => isLocked ? 1 : 0);
        var prior = data.normalizedValues[i];
        GUILayout.BeginHorizontal();
        var oldValue = data.normalizedValues[i];
        data.normalizedValues[i] = GUILayout.HorizontalSlider(data.normalizedValues[i],
                                                                             0f,
                                                                             totalLength);
        if (_lockStates[i] || lockedCount >= data.normalizedValues.Length-1)
        {
            data.normalizedValues[i] = oldValue;
        }
        if (GUILayout.Button(_lockStates[i] ? "unlock" : "lock"))
        {
            _lockStates[i] = !_lockStates[i];
        }
        GUILayout.EndHorizontal();
        var after  = data.normalizedValues[i];
        if (lockedCount < data.normalizedValues.Length-1)
        {
            var delta = (after - prior)/(data.normalizedValues.Length-lockedCount-1);
            for (int j = 0; j < data.normalizedValues.Length; j++)
            {
                if (i != j && !_lockStates[j])
                {
                    var test = data.normalizedValues[j] - delta;
                    var adjustedDelta = delta;
                    if (test < 0f)
                    { // we cant allow this to happen , backtrack previous value.

                        adjustedDelta -= -test;
                        data.normalizedValues[i] += test;
                    }
                    data.normalizedValues[j] -= adjustedDelta;
                }
                
            }
        }
        GUILayout.Space(12.0f);
    }
        
}
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .