How to save health between scenes?

Hello I have this script for my player health, and it works correctly, and I want to save my life between scenes, I tried with a singleton and playerprefs, but I’m rookie, and I can’t do it. This is my script for health:

public class Health : MonoBehaviour
{
    public delegate void OnEventDelegate();

    public event OnEventDelegate OnDeath;

    public delegate void OnHealthChangeDelegate(int amount);

    public event OnHealthChangeDelegate OnHealthChanged;


    [SerializeField] private int maxHealth = 50;
    private int _currentHealth;
    [SerializeField] private ParticleSystem hitEffect;
    [SerializeField] private ParticleSystem deathEffect;
    [SerializeField] private GameObject additionalDeathEffect; // Nuevo objeto a activar al morir
    private bool _isDead = false;

    public bool IsDead
    {
        get { return _isDead; }
    }


    protected virtual void Awake()
    {
        _currentHealth = maxHealth;
    }

    public void TakeDamage(int damage)
    {
        _currentHealth = Mathf.Clamp(_currentHealth - damage, 0, int.MaxValue);

        if (OnHealthChanged != null) OnHealthChanged(-damage);

        if (_currentHealth <= 0)
        {
            PlayEffect(deathEffect);
            Die();
        }
        else
        {
            PlayEffect(hitEffect);
        }
    }

    protected virtual void Die()
    {
        _isDead = true;
        if (OnDeath != null) OnDeath();
        // Activar el nuevo efecto de muerte si está asignado
        if (additionalDeathEffect != null)
        {
            additionalDeathEffect.SetActive(true);
        }
    }

    void PlayEffect(ParticleSystem effect)
    {
        if (effect != null)
        {
            // Obtener la posición actual y añadir el desplazamiento en el eje Y
            Vector3 spawnPosition = transform.position + new Vector3(0f, 0.5f, 0f);
            ParticleSystem instance = Instantiate(effect, spawnPosition, Quaternion.identity);
            // Rotar el efecto de partículas en 90 grados en el eje X
            instance.transform.rotation = Quaternion.Euler(-90f, 0f, 0f);
            Destroy(instance.gameObject, instance.main.duration + instance.main.startLifetime.constantMax);
        }
    }

    public int GetHealth()
    {
        return _currentHealth;
    }

    public void Restore(int amount)
    {
        _currentHealth = Mathf.Clamp(_currentHealth + amount, 0, maxHealth);
        if (OnHealthChanged != null) OnHealthChanged(amount);
    }
}

and the script for the UI:

public class UIDisplay : MonoBehaviour
{
    [Header("Player Stats")] [SerializeField]
    private Slider healthSlider;

    [SerializeField] private Health playerHealth;
    [SerializeField] private TextMeshProUGUI scoreText;
    [SerializeField] private TextMeshProUGUI levelText;

    private LevelManager _levelManager;

    [Header("Animations")] [SerializeField]
    Animation scoreAnimation;

    [SerializeField] Animation healthAnimation;

    void Start()
    {
        GameState.Instance.OnScoreChanged += UpdateScore;

        _levelManager = FindObjectOfType<LevelManager>();
        levelText.text = _levelManager.GetCurrentLevelTitle();

        healthSlider.maxValue = playerHealth.GetHealth();
        healthSlider.value = healthSlider.maxValue;

        scoreText.text = GameState.Instance.GetScore().ToString();

        playerHealth.OnHealthChanged += UpdateHealth;
    }

    void UpdateHealth(int amount)
    {
        int newHealth = playerHealth.GetHealth();

        healthSlider.value = newHealth;

        if (amount < 0 && healthAnimation)
        {
            healthAnimation.Play();
        }
    }

    void UpdateScore()
    {
        if (scoreAnimation)
        {
            scoreAnimation.Play();
        }

        scoreText.text = GameState.Instance.GetScore().ToString();
    }
   
    private void OnDestroy()
    {
        // Com que _gameState és un singleton, si no ens desuscribim quan
        // es canvia de nivell continua intentant actualitzar la UI antiga
        GameState.Instance.OnScoreChanged -= UpdateScore;
    }
}

How I can do it?

Thanks.

Use Unity - Scripting API: Object.DontDestroyOnLoad
Something like this is Awake:

Object.DontDestroyOnLoad(this.gameObject);

Also, it looks like you have a bug in the slider code:

healthSlider.maxValue = playerHealth.GetHealth();
 healthSlider.value = healthSlider.maxValue;

You probably want:

healthSlider.maxValue = playerHealth.maxValue;
 healthSlider.value = playerHealth.GetHealth()

(not sure)

1 Like

Hello

I have changed the health code, thank you. But I don’t know where I should put this line:

Object.DontDestroyOnLoad(this.gameObject);

in the health script or in the UI?

Regards

You call it on any gameobjects that you want to persist from scene to scene. Probably want to have both the health script and UI script gameobjects persist. One trick is to just create a gameobject and parent the health and UI gameobjects to the parent, then call DontDestroyOnLoad on the parent. The parent including all its children will persist when a scene load occurs.

1 Like

You will need to be a bit careful with DontDestroyOnLoad if the object contains references to other objects. Your Health class has some SerializeReferences to ParticleSystems and additionalDeathEffect. If those objects unload but Health does not, then Health will have stale or null references.

If it gets too complicated to untangle the objects that will unload, then one option is to create a separate simpler script with DontDestroyOnLoad which is only used to store persistent data. This could be a singleton class which your other scripts could access to query the persistent data.

1 Like

I tried putting Ondestroy in both scripts but got null errors. I have a singleton for my Gamestate and Score works fine with this method between scenes, but how can I put the health variable inside and does it work fine between scenes?

public class GameState : MonoBehaviour
{
    public delegate void OnEventDelegate();

    public event OnEventDelegate OnScoreChanged;
    public event OnEventDelegate OnAlertStateChange;

    private int _score;

    private static GameState _instance;

    private int _alertedEnemies = 0;

    public static GameState Instance
    {
        get { return _instance; }
    }

    private int _currentLevel;


    public int CurrentLevel
    {
        get { return _currentLevel; }
        set { _currentLevel = value; }
    }

    private void Awake()
    {
        if (_instance != null)
        {
            gameObject.SetActive(false);
            Destroy(gameObject);
        }
        else
        {
            _instance = this;
            DontDestroyOnLoad(gameObject);
        }
    }

    public int GetScore()
    {
        return _score;
    }

    public void IncreaseScore(int value)
    {
        _score += value;
        Mathf.Clamp(_score, 0, int.MaxValue);

        if (OnScoreChanged != null) OnScoreChanged();
    }

    public void Reset()
    {
        _score = 0;
        _currentLevel = 0;

        ResetAlert();
    }

    public void ResetAlert()
    {
        _alertedEnemies = 0;
    }

    public void IncreaseAlert()
    {
        _alertedEnemies++;

        if (_alertedEnemies == 1 && OnAlertStateChange != null)
        {
            OnAlertStateChange();
        }
    }

    public void DecreaseAlert()
    {
        _alertedEnemies--;
        if (_alertedEnemies == 0 && OnAlertStateChange != null)
        {
            OnAlertStateChange();
        }
    }

    public bool IsAlerted()
    {
        return _alertedEnemies > 0;
    }
}

Similar to how you have defined CurrentLevel in your Gamestate class, you could also define CurrentHealth. In the Start() function of your Health class, you could set the currentHealth equal to GameState.Instance.CurrentHealth.

To keep the GameState version of CurrentHealth up to date, there are a few options.

  1. Remove _currentHealth from Health and always access the state in GameState.
  2. Whenever _currentHealth changes in Health, also change the state in GameState.
  3. Add an OnDestroy method to Health which copies _currentHealth to the GameState copy of CurrentHealth.
1 Like

I tried all of this and I have errors because I have the same health script for Player and enemies, and this is too much for me, maybe I need to try to learn more programming before I start all this.

If the Health script is the same for Players and enemies, then something will need to handle the unique behavior for the Player. This could be done with a separate script that only exists on the player and interacts with the Health script after the scene loads. Or you could add a bool to the Health script that you can set in the inspector to use the special Player behavior.

1 Like

I tried putting an external script with a singleton with a reference to the player’s health and the player’s save data preferences, but when I start the game my player has 0 health. My health script it’s the same.

public class HealthManager : MonoBehaviour
{
    public static HealthManager Instance { get; private set; }
    private Health playerHealth;

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject); // Si ya hay una instancia, destruye este GameObject
            return;
        }

        Instance = this; // Establece esta instancia como la instancia única
        DontDestroyOnLoad(gameObject);

        // Obtener una referencia al componente Health del jugador al iniciar
        GameObject player = GameObject.FindGameObjectWithTag("Player");
        if (player != null)
        {
            playerHealth = player.GetComponent<Health>();
        }

        // Suscribirse al evento sceneLoaded cuando este script se habilita
        SceneManager.sceneLoaded += OnSceneLoaded;
    }

    private void OnDestroy()
    {
        // Asegurarse de cancelar la suscripción al evento sceneLoaded cuando este script se destruye
        SceneManager.sceneLoaded -= OnSceneLoaded;
    }

    // Método que se llama cada vez que se carga una nueva escena
    private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
    {
        // Volver a obtener la referencia al componente Health del jugador si es necesario
        if (playerHealth == null)
        {
            GameObject player = GameObject.FindGameObjectWithTag("Player");
            if (player != null)
            {
                playerHealth = player.GetComponent<Health>();
            }
        }

        // Restaurar la salud del jugador si se encuentra
        if (playerHealth != null)
        {
            int savedHealth = PlayerPrefs.GetInt("PlayerHealth", playerHealth.MaxHealthValue);
            playerHealth.Restore(savedHealth - playerHealth.GetHealth());
        }
    }
}

Doesn’t work, I have two diferent players in every scene, and each one have diference references, I don’t know if this it’s ok.

You can add a little static script to your project and just before switching to the next scene you store your player’s health in the static script. Then when the scene loads and your player script awakens it can grab the stored health from the static script.

public static class Stats
{
    public static int health;
}

Personally I use player prefs because I’m usually wanting to carry stats from one game session to the next and so it’s no trouble to just do the same when switching scenes.

1 Like

Depending on the scale of your project, a static class is fine, or using scriptable objects as well. You will just need to be mindful of resetting/initialising the appropriate values where appropriate. I would only do this for very small projects though.

My personal best-practice solution would be to have the player in its own scene, and take an additive scene loading approach with your project. This way you don’t need to deal with static classes, scriptable objects or singletons, because you’ll generally always have the player’s scene loaded at all times. You just need to load and unload levels as necessary.

Then your health can be saved between gameplay sessions using your save system, preferably by writing the necessary data to disk (and not using PlayerPrefs).

1 Like

Finally I did it, I tested your vision with a static script and it worked, here my two scripts.

GameManager
private void DelayedLoadLevel(string sceneName, float delay)
{
    if (OnLevelChange != null)
        OnLevelChange();

    // Obtener la referencia al script de salud del jugador
    Health playerHealthScriptReference = FindObjectOfType<Health>();

    // Guardar la salud actual del jugador en la clase estática antes de cambiar de escena
    if (playerHealthScriptReference != null && playerHealthScriptReference.CompareTag("Player"))
    {
        PlayerStats.playerHealth = playerHealthScriptReference.GetHealth();
    }

    // Invocar la carga de escena después del retraso especificado
    Invoke("LoadSceneWithDelay", delay);

    // Almacenar el nombre de la escena en una variable temporal para que esté disponible en el método auxiliar
    sceneToLoad = sceneName;
}
public static class PlayerStats
{
    public static int playerHealth;
}
protected virtual void Awake()
{
    //_currentHealth = maxHealth;
    // Verificar si el objeto actual tiene la etiqueta "Player"
    if (gameObject.CompareTag("Player"))
    {
        // Recuperar la salud guardada del jugador desde la clase estática
        int savedHealth = PlayerStats.playerHealth;

        // Si la salud guardada es 0, inicializarla con el valor predeterminado
        if (savedHealth == 0)
        {
            _currentHealth = maxHealth;
        }
        else
        {
            _currentHealth = savedHealth;
        }
    }
    else
    {
        // Si el objeto no es el jugador, inicializar la salud con el valor predeterminado
        _currentHealth = maxHealth;
    }
private void OnDestroy()
{
     // Guardar la salud actual del jugador en la clase estática antes de destruir el objeto
     PlayerStats.playerHealth = _currentHealth;
}

Wow, thanks to all of you, this is my hardest programming problem solved thanks to the community!