Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

Question How to save health between scenes?

Discussion in 'Scripting' started by javilevel, Mar 28, 2024.

  1. javilevel

    javilevel

    Joined:
    Aug 5, 2020
    Posts:
    11
    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:
    Code (CSharp):
    1. public class Health : MonoBehaviour
    2. {
    3.     public delegate void OnEventDelegate();
    4.  
    5.     public event OnEventDelegate OnDeath;
    6.  
    7.     public delegate void OnHealthChangeDelegate(int amount);
    8.  
    9.     public event OnHealthChangeDelegate OnHealthChanged;
    10.  
    11.  
    12.     [SerializeField] private int maxHealth = 50;
    13.     private int _currentHealth;
    14.     [SerializeField] private ParticleSystem hitEffect;
    15.     [SerializeField] private ParticleSystem deathEffect;
    16.     [SerializeField] private GameObject additionalDeathEffect; // Nuevo objeto a activar al morir
    17.     private bool _isDead = false;
    18.  
    19.     public bool IsDead
    20.     {
    21.         get { return _isDead; }
    22.     }
    23.  
    24.  
    25.     protected virtual void Awake()
    26.     {
    27.         _currentHealth = maxHealth;
    28.     }
    29.  
    30.     public void TakeDamage(int damage)
    31.     {
    32.         _currentHealth = Mathf.Clamp(_currentHealth - damage, 0, int.MaxValue);
    33.  
    34.         if (OnHealthChanged != null) OnHealthChanged(-damage);
    35.  
    36.         if (_currentHealth <= 0)
    37.         {
    38.             PlayEffect(deathEffect);
    39.             Die();
    40.         }
    41.         else
    42.         {
    43.             PlayEffect(hitEffect);
    44.         }
    45.     }
    46.  
    47.     protected virtual void Die()
    48.     {
    49.         _isDead = true;
    50.         if (OnDeath != null) OnDeath();
    51.         // Activar el nuevo efecto de muerte si está asignado
    52.         if (additionalDeathEffect != null)
    53.         {
    54.             additionalDeathEffect.SetActive(true);
    55.         }
    56.     }
    57.  
    58.     void PlayEffect(ParticleSystem effect)
    59.     {
    60.         if (effect != null)
    61.         {
    62.             // Obtener la posición actual y añadir el desplazamiento en el eje Y
    63.             Vector3 spawnPosition = transform.position + new Vector3(0f, 0.5f, 0f);
    64.             ParticleSystem instance = Instantiate(effect, spawnPosition, Quaternion.identity);
    65.             // Rotar el efecto de partículas en 90 grados en el eje X
    66.             instance.transform.rotation = Quaternion.Euler(-90f, 0f, 0f);
    67.             Destroy(instance.gameObject, instance.main.duration + instance.main.startLifetime.constantMax);
    68.         }
    69.     }
    70.  
    71.     public int GetHealth()
    72.     {
    73.         return _currentHealth;
    74.     }
    75.  
    76.     public void Restore(int amount)
    77.     {
    78.         _currentHealth = Mathf.Clamp(_currentHealth + amount, 0, maxHealth);
    79.         if (OnHealthChanged != null) OnHealthChanged(amount);
    80.     }
    81. }
    and the script for the UI:

    Code (CSharp):
    1. public class UIDisplay : MonoBehaviour
    2. {
    3.     [Header("Player Stats")] [SerializeField]
    4.     private Slider healthSlider;
    5.  
    6.     [SerializeField] private Health playerHealth;
    7.     [SerializeField] private TextMeshProUGUI scoreText;
    8.     [SerializeField] private TextMeshProUGUI levelText;
    9.  
    10.     private LevelManager _levelManager;
    11.  
    12.     [Header("Animations")] [SerializeField]
    13.     Animation scoreAnimation;
    14.  
    15.     [SerializeField] Animation healthAnimation;
    16.  
    17.     void Start()
    18.     {
    19.         GameState.Instance.OnScoreChanged += UpdateScore;
    20.  
    21.         _levelManager = FindObjectOfType<LevelManager>();
    22.         levelText.text = _levelManager.GetCurrentLevelTitle();
    23.  
    24.         healthSlider.maxValue = playerHealth.GetHealth();
    25.         healthSlider.value = healthSlider.maxValue;
    26.  
    27.         scoreText.text = GameState.Instance.GetScore().ToString();
    28.  
    29.         playerHealth.OnHealthChanged += UpdateHealth;
    30.     }
    31.  
    32.     void UpdateHealth(int amount)
    33.     {
    34.         int newHealth = playerHealth.GetHealth();
    35.  
    36.         healthSlider.value = newHealth;
    37.  
    38.         if (amount < 0 && healthAnimation)
    39.         {
    40.             healthAnimation.Play();
    41.         }
    42.     }
    43.  
    44.     void UpdateScore()
    45.     {
    46.         if (scoreAnimation)
    47.         {
    48.             scoreAnimation.Play();
    49.         }
    50.  
    51.         scoreText.text = GameState.Instance.GetScore().ToString();
    52.     }
    53.    
    54.     private void OnDestroy()
    55.     {
    56.         // Com que _gameState és un singleton, si no ens desuscribim quan
    57.         // es canvia de nivell continua intentant actualitzar la UI antiga
    58.         GameState.Instance.OnScoreChanged -= UpdateScore;
    59.     }
    60. }
    How I can do it?

    Thanks.
     
  2. ArachnidAnimal

    ArachnidAnimal

    Joined:
    Mar 3, 2015
    Posts:
    1,936
    Use https://docs.unity3d.com/ScriptReference/Object.DontDestroyOnLoad.html
    Something like this is Awake:
    Code (csharp):
    1.  
    2. Object.DontDestroyOnLoad(this.gameObject);
    3.  
    Also, it looks like you have a bug in the slider code:
    Code (csharp):
    1.  
    2. healthSlider.maxValue = playerHealth.GetHealth();
    3.  healthSlider.value = healthSlider.maxValue;
    4.  
    You probably want:
    Code (csharp):
    1.  
    2. healthSlider.maxValue = playerHealth.maxValue;
    3.  healthSlider.value = playerHealth.GetHealth()
    4.  
    (not sure)
     
    javilevel likes this.
  3. javilevel

    javilevel

    Joined:
    Aug 5, 2020
    Posts:
    11
    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
     
  4. ArachnidAnimal

    ArachnidAnimal

    Joined:
    Mar 3, 2015
    Posts:
    1,936
    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.
     
    javilevel likes this.
  5. dstears

    dstears

    Joined:
    Sep 6, 2021
    Posts:
    154
    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.
     
    javilevel likes this.
  6. javilevel

    javilevel

    Joined:
    Aug 5, 2020
    Posts:
    11
    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?


    Code (CSharp):
    1. public class GameState : MonoBehaviour
    2. {
    3.     public delegate void OnEventDelegate();
    4.  
    5.     public event OnEventDelegate OnScoreChanged;
    6.     public event OnEventDelegate OnAlertStateChange;
    7.  
    8.     private int _score;
    9.  
    10.     private static GameState _instance;
    11.  
    12.     private int _alertedEnemies = 0;
    13.  
    14.     public static GameState Instance
    15.     {
    16.         get { return _instance; }
    17.     }
    18.  
    19.     private int _currentLevel;
    20.  
    21.  
    22.     public int CurrentLevel
    23.     {
    24.         get { return _currentLevel; }
    25.         set { _currentLevel = value; }
    26.     }
    27.  
    28.     private void Awake()
    29.     {
    30.         if (_instance != null)
    31.         {
    32.             gameObject.SetActive(false);
    33.             Destroy(gameObject);
    34.         }
    35.         else
    36.         {
    37.             _instance = this;
    38.             DontDestroyOnLoad(gameObject);
    39.         }
    40.     }
    41.  
    42.     public int GetScore()
    43.     {
    44.         return _score;
    45.     }
    46.  
    47.     public void IncreaseScore(int value)
    48.     {
    49.         _score += value;
    50.         Mathf.Clamp(_score, 0, int.MaxValue);
    51.  
    52.         if (OnScoreChanged != null) OnScoreChanged();
    53.     }
    54.  
    55.     public void Reset()
    56.     {
    57.         _score = 0;
    58.         _currentLevel = 0;
    59.  
    60.         ResetAlert();
    61.     }
    62.  
    63.     public void ResetAlert()
    64.     {
    65.         _alertedEnemies = 0;
    66.     }
    67.  
    68.     public void IncreaseAlert()
    69.     {
    70.         _alertedEnemies++;
    71.  
    72.         if (_alertedEnemies == 1 && OnAlertStateChange != null)
    73.         {
    74.             OnAlertStateChange();
    75.         }
    76.     }
    77.  
    78.     public void DecreaseAlert()
    79.     {
    80.         _alertedEnemies--;
    81.         if (_alertedEnemies == 0 && OnAlertStateChange != null)
    82.         {
    83.             OnAlertStateChange();
    84.         }
    85.     }
    86.  
    87.     public bool IsAlerted()
    88.     {
    89.         return _alertedEnemies > 0;
    90.     }
    91. }
     
  7. dstears

    dstears

    Joined:
    Sep 6, 2021
    Posts:
    154
    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.
     
    javilevel likes this.
  8. javilevel

    javilevel

    Joined:
    Aug 5, 2020
    Posts:
    11
    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.
     
  9. dstears

    dstears

    Joined:
    Sep 6, 2021
    Posts:
    154
    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.
     
    javilevel likes this.
  10. javilevel

    javilevel

    Joined:
    Aug 5, 2020
    Posts:
    11
    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.

    Code (CSharp):
    1. public class HealthManager : MonoBehaviour
    2. {
    3.     public static HealthManager Instance { get; private set; }
    4.     private Health playerHealth;
    5.  
    6.     private void Awake()
    7.     {
    8.         if (Instance != null && Instance != this)
    9.         {
    10.             Destroy(gameObject); // Si ya hay una instancia, destruye este GameObject
    11.             return;
    12.         }
    13.  
    14.         Instance = this; // Establece esta instancia como la instancia única
    15.         DontDestroyOnLoad(gameObject);
    16.  
    17.         // Obtener una referencia al componente Health del jugador al iniciar
    18.         GameObject player = GameObject.FindGameObjectWithTag("Player");
    19.         if (player != null)
    20.         {
    21.             playerHealth = player.GetComponent<Health>();
    22.         }
    23.  
    24.         // Suscribirse al evento sceneLoaded cuando este script se habilita
    25.         SceneManager.sceneLoaded += OnSceneLoaded;
    26.     }
    27.  
    28.     private void OnDestroy()
    29.     {
    30.         // Asegurarse de cancelar la suscripción al evento sceneLoaded cuando este script se destruye
    31.         SceneManager.sceneLoaded -= OnSceneLoaded;
    32.     }
    33.  
    34.     // Método que se llama cada vez que se carga una nueva escena
    35.     private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
    36.     {
    37.         // Volver a obtener la referencia al componente Health del jugador si es necesario
    38.         if (playerHealth == null)
    39.         {
    40.             GameObject player = GameObject.FindGameObjectWithTag("Player");
    41.             if (player != null)
    42.             {
    43.                 playerHealth = player.GetComponent<Health>();
    44.             }
    45.         }
    46.  
    47.         // Restaurar la salud del jugador si se encuentra
    48.         if (playerHealth != null)
    49.         {
    50.             int savedHealth = PlayerPrefs.GetInt("PlayerHealth", playerHealth.MaxHealthValue);
    51.             playerHealth.Restore(savedHealth - playerHealth.GetHealth());
    52.         }
    53.     }
    54. }
    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.
     
  11. zulo3d

    zulo3d

    Joined:
    Feb 18, 2023
    Posts:
    1,054
    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.
    Code (CSharp):
    1. public static class Stats
    2. {
    3.     public static int health;
    4. }
    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.
     
    javilevel likes this.
  12. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,211
    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).
     
    javilevel likes this.
  13. javilevel

    javilevel

    Joined:
    Aug 5, 2020
    Posts:
    11
    Finally I did it, I tested your vision with a static script and it worked, here my two scripts.
    Code (CSharp):
    1. GameManager
    2. private void DelayedLoadLevel(string sceneName, float delay)
    3. {
    4.     if (OnLevelChange != null)
    5.         OnLevelChange();
    6.  
    7.     // Obtener la referencia al script de salud del jugador
    8.     Health playerHealthScriptReference = FindObjectOfType<Health>();
    9.  
    10.     // Guardar la salud actual del jugador en la clase estática antes de cambiar de escena
    11.     if (playerHealthScriptReference != null && playerHealthScriptReference.CompareTag("Player"))
    12.     {
    13.         PlayerStats.playerHealth = playerHealthScriptReference.GetHealth();
    14.     }
    15.  
    16.     // Invocar la carga de escena después del retraso especificado
    17.     Invoke("LoadSceneWithDelay", delay);
    18.  
    19.     // Almacenar el nombre de la escena en una variable temporal para que esté disponible en el método auxiliar
    20.     sceneToLoad = sceneName;
    21. }
    Code (CSharp):
    1. public static class PlayerStats
    2. {
    3.     public static int playerHealth;
    4. }
    Code (CSharp):
    1. protected virtual void Awake()
    2. {
    3.     //_currentHealth = maxHealth;
    4.     // Verificar si el objeto actual tiene la etiqueta "Player"
    5.     if (gameObject.CompareTag("Player"))
    6.     {
    7.         // Recuperar la salud guardada del jugador desde la clase estática
    8.         int savedHealth = PlayerStats.playerHealth;
    9.  
    10.         // Si la salud guardada es 0, inicializarla con el valor predeterminado
    11.         if (savedHealth == 0)
    12.         {
    13.             _currentHealth = maxHealth;
    14.         }
    15.         else
    16.         {
    17.             _currentHealth = savedHealth;
    18.         }
    19.     }
    20.     else
    21.     {
    22.         // Si el objeto no es el jugador, inicializar la salud con el valor predeterminado
    23.         _currentHealth = maxHealth;
    24.     }
    25. private void OnDestroy()
    26. {
    27.      // Guardar la salud actual del jugador en la clase estática antes de destruir el objeto
    28.      PlayerStats.playerHealth = _currentHealth;
    29. }
    Wow, thanks to all of you, this is my hardest programming problem solved thanks to the community!