Home > OS >  Unity best practice for displaying initial values in UI
Unity best practice for displaying initial values in UI

Time:01-07

I recently ran into a situation where I have a player object to store health and mana values and my UI subscribes to changes to these values.

For example:

Player class

int maxHP;

void Start()
{
    maxHP = 100;
}

Now I have some logic that triggers an event whenever the maxHP value changes and calls a function in my UI

UI class

private void UpdatePlayerHP(int currentHP, int maxHP)
    {
        playerHPText.GetComponent<TextMeshProUGUI>().text = maxHP.ToString()
    }

However, the player object is persistent between scenes and already exists when the scene with the UI is loaded, therefore the value change event is not transmitted to the UI. I think this might be a common problem and I will probably run into this a lot during the project as there are lots of scene changes...

If someone could recommend a workaround for this (other than making maxHP public and directly accessing it from the UI) or give any best practice advise, I would really appreciate it :)

CodePudding user response:

Unity has an event called:

SceneManager.sceneLoaded

However, I don't recommend using this because it completely bypasses things like if the object is enabled or not.

Have you considered making the UI persistent? That would solve your problem, as well as similar ones you might encounter when you want to have a score-meter, a timer or something else.

Every other approach involves giving the UI a reference to the Player, which is generally something you want to avoid.

CodePudding user response:

As a general answer to this: You will have to call the callback once manually the moment you attach the listener.

E.g. say you have an event and value like

private int health = 100;
// Readonly access
public int Health => health;
public event Action<int> HealthChanged;

then you would do

theClass.HealthChanged  = OnHealthChanged;
OnHealthChanged(theClass.Health);

This is a bit uncanny of course - you have to not forget to call it immediately, expose the value public at least readonly etc.

=> you could abstract it a bit into kind of an observable pattern

public class Observable<T>
{
    private T value;
    public T Value => value;
    private event Action<T> valueChanged;

    public Observable(T initialValue)
    {
        value = initialValue;
    }

    public void Subscribe(Action<T> callback)
    {
        // subscribe and call once immediately 
        valueChanged  = callback;
        callback?.Invoke(value);
    }

    public void Set(T newValue)
    {
        value = newValue;
        valueChanged?. Invoke (newValue);
    }
}

And then basically have a

public Observable<int> Health;

and the subscriber can simply do

theClass.Health.Subscribe(OnHealthChanged);

which will include one immediate call of the passed callback.


But before going any deeper into this I would strongly recommend UniRX for this.

Their BehaviourSubject basically does something pretty similar but way better than me and more advanced and the package offers some query like filters, advanced reactive use cases like combining multiple observables etc.

you would e.g. do

private readonly BehaviorSubject<int> health = new (100);
public IObservable<int> Health => health;

whenever you set it call

health.OnNext(newHealth);

and subscribers would do

theClass.Health.Subscribe(OnHealthChanged);

now OnHealthChanged is called whenever the health is changed but also once right when you do the subscription.

Furthermore the Subscribe returns an IDisposable you can simply Dispose => not subscribed anymore. This even works with lambda expressions!

  • Related