0
\$\begingroup\$

Now, I now the title is really obscure, but I have tried to solve the following problem for so long and most of it is integrated into my game so far that I can't just copy & paste it here, so this is as simple as I can explain it:

I'm saving my data using NewtonSoft Json.

I basically have a singleton manager script that stores all quests in the game (quests), and all completed quests (completedQuests).

I use a property for getting all quests, since I don't want the base collection of every quest to ever change.

The SaveData class is what I am saving in Json.

public class GameStatistics : MonoBehaviour
{
    [Header("Settings:")]
    [SerializeField] private QuestTemplate[] quests;

    public QuestTemplate[] Quests
    {
        get { return quests; }
        private set { quests = value; }
    }

    public List<QuestTemplate> completedQuests { get; set; } = new List<QuestTemplate>();

    public static GameStatistics Instance { get; private set; }

    private void Awake()
    {
        if(Instance == null)
        {
            Instance = this;
        }
        else
        {
            Destroy(gameObject);
        }

        DontDestroyOnLoad(gameObject);
    }

    [System.Serializable]
    public class SaveData
    {
        [SerializeField] public List<QuestTemplate.QuestData> questDatas = new List<QuestTemplate.QuestData>();

        [JsonConstructor]
        public SaveData(List<QuestTemplate.QuestData> questDatas)
        {
            this.questDatas = questDatas ;
        }

        public SaveData()
        {
            questDatas = new List<QuestTemplate.QuestData>();
        }
    }
}

Every QuestTemplate also contains 2 other classes, one for messages that are shown once the quest is completed (QuestMessage), and one for all data that needs to be saved (QuestData).

As you can see there are 2 QuestMessage lists in total, this is because it isn't possible to save Dictionaries with complex keys properely with NewtonSoft Json, but I still need to know which message I am storing, so I put a whole list of QuestMessages in there.

public class QuestTemplate : ScriptableObject
{
    [Header("Settings:")]
    public QuestData questData;
    public float timeLimit;
    public List<QuestMessage> messages = new List<QuestMessage>();

    [System.Serializable]
    public class QuestData
    {
        public string saveName;
        [HideInInspector] public float completionTime;
        [HideInInspector] public List<QuestMessage> messages = new List<QuestMessage>();
    }

    [System.Serializable]
    public class QuestMessage
    {
        public string headline;
        public string sender;
        [public string text;
        [HideInInspector] public bool read = false;
    }
}

This is my script I use for saving / loading:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System;
using Newtonsoft.Json;

public static class SavingSystem
{
    private static readonly string SaveFileFolder = Application.persistentDataPath + "/Saves/";

    public static void Save(GameStatistics.SaveData data)
    {
        if(Directory.Exists(SaveFileFolder) == false)
        {
            Directory.CreateDirectory(SaveFileFolder);
        }

        string _savePath = SaveFileFolder + "Data.json";

        string _data = JsonConvert.SerializeObject(data, formatting: Formatting.Indented, new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore});

        File.WriteAllText(SaveFileFolder + "Data.json", _data);
    }

    public static GameStatistics.SaveData Load()
    {
        string _savePath = SaveFileFolder + "Data.json";

        if(File.Exists(_savePath) == true)
        {
            string _data = File.ReadAllText(_savePath);
            return (JsonConvert.DeserializeObject<GameStatistics.SaveData>(_data));
        }
        else
        {
            return(null);
        }
    }

    public static void DeleteData()
    {
        string _savePath = SaveFileFolder + "Data.json";

        if(File.Exists(_savePath) == true)
        {
            File.Delete(_savePath);
        }
        else
        {
            Debug.LogWarning("File doesn't exist.");
        }
    }
}

The Problem:

The saving to Json and loading from Json itself seems to be working alright, the problem is that whenever I am changing read (e.g. to true when the message has been read) the value seems to be persistent even when deleting the Data.json file, even restarting the game doesn't reset it, the only thing that managed to do that was restarting my PC completely.

I did some checks with Debug.Logs and it seems that whenever a message.read in the completedQuests' questDatas is changed, that also changes the base array quests, which is then persistent until I restart my PC.

I have absolutely no idea what to do, I tried so many thing and I have literally spend like 3 days in total working on some sort of fix.

If you have any idea what could be the problem, be it some strange behaviour of a list or array or the way my save structure is set up, please post it, at this point I am willing to try anything.

\$\endgroup\$
0

1 Answer 1

0
\$\begingroup\$

The problem you encounter comes from the fact that you use ScriptableObject. From the docu:

A ScriptableObject is a data container that you can use to save large amounts of data, independent of class instances. One of the main use cases for ScriptableObjects is to reduce your Project’s memory usage by avoiding copies of values. This is useful if your Project has a Prefab that stores unchanging data in attached MonoBehaviour scripts.

Every time you instantiate that Prefab, it will get its own copy of that data. Instead of using the method, and storing duplicated data, you can use a ScriptableObject to store the data and then access it by reference from all of the Prefabs. This means that there is one copy of the data in memory.

That means when you change the value of your quest at run time, your scriptable object gets updated. So your QuestTemplate should not contain things like completionTime or read. Instead give it a unique quest id. Now you can make a normal GameObject - or even better, use the plain old data structure (POD has the benefit that serializing and dezserializing becomes much easier), attach the values that you would like to persist/ that can be updated at runtime. Your save load function does not need to actually save things like the text in QuestMessage, you would store the id of quest XY and the status. This will make things as well easier when you want to implement different languages.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ I'd recommend storing the completionTime and read status in a "plain old data structure" class or struct, rather than anything related to GameObjects/MonoBehaviours. This makes serializing and deserializing it trivial. \$\endgroup\$
    – DMGregory
    Jul 30, 2023 at 11:08
  • \$\begingroup\$ I've changed everything to use another class to store every variable that needs to be saved, and it is working now! The only slight problem I have now, is that accessing values when working with collections of e.g. QuestMessages is really complex, especially when not saving the entire message in the data I need 2 loops to get the text, headline, etc... from the templates, and then another loop to get the "read" of the message in the message datas. So maybe You have some ideas how to improve on the current structure. \$\endgroup\$
    – Felix193
    Jul 30, 2023 at 16:48
  • \$\begingroup\$ @Felix193 Simply make a new question with your new code. A question should always focus on one problem. If this solved your direct asked question, you can accept it as an answer by the checkmark. \$\endgroup\$
    – Zibelas
    Jul 30, 2023 at 18:00

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .