Issues with saving and loading certain pieces of data from RPG game

Hi there, I’m working on my first game and it’s an RPG with lots of attributes, skills, and items so I decided it was about time to start working on the save/load system last week and after some help from this forum I decided to try JSON utilities for this.

Some things work great (primitive data types), but others have had me pulling my hair out because I can’t seem to get it working, and I can’t find any tutorials/guides that deal with saving anything more complex than primitive data types.

Here is the current situation:

I’m trying to currently save everything having to do with the player (attributes, and inventory mainly). I can save the attributes just fine (strength, dexterity, current health, etc.). All of those are either ints or ScriptableObjects that hold int values.

The issue is with the inventory. My player’s inventory is an SO with List<Item> inventoryList = new List<Item>(); and Item is a public class with a bunch of item properties (name, type, value, etc.).

I can’t for the life of me figure out how to save that list of Items to the JSON file that I was working with.

Here is my SaveLoadManager for saving to JSON:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SaveLoadManager : MonoBehaviour
{
    Player player;

    private void Awake()
    {
        player = FindObjectOfType<Player>();
    }
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.H))
        {
            Save();
        }
        else if (Input.GetKeyDown(KeyCode.J))
        {
            Load();
        }
    }
    public void Save()
    {
        SaveObject saveObject = CreateSaveObject();
      
        foreach (Item item in player.Inventory.InventoryList)
        {
            Debug.Log("Adding " + item.ItemName + " to save list...");
            saveObject.itemList.Add(item);
        }

        string jsonSave = JsonUtility.ToJson(saveObject);
        SaveLoadSystem.SaveData(jsonSave);
    }
    public void Load()
    {
        string loadString = SaveLoadSystem.LoadData();

        if (loadString != null)
        {
            SaveObject saveObject = JsonUtility.FromJson<SaveObject>(loadString);
            AllocateLoad(saveObject);
            Debug.Log("Loaded: " + loadString);
            player.PlayerMoney.CalculateMoney();
        }
        else
        {
            Debug.Log("No save file to load");
        }
      

    }
    private SaveObject CreateSaveObject()
    {
        SaveObject newSaveObject = new SaveObject
        {
            strength = player.PlayerStats.Strength,
            dexterity = player.PlayerStats.Dexterity,
            constitution = player.PlayerStats.Constitution,
            intelligence = player.PlayerStats.Intelligence,
            perception = player.PlayerStats.Perception,
            charisma = player.PlayerStats.Charisma,
            raceBaseHP = player.PlayerStats.BaseHealth,
            currentHP = player.PlayerStats.CurrentPlayerHP.IntVariable,

            playerCopper = player.PlayerMoney.Copper,
            playerSilver = player.PlayerMoney.Silver,
            playerGold = player.PlayerMoney.Gold,

            itemList = new List<Item>(),

        };

        return newSaveObject;
    }
    private void AllocateLoad(SaveObject saveObject)
    {
        player.PlayerStats.Strength = saveObject.strength;
        player.PlayerStats.Dexterity = saveObject.dexterity;
        player.PlayerStats.Constitution = saveObject.constitution;
        player.PlayerStats.Intelligence = saveObject.intelligence;
        player.PlayerStats.Perception = saveObject.perception;
        player.PlayerStats.Charisma = saveObject.charisma;
        player.PlayerStats.BaseHealth = saveObject.raceBaseHP;
        player.PlayerStats.CurrentPlayerHP.IntVariable = saveObject.currentHP;
      
        player.PlayerMoney.Copper = saveObject.playerCopper;
        player.PlayerMoney.Silver = saveObject.playerSilver;
        player.PlayerMoney.Gold = saveObject.playerGold;


    }
    private class SaveObject
    {
        public int strength;
        public int dexterity;
        public int constitution;
        public int intelligence;
        public int perception;
        public int charisma;
        public int raceBaseHP;
        public int currentHP;

        public int playerCopper;
        public int playerSilver;
        public int playerGold;

        public List<Item> itemList;
    }
}

And here is my SaveLoadSystem code which basically just writes this manager’s work to the file:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;


public static class SaveLoadSystem
{
    private static string SAVE_FOLDER = "I:/Game Dev/PE Saves/";
    private static string DATA_PATH = "MyGameSave.sav";
    private static string FULL_PATH = SAVE_FOLDER + DATA_PATH;

    public static void SaveData(string saveString)
    {
        if (!Directory.Exists(SAVE_FOLDER))
        {
            Directory.CreateDirectory(SAVE_FOLDER);
        }
        Debug.Log("Save Location: " + SAVE_FOLDER);
        File.WriteAllText(FULL_PATH, saveString);
        Debug.Log("Saved: " + saveString);      
    }

    public static string LoadData()
    {
        if (File.Exists(FULL_PATH))
        {
            string loadString = File.ReadAllText(FULL_PATH);                  
            return loadString;
        }
        else
        {          
            return null;
        }      
    }
}

I’m guessing that what I’m doing is just not an option, and that I need to save the inventory items to the file in a different way, but I don’t know how. Since I have to pass in an object for the JSON utility I need to get all the items in the player’s inventory to save onto that object and I can’t figure out how to do that.

I even tried switching to a binary formatter thinking maybe I could figure that out but nope.

Any helps with this would be greatly appreciated, thanks so much!

EDIT: Was asked to post my Item class so here it is:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class Item
{
    // Main properties
    string itemName = "Item name";
    ItemSubType itemSubType;
    float weight = 0.0f;
    Quality itemQuality;
    private bool consumable = false;

    // Weapon properties
    int minDamage = 10;
    int maxDamage = 15;
    float attackSpeedBonus = 1f;
    WeaponSpeed weaponSpeed;
    float range = 2f;
    bool ranged = false;

    // Armor properties
    int armor = 0;
   
    // Potion properties
    int healAmount = 0;
    int damageAmount = 0;

    // Attribute properties
    int strength = 0;
    int dexterity = 0;
    int constitution = 0;
    int intelligence = 0;
    int perception = 0;
    int charisma = 0;

    // Modifiers

    List<Modifier> modifiers = new List<Modifier>();

    // Ownership properties
    Owner owner;
    bool stolen = false;

    // Value properties
    int copper = 0;
    int silver = 0;
    int gold = 0;

    float floatCurrency;
    int whole;
    float remainder;
    
    public void SetItemProperties(Item item)
    {
        itemName = item.ItemName;
        itemSubType = item.ItemSubType;
        weight = item.Weight;
        itemQuality = item.ItemQuality;
        consumable = item.Consumable;
        minDamage = item.MinDamage;
        maxDamage = item.MaxDamage;
        attackSpeedBonus = item.AttackSpeedBonus;
        weaponSpeed = item.WeaponSpeed;
        range = item.Range;
        ranged = item.Ranged;
        armor = item.Armor;
        healAmount = item.HealAmount;
        damageAmount = item.DamageAmount;
        modifiers = item.Modifiers;
        copper = item.Copper;
        silver = item.Silver;
        gold = item.Gold;
    }

    public void CalculateValue(Item item)
    {
        floatCurrency = (float)item.copper / 100;
        whole = (int)Mathf.Floor(floatCurrency);
        remainder = item.copper;
        remainder = remainder - whole * 100;

        item.copper = (int)remainder;
        item.silver += whole;

        floatCurrency = (float)item.silver / 100;
        whole = (int)Mathf.Floor(floatCurrency);
        remainder = item.silver;
        remainder = remainder - whole * 100;

        item.silver = (int)remainder;
        item.gold += whole;
    }

    #region Getters & Setters
   
    public string ItemName
    {
        get { return itemName; }
        set { itemName = value; }
    }
    public ItemSubType ItemSubType
    {
        get { return itemSubType; }
        set { itemSubType = value; }
    }
    public float Weight
    {
        get { return weight; }
        set { weight = value; }
    }
    public Quality ItemQuality
    {
        get { return itemQuality; }
        set { itemQuality = value; }
    }
    public bool Consumable
    {
        get { return consumable; }
        set { consumable = value; }
    }
    public int MinDamage
    {
        get { return minDamage; }
        set { minDamage = value; }
    }
    public int MaxDamage
    {
        get { return maxDamage; }
        set { maxDamage = value; }
    }
    public float AttackSpeedBonus
    {
        get { return attackSpeedBonus; }
        set { attackSpeedBonus = value; }
    }
    public WeaponSpeed WeaponSpeed
    {
        get { return weaponSpeed; }
        set { weaponSpeed = value; }
    }
    public float Range
    {
        get { return range; }
        set { range = value; }
    }
    public bool Ranged
    {
        get { return ranged; }
        set { ranged = value; }
    }
    public int Armor
    {
        get { return armor; }
        set { armor = value; }
    }
    public int HealAmount
    {
        get { return healAmount; }
        set { healAmount = value; }
    }
    public int DamageAmount
    {
        get { return damageAmount; }
        set { damageAmount = value; }
    }
    public int Strength
    {
        get { return strength; }
        set { strength = value; }
    }
    public int Dexterity
    {
        get { return dexterity; }
        set { dexterity = value; }
    }
    public int Constitution
    {
        get { return constitution; }
        set { constitution = value; }
    }
    public int Intelligence
    {
        get { return intelligence; }
        set { intelligence = value; }
    }
    public int Perception
    {
        get { return perception; }
        set { perception = value; }
    }
    public int Charisma
    {
        get { return charisma; }
        set { charisma = value; }
    }
    public List<Modifier> Modifiers
    {
        get { return modifiers; }
        set { modifiers = value; }
    }
    public Owner Owner
    {
        get { return owner; }
        set { owner = value; }
    }
    public bool Stolen
    {
        get { return stolen; }
        set { stolen = value; }
    }
    public int Copper
    {
        get { return copper; }
        set { copper = value; }
    }
    public int Silver
    {
        get { return silver; }
        set { silver = value; }
    }
    public int Gold
    {
        get { return gold; }
        set { gold = value; }
    }
    #endregion
}

Can you post your Item class code as well? It maybe that Item can’t be serialized for one reason or another, which would mean that no amount of formatting will let it be saved. While I’m waiting for that, make sure that Item is marked System.Serializable and that all of its variables that you want to save are public and serializable. Generally speaking, Unity objects are not serializable, so you have to save the properties of Unity objects you want some other way.

Thanks for the reply, I posted my Item class.

I had made it serializable but I didn’t realize that every variable needed to be public.

This is a bit weird to me, based on what you’re saying, does that mean that anything I want to be saved in my game has to be public? Feels a bit counterintuitive since I’ve always learned not to make things public as a general rule.

There are some serializers that can handle private variables, but if you think about how you would write a serializer yourself, how would it know about private fields? It would have to use reflection to find them, which isn’t ideal for most cases. One approach you could use is to have a data object that just contains the fields you want to serialize, and then pass that to a constructor (or read it on Awake or the serialization callbacks Unity - Scripting API: ISerializationCallbackReceiver). That way, you have the public data object that only exists for the purposes of serialization. When your object is serialized, it will write out an ItemData object. Then, when it is deserialized, it will read the ItemData object back in and update itself. In my experience, that’s generally how serialization goes; you only serialize public data, and if you want to serialize private data, you make a public representation (or Surrogate) of it that can be serialized.
EDIT:
I haven’t used the Binary Formatter in a while and it looks like I’m wrong. Lots of places are mentioning that it can serialize private fields, so I’m guessing something else was going wrong when you made that attempt.

1 Like

Thanks again for the reply! So if I’m understanding you correctly, you’re suggesting I make a new class like ItemSaveData that has all public variables and when I go to save items I copy my item’s properties onto that class and save those onto the JSON instead?

Sort of like I’m doing in my above code with the SaveObject class.

I thought about doing that, but then I would have multiple objects to add to the JSON wouldn’t I? Isn’t that an issue?

EDIT: Oh wait! I guess I could change my List on SaveObject to a List? I’ll go try this haha

Yep, that’s right. When I’m making save systems for my games, I use JSON to save everything and it works exactly like that. There’s the runtime data (Item) and then there’s the serialization data (ItemSaveData). ItemSaveData is only used for serialization, and you have to maintain it as you change what fields an Item has, but I’ve found that to be a good thing since it discourages me from making tons of changes to what needs to be serialized, although some others may find that philosophy questionable.

1 Like

Ah I think I got this working, thanks a million for the help!

One thing that still is giving me trouble, and I’d love to hear your thoughts on this cause it seems like you’ve made this kind of system before.

If you look at my Item class, a few of those properties are actually ScriptableObjects, like ItemSubType, which I was using basically as you would use enums. This method of doing things made it easier to create more types on the fly without going into the code though and I was really liking that.

How would I got about saving something like that to the new Serializable ItemSaveData? Any ideas on this?

The only thing I can think of is to save the string name of the ItemSubType SO and then when I’m recreating the item in the player’s inventory, applying the ItemSubType that matches the string that got saved.

Just wondering if there’s a better way of doing this though.

FYI, I have an answer on another thread regarding serialization. Check out if you like, although it would mean replacing your saving objects with more standard solution:
https://discussions.unity.com/t/732665/10

Yeah, I think that is ideal in your case. I’m actually working on an RPG right now as well haha. In cases where I don’t want to make every subclass have its own serialized data type, I follow the web standard and simply make the first field in the json “type”:“someString” and then look at that before continuing to deserialize it, which sounds exactly like what you are planning to do. If the different subtype you have are very different and require different fields, you should probably give them different serialization data instead. In my game, weapons and consumables (like a health kit) are both subclasses of Item, and they have their own serialization data (WeaponData, ConsumableData) that are subclasses of ItemData.

@WallaceT_MFM

Thanks a ton for all the help, I’m making a ton of progress now (at least compared to where I was at haha). Best of luck to you on the RPG!

@

Thanks for the reply, I’ll be sure to look through this!