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!

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 *== timeValue)*

{
index1 = i;
index2 = i;
return;
}
else if (ghost.timeStamp < 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);
}
}
}