How to add new data and keep old existing data - c#

I'm nearly done making my mobile game and I have have a DATA script using what is shown in this video. I have a list which holds the values of different challenges that the player can complete. How would I update the game so that I can add more challenges whilst still keeping the old data.
(The challenge data basically contains whether it has been completed and how far off being completed it is)
I have had a look at this guide but I don't quite understand it. I'm new to serialization.
Thank you in advance :)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Xml;
using System.Xml.Serialization;
using System.IO;
[System.Serializable]
public class XMLManager : MonoBehaviour {
public static XMLManager dataManagement;
public gameData data;
void Awake()
{
//File.Delete(Application.dataPath + "/StreamingFiles/XML/item_data.xml");
System.Environment.SetEnvironmentVariable("MONO_REFLECTION_SERIALIZER", "yes");
dataManagement = this;
DontDestroyOnLoad(gameObject);
}
public void SaveData()
{
XmlSerializer serializer = new XmlSerializer(typeof(gameData));
System.Environment.SetEnvironmentVariable("MONO_REFLECTION_SERIALIZER", "yes");
FileStream stream = new FileStream(Application.dataPath + "/StreamingFiles/XML/item_data.xml", FileMode.Create);
serializer.Serialize(stream, data);
stream.Close();
}
public void LoadData()
{
System.Environment.SetEnvironmentVariable("MONO_REFLECTION_SERIALIZER", "yes");
if (File.Exists(Application.dataPath + "/StreamingFiles/XML/item_data.xml"))
{
XmlSerializer serializer = new XmlSerializer(typeof(gameData));
FileStream stream = new FileStream(Application.dataPath + "/StreamingFiles/XML/item_data.xml", FileMode.Open);
data = serializer.Deserialize(stream) as gameData;
stream.Close();
}
else
{
print("SaveData");
SaveData();
}
}
}
[System.Serializable]
public class gameData
{
public List<ChallengeStatus> Challenges;
public int HighScore;
public int CoinsCollected;
public List<bool> Unlocked;
public int GamesPlayed;
public int currentChallenge;
}
[System.Serializable]
public class ChallengeStatus
{
public int id;
public string title;
public int current;
public int max;
public int reward;
public bool completed;
public bool claimed;
}

First you should have a look at Unity XML Serialization and use proper attributes. You don't totally need them (except maybe the [XmlRoot]) but they let you customize your Xml file. If not provided Unity uses the variable names and uses sub-elements instead of attributes. However afaik this works only for primitives (int, float, string, bool, etc) and lists of them not for your own class ChallengeStatus. So at least for the list of your class you have to provide attributes:
[System.Serializable]
[XmlRoot("GameData")]
public class GameData
{
[XmlArray("Challenges")]
[XmlArrayItem("ChallengeStatus)]
public List<ChallengeStatus> Challenges;
//...
}
Now I don't really understand why you need to keep the old XML file when saving a new one but if you want to keep the current file I would add an int FileCounter .. ofcourse not in the same XML file ;) Might be e.g. via PlayerPrefs or a second simple text file only including the number or something similar.
Note it is better/saver to use Path.Combine for concatenate systempaths) - the overload taking an array of strings requires .Net4. Something like
private string FilePath
{
get
{
//TODO first load / read the FileCounter from somewhere
var paths = {
Application.dataPath,
"StreamingFiles",
"XML",
// here you get from somewhere and use that global FileCounter
string.Format("item_data_{0}.xml", FileCounter)};
return Path.Combine(paths);
}
}
Than you can increase that global FileCounter everytime you save the file.
public void SaveData()
{
//TODO somehow increase the global value
// store to PlayerPrefs or write a new file or ...
FileCounter += 1;
System.Environment.SetEnvironmentVariable("MONO_REFLECTION_SERIALIZER", "yes");
// better use the "using" keyword for streams
// use the FilePath field to get the filepath including the filecounter
using(FileStream stream = new FileStream(FilePath, FileMode.Create))
{
XmlSerializer serializer = new XmlSerializer(typeof(gameData))
serializer.Serialize(stream, data);
}
}
And read the file with the current FileCounter without increasing it
public void LoadData()
{
System.Environment.SetEnvironmentVariable("MONO_REFLECTION_SERIALIZER", "yes");
if (File.Exists(FilePath))
{
// better use a "using" block for streams
// use the FilePath field to get the filepath including the filecounter
using(FileStream stream = new FileStream(FilePath, FileMode.Open))
{
XmlSerializer serializer = new XmlSerializer(typeof(gameData));
data = serializer.Deserialize(stream) as gameData;
}
}
else
{
print("SaveData");
SaveData();
}
}
Hint 1:
As soon as you provide a constructor for your classes e.g.
public GameData(List<ChallengeStatus> challenges)
{
Challenges = challenges;
}
than you always have to also provide a default constructor (even if it does nothing)
public GameData(){ }
Hint 2:
You should always initialize your lists:
public class GameData
{
public List<ChallengeStatus> Challenges = new List≤ChallangeStatus>();
//...
public List<bool> Unlocked = new List<bool>();
//...
}
Hint 3:
Btw you don't need that [System.Serializable] for XmlManager since it inherits from MonoBehaviour which already is serializable anyway.

Related

Unity - How to save and load lists (Binary Saving and Loading)

I'm trying to figure out how you would save & load list of data. I've gotten my code to work with single data but unsure of how a list one would work. I have a Character class so that means I can have multiple characters and I would like to save each of those character's hp, mana etc.
//SaveManager class that I can call from anywhere to save and load
public static class SaveManager
{
//SAVE
public static void Save(Character c)
{
BinaryFormatter formatter = new BinaryFormatter();
string path = Application.persistentDataPath + "/Save.save";
FileStream stream = new FileStream(path, FileMode.Create);
PartyData data = new PartyData(c);
formatter.Serialize(stream, data);
stream.Close();
}
//LOAD
public static PartyData Load()
{
string path = Application.persistentDataPath + "/Save.save";
if (File.Exists(path))
{
BinaryFormatter formatter = new BinaryFormatter();
FileStream stream = new FileStream(path, FileMode.Open);
PartyData data = formatter.Deserialize(stream) as PartyData;
stream.Close();
return data;
}
else
{
Debug.Log("Save FIle not Found");
return null;
}
}
}
PartyData class is what I'm using to save the Character data to.
[System.Serializable]
public class PartyData
{
public int hp;
public PartyData(Character cParty)
{
hp = cParty.HP;
}
}
Character class that contains stats etc
public class Character
{
public int HP { get; set; }
public int Mana { get; set; }
}
And then finally I have the CharacterParty class that I attach to a gameobject, this class contains multiple characters and calls the Save and Load functions:
public class CharacterParty : MonoBehaviour
{
[SerializeField] List<Character> characters;
public List<Character> Characters
{
get
{
return characters;
}
}
public void SaveParty()
{
SaveManager.SaveMC(characters[0]);
}
public void LoadParty()
{
PartyData data = SaveManager.LoadMC();
characters[0].HP = data.hp;
}
}
Now for testing purposes I tried saving & loading just the character's hp at index 0 and it works but now I wanna save a list of multiple character's hp and mana etc. I just don't know how the list would work with serialization saving and loading. My code probably needs a little bit of change to get the list working so I hope someone can help me out with an example.
Use JsonUtility class - this is Unity's built-in utility for JSON.
Also with this you can serialize any class without [System.Serializable]
Save:
// Parties list
List<PartyData> parties = new List<PartyData>();
// Add your parties here
// First argument is an instance of a class or any other object
// If second argument set to true, it makes JSON more human-readable
string json = JsonUtility.ToJson(parties, false);
// .. saving the file
Load:
// .. loading the file
// First argument is the JSON
List<PartyData> parties = JsonUtility.FromJson<List<PartyData>>(json);
// .. do stuff with the list
EDIT:
If you really want to use BinaryFormatter, then this is for saving:
List<PartyData> parties = new List<PartyData>();
// .. add parties
formatter.Serialize(stream, parties)
For loading:
List<PartyData> parties = formatter.Deserialize(stream) as List<PartyData>;
// .. do stuff
SECOND EDIT:
This one should work.
You would do something like that:
// Save class
[Serializable]
public class Save
{
public List<PartyData> parties;
// .. add other stuff here
}
// For loading
try
{
// Deserializing the save
Save save = (Save)formatter.Deserialize(stream);
// .. do stuff with other data if you added some
foreach (PartyData data in save.parties)
{
// .. do stuff with data
}
}
catch (Exception e)
{
// .. can you just acnowledge... E
// Oopsie! An error occured. The save file is probably COWWUPTED
Debug.LogWarning($"An error occured while loading the file: {e}");
}
// For saving
List<PartyData> parties = new List<PartyData>();
// .. add parties by calling parties.Add(party_here);
// Creating a save
Save save = new Save();
// Adding parties to save
save.parties = parties;
// Serialize to a file
formatter.Serialize(stream, parties);

Saving objects with constructor to xml

Hello everyone i need some help what do i need to change in this code to be able to serialize (save) this file?
I'm trying to create a inventory file that will save crafted items (this one in particular saves crafted components) i have a almost identical code for saving crafted weapons. Searching the net i couldn't find a way to serialize this object because it uses parameters, but i need to add data into that list and save it. Is there a way around it ? if yes how ? THANK YOU !
the error im getting is
InvalidOperationException: ComponentDB.ItemEntry cannot be serialized because it does not have a parameterless constructor.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Xml;
using System.Xml.Serialization;
using System.IO;
using System;
using System.Linq;
public class ComponentDB : MonoBehaviour
{
public ItemDatabase itemDB;
public string Slot;
public static ComponentDB ins;
void Awake()
{
ins = this;
Slot = "Slot1";
}
public void SaveItems()
{
XmlSerializer serializer = new XmlSerializer(typeof(ItemDatabase));
FileStream stream = new FileStream(Application.dataPath + "/StreamingAssets/Save/" + Slot + "CraftedComp.xml", FileMode.Create);
serializer.Serialize(stream, itemDB);
stream.Close();
}
public void LoadItems()
{
XmlSerializer serializer = new XmlSerializer(typeof(ItemDatabase));
FileStream stream = new FileStream(Application.dataPath + "/StreamingAssets/Save/" + Slot + "CraftedComp.xml", FileMode.Open);
itemDB = serializer.Deserialize(stream) as ItemDatabase;
stream.Close();
}
[Serializable]
public class ItemEntry
{
public string Name;
public string Data;
public int Amount;
public ItemEntry(string iName, string idata, int iAmount)
{
Name = iName;
Data = idata;
Amount = iAmount;
}
}
[Serializable]
public class ItemDatabase
{
public List<ItemEntry> list = new List<ItemEntry>();
}
public void ManageItemsInv(string input_name, string input_list, int input_amount)
{
ins.itemDB.list.Add(new ItemEntry(input_name, input_list, input_amount));
}
}
Your code are almost OK, except: you missed a parameterless constructor for ItemEntry, then you could not serialize it.
Solution is quite simple: create a parameterless constructor in ItemEntry class:
[Serializable]
public class ItemEntry
{
public string Name;
public string Data;
public int Amount;
//parameterless constructor for XmlSerializer
public ItemEntry()
{
}
public ItemEntry(string iName, string idata, int iAmount)
{
Name = iName;
Data = idata;
Amount = iAmount;
}
}

Adding new values to dictionary corrupts old save file

I am using a simple method of serializing and deserializing data for my save files which looks like this
//Object that is being stored
[System.Serializable]
public class GameData{
public int units;
public int scanRange;
public int gains;
public int reputation;
public int clicks;
public Dictionary<string,bool> upgradesPurchased;
public Dictionary<string,bool> upgradesOwned;
public Dictionary<string,bool> achievementsEarned;
public GameData(int units_Int,int scan_Range,int gains_Int,int reputation_Int,int clicks_Int,Dictionary<string,bool> upgrades_Purchased,Dictionary<string,bool> upgrades_Owned,Dictionary<string,bool> achievements_Earned){
units = units_Int;
scanRange = scan_Range;
gains = gains_Int;
reputation = reputation_Int;
clicks = clicks_Int;
upgradesPurchased = upgrades_Purchased;
upgradesOwned = upgrades_Owned;
achievementsEarned = achievements_Earned;
}
}
//Method that handles saving the object
public void SaveFile(){
string destination = Application.persistentDataPath + DATA_FILE;
FileStream file;
if (File.Exists (destination)) {
file = File.OpenWrite (destination);
} else {
file = File.Create (destination);
}
GameData data = new GameData (GameManager.Instance.units,GameManager.Instance.scanRange,GameManager.Instance.gains,GameManager.Instance.reputation,GameManager.Instance.clicks,UpgradeManager.Instance.upgradesPurchased,UpgradeManager.Instance.upgradesOwned,AchievementManager.Instance.achievementsEarned);
BinaryFormatter bf = new BinaryFormatter ();
bf.Serialize (file, data);
file.Close ();
NotificationsBar.Instance.ShowNotification ("Game saved success");
}
//Method that loads the object
public void LoadFile(){
string destination = Application.persistentDataPath + DATA_FILE;
FileStream file;
if (File.Exists (destination)) {
file = File.OpenRead (destination);
} else {
UpgradeManager.Instance.FirstLoad ();
return;
}
BinaryFormatter bf = new BinaryFormatter ();
GameData data = (GameData)bf.Deserialize (file);
file.Close ();
GameManager.Instance.units = data.units;
GameManager.Instance.scanRange = data.scanRange;
GameManager.Instance.gains = data.gains;
GameManager.Instance.reputation = data.reputation;
GameManager.Instance.clicks = data.clicks;
UpgradeManager.Instance.upgradesPurchased = data.upgradesPurchased;
UpgradeManager.Instance.upgradesOwned = data.upgradesOwned;
AchievementManager.Instance.achievementsEarned = data.achievementsEarned;
Debug.Log ("Units: " + data.units);
}
Theres a lot of code here but this is so everyone has a clear picture of what the entire system looks like
So the issue with this method is when adding a new value to the dictionary passed to GameData UpgradeManager.Instance.upgradesPurchased I will get an error when searching for data within the dictionary key not present in dictionary
My analysis is that due to the new value being added there is an offset in the dictionary from where the new value is placed and what used to be in that place
What I expected to happen when I first wrote out the code wa the dictionary would just autopopulate the new values and overwrite the old data
For a visual representation of what I mean lets say you have 2 upgrades
Upgrade1,Upgrade2
Now this is saved
Now the code changes and you have 3 upgrades
Upgrade1,Upgrade3,Upgrade2
What I assume would happen is the new value is just added into the save
So I am not exactly sure why this is happening....
Whilst I can't see the exact cause of the issue I would suggest the following:
First, take your save/load logic out of your GameData class and put it into a SaveDataManager class, that way you segregate responsibility.
From there, you can simplify your GameData class down to a struct making serialisation/desrialisation easier.
Then in your main game class whenever you have to load the game you can do something along the lines of:
SaveGameManger sgManager = new SaveGameManager(file);
gameData = sgManager.LoadGame()
This will make your code much easier to maintain and if this doesn't fix your problem it will be a lot easier to find.
Further to this, it will also allow you to build unit tests that verify the integrity of you load and save logic.
I've not had a chance to test it, but your separated and refactored code would look something like this (although it needs some validation checks added and whatnot):
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
namespace temp
{
public class GameLoop
{
private SaveGameManager sgManager;
private GameData data;
private bool isPlaying;
public GameLoop()
{
sgManager = new SaveGameManager("MYSAVELOCATION");
data = sgManager.LoadGame();
isPlaying = true;
}
private void PlayGame()
{
while (isPlaying)
{
//All of your game code
}
}
}
public class SaveGameManager
{
private string saveFile;
private BinaryFormatter formatter;
public SaveGameManager(string file)
{
saveFile = file;
formatter = new BinaryFormatter();
}
public GameData LoadGame()
{
using (StreamReader reader = new StreamReader(saveFile))
{
return (GameData)formatter.Deserialize(reader.BaseStream);
}
}
public void SaveGame(GameData data)
{
using (StreamWriter writer = new StreamWriter(saveFile))
{
formatter.Serialize(writer.BaseStream, data);
}
}
}
[Serializable]
public struct GameData
{
public int units;
public int scanRange;
public int gains;
public int reputation;
public int clicks;
public Dictionary<string, bool> upgradesPurchased;
public Dictionary<string, bool> upgradesOwned;
public Dictionary<string, bool> achievementsEarned;
}
}
And I really would consider switching out your string keys for upgrades in favour of enums... Much less error prone.

Failed to open Binary file Unity

This is how I save leveldata in my development kit.(this runs in dev program)
And the data is able to be correctly restored in dev kit aswell.
public void Savedata()
{
List<List<float>> tempfloatlist = new List<List<float>>();
foreach (List<Vector2> ele in routes)
{
conversions.Vec2float temp = new conversions.Vec2float();
tempfloatlist.Add(temp.conv2float(ele));
}
BinaryFormatter binform = new BinaryFormatter();
FileStream savefile = File.Create(Application.persistentDataPath +
"/DevData.bytes");
DevData savecontainer = new DevData();
savecontainer.routenames = routenames;
savecontainer.routes = tempfloatlist;
savecontainer.waves = waves;
binform.Serialize(savefile, savecontainer);
savefile.Close();
}
This is how I try to open the data after I moved the file in resources.(this runs in actual game) See line \ERROR
NullReferenceException: Object reference not set to an instance of an
object GameControl.LoadLevelData () (at Assets/GameControl.cs:70)
GameControl.Awake () (at Assets/GameControl.cs:26)
I fear that I am not opening the file correct way.
private void LoadLevelData()
{
TextAsset devdataraw = Resources.Load("DevData") as TextAsset;
BinaryFormatter binform = new BinaryFormatter();
Stream loadfile = new MemoryStream(devdataraw.bytes);
DevData devdata = binform.Deserialize(loadfile) as DevData;
\\ERROR happens here, no correct data to be loaded in routenames.
routenames = devdata.routenames;
waves = devdata.waves;
routes = new List<List<Vector2>>();
foreach (List<float> ele in devdata.routes)
{
conversions.float2vec temp = new conversions.float2vec();
routes.Add(temp.conv2vec(ele));
}
loadfile.Close();
}
[Serializable()]
class DevData
{
public List<List<float>> routes;
public List<string> routenames;
public List<Wave> waves;
}
namespace WaveStructures
{
[Serializable()]
public class Enemy
{
public int enemytype;
public int movementpattern;
public int maxhealth;
public int speed;
}
[Serializable()]
public class Spawntimer
{
public float timer;
public int amount;
}
[Serializable()]
public class Wave
{
public List<Enemy> Enemylist;
public List<Spawntimer> Enemyspawnsequence;
public int[] enemypool;
}
}
The serializer is having hard time serializing the data.
There are just two possible solution to try:
1.Notice the () in your [Serializable()]. Remove that. That should be [Serializable]. Another user mention that this is valid. Make sure to do #2.
2.Make sure that every class you want to serialize is placed in its own file. Make sure that it does not inherit from MonoBehaviour either.
For example, DevData class should be in its own file called DevData.cs. you should also do this for the Wave and other classes that you will serialize.
Finally, if this does not solve your problem, it's a known problem that BinaryFormatter causes so many issues when used in Unity. You should abandon it and use Json instead. Take a look at this post which describes how to use Json for this.

problem Storing a list of Objects in Isolated Storage

I am trying to store a list of objects I created in the isolated storage and be able to display them in a list by auto generating a title for them. So far the code works but once I tombstone the app and start it up all my data is saved except for the list of objects. I think my problem may be with how I initialize the list in the first place, or possibly how I am displaying the names. Any help is appreciated.
this code is in my App.xaml.cs:
public partial class App : Application
{
public List<my_type> testList = new List<my_type>();
void loadvalues()
{
IsolatedStorageSettings settings = IsolatedStorageSettings.ApplicationSettings;
List<my_Type> L;
if (settings.TryGetValue<List<DrinkSesh>>("Storage", out L))
{ testList = L; }
}
void savevalues()
{
IsolatedStorageSettings settings = IsolatedStorageSettings.ApplicationSettings;
settings["Storage"] = testList;
settings.Save();
}
}
This code is on my mainPage to add the items to the list:
(Application.Current as App).testList.Add(new my_type());
and this code is to implement the titles onto the screen on a different page:
public different_class()
{
{
InitializeComponent();
for (i = 0; i < (Application.Current as App).testList.Count; i++)
{
CreateATextBlock((Application.Current as App).testList[i].Title_ToString(), i);
}
}
private void CreateATextBlock(String title,int num)
{
testblockname = new TextBlock();
testblockname.Text = (num + 1) + ". " + title;
DrList.Children.Add(testblockname);
}
}
Thank you in advance!
Here is the code I use to save and load lists of objects from isolated storage.
public class IsoStoreHelper
{
private static IsolatedStorageFile _isoStore;
public static IsolatedStorageFile IsoStore
{
get { return _isoStore ?? (_isoStore = IsolatedStorageFile.GetUserStoreForApplication()); }
}
public static void SaveList<T>(string folderName, string dataName, ObservableCollection<T> dataList) where T : class
{
if (!IsoStore.DirectoryExists(folderName))
{
IsoStore.CreateDirectory(folderName);
}
string fileStreamName = string.Format("{0}\\{1}.dat", folderName, dataName);
using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream(fileStreamName, FileMode.Create, IsoStore))
{
DataContractSerializer dcs = new DataContractSerializer(typeof(ObservableCollection<T>));
dcs.WriteObject(stream, dataList);
}
}
public static ObservableCollection<T> LoadList<T>(string folderName, string dataName) where T : class
{
ObservableCollection<T> retval = new ObservableCollection<T>();
if (!IsoStore.DirectoryExists(folderName))
{
IsoStore.CreateDirectory(folderName);
}
string fileStreamName = string.Format("{0}\\{1}.dat", folderName, dataName);
using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream(fileStreamName, FileMode.OpenOrCreate, IsoStore))
{
if (stream.Length > 0)
{
DataContractSerializer dcs = new DataContractSerializer(typeof(ObservableCollection<T>));
retval = dcs.ReadObject(stream) as ObservableCollection<T>;
}
}
return retval;
}
}
By simply adding your collection (List) to your IsolatedStorageSettings you are relying on the built in serializer (the DataContractSerializer) to serialize your object for persisting to disk.
Are you sure your object can be correctly serialized and deserialized?
Doing this yourself is probably the easiest way to do this.
If you do this yourself, not that:
- DataContractSerializer is slow
- DataContractJsonSerializer is faster
- Json.net is faster still
- Binary serialization is fastest.
When serializing yourself you should also save in an IsolatedStorageFile rahter than in the settings. This can help with performance and also adds flexibility which can aid debugging and testing.

Categories