- Home /
Save levels gameobjects when closing game/leaving scene
Hello,
I didn't know how to search it on the website so I'm just opening a new question.
I have a level with a lot of different objects etc that you can push around.
When you have pushed around some objects and you close the game/app and when you start it again next time I want all the objects that you pushed to be where you pushed them last time etc, how can I safe all the information of the objects when you close the scene or when you close the game completely?
Answer by Cherno · Apr 20, 2015 at 11:27 AM
I'm sorry if I disappoint you, but there are no simple solutions for this rather common feature. It will all involve handling Fields and Values of class instances to some degree. As a start, look into using BinaryFormatters to write serialized data to a file and load and deserialize that data. From there, try to think about how a gameObject can be saved in regards to position and rotation. You need a "Container" style class which holds the Transform values in a way that they can be serialized (Vector3 Type can't be serialized, for example, to a conversion is needed). You also need a unique ID for each gameObject so the correct values can be assigned to the correct object after loading, as well as a way to find out how which prefab to instantiate for that object upon loading.
So, let's start with the Container class:
using UnityEngine;
using System.Collections;
[System.Serializable]
public class SceneObject {
public string name;
public int id;
public bool dontLoad;
public float posX;
public float posY;
public float posZ;
public float rotX;
public float rotY;
public float rotZ;
public float rotW;
}
We also need a MonoBehaviour script on every gameobject that should be saved:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class ObjectIdentity : MonoBehaviour
{
public int id = -1;
public bool dontLoad = false;
public void SetID() {
id = 0;
List<int> takenIDs = new List<int>();
ObjectIdentity[] obj = GameObject.FindObjectsOfType(typeof (ObjectIdentity)) as ObjectIdentity[];
foreach (ObjectIdentity o in obj) {
if(o.transform.gameObject != gameObject) {
takenIDs.Add (o.id);
//Debug.Log("taken ID added: " + o.id);
}
}
if(takenIDs.Count > 0) {
for(id = 0; id < takenIDs.Count; id++) {
if(takenIDs.Contains(id) == false) {
break;
}
}
}
//Debug.Log("New Object ID: " + id);
}
Every time a gameobject with an Identifier script is created, the SetID needs to be called. It's use ful to create an Editor Extension to call the function during Edit mode if the scene is edited in Edit mode as opposed to runtime.
Next, we need a class that can hold our whole scene, including a List of instances of the class above, thereby storing our gameobjects in serializable form:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
[System.Serializable]
public class Game { //don't need ": Monobehaviour" because we are not attaching it to a game object
public string savegameName;
public List<SceneObject> sceneObjects = new List<SceneObject>();
}
Now, we need a function on one of our MonoBehaviour scripts that collects the gameobjects in the scene and adds them to a new instance of the Game class, and then calling anothe rfunction that does the actual saving.
public void SaveGame() {
Game newGame = new Game();
object[] obj = GameObject.FindObjectsOfType(typeof (GameObject));
foreach (object o in obj) {
GameObject g = (GameObject) o;
ObjectIdentity idScript = g.GetComponent<ObjectIdentity>();
if (idScript != null) {
SceneObject newObject = new SceneObject();
newObject.name = g.name;
newObject.id = idScript.id;
newObject.dontLoad = idScript.dontLoad;
newObject.posX = g.transform.position.x;
newObject.posY = g.transform.position.y;
newObject.posZ = g.transform.position.z;
newObject.rotX = g.transform.rotation.x;
newObject.rotY = g.transform.rotation.y;
newObject.rotZ = g.transform.rotation.z;
newObject.rotW = g.transform.rotation.w;
}
}
newGame.savegameName = "blubb";
SaveLoad.Save(newGame);
}
Now for the writing and reading of files itself, here the BinaryFormatter comes into play:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
public static class SaveLoad {
public static Game loadedGame = new Game();
public static List<Game> savedGames = new List<Game>();
//it's static so we can call it from anywhere
public static void Save(Game saveGame) {
//SaveLoad.savedGames.Add(Game.current);
BinaryFormatter bf = new BinaryFormatter();
string path = "C:/Test Savegames/";
//path = Application.persistentDataPath is a string, so if you wanted you can put that into debug.log if you want to know where save games are located
FileStream file = File.Create (path + saveGame.savegameName + ".gd"); //you can call it anything you want, but the Directory must be present
bf.Serialize(file, saveGame);
file.Close();
Debug.Log("Saved Game: " + saveGame.savegameName);
}
public static void Load(string gameToLoad) {
string path = "C:/Test Savegames/";
if(File.Exists(path + gameToLoad + ".gd")) {
BinaryFormatter bf = new BinaryFormatter();
FileStream file = File.Open(path + gameToLoad + ".gd", FileMode.Open);
loadedGame = (Game)bf.Deserialize(file);
file.Close();
Debug.Log("Loaded Game: " + loadedGame.savegameName);
}
}
}
To load, we need another function similar to SaveGame(). We also need a Dictionary so we can instantiate a gameobject based on it's name. Ther are other ways to do this, of course, such as having the ObjectIdentifierscript keep the path and name of it's prefab.
public Dictionary<string,GameObject> PrefabDict;
void Start() {
PrefabDict = new Dictionary<string,GameObject>();
GameObject[] objects = Resources.LoadAll<GameObject>("Prefabs");//or whereever prefabs are stored.
foreach (var o in objects) {
if(PrefabDict.ContainsKey(o.name) == false) {
PrefabDict.Add (o.name, o);
}
}
}
public void LoadGame() {
ClearScene();
SaveLoad.Load("blubb");
Game loadedGame = SaveLoad.loadedGame;
int x = 0;
int y = 0;
int z = 0;
foreach(SceneObject loadedObject in loadedGame.sceneObjects) {
if(loadedObject.dontLoad == false) {
GameObject goCur = Instantiate(PrefabDict[loadedObject.name], new Vector3(loadedObject.posX, loadedObject.posY,loadedObject.posZ), new Quaternion(loadedObject.rotX,loadedObject.rotY,loadedObject.rotZ,loadedObject.rotW)) as GameObject;
goCur.name = loadedObject.name;
if(goCur.GetComponent<ObjectIdentity>() == false) {
ObjectIdentity oi = goCur.AddComponent<ObjectIdentity>();
}
ObjectIdentity idScript = goCur.GetComponent<ObjectIdentity>();
idScript.id = loadedObject.id;
//Debug.Log("GameObject loaded: " + goCur.name);
}
}
}
public void ClearScene() {
object[] obj = GameObject.FindObjectsOfType(typeof (GameObject));
foreach (object o in obj) {
GameObject g = (GameObject) o;
var destroyScript = g.GetComponent<DontDestroy>();
if (destroyScript == null || destroyScript != null && destroyScript.dontDestroy == false)
{
Destroy (g);
}
}
}
Any questions? Didn't think so ;) This would be the , in my opinion, most basic way of saving and loading gameobject (only their position and rotation, of course). Keeping track of Components is much, much more difficult.
Thank you a lot for your answer and all the effort you have put into this, this is indeed what I'm looking for and as you said it's a rather common thing that is somewhat difficult to fix.
I really appreciate your answer and the great explanation! let's hope for a more simple way of doing this with unity somewhere in the future :).
Well, I generally avoid saving to PlayerPrefs but from what I have read so far, it might be possible to use the PlayerPref setvalue functions to maybe set the transform values for all gameobject or something.
You reckon? I have used playerprefs before to use with gameobjects but that was just when I had a number of objects that could be active and not active and would save the playerprefs as an int with 0 and 1 and then looked at the start of the scene wether it was already picked up or not and would set active if not etc, how'd you think you would do it with pos/rot?
Answer by Ekta-Mehta-D · Apr 17, 2015 at 11:20 AM
But I have for example 100 gameobjects taht I want to save all the rotations and positions of when you open the scene next time, i dont think this will save those, right? how would i do that wouldnt that require a lot of code?
Answer by julianjulianov · Feb 14, 2021 at 02:34 PM
Hello! I found a simpler way to keep the level the player has reached. I have created 7 levels. If the player wants to start from the first level, I have indicated with a message that this is done through the menu by pressing the Quit the game button.
public class MainMenu: MonoBehavior
{
public void QuitGame ()
{
PlayerPrefs.DeleteKey ("Levels");
Application.Quit ();
}
}
This is the script attached to the game menu!
I will now show the script for saving the level of the game.
public void Start()
{
int counterLevels = PlayerPrefs.GetInt("Levels");
if (counterLevels > 0 && SceneManager.GetSceneByName("Level 1").isLoaded)
{
SceneManager.LoadScene("Level " + (counterLevels + 1));
}
}
public void Update()
{
foreach (Enemy enemy in _enemies)
{
if (enemy != null)
{
string _nameCurrentScene = SceneManager.GetActiveScene().name;
SceneManager.LoadScene(_nameCurrentScene);
return;
}
}
int counterLevels = PlayerPrefs.GetInt("Levels");
PlayerPrefs.SetInt("Levels", counterLevels + 1);
SceneManager.LoadScene(counterLevels + 1);
}
You can indeed save this way, but PlayerPrefs are saved in easily accessible, editable and human readable files such as the plist file on iOS and you can change the values quite easily. So that makes sensitive data exposed and/or cheating very easy depending on what you’re storing. It is much better practice using the binary formatter and saving files deeper in the file system of the device. I personally only ever use PlayerPrefs for simple settings, but never for player progress or sensitive data.