Search Unity

Bug JSON Saving System loads wrong information

Discussion in 'Scripting' started by MinhocaNice, Jan 23, 2022.

  1. MinhocaNice

    MinhocaNice

    Joined:
    May 3, 2020
    Posts:
    249
    I have absolutely no idea what is going on. I made a script for getting the Player's options in the options menu and it simply refuses to load the options correctly. It only loads the Resolution correctly and sets everything else to what appears to be 0. I can't find a single mistake in my code.

    Here is the breakdown of each portion of the code:

    Awake
    method, sets the ResolutionDropdown with all resolutions available and sets the default options.

    Code (CSharp):
    1.     void Awake()
    2.     {
    3.         if (ResolutionDropdown != null)
    4.         {
    5.             ResolutionArray = Screen.resolutions.Select(resolution => new Resolution { width = resolution.width, height = resolution.height }).Distinct().ToArray();
    6.             ResolutionDropdown.ClearOptions();
    7.  
    8.             int CurrentResolutionIndex = 0;
    9.             List<string> DropdownOptions = new List<string>();
    10.             for (int i = 0; i < ResolutionArray.Length; i++)
    11.             {
    12.                 string Option = ResolutionArray [i].width + " x " + ResolutionArray [i].height;
    13.                 DropdownOptions.Add(Option);
    14.  
    15.                 if (ResolutionArray [i].width == Screen.width && ResolutionArray [i].height == Screen.height)
    16.                 {
    17.                     CurrentResolutionIndex = i;
    18.                 }
    19.             }
    20.  
    21.  
    22.             ResolutionDropdown.AddOptions(DropdownOptions);
    23.             ResolutionDropdown.value = CurrentResolutionIndex;
    24.             ResolutionDropdown.RefreshShownValue();
    25.         }
    26.  
    27.         if (GraphicsQualityDropdown != null)
    28.         {
    29.             for (int i = 0; i < GraphicsQualityArray.Length; i++)
    30.             {
    31.                 GraphicsQualityArray [i] = GraphicsQualityDropdown.options[i].text;
    32.             }
    33.         }
    34.  
    35.         SetDefaultOptions();
    36.         LoadOptions();
    37.         SaveOptions();
    38.     }
    SetDefaultOptions()
    creates a file named defaultoptions.json that should be accessed in case the game can't find the actual options.json file.

    Code (CSharp):
    1.     public void SetDefaultOptions()
    2.     {
    3.         OptionsScript.Resolution = 0;
    4.         OptionsScript.Fullscreen = false;
    5.         OptionsScript.Volume = -40f;
    6.         OptionsScript.GraphicsQuality = 0;
    7.  
    8.         //Save Options to a .json file//
    9.         string DefaultOptionsJSON = JsonUtility.ToJson(OptionsScript);
    10.         File.WriteAllText(Application.dataPath + "/defaultoptions.json", DefaultOptionsJSON);
    11.  
    12.         if (!File.Exists(Application.dataPath + "/options.json"))
    13.         {
    14.             File.WriteAllText(Application.dataPath + "/options.json", DefaultOptionsJSON);
    15.         }
    16.     }
    As the name suggests,
    LoadOptions()
    loads all options from file (options.json), which is updated everytime the player changes one of the main options (resolution, whether the game is fullscreen or not, audio volume, and the graphical quality, respectively).

    Code (CSharp):
    1. public void LoadOptions()
    2.     {
    3.         //Load Options from a .json file//
    4.         if (File.Exists(Application.dataPath + "/options.json"))
    5.         {
    6.             string OptionsJSON = File.ReadAllText(Application.dataPath + "/options.json");
    7.             JsonUtility.FromJsonOverwrite(OptionsJSON, OptionsScript);
    8.  
    9.             SetResolution(OptionsScript.Resolution);
    10.             SetFullscreen(OptionsScript.Fullscreen);
    11.             SetVolume(OptionsScript.Volume);
    12.             SetGraphicsQuality(OptionsScript.GraphicsQuality);
    13.         }
    14.         else
    15.         {
    16.             ResetAllOptions();
    17.         }
    18.     }
    SaveOptions()
    saves the options everytime the player changes them in options.json, so next time the game is opened it can open that file to know the options the player has chosen previously.
    Code (CSharp):
    1.     public void SaveOptions()
    2.     {
    3.         //Save Options to a .json file//
    4.         string OptionsJSON = JsonUtility.ToJson(OptionsScript);
    5.         File.WriteAllText(Application.dataPath + "/options.json", OptionsJSON);
    6.     }
    In case anything goes wrong, or the player simply wants all options to be set to default values, the
    ResetAllOptions()
    should come to rescue and load the options from defaultoptions.json instead of options.json. This did not happen in the error mentioned, though it shouldn't happen in the first place.
    Code (CSharp):
    1.     public void ResetAllOptions()
    2.     {
    3.         //Load Options from a .json file//
    4.         if (File.Exists(Application.dataPath + "/defaultoptions.json"))
    5.         {
    6.             string DefaultOptionsJSON = File.ReadAllText(Application.dataPath + "/defaultoptions.json");
    7.             JsonUtility.FromJsonOverwrite(DefaultOptionsJSON, OptionsScript);
    8.  
    9.             SetResolution(OptionsScript.Resolution);
    10.             SetFullscreen(OptionsScript.Fullscreen);
    11.             SetVolume(OptionsScript.Volume);
    12.             SetGraphicsQuality(OptionsScript.GraphicsQuality);
    13.         }
    14.         else
    15.         {
    16.             SetDefaultOptions();
    17.         }
    18.  
    19.         SaveOptions();
    20.     }
    These methods set the main options the player can choose from and are set in the options menu with input from dropdowns, buttons, sliders, etc. Every time one of them is changed, it immediately saves the new values to the options.json file with
    SaveOptions()
    . They seem to be working correctly.
    Code (CSharp):
    1.     public void SetResolution (int ResolutionIndex)
    2.     {
    3.         Resolution AppliedResolution = ResolutionArray[ResolutionIndex];
    4.         Screen.SetResolution(AppliedResolution.width, AppliedResolution.height, Screen.fullScreen);
    5.  
    6.         if (ResolutionText != null)
    7.         {
    8.             ResolutionText.text = "Resolution: " + ResolutionArray [ResolutionIndex].width + " x " + ResolutionArray [ResolutionIndex].height;
    9.         }
    10.  
    11.         OptionsScript.Resolution = ResolutionIndex;
    12.         SaveOptions();
    13.     }
    14.  
    15.     public void SetFullscreen (bool Fullscreen)
    16.     {
    17.         Screen.fullScreen = Fullscreen;
    18.         OptionsScript.Fullscreen = Fullscreen;
    19.         SaveOptions();
    20.     }
    21.  
    22.     public void SetVolume (float Volume)
    23.     {
    24.         Mixer.SetFloat("MasterVolume", Volume);
    25.  
    26.         if (VolumeSlider != null)
    27.         {
    28.             if (VolumeText != null)
    29.             {
    30.                 VolumeText.text = "Volume: " + VolumeSlider.normalizedValue * 100f + "%";
    31.             }
    32.         }
    33.  
    34.         OptionsScript.Volume = Volume;
    35.         SaveOptions();
    36.     }
    37.  
    38.     public void SetGraphicsQuality (int Quality)
    39.     {
    40.         QualitySettings.SetQualityLevel(Quality);
    41.  
    42.         if (GraphicsQualityText != null)
    43.         {
    44.             GraphicsQualityText.text = "Graphics quality: " + GraphicsQualityArray [Quality];
    45.         }
    46.  
    47.         OptionsScript.GraphicsQuality = Quality;
    48.         SaveOptions();
    49.     }
    From what I could analyse, the culprit seems to be
    LoadOptions()
    as it seems to not read the file completely, instead only reading the first "resolution" value and ignoring all rest. I checked the options.json itself and it does change correctly when I change the options in the menu. There is nothing in code that seems to be wrong though and I have no clue why this method is not working, it seems Unity hates me.

    Here is the full code in case it's needed:

    Code (CSharp):
    1. using System.IO;
    2. using System.Linq;
    3. using System.Collections;
    4. using System.Collections.Generic;
    5. using UnityEngine;
    6. using UnityEngine.UI;
    7. using UnityEngine.Audio;
    8.  
    9. public class OptionsManager : MonoBehaviour
    10. {
    11.     public AudioMixer Mixer;
    12.     public Slider VolumeSlider;
    13.     public Text VolumeText;
    14.     public Resolution[] ResolutionArray;
    15.     public Dropdown ResolutionDropdown;
    16.     public Text ResolutionText;
    17.     public string [] GraphicsQualityArray;
    18.     public Dropdown GraphicsQualityDropdown;
    19.     public Text GraphicsQualityText;
    20.     public Options OptionsScript;
    21.  
    22.     void Awake()
    23.     {
    24.         if (ResolutionDropdown != null)
    25.         {
    26.             ResolutionArray = Screen.resolutions.Select(resolution => new Resolution { width = resolution.width, height = resolution.height }).Distinct().ToArray();
    27.             ResolutionDropdown.ClearOptions();
    28.  
    29.             int CurrentResolutionIndex = 0;
    30.             List<string> DropdownOptions = new List<string>();
    31.             for (int i = 0; i < ResolutionArray.Length; i++)
    32.             {
    33.                 string Option = ResolutionArray [i].width + " x " + ResolutionArray [i].height;
    34.                 DropdownOptions.Add(Option);
    35.  
    36.                 if (ResolutionArray [i].width == Screen.width && ResolutionArray [i].height == Screen.height)
    37.                 {
    38.                     CurrentResolutionIndex = i;
    39.                 }
    40.             }
    41.  
    42.  
    43.             ResolutionDropdown.AddOptions(DropdownOptions);
    44.             ResolutionDropdown.value = CurrentResolutionIndex;
    45.             ResolutionDropdown.RefreshShownValue();
    46.         }
    47.  
    48.         if (GraphicsQualityDropdown != null)
    49.         {
    50.             for (int i = 0; i < GraphicsQualityArray.Length; i++)
    51.             {
    52.                 GraphicsQualityArray [i] = GraphicsQualityDropdown.options[i].text;
    53.             }
    54.         }
    55.  
    56.         SetDefaultOptions();
    57.         LoadOptions();
    58.         SaveOptions();
    59.     }
    60.  
    61.     public void SetResolution (int ResolutionIndex)
    62.     {
    63.         Resolution AppliedResolution = ResolutionArray[ResolutionIndex];
    64.         Screen.SetResolution(AppliedResolution.width, AppliedResolution.height, Screen.fullScreen);
    65.  
    66.         if (ResolutionText != null)
    67.         {
    68.             ResolutionText.text = "Resolution: " + ResolutionArray [ResolutionIndex].width + " x " + ResolutionArray [ResolutionIndex].height;
    69.         }
    70.  
    71.         OptionsScript.Resolution = ResolutionIndex;
    72.         SaveOptions();
    73.     }
    74.  
    75.     public void SetFullscreen (bool Fullscreen)
    76.     {
    77.         Screen.fullScreen = Fullscreen;
    78.         OptionsScript.Fullscreen = Fullscreen;
    79.         SaveOptions();
    80.     }
    81.  
    82.     public void SetVolume (float Volume)
    83.     {
    84.         Mixer.SetFloat("MasterVolume", Volume);
    85.  
    86.         if (VolumeSlider != null)
    87.         {
    88.             if (VolumeText != null)
    89.             {
    90.                 VolumeText.text = "Volume: " + VolumeSlider.normalizedValue * 100f + "%";
    91.             }
    92.         }
    93.  
    94.         OptionsScript.Volume = Volume;
    95.         SaveOptions();
    96.     }
    97.  
    98.     public void SetGraphicsQuality (int Quality)
    99.     {
    100.         QualitySettings.SetQualityLevel(Quality);
    101.  
    102.         if (GraphicsQualityText != null)
    103.         {
    104.             GraphicsQualityText.text = "Graphics quality: " + GraphicsQualityArray [Quality];
    105.         }
    106.  
    107.         OptionsScript.GraphicsQuality = Quality;
    108.         SaveOptions();
    109.     }
    110.  
    111.     public void ResetAllOptions()
    112.     {
    113.         //Load Options from a .json file//
    114.         if (File.Exists(Application.dataPath + "/defaultoptions.json"))
    115.         {
    116.             string DefaultOptionsJSON = File.ReadAllText(Application.dataPath + "/defaultoptions.json");
    117.             JsonUtility.FromJsonOverwrite(DefaultOptionsJSON, OptionsScript);
    118.  
    119.             SetResolution(OptionsScript.Resolution);
    120.             SetFullscreen(OptionsScript.Fullscreen);
    121.             SetVolume(OptionsScript.Volume);
    122.             SetGraphicsQuality(OptionsScript.GraphicsQuality);
    123.         }
    124.         else
    125.         {
    126.             SetDefaultOptions();
    127.         }
    128.  
    129.         SaveOptions();
    130.     }
    131.  
    132.     public void SetDefaultOptions()
    133.     {
    134.         OptionsScript.Resolution = 0;
    135.         OptionsScript.Fullscreen = false;
    136.         OptionsScript.Volume = -40f;
    137.         OptionsScript.GraphicsQuality = 0;
    138.  
    139.         //Save Options to a .json file//
    140.         string DefaultOptionsJSON = JsonUtility.ToJson(OptionsScript);
    141.         File.WriteAllText(Application.dataPath + "/defaultoptions.json", DefaultOptionsJSON);
    142.  
    143.         if (!File.Exists(Application.dataPath + "/options.json"))
    144.         {
    145.             File.WriteAllText(Application.dataPath + "/options.json", DefaultOptionsJSON);
    146.         }
    147.     }
    148.  
    149.     public void LoadOptions()
    150.     {
    151.         //Load Options from a .json file//
    152.         if (File.Exists(Application.dataPath + "/options.json"))
    153.         {
    154.             string OptionsJSON = File.ReadAllText(Application.dataPath + "/options.json");
    155.             JsonUtility.FromJsonOverwrite(OptionsJSON, OptionsScript);
    156.  
    157.             SetResolution(OptionsScript.Resolution);
    158.             SetFullscreen(OptionsScript.Fullscreen);
    159.             SetVolume(OptionsScript.Volume);
    160.             SetGraphicsQuality(OptionsScript.GraphicsQuality);
    161.         }
    162.         else
    163.         {
    164.             ResetAllOptions();
    165.         }
    166.     }
    167.  
    168.     public void SaveOptions()
    169.     {
    170.         //Save Options to a .json file//
    171.         string OptionsJSON = JsonUtility.ToJson(OptionsScript);
    172.         File.WriteAllText(Application.dataPath + "/options.json", OptionsJSON);
    173.     }
    174. }
    175.  
     
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,930
    I can't see that you've posted the actual class that encapsulates the options information, but I see you're using the JSON utility, which always to be a source of problems when writing and reading JSON.

    I would try using a more robust JSON like Newtonsoft.JSON. You shouldn't need to change anything other than the scripts that load and write the file, and perhaps put a few attributes on your 'Options' class as well.
     
    MinhocaNice likes this.
  3. MinhocaNice

    MinhocaNice

    Joined:
    May 3, 2020
    Posts:
    249
    Here it is:
    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5.  
    6. [Serializable]
    7. [CreateAssetMenu]
    8. public class Options : ScriptableObject
    9. {
    10.     [Header("Main")]
    11.     public int Resolution;
    12.     public bool Fullscreen;
    13.     public float Volume;
    14.     public int GraphicsQuality;
    15. }
    16.  

    What is the matter with JSON utility?
     
  4. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,930
    Well I think I can see the problem. While JSON utility can serialise Monobehaviours and Scriptable objects, you can't recreate such objects by deserialising data. You should encapsulate your options in a plain serialisable C# class and write/read that instead.

    It has a lot of limitations with certain very common data structures (normally to do with collections and dictionaries). But it also gives no such warning in situations where it can't serialise/deserialise data, hence this situation where you're left scratching your head when something isn't working.
     
    MinhocaNice likes this.
  5. MinhocaNice

    MinhocaNice

    Joined:
    May 3, 2020
    Posts:
    249
    I see, though I am trying to write down
    Newtonsoft.JSON
    in code and it's not appearing anything, does this need a special library to be accessed?
     
  6. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,930
    You'll need to put
    using Newtonsoft.Json;
    wherever you need to use it, that's if any of the packages you're currently using have already added it to your project (a lot of Unity packages use it). You can just check this by seeing if there's a 'Newtonsoft Json' folder in your project window's Packages folder. If not I believe there's a Unity package on the asset store for it (it's free).

    And I suggest reading the documentation for it too: https://www.newtonsoft.com/json/help/html/introduction.htm as there's a little more you need to do than you might with the built in Unity serialiser. Though as noted you will only need to slightly adjust the script for reading and writing the file, and decorate your plain C# options class with some attributes.
     
  7. MinhocaNice

    MinhocaNice

    Joined:
    May 3, 2020
    Posts:
    249
    When searching about this, I found conflicting information. I saw people saying JSONUtility is faster and more recommended for simpler applications, and that Newtonsoft.Json had multiple different variants made along the years.

    I tried changing the Options class to make it more easily serializable but it didn't change anything:
    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4. [Serializable]
    5. public class Options
    6. {
    7.     public int Resolution;
    8.     public bool Fullscreen;
    9.     public float Volume;
    10.     public int GraphicsQuality;
    11. }
    12.  
    I am still not sure if I should really get rid of JSON Utility. I don't want to download a third party package for something so simple that has an actual official API. I will wait for a second opinion on this issue.
     
  8. MinhocaNice

    MinhocaNice

    Joined:
    May 3, 2020
    Posts:
    249
    Another funny thing is that I used the same exact method for a much more lenghy method that kept track of all the controls the player binded and the mouse sensitivity and yet that seemed to work fine while I am struggling with saving a method that has nothing but 2 integers, a boolean and a float... :/

    Just tried FromJson instead of FromJsonOverwrite and had the same issue, there has to be a problem with my code there is no way in hell something so basic is not being handled properly by JSON Utility and no one else found that out.
     
  9. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    1,261
    When you are deserializing a ScriptableObject, you must use the overwrite method of the JsonUtility. I'm not sure but you may need to instantiate that ScriptableObject and then do the overwrite on that object.

    How you should work it is; create an Options asset that holds the defaults. Make a reference to that in your Options manager. When loading check if the options.json file exists. If it doesn't create it using the reference to the default options. If it does exist load it and use the overwrite method to overwrite the reference to the default. Don't worry it won't make permanent changes to the default reference, you can only do that in the editor.

    Another problem I can see is that you are saving it to Application.dataPath. In the wild that will not work as expected. You need to use Application.persistentDataPath, this is where you can read and write to on all platforms. As for your default settings, you are doing the same thing and trying to set them up in the wrong place.
     
  10. MinhocaNice

    MinhocaNice

    Joined:
    May 3, 2020
    Posts:
    249
    I did use overwrite, I changed later because it wasn't working properly, then I tried using a plain class instead of ScriptableObject and it still didn't work.

    Yes, I tried all of that.

    Alright, I will try it with
    Application.persistentDataPath
    . I am not sure what you meant with the default settings, what is wrong with that?
     
  11. MinhocaNice

    MinhocaNice

    Joined:
    May 3, 2020
    Posts:
    249
    I just tried
    Application.persistentDataPath
    and did not fix the issue. Interestingly, I also used it on the other saving method I mentioned earlier that is working and it continues to function normally even though it's much more complex than this one.

    I think I will stick with
    Application.dataPath
    however because I don't like filling up space in the user's AppData folder without them being told about it, I much rather have everything related to the game in a single folder together with the executable (and this game is only for PC anyways so I don't worry about other platforms).
     
  12. Chris-Trueman

    Chris-Trueman

    Joined:
    Oct 10, 2014
    Posts:
    1,261
    You may experience issues outside of the editor with it so be warned. I was told it was a read only directory, but that may only be for mobile/windows store.

    The method I described will already have a ScriptableObject waiting to be overwritten so you only need to check for options.json and JsonUtility.FromJsonOverwrite(OptionsJson, OptionsScript) if it does exist, if not then create/save the options.json file. You already have this setup somewhat, you need to put an asset into the OptionsScript reference and use that as the defaults instead of hard coding it.

    The creation of defaultoptions.json is redundant as well. If the code knows what the default settings are just create options.json with the defaults if it doesn't exits. You are wasting time reading data from a file when you already have it hard coded in the script or in the asset reference.

    As for it not working, make sure the output of the json is correct, take a look at the file. Use some debug statements to follow the process and print out the json string before saving it and after loading the file. Debug.log is your best friend use it and use it often, it will help you track down what is happening with your code.