A better Pickup System for the Third Person Variant of the RPG Course

OK so for the time being, here’s a quick rundown of exactly what was going on (because honestly, I’m a little confused as of what was happening)

(This assumes that you’ve done the banking tutorial, and the third person RPG Conversion course by Brian, which… have the pre-requisite of the entire RPG Series, and the Third Person Combat and Traversal Course. So, you got 7 pre-requisites for this to make sense)

  1. Put the old pickup solution aside, and implement a new state machine:
                // stateMachine.SwitchState(new PlayerPickupState(stateMachine, stateMachine.PickupFinder.CurrentTarget));

                // Our current Pickup State above is being replaced by an advanced 'PlayerInventoryCollectionState.cs' below.
                // This is essentially an alternative for picking stuff up in the game world, using a one-way transfer, pickup
                // inventory-based approach that focuses on giving the player extra freedom, and speed, on what exactly they want to
                // pickup in the game world (it works by combining the 'IInventorySource.cs' Interface, specifically created for this
                // system, with 'PickupFinder.cs'):

                stateMachine.SwitchState(new PlayerInventoryCollectionState(stateMachine));

and of course, create a new state machine for that:

using GameDevTV.Inventories;

namespace RPG.States.Player {

public class PlayerInventoryCollectionState : PlayerBaseState
{
    public PlayerInventoryCollectionState(PlayerStateMachine stateMachine) : base(stateMachine) {}

    public static event System.Action<IInventorySource, System.Action> OnCollectionStateOpened;

    private void OnCollectionFinished() 
    {
        stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
    }

    public override void Enter()
    {
        OnCollectionStateOpened?.Invoke(stateMachine.PickupFinder, OnCollectionFinished);
    }

    public override void Tick(float deltaTime)
    {
        Move(deltaTime);
    }

    public override void Exit() {}
}

}
  1. Convert your Inventory to an IInventorySource Interface, and implement all the necessary functions (if you got any complains about missing stuff or what not, just throw it into your interface. The interface below is what worked for my case):
using System;
using System.Collections;
using UnityEngine;
using System.Collections.Generic;

namespace GameDevTV.Inventories 
{
    // This interface allows the player to classify between his inventory, and other inventories
    // (mainly the Pickup Inventory, for our replacement to our classic pickup approach, and the bank inventory)

    public interface IInventorySource
    {
        /// <summary>
        /// Solution to allow this interface access to part of the MonoBehaviour
        /// </summary>
        /// <returns></returns>
        GameObject GetGameObject();

        /// <summary>
        /// Broadcasts when the items in the slots are added/removed
        /// </summary>
        event Action inventoryUpdated;

        /// <summary>
        /// What's the display name of this Inventory?
        /// </summary>
        /// <returns></returns>
        string GetDisplayName();

        /// <summary>
        /// Could this item fit anywhere in the inventory?
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public bool HasSpaceFor(InventoryItem item);

        /// <summary>
        /// Do you have space for IEnumerable Inventory Items? Specifically made for checking sword and shield positionings
        /// </summary>
        /// <param name="items"></param>
        /// <returns></returns>
        public bool HasSpaceFor(IEnumerable<InventoryItem> items);

        /// <summary>
        /// How many slots are in the inventory?
        /// </summary>
        /// <returns></returns>
        int GetSize();

        /// <summary>
        /// Attempt to add the items to the first available slot
        /// </summary>
        /// <param name="item">The item to add</param>
        /// <param name="number">The number to add</param>
        /// <returns></returns>
        bool AddToFirstEmptySlot(InventoryItem item, int number, bool refresh = true);

        /// <summary>
        /// Is there an instance of the item in the inventory?
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        bool HasItem(InventoryItem item);

        /// <summary>
        /// Return the item type in the given slot
        /// </summary>
        /// <param name="slot"></param>
        /// <returns></returns>
        InventoryItem GetItemInSlot(int slot);

        /// <summary>
        /// Get the number of items in the given slot
        /// </summary>
        /// <param name="slot"></param>
        /// <returns></returns>
        int GetNumberInSlot(int slot);

        /// <summary>
        /// Remove a number of items from the given slot. Will never remove more than there are
        /// </summary>
        /// <param name="slot"></param>
        /// <param name="number"></param>
        void RemoveFromSlot(int slot, int number, bool refresh = true);

        /// <summary>
        /// Will add an item to the given slot if possible. If there is already
        /// a stack of this type, it will add to the existing stack. Otherwise,
        /// it will be added to the first empty slot
        /// </summary>
        /// <param name="slot"></param>
        /// <param name="item"></param>
        /// <param name="number"></param>
        /// <returns></returns>
        public bool AddItemToSlot(int slot, InventoryItem item, int number, bool refresh = true);

        /// <summary>
        /// Transfer all items in the Inventory from one inventory to another
        /// </summary>
        /// <param name="otherInventory"></param>
        public void TransferAllInventory(Inventory otherInventory); // This is only here so 'InventoryUI' would be quiet

        /// <summary>
        /// Remove InventoryItem item, in 'number' quantity, from the Inventory
        /// </summary>
        /// <param name="item"></param>
        /// <param name="number"></param>
        /// <param name="refresh"></param>
        public void RemoveItem(InventoryItem item, int number, bool refresh = true);
    }
}
  1. in ‘InventoryUI.cs’, change your ‘Inventory’ type to ‘IInventorySource’ as follows:
        public IInventorySource SelectedInventory => selectedInventory;

        // CACHE
        IInventorySource selectedInventory;

  1. Fix all the Syntax errors that this conversion will create in ‘InventorySlotUI.cs’ (use the Interface functions above)

I’ll leave that up to the reader, but anything like ‘TryGetComponent’ and other stuff, try use the ‘GetGameObject()’ from the Interface first. Most of these bugs will be gone by that one. For example:

inventory.CompareTag("Player");

won’t work anymore, but this will work:

inventory.GetGameObject().CompareTag("Player")
  1. Create a ‘PickupWindow.cs’ script, and attach it to your ‘PickupInventory’, which you will setup in your hierarchy:
using System;
using GameDevTV.Inventories;
using GameDevTV.UI.Inventories;
using RPG.States.Player;
using RPG.UI;
using RPG.UI.Inventories;
using UnityEngine;

public class PickupWindow : WindowController
{
    [SerializeField] private InventoryWindow inventoryWindow;

    private System.Action callback;

    protected override void Subscribe()
    {
        PlayerInventoryCollectionState.OnCollectionStateOpened += TogglePickupWindow;
        gameObject.SetActive(false);
    }

    protected override void Unsubscribe()
    {
        PlayerInventoryCollectionState.OnCollectionStateOpened -= TogglePickupWindow;
    }

    private void TogglePickupWindow(IInventorySource source, Action finishedCallback) 
    {
        callback = finishedCallback;
        inventoryWindow.gameObject.SetActive(true);
        GetComponentInChildren<InventoryUI>().Setup(source.GetGameObject());
        gameObject.SetActive(true);
    }

    protected override void OnDisable()
    {
        base.OnDisable();
        inventoryWindow.gameObject.SetActive(false);
        callback?.Invoke();
    }
}

  1. Implement your IInventorySource.cs script in your ‘PickupFinder.cs’ script:
using System;
using System.Collections.Generic;
using System.Linq;
using GameDevTV.Inventories;
using RPG.Core;
using UnityEngine;

namespace RPG.Inventories 
{
    public class PickupFinder : RangeFinder<PickupTarget>, IInventorySource
    {
        protected override void AddTarget(PickupTarget target)
        {
            base.AddTarget(target);
            target.OnPickedUp += RemoveTarget;
            Debug.Log($"Pickup Finder: Adding {target.name}");
        }

        protected override void RemoveTarget(PickupTarget target)
        {
            base.RemoveTarget(target);
            target.OnPickedUp -= RemoveTarget;
            Debug.Log($"Pickup Finder: Removing {target.name}");
        }

        public PickupTarget GetNearestPickup()
        {
            CurrentTarget = Targets.OrderBy(t => Vector3.Distance(transform.position, t.transform.position)).FirstOrDefault();
            return CurrentTarget;
        }

        // ------------------------------------ IINVENTORYSOURCE INTERFACE IMPLEMENTATION ---------------------------------------------

        // Most of the Functions down here are for 'IInventorySource.cs' to work. We don't need them all,
        // but Interfaces demand everything to be used, so most will be empty
        // (it's either this, or a performance-intense solution of programming a second inventory, which
        // can be a bit of a messy solution)

        // (This is all invoked from 'PlayerInventoryCollectionState.cs', the replacement for 'PlayerPickupState.cs')

        public event Action inventoryUpdated;

        public bool AddItemToSlot(int slot, InventoryItem item, int number, bool refresh = true)
        {
            return false; // No returns. Only takes
        }

        public bool AddToFirstEmptySlot(InventoryItem item, int number, bool refresh = true)
        {
            return false; // No returns. Only takes
        }

        public string GetDisplayName()
        {
            return "PICKUPS"; // Controls the Title of the 'Pickups' Window
        }

        public GameObject GetGameObject()
        {
            return gameObject;
        }

        public InventoryItem GetItemInSlot(int slot)
        {
            return Targets[slot].GetComponent<Pickup>().GetItem();
        }

        public int GetNumberInSlot(int slot)
        {
            return Targets[slot].GetComponent<Pickup>().GetNumber();
        }

        public int GetSize()
        {
            return Targets.Count; // How many total Pickups are around you...?
        }

        public bool HasItem(InventoryItem item)
        {
            foreach (PickupTarget pickupTarget in Targets) 
            {
                if (pickupTarget.GetComponent<Pickup>().GetItem() == item) return true; // if you got this far, it means you found the item
            }
            return false; // can't find it nearby? Then it's not there
        }

        public bool HasSpaceFor(InventoryItem item)
        {
            return false; // No returns. Only takes
        }

        public bool HasSpaceFor(IEnumerable<InventoryItem> items)
        {
            return false; // No returns. Only takes
        }

        public void RemoveFromSlot(int slot, int number, bool refresh = true)
        {
            int remainder = GetNumberInSlot(slot) - number;
            if (remainder > 0) 
            {
                Targets[slot].GetComponent<Pickup>().SetNumber(remainder);
            }
            else 
            {
                var item = Targets[slot];
                Targets.Remove(item);
                Destroy(item.gameObject);
            }
            if (refresh) {inventoryUpdated?.Invoke();}
        }

        public void RemoveItem(InventoryItem item, int number, bool refresh = true)
        {
            if (item == null) return; // No item, no pickup

            foreach (PickupTarget pickupTarget in Targets) 
            {
                var pickup = pickupTarget.GetComponent<Pickup>();
                if (pickup.GetItem() == item) // you found an item to pickup
                {
                    int currentNumber = pickup.GetNumber();
                    if (currentNumber > number) // consume the amount of item picked up by the player (below max limit)
                    {
                        pickup.SetNumber(currentNumber - number);
                        if (refresh) {inventoryUpdated?.Invoke();}
                        return;
                    }
                    else // max number of consumed items taken exceeded, so consume everything
                    {
                        number -= currentNumber;
                        Targets.Remove(pickupTarget);
                        Destroy(pickupTarget.gameObject);
                        if (refresh) {inventoryUpdated?.Invoke();}
                        if (number <= 0) return;
                    }
                }
            }
        }

        public void TransferAllInventory(Inventory otherInventory)
        {
            // For now, this is empty. It's just to get 'InventoryUI' to be quiet about whole inventory
            // transfers, that it made it in the 'IInventorySource' Interface (but we don't really need it for now)
        }

        // ------------------------------------ END OF IINVENTORYSOURCE INTERFACE IMPLEMENTATION --------------------------------------

    }

}

Again, this is what worked for me

  1. Test run, and enjoy

(Until Brian lets me know if we’re getting the slots to not vanish or not, this is what worked for me :slight_smile:)

and… @Brian_Trotter there’s another bug that pops up when you try to press on any player inventory slot (with an item in it) whilst the Pickup Inventory is open:

NullReferenceException: Object reference not set to an instance of an object
GameDevTV.UI.Inventories.InventorySlotUI.TransferToOtherInventory (GameDevTV.Inventories.Inventory otherInventory, GameDevTV.Inventories.InventoryItem item, System.Int32 number) (at Assets/GameDev.tv Assets/Scripts/UI/Inventories/InventorySlotUI.cs:273)
GameDevTV.UI.Inventories.InventorySlotUI.TryHandleRightClick () (at Assets/GameDev.tv Assets/Scripts/UI/Inventories/InventorySlotUI.cs:107)
UnityEngine.Events.InvokableCall.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.Events.UnityEvent.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.UI.Button.Press () (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Button.cs:70)
UnityEngine.UI.Button.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Button.cs:114)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IPointerClickHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:57)
UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:272)
UnityEngine.EventSystems.EventSystem:Update() (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:514)

Which leads to this function in ‘InventorySlotUI.cs’:

        // Transferring stuff from the Inventory to our Bank:
        private void TransferToOtherInventory(Inventory otherInventory, InventoryItem item, int number) {

                    if (otherInventory.HasSpaceFor(item)) {

                        otherInventory.AddToFirstEmptySlot(inventory.GetItemInSlot(index), number);
                        inventory.RemoveItem(item, number);
                        Setup(inventory, index); // RECHECK Second argument, if it's 'item' or 'index'

                    }

                }

Specifically, this line:

                    if (otherInventory.HasSpaceFor(item)) {

OH AND THERE IS ONE MAJOR BUG:

With the new system in place, the game build version (i.e: When you “Build and Run” the game, making a prototype outside the Unity Engine) can no longer be opened with the “I” button from the get go. You have to either open the bank or a pickup inventory (and the pickup inventory takes 2 clicks for it to work the first time for some reason, both in Unity and in the final build. It’s one of these unexplained bugs that exist) and then hit the I key for it to work, and then pray that it works (literally no guarantees it’ll work, and I have absolutely no idea why)… And this problem didn’t exist before we made these changes. What did we miss out on, and how do we fix it??

The “I” Inventory key problem doesn’t exist in the engine, it only exists when you build the game, and this wasn’t there before we introduced this system. How do we fix these problems?

(I did a little bit of prototyping and decided to reverse my Pickup Approach to the previous one, with the animation, but the problem still exists, so I’m guessing it’s a coding issue because of all of the code that was recently introduced above)

And it literally gets worse the more you save and quit, and return :sweat_smile: (please don’t get me wrong, I’m not throwing any blames here, but something is completely wrong here, and this is my priority to fix right now. This bug, and the one with the if statement mentioned above)


Edit: I deleted the library folder and re-opened my project, and the issues in the game build exist in the game engine too.

Unfortunately, the inventory issue shows me absolutely no errors (but I know something is holding it back, because typically the first call (through manual enabling and disabling in the hierarchy itself) gets no response, and the second call in the hierarchy gets the call. Something we introduced is holding it back), although the problem exists, and the Pickup issue becomes significant after I save and restore my game, giving me this error:

MissingReferenceException: The object of type 'InventoryWindow' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
PickupWindow.TogglePickupWindow (GameDevTV.Inventories.IInventorySource source, System.Action finishedCallback) (at Assets/GameDev.tv Assets/Scripts/UI/Inventories/PickupWindow.cs:29)
RPG.States.Player.PlayerInventoryCollectionState.Enter () (at Assets/Project Backup/Scripts/State Machines/Player/PlayerInventoryCollectionState.cs:18)
RPG.States.StateMachine.SwitchState (RPG.States.State newState) (at Assets/Project Backup/Scripts/State Machines/StateMachine.cs:13)
RPG.States.Player.PlayerFreeLookState.InputReader_HandlePickupEvent () (at Assets/Project Backup/Scripts/State Machines/Player/PlayerFreeLookState.cs:173)
RPG.InputReading.InputReader.OnPickup (UnityEngine.InputSystem.InputAction+CallbackContext context) (at Assets/Project Backup/Scripts/Input Controls/InputReader.cs:216)
UnityEngine.InputSystem.Utilities.DelegateHelpers.InvokeCallbacksSafe[TValue] (UnityEngine.InputSystem.Utilities.CallbackArray`1[System.Action`1[TValue]]& callbacks, TValue argument, System.String callbackName, System.Object context) (at Library/PackageCache/com.unity.inputsystem@1.5.1/InputSystem/Utilities/DelegateHelpers.cs:46)
UnityEngine.InputSystem.LowLevel.<>c__DisplayClass7_0:<set_onUpdate>b__0(NativeInputUpdateType, NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate(NativeInputUpdateType, IntPtr)

which then permanently freezes the player from movement, where he’s idle with no way out of it. It leads to this part of ‘PickupWindow.TogglePickupWindow’:

    private void TogglePickupWindow(IInventorySource source, Action finishedCallback)
    {
        callback = finishedCallback;
        inventoryWindow.gameObject.SetActive(true); // <-- syntax error line
        GetComponentInChildren<InventoryUI>().Setup(source.GetGameObject());
        gameObject.SetActive(true);
    }

and when picking stuff up consistently through ‘PickupFinder’, I’m getting this “harmless” error:

ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
System.Collections.Generic.List`1[T].get_Item (System.Int32 index) (at <88e4733ac7bc4ae1b496735e6b83bbd3>:0)
RPG.Inventories.PickupFinder.GetItemInSlot (System.Int32 slot) (at Assets/GameDev.tv Assets/Scripts/Inventories/PickupFinder.cs:65)
GameDevTV.UI.Inventories.InventorySlotUI.Setup (GameDevTV.Inventories.IInventorySource inventory, System.Int32 index) (at Assets/GameDev.tv Assets/Scripts/UI/Inventories/InventorySlotUI.cs:45)
GameDevTV.UI.Inventories.InventorySlotUI.TransferToOtherInventory (GameDevTV.Inventories.Inventory otherInventory, GameDevTV.Inventories.InventoryItem item, System.Int32 number) (at Assets/GameDev.tv Assets/Scripts/UI/Inventories/InventorySlotUI.cs:278)
GameDevTV.UI.Inventories.InventorySlotUI.TryHandleRightClick () (at Assets/GameDev.tv Assets/Scripts/UI/Inventories/InventorySlotUI.cs:98)
UnityEngine.Events.InvokableCall.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.Events.UnityEvent.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.UI.Button.Press () (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Button.cs:70)
UnityEngine.UI.Button.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Button.cs:114)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IPointerClickHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:57)
UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:272)
UnityEngine.EventSystems.EventSystem:Update() (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:514)

which leads to this part of my ‘IInventorySource’ Interface in my ‘PickupFinder.cs’ script:

        public InventoryItem GetItemInSlot(int slot)
        {
            return Targets[slot].GetComponent<Pickup>().GetItem();
        }

[SOLUTION] That pesky little bug in ‘PickupWindow.cs’:

    protected override void OnDisable()
    {
        base.OnDisable();
        // inventoryWindow.gameObject.SetActive(false); // <-- this is the line that was causing me all the trouble.
        callback?.Invoke();
    }

I’ll control the Inventory Window through the hot key instead, and my Pickup shuts the inventory down through the ‘X’ (Quit button) key, which has a reference to ‘ThirdPersonShowHideUI.CloseAllWindows()’, which does the closing, anyway :slight_smile:

No offence, but this brute force solution was a bad one :sweat_smile:

The next bugs to fix, are:

  1. [SOLVED - Clean up the “Setup” line in ‘InventorySlotUI.TransferToOtherInventory()’] Clean up the Pickup ‘ArgumentOutOfRangeException’ issue, when you’re taking stuff out of the pickup inventory (just to keep my compiler clean :slight_smile:)

  2. The NRE when we click on something in the player’s inventory but the Pickup Inventory is still open (where it thinks we should be transferring something, but it can’t, because… well… there’s no slots in the pickup inventory to accept it. This transfer attempt must be returned/stopped, otherwise it’ll produce some severe errors). This one has severe consequences on that slot later on, so it needs to be fixed

  3. [SOLVED] Try Save and Load and return to the game, and the Pickup System won’t work. Instead, it’ll give you this error:

MissingReferenceException: The object of type 'InventoryWindow' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
PickupWindow.TogglePickupWindow (GameDevTV.Inventories.IInventorySource source, System.Action finishedCallback) (at Assets/GameDev.tv Assets/Scripts/UI/Inventories/PickupWindow.cs:30)
RPG.States.Player.PlayerInventoryCollectionState.Enter () (at Assets/Project Backup/Scripts/State Machines/Player/PlayerInventoryCollectionState.cs:18)
RPG.States.StateMachine.SwitchState (RPG.States.State newState) (at Assets/Project Backup/Scripts/State Machines/StateMachine.cs:13)
RPG.States.Player.PlayerFreeLookState.InputReader_HandlePickupEvent () (at Assets/Project Backup/Scripts/State Machines/Player/PlayerFreeLookState.cs:173)
RPG.InputReading.InputReader.OnPickup (UnityEngine.InputSystem.InputAction+CallbackContext context) (at Assets/Project Backup/Scripts/Input Controls/InputReader.cs:219)
UnityEngine.InputSystem.Utilities.DelegateHelpers.InvokeCallbacksSafe[TValue] (UnityEngine.InputSystem.Utilities.CallbackArray`1[System.Action`1[TValue]]& callbacks, TValue argument, System.String callbackName, System.Object context) (at Library/PackageCache/com.unity.inputsystem@1.5.1/InputSystem/Utilities/DelegateHelpers.cs:46)
UnityEngine.InputSystem.LowLevel.<>c__DisplayClass7_0:<set_onUpdate>b__0(NativeInputUpdateType, NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate(NativeInputUpdateType, IntPtr)

and the same case goes with the ‘PickupWindow’ as well, which leads to the two following lines:

// in 'PickupWindow.cs'

    private void TogglePickupWindow(IInventorySource source, Action finishedCallback)
    {
        callback = finishedCallback;
        inventoryWindow.gameObject.SetActive(true); // <-- when we return to the game from a save and load, somehow this is destroyed, and we need to find a way to re-initialize it
        GetComponentInChildren<InventoryUI>().Setup(source.GetGameObject());
        gameObject.SetActive(true); // <-- same problem goes for this line
    }

After a little bit of investigation, the problem is because it never gets initialized through code when we return from the save. It only gets initialized when the game starts from the main scene, and that’s it (and when you switch scenes, that gets destroyed, hence the problem. I’m thinking of a ‘DontDestroyOnLoad’ solution, but I think I’ll wait for Brian’s input on this before doing anything reckless).

I gave this some thinking, and I can’t think of a proper solution for it (mainly because when the scene starts, both are inactive, and I have no way of being able to get an inactive gameObject in the game scene), or so I think (I’m surprised the inventory and other windows work just fine. I’m starting to think it’s something to do with the Pickups instead. I’m not sure)

We need a way to initialize all those windows one way or the other

  1. [SOLVED] Figure out why it needs the ‘P’ key to be pressed twice to activate the Pickups the first time

As mentioned before, the Pickup when the game just started typically takes 2 clicks to get it to work. To eliminate this bug, here’s the line in ‘PickupWindow.Subscribe()’ that had to be eliminated:

    protected override void Subscribe()
    {
        // Called in 'WindowController.Awake()'
        PlayerInventoryCollectionState.OnCollectionStateOpened += TogglePickupWindow;
        // gameObject.SetActive(false); // don't need this here. Something else somewhere does the deactivation, so we don't need it twice
    }

This is because… I don’t have all of that click to transfer stuff in the course repo or the third person project so it was missed completely. Remove the Setup() because this square will dissappear when the inventory is redrawn anyways.

I’m not sure, because it wasn’t an issue on my end… If you’ve got I set up as the Open Inventory action, it should be invoking within the PlayerFreeLookState.

Did you drag the InventoryWindow into the PickupWindow’s inspector???

Are you trying to keep blank spaces after you remove an item, because this is exactly why I said you can’t do that.

Whenever you remove an item from the PickupFinder’s inventory, it has to be redrawn because it’s data source is Pickups which will shrink as we go, meaning that if that sword was the last item in the list of items at position 20, and you pick up item 18, then the sword becomes item 19 because the data source contracts.

I’m not sure why that would be affected by Save and Load since… serialized references aren’t affected by the Saving System

Are you talking about Load and Save as in going through the menus, or using the S and L keys, because at this point, the S and L keys should have been completely removed from the game (assuming you’ve gone through the Shps and Abilities, which I’m pretty sure you have).

If you’re wanting to keep those S and L keys, the issue here is the same issue we have with a LOT of other things when using those keys, When you press L, the scene is not restored from a virgin state, and this causes many state issues.

Go into SavingWrapper.cs Update and find

if(Input.GetKeyDown(KeyCode.L))
{
    Load();
}

and change the instruction from Load() to

StartCoroutine(LoadLastScene());

[I FIXED EVERYTHING MENTIONED BELOW. AT THE VERY LAST COMMENT THERE’S JUST PLANS OF WHAT I’D LIKE TO DO NEXT SO I DON’T RETURN TO THIS SECTION OF THE PROJECT AGAIN]

yes, and when I return to the saving scene it still exists, but the complaint still shows up, and it bugs the entire game out when I try to pick stuff up after the player returns to the scene

I didn’t try anything new after your previous post. I’m just trying to clean up the bugs. I assigned the InventoryWindow in the PickupWindow’s inspector, and it’s still giving me this error. It’s been driving me to insanity for 2 days now :sweat_smile: (and the exact same problem happens with the InventoryWindow as well. They both are somehow destroyed by a scene transition, and I can’t figure out what in the world is going on)

Even I’m clueless of what’s going on, and the worse part is that I don’t even know where to start fixing this from. It’s really really bothering me (apologies for the rant)

I’m talking about the Buttons on the game UI. I eliminated the S and L keys along with the course :slight_smile:

as soon as the backup is done, I’ll do that :slight_smile: (in about 8-10 minutes) - Edit: Yup, that fixed it

I fixed that one. It was a line in ‘OnDisable’ of ‘PickupWindow’ that was causing this problem

AFTER TWO FULL DAYS OF TRYING TO SOLVE THAT THIRD PROBLEM, I REALIZED I HAD A SPELLING MISTAKE IN ‘WindowController.cs’:

        // Protected and Virtual, therefore this method can be overridden by any inheriting scripts, and tuned:
        protected virtual void OnDestroy()
        {
            Unsubscribe();
        }

I ACCIDENTALLY NAMED ‘OnDestroy’ AS ‘Destroy’!!!

Anyway, here’s the new 3 NREs that come after that one, when I quit my main game scene to a new one:

NullReferenceException: Object reference not set to an instance of an object
RPG.UI.Inventories.EquipmentWindow.Unsubscribe () (at Assets/Project Backup/Scripts/UI/EquipmentWindow.cs:20)
RPG.UI.WindowController.OnDestroy () (at Assets/Project Backup/Scripts/UI/WindowController.cs:29)

NullReferenceException: Object reference not set to an instance of an object
RPG.UI.Inventories.InventoryWindow.Unsubscribe () (at Assets/Project Backup/Scripts/UI/InventoryWindow.cs:20)
RPG.UI.WindowController.OnDestroy () (at Assets/Project Backup/Scripts/UI/WindowController.cs:29)
NullReferenceException: Object reference not set to an instance of an object
RPG.UI.PauseMenuUI.Unsubscribe () (at Assets/Project Backup/Scripts/UI/PauseMenuUI.cs:26)
RPG.UI.WindowController.OnDestroy () (at Assets/Project Backup/Scripts/UI/WindowController.cs:29)

Apparently the Equipment window, the Inventory Window and the Pause Menu don’t have something working for them that the Pickup Window has

And what they’re missing is… their respective ‘ToggleWindow’ functions :slight_smile: (they’re not missing anything, but does event subscription and unsubscription really consider inherited functions? Why is this bug even a thing to begin with?)

Edit: I solved all 3 of the problems above. Apparently all of them were unable to find the player gameObject component, because… well… he gets destroyed before they can get a hold of him when we switch scenes

So what I did, was find an InputReader reference made in the WindowController awake function, and in all 3 scripts, I have replaced these two lines of code (this is an example from my equipment window):

            // GameObject player = GameObject.FindWithTag("Player");
            // player.GetComponent<InputReader>().EquipmentEvent += ToggleWindow;

with this:

            InputReader.EquipmentEvent += ToggleWindow;

Obviously you need to rename the event according to where you’re going and coming from


@Brian_Trotter Now only remains one single problem, and this topic is over (until I work on a slider that will allow the players choice on how many items they want to transfer, based on what’s in the current inventory slot. Stackable item transfer is still a little too limited):

OK so mini update, addressing the problem right above this comment (the one where I quoted myself):

First off, I wrote this debug (I found this in ‘ItemDropper.cs’ and it’s been incredibly helpful recently) in ‘TransferToOtherInventory()’, to recall where this function gets called from:

            Debug.Log($"TransferToOtherInventory is called from {new System.Diagnostics.StackTrace().GetFrame(1).GetMethod().Name}");

(someone down the line might find this useful)

I went to ‘InventorySlotUI.cs’ and found this block of code, which is responsible for transferring stuff from the player’s inventory to other inventories:

var otherInventoryUI = FindObjectsOfType<InventoryUI>().FirstOrDefault(ui => ui.IsOtherInventory);  // returns other inventory, or null

            if (otherInventoryUI != null && otherInventoryUI.gameObject.activeSelf && otherInventoryUI.SelectedInventory != null) 
            {
                Inventory otherInventory = otherInventoryUI.SelectedInventory as Inventory;
                TransferToOtherInventory(otherInventory, item, 1);
                return;
            }

and I went ahead and added this block of code:

var otherInventoryUI = FindObjectsOfType<InventoryUI>().FirstOrDefault(ui => ui.IsOtherInventory);  // returns other inventory, or null

            if (otherInventoryUI != null && otherInventoryUI.gameObject.activeSelf && otherInventoryUI.SelectedInventory != null) 
            {
// ----------------------------------- NEW CODE --------------------------------------------------------
                if (otherInventoryUI.GetComponentInParent<PickupWindow>()) 
                {
                    Debug.Log($"Transferring to Pickup Inventory is prohibited");
                    return;
                }
// --------------------------------------------------------------------------------------------------------------
                Inventory otherInventory = otherInventoryUI.SelectedInventory as Inventory;
                TransferToOtherInventory(otherInventory, item, 1);
                return;
            }

Now, by notifications and logic, it does the trick and the Null Reference Exception is gone

HOWEVER, this slot becomes unclickable as well after this command (like it did before anyway). In other words, if you click the slot, WITH THE PICKUP INVENTORY BEING OPEN AS THE 'SelectedInventory’, you’ll get no response from that slot ever again, unless you drag and drop it somewhere else

I want to fix this. If it helps, here’s my entire ‘TryHandleRightClick()’ function, which is responsible for this whole ‘Click to Equip’ system:

        // Equipping a Weapon by clicking on it, and banking it if the bank is open:
        public void TryHandleRightClick() 
        {

            if (isHandlingRightClick) return;
            isHandlingRightClick = true;

            InventoryItem item = inventory.GetItemInSlot(index);
            int number = inventory.GetNumberInSlot(index);
            
            if (item == null || number < 1) {
                isHandlingRightClick = false;
                return; // inventory/bank slot is empty, so do nothing
            }

            // If you're pressing on an item inside the Bank's Inventory Slot UIs, send it to the players' inventory:
            if (!inventory.GetGameObject().CompareTag("Player"))
            {
                TransferToOtherInventory(Inventory.GetPlayerInventory(), item, 1);
                return;
            }

            var otherInventoryUI = FindObjectsOfType<InventoryUI>().FirstOrDefault(ui => ui.IsOtherInventory);  // returns other inventory, or null

            if (otherInventoryUI != null && otherInventoryUI.gameObject.activeSelf && otherInventoryUI.SelectedInventory != null) 
            {
                if (otherInventoryUI.GetComponentInParent<PickupWindow>()) 
                {
                    Debug.Log($"Transferring to Pickup Inventory is prohibited");
                    return;
                }

                Inventory otherInventory = otherInventoryUI.SelectedInventory as Inventory;
                TransferToOtherInventory(otherInventory, item, 1);
                return;
            }

            if (item is EquipableItem equipableItem)
            {

                if (!equipableItem.CanEquip(equipableItem.GetAllowedEquipLocation(), equipment)) return;    // The line solely responsible for checking for Predicate conditions, prior to being able to wield a weapon (if you're not high enough of a level, you can't wield that)

                // Test: Check 'Equipment.MaxAcceptable()' to confirm if a weapon is 2-handed or not:
                // if (equipment.MaxAcceptable(equipableItem.GetAllowedEquipLocation(), equipableItem) == 0) return;

                if (equipableItem is WeaponConfig weapon)
                {
                    TryEquipWeapon(weapon);
                    return;
                }

                if (equipableItem is ShieldConfig shield)
                {
                    TryEquipShield(shield);
                    return;
                }

                /* // TEST (Delete if failed): Equipping dual hand weapons into the shield slot:
                if (equipableItem is WeaponConfig dualHandWeapon) 
                {
                    TryEquipDualHandWeapon(dualHandWeapon);
                    return;
                } */

                // if equipableItem is WeaponConfig dualHandWeapon, then TryEquipDualHandWeapon here

                // Equipment equipment = Inventory.GetPlayerInventory().GetComponent<Equipment>();
                EquipableItem equippedItem = equipment.GetItemInSlot(equipableItem.GetAllowedEquipLocation());

                equipment.RemoveItem(equipableItem.GetAllowedEquipLocation());
                equipment.AddItem(equipableItem.GetAllowedEquipLocation(), equipableItem);
                RemoveItems(1);

                if (equippedItem != null)
                {
                    AddItems(equippedItem, 1);
                }

            }

            else if (item is ActionItem actionItem) {

                ActionStore actionStore = inventory.GetGameObject().GetComponent<ActionStore>();
                int slot = actionStore.GetFirstEmptySlot();

                if (slot > -1) {

                    actionStore.AddAction(actionItem, slot, GetNumber());
                    RemoveItems(GetNumber());

                }

            }

            else if (item is AmmunitionItem ammunitionItem && inventory.GetGameObject().TryGetComponent(out Quiver quiver))
            {

                // If there is a "Quiver" Equipment slot, and what we clicked on is a Rangers' arrow,
                // then we shall transfer the arrow to the dedicated slot

                Debug.Log($"{ammunitionItem} is ammo, transferring to {quiver}");
                TransferToQuiver(ammunitionItem, quiver, number);

            }

        }

and after that, this system is clear for now :slight_smile:

Edit: I fixed it:

                if (otherInventoryUI.GetComponentInParent<PickupWindow>()) 
                {
                    Debug.Log($"Transfer to Pickup Inventory is not permitted");
                    isHandlingRightClick = false; // if you forget this, you're going to get a severe bug trying to access this function again
                    return;
                }

And that fixed the last of everything this system introduced :slight_smile:

as for the bank mess I had, I placed a ‘HidingPanelPlaceHolder’ Script on the Hiding panel, and referenced it so I can access the bank properly again in ‘ThirdPersonShowHideUI.cs’ (my copy of ‘ShowHideUI.cs’, which I initially used for banking)

Alright now that everything is fixed and this section is stable, here’s what I want to do next so I don’t return to this topic again later:

  1. Fix the optimization issues. Bank transfers over long periods of time are insanely slow, even with our previous attempts in the Inventory itself, and I suspect it can be fixed with an if statement that asks the ‘Setup’ function to ignore null or empty slots (or in ‘InventoryUI.Redraw()’, it’s gotta be one of these two)

After a little bit of exploration, it is indeed in the ‘Redraw()’ function of ‘InventoryUI.cs’, specifically this function:

private void Redraw() // ORIGINAL REDRAW FUNCTION
        {
            // Re-creating the inventory from scratch, over each 'InventoryItemPrefab',
            // literally that's it!

            foreach (Transform child in transform)
            {
                Destroy(child.gameObject);
            }

            for (int i = 0; i < selectedInventory.GetSize(); i++)
            {
                
                var itemUI = Instantiate(InventoryItemPrefab, transform);
                itemUI.Setup(selectedInventory, i);
                Debug.Log($"Refresh done");
            }
        }

What I want to do, is destroy only the non-null slots, and also only refresh the non-null slots, because if that doesn’t happen, performance will suffer on the low-end machines (this is, by far, the one and only performance problem that I have and have been desperately trying to eliminate for a while now)

I tried doing this in ‘Redraw’, right before instantiating the slots back in:

                if (selectedInventory.GetItemInSlot(i) == null) continue; // Skip empty slots

and whilst it dramatically improved performance on its own, but now the slots don’t exist unless occupied (now I see why RuneScape had a plain background color for their inventories), JUST LIKE THE PICKUP INVENTORY. It makes telling how many remaining slots almost impossible, and, unlike the Pickup Inventory, this makes a huge difference to be honest, because the player can’t easily tell how many slots he has left now… So, I’ll need a different approach that both keeps the original slots setup (like they currently do, respectively), and also skips working with the empty slots later on when the inventory is being worked with

This might also be helpful, from ‘InventorySlotUI.cs’:

        public void OnPointerClick(PointerEventData eventData) {

                if (LastUIClicked != this) {

                    LastUIClicked = this;
                    Invoke(nameof(TimesUp), .001f); // a very small timer means spam clicks are impossible, eliminating potential bugs coming out of that
                    Debug.Log($"{index} was clicked once");

                }

                else {
                    HandleDoubleClick();
                }
            }

It always tells me which slot was clicked, so I figured we can probably use it to redraw that particular slot in some way

  1. Create a slider so we can choose how many items we want to transfer between inventories (only if they are stackable)

  2. Down the line (not anytime soon for now), if I decide abilities aren’t my thing for sure, I will use the slots as a ‘Construction Memory’ system, where it remembers your most used building parts for when you’re doing construction

I seem to recall we did this already some time last year… just can’t find the thread.

The way the system exists by default, all the InventorySlotUI are destroyed every time there is a change in the underlying data… For a single transaction, with moderate sized inventories (say under 30), this is pretty much not noticed, but for a bank, especially one with lots of slots, that’s a lot of garbage to collect.

For that, it’s better to implement a Factory solution, that is an ObjectPool.

That, however, is a broader topic that is NOT specifically tied to the ThirdPersonRPG course, so it’s going to get it’s own topic.

We did it back then only for ‘TransferAllToInventory’, essentially we only optimized the transfer of full inventories to other inventories (I temporarily disabled that button back then, I’ll re-enable it soon, once the entire storage UI chapter is done), but we never optimized the individual slot transfers, which honestly matters a lot more :slight_smile:

anytime soon? This is about to get a whole lot more interesting, xD - if so, I’ll go watch the Object Pooling lecture in the designer pattern course. I won’t start the course anytime soon (I don’t want to mix everything up), but this might be helpful for me to get an idea of what we’re about to get into

I went through this video: https://www.youtube.com/watch?v=Z1CDJASi4SQ

and let’s just say… I feel dumb :sweat_smile: (I wasn’t in Software Engineering in uni :stuck_out_tongue:)

but from my limited understanding, the guy made a factory for the weapon, a factory for something else, and then those two factories had a parent factory I think

and I have a funky feeling that because of how I developed my bank, I’m about to get into a ton of errors… At this point in time, I’m not surprised. I got whacked enough off this project that it doesn’t hurt me anymore lol

Actually, it’s not as hard as it sounds, and since the InventoryUI is already managing the life cycle (because it’s destroying the InventorySlotUI) we can put the whole pool in the InventoryUI class.

        private IObjectPool<InventorySlotUI> slotPool;
        
        
        // LIFECYCLE METHODS

        private void Awake() 
        {
            if (isPlayerInventory)
            {
                selectedInventory = Inventory.GetPlayerInventory();
                selectedInventory.inventoryUpdated += Redraw; 
            }
            slotPool = new ObjectPool<InventorySlotUI>(
                CreateSlot,
                OnGetSlot,
                OnReleaseSlot,
                OnDestroySlot
            );
        }
        //Object Pooling
        private InventorySlotUI CreateSlot()
        {
            return Instantiate(InventoryItemPrefab, transform);
        }

        private void OnGetSlot(InventorySlotUI slot)
        {
            slot.gameObject.SetActive(true);
        }
        
        private void OnReleaseSlot(InventorySlotUI slot)
        {
            slot.gameObject.SetActive(false);
        }
        
        private void OnDestroySlot(InventorySlotUI slot)
        {
            Destroy(slot.gameObject);
        }

What in the world is going on here… :sweat_smile: - do I delete the ‘Redraw’ method, or is this just an addon?

Oops, forgot something really important or things will get way out of order…

        private void OnGetSlot(InventorySlotUI slot)
        {
            slot.transform.parent = transform;
            slot.gameObject.SetActive(true);
        }
        
        private void OnReleaseSlot(InventorySlotUI slot)
        {
            slot.gameObject.SetActive(false);
            slot.transform.parent = null;
        }

Just an addon…
But I realize I also need the Redraw function which I should have pasted in.

        private void Redraw()
        {
            if (selectedInventory==null) return;
            foreach (Transform child in transform)
            {
                slotPool.Release(child.GetComponent<InventorySlotUI>()); //Edit
            }

            for (int i = 0; i < selectedInventory.GetSize(); i++)
            {
                var itemUI = slotPool.Get();
                itemUI.Setup(selectedInventory, i);
            }
        }

Hang on a second please, let me try writing this in my ‘InventoryUI.cs’ and we can see if the final script will make sense or not :smiley:

There’s… a lot of logical errors, and a few severe syntax errors :sweat_smile:

The logical error I see is a bunch of severely duplicated inventory slots, and undefined behaviour

and here’s the Syntax Errors:

NullReferenceException: Object reference not set to an instance of an object
GameDevTV.UI.Inventories.InventorySlotUI.GetItem () (at Assets/GameDev.tv Assets/Scripts/UI/Inventories/InventorySlotUI.cs:64)
GameDevTV.UI.Inventories.ItemTooltipSpawner.CanCreateTooltip () (at Assets/GameDev.tv Assets/Scripts/UI/Inventories/ItemTooltipSpawner.cs:16)
GameDevTV.Core.UI.Tooltips.TooltipSpawner.UnityEngine.EventSystems.IPointerEnterHandler.OnPointerEnter (UnityEngine.EventSystems.PointerEventData eventData) (at Assets/GameDev.tv Assets/Scripts/Utils/UI/Tooltips/TooltipSpawner.cs:57)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IPointerEnterHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:29)
UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:272)
UnityEngine.EventSystems.EventSystem:Update() (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:514)

which leads to this, in ‘InventorySlotUI.cs’:

        public InventoryItem GetItem()
        {
            return inventory.GetItemInSlot(index);
        }

and there’s also this one:

NullReferenceException: Object reference not set to an instance of an object
GameDevTV.UI.Inventories.InventorySlotUI.TryHandleRightClick () (at Assets/GameDev.tv Assets/Scripts/UI/Inventories/InventorySlotUI.cs:88)
UnityEngine.Events.InvokableCall.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.Events.UnityEvent.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.UI.Button.Press () (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Button.cs:70)
UnityEngine.UI.Button.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Button.cs:114)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IPointerClickHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:57)
UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:272)
UnityEngine.EventSystems.EventSystem:Update() (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:514)

which leads to this:

InventoryItem item = inventory.GetItemInSlot(index);

and… the last one:

NullReferenceException: Object reference not set to an instance of an object
RPG.States.Player.PlayerFreeLookState..ctor (RPG.States.Player.PlayerStateMachine stateMachine) (at Assets/Project Backup/Scripts/State Machines/Player/PlayerFreeLookState.cs:17)
RPG.States.Player.PlayerInventoryCollectionState.OnCollectionFinished () (at Assets/Project Backup/Scripts/State Machines/Player/PlayerInventoryCollectionState.cs:13)
PickupWindow.OnDisable () (at Assets/GameDev.tv Assets/Scripts/UI/Inventories/PickupWindow.cs:39)

in ‘PlayerFreeLookState.cs’:

        private Inventory playerInventory = GameObject.FindWithTag("Player").GetComponent<Inventory>();

For the moment I’ll reverse the changes, just to make sure that it’s not because of anything silly I did (and… yup, that’s the source of the error)

and if it helps (I ran out of edit limit, again), this is my original ‘InventoryUI.cs’ script, before pooling:

and if it helps (and as a backup), this is my original ‘InventoryUI.cs’ script, before Object Pooling:

using UnityEngine;
using GameDevTV.Inventories;
using TMPro;
using UnityEngine.Events;
using System.Linq;
using UnityEngine.Pool;

namespace GameDevTV.UI.Inventories
{
    /// <summary>
    /// To be placed on the root of the inventory UI. Handles spawning all the
    /// inventory slot prefabs.
    /// </summary>
    public class InventoryUI : MonoBehaviour
    {
        // CONFIG DATA
        [SerializeField] InventorySlotUI InventoryItemPrefab = null;

        // BANK System CONFIG DATA:
        [SerializeField] bool isPlayerInventory = true;
        [SerializeField] TextMeshProUGUI Title;

        // 3 bank <-> Inventory states: 
        // 'otherInventoryUI' (bank) is open, 
        // 'otherInventoryUI' (bank) is closed, 
        // YOU are the 'otherInventoryUI' (bank)

        public bool IsOtherInventory => !isPlayerInventory;
        public IInventorySource SelectedInventory => selectedInventory;

        [SerializeField] private UnityEvent EnabledEvent;
        [SerializeField] private UnityEvent DisabledEvent;

        // CACHE
        IInventorySource selectedInventory;

        // LIFECYCLE METHODS

        private void Awake() 
        {
            // if we are dealing with the player Inventory, set it up (just don't do it for the Players' bank)
            if (isPlayerInventory) 
            {
            selectedInventory = Inventory.GetPlayerInventory();
            selectedInventory.inventoryUpdated += Redraw; // you need this inventory subscription
            }
        }

        private void Start() // ORIGINAL START Function:
        {
            // Redraw the PLAYER INVENTORY, JUST NOT THE BANK!
            if (isPlayerInventory) Redraw();
        }

        // PRIVATE

        private void Redraw() // ORIGINAL REDRAW FUNCTION
        {
            // Re-creating the inventory from scratch, over each 'InventoryItemPrefab',
            // literally that's it!

            if (selectedInventory == null) return;

            foreach (Transform child in this.transform)
            {
                Destroy(child.gameObject);
            }

            for (int i = 0; i < selectedInventory.GetSize(); i++)
            {
                var itemUI = Instantiate(InventoryItemPrefab, transform);
                itemUI.Setup(selectedInventory, i);
                Debug.Log($"Refresh done");
            }
        }

        // PUBLIC

        public bool Setup(GameObject user) 
        {
            if (user.TryGetComponent(out selectedInventory)) {
                // if the object has an inventory (our bank in this case), set the Selected Inventory Up, 
                // Subscribe to the Redraw() changes, and then redraw the inventory (to boot it up for the first time):
                selectedInventory.inventoryUpdated += Redraw; // you ALSO need this inventory subscription
                Title.text = selectedInventory.GetDisplayName();
                Redraw();
                return true;
            }
            return false;
        }

        private void OnEnable()
        {
            EnabledEvent?.Invoke();
        }

        private void OnDisable()
        {
            DisabledEvent?.Invoke();
        }

        public void TransferAllInventory()
        {
            if (selectedInventory != null)
            {
                selectedInventory.TransferAllInventory(GetOtherInventory());
            }
        }

        private Inventory GetOtherInventory()
        {

            if (IsOtherInventory) return Inventory.GetPlayerInventory();

            // return the FIRST other Inventory it finds (i.e: The bank), or null:
            var otherInventoryUI = FindObjectsOfType<InventoryUI>().FirstOrDefault(ui => ui.IsOtherInventory);

            //Check to see if it's not null (should never happen), if it's Active, and if the otherInventory's inventory is valid.
            if (otherInventoryUI != null && otherInventoryUI.gameObject.activeSelf && otherInventoryUI.SelectedInventory != null)
            {
                Inventory otherInventory = otherInventoryUI.SelectedInventory as Inventory;
                return otherInventory;
            }

            return null;
        
        }

    }

}

This has nothing to do with the above code I posted…

I think the other errors are due to something very very silly… We already have placeholder slots in the UI that are destroyed in Redraw… but… they’re not in the pool.

So we need to eliminate these BEFORE we start utilizing the pool

        private void Start()
        {
            foreach (Transform child in transform)
            {
                Destroy(child.gameObject);
            }
            if(isPlayerInventory)
            {
                Redraw();
            }
        }

Like the line in PlayerFreeLookState, I don’t see a reason this should exist from the code we added… this error is saying that inventory was not set…

that line was there from way before we got this far, xD

Other than that, let me try again

It’s suggesting that it found another Player GameObject which didn’t have an Inventory on it…i.e. something in the game is tagged Player that shouldn’t be.

I think for that last one it’s because I forgot to unsubscribe something. It only shows at the end of the scene, and I believe that error was there before we got into this topic if I recall correctly. It was something probably to do with missing code in an OnDisable perhaps

Update about the object pooling: There’s no errors, but extremely undefined logical behaviour right now. Give me a minute, I’ll upload a video on the drive and share it here with you

Privacy & Terms