Home > front end >  How to change the value of field simultaneously in two classes without using "static"? C#.
How to change the value of field simultaneously in two classes without using "static"? C#.

Time:12-24

I have two classes Player and PlayerUI in Player class I have its 'health' value and in PlayerUI class I have a Slider which value updates each frame according to 'health' value in Player class. I also have a Damage method that changes 'health' value. How can I change the value of field 'health' simultaneously in PlayerUI class and in Player class without using static?

Player class:

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

public class Player : MonoBehaviour
{

    public static float health = 150;

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Q))
        {
            Damage();
        }
    }

    void Damage()
    {
        health -= 10;
    }
}

PlayerUI class:

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

public class PlayerUI : MonoBehaviour
{
    public Slider hpBar;

    private void Awake()
    {
        hpBar.maxValue = Player.health;
    }

    private void Update()
    {
        hpBar.value = Player.health;
    }
}

CodePudding user response:

In general you don't want to poll and set values in Update every frame but rather make your code more event driven => Only change the UI slider in the moment the value is actually changed.

I would use a property to make it event driven.

The main questions with this kind of constructs are: "Who shall be responsible for what?" and "Who shall know who?"

Option A

For example: Shall the player actively control the UI, without the UI knowing that a Player even exists?

  • The player "knows" the PlayerUI => it has its reference
  • The player actively informs the UI and updated the slider value

like e.g.

public class Player : MonoBehaviour
{
    // In this case the Player needs to know the UI
    public PlayerUI ui;

    private float health = 150;
    public float Health
    {
        get => health;
        set
        {
            health = value;
            // Whenever the value of Health is changed actively update the UI
            ui.hpBar.value = value;
        }
    }

    private void Start ()
    {
        // initially inform the UI
        Health = health;
    }
    
    ...

    void Damage()
    {
        // Important: don't change the field anymore but rather the property!
        Health -= 10;
    }
}

Advantage

  • Player gains more power and responsibility and possibly becomes the core component that ensures a clean interoperability between multiple subcomponents like the UI and others. It would also be the central API point for accessing these sub components from the outside

Disadvantage

  • As the player needs to know all the subcomponents, everytime you add one you have to hard code it into the class and configure the references and connections between the subcomponents

Option 2

Or should it rather be the other way round and the Player doesn't even know that a UI exists?

-> Instead of knowing the UI you could add a UnityEvent (just like the button onClick) in befores code example where you can attach callbacks via the Inspector or on runtime to react to every change of the health property.

  • The Player simply invokes his event, not knowing/caring who is listening to it
  • The UI hooks up to the event -> The UI knows the player

like e.g.

public class Player : MonoBehaviour
{
    // In this case the Player doesn't know anyone

    // attach listeners via the Inspector or on runtime via code
    public UnityEvent<float> OnHealthChanged;

    private float health = 150;
    public float Health
    {
        get => health;
        set
        {
            health = value;
            // Whenever the value of Health is changed just raise the event
            // you don't care who is listening or not, but whoever is will get informed
            OnHealthChanged.Invoke(value);
        }
    }

    private void Start ()
    {
        // initially inform all listeners
        // Note that for timing reasons it is essential that the listeners are attached in "Awake"
        // so before this "Start" is called
        // Alternatively the listeners can of course also ONCE poll the value directly -> Up to you
        Health = health;
    }
    
    ...

    void Damage()
    {
        // Important: don't change the field anymore but rather the property!
        Health -= 10;
    }
}

The UI can hook up to the player and listen to the event and react to it like e.g.

public class PlayerUI : MonoBehaviour
{
    ...

    // In this case the UI needs to know the player
    public Player player;

    private void Awake ()
    {
        player.OnHealthChanged.AddListener(UpdateSlider);
    }

    private void OnDestroy ()
    {
        if(player) player.OnHealthChanged.RemoveListener(UpdateSlider);
    }

    private void UpdateSlider (float value)
    {
        hpBar.value = value;
    }
}

Advantage

  • This solves exactly the Disadvantage of Option A and allows to extremely flexible add and remove listeners without the Player having to care at all.

Disadvantage

  • With a lot of subcomponents and events and listeners it can quick get out of hand and become hard to debug and maintain. Also as noted this is more open for race conditions and timing and order issues.

CodePudding user response:

Imo the best approach would be to create an event like "OnPlayerDamaged" in Player script that is raised when player gets any damage. Then create a method in PlayerUI that is subscribed to OnPlayerDamaged event and changes your healthbar.

Player script:

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

public class Player : MonoBehaviour
{
    public static event Action<float> OnPlayerDamaged; 

    private float health = 150;

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Q))
        {
            Damage();
        }
    }

    void Damage()
    {
        health -= 10;
        OnPlayerDamaged.Invoke(health)
    }

    public static float GetHealth()
    {
        return health;
    }
}

PlayerUI

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

public class PlayerUI : MonoBehaviour
{
    public Slider hpBar;

    private void Awake()
    {
        hpBar.maxValue = Player.GetHealth();
    }
    
    private void OnEnable()
    {
        Player.OnPlayerDamaged  = ChangeHealth;
    }

    private void OnDisable()
    {
        Player.OnPlayerDamaged -= ChangeHealth;
    }

    private void Update()
    {

    }

    public void ChangeHealth(float currentHealth)
    {
        hpBar.value = currentHealth;
    }
}

With this approach you can later add anything you want that uses the information about player getting damaged like screen effects or audio change. Just add the method to the event just like the ChangeHealth() method in our case. Remember to remove the method from the event on script disable to avoid multiple subscription of the same method.

GetHealth() method is a quick hack there but you should use ScriptableObject for player statistics like health and reference it where you want to use it. Then you don't need to pass currentHealth in the event and when it is raised just get value from scriptable object.

  • Related