- Home /
Racing Game: Saving and loading replay data?
I've got a racing game and a replay system set up already. I couldn't go the input-based route because the precise mouse movements in the game are too crucial.
I have a bool that checks if it should be recording or playing. When '`isRecord`', the script records 3 lists [Time (float), Position (vector3), Rotation (quaternion)] to a scriptable object. When '`isReplay`', it gets 2 indexes and lerps/slerps between them.
I'd like to record it to a file (because next I want to figure out how to make the replay data retrievable from the cloud), but the binary formatter doesn't seem to like the data. I'm not sure if it's because of the list, vector3, or quaternions, but it just doesn't work. This is probably where I need the most help. Then I'd like it to be able to load a replay file made this way to the scriptable object so the run can be replayed.
Any input is extremely appreciated. Thanks!
Are you just trying to use complex structs (i.e. Vector3 and Quaternion) with a BinaryFormatter as-is? From what I've seen, it's not quite that simple and requires organizing the data for both reading and writing.
Yeah I read a bit about that. Do you know if there are alternative methods of saving and loading stuff? For example it would be great to be able to export the state of the scriptable object to be loaded back later. Not sure if that's possible
I can't say that I'm already familiar with any approach to this that's just available as-is. Overall, it should be straightforward enough to compile the data down as-is. Basically, the way you want to approach it will dictate the way you can utilize it.
For example, let's say that instead of basing a replay on inputs and current states, you based it exclusively on position and rotation on every physics update (50-per-second by default). No compression, just raw, linear data with no gaps between anything.
If you took the position (Vector3, 12 bytes) and rotation (Quaternion, 16 bytes), 50 times per second (physics default rate), converted those into bytes and placed them end-to-end:
28 bytes * 50-per-second = 1400
1400 bytes * 60 seconds = 84000 bytes (~84kb)
84000 * 5 $$anonymous$$utes (long race by Mario Kart standards) = 420000 (~420kb)
Basically, in the worst-case scenario of utilizing no velocities or input states and with nothing really planned out for a self-verifying replay, you'd still wind up with a few hundred kilobytes of data in a maximally-inefficient file: Small by today's standards of file size, but still technically huge for the simple data it represents.
Really, what it all comes down to in the end is figuring out what data you want to store and how you want it organized. There's not really any "standardized" way of doing this sort of thing, since the types and number of Component scripts would always vary on a game-by-game basis.
Essentially, something like: Are there powerups? Should they be accurately portrayed during a replay? That would require at least a starting time/state for when it's activated, and/or a separate time/state for when it's acquired if it doesn't automatically activate.
That alone would be a rather confounding factor in deter$$anonymous$$ing what should go into replay data, so there's not really any reasonable way to genericize a state-saving system to support it without it being bloated with a huge amount of data that's irrelevant a majority of the time.
Answer by justinenayat · Oct 02, 2021 at 12:30 PM
I figured it out! For the sake of anyone with a similar problem I'm going to list out my codes:
Here's how I call the save/load methods
public Ghost ghost;
void YourFunction()
{
//To Load
ghost.LoadNow(ghost);
//To Save
ghost.SaveNow(ghost);
}
GhostRecord.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
public class GhostRecord : MonoBehaviour
{
public Ghost ghost;
private float timer;
private float timeValue;
private void Awake()
{
if (ghost.isRecord)
{
ghost.ResetData();
timeValue = 0;
timer = 0;
}
}
void Update()
{
if (GameControl.runStarted == true)
{
timer += Time.unscaledDeltaTime;
timeValue += Time.unscaledDeltaTime;
if (ghost.isRecord & timer >= 1 / ghost.recordFrequency)
{
ghost.timeStamp.Add(timeValue);
ghost.position.Add(this.transform.position);
ghost.rotation.Add(this.transform.rotation);
timer = 0;
}
}
}
}
Ghost.cs
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
using System.IO;
[CreateAssetMenu]
public class Ghost : ScriptableObject
{
public bool isRecord;
public bool isReplay;
public float recordFrequency;
public List<float> timeStamp;
public List<Vector3> position;
public List<Quaternion> rotation;
public void ResetData()
{
timeStamp.Clear();
position.Clear();
rotation.Clear();
}
public void SaveNow(Ghost a_Ghost)
{
SaveJsonData(a_Ghost);
}
private static void SaveJsonData(Ghost a_Ghost)
{
ReplayData rd = new ReplayData();
a_Ghost.PopulateReplayData(rd);
if (FileManager.WriteToFile("replaydata.replay", rd.ToJson()))
{
Debug.Log("Save Complete");
}
}
public void PopulateReplayData(ReplayData a_ReplayData)
{
a_ReplayData.d_timeStamp = timeStamp;
a_ReplayData.d_position = position;
a_ReplayData.d_rotation = rotation;
}
public void LoadNow(Ghost a_Ghost)
{
LoadJsonData(a_Ghost);
}
private static void LoadJsonData(Ghost a_Ghost)
{
if (FileManager.LoadFromFile("replaydata.replay", out var json))
{
ReplayData rd = new ReplayData();
rd.LoadFromJson(json);
a_Ghost.LoadFromReplayData(rd);
Debug.Log("Load Complete");
}
}
public void LoadFromReplayData(ReplayData a_ReplayData)
{
timeStamp = a_ReplayData.d_timeStamp;
position = a_ReplayData.d_position;
rotation = a_ReplayData.d_rotation;
}
}
ReplayData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class ReplayData
{
public List<float> d_timeStamp;
public List<Vector3> d_position;
public List<Quaternion> d_rotation;
public string ToJson()
{
return JsonUtility.ToJson(this);
}
public void LoadFromJson(string a_Json)
{
JsonUtility.FromJsonOverwrite(a_Json, this);
}
}
public interface ISaveable
{
void PopulateReplayData(ReplayData a_ReplayData);
void LoadFromReplayData(ReplayData a_ReplayData);
}
GhostPlayer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GhostPlayer : MonoBehaviour
{
public Ghost ghost;
private float timeValue;
private int index1;
private int index2;
private void Awake()
{
timeValue = 0;
ghost.LoadNow(ghost);
}
void Update()
{
if (GameControl.runStarted == true)
{
timeValue += Time.unscaledDeltaTime;
if (ghost.isReplay)
{
GetIndex();
SetTransform();
}
}
}
private void GetIndex()
{
for (int i = 0; i < ghost.timeStamp.Count - 2; i++)
{
if (ghost.timeStamp[i] == timeValue)
{
index1 = i;
index2 = i;
return;
}
else if (ghost.timeStamp[i] < timeValue & timeValue < ghost.timeStamp[i + 1])
{
index1 = i;
index2 = i + 1;
return;
}
}
index1 = ghost.timeStamp.Count - 1;
index2 = ghost.timeStamp.Count - 1;
}
private void SetTransform()
{
if (index1 == index2)
{
this.transform.position = ghost.position[index1];
this.transform.rotation = ghost.rotation[index1];
}
else
{
float interpolationFactor = (timeValue - ghost.timeStamp[index1]) / (ghost.timeStamp[index2] - ghost.timeStamp[index1]);
this.transform.position = Vector3.Lerp(ghost.position[index1], ghost.position[index2], interpolationFactor);
this.transform.rotation = Quaternion.Slerp(ghost.rotation[index1], ghost.rotation[index2], interpolationFactor);
}
}
}
Your answer
Follow this Question
Related Questions
Making a less complicated save system. 2 Answers
How To Make Settings File Over XML? 1 Answer
PlayerPrefs not saving in build 1 Answer
Future compatible saveClass? 1 Answer