I'm using Unity 2017 and wish to serialize save game data like so:
public void Save(Statistics data)
{
BinaryFormatter bf = new BinaryFormatter();
FileStream file = File.Create(Application.persistentDataPath
+ "/playerInfo.dat");
bf.Serialize(file, data);
file.Close();
}
The LevelData class is marked Serializable and contains only strings and ints. The class that implements IPersistentData meets those requirements too.
I did look for the problem and found that I should set an environment variable in my Awake method that I did, but that didn't solve anything. I'm using Windows 10 as operating system to develop on but the game will be made for Android.
When attempting to save, I always get this exception
SerializationException: Unexpected binary element: 255
System.Runtime.Serialization.Formatters.Binary.ObjectReader.ReadObject (BinaryElement element, System.IO.BinaryReader reader, System.Int64& objectId, System.Object& value, System.Runtime.Serialization.SerializationInfo& info)
Any sort of advice is welcome to get around the issue.
EDIT
Updating after trying the hints of Programmer:
The class I wish to serialize:
[Serializable]
public class Statistics
{
public int LastPlayedLevel;
public Statistics()
{
MusicVolume = 1f;
SFXVolume = 1f;
LevelDatas = new List<LevelData>();
LastPlayedLevel = 1;
}
public float MusicVolume;
public float SFXVolume;
public List<LevelData> LevelDatas;
}
And the LevelData:
[Serializable]
public class LevelData
{
public LevelData(int levelNumber,string name)
{
this.LevelNumber = levelNumber;
this.Name = name;
this.BestPercent = 0;
this.DeathCount = 0;
}
public int LevelNumber;
public string Name;
public int BestPercent;
public uint DeathCount;
}
Related
Pretty much what it says in the title.
I've built a second .dll file which contains my Content Importer/Processor and added that .dll file in my 'References' section for my Content Manager for my project, but nothing. The Manager doesn't detect my custom importer/processor. No idea what's going on and I wasn't able to find anyone else having an issue like this, so I was hoping someone more experienced could help me out here!
I am using Json.NET for the Json Deserialization by the way.
Thank you in advence :)
MapJson Code:
public class MapJson
{
[JsonProperty("name")]
public String Name = "";
[JsonProperty("width")]
public Int32 MapWidth = 0;
[JsonProperty("height")]
public Int32 MapHeight = 0;
}
Importer Code:
[ContentImporter(".amap", DefaultProcessor = "MapProcessor", DisplayName = "Map Importer - Engine")]
public class MapImporter : ContentImporter<MapJson>
{
public override MapJsonImport(string filename, ContentImporterContext context)
{
string json = new FileHandle(filename).ReadAll();
MapJson data = JsonConvert.DeserializeObject<MapJson>(json);
return data;
}
}
Processor Code:
[ContentProcessor(DisplayName = "Map Processor - Engine")]
public class MapProcessor : ContentProcessor<MapJson, MapJson>
{
public override MapJson Process(MapJson input, ContentProcessorContext context)
{
return input;
}
}
Writer Code:
[ContentTypeWriter]
public class MapWriter : ContentTypeWriter<MapJson>
{
protected override void Write(ContentWriter writer, MapJson value)
{
writer.Write(value.Name);
writer.Write(value.MapWidth);
writer.Write(value.MapHeight);
}
}
Reader Code:
public class MapReader : ContentTypeReader<Map>
{
protected override Map Read(ContentReader reader, Map existingInstance)
{
MapJson data = new MapJson();
data.Name = reader.ReadString();
data.MapWidth = reader.ReadInt32();
data.MapHeight = reader.ReadInt32();
// this constructor just sets my 'Map' class's Name, MapWidth and MapHeight variables
return new Map(data);
}
}
Well uhh this is embarrassing...
The solution was to first build the game project, then to actually re-build the content importer/processor project, and then to link it with the content manager!
I feel so stupid haha
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.
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.
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 serialize an object containing a list of very large composite object graphs (~200000 nodes or more) using Protobuf-net. Basically what I want to achieve is to save the complete object into a single file as fast and as compact as possible.
My problem is that I get an out-of-memory-exception while trying to serialize the object. On my machine the exception is thrown when the file size is around 1.5GB. I am running a 64 bit process and using a StreamWriter as input to protobuf-net. Since I am writing directly to a file I suspect that some kind of buffering is taking place within protobuf-net causing the exception. I have tried to use the DataFormat = DataFormat.Group attribute but with no luck so far.
I can avoid the exception by serializing each composite in the list to a separate file but I would prefer to have it all done in one go if possible.
Am I doing something wrong or is it simply not possible to achieve what i want?
Code to illustrate the problem:
class Program
{
static void Main(string[] args)
{
int numberOfTrees = 250;
int nodesPrTree = 200000;
var trees = CreateTrees(numberOfTrees, nodesPrTree);
var forest = new Forest(trees);
using (var writer = new StreamWriter("model.bin"))
{
Serializer.Serialize(writer.BaseStream, forest);
}
Console.ReadLine();
}
private static Tree[] CreateTrees(int numberOfTrees, int nodesPrTree)
{
var trees = new Tree[numberOfTrees];
for (int i = 0; i < numberOfTrees; i++)
{
var root = new Node();
CreateTree(root, nodesPrTree, 0);
var binTree = new Tree(root);
trees[i] = binTree;
}
return trees;
}
private static void CreateTree(INode tree, int nodesPrTree, int currentNumberOfNodes)
{
Queue<INode> q = new Queue<INode>();
q.Enqueue(tree);
while (q.Count > 0 && currentNumberOfNodes < nodesPrTree)
{
var n = q.Dequeue();
n.Left = new Node();
q.Enqueue(n.Left);
currentNumberOfNodes++;
n.Right = new Node();
q.Enqueue(n.Right);
currentNumberOfNodes++;
}
}
}
[ProtoContract]
[ProtoInclude(1, typeof(Node), DataFormat = DataFormat.Group)]
public interface INode
{
[ProtoMember(2, DataFormat = DataFormat.Group, AsReference = true)]
INode Parent { get; set; }
[ProtoMember(3, DataFormat = DataFormat.Group, AsReference = true)]
INode Left { get; set; }
[ProtoMember(4, DataFormat = DataFormat.Group, AsReference = true)]
INode Right { get; set; }
}
[ProtoContract]
public class Node : INode
{
INode m_parent;
INode m_left;
INode m_right;
public INode Left
{
get
{
return m_left;
}
set
{
m_left = value;
m_left.Parent = null;
m_left.Parent = this;
}
}
public INode Right
{
get
{
return m_right;
}
set
{
m_right = value;
m_right.Parent = null;
m_right.Parent = this;
}
}
public INode Parent
{
get
{
return m_parent;
}
set
{
m_parent = value;
}
}
}
[ProtoContract]
public class Tree
{
[ProtoMember(1, DataFormat = DataFormat.Group)]
public readonly INode Root;
public Tree(INode root)
{
Root = root;
}
}
[ProtoContract]
public class Forest
{
[ProtoMember(1, DataFormat = DataFormat.Group)]
public readonly Tree[] Trees;
public Forest(Tree[] trees)
{
Trees = trees;
}
}
Stack-trace when the exception is thrown:
at System.Collections.Generic.Dictionary`2.Resize(Int32 newSize, Boolean forceNewHashCodes)
at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
at ProtoBuf.NetObjectCache.AddObjectKey(Object value, Boolean& existing) in NetObjectCache.cs:line 154
at ProtoBuf.BclHelpers.WriteNetObject(Object value, ProtoWriter dest, Int32 key, NetObjectOptions options) BclHelpers.cs:line 500
at proto_5(Object , ProtoWriter )
I am trying to do a workaround where I serialize the array of trees one at a time to a single file using the SerializeWithLengthPrefix method. Serialization seems work - I can see the filesize is increased after each tree in the list is added to the file. However, when I try to Deserialize the trees I get the Invalid wire-type exception. I am creating a new file when I serialize the trees so the file should be garbage free - unless I am writing garbage of cause ;-). My serialize and deserialization methods are listed below:
using (var writer = new FileStream("model.bin", FileMode.Create))
{
foreach (var tree in trees)
{
Serializer.SerializeWithLengthPrefix(writer, tree, PrefixStyle.Base128);
}
}
using (var reader = new FileStream("model.bin", FileMode.Open))
{
var trees = Serializer.DeserializeWithLengthPrefix<Tree[]>>(reader, PrefixStyle.Base128);
}
Am I using the method in a incorrect way?
It wasn't helping that the AsReference code was only respecting default data-format, which means it was trying to hold data in memory so that it can write the object-length prefix back into the data-stream, which is exactly what we don't want here (hence your quite correct use of DataFormat.Group). That will account for buffering for an individual branch of the tree. I've tweaked it locally, and I can definitely confirm that it is now writing forwards-only (the debug build has a convenient ForwardsOnly flag that I can enable which detects this and shouts).
With that tweak, I have had it work for 250 x 20,000, but I'm getting secondary problems with the dictionary resizing (even in x64) when working on the 250 x 200,000 - like you say, at around the 1.5GB level. It occurs to me, however, that I might be able to discard one of these (forwards or reverse) respectively when doing each of serialization / deserialization. I would be interested in the stack-trace when it breaks for you - if it is ultimately the dictionary resize, I may need to think about moving to a group of dictionaries...