Guidance on How to Learn About Loading Saved GameState on Scene Changes

Hi! I’m so sorry, this has probably been covered a lot - but I can’t find anything to help me break through days of trial and error on this. I want to apologize in advance for any misunderstandings of terms in C# and Unity as I am a functional JS developer and this OOP stuff is HARD! :smile: I also lose cognitive function when I get frustrated, so I may be a bit incoherent… I super appreciate any of you taking a look at my question.

I created a GameManager using the Singleton pattern to manage state and am currently just saving state into Lists and what not in that instance. My question is: How do I rehydrate the scene when I come back to it after leaving?

This forum answer beautifully details the steps required to save data between scenes. My question is based on Step 2 of the load process outlined in that answer:

“Step 2 - iterate all data, reinject it into the running game. This might take multiple steps: iterate some data to figure out what scene(s) to load, then load that scene(s), then iterate the rest of the data to inject it into the running scene, possibly even creating additional GameObjects in the scene based on the save data.”

How do I “reinject” the data into the running game? Currently, I’m struggling because when the scene loads, all the game objects already exist and I have no way of referencing these existing game objects and adding the data back into them. (The game objects were created in the Unity GUI, I am not creating them programmatically.) So, the expected scene loads, but the game objects in that scene are not aware of the data that their previous incarnations had.

I have tried adding DontDestroyOnLoad(gameObject) into each game object’s Awake method, but this doesn’t let the properties of the attached class persist.

I have tried saving a reference to the game object instance ID and then attempting to find the game object instance ID when the scene changes, but it seems that these IDs change or perhaps the game objects change themselves in between scenes.
I have tried finding game objects by name (they all have unique names that I set in the Unity GUI) and then adding the data in from there, but nothing seems to happen when I attempt this (code below).

Here is some code that might be of interest to you:

Singleton GameManager

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.VisualScripting;
using UnityEditor;
using UnityEngine;

public class GameManager : MonoBehaviour
{
  public static GameManager Instance;

  public GameDataManager GameState;
  public static event Action<GameStateTrigger> OnGameStateChanged;


  void Awake()
  {
    if (Instance != null)
    {
      Destroy(gameObject);
      return;
    }
    Instance = this;
    DontDestroyOnLoad(gameObject);
  }

  void Start()
  {
    GameState = new GameDataManager();
    GameObject[] CollectablePrefabs = Resources.LoadAll<GameObject>("Prefabs");
    GameState.SetupCollectables(CollectablePrefabs);
    UpdateGameState(GameStateTrigger.Setup);
  }

  // ---------STATE UPDATES---------- //

  public void UpdateGameState(GameStateTrigger newState)
  {
    GameState.UpdateState(newState);

    switch (GameState.CurrentGameState)
    {
      case GameStateTrigger.Setup:

        break;
      case GameStateTrigger.PostSetup:
        break;
      case GameStateTrigger.PlayerCollectItem:
        GameState.CheckCollectionCompleted();
        break;
      case GameStateTrigger.Win:
        // show closing UI.
        break;
      default:
        throw new ArgumentOutOfRangeException(nameof(newState), newState, null);
    }

    OnGameStateChanged?.Invoke(newState);
  }

  // ---------HELPER CLASSES---------- //

  public class GameDataManager
  {
    public GameStateTrigger CurrentGameState { get; private set; }
    public List<SpawnableItem> AvailableCollectables { get; private set; }
    public List<string> GoalItems { get; private set; }
    public List<SpawnableItem> PlayerCollectedItems { get; private set; }
    public Dictionary<string, ItemSpawnerData> spawners {get; private set; }

    public GameDataManager()
    {
      GoalItems = new List<string> { "Star Fish", "Clown Fish", "Angel Fish" };
      PlayerCollectedItems = new List<SpawnableItem>();
      spawners = new Dictionary<string, ItemSpawnerData>();
      CurrentGameState = GameStateTrigger.Initialized;
    }

    public void UpdateState(GameStateTrigger newState)
    {
      CurrentGameState = newState;
    }

    // ---------ACTIONS---------- //

    public void SetupCollectables(GameObject[] CollectablePrefabs)
    {

      List<SpawnableItem> items = new List<SpawnableItem>();

      foreach (var prefab in CollectablePrefabs)
      {
        items.Add(new SpawnableItem(prefab.name, prefab));
      }
      AvailableCollectables = items;
    }

    public void CollectItem(SpawnableItem item)
    {
      PlayerCollectedItems.Add(item);
    }

    public void CheckCollectionCompleted()
    {
      bool allFound = AvailableCollectables.All(item => PlayerCollectedItems.Contains(item));
      if (allFound)
      {
        GameManager.Instance.UpdateGameState(GameStateTrigger.Win);
      }
    }

    public void AddItemSpawner( GameObject spawner, GameObject Sparkles, SpawnableItem item, bool found)
    {
      spawners[spawner.name] = new ItemSpawnerData(spawner, Sparkles, item, found);
    }

    public ItemSpawnerData FindItemSpawner(string name)
    {
      ItemSpawnerData matchingSpawner = spawners[name];
      return matchingSpawner;
    }
  }

  public class ItemSpawnerData
  {
    public GameObject spawner;
    public GameObject Sparkles;
    public bool found;
    public SpawnableItem item;

    public ItemSpawnerData(GameObject spawner, GameObject Sparkles, SpawnableItem item, bool found)
    {
      this.spawner = spawner;
      this.Sparkles = Sparkles;
      this.item = item;
      this.found = found;
    }
  }

}

// ---------ENUMS & INTERFACES ---------- //

public enum GameStateTrigger
{
  Initialized,
  Setup,
  PostSetup,
  PlayerCollectItem,
  Win,
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEditor;
using System.Linq;
using UnityEngine.SceneManagement;

public class ItemSpawner : MonoBehaviour
{
System.Guid uuid;
GameObject Sparkles;
SpawnableItem item;
bool inCollision;
bool found;

// dev must assign Canvas GameObject as uiBox to each ItemSpawner.
public GameObject uiBox;
// dev must assign Canvas->Image->Button to acceptItemButton in each ItemSpawner.
public Button acceptItemButton;


void Awake()
{
GameManager.OnGameStateChanged += GameManager_OnGameStateChanged;
SceneManager.sceneLoaded += OnSceneLoaded;

DontDestroyOnLoad(gameObject);
}

void GameManager_OnGameStateChanged(GameStateTrigger newState)
{
switch (newState)
{
case GameStateTrigger.Setup:
uuid = System.Guid.NewGuid();
GameObject SparklesPrefab = Resources.Load<GameObject>("Prefabs/Sparkles");
Sparkles = Instantiate(SparklesPrefab, transform.position, Quaternion.identity);
item = GetRandomSpawnable(GameManager.Instance.GameState.AvailableCollectables);
found = false;
uiBox.SetActive(false);
GameManager.Instance.GameState.AddItemSpawner(gameObject, Sparkles, item, found);
GameManager.Instance.GameState.UpdateState(GameStateTrigger.PostSetup);

break;
case GameStateTrigger.PlayerCollectItem:
GameManager.Instance.GameState.CollectItem(item);
break;
}
}


void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// Can't figure this part out. How can I load the data into each game object that has this script?
if (GameManager.Instance != null)
{
if (GameManager.Instance.GameState != null)
{

foreach (KeyValuePair<string, GameManager.ItemSpawnerData> kvp in GameManager.Instance.GameState.spawners)
{
Debug.Log($"Key: {kvp.Key}, Value: {kvp.Value.item.ItemName}");
}
Debug.Log($"current id: {uuid}");
GameManager.ItemSpawnerData spawner = GameManager.Instance.GameState.FindItemSpawner(gameObject.name);
// I think I am missing how to attach this spawner data to the real game object.
if (spawner != null)
{
Sparkles = spawner.Sparkles;
found = spawner.found;
item = spawner.item;
}
}
}
}

void Update()
{
if (Input.GetButtonDown("Submit") && inCollision && !found)
{
DisplayUIBox(item);
}
}

void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Player")
{
inCollision = true;
}
}

public SpawnableItem GetRandomSpawnable(List<SpawnableItem> findableItems)
{
int random = Random.Range(0, findableItems.Count);
return findableItems.ElementAt(random);
}

void DisplayUIBox(SpawnableItem item)
{
uiBox.SetActive(true);

Image foundItemUI = GameObject.Find("FoundItemUI").GetComponent<Image>();

foundItemUI.sprite = item.Prefab.GetComponent<SpriteRenderer>().sprite;

GameObject foundItemText = GameObject.Find("FoundItemText");
foundItemText.GetComponent<TextMeshProUGUI>().text = "Would you like to take this " + item.ItemName + "?";

acceptItemButton.onClick.RemoveAllListeners();
acceptItemButton.onClick.AddListener(() => triggerCollectItem(item));
}

void triggerCollectItem(SpawnableItem item)
{
Debug.Log("triggered1");

GameManager.Instance.UpdateGameState(GameStateTrigger.PlayerCollectItem);
inCollision = false;
Sparkles.SetActive(false);
found = true;
uiBox.SetActive(false);
}
}

I super appreciate any pointers on where to look, what to Google, or how to think about this.

You can save the status of your scene to whatever format you like and then when you want to reload the scene you destroy all the objects in the current scene and then load your data and instantiate all the objects within.

The format can be something like:

prefab name, , ,
prefab name, , ,
prefab name, , ,

While the above is easy to implement it does require that all the objects are prefabs. If that’s a problem then why not just use a scene save/load solution that’s freely available?.

Thanks so much for the information! It’s sounds like the common practice is to programmatically create the game objects (from prefabs) when loading a scene, instead of relying on the Unity GUI. That’s good to know and I will implement that to see how it feels.

I do think using a scene save/load solution that’s freely available would be the best solution, if creating all the game objects on scene load doesn’t work out well - but I can’t find any. I think maybe I do not know what to search for. I’ll dig around some more and see if I can find something. Hearing someone say that they exist at least gives me hope. I appreciate it!

I wanted to circle back to this in case anybody ever gets stuck with reloading data after a scene change.

Ultimately, I didn’t want to invest in the time to restructure my code to allow me to programmatically create game objects when a scene changes. It seemed a shame to rip everything out that I had already placed using the Unity GUI - I’m sure this is the sunk cost fallacy speaking, though. What I really wanted was to be able to grab existing game objects, rehydrate them with the data from my Singleton GameManager and get on with my day. While it would be super rad to just have a pre-built solution to work with, I couldn’t find anything that actually handled specifically the part that I was struggling with. There are tons of free serialization tools, but I’m not struggling with saving the data, just struggling with getting it back into the scene. If anybody knows the name of any tools like that, please share because I just don’t know enough to know what to Google.

Here’s how I accomplished my task:

  • I saved only concrete data in my game state, so no references to objects. I saved this data in a dictionary, where the gameobject name was the key (they are all unique) and the data was the value in the form of an arbitrary interface/class. I couldn’t get references to match up on scene changes even using DontDestroyOnLoad, so that’s why I did not save references.
  • Then, I subscribed to scene changes in my GameManager using SceneManager.sceneLoaded += OnSceneLoaded; and added the OnSceneLoaded method.
  • In that method, I looped through the game state dictionary and used GameObject.Find(Key) in each loop to find each game object whose name matched to my state.
  • Then, I added my class to that found game object viafoundGameObject.AddComponent<CLASSHERE>();
  • Once that was done, I just needed to make sure that the newly attached code got all the data it needed. In the newly attached script’s Start method, I called a reload method which reached out to the Game state, found the matching game object data in the dictionary and updated all its properties.

This is probably not a great solution. There’s a lot of back and forth between the classes and I think it feels like spaghetti. However, if you’re really tired like me and just want to experience a tiny win before going back to struggling, then this was quick and easy compared to other options. I am dreaming of a nice, free pre-built solution for my future, though. :sweat_smile: In the future, I’ll architect my game to make it easy to build game objects programmatically.

TL,DR: It’s possible to just find active game objects using their names with the .Find() method. Once you have those, you can attach whatever script you want to using the .AddComponent() method. Then, create a function to grab your saved data from the GameManager and update your properties accordingly.