Page 3 of 7

Re: Save System Question ;)

Posted: Fri May 13, 2022 5:06 pm
by HawkX
.... I am so sorry for "harassing" you so much....

I know it is not your place to "teach" me how to be a proper or decent programmer... yet you do... and i cannot thank you enough for all you have done!! I can only hope that your business is profitable and that you have a decent living!!

Back to this "new" thing i'm trying to achieve...

I have made a ton of progress on creating my "Saver" module... made my own class which works PERFECTLY now!

I'm gonig to paste a copy here (just for reference so you see the type and logic I created which might help you understand what I'm trying to achieve with my actual question)

Code: Select all

namespace PixelCrushers
{
    public class ShumaPickupSaver : Saver
    {
        [Serializable]
        public class Data
        {
            public PickupSpawner pickup;
            public Vector3 position;
            public InventoryItem item;
            public int number;
            public bool boolPickupPickedUp;
            public bool boolInstanciated;
        }

        public override string RecordData()
        {
            /// This method should return a string that represents the data you want to save.
            /// You can use SaveSystem.Serialize() to serialize a serializable object to a 
            /// string. This will use the serializer component on the Save System GameObject,
            /// which defaults to Unity's built-in JSON serialization. Remember that Unity
            /// cannot directly serialize lists or arrays, so you must put them inside a
            /// class.
            /// 
            /// If you use a class to hold the data, use SaveSystem.Serialize to return a 
            /// serialized version:
            ///

            Data data = new Data();
            data.pickup = GetComponent<PickupSpawner>();
            data.boolPickupPickedUp = GetComponent<PickupSpawner>().boolPickupPickedUp;
            data.item = GetComponent<PickupSpawner>().item;
            data.number = GetComponent<PickupSpawner>().number;
            data.boolInstanciated = GetComponent<PickupSpawner>().boolPickupInstanciated;
            return SaveSystem.Serialize(data);

            //return string.Empty;
        }

        public override void ApplyData(string s)
        {
            /// This method should process the string representation of saved data and apply
            /// it to the current state of the game. You can use SaveSystem.Deserialize()
            /// to deserialize the string to an object that specifies the state to apply to
            /// the game.
            /// 

            if (string.IsNullOrEmpty(s)) return; // No data to apply.
            Data data = SaveSystem.Deserialize<Data>(s);
            if (data == null) return; // Serialized string isn't valid.

            if (data.boolInstanciated)
            {
                GetComponent<PickupSpawner>().item = data.item;
                GetComponent<PickupSpawner>().number = data.number;
            }
            if (data.boolPickupPickedUp)
            {
                GetComponent<PickupSpawner>().boolPickupPickedUp = true;
                if (GetComponent<PickupSpawner>().boolPickupSpawned == false)
                {
                    GetComponent<PickupSpawner>().SpawnPickup();
                }
                GetComponent<PickupSpawner>().DestroyPickup();
            }
            else if (GetComponent<PickupSpawner>().boolPickupSpawned == false)
            {
                GetComponent<PickupSpawner>().SpawnPickup();
            }
        }
Now, what i'm trying to do is to "make this work" but on a "destructible saver" which ALSO spawns a "spawned object"

The destructible saver works great no problems here...

The Spawned Object also works, and gets restored by the SpawnedObjectManager

However, the "spawned object" contains a very important script reference (InventoryItem item) and (int Number) which are not getting saved or restored...

Any tips on how where to edit/modify your script to make them work with it? :)

Thanks again and have a great weekend!

Re: Save System Question ;)

Posted: Fri May 13, 2022 5:41 pm
by Tony Li
Hi,

By default, Unity only serializes basic types (bool, float, Vector3, string, etc.) and lists of those types. If InventoryItem is a reference to an asset such as a ScriptableObject asset, it won't be able to serialize the reference. Could that be what's happening here?

Re: Save System Question ;)

Posted: Fri May 13, 2022 6:09 pm
by HawkX
actually the item is a long "hash" number...

here is the content (un encrypted json) from my save file from one of the pickup on the ground :

Code: Select all

{
            "key": "Pickup Spawner (03)ShumaPickupSaver",
            "sceneIndex": -1,
            "data": "{\n    \"pickup\": {\n        \"instanceID\": 221492\n    },\n    \"position\": {\n        \"x\": 0.0,\n        \"y\": 0.0,\n        \"z\": 0.0\n    },\n    \"item\": {\n        \"instanceID\": 30458\n    },\n    \"number\": 12,\n    \"boolPickupPickedUp\": false,\n    \"boolInstanciated\": false\n}"
        },
the "InventoryItem item" is "instanceID 30458", and number (int) is 12... the one on the ground is correctly saved and restored using my custom script I pasted above...

the one that doesnt work is the one spawned from a destructible object that is restored with the spawnedobjectmanager

The "Pickup Spawner" is restored to its proper position, but its "item" is null and number is 0...

Re: Save System Question ;)

Posted: Fri May 13, 2022 9:04 pm
by Tony Li
On your spawned object, is the Spawned Object component's Save Unique Saver Data checkbox ticked?

Is instanceID a Unity object instance ID? If so, they're not stable. They're different every time Unity instantiates an object.

If none of that info helps, can you send a reproduction project to tony (at) pixelcrushers.com?

Re: Save System Question ;)

Posted: Fri May 13, 2022 10:02 pm
by HawkX
the project is already close to 8gb and i dont think the company would like me to send it... making a "tiny" version including all the necessary things would take way too many hours...

so i'll try to answer your questions and give as much info as possible...
I'll also attach my full save file...

yes the spawnsaver has all checkbox checked including Save Unique Saver Data...

there are 2 instance id for every "Pickup Spawner (xx)ShumaPickupSaver"
the first one is the "pickup (gameobject)" i assume.. that one seems to indicate the gameobject id in the scene... since when i tried to use my own save system (copied in previous message) on the "instanciated ones", the id was always negative...

the 2nd instance id (for item) seems to be always the same "depending" on the item type... (i currently have 4 item types scriptable object) (30490 and 30444 are only there twice), and i put a ton of potions and spirits on the ground to test out another inventory module earlier, (30458 = potion) and (30444 = spirit) -- not that the numbers actually matter, but they are not "random" or generated, since reloading the save for those works.. (but they were part of the scene and were not instantiated by the destruction of a crate or bottle)

in the case of the SaveDataPrefabSpawnedObjectManager, you see the 2 gameobject that were copied, but their value of item and number are both at 0

Re: Save System Question ;)

Posted: Sat May 14, 2022 8:51 am
by Tony Li
Can you share the class definitions for PickupSpawner and InventoryItem?

If either of those classes have fields that are not basic types (bool, float, Vector3, etc.), please include the class definitions for those fields, too.

Re: Save System Question ;)

Posted: Sat May 14, 2022 11:01 am
by HawkX
PickupSpawner :

Code: Select all

using UnityEngine;
using GameDevTV.Saving;

namespace GameDevTV.Inventories
{
    /// <summary>
    /// Spawns pickups that should exist on first load in a level. This
    /// automatically spawns the correct prefab for a given inventory item.
    /// </summary>
    public class PickupSpawner : MonoBehaviour, ISaveable
    {
        // CONFIG DATA
        [SerializeField] public InventoryItem item = null;
        [SerializeField] public int number = 1;

        public bool boolPickupSpawned = false;
        public bool boolPickupPickedUp = false;
        public bool boolPickupInstanciated = false;

        // LIFECYCLE METHODS
        private void Awake()
        {
            // Spawn in Awake so can be destroyed by save system after.
            if (item != null)
            {
                if (boolPickupPickedUp == false)
                SpawnPickup();
            }
        }

        // PUBLIC

        /// <summary>
        /// Returns the pickup spawned by this class if it exists.
        /// </summary>
        /// <returns>Returns null if the pickup has been collected.</returns>
        public Pickup GetPickup() 
        { 
            return GetComponentInChildren<Pickup>();
        }

        /// <summary>
        /// True if the pickup was collected.
        /// </summary>
        public bool isCollected() 
        { 
            return GetPickup() == null;
        }

        //PRIVATE

        public void RestorePickup()
        {

        }

        public void SpawnPickup()
        {
            if (boolPickupSpawned == false)
            {
                var spawnedPickup = item.SpawnPickup(transform.position, number);
                spawnedPickup.transform.SetParent(transform);
                boolPickupSpawned = true;
            }
        }

        public void DestroyPickup()
        {
            var pickup = GetPickup();
            if (pickup)
            {
                pickup.transform.SetParent(null);
                Destroy(pickup.gameObject);
            }
        }

        object ISaveable.CaptureState()
        {
            return isCollected();
        }

        void ISaveable.RestoreState(object state)
        {
            bool shouldBeCollected = (bool)state;

            if (shouldBeCollected && !isCollected())
            {
                DestroyPickup();
            }

            if (!shouldBeCollected && isCollected())
            {
                SpawnPickup();
            }
        }
    }
}
InventoryITEM

Code: Select all

using System;
using System.Collections.Generic;
using UnityEngine;
using static GameDevTV.Inventories.Equipment;

namespace GameDevTV.Inventories
{
    /// <summary>
    /// A ScriptableObject that represents any item that can be put in an
    /// inventory.
    /// </summary>
    /// <remarks>
    /// In practice, you are likely to use a subclass such as `ActionItem` or
    /// `EquipableItem`.
    /// </remarks>
    public abstract class InventoryItem : ScriptableObject, ISerializationCallbackReceiver
    {
        // CONFIG DATA
        [Tooltip("Auto-generated UUID for saving/loading. Clear this field if you want to generate a new one.")]
        [SerializeField] string itemID = null;
        [Tooltip("Item name to be displayed in UI.")]
        [SerializeField] string displayName = null;
        [Tooltip("Item description to be displayed in UI.")]
        [SerializeField][TextArea] string description = null;
        [Tooltip("The UI icon to represent this item in the inventory.")]
        [SerializeField] Sprite icon = null;
        [Tooltip("The prefab that should be spawned when this item is dropped.")]
        [SerializeField] Pickup pickup = null;
        [Tooltip("If true, multiple items of this type can be stacked in the same inventory slot.")]
        [SerializeField] bool stackable = false;

        [Tooltip("WWise Audio Pickup Clip Name.")]
        [SerializeField] string WWisePickupClip = "";

        [SerializeField] ItemCategory category;

        // STATE
        static Dictionary<string, InventoryItem> itemLookupCache;

        // PUBLIC

        public ItemCategory GetCategory() => category;

        /// <summary>
        /// Get the inventory item instance from its UUID.
        /// </summary>
        /// <param name="itemID">
        /// String UUID that persists between game instances.
        /// </param>
        /// <returns>
        /// Inventory item instance corresponding to the ID.
        /// </returns>
        public static InventoryItem GetFromID(string itemID)
        {
            if (itemLookupCache == null)
            {
                itemLookupCache = new Dictionary<string, InventoryItem>();
                var itemList = Resources.LoadAll<InventoryItem>("");
                foreach (var item in itemList)
                {
                    if (itemLookupCache.ContainsKey(item.itemID))
                    {
                        Debug.LogError(string.Format("Looks like there's a duplicate GameDevTV.UI.InventorySystem ID for objects: {0} and {1}", itemLookupCache[item.itemID], item));
                        continue;
                    }

                    itemLookupCache[item.itemID] = item;
                }
            }

            if (itemID == null || !itemLookupCache.ContainsKey(itemID)) return null;
            return itemLookupCache[itemID];
        }

        /// <summary>
        /// Spawn the pickup gameobject into the world.
        /// </summary>
        /// <param name="position">Where to spawn the pickup.</param>
        /// <param name="number">How many instances of the item does the pickup represent.</param>
        /// <returns>Reference to the pickup object spawned.</returns>
        public Pickup SpawnPickup(Vector3 position, int number)
        {
            var pickup = Instantiate(this.pickup);
            pickup.transform.position = position;
            pickup.Setup(this, number);
            return pickup;
        }

        public Sprite GetIcon()
        {
            return icon;
        }

        public string GetItemID()
        {
            return itemID;
        }

        public bool IsStackable()
        {
            return stackable;
        }
        
        public string GetDisplayName()
        {
            return displayName;
        }

        public string GetDescription()
        {
            return description;
        }

        public string GetWWiseString()
        {
            return WWisePickupClip;
        }

        // PRIVATE
        
        void ISerializationCallbackReceiver.OnBeforeSerialize()
        {
            // Generate and save a new UUID if this is blank.
            if (string.IsNullOrWhiteSpace(itemID))
            {
                itemID = System.Guid.NewGuid().ToString();
            }
        }

        void ISerializationCallbackReceiver.OnAfterDeserialize()
        {
            // Require by the ISerializationCallbackReceiver but we don't need
            // to do anything with it.
        }
    }
}
Pickup (not sure if usefull or not)

Code: Select all

using UnityEngine;

namespace GameDevTV.Inventories
{
    /// <summary>
    /// To be placed at the root of a Pickup prefab. Contains the data about the
    /// pickup such as the type of item and the number.
    /// </summary>
    public class Pickup : MonoBehaviour
    {
        // STATE
        InventoryItem item;
        int number = 1;

        // CACHED REFERENCE
        Inventory inventory;

        // LIFECYCLE METHODS
        GameObject player;

        private void Start()
        {
            RefreshReferences();
        }

        void RefreshReferences()
        {
            if (player == null)
            {
                player = GameObject.FindWithTag("MainUniqueController");
            }
            if (inventory == null)
            {
                inventory = player.GetComponent<Inventory>();
            }
            if (item == null)
            {
                //TODO : find a way to get the number from the 
                Setup(this.item, 1);
            }
        }

        // PUBLIC

        /// <summary>
        /// Set the vital data after creating the prefab.
        /// </summary>
        /// <param name="item">The type of item this prefab represents.</param>
        /// <param name="number">The number of items represented.</param>
        public void Setup(InventoryItem item, int number)
        {
            this.item = item;
            if (!item.IsStackable())
            {
                number = 1;
            }
            this.number = number;
        }

        public InventoryItem GetItem()
        {
            RefreshReferences();
            return item;
        }

        public int GetNumber()
        {
            RefreshReferences();
            return number;
        }

        public void PickupItem()
        {
            bool foundSlot = inventory.AddToFirstEmptySlot(item, number);
            if (foundSlot)
            {
                WWiseAudioPlayer.WS.PlaySFXPickup(item.GetWWiseString());
                Destroy(gameObject);
                if (transform.parent.childCount > 1)
                {
                    GameObject particle = transform.parent.GetChild(1).gameObject;
                    Destroy(particle);
                }
            }
        }

        public bool CanBePickedUp()
        {
            RefreshReferences();
            return inventory.HasSpaceFor(item);
        }
    }
}
Let me know if you think you need anything else :)

Re: Save System Question ;)

Posted: Sat May 14, 2022 1:22 pm
by Tony Li
Hi,

The issue with saving the InventoryItem reference is because InventoryItem is a ScriptableObject. In your Data class, Unity can't serialize a stable reference to a ScriptableObject asset, or any object derived from UnityEngine.Object for that matter, including MonoBehaviour.

Also, unrelated to the issue but still a point of optimization: Your Data class doesn't need to save a reference to PickupSpawner. It wouldn't be able to save the reference anyway since it's a MonoBehaviour.

Anyway, getting back to InventoryItems, you can save their itemID strings:

Code: Select all

[Serializable]
public class Data
{
    public Vector3 position;
    public string itemID;
    public int number;
    public bool boolPickupPickedUp;
    public bool boolInstanciated;
}
Notice that each of the types above is a basic type.

Then you can record it like this:

Code: Select all

Data data = new Data();
PickupSpawner pickup = GetComponent<PickupSpawner>(); // Call GetComponent once and cache value.
data.boolPickupPickedUp = pickup.boolPickupPickedUp;
data.itemID = (pickup.item != null) ? pickup.item.itemID : null;
data.number = pickup.number;
data.boolInstanciated = pickup.boolPickupInstanciated;
return SaveSystem.Serialize(data);
And you can restore (apply) it like this:

Code: Select all

if (string.IsNullOrEmpty(s)) return; // No data to apply.
Data data = SaveSystem.Deserialize<Data>(s);
if (data == null) return; // Serialized string isn't valid.

PickupSpawner pickup = GetComponent<PickupSpawner>(); // Call GetComponent once and cache value.

if (data.boolInstanciated)
{
    pickup.item = GetItemFromID(data.itemID);
    pickup.number = data.number;
}
if (data.boolPickupPickedUp)
{
    pickup.boolPickupPickedUp = true;
    if (pickup.boolPickupSpawned == false)
    {
        pickup.SpawnPickup();
    }
    pickup.DestroyPickup();
}
else if (pickup.boolPickupSpawned == false)
{
    pickup.SpawnPickup();
}
This assumes you have a method GetItemFromID(string itemID) that returns the item that has the specified ID.

Re: Save System Question ;)

Posted: Sat May 14, 2022 4:45 pm
by HawkX
wow thank you so much for all of that! on a Saturday no less!! :)

Yes there is a "GetItemFromID(string itemID)"

so... thats an nice improvement to my own "saving system" i made... should i put my own system on the prefab that gets instantiated? or if i just add that part of code to the "SpawnedObjectManager"?

Re: Save System Question ;)

Posted: Sat May 14, 2022 4:59 pm
by Tony Li
Put it on your saver script.