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.

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.

In case you have RDBMS type of data, you can opt for SQLite