How to save List with help of JSON? - c#

everyone!
Tried to make saver of my List with help of combination JSON and PlayerPrefs but stucked with the problem. It don't save my list and don't load. It only giving me my list with zero elements.
Code:
private List<Toggle> _listActiveToggles = new List<Toggle>();
public void Awake()
{
Load();
foreach (Toggle toggle in _listActiveToggles)
{
toggle.isOn = true;
}
}
public void ChooseUsableBuffs(Toggle toggle)
{
int level = PlayerPrefs.GetInt("HDD") / 10;
if (_listActiveToggles.Any(x => toggle))
{
_listActiveToggles.Remove(toggle);
Debug.Log(_listActiveToggles.Count);
return;
}
if (_listActiveToggles.Count >= level)
{
_listActiveToggles[0].isOn = false;
_listActiveToggles.RemoveAt(0);
}
_listActiveToggles.Add(toggle);
Save();
Debug.Log(_listActiveToggles.Count);
}
public void Save()
{
PlayerPrefs.SetString("Key" ,JsonUtility.ToJson(_listActiveToggles));
}
public void Load()
{
_listActiveToggles = JsonUtility.FromJson<List<Toggle>>(PlayerPrefs.GetString("Key"));
}
Tried to check what JSON contains and it showed only: {}

The JsonUtility can not directly serialized array and list types.
You rather need a wrapper class like
[Serializable]
public class ToggleData
{
public List<Toggle> _listActiveToggles = new List<Toggle>();
}
public ToggleData data;
Which now you can serialize
var json = JsonUtility.ToJson(Data);
and deserilialize
JsonUtility.FromJsonOverwrite(json, data);
However, in your specific case I guess it will not work anyway. UnityEngine.UI.Toggle is of type MonoBehaviour and you can not simply deserilialize these from JSON!
You can only store their settings and loading settings into existing toggles.
I guess actually it should be enough to store a list of bool to JSON and iterate over your toggles and the list and do e.g.
using System.Linq;
...
[Serializable]
public class ToggleData
{
public List<bool> ToggleStates = new List<book>();
}
public ToggleData data;
[SerializeField] private List<Toggle> _listActiveToggles = new List<Toggle>();
And then save via
data.ToggleStates = _listActiveToggles.Select(t => t.isOn).ToList();
var json = JsonUtility.ToJson(Data);
and deserilialize via
JsonUtility.FromJsonOverwrite(json, data);
for(var i = 0; i < Mathf.Min(data.ToggleStates.Count, _listActiveToggles.Count); i++)
{
_listActiveToggles[i].isOn = data.ToggleStates[i];
}
Note however that this means you should reference all Toggles you want to save the state for in your list via the Inspector and can not simply remove or add entries on runtime!

Related

Unity problems with loading save

Loosing my mind over it, but still cant find what's wrong. I have 2 List variables (items, playerEquipment.equipment), trying to save both of them and load after, but it gives me strange results. For items its always good, saves and loads all the time, but for playerEquipment.equipment behaviour is different - if i save and then immidiatly load without exiting play mode, i get right result from load function, but if i save, stop play mode, start play mode, and then load - i get list of "null" is result.
Here is my save code
public void SavePlayer()
{
SaveData data = new SaveData();
data.level = level;
data.nick = nick;
data.experience = experience;
data.experienceToNextLevel = experienceToNextLevel;
data.maxHP = maxHP;
data.minAttack = minAttack;
data.maxAttack = maxAttack;
data.coins = coins;
data.items = items;
data.equipment = playerEquipment.equipment;
for (int i = 0; i < data.equipment.Count; i++)
Debug.Log("Saved type " + data.equipment[i].type + " with id " + data.equipment[i].id+" and type "+ data.equipment[i].type);
//Save data from PlayerInfo to a file named players
DataSaver.saveData(data, "players");
}
Here is my load code
public void LoadPlayer()
{
SaveData data = DataSaver.loadData<SaveData>("players");
if (data == null)
{
Debug.Log("ERROR: data not loaded");
return;
}
level = data.level;
nick = data.nick;
experience = data.experience;
experienceToNextLevel = data.experienceToNextLevel;
maxHP = data.maxHP;
currentHP = data.maxHP;
minAttack = data.minAttack;
maxAttack = data.maxAttack;
coins = data.coins;
items = data.items;
playerEquipment.equipment = data.equipment;
}
SaveData is
[Serializable]
class SaveData
{
//all data types and names that go to save
public int level;
public string nick;
public int experience;
public int experienceToNextLevel;
public int maxHP;
public float minAttack;
public float maxAttack;
public int coins;
//public List<String> itemsString;
//public List<String> equipmentString;
public List<Item> items;
public List<Item> equipment;
}
Save code i grabbed from this answer and didnt change it
UPD:
Item class is
public enum ItemType { HELMET, SHOULDERS, WEAPON_MAIN, WEAPON_SECOND, BODY, ARMOR, HANDS, PANTS, BOOTS }
[CreateAssetMenu(menuName = "item")]
[Serializable]
public class Item : ScriptableObject
{
public int id;
public Sprite sprite;
public string itemName;
public ItemType type;
}
You can not save and load a list of ScriptableObjects like that. JsonUtility.ToJson saves these references as InstanceIDs which are nor persistent. That is why it works when you do not stop play mode.
Since you already implemented an ID you could save a list of IDs instead and add a method which finds them by that references and adds adds them back to your playerEquipment.equipment
Edit: This means your statement of it working for the item list is likely false, you should check and edit your answer accordingly.

Unity: Cannot modify / add values to an array in System.Serializable

I have a System.Serializable class
[System.Serializable]
public class _answersData
{
public string word;
public string wordmeaning;
public string wordmeaning2;
public GameObject result;
}
I then have a monobehaviour class where an array of _answerData class is declared
public class gamePlay : MonoBehaviour
{
public _answersData[] answersData;
}
I am then trying to add data to _answerData array from another script using below method
public class challengeScript: MonoBehaviour
{
void Start()
{
gameplayScript = gameObject.GetComponent<gamePlay>();
populate();
}
void populate()
{
answersCount = int.Parse(snapshot.Child("Answers").Child("Count").Value.ToString());
gameplayScript.answersData = new _answersData[answersCount];
for (int i = 0; i < answersCount; i++)
{
string positionNode = "Answer" + (i + 1).ToString();
string _word = snapshot.Child("Answers").Child(positionNode).Child("Word").Value.ToString();
gameplayScript.answersData[i].word = _word;
}
}
}
but I am getting NullReferenceException at 'gameplayScript.answersData[i].word = _word' line
Here's the error:
NullReferenceException: Object reference not set to an instance of an object
Can someone guide what I am doing wrong here?
Just because it is [Serializable] doesn't mean the elements are automatically created once you create the array. By default all reference type elements will still be null.
Only within Unity the Inspector itself automatically creates instances of [Serializable] types and initializes the array itself.
In code you have to actually create your element instances yourself like e.g.
var answerData = new _answerData();
answerData.word = _word;
gameplayScript.answersData[i] = answerData;

How can I init a class with string array?

I have this script that get the properties by names from a shader :
At the bottom I'm changing one of the properties values by giving the property name: "Vector1_570450D5"
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.PlayerLoop;
public class ChangeShaders : MonoBehaviour
{
public Material material;
public float duration = 1;
private List<string> propertiesNames = new List<string>();
private void Awake()
{
material = GetComponent<Renderer>().material;
var propertiesCount = ShaderUtil.GetPropertyCount(material.shader);
for(int i = 0; i < propertiesCount; i++)
{
propertiesNames.Add(ShaderUtil.GetPropertyName(material.shader, i));
}
}
void Update()
{
var currentValue = Mathf.Lerp(-1, 1, Mathf.PingPong(Time.time / duration, 1));
material.SetFloat("Vector1_570450D5", currentValue);
}
}
but instead typing manual the property name I want to create a class for each property name so I will be able to type inside the SetFloat something like :
material.SetFloat(myProperties.Vector1_570450D5, currentValue);
In this case there are 5 properties so I want to be able to do :
material.SetFloat(myProperties.Vector1_570450D5, currentValue);
Or
material.SetFloat(myProperties.Color_50147CDB, currentValue);
So I thought to make this script with the attribute executeallways to create a editor script only for getting the properties and then to use the properties in this mono script like in the examples I gave.
Just to your title: You have a List<string>, not an array ;)
Before filling it with values you would need to initialize it. You can also do that together with the field declaration:
public List<string> names = new List<string>();
Even simpler would then be a proper constructor and e.g. using
public class MyClass
{
public List<string> names = new List<string>();
public MyClass(List<string> initialNames)
{
// This creates a new List<string> copying all elements of initialNames
names = new List<string>(initialNames);
}
}
And then use it like
var myClass = new MyClass(names);
If I understand correct you are further looking for a property like e.g.
public string this[int index] => names[i];
which would allow you to access a specific entry directly via
myClass[i];
instead of
myClass.names[i];

Protobuf.NET - Issue with "Possible Recursion Detected" when the triggering references are in a List

First let me show a simple test case of the problem and how to trigger it. Here is the class:
class ProtoRecurseTest
{
private int nextPayload = 1;
public int Payload { get; private set; } = 0;
public ProtoRecurseTest Back { get; private set; } = null;
public List<ProtoRecurseTest> Children { get; set; } = new List<ProtoRecurseTest>();
public ProtoRecurseTest Add()
{
ProtoRecurseTest result = new ProtoRecurseTest(this, nextPayload++);
Children.Add(result);
return result;
}
public ProtoRecurseTest()
{
}
private ProtoRecurseTest(ProtoRecurseTest parent, int payload)
{
Back = parent;
this.Payload = payload;
nextPayload = payload + 1;
}
private static void ToStringHelper(ProtoRecurseTest proto, StringBuilder sb)
{
sb.Append(proto.Payload + " -> ");
// another little hassle of protobuf due to empty list -> null deserialization
if (proto.Children != null)
{
foreach (var child in proto.Children)
ToStringHelper(child, sb);
}
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
ToStringHelper(this, sb);
return sb.ToString();
}
}
There are no protobuf annotations as that is being taken care of programmatically. I have manually ensured that the class along with Back and Children are all added to the schema with .AsReferenceDefault = true.
The recursion triggering occurs when an instance is populated to a depth of at least 8 bizarrely enough. 7 is fine. Population code is straight forward:
ProtoRecurseTest recurseTest = new ProtoRecurseTest();
ProtoRecurseTest recurseItem = recurseTest;
for (int i = 0; i < 8; i++)
recurseItem = recurseItem.Add();
And then serialize recurseTest. This behavior only occurs when the children are in a list but in a list it occurs even with only 1 child per list as you end up with from the sample populating code. When I replaced the children with a single reference everything serialized fine.
This is using ProtoBuf.NET 2.1.0.0.
Here is a solution, but one I'm not particularly fond of. It was based on #marc-gravell 's answer to this question.
class ProtoRecurseTest : ISerializationManagementCallbacks
{
private int nextPayload = 1;
public int Payload { get; private set; } = 0;
public ProtoRecurseTest Back { get; private set; } = null;
public List<ProtoRecurseTest> Children { get; set; } = new List<ProtoRecurseTest>();
public ProtoRecurseTest Add()
{
ProtoRecurseTest result = new ProtoRecurseTest(this, nextPayload++);
Children.Add(result);
return result;
}
public ProtoRecurseTest()
{
}
private ProtoRecurseTest(ProtoRecurseTest parent, int payload)
{
Back = parent;
this.Payload = payload;
nextPayload = payload + 1;
}
private static void ToStringHelper(ProtoRecurseTest proto, StringBuilder sb)
{
sb.Append(proto.Payload + " -> ");
// another little hassle of protobuf due to empty list -> null deserialization
if (proto.Children != null)
{
foreach (var child in proto.Children)
ToStringHelper(child, sb);
}
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
ToStringHelper(this, sb);
return sb.ToString();
}
static void PreSerializationHelper(ProtoRecurseTest instance)
{
instance.Back = null;
foreach (var child in instance.Children)
PreSerializationHelper(child);
}
public void BeforeSerialization()
{
PreSerializationHelper(this);
}
static void PostDeserializationHelper(ProtoRecurseTest instance, ProtoRecurseTest parent)
{
if (instance.Children == null)
instance.Children = new List<ProtoRecurseTest>();
instance.Back = parent;
foreach (var child in instance.Children)
PostDeserializationHelper(child, instance);
}
public void AfterDeserialization()
{
PostDeserializationHelper(this, null);
}
}
The calls to serialize/deserialize now simply check if the type can be casted to the ISerializationManagementCallbacks (which was just provide a BeforeSerialization and AfterDeserialization method) before doing their business and then calling the appropriate methods if so. And it works fine.
However, I'm not really fond of mixing ever more serialization issues into my codebase - that's actually why the schema is generated programmatically. Ideally I'd like to separate the serialization entirely but the empty list -> null issue (which not to say it's an "issue" per se but just an undesirale part of the protobuf standard) already makes that impossible, but this would be another even more esoteric issue to work around and I do think this one might indeed be an issue.

Self-contained generic memento

Dearest fellow programmers,
I seem to lack some understanding as of how the referencing works in C#.
The case:
I tried to implement some sort of Memento proxy which would wrap an interface and store every parameter that we're provided to the method calls and store these into a list.
Whenever necessary we could call the RestoreState and the objects would "reset" to the original state.
The code:
Consumer and model object
class Program
{
static void Main(string[] args)
{
IMemento memento = new Memento();
PrestationInfo prestationInfo2 = new PrestationInfo { Advance = 2 };
memento.Add(prestationInfo2);
Console.WriteLine(prestationInfo2.Advance); //Expect 2
prestationInfo2.Advance = 1;
Console.WriteLine(prestationInfo2.Advance); //Expect 1
memento.RestoreState();
Console.WriteLine(prestationInfo2.Advance); //Expect 2, but still 1
Console.ReadKey();
}
}
[Serializable]
public class PrestationInfo
{
public int Advance { get; set; }
}
Memento
public interface IMemento
{
void Add(object pItem);
void RestoreState();
}
public class Memento : IMemento
{
public Memento()
{
MementoList = new Dictionary<long, object>();
ReferenceList = new List<object>();
ObjectIDGenerator = new ObjectIDGenerator();
}
private ObjectIDGenerator ObjectIDGenerator { get; set; }
private Dictionary<long, object> MementoList { get; set; }
private List<object> ReferenceList { get; set; }
public void Add(object pItem)
{
bool firstTime;
long id = ObjectIDGenerator.GetId(pItem, out firstTime);
if (firstTime)
{
var mementoObject = DeepCopy(pItem);
MementoList.Add(id, mementoObject);
ReferenceList.Add(pItem);
}
}
public void RestoreState()
{
for (int i = 0; i < ReferenceList.Count; i++)
{
object reference = ReferenceList[i];
bool firstTime;
long id = ObjectIDGenerator.GetId(reference, out firstTime);
if (MementoList.ContainsKey(id))
{
object mementoObject = MementoList[id];
reference = mementoObject;
//reference = PropertyCopy<PrestationInfo>.CopyFrom(mementoObject as PrestationInfo); //Property copy
//Interlocked.Exchange(ref reference, mementoObject); //Also tried this
}
}
}
private static TCopy DeepCopy<TCopy>(TCopy pObjectToCopy)
{
using (MemoryStream memoryStream = new MemoryStream())
{
BinaryFormatter binaryFormatter = new BinaryFormatter();
binaryFormatter.Serialize(memoryStream, pObjectToCopy);
memoryStream.Position = 0;
return (TCopy)binaryFormatter.Deserialize(memoryStream);
}
}
}
Extra info
My guess is, I'm doing/understand something wrong regarding the List.
I also tried the Interlocked.Exchange, playing around with "ref"'s, using WeakReference's and storing the object into a CareTaker object (and storing that CareTaker into the List), implement some copy Property thing...
And ... I just can't see it.
My expected result would be the PrestationInfo.Advance property containing the value 2. But it keeps
Try this:
Change the Add method:
public long Add(object pItem)
{
bool firstTime;
long id = ObjectIDGenerator.GetId(pItem, out firstTime);
if (firstTime)
{
var mementoObject = DeepCopy(pItem);
MementoList.Add(id, mementoObject);
ReferenceList.Add(pItem);
}
return id; // i need my memento! LOL
}
You should also add this accessor method:
public object GetRestoredState(long id)
{
return MementoList[id]; // you should put some range check here
}
Now that you have your id, you can fetch the restored state this way:
memento.RestoreState();
prestationInfo2 = memento.GetRestoredState(savedId); // <-- you got this when you called the Add()...
Console.WriteLine(prestationInfo2.Advance); //Expect 2, but still 1
Follow ups: you can also make the IMemento into a IMemento<T>, and adjust your code accordingly
Memento.Add needs a ref parameter modifier to access the original reference type pointer.
https://msdn.microsoft.com/en-us/library/14akc2c7.aspx?f=255&MSPPError=-2147217396
It looks like the problem is in your understanding of references in .NET
public void RestoreState()
{
for (int i = 0; i < ReferenceList.Count; i++)
{
object reference = ReferenceList[i];
bool firstTime;
long id = ObjectIDGenerator.GetId(reference, out firstTime);
if (MementoList.ContainsKey(id))
{
object mementoObject = MementoList[id];
reference = mementoObject;
//reference = PropertyCopy<PrestationInfo>.CopyFrom(mementoObject as PrestationInfo); //Property copy
//Interlocked.Exchange(ref reference, mementoObject); //Also tried this
}
}
}
The RestoreState method above does not return anything, and you're strictly operating on references, not their internal state. Inside your method object reference is a local reference. It is not the same as the external prestationInfo2 and your method simply makes reference point (refer) to the previously saved copy of the state of presentationInfo2.
You could modify it something like this:
public object RestoreState()
{
for (int i = 0; i < ReferenceList.Count; i++)
{
object reference = ReferenceList[i];
bool firstTime;
long id = ObjectIDGenerator.GetId(reference, out firstTime);
if (MementoList.ContainsKey(id))
{
object mementoObject = MementoList[id];
reference = mementoObject;
return reference;
}
}
return null;
}
And then call it like this:
presentationInfo2 = memento.RestoreState();
If you want the memento to track objects and magically restore their state you will have to make the objects themselves aware of the memento which introduces coupling or use reflection to modify the tracked references internal state. Basically you don't deserialize the persisted state into a new object but use reflection to overwrite the previously stored internal state into the tracked object reference.
Be careful though to store references using WeakReference otherwise you will find yourself with a nice case of memory leak.

Categories