I have two scenes in my game - Main Menu and Game. I already made player's position and rotation persist between 2 scenes using binary formatter and serialization. But, I struggle to make time in the Game scene persist between 2 scenes (and game sessions). To put it simply:
I paste some code (saving and loading pos & rot) so you have an idea of what's going on.
public static class DataSerializer
{
public static void SerializeData(Player player)
{
BinaryFormatter bf = new BinaryFormatter();
string dataPath = Application.persistentDataPath + "/data.txt";
FileStream stream = new FileStream(dataPath, FileMode.Create);
PlayerData data = new PlayerData(player);
bf.Serialize(stream, data);
stream.Close();
}
public static float[] DeserializeData()
{
string dataPath = Application.persistentDataPath + "/data.txt";
if (File.Exists(dataPath))
{
BinaryFormatter bf = new BinaryFormatter();
FileStream stream = new FileStream(dataPath, FileMode.Open);
PlayerData data = bf.Deserialize(stream) as PlayerData;
stream.Close();
return data.stats;
}
else
{
Debug.LogError("File does not exist.");
return new float[6];
}
}
}
[Serializable]
public class PlayerData
{
public float[] stats;
public PlayerData(Player player)
{
stats = new float[6];
stats[0] = player.transform.position.x;
stats[1] = player.transform.position.y;
stats[2] = player.transform.position.z;
stats[3] = player.transform.eulerAngles.x;
stats[4] = player.transform.eulerAngles.y;
stats[5] = player.transform.eulerAngles.z;
}
}
public class Player : MonoBehaviour
{
public GameObject player;
void Start()
{
float[] loadedStats = DataSerializer.DeserializeData();
Vector3 position = new Vector3(loadedStats[0], loadedStats[1], loadedStats[2]);
Vector3 rotation = new Vector3(loadedStats[3], loadedStats[4], loadedStats[5]);
player.transform.position = position;
player.transform.rotation = Quaternion.Euler(rotation);
}
public void Save()
{
DataSerializer.SerializeData(this);
}
}
I would appreciate it if you would help me with saving & loading game-time in the same way.
Thank you!
First of all why so complicated with that float[]
. In my eyes it just makes it hard to read/understand and is error prone.
I would simply use a structure like the following (if you don't like it - sure stick to float[7]
why not ^^)
Update
NOTE: Do not use BinaryFormatter
anymore! → using JsonUtility
instead
public static class DataSerializer
{
private static readonly dataPath = Path.Combine(Application.persistentDataPath, "data.txt");
public static void SerializeData(Player player)
{
var data = new PlayerData(player);
var json = JsonUtility.ToJson(data);
File.WriteAllText(dataPath, json);
}
public static bool DeserializeData(out PlayerData data)
{
data = null;
if (!File.Exists(dataPath))
{
Debug.LogError("File does not exist.");
return false;
// alternatively create some default data instead
}
var json = File.ReadAllText(dataPath);
var data = JsonUtility.FromJson<PlayerData>(json);
return true;
}
}
[Serializable]
public class PlayerData
{
public SerializableVector3 Position;
public SerializableVector3 EulerAngles;
public float TotalTimePlayed;
public PlayerData(Player player)
{
Position = player.transform.position;
EulerAngles = player.transform.eulerAngles;
TotalPlayTime = player.TotalTimePlayed;
}
}
[Serializable]
public class SerializableVector3
{
public float x;
public float y;
public float z;
public SerializableVector3(Vector3 vec)
{
x = vec.x;
y = vec.y;
z = vec.z;
}
public Vector3 ToVector3()
{
return new Vector3(x,y,z);
}
public static implicit operator Vector3(SerializableVector3 vec)
{
return vec.ToVector3();
}
public static implicit operator SerializableVector3(Vector3 vec)
{
return new SerializableVector3(vec);
}
}
Then for tracking the total played time there are multiple ways.
Either store the last save DateTime.Now
so you can simply add the time currently played when saving:
float timePlayedThisSession = (DateTime.Now - dateTimeLastSaved).TotalSeconds;
Pro: The system handles it and it's a one time calculation
Con: The user can simply change the system time -> possibly "hack" your app stats
Or you can simply use Time.unscaledTime
which anyway is the time since the last app start.
Pro: User can not simply alter this value
Con: Depending on your use case you might not be able to use the Time.unscaledTime
if e.g. you allow to reset to the last save within your app (because in this case the Time.time
simply continues counting up since app start)
Or you can go fully "manual" and track the played time fully on your own using Time.unscaledDeltaTime
like e.g.
private float timePlayedThisSession;
private void Update()
{
timePlayedThisSession += Time.unscaledDeltaTime;
}
Pro: Full control + can easily reset or load a value into that field.
Con: Overhead for the Update
method being called every frame ... but this is so small that it won't matter at all! ;)
Fazit I would go with the last option and do e.g.
public class Player : MonoBehaviour
{
public GameObject player;
private float totalTimePlayed;
// Read-only access
public float TotalTimePlayed => totalTimePlayed;
void Start()
{
ResetToLastSave ();
}
private void Update ()
{
totalTimePlayed += Time.unscaledDeltaTime;
}
// Just adding this to show you could even simply add a method to allow your user revert to the last save
public void ResetToLastSave()
{
if(!DataSerializer.DeserializeData(out var loadedStats))
{
Debug.LogWarning("Could not load!", this);
return;
}
player.transform.position = loadedStats.Position;
player.transform.eulerAngles = loadedStats.Rotation;
totalTimePlayed = loadedStats.TotalTimePlayed;
}
public void Save()
{
DataSerializer.SerializeData(this);
}
}