Dropped Pickups Not Staying In Place After Level Transition

Hi again,

This is becoming a regular occurrence. :stuck_out_tongue:

I’ve completed this section of the tutorial last night and began testing to ensure there’s no issues, and I came across one that I can’t quite figure out. When I drop an item in Scene A, and travel through the portal to Scene B, upon returning to Scene A the pickup is missing and I have four errors:

Can't remove Pickup (Script) because ClickablePickup (Script) depends on it.

I have a feeling it’s come about from implementing the changes discussed here: Pickup Spawner bug - Unity Courses / Ask - GameDev.tv or here: Another bug with the portals’ saving parts - Unity Courses / Talk - GameDev.tv

I’ll post my PickupSpawner.cs below:

using UnityEngine;
using RPG.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] InventoryItem item = null;
        [SerializeField] int number = 1;

        // LIFECYCLE METHODS
        private void Awake()
        {
            // Spawn in Awake so can be destroyed by save system after.
            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 hasBeenDestroyedByRestoreState = false;
        
        public bool isCollected() 
        {
            return GetPickup() == null || hasBeenDestroyedByRestoreState;
        }

        //PRIVATE

        private void SpawnPickup()
        {
            var spawnedPickup = item.SpawnPickup(transform.position, number);
            spawnedPickup.transform.SetParent(transform);
        }

        private void DestroyPickup()
        {
            var pickup = GetPickup();
            if (pickup)
            {
                pickup.transform.SetParent(null);
                Destroy(pickup);
                hasBeenDestroyedByRestoreState = true;
            }
        }

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

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

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

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

Thanks!

I forgot to mention: Drops dropped by enemies work, and remain in place by the dead body when switching scenes.

So, I decided to restore my PickupSpawner.cs script back to the default and run some tests:

using UnityEngine;
using RPG.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] InventoryItem item = null;
        [SerializeField] int number = 1;

        // LIFECYCLE METHODS
        private void Awake()
        {
            // Spawn in Awake so can be destroyed by save system after.
            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

        private void SpawnPickup()
        {
            var spawnedPickup = item.SpawnPickup(transform.position, number);
            spawnedPickup.transform.SetParent(transform);
        }

        private void DestroyPickup()
        {
            if (GetPickup())
            {
                Destroy(GetPickup().gameObject);
            }
        }

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

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

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

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

This removes any errors that occur due to the changes in DestroyPickup(), but the drops still aren’t saved when leaving one scene and returning.

Pickups that were previously collected from Scene A are no longer being respawned, which is good. Enemy drops are still working as intended.

So the drops that still aren’t working are the ones from the Player’s ItemDropper?

The first error (the Pickup can’t be removed) was because you were destroying the Pickup script not the GameObject. This was fixed when you switched to the old PickupSpawner.

The items you drop are handled by ItemDropper.cs (and more locally, RandomDropper.cs). Assuming you haven’t made any changes to ItemDropper.cs, paste in your RandomDropper.cs and we’ll take a look.

Hi!
Yup, just the player’s item drops. Enemy drops are saving and restoring correctly.

The first error came about from the changes detailed here: Another bug with the portals’ saving parts - Unity Courses / Talk - GameDev.tv
I have since undone those changes so that I don’t get that error.

I’m pretty sure ItemDropper.cs has no changes, but to be sure I’ll post them both below:

ItemDropper.cs

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using RPG.Saving;

namespace GameDevTV.Inventories
{
    /// <summary>
    /// To be placed on anything that wishes to drop pickups into the world.
    /// Tracks the drops for saving and restoring.
    /// </summary>
    public class ItemDropper : MonoBehaviour, ISaveable
    {
        // STATE
        private List<Pickup> droppedItems = new List<Pickup>();
        private List<DropRecord> otherSceneDroppedItems = new List<DropRecord>();

        // PUBLIC

        /// <summary>
        /// Create a pickup at the current position.
        /// </summary>
        /// <param name="item">The item type for the pickup.</param>
        /// <param name="number">
        /// The number of items contained in the pickup. Only used if the item
        /// is stackable.
        /// </param>
        public void DropItem(InventoryItem item, int number)
        {
            SpawnPickup(item, GetDropLocation(), number);
        }

        /// <summary>
        /// Create a pickup at the current position.
        /// </summary>
        /// <param name="item">The item type for the pickup.</param>
        public void DropItem(InventoryItem item)
        {
            SpawnPickup(item, GetDropLocation(), 1);
        }

        // PROTECTED

        /// <summary>
        /// Override to set a custom method for locating a drop.
        /// </summary>
        /// <returns>The location the drop should be spawned.</returns>
        protected virtual Vector3 GetDropLocation()
        {
            return transform.position;
        }

        // PRIVATE

        public void SpawnPickup(InventoryItem item, Vector3 spawnLocation, int number)
        {
            var pickup = item.SpawnPickup(spawnLocation, number);
            droppedItems.Add(pickup);
        }

        [System.Serializable]
        private struct DropRecord
        {
            public string itemID;
            public SerializableVector3 position;
            public int number;
            public int sceneBuildIndex;
        }

        object ISaveable.CaptureState()
        {
            RemoveDestroyedDrops();
            var droppedItemsList = new List<DropRecord>();
            int buildIndex = SceneManager.GetActiveScene().buildIndex;
            foreach (Pickup pickup in droppedItems)
            {
                var droppedItem = new DropRecord();

                droppedItem.itemID = pickup.GetItem().GetItemID();
                droppedItem.position = new SerializableVector3(pickup.transform.position);
                droppedItem.number = pickup.GetNumber();
                droppedItem.sceneBuildIndex = buildIndex;
                droppedItemsList.Add(droppedItem);
            }
            droppedItemsList.AddRange(otherSceneDroppedItems);
            return droppedItemsList;
        }

        void ISaveable.RestoreState(object state)
        {
            var droppedItemsList = (List<DropRecord>)state;
            int buildIndex = SceneManager.GetActiveScene().buildIndex;
            otherSceneDroppedItems.Clear();
            foreach (var item in droppedItemsList)
            {
                if (item.sceneBuildIndex != buildIndex)
                {
                    otherSceneDroppedItems.Add(item);
                    continue;
                }
                var pickupItem = InventoryItem.GetFromID(item.itemID);
                Vector3 position = item.position.ToVector();
                int number = item.number;
                SpawnPickup(pickupItem, position, number);
            }
        }

        /// <summary>
        /// Remove any drops in the world that have subsequently been picked up.
        /// </summary>
        private void RemoveDestroyedDrops()
        {
            var newList = new List<Pickup>();
            foreach (var item in droppedItems)
            {
                if (item != null)
                {
                    newList.Add(item);
                }
            }
            droppedItems = newList;
        }
    }
}

RandomDropper.cs

using UnityEngine;
using UnityEngine.AI;
using GameDevTV.Inventories;
using RPG.Stats;

namespace RPG.Inventories
{
    public class RandomDropper : ItemDropper
    {
        // CONFIG DATA
        [Tooltip("How far can the pickups be scattered from the dropper.")]
        [SerializeField] float scatterDistance = 1;

        [SerializeField] DropLibrary dropLibrary;

        // CONSTANTS
        const int ATTEMPTS = 30;

        public void RandomDrop()
        {
            var baseStats = GetComponent<BaseStats>();
            var drops = dropLibrary.GetRandomDrops(baseStats.GetLevel());

            foreach (var drop in drops)
            {
                DropItem(drop.item, drop.number);
            }
        }

        protected override Vector3 GetDropLocation()
        {
            for (int i = 0; i < ATTEMPTS; i++)
            {
                Vector3 randomPoint = transform.position + Random.insideUnitSphere * scatterDistance;
                NavMeshHit hit;
                if (NavMesh.SamplePosition(randomPoint, out hit, 0.1f, NavMesh.AllAreas))
                {
                    return hit.position;
                }
            }
            return transform.position;
        }
    }
}

I have noticed another issue. My sword pickup is planted into the ground blade down, which works for where I want to spawn it. However, whenever I drop the sword, you can’t actually see the drop. Running around in the vicinity will pick it up though. Looking for it in the inspector I can see that it’s spawning underground. It’s almost like I need to offset it by Y+1.35 to get it to place correctly. What’s the best way to tackle this one?

Thanks again!
Mark.

Check to make sure that the player in both scenes has a RandomDropper on it. If the 2nd scene doesn’t have one, then the information will get lost when you transition to the 2nd scene, and won’t be there when you go back to the first scene.

I thought for sure that’s what I’d forgotten to do, but sadly the player in the second scene does indeed have a RandomDropper. I ran a quick test to see whether items dropped in the second scene would persist, but sadly they don’t.

It’s almost as if the game clears the player’s dropped items when you leave the scene, but doesn’t save that information. As far as the game is concerned, those items are gone. :laughing:

Which makes the fact that enemy drops work correctly and persist throughout both scenes even weirder.

It does, since they’re using the same code… In other words, you really should be seeing either all of the characters save/restore or none of them…

One last check before we escalate, make sure that the Players have ONLY one RandomDropper.

Oh man, I can’t believe I didn’t see it earlier. :joy:

I’m guessing that I originally had one on the player, but then when everyone else needed one I updated the base Character prefab that they all share as well, so the player ended up with two!

Thanks again,
Mark.

I know it took a few questions to get there, but that’s sometimes how the debugging process works, especially when the problem is in the scene, not the code.

So what happens is that the first RandomItemDropper is the one that drops the items and keeps track of them. It dutifully passes those items to CaptureState… Then the second RandomItemDropper gets called and returns no objects save, which completely overwrites the first RandomItemDropper’s capture in the Dictionary. When we restore, both RandomItemDroppers get their captures from the RestoreState and of course, they’re empty because the 2nd one overwrote the 1st one. Glad we’ve got that one sorted!

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms