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!