- Home /
Lots of persistent data
I'm still pretty new to Unity and I'm looking for some advice before I barrel ahead. I'm making a game where the player can set up a room. There can be multiple objects and they can be moved anywhere in the room. There will also be an inventory of items which are not currently in the room. I want to track all this info so when the player closes the game and reopens it, everything is where they left it and their inventory retains it's counts.
I could store all this information in the playerprefs or I could do the same with a filestream, but I think I would have to have a ton of variables created to track everything. Is there a better, more efficient way to keep track of all the various objects and where they are besides creating and storing a variable for every potential object? Any advice would be greatly appreciated.
Answer by CarterG81 · Feb 10, 2017 at 02:41 AM
Is there a better, more efficient way to keep track of all the various objects and where they are besides creating and storing a variable for every potential object?
If you don't store a variable for every potential object, how would the computer know anything? To save every object's position in the world, you'd need to store those variable. If you want to keep track of the player's name, then you'd have to store that string.
However, there are ways to make these variables much more manageable. By using a lot of simple methods, you can eventually make this very easy to do; simple to read & maintain. It is especially easy if you also use this data as part of the "simulation" side of your game- separate from Unity. Update the data, and have Unity change based on the results. (I'll try to explain that in abit).
I would personally stay away from playerprefs unless for some reason you need it (web deployment?).
This is actually simpler than it sounds, even when you have tens of thousands of objects with large numbers of variables.
I have a very large open world with tens of thousands of objects, characters, tiles, etc.
For this, I keep it very simple. A single class which holds everything. Saving, Loading, and sending data across the network (multiplayer) are all done with a single command, which simply saves this class to file. While you could simply update this data when you want to save... what I do is (required for my large open world multiplayer game) is to always have it updated. I use it exclusively as my simulation, separate from Unity. Anything Unity based, like gameobjects, is updated from the data (not vice versa) if that makes sense.
The key is to keep all of this data as [Serializable].
[Serializable]
public class World
{
public Dictionary<myVector2, TileData> WorldTiles = new Dictionary<myVector2, TileData>(); //Collection of all tiles.
public Dictionary<string, ObjectData> allObjectsData = new Dictionary<string, ObjectData>(); //Collection of all objects.
public Dictionary<string, PlayerCharacterData> allPlayerCharactersData = new Dictionary<string, PlayerCharacterData>(); //Collection of all players.
}
These are my three main containers of data. One for the world (terrain), which consists of Tiles. One for all objects in the game - no matter their complixty they all derive from a base class called ObjectData. Finally, all the PlayerCharacter data, which also derives from ObjectData, but is separate for readability (because it's so incredibly important, and has so much more information than any other object type).
Everything could derive from ObjectData, even the tiles, but it's up to you & how you want it to read.
[Serializable]
public class TileData
{
public myVector3 myPosition = new myVector3(); //Tile's Transform.Position
public myVector2 tilePosition = new myVector2(); //Tile's TilePosition
public string myName; //Name of Prefab to load (What Tile type?)
public string myBiome; //Biome the Tile belongs to
//public List<ObjectData> myObjects = new List<ObjectData>(); //A list of all the Tile's objects.
}
As you can see, this is very simple stuff. Mostly just integers (Vectors) and strings.
[Serializable]
public class ObjectData
{
public myVector3 myPosition = new myVector3(); //Object's world transform.position
public string myName = "Blank"; //Name of Prefab to load
public string objectGUID; //UNIQUE ID
}
And some of the objects which derive from ObjectData
//Pickable Items (Apple, Broccoli)
[Serializable]
public class PickableItemData : ObjectData
{
public int currentItemStack; //Current stack#
}
//HarvestableItem (Base Class for everything harvestable: Tree, Stone, Flax)
[Serializable]
public class HarvestableItemData : ObjectData
{
public bool Harvested = false;
public bool Gathered = false;
public int AvailableFruit;
}
//ContainerObject
[Serializable]
public class ContainerObjectData : ObjectData
{
public bool isPlayerEquipped = false;
public bool isOpened = false;
public Item[] Storage;
public bool isBundle = true;
}
The more complex classes, such as Player
[Serializable]
public class PlayerCharacterData : ObjectData
{
public Item[] BackPack; //Items in backpack
public Item[] Equipment; //Items equipped
public Item inMouseHand; //Item in hand
//public Dictionary<ItemType, List<RecipeData>> favoritedRecipes = new Dictionary<ItemType, List<RecipeData>>();
public Dictionary<ItemType, Dictionary<string, RecipeData>> allRecipes = new Dictionary<ItemType, Dictionary<string, RecipeData>>();
public Dictionary<ItemType, List<string>> allFavoriteTabs = new Dictionary<ItemType, List<string>>();
//Character Type (Who are they?)
public string characterName;
public int CurrentHealth;
public int CurrentHunger;
public int CurrentThirst;
}
And of course you have to create your own Serializable Vectors.
//Serializable Vectors
[Serializable]
public class myVector3
{
public float x;
public float y;
public float z;
}
[Serializable]
public class myVector2
{
public float x;
public float z;
}
To save/load it from file, I simply use BinaryFormatter.
public void Save(int saveSlot)
{
///Open or Create Save File
Debug.Log("Saving File to: " + Application.persistentDataPath + " ...");
BinaryFormatter bf = new BinaryFormatter();
FileStream file = File.Open(Application.persistentDataPath + "/SaveData" + saveSlot + ".dat", FileMode.OpenOrCreate);
//Create new SaveData. This will be everything that is saved.
World saveData = theWorld;
bf.Serialize(file, saveData );
file.Close();
And the load function
//Load the file into SaveData.
public World Load(int saveSlot)
{
if (!File.Exists(Application.persistentDataPath + "/SaveData" + saveSlot + ".dat"))
{
Debug.Log("File Not Found! Load Failed.");
return null;
}
BinaryFormatter bf = new BinaryFormatter(); //Serializer
FileStream file = File.Open(Application.persistentDataPath + "/SaveData" + saveSlot + ".dat", FileMode.Open); //Open File
World worldData = (World)bf.Deserialize(file); //Load Data.
file.Close(); //Close File.
return worldData ; //Return Saved Data.
}
}
And an example sending this data across the network (multiplayer)...
/* LOAD WORLD DATA */
//1. Send request to Server
public void ClientRequestWorldData()
{
Debug.Log("Client: STEP1 - Requesting WorldData from Server");
networkObject.SendRpc("RequestServerTo_SendWorld", Receivers.Server); //1. Send request to Server
}
//2. Server receives request. Sends World Data to requesting client.
public override void RequestServerTo_SendWorld(RpcArgs args)
{
Debug.Log("Server: STEP2 - Request to SendWorld received. Sending Data to Client.");
byte[] myBytes = MasterWorld.ObjectToByteArray(MasterWorld.theWorldData);
networkObject.SendRpc(args.Info.SendingPlayer, "Client_ReceiveWorld", Receivers.Target, myBytes);
}
//3. Client Receives World Data from Server.
public override void Client_ReceiveWorld(RpcArgs args)
{
Debug.Log("Client: Step3: Received World Data from Server! Deserializing Data...");
byte[] worldBytes = args.GetNext<byte[]>();
World world = (World)MasterWorld.ByteArrayToObject(worldBytes);
MasterWorld.theWorldData = world;
ClientGlobals.theClient.LoadWorld(MasterWorld.theWorldData);
}
// Convert an object to a byte array
public static byte[] ObjectToByteArray(System.Object obj)
{
BinaryFormatter bf = new BinaryFormatter();
using (var ms = new MemoryStream())
{
bf.Serialize(ms, obj);
return ms.ToArray();
}
}
// Convert a byte array to an Object
public static System.Object ByteArrayToObject(byte[] arrBytes)
{
using (var memStream = new MemoryStream())
{
var binForm = new BinaryFormatter();
memStream.Write(arrBytes, 0, arrBytes.Length);
memStream.Seek(0, SeekOrigin.Begin);
var obj = binForm.Deserialize(memStream);
return obj;
}
}
It seems more complicated than it looks. Once it's setup, it's simple to access any of the data.
/* World */
public void LoadWorld(World worldData)
{
Debug.Log("Loading world...");
StartCoroutine("LoadWorldIteratively", worldData);
}
IEnumerator LoadWorldIteratively(World worldData)
{
Debug.Log("Loading World Iteratively...");
int z = 0;
Debug.Log("Loading World Tiles...");
foreach (KeyValuePair<myVector2, TileData> tileData in worldData.WorldTiles)
{
Vector3 position = new Vector3(tileData.Value.myPosition.x, tileData.Value.myPosition.y, tileData.Value.myPosition.z);
GameObject tile = (GameObject)Instantiate(AllTilePrefabs[tileData.Value.myName], position, Quaternion.identity);
tile.GetComponent<Tile>().LoadTile(tileData.Value);
z++;
}
Debug.Log("Loading GameWorldObjects...");
z = 0;
foreach (KeyValuePair<string, ObjectData> objectData in worldData.allObjectsData)
{
Vector3 position = new Vector3(objectData.Value.myPosition.x, objectData.Value.myPosition.y, objectData.Value.myPosition.z);
GameObject gwo = (GameObject)Instantiate(AllObjectPrefabs[objectData .Value.myName], position, Quaternion.identity);
gwo.GetComponent<GameWorldObject>().LoadObjectData(objectData.Value);
z++;
}
yield return new WaitForEndOfFrame();
Debug.Log("World has Finished Loading!");
WorldFinishedLoading = true;
yield break;
}
And an example of the only function called after instantiation (Load saved data)
public virtual void LoadObjectData(ObjectData newObjectData)
{
myObjectData = newObjectData;
Vector3 myPosition = new Vector3(myObjectData.myPosition.x, myObjectData.myPosition.y, myObjectData.myPosition.z);
transform.position = myPosition;
myTilePosition = GetTilePosition();
MasterWorld.allGameWorldObjects.Add(myObjectData.objectGUID, this);
MasterWorld.allTiles[myTilePosition].myGOBS.Add(gameObject); //Add gameobject to the Tile's list of Unity GameObjects
transform.SetParent(MasterWorld.allTiles[myTilePosition].transform); //Parent the GameObject to Tile.
}
And the only function required when creating a new object
public virtual void SetupNewObject()
{
//UNITY
name = name.Replace("(Clone)", "");
//ObjectData
myObjectData.myPosition.x = transform.position.x;
myObjectData.myPosition.y = transform.position.y;
myObjectData.myPosition.z = transform.position.z;
myObjectData.myName = name;
myObjectData.objectGUID = Guid.NewGuid().ToString();
//Debug.Log("My UniqueID is: " + myObjectData.objectGUID);
//GameWorldObject
myTilePosition = GetTilePosition();
//Register with MasterWorld
MasterWorld.allGameWorldObjects.Add(myObjectData.objectGUID, this);
MasterWorld.theWorldData.allObjectsData.Add(myObjectData.objectGUID, myObjectData);
MasterWorld.allTiles[myTilePosition].myGOBS.Add(gameObject); //Add gameobject to the Tile's list of Unity GameObjects
transform.SetParent(MasterWorld.allTiles[myTilePosition].transform); //Parent the GameObject to Tile.
}
This should also show how you can update gameobjects/data.
I'm sick with the Flu & on my small netbook, so I'm not sure if this fully answers the question or if it's complete in explanation. But I hope this helps.
Ultiamtely, this is an awesome way for me to handle saving/loading/sending data. It is very clean & readable for me, easy to understand & track, easy to access, and extremely easy to save/load/send. I just call one method and it's done. Creating Unity gameobjects from the pure C# data is just a matter of instantiation by name and then one load method.
You don't have to use Dictionaries if you don't want to. I initially used arrays, then Lists, and finally Dictionaries for faster access times alongside much easier management.
That looks very promising! Thank you so much for the suggestion. This looks like it will work perfectly. I'll give it a shot straight away!
That is fantastic! :) I am so glad I was able to help! If anything doesn't work out, or if I explained anything poorly, feel free to comment & I'll try to update the answer so it's better for others in the future.
This is my first post on any unity forum, so appologies if ive done something wrong considering the age of this question. @CarterG81 Thanks so much for this answer, its given me great help with understanding saving and loading data classes. Im currently having some trouble implementing data storage into my own project, and was wondering if I could get some more insight into how these scripts were structured and how they communicate/are updated by other classes.
Im fairly new to unity and have been mesing around with making a 3D platformer style game. I currently have a room of pickups, and an ontriggerEnter that updates a static 'ScoreData' Dictionary within my 'GameData' class, with the kind of pickup it is and how much it scores.
Everything updates fine on pickup, and I understand how to serialize that data and bring it back in, but I'm confused as to how to then update the GameData with the one I have previously saved. $$anonymous$$y guess is that its something to do with 'ScoreData' being static, but then I fall stuck again not knowing how to update it when I run over a pickup.
Hopefully you could help clear up some confusion for me. Thanks!
Can you reply with a pastebin of your code? It is much easier for me to just go through it and fix it myself, then explain the changes.
And to clarify, your game works fine at runtime, but you need help LOADING the data from your save file? Is that correct?
If I dont respond, you can re$$anonymous$$d me again by P$$anonymous$$ing me on the forums. I check that more often than my email (here).
You can also post the problem and code on the forums in a new thread or new question here, then P$$anonymous$$ me the link on the forums.
Answer by mehtanitish · Apr 28, 2020 at 07:28 PM
In case you have RDBMS type of data, you can opt for SQLite
Your answer
Follow this Question
Related Questions
How to make it so Unity remembers purchases made by players whenever they play? (C#) 1 Answer
How to make Unity remember purchases made by players using Firebase? 0 Answers
Persistent data, Save file doesn't come out as hoped 1 Answer
Keeping native data around across recompile 0 Answers
Most convenient and fast way to persist/access/modify many values from scene to scene? (C#) 0 Answers