- Home /
How to load multiple entries from one Binary file?
Hello all,
In my game I save and load data using Binary file. The save and load of one item was successful for me, however my goal is to save each passed level and once the game is over, show the player their gaming summary of all passed levels and the score for each level. This is the code so far (based in this tutorial):
using System;
[Serializable]
public class Player_Tutorial
{
private string _itemName;
private int _itemNumber;
private float _score;
public Player_Tutorial() { }
public Player_Tutorial(string name, int number, float score)
{
_itemName = name;
_itemNumber = number;
_score = score;
}
public string ItemName { get { return _itemName;} set { _itemName = value;}}
//similar get/set
Data code:
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;
public class SavingData_Tutorial : MonoBehaviour
{
private Player_Tutorial myPlayer;
private string DATA_PATH = "/MyGame.dat";
private string path;
private void Start()
{
path = Application.persistentDataPath + DATA_PATH;
//SaveData();
LoadData();
if (myPlayer!=null)
{
print("Player name: " + myPlayer.ItemName + " Player itemNumber: " + myPlayer.ItemNumber + " Player nw: " + myPlayer.Score);
}
}
//this will called once a level is completed
void SaveData()
{
FileStream file = null;
try
{
BinaryFormatter bf = new BinaryFormatter();
file = new FileStream(path, FileMode.Append);
Player_Tutorial p = new Player_Tutorial("The wood", 1, 30);//for testing
bf.Serialize(file, p); //store this data at this file location
}
catch (Exception e)
{
if (e !=null)
{
Debug.Log("File not found: " + e.Message);
}
}
finally
{
if (file != null) file.Close();
}
}
void LoadData()
{
FileStream file = null;
try
{
BinaryFormatter bf = new BinaryFormatter();
file = File.Open(path, FileMode.Open);
myPlayer = bf.Deserialize(file) as Player_Tutorial;
}
catch(Exception e)
{
Debug.Log("Could not read from file: " + e.Message);
}
finally
{
if (file != null) file.Close();
}
}
}
The goal is to show the summary of each entry in a UI canvas at the end of the game, for example:
"The wood", level 1 - score: 30
"The ocean", level 2 - score:10
"The moon", level 3 - score: 50
Is it a good idea to use binary files for that?
Thank you
There is no reason why you cant save what every you want as binary but...
Does it need to be binary data? I try to stay away from using binary file types for this sort of application. There are way easier ways to serialise and deserialise data in unity outside of the standard persistence methods which are less prohibitive. I.e. the amount of times having files in a plain text format has saved me by being able to debug and fix corrupted project files or save states.
I just made you a 17 $$anonymous$$ute video explaining it. Sorry if its a bit ranty and wombly its 1 am and I have work in the morning.
Thanks for the video. Please keep it on youtube for a while.
It's unlisted I wont remove it so not to worry.
Answer by sacredgeometry · Jan 26, 2020 at 05:47 PM
So the premise is simple. Your problem is that you are trying to write multiple blobs of data into a binary file with their headers.
You dont need to do this: What you should do instead is write a collection/ array into the file instead (or use composition to create a SaveState object that contains all the data you want in its fields/ members. )
On save: You dont need to append to the file you just need to wipe it and save the whole blob of data in the file again.
On load: you just need to deserialise the file into the same collection/ array type and it will all work as demonstrated below.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;
public class FileSaver<TSaveFile>
{
public List<TSaveFile> Data = new List<TSaveFile>();
private string _path;
public FileSaver(string path)
{
if(typeof(TSaveFile).GetCustomAttributes(typeof(SerializableAttribute), false).Length == 0)
throw new NotSupportedException($"{nameof(TSaveFile)} must be be decorated with a SerializableAttribute");
_path = path;
LoadData();
}
public void SaveData(params TSaveFile[] data)
{
Data.AddRange(data);
using(FileStream stream = File.Create(_path))
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, Data);
}
}
public IEnumerable<TSaveFile> LoadData()
{
if(!File.Exists(_path)) SaveData();
using(FileStream stream = File.OpenRead(_path))
{
if(stream.Length != 0)
{
BinaryFormatter formatter = new BinaryFormatter();
Data = formatter.Deserialize(stream) as List<TSaveFile>;
}
}
return Data;
}
}
public class SaveManager : MonoBehaviour
{
FileSaver<PlayerTutorial> PlayerTutorialSaver = new FileSaver<PlayerTutorial>("playerTutorials.save");
void Update()
{
if(Input.GetKeyDown(KeyCode.Q))
{
PlayerTutorialSaver.SaveData(new PlayerTutorial{
SceneName = "Wooter"
});
}
else if(Input.GetKeyDown(KeyCode.W))
{
var data = PlayerTutorialSaver.LoadData();
// NOTE: Use data here
Debug.Log(string.Join(", ", data.Select(d => d.SceneName)));
}
}
}
Thank you for your answer, it thought me a lot.
I've tried you method, and its difficult for me to show the output of the LoadData().
This is how my code looks now:
public class SavingData_Tutorial : $$anonymous$$onoBehaviour
{
public List<Player_Tutorial> playerData;
private string DATA_PATH = "/$$anonymous$$yGame.dat";
private string path;
private void Start()
{
path = Application.persistentDataPath + DATA_PATH;
playerData = new List<Player_Tutorial>();
}
public void SaveData()
{
FileStream stream;
if (!File.Exists(path))
{
stream = new FileStream(path, File$$anonymous$$ode.Create);
}
else
{
stream = new FileStream(path, File$$anonymous$$ode.Append);
}
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, playerData);
stream.Close();
}
public List<Player_Tutorial> LoadData()
{
List<Player_Tutorial> output;
using(FileStream stream = File.OpenRead(path))
{
BinaryFormatter formatter = new BinaryFormatter();
output = formatter.Deserialize(stream) as List<Player_Tutorial>;
}
return output;
}
}
and this is the manager code:
void Update()
{
//Save data
if (Input.Get$$anonymous$$ey($$anonymous$$eyCode.Q))
{
Player_Tutorial pt = new Player_Tutorial();
//pt.SceneName = "The_Woods";
//pt.Number =1;
//pt.Boolean = true;
pt.SceneName = "The_Sea";
pt.Number = 2;
pt.Boolean = false;
data$$anonymous$$.playerData.Add(pt);
print("saving data ");
data$$anonymous$$.SaveData();
}
//Load data
if (Input.Get$$anonymous$$eyDown($$anonymous$$eyCode.W))
{
data$$anonymous$$.LoadData();
//Need to see the loaded data but it's not working:
Player_Tutorial [] names = data$$anonymous$$.LoadData().ToArray();
for (int i = 0; i < names.Length; i++)
{
print("items are: " + names[i]);
}
}
}
I don't now how to "reveal" or "unwrap" the variables in the class "Player_Tutorial" in the List
I need to be able to see: SceneName[0] = "The_woods", SceneName[1] = "The_Sea" etc...
How to get to the variables in the list? all it gives me: "items are: Player_Tutorial"
I can't get the nice look you get when assigning var loadedData = LoadData()
The same way you would access anything in a array it has an indexer and is itterable.
To access the specific thing at a specific index you would use the [] syntax.
so for the first item it would be
loadedData[0]
Then you would address its properties like
loadedData[0].$$anonymous$$yProperty
That said you will get an out of range exception if you try to access an item at an index larger than the collection size so what you could do is iterate over the collection with a loop.
If your collection is always going to be the same size you could make them to another class that has a set amount of properties/ fields of the type in your collection and just set them like:
$$anonymous$$yClass.$$anonymous$$yPropertyA = loadedData[0];
$$anonymous$$yClass.$$anonymous$$yPropertyB = loadedData[1];
etc.
Is that what you were asking?
Alternatively you could use ElementAtOrDefault
This would let you null coalesce or propagate like:
var name = $$anonymous$$yArray?.ElementAtOrDefault(1)?.Name;
This way it wont throw an exception but ins$$anonymous$$d just set name to null if either the array is null or there is no element at index 1.
Similarly if you wanted to set a default value in that case you could do
var name = $$anonymous$$yArray?.ElementAtOrDefault(1)?.Name ?? "Default Name";
p.s you dont need the ToArray() to iterate over a List. Its an IEnumerable
The Length Property is just a $$anonymous$$ethod called Count().
Something is not working for me.
if (Input.Get$$anonymous$$eyDown($$anonymous$$eyCode.W))
{
var data = data$$anonymous$$.LoadData();
//Need to see the loaded data
print(data.Count);
}
Doing this, showed me the list has only one object.
Then I use the method as it is:
public void SaveData()
{
FileStream stream;
if (!File.Exists(path))
{
stream = new FileStream(path, File$$anonymous$$ode.Create);
}
else
{
stream = new FileStream(path, File$$anonymous$$ode.Append);
}
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, new List<Player_Tutorial>
{
new Player_Tutorial
{
SceneName = "The_Woods",
Number = 1,
Boolean = true
},
new Player_Tutorial
{
SceneName = "The_$$anonymous$$oon",
Number = 2,
Boolean = false
}
});
stream.Close();
}
public List<Player_Tutorial> LoadData()
{
List<Player_Tutorial> output;
using(FileStream stream = File.OpenRead(path))
{
BinaryFormatter formatter = new BinaryFormatter();
output = formatter.Deserialize(stream) as List<Player_Tutorial>;
}
return output;
}
No matter how many times I save, or even if the string SceneName is changed, the list count is now 2, when it of course has more than two entries.