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.
Related
I am new to stackoverflow and to Unity3D, so I am sorry if I am doing things wrong.
So currently, I am making a puzzle game. It has 50 different levels.
I need for each of them, to save 3 or 4 variables.
For example, when level 1 is cleared, I want it to store (int)hitCounts, (bool)cleared, (int)bestHitCounts.
I don't wanna use playerPrefs, as I don't want it to be readable from outside the box. I want it be converted to a binary file.
here is what I have :
#1 : made a static class TGameDat
[System.Serializable]
public class TGameDat
{
public int tGameDatInt;
public bool tGameDatBool;
public int tSceneIndex;
public TGameDat (TPlayer player)
{
tGameDatInt = player.tInt;
tGameDatBool = player.tBool;
tSceneIndex = player.tScene;
}
}
#2 : then made Tplayer(monobehaviour)
public class TPlayer : MonoBehaviour
{
public int tInt = 0;
public bool tBool = false;
public int tScene;
public List<TPlayer> TestGameDatList = new List<TPlayer>();
private void Start()
{
TSceneMaker();
}
public void TSceneMaker()
{
tScene = SceneManager.GetActiveScene().buildIndex;
}
public void TNextScene()
{
SceneManager.LoadScene(tScene + 1);
}
public void TPreviousScene()
{
SceneManager.LoadScene(tScene - 1);
}
public void TSaveVariables()
{
TSave.TSavePlayer(this);
TestGameDatList.Add(this);
Debug.Log("saved");
Debug.Log(tInt + " " + tBool + " " + tScene);
}
public void TLoadVariables()
{
List<TGameDat> data = TSave.TLoadPlayer(this);
Debug.Log("loaded. data count = " + data.Count + " tSceneIndex " + tScene);
tInt = data[0].tGameDatInt;
tBool = data[0].tGameDatBool;
tScene = data[0].tSceneIndex;
}
}
#3 : finally I created a save and load system :
public static class TSave
{
public static void TSavePlayer (TPlayer player)
{
BinaryFormatter formatter = new BinaryFormatter();
List<TGameDat> data = new List<TGameDat>();
string path = Application.persistentDataPath + "/Tsave_" + player.tScene + ".fun";
if(File.Exists(path))
{
FileStream stream = File.Open(path, FileMode.Open);
data.Add(new TGameDat(player));
formatter.Serialize(stream, data);
stream.Close();
}
else
{
FileStream stream = File.Create(path);
data.Add(new TGameDat(player));
formatter.Serialize(stream, data);
stream.Close();
}
}
public static List<TGameDat> TLoadPlayer(TPlayer player)
{
string path = Application.persistentDataPath + "/Tsave_" + player.tScene + ".fun";
if(File.Exists(path))
{
BinaryFormatter formatter = new BinaryFormatter();
FileStream stream = File.Open(path, FileMode.Open);
List<TGameDat> data = new List<TGameDat>();
data = formatter.Deserialize(stream) as List<TGameDat>;
stream.Close();
return data;
}
else
{
Debug.LogError("Save file not found in " + path);
return null;
}
}
}
So, here are my problems :
1 : in the current situation, each scene compiles a binary file. So at the end, it will have a bunch of binary files piled up... Like 50, as I have 50 scenes... isn't it too many?
2 : of course I tried the make a single save file using List, and each level would come to add its own variable data in it.
But instead of adding the data, it would simply replace the previous data. Then there is always only 1 index in the List.
Therefore, when I load, the variables are from the last played level! And when I try to play another level after playing the first level, because there is only 1 index in the list, I get out of range error.
How shall I approach this?
sorry for the long long text!
thank you for your inputs!
first thing first, do you really need every level to have its own save data? Because if you only need to store the data between one level and another I would suggest you use some kind of PlayerState class that stores the data of the previous level.
But if you really need to store the data of every level then I'll recommend you using a dictionary rather than a simple list.
Here is an example of how I would do it
Note: I haven't tested this code yet!
SaveGameManager class
public string SavePath => Application.persistentDataPath + "/save.dat";
public static SaveGameManager Instance; // Singleton pattern
private Dictionary<string, TGameDat> gameData;
private void Awake()
{
if (Instance != null)
{
Destroy(this.gameObject);
return;
}
// Singleton initialization
Instance = this;
// Keep the object when changing scenes
DontDestroyOnLoad(this.gameObject);
LoadGameData();
}
public TGameDat GetGameData(string key)
{
if (gameData.TryGetValue(key, out TGameDat data))
{
return data;
}
Debug.Log($"Unable to find saved data with key {key}");
return null;
}
public void SetGameData(string key, TGameDat data)
{
// Sets a value with given key and save it to file
gameData[key] = data;
SaveGameData();
}
public void SaveGameData()
{
Serializer.SaveBinaryFile(gameData, SavePath);
}
public void LoadGameData()
{
var savedData = Serializer.LoadBinaryFile(SavePath);
if (savedData != null)
{
gameData = (Dictionary<string, TGameDat>)savedData;
}
else
{
// Creating and saving new data because we can't found
// any that already stored in path
gameData = new Dictionary<string, TGameDat>();
SaveGameData();
}
}
And then, the Serializer class
public static void SaveBinaryFile(object data, string path)
{
using (var stream = new FileStream(path, FileMode.Create))
{
var formatter = new BinaryFormatter();
formatter.Serialize(stream, data);
}
}
public static object LoadBinaryFile(string path)
{
if (!File.Exists(path))
{
// Trying to load a file that does not exist
return null;
}
using (var stream = new FileStream(path, FileMode.Open))
{
var formatter = new BinaryFormatter();
return formatter.Deserialize(stream);
}
}
And then you can use it like this
public TGameDat data;
public void TSaveVariables()
{
SaveGameManager.Instance.SetGameData(level.id, this.data);
}
public void TLoadVariables()
{
var savedData = SaveGameManager.Instance.GetGameData(level.id);
if (savedData != null)
{
this.data = savedData;
}
else
{
// We don't have any save file for this level yet
savedData = new TGameDat();
}
}
You can change level.id to whatever identifier you wanted to use.
I have tried alot of solution but no one is working in my case, my question is simple but i cant find any answer specially for windows build. I have tried to load json form persistent , streaming and resource all are working good in android but no any solution work for windows build. here is my code please help me.
public GameData gameData;
private void LoadGameData()
{
string path = "ItemsData";
TextAsset targetFile = Resources.Load<TextAsset>(path);
string json = targetFile.text;
gameData = ResourceHelper.DecodeObject<GameData>(json);
// gameData = JsonUtility.FromJson<GameData>(json);
print(gameData.Items.Count);
}
here is my data class
[Serializable]
public class GameData
{
[SerializeField]
public List<Item> Items;
public GameData()
{
Items = new List<Item>();
}
}
public class Item
{
public string Id;
public string[] TexturesArray;
public bool Active;
public Item()
{
}
public Item(string _id, string[] _textureArray , bool _active = true)
{
Id = _id;
TexturesArray = _textureArray;
Active = _active;
}
}
In order to be (de)serialized Item should be [Serializable]
using System;
...
[Serializable]
public class Item
{
...
}
Then you could simply use Unity's built-in JsonUtility.FromJson:
public GameData gameData;
private void LoadGameData()
{
string path = "ItemsData";
TextAsset targetFile = Resources.Load<TextAsset>(path);
string json = targetFile.text;
gameData = JsonUtility.FromJson<GameData>(json);
print(gameData.Items.Count);
}
For loading something from e.g. persistentDataPath I always use something like
var path = Path.Combine(Application.persistentDataPath, "FileName.txt")
using (var fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (var sr = new StreamReader(fs))
{
var json = sr.ReadToEnd();
...
}
}
For development I actually place my file into StreamingAssets (streamingAssetsPath) while running the code in the Unity Editor.
Then on runtime I read from persistentFilePath. If the file is not there I first copy it from the streamingassetsPath the first time.
Here I wrote more about this approach
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.
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.
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.