I have spent the night writing this saver/loader using the built in C# xml serialization and saves in your Documents folder in a folder called “MyGameSaveDatas”.
I tried to design it in a way that could be used fairly generically. It probably has lots of bugs and what not. Also, I did not handle the Texture part of things as I dont know what you mean by loading and saving textures. Did you want people to load from their own files, or just the textures you supply them that we can grab via Resources.Load?
Here is a video demonstration
I don’t know why the lighting gets messed up when I load the objects in.
The actual saving code you would be working with would look like this
Click for example code
using System;
using UnityEngine;
public class SpecialCube : SaveableGameObject
{
[Serializable]
public struct SpecialCubeSaveData
{
public SerializableTransform transform;
//public Texture texture; //We need a different way of handling texture
public bool hasCamera;
public SerializableTransform cameraTransform;
}
public override string SaveMe()
{
SpecialCubeSaveData cubeSaveData = new SpecialCubeSaveData();
cubeSaveData.transform = new SerializableTransform(transform);
//cubeSaveData.texture = GetComponent<MeshRenderer>().material.mainTexture;
Camera camera = transform.GetComponentInChildren<Camera>();
if(camera != null)
{
cubeSaveData.hasCamera = true;
cubeSaveData.cameraTransform = new SerializableTransform(camera.transform);
}
return XMLSerialization.ToXMLString<SpecialCubeSaveData>(cubeSaveData);
}
public override void LoadMe(string xmlData)
{
SpecialCubeSaveData cubeSavedData = XMLSerialization.StringToObject<SpecialCubeSaveData>(xmlData);
transform.position = cubeSavedData.transform.position;
transform.rotation = cubeSavedData.transform.rotation;
transform.localScale = cubeSavedData.transform.scale;
//GetComponent<MeshRenderer>().material.mainTexture = cubeSavedData.texture;
if(cubeSavedData.hasCamera)
{
GameObject camera = new GameObject("Camera");
camera.AddComponent<Camera>();
camera.transform.SetParent(transform);
camera.transform.position = cubeSavedData.cameraTransform.position;
camera.transform.rotation = cubeSavedData.cameraTransform.rotation;
}
}
}
For each prefab you would want to save, you would have a single Component that inherits SaveableGameObject. You would create a struct inside the class that has all the saving details you want (we use a struct and not the component itself because we will be creating the object and unity doesnt like creating components without Instantiate and what not), and then you would implement the SaveMe and LoadMe methods. SaveMe puts the data inside the object and then returns the xml string, and LoadMe grabs the xml string, creates the saved data object and then you tell it what to do with it.
The SaveableGameObject class looks like this
Click for code
using System;
using UnityEngine;
[DisallowMultipleComponent]
public abstract class SaveableGameObject : MonoBehaviour, ISaveable
{
//We are going to need the original prefab name to Load it in later.
public string originalPrefabNameReference;
void Awake()
{
//We can only have 1 ISaveable per gameobject.
//We could probably setup a unique id system for each gameobject and their ISaveables so that we can have multiple ISaveables, kinda like
//how the unity networking is setup, but for simplicity we will keep it to one per object.
if(GetComponents<ISaveable>().Length > 1) GameObject.Destroy(this);
//We could avoid this by when pressing save we check all gameobjects and their components to see if they implement ISaveable.
//However, that could cause poor performance depending on how many gameobjects you have in the scene.
//So instead I will just subscribe this object to the savemanager.
SaveManager.saveables.Add(this);
}
void OnDestroy()
{
SaveManager.saveables.Remove(this);
}
public SaveData Save()
{
return new SaveData(originalPrefabNameReference, SaveMe());
}
public void Load(string xmlData)
{
LoadMe(xmlData);
}
public abstract string SaveMe();
public abstract void LoadMe(string xmlData);
}
The only thing you need to worry about is the originalPrefabNameReference. In order to Instantiate the prefab, I need to know its name / path so I can call Resources.Load (it must be in a resources folder).
Ill post the rest of the code below and I will also include the project file so you can see how the UI is setup and what not.
Click for rest of code
using System;
public interface ISaveable
{
SaveData Save();
void Load(string xmlData);
}
using System;
using UnityEngine;
[Serializable]
public struct SerializableTransform
{
public Vector3 position;
public Quaternion rotation;
public Vector3 scale;
public SerializableTransform(Transform transform)
{
this.position = transform.position;
this.rotation = transform.rotation;
this.scale = transform.localScale;
}
}
using System;
[Serializable]
public struct SaveData
{
public string resourceName;
public string xmlData;
public SaveData(string resourceName, string xmlData)
{
this.resourceName = resourceName;
this.xmlData = xmlData;
}
}
using System;
using System.Collections.Generic;
[Serializable]
public class GameSaveData
{
public string saveName;
public List<SaveData> saveDatas;
public GameSaveData(){}
public GameSaveData(string saveName, List<SaveData> saveDatas)
{
this.saveName = saveName;
this.saveDatas = saveDatas;
}
}
using System;
using UnityEngine;
using System.Collections.Generic;
using System.IO;
public class SaveManager
{
public static string folderPath = IOHelper.GetOrCreateFolderInMyDocumentsPath("MyGameSaveDatas");
public static string filePath = Path.Combine(folderPath, "GameSaves.MyGame");
public static HashSet<ISaveable> saveables = new HashSet<ISaveable>();
static List<GameSaveData> gameSaves; //Instead of a separate file per game save, they will all be in one file.
public static GameSaveData currentGameSave {get {return gameSaves[0];}}
public static void SaveGame(GameSaveData saveData)
{
//We need to make sure we load in all our previous saves since we are saving them all in the same file, otherwise the new save will overwrite all old saves.
if(gameSaves == null) LoadGameSaves();
if(saveData != null)
{
if(!gameSaves.Contains(saveData))
{
gameSaves.Add(saveData);
}
SetGameSaveToFirstInList(saveData);
}
XMLSerialization.ToXMLFile(gameSaves, filePath);
}
public static void LoadGame(GameSaveData data)
{
if(data != null)
{
//Since we are going to load a new game, we need to clear the saveables since every object will be recreated.
saveables.Clear();
SetGameSaveToFirstInList(data);
//Now we load in the objects.
for(int i = 0; i < data.saveDatas.Count; i++)
{
ISaveable obj = ((GameObject)GameObject.Instantiate(Resources.Load(data.saveDatas[i].resourceName))).GetComponent<ISaveable>();
obj.Load(data.saveDatas[i].xmlData);
}
}
}
public static List<GameSaveData> LoadGameSaves()
{
if(!File.Exists(filePath))
{
gameSaves = new List<GameSaveData>();
}else{
gameSaves = XMLSerialization.FileToObject<List<GameSaveData>>(filePath);
}
return gameSaves;
}
//We keep our current save at the front of the list to keep track of it. Another way I guess would be to add a unique identifier to each save data.
static void SetGameSaveToFirstInList(GameSaveData data)
{
int itemIndex = gameSaves.IndexOf(data);
if(itemIndex > 0)
{
gameSaves.RemoveAt(itemIndex);
gameSaves.Insert(0, data);
}
}
public static void DeleteAllGameSaves()
{
gameSaves.Clear();
SaveGame(null);
}
public static void DeleteGameSave(GameSaveData data)
{
gameSaves.Remove(data);
SaveGame(null);
}
}
using System;
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
public class SaveUI : MonoBehaviour
{
public Button newSaveButton;
public InputField newSaveNameInputField;
public Button saveButton;
public Button showSavesButton;
public Button deleteAllButton;
public Button saveSelectedButton;
public Button loadSelectedButton;
public Button deleteSelectedButton;
public RectTransform gameSavesDisplayContainer;
public Button gameSavesDisplayButtonPrefab;
Dictionary<Button, GameSaveData> gameSavesDisplayButtons = new Dictionary<Button, GameSaveData>();
Button currentSelectedButton;
static GameSaveData loadThisDataAfterSceneReset;
void Awake()
{
newSaveButton.onClick.AddListener(NewSaveGame);
saveButton.onClick.AddListener(SaveGame);
deleteAllButton.onClick.AddListener(DeleteAllSavedGames);
showSavesButton.onClick.AddListener(ShowGameSaves);
saveSelectedButton.onClick.AddListener(SaveSelectedGame);
loadSelectedButton.onClick.AddListener(LoadSelectedGame);
deleteSelectedButton.onClick.AddListener(DeleteSelectedGame);
}
void OnLevelWasLoaded(int level)
{
if(loadThisDataAfterSceneReset != null)
{
SaveManager.LoadGame(loadThisDataAfterSceneReset);
loadThisDataAfterSceneReset = null;
ShowGameSaves();
}
}
void NewSaveGame()
{
GameSaveData saveData = new GameSaveData();
saveData.saveName = newSaveNameInputField.text;
newSaveNameInputField.text = "";
saveData.saveDatas = GetSaveDatas();
SaveManager.SaveGame(saveData);
ShowGameSaves();
}
void SaveGame()
{
SaveManager.currentGameSave.saveDatas = GetSaveDatas();
SaveManager.SaveGame(SaveManager.currentGameSave);
}
List<SaveData> GetSaveDatas()
{
List<SaveData> saveDatas = new List<SaveData>();
foreach(ISaveable saveable in SaveManager.saveables)
{
saveDatas.Add(saveable.Save());
}
return saveDatas;
}
void SaveSelectedGame()
{
if(currentSelectedButton != null)
{
gameSavesDisplayButtons[currentSelectedButton].saveDatas = GetSaveDatas();
SaveManager.SaveGame(gameSavesDisplayButtons[currentSelectedButton]);
ShowGameSaves();
}
}
void LoadSelectedGame()
{
if(currentSelectedButton != null)
{
//We need to make sure the scene fully loaded before we load our saved game state.
loadThisDataAfterSceneReset = gameSavesDisplayButtons[currentSelectedButton];
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
}
void DeleteSelectedGame()
{
if(currentSelectedButton != null)
{
SaveManager.DeleteGameSave(gameSavesDisplayButtons[currentSelectedButton]);
ShowGameSaves();
}
}
void DeleteAllSavedGames()
{
SaveManager.DeleteAllGameSaves();
ShowGameSaves();
}
void OnGameSaveSelect()
{
if(currentSelectedButton != null) currentSelectedButton.GetComponent<Image>().color = Color.white;
currentSelectedButton = EventSystem.current.currentSelectedGameObject.GetComponent<Button>();
currentSelectedButton.GetComponent<Image>().color = Color.cyan;
}
void ShowGameSaves()
{
foreach(RectTransform rect in gameSavesDisplayContainer)
{
GameObject.Destroy(rect.gameObject);
}
List<GameSaveData> gameSaves = SaveManager.LoadGameSaves();
for(int i = 0; i < gameSaves.Count; i++)
{
Button button = GameObject.Instantiate(gameSavesDisplayButtonPrefab);
button.GetComponentInChildren<Text>().text = gameSaves[i].saveName;
button.transform.SetParent(gameSavesDisplayContainer);
button.onClick.AddListener(OnGameSaveSelect);
gameSavesDisplayButtons.Add(button, gameSaves[i]);
}
}
}
using System;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
public class XMLSerialization
{
public static void ToXMLFile<T>(T xmlObj, string filePath)
{
if(xmlObj == null) return;
File.WriteAllText(filePath, ToXMLString<T>(xmlObj));
}
public static T FileToObject<T>(string filePath) where T : new()
{
if(!File.Exists(filePath)) throw new FileNotFoundException();
return StringToObject<T>(File.ReadAllText(filePath));
}
public static string ToXMLString<T>(T xmlObj)
{
if(xmlObj == null) return String.Empty;
XmlSerializer xmlserializer = new XmlSerializer(typeof(T));
using(StringWriter stringWriter = new StringWriter())
{
using(XmlWriter writer = XmlWriter.Create(stringWriter, new XmlWriterSettings(){Indent = true}))
{
xmlserializer.Serialize(writer, xmlObj);
return stringWriter.ToString();
}
}
}
public static T StringToObject<T>(string xml) where T : new()
{
if(String.IsNullOrEmpty(xml)) return new T();
XmlSerializer xmlserializer = new XmlSerializer(typeof(T));
using(StringReader stringReader = new StringReader(xml))
{
using(XmlReader reader = XmlReader.Create(stringReader))
{
if(xmlserializer.CanDeserialize(reader))
{
return (T)(xmlserializer.Deserialize(reader));
}else{
return new T();
}
}
}
}
}
using System;
using System.IO;
using UnityEngine;
public class IOHelper
{
public static DirectoryInfo CreateFolder(string path)
{
if(!Directory.Exists(path))
{
return Directory.CreateDirectory(path);
}
return null;
}
public static string GetOrCreateFolderInMyDocumentsPath(string folderName)
{
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), folderName);
CreateFolder(path);
return path;
}
}
It might be poorly designed and someone might come around giving a one liner that does it all, who knows =)
Theres a lot of code, but I hope when you break things down youll see that its just a lot of code and not difficult code.
2665689–188085–SaveCubeExample.zip (613 KB)