using System.Collections; using System.Collections.Generic; using System.IO; using System.Reflection; using UnityEngine; using UnityEngine.SceneManagement; public class SaveLoadManager : MonoBehaviour { private static SaveLoadManager instance; public static SaveLoadManager Instance => instance; [Header("Save Settings")] public string saveFileName = "save_{0}.json"; public int maxSaveSlots = 5; [Header("Debug")] public bool enableDebugLogs = true; private string saveDirectory; void Awake() { if (instance != null && instance != this) { Destroy(gameObject); return; } instance = this; DontDestroyOnLoad(gameObject); saveDirectory = Application.persistentDataPath; SceneManager.sceneLoaded += OnSceneLoaded; } void OnDestroy() { SceneManager.sceneLoaded -= OnSceneLoaded; } void OnSceneLoaded(Scene scene, LoadSceneMode mode) { Time.timeScale = 1f; Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; int loadedSlot = PlayerPrefs.GetInt("LoadedSaveSlot", -1); if (loadedSlot >= 0) { StartCoroutine(LoadAfterFrame(loadedSlot)); } } private IEnumerator LoadAfterFrame(int slot) { yield return null; LoadGameFromSlot(slot); PlayerPrefs.SetInt("LoadedSaveSlot", -1); PlayerPrefs.Save(); } public bool SaveGame(int slot, string saveName) { if (slot < 0 || slot >= maxSaveSlots) { LogError($"Invalid save slot: {slot}"); return false; } GameSaveData saveData = new GameSaveData(); saveData.slotNumber = slot; saveData.slotName = saveName; saveData.isEmpty = false; saveData.saveDate = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); saveData.sceneName = SceneManager.GetActiveScene().name; saveData.buildVersion = Application.version; saveData.blockGroupData = SaveBlockGroups(); saveData.playerData = SavePlayerData(); saveData.cameraData = SaveCameraData(); saveData.sceneObjectData = SaveSceneObjects(); saveData.gameStateData = SaveGameState(); string json = JsonUtility.ToJson(saveData, true); string path = GetSavePath(slot); try { File.WriteAllText(path, json); Log($"Game saved successfully to slot {slot}: {path}"); return true; } catch (System.Exception e) { LogError($"Failed to save game: {e.Message}"); return false; } } public GameSaveData LoadGame(int slot) { string path = GetSavePath(slot); if (!File.Exists(path)) { LogWarning($"No save file found at slot {slot}"); return null; } try { string json = File.ReadAllText(path); GameSaveData saveData = JsonUtility.FromJson(json); return saveData; } catch (System.Exception e) { LogError($"Failed to load save file: {e.Message}"); return null; } } public void LoadGameFromSlot(int slot) { Time.timeScale = 1f; GameSaveData saveData = LoadGame(slot); if (saveData == null) return; if (saveData.sceneName != SceneManager.GetActiveScene().name) { PlayerPrefs.SetInt("LoadedSaveSlot", slot); PlayerPrefs.Save(); SceneManager.LoadScene(saveData.sceneName); return; } StartCoroutine(RestoreGameStateCoroutine(saveData)); } public bool DeleteSave(int slot) { string path = GetSavePath(slot); if (File.Exists(path)) { try { File.Delete(path); Log($"Save slot {slot} deleted"); return true; } catch (System.Exception e) { LogError($"Failed to delete save: {e.Message}"); return false; } } return false; } public List GetAllSaveSlots() { List slots = new List(); for (int i = 0; i < maxSaveSlots; i++) { string path = GetSavePath(i); if (File.Exists(path)) { try { string json = File.ReadAllText(path); GameSaveData data = JsonUtility.FromJson(json); slots.Add(new SaveSlot { slotNumber = i, slotName = data.slotName, isEmpty = false, saveDate = data.saveDate, sceneName = data.sceneName, savedGroups = data.blockGroupData }); } catch { slots.Add(new SaveSlot { slotNumber = i, isEmpty = true, slotName = "Empty Slot" }); } } else { slots.Add(new SaveSlot { slotNumber = i, isEmpty = true, slotName = "Empty Slot" }); } } return slots; } private List SaveBlockGroups() { List groupDataList = new List(); BlockGroup[] groups = Object.FindObjectsOfType(); Dictionary groupIds = new Dictionary(); int groupIdCounter = 0; foreach (var group in groups) { if (group == null) continue; groupIds[group.gameObject] = groupIdCounter++; } Dictionary blockIds = new Dictionary(); int idCounter = 0; foreach (var group in groups) { if (group == null || group.blocks == null) continue; foreach (var block in group.blocks) { if (block == null) continue; blockIds[block] = idCounter++; } } foreach (var group in groups) { if (group == null) continue; BlockGroupData data = new BlockGroupData(); data.groupId = groupIds[group.gameObject]; data.groupName = group.name; data.position = group.transform.position; data.rotation = group.transform.rotation; data.localScale = group.transform.localScale; data.tag = group.tag; data.layer = group.gameObject.layer; data.componentsData = SaveAllComponents(group.gameObject); Rigidbody groupRb = group.GetComponent(); if (groupRb != null) { data.hasRigidbody = true; data.velocity = groupRb.linearVelocity; data.angularVelocity = groupRb.angularVelocity; data.isKinematic = groupRb.isKinematic; data.mass = groupRb.mass; data.useGravity = groupRb.useGravity; data.drag = groupRb.linearDamping; data.angularDrag = groupRb.angularDamping; } FixedJoint groupJoint = group.GetComponent(); if (groupJoint != null && groupJoint.connectedBody != null) { data.hasJoint = true; GameObject connectedGroupGo = groupJoint.connectedBody.gameObject; data.connectedGroupId = groupIds.TryGetValue(connectedGroupGo, out int gid) ? gid : -1; data.breakForce = groupJoint.breakForce; data.breakTorque = groupJoint.breakTorque; data.enableCollision = groupJoint.enableCollision; } data.blocks = new List(); if (group.blocks != null) { foreach (var block in group.blocks) { if (block == null) continue; BlockData blockData = new BlockData(); blockData.blockId = blockIds[block]; blockData.blockName = block.name.Replace("(Clone)", "").Trim(); blockData.localPosition = block.transform.localPosition; blockData.localRotation = block.transform.localRotation; blockData.localScale = block.transform.localScale; blockData.tag = block.tag; blockData.layer = block.layer; blockData.isActive = block.activeSelf; blockData.componentsData = SaveAllComponents(block); blockData.childrenData = SaveAllChildren(block); data.blocks.Add(blockData); } } groupDataList.Add(data); } return groupDataList; } private List SaveAllComponents(GameObject obj) { List components = new List(); Component[] allComponents = obj.GetComponents(); foreach (Component comp in allComponents) { if (comp == null) continue; if (comp is Transform) continue; if (comp is Rigidbody) continue; if (comp is FixedJoint) continue; ComponentData compData = new ComponentData(); compData.componentType = comp.GetType().AssemblyQualifiedName; compData.fields = new List(); FieldInfo[] fields = comp.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance); foreach (FieldInfo field in fields) { if (field.IsNotSerialized) continue; TrySaveField(comp, field, compData); } FieldInfo[] privateFields = comp.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance); foreach (FieldInfo field in privateFields) { if (field.IsNotSerialized) continue; if (!System.Attribute.IsDefined(field, typeof(SerializeField))) continue; TrySaveField(comp, field, compData); } if (compData.fields.Count > 0) { components.Add(compData); } } return components; } // Saves a single field. Unlike the old code this does NOT drop fields whose // JSON serializes to "{}", so struct/class fields aren't silently lost on load. private void TrySaveField(Component comp, FieldInfo field, ComponentData compData) { try { object value = field.GetValue(comp); FieldData fieldData = new FieldData(); fieldData.fieldName = field.Name; fieldData.fieldType = field.FieldType.AssemblyQualifiedName; fieldData.jsonValue = JsonUtility.ToJson(value); compData.fields.Add(fieldData); } catch { } } private List SaveAllChildren(GameObject parent) { List flat = new List(); Dictionary indexOf = new Dictionary(); Queue queue = new Queue(); // seed with direct children of parent for (int i = 0; i < parent.transform.childCount; i++) queue.Enqueue(parent.transform.GetChild(i)); while (queue.Count > 0) { Transform child = queue.Dequeue(); int selfIndex = flat.Count; indexOf[child] = selfIndex; int parentIndex = -1; if (child.parent != parent.transform) { if (!indexOf.TryGetValue(child.parent, out parentIndex)) parentIndex = -1; // defensive } // nth occurrence of this name among preceding same-parent siblings int occurrence = 0; for (int i = 0; i < child.GetSiblingIndex(); i++) if (child.parent.GetChild(i).name == child.name) occurrence++; ChildData cd = new ChildData(); cd.selfIndex = selfIndex; cd.parentIndex = parentIndex; cd.childName = child.name; cd.nameOccurrenceIndex = occurrence; cd.siblingIndex = child.GetSiblingIndex(); cd.tag = child.tag; cd.layer = child.gameObject.layer; cd.isActive = child.gameObject.activeSelf; cd.localPosition = child.localPosition; cd.localRotation = child.localRotation; cd.localScale = child.localScale; cd.componentsData = SaveAllComponents(child.gameObject); flat.Add(cd); for (int i = 0; i < child.childCount; i++) queue.Enqueue(child.GetChild(i)); } return flat; } private PlayerData SavePlayerData() { PlayerData data = new PlayerData(); GameObject player = GameObject.FindGameObjectWithTag("Player"); if (player == null) { player = GameObject.Find("Player") ?? GameObject.Find("FirstPersonController"); } if (player != null) { data.exists = true; data.position = player.transform.position; data.rotation = player.transform.rotation; data.localScale = player.transform.localScale; data.isActive = player.activeSelf; data.componentsData = SaveAllComponents(player); data.childrenData = SaveAllChildren(player); } return data; } private CameraData SaveCameraData() { CameraData data = new CameraData(); Camera mainCam = Camera.main; if (mainCam != null) { data.position = mainCam.transform.position; data.rotation = mainCam.transform.rotation; data.fieldOfView = mainCam.fieldOfView; data.nearClipPlane = mainCam.nearClipPlane; data.farClipPlane = mainCam.farClipPlane; data.isActive = mainCam.gameObject.activeSelf; } return data; } private List SaveSceneObjects() { List objects = new List(); GameObject[] saveables = GameObject.FindGameObjectsWithTag("Saveable"); foreach (var obj in saveables) { SceneObjectData data = new SceneObjectData { objectName = obj.name, position = obj.transform.position, rotation = obj.transform.rotation, localScale = obj.transform.localScale, isActive = obj.activeSelf, tag = obj.tag, layer = obj.layer, componentsData = SaveAllComponents(obj), childrenData = SaveAllChildren(obj) }; objects.Add(data); } return objects; } private GameStateData SaveGameState() { GameStateData data = new GameStateData { timeScale = Time.timeScale, }; return data; } private void RestorePlayerData(PlayerData data) { if (data == null || !data.exists) return; GameObject player = GameObject.FindGameObjectWithTag("Player"); if (player == null) { player = GameObject.Find("Player") ?? GameObject.Find("FirstPersonController"); } if (player == null) return; // Disable the CharacterController while teleporting; it fights direct // transform writes and can eat the position change otherwise. CharacterController cc = player.GetComponent(); bool ccWasEnabled = false; if (cc != null) { ccWasEnabled = cc.enabled; cc.enabled = false; } // Reset any accumulated motion so the player doesn't inherit old velocity. FirstPersonController fpc = player.GetComponent(); if (fpc != null) { fpc.ResetMotionState(); } player.transform.position = data.position; player.transform.rotation = data.rotation; player.transform.localScale = data.localScale; player.SetActive(data.isActive); RestoreAllComponents(player, data.componentsData); RestoreAllChildren(player, data.childrenData); // Re-enable the controller and flush the transform so physics agrees. Physics.SyncTransforms(); if (cc != null) { cc.enabled = ccWasEnabled; } } private void RestoreCameraData(CameraData data) { if (data == null) return; Camera mainCam = Camera.main; if (mainCam == null) return; mainCam.transform.position = data.position; mainCam.transform.rotation = data.rotation; mainCam.fieldOfView = data.fieldOfView; mainCam.nearClipPlane = data.nearClipPlane; mainCam.farClipPlane = data.farClipPlane; mainCam.gameObject.SetActive(data.isActive); } private IEnumerator RestoreGameStateCoroutine(GameSaveData saveData) { yield return new WaitForEndOfFrame(); yield return new WaitForFixedUpdate(); RestoreBlockGroups(saveData.blockGroupData); yield return new WaitForFixedUpdate(); RestorePlayerData(saveData.playerData); RestoreCameraData(saveData.cameraData); RestoreSceneObjects(saveData.sceneObjectData); RestoreGameState(saveData.gameStateData); Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; Log($"Game state restored from save: {saveData.slotName}"); } private void RestoreBlockGroups(List groupDataList) { if (groupDataList == null || groupDataList.Count == 0) return; // Destroy existing groups immediately so old colliders/rigidbodies // can't interact with the freshly created ones for a frame. BlockGroup[] existingGroups = Object.FindObjectsOfType(); foreach (var group in existingGroups) { if (group != null) DestroyImmediate(group.gameObject); } Dictionary createdGroupsById = new Dictionary(); Dictionary createdBlocksById = new Dictionary(); List groupJointConnections = new List(); foreach (var groupData in groupDataList) { GameObject groupObj = new GameObject(groupData.groupName); groupObj.transform.position = groupData.position; groupObj.transform.rotation = groupData.rotation; groupObj.transform.localScale = groupData.localScale; groupObj.tag = groupData.tag; groupObj.layer = groupData.layer; BlockGroup bg = groupObj.AddComponent(); createdGroupsById[groupData.groupId] = groupObj; RestoreAllComponents(groupObj, groupData.componentsData); if (groupData.hasRigidbody) { Rigidbody rb = groupObj.GetComponent(); if (rb == null) rb = groupObj.AddComponent(); rb.mass = groupData.mass; rb.linearDamping = groupData.drag; rb.angularDamping = groupData.angularDrag; rb.useGravity = groupData.useGravity; rb.isKinematic = groupData.isKinematic; // Only assign velocities on a non-kinematic body; setting // velocity on a kinematic Rigidbody warns and does nothing. if (!groupData.isKinematic) { rb.linearVelocity = groupData.velocity; rb.angularVelocity = groupData.angularVelocity; } } List groupBlocks = new List(); if (groupData.blocks != null) { foreach (var blockData in groupData.blocks) { GameObject block = CreateBlock(blockData, groupObj.transform); if (block != null) { groupBlocks.Add(block); createdBlocksById[blockData.blockId] = block; } } } bg.Initialize(groupBlocks); if (groupData.hasJoint && groupData.connectedGroupId >= 0) { int connectedId = groupData.connectedGroupId; float breakForce = groupData.breakForce; float breakTorque = groupData.breakTorque; bool enableCollision = groupData.enableCollision; GameObject thisGroupObj = groupObj; groupJointConnections.Add(() => { if (createdGroupsById.TryGetValue(connectedId, out GameObject connectedGroup)) { Rigidbody connectedRb = connectedGroup.GetComponent(); if (connectedRb == null) return; FixedJoint joint = thisGroupObj.AddComponent(); joint.connectedBody = connectedRb; joint.breakForce = breakForce; joint.breakTorque = breakTorque; joint.enableCollision = enableCollision; } }); } } Physics.SyncTransforms(); foreach (var jointAction in groupJointConnections) { jointAction(); } } private GameObject CreateBlock(BlockData data, Transform parent) { GameObject block = null; GameObject prefab = Resources.Load("Blocks/" + data.blockName); if (prefab != null) { block = Instantiate(prefab, parent); } else { block = GameObject.CreatePrimitive(PrimitiveType.Cube); block.transform.SetParent(parent, false); } if (block == null) return null; block.name = data.blockName; block.transform.localPosition = data.localPosition; block.transform.localRotation = data.localRotation; block.transform.localScale = data.localScale; block.tag = data.tag; block.layer = data.layer; block.SetActive(data.isActive); RestoreAllComponents(block, data.componentsData); RestoreAllChildren(block, data.childrenData); return block; } private void RestoreAllComponents(GameObject obj, List componentsData) { if (componentsData == null) return; foreach (ComponentData compData in componentsData) { System.Type type = System.Type.GetType(compData.componentType); if (type == null) continue; Component comp = obj.GetComponent(type); if (comp == null) { comp = obj.AddComponent(type); } if (comp == null || compData.fields == null) continue; foreach (FieldData fieldData in compData.fields) { System.Type fieldType = System.Type.GetType(fieldData.fieldType); if (fieldType == null) continue; // Search the whole type hierarchy so inherited private // [SerializeField] fields are found too. FieldInfo field = GetFieldInHierarchy(type, fieldData.fieldName); if (field == null) continue; // NEVER overwrite live scene references. JsonUtility cannot // rebuild UnityEngine.Object references, so restoring them // nulls the field and breaks the component (e.g. the player's // characterController / cameraTransform go null and movement dies). if (typeof(UnityEngine.Object).IsAssignableFrom(field.FieldType)) continue; if (IsUnityObjectCollection(field.FieldType)) continue; try { if (!string.IsNullOrEmpty(fieldData.jsonValue)) { object value = JsonUtility.FromJson(fieldData.jsonValue, fieldType); if (value != null) { field.SetValue(comp, value); } } } catch { } } } } private FieldInfo GetFieldInHierarchy(System.Type type, string fieldName) { while (type != null) { FieldInfo field = type.GetField( fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); if (field != null) return field; type = type.BaseType; } return null; } private static bool IsUnityObjectCollection(System.Type t) { if (t.IsArray) return typeof(UnityEngine.Object).IsAssignableFrom(t.GetElementType()); if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(System.Collections.Generic.List<>)) return typeof(UnityEngine.Object).IsAssignableFrom(t.GetGenericArguments()[0]); return false; } private void RestoreAllChildren(GameObject parent, List childrenData) { if (childrenData == null || childrenData.Count == 0) return; // created[i] is the GameObject for childrenData[i] (selfIndex == i) GameObject[] created = new GameObject[childrenData.Count]; // ---- Pass 1: resolve/create every node ---- // For direct children (parentIndex == -1) try to reuse an existing child // by name + occurrence, matching the old behavior. Deeper nodes are new. Dictionary> existingByName = new Dictionary>(); foreach (Transform t in parent.transform) { if (!existingByName.TryGetValue(t.name, out var list)) { list = new List(); existingByName[t.name] = list; } list.Add(t); } HashSet usedChildren = new HashSet(); for (int i = 0; i < childrenData.Count; i++) { ChildData cd = childrenData[i]; GameObject go = null; if (cd.parentIndex < 0) { // direct child of root: try to reuse existing by name + occurrence if (existingByName.TryGetValue(cd.childName, out var candidates)) { int occ = 0; foreach (Transform cand in candidates) { if (usedChildren.Contains(cand)) continue; if (occ == cd.nameOccurrenceIndex) { go = cand.gameObject; usedChildren.Add(cand); break; } occ++; } } } if (go == null) { go = new GameObject(cd.childName); go.transform.SetParent(parent.transform, false); } created[i] = go; } // ---- Pass 2: reparent by parentIndex, then apply TRS + components ---- for (int i = 0; i < childrenData.Count; i++) { ChildData cd = childrenData[i]; GameObject go = created[i]; if (cd.parentIndex >= 0 && cd.parentIndex < created.Length) go.transform.SetParent(created[cd.parentIndex].transform, false); else go.transform.SetParent(parent.transform, false); go.name = cd.childName; go.transform.localPosition = cd.localPosition; go.transform.localRotation = cd.localRotation; go.transform.localScale = cd.localScale; go.tag = cd.tag; go.layer = cd.layer; go.SetActive(cd.isActive); RestoreAllComponents(go, cd.componentsData); } // Optional sibling ordering (direct children only, matches save order) for (int i = 0; i < childrenData.Count; i++) { ChildData cd = childrenData[i]; if (cd.parentIndex < 0) created[i].transform.SetSiblingIndex(cd.siblingIndex); } // ---- Destroy leftover direct children not present in save data ---- List toDestroy = new List(); foreach (Transform t in parent.transform) { if (usedChildren.Contains(t)) continue; bool isNewlyCreated = false; for (int i = 0; i < created.Length; i++) if (created[i] != null && created[i].transform == t) { isNewlyCreated = true; break; } if (isNewlyCreated) continue; bool foundInSaved = false; foreach (var cd in childrenData) if (cd.parentIndex < 0 && cd.childName == t.name) { foundInSaved = true; break; } if (!foundInSaved) toDestroy.Add(t); } foreach (Transform t in toDestroy) Destroy(t.gameObject); } private void RestoreSceneObjects(List objects) { if (objects == null) return; // Group saved data by name so we can match on occurrence index too, // avoiding the same-name mixup at scene-object level. Dictionary> existingByName = new Dictionary>(); GameObject[] saveables = GameObject.FindGameObjectsWithTag("Saveable"); foreach (var obj in saveables) { if (!existingByName.TryGetValue(obj.name, out var list)) { list = new List(); existingByName[obj.name] = list; } list.Add(obj); } Dictionary occurrenceCounters = new Dictionary(); foreach (var data in objects) { int occurrence = occurrenceCounters.TryGetValue(data.objectName, out int c) ? c : 0; occurrenceCounters[data.objectName] = occurrence + 1; GameObject target = null; if (existingByName.TryGetValue(data.objectName, out var candidates) && occurrence < candidates.Count) { target = candidates[occurrence]; } if (target == null) continue; target.transform.position = data.position; target.transform.rotation = data.rotation; target.transform.localScale = data.localScale; target.tag = data.tag; target.layer = data.layer; target.SetActive(data.isActive); RestoreAllComponents(target, data.componentsData); RestoreAllChildren(target, data.childrenData); } } private void RestoreGameState(GameStateData data) { if (data == null) { Time.timeScale = 1f; return; } // Force running timescale; don't restore a paused (0) state on load. Time.timeScale = data.timeScale <= 0f ? 1f : data.timeScale; } private string GetSavePath(int slot) { return Path.Combine(saveDirectory, string.Format(saveFileName, slot)); } private void Log(string msg) { if (enableDebugLogs) Debug.Log($"[SaveLoad] {msg}"); } private void LogWarning(string msg) { if (enableDebugLogs) Debug.LogWarning($"[SaveLoad] {msg}"); } private void LogError(string msg) { Debug.LogError($"[SaveLoad] {msg}"); } } // --------------------------------------------------------------------------- // Serializable data classes // --------------------------------------------------------------------------- [System.Serializable] public class GameSaveData { public int slotNumber; public string slotName; public bool isEmpty; public string saveDate; public string sceneName; public string buildVersion; public List blockGroupData = new List(); public PlayerData playerData; public CameraData cameraData; public List sceneObjectData = new List(); public GameStateData gameStateData; } [System.Serializable] public class BlockGroupData { public int groupId; public string groupName; public Vector3 position; public Quaternion rotation; public Vector3 localScale; public string tag; public int layer; public bool hasRigidbody; public Vector3 velocity; public Vector3 angularVelocity; public bool isKinematic; public float mass; public bool useGravity; public float drag; public float angularDrag; public bool hasJoint; public int connectedGroupId = -1; public float breakForce; public float breakTorque; public bool enableCollision; public List componentsData = new List(); public List blocks = new List(); } [System.Serializable] public class BlockData { public int blockId; public string blockName; public Vector3 localPosition; public Quaternion localRotation; public Vector3 localScale; public string tag; public int layer; public bool isActive; public List componentsData = new List(); public List childrenData = new List(); } [System.Serializable] public class ChildData { public int selfIndex; // index of this node in the flat list public int parentIndex; // -1 == direct child of the owning root public string childName; // kept as childName to minimize churn public int nameOccurrenceIndex; // nth child of this name under its parent public int siblingIndex; // sibling order under its parent public string tag; public int layer; public bool isActive; public Vector3 localPosition; public Quaternion localRotation; public Vector3 localScale; public List componentsData; // NOTE: no nested childrenData — recursion removed } [System.Serializable] public class ComponentData { public string componentType; public List fields = new List(); } [System.Serializable] public class FieldData { public string fieldName; public string fieldType; public string jsonValue; } [System.Serializable] public class PlayerData { public bool exists; public Vector3 position; public Quaternion rotation; public Vector3 localScale; public bool isActive; public List componentsData = new List(); public List childrenData = new List(); } [System.Serializable] public class CameraData { public Vector3 position; public Quaternion rotation; public float fieldOfView; public float nearClipPlane; public float farClipPlane; public bool isActive; } [System.Serializable] public class SceneObjectData { public string objectName; public Vector3 position; public Quaternion rotation; public Vector3 localScale; public bool isActive; public string tag; public int layer; public List componentsData = new List(); public List childrenData = new List(); } [System.Serializable] public class GameStateData { public float timeScale = 1f; } [System.Serializable] public class SaveSlot { public int slotNumber; public string slotName; public bool isEmpty; public string saveDate; public string sceneName; public List savedGroups = new List(); }