Merging the Third Person Controller Course with the RPG Course

// TEST
        private void OnEnable() 
        {
            if(!inventory) return;
            inventory.GetGameObject().GetComponent<Inventory>().OnIndividualSlotChanged += Inventory_IndividualSlotChanged;
        }

        // TEST
        private void OnDisable() 
        {
            if(!inventory) return;
            inventory.GetGameObject().GetComponent<Inventory>().OnIndividualSlotChanged -= Inventory_IndividualSlotChanged;
        }

OK I don’t mean to sound like a nuisance, but…:

Operator '!' cannot be applied to operand of type 'IInventorySource'

(That’s a syntax error the second I type that out)

This topic just won’t make it easy… :sweat_smile:

Anyway, when we write it like this, the syntax error is gone, but the NRE error still persists:

            if (!inventory.GetGameObject().GetComponent<Inventory>()) return;

The fun of keeping 13 codebases in my head + modifications…

if(inventory==null) return;

Well… the heavy load of the issue has been successfully transferred to the next line of each one of these functions (the lines of the subscription and unsubscription):

// TEST
        private void OnEnable()
        {
            if (inventory == null) return;
            inventory.GetGameObject().GetComponent<Inventory>().OnIndividualSlotChanged += Inventory_IndividualSlotChanged;
        }

        // TEST
        private void OnDestroy()
        {
            if (inventory == null) return;
            inventory.GetGameObject().GetComponent<Inventory>().OnIndividualSlotChanged -= Inventory_IndividualSlotChanged;
        }

not to mention this function is just being the biggest nuisance on planet earth right now:

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

:sweat_smile:

OK umm, how about this. I’ll outline every single step I have taken so far, and maybe we can find the problem of exactly where I went wrong, although 100% of the slots flying around and acting insanely weird are coming from this one single function:

        private void Redraw() // ORIGINAL REDRAW FUNCTION
        {
            // Re-creating the inventory from scratch, over each 'InventoryItemPrefab',
            // literally that's it! (WILL ONLY BE DONE ONCE WHEN THE GAME STARTS)

            // TEST
            if (selectedInventory == null) return;

            foreach (Transform child in transform)
            {
                // TEST
                slotPool.Release(child.GetComponent<InventorySlotUI>());


                // Destroy(child.gameObject); // OBJECT POOL TEST BELOW
            }

            for (int i = 0; i < selectedInventory.GetSize(); i++)
            {
                // var itemUI = Instantiate(InventoryItemPrefab, transform); // OBJECT POOL TEST BELOW
                // itemUI.Setup(selectedInventory, i);

                // TEST
                var itemUI = slotPool.Get();
                itemUI.transform.SetParent(transform);
                itemUI.Setup(selectedInventory, i);
            }
        }

Alright here it goes, and if this doesn’t work, then I’m better off leaving my bad performance inventory as is and calling it a day. On the long term, I’ll pay the price for this, but I don’t know what else to do:

  1. in ‘Inventory.cs’, I created this event:
        // TEST
        public event System.Action<int> OnIndividualSlotChanged;

and this was called everywhere ‘inventoryUpdated?.Invoke()’ was called:

// AddToFirstEmptySlot:

        /// <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>Whether or not the item could be added.</returns>
        public bool AddToFirstEmptySlot(InventoryItem item, int number, bool refresh = true)
        {

            // If we picked up money, automatically send it to the pouch:
            foreach (var store in GetComponents<IItemStore>()) 
            {
                number -= store.AddItems(item, number);
            }

            if (number <= 0) return true;

            // ----------------------------------------------------------

            int i = FindSlot(item);

            if (i < 0)
            {
                return false;
            }

            slots[i].item = item;
            slots[i].number += number;
            if (refresh) 
            {
                inventoryUpdated?.Invoke();
                OnIndividualSlotChanged?.Invoke(i);
            }
            return true;
        }

// RemoveFromSlot():

        /// <summary>
        /// Remove a number of items from the given slot. Will never remove more
        /// that there are.
        /// </summary>
        public void RemoveFromSlot(int slot, int number, bool refresh = true)
        {
            slots[slot].number -= number;
            if (slots[slot].number <= 0)
            {
                slots[slot].number = 0;
                slots[slot].item = null;
            }
            if (refresh) 
            {
                inventoryUpdated?.Invoke();
                OnIndividualSlotChanged?.Invoke(slot);
            }
        }

// AddItemToSlot():

        /// <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">The slot to attempt to add to.</param>
        /// <param name="item">The item type to add.</param>
        /// <param name="number">The number of items to add.</param>
        /// <returns>True if the item was added anywhere in the inventory.</returns>
        public bool AddItemToSlot(int slot, InventoryItem item, int number, bool refresh = true)
        {
            if (slots[slot].item != null)
            {
                return AddToFirstEmptySlot(item, number, refresh);
            }

            var i = FindStack(item);
            if (i >= 0)
            {
                slot = i;
            }

            slots[slot].item = item;
            slots[slot].number += number;
            if (refresh) 
            {
                inventoryUpdated?.Invoke();
                OnIndividualSlotChanged?.Invoke(slot);
            }
            return true;
        }

// RemoveItem():

public void RemoveItem(InventoryItem item, int number, bool refresh = true) 
{

    if (item == null) return;

    for (int i = 0; i < slots.Length; i++) {

        if (object.ReferenceEquals(slots[i].item, item)) {

            slots[i].number -= number;
            if (slots[i].number <= 0) 
            {

                slots[i].item = null;
                slots[i].number = 0;

            }

            if (refresh) 
            {
                inventoryUpdated?.Invoke();
                OnIndividualSlotChanged?.Invoke(i);
            }
            return;

        }

    }

}

  1. I added the event in ‘IInventorySource.cs’ as well:
        /// <summary>
        /// Broadcasts when an individual inventory slot has changed
        /// </summary>
        event Action<int> OnIndividualSlotChanged;

which means, in ‘PickupFinder.cs’, I did what I did in ‘Inventory.cs’ as well:

        public event Action<int> OnIndividualSlotChanged;

// RemoveFromSlot():

        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();
                OnIndividualSlotChanged?.Invoke(slot);
            }
        }

// RemoveItem:

        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();
                            OnIndividualSlotChanged?.Invoke(currentNumber);
                        }
                        return;
                    }
                    else // max number of consumed items taken exceeded, so consume everything
                    {
                        number -= currentNumber;
                        Targets.Remove(pickupTarget);
                        Destroy(pickupTarget.gameObject);
                        if (refresh) 
                        {
                            inventoryUpdated?.Invoke();
                            OnIndividualSlotChanged?.Invoke(currentNumber);
                        }
                        if (number <= 0) return;
                    }
                }
            }
        }

  1. in ‘InventorySlotUI.cs’, I added a few lines of code:
        // TEST
        private void OnEnable()
        {
            if (inventory == null) return;
            inventory.OnIndividualSlotChanged += Inventory_IndividualSlotChanged;
        }

        // TEST
        private void OnDestroy()
        {
            if (inventory == null) return;
            inventory.OnIndividualSlotChanged -= Inventory_IndividualSlotChanged;
        }

        // TEST
        private void Inventory_IndividualSlotChanged(int slotChanged)
        {
            if (index == slotChanged)
            {
                Refresh(inventory, slotChanged);
            }
        }

        // TEST
        public void Refresh(IInventorySource inventory, int index) 
        {
            icon.SetItem(inventory.GetItemInSlot(index), inventory.GetNumberInSlot(index));
        }

  1. in ‘InventoryUI.cs’, we created the pool:
        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
            }

            // TEST
            slotPool = new LinkedPool<InventorySlotUI>(Pool_Create, Pool_Get, Pool_Release, Pool_Destroy);
        }

        private InventorySlotUI Pool_Create() 
        {
            return Instantiate(InventoryItemPrefab, transform);
        }

        private void Pool_Get(InventorySlotUI obj) 
        {
            obj.gameObject.SetActive(true);
        }

        private void Pool_Release(InventorySlotUI obj) 
        {
            obj.transform.SetParent(physicalPool);
            obj.gameObject.SetActive(false);
        }

        private void Pool_Destroy(InventorySlotUI obj) 
        {
            Destroy(obj.gameObject);
        }

  1. We tried testing the pool in ‘Redraw()’, but my code is failing terribly at that:
        private void Redraw() // ORIGINAL REDRAW FUNCTION
        {
            // Re-creating the inventory from scratch, over each 'InventoryItemPrefab',
            // literally that's it! (WILL ONLY BE DONE ONCE WHEN THE GAME STARTS)

            // TEST
            if (selectedInventory == null) return;

            foreach (Transform child in transform)
            {
                // TEST
                // slotPool.Release(child.GetComponent<InventorySlotUI>());


                Destroy(child.gameObject); // OBJECT POOL TEST BELOW
            }

            for (int i = 0; i < selectedInventory.GetSize(); i++)
            {
                var itemUI = Instantiate(InventoryItemPrefab, transform); // OBJECT POOL TEST BELOW
                itemUI.Setup(selectedInventory, i);

                // TEST
                // var itemUI = slotPool.Get();
                // itemUI.transform.SetParent(transform);
                // itemUI.Setup(selectedInventory, i);
            }
        }

and 7. I create a ‘physicalPool’ in the hierarchy, and went to each ‘InventoryUI.cs’ I can think of, and tried hooking them up. I can see it as it works, but the mathematics behind the job is insanely wrong

and don’t forget ‘InventoryUI.Start()’:

private void Start() // ORIGINAL START Function:
        {
            foreach (Transform child in transform) 
            {
                Destroy(child.gameObject);
            }

            // Redraw the PLAYER INVENTORY, JUST NOT THE BANK!
            if (isPlayerInventory) Redraw();
        }

These are all the steps I’ve taken, if it helps in anyway

Since you said you weren’t worried about the pickups at this time, I forgot about the IInventory interface…

You’ll need to add the event to the IInterface just like whe inventoryUpdated event.

Then you can use inventory.OnIndividualSlotChanged (and of course, you’ll have to implement this method in the PickupFinder as well.

This brings up a whole new can of ugly worms, though, because the PickupFinder does not have as concrete of an idea about slot… as once you remove an item, the slot count will change…

So maybe this construction might be better…

if(inventory!=null && inventory.GetGameObject().TryGetComponent(out Inventory i))
{
     i.OnIndividualSlotChanged += Inventory_IndividualSlotChanged;
}

And leave the Pickups to use InventoryUI’s full redraw (if you have 400 items in pickup range, that’s a whole different can of worms).

Is that implemented in ‘InventorySlotUI.cs’?

the problem is… EVERY SINGLE SORT OF INVENTORY in the game uses that function. Do I re-write a dedicated variant in ‘InventoryUI.cs’, or…?!


I’ll be honest though, I am sure the entire problem lies here:

        private void Redraw() // ORIGINAL REDRAW FUNCTION
        {
            // Re-creating the inventory from scratch, over each 'InventoryItemPrefab',
            // literally that's it! (WILL ONLY BE DONE ONCE WHEN THE GAME STARTS)

            // TEST
            if (selectedInventory == null) return;

            foreach (Transform child in transform)
            {
                // TEST
                slotPool.Release(child.GetComponent<InventorySlotUI>());


                // Destroy(child.gameObject); // OBJECT POOL TEST BELOW
            }

            for (int i = 0; i < selectedInventory.GetSize(); i++)
            {
                // var itemUI = Instantiate(InventoryItemPrefab, transform); // OBJECT POOL TEST BELOW
                // itemUI.Setup(selectedInventory, i);

                // TEST
                var itemUI = slotPool.Get();
                itemUI.transform.SetParent(transform);
                itemUI.Setup(selectedInventory, i);
            }
        }

whatever secret sauce this function has, it’s giving me a major headache. It doesn’t matter which inventory we are mentioning, this function is the “make or break” of it all, and if we can’t deal with this nothing else really matters tbh :sweat_smile: (everytime I reverse this function, it works perfectly regardless of what everything else is doing)

OK I know this sounds a little crazy, but Object Pooling really is being a headache, and it seems to be causing me more problems than solutions, and no matter what I did, it doesn’t seem to be working for me. Let’s try the slot-based refresh instead. I don’t know what kind of crazy challenges will I be dealing with, but Object Pooling is about to make me give up on this major problem tbh, and that’s not really an option but I don’t know what else to do with Object Pooling :sweat_smile:

I read about it as well from the Unity Forums, someone said it’s a huge headache because you need to keep resetting the values and what not… Doesn’t sound worth it for this thing tbh

@Brian_Trotter OK so I’m attempting a slot-based refresh, but this one is a bit of a headache. Here’s what I’ve done so far:

  1. Get and Set a Source Slot Index. This will be the Inventory Slot UI the item is leaving:
// in 'InventorySlotUI.cs':

// variable:

        private int sourceSlotIndex;

        public int GetSourceSlotIndex() 
        {
            return sourceSlotIndex;
        }

        public void SetSourceSlotIndex(int sourceSlotIndex) 
        {
            this.sourceSlotIndex = sourceSlotIndex;
        }

// when it leaves the UI:

        // Transferring stuff from the Inventory to our Bank:
        private void TransferToOtherInventory(Inventory otherInventory, InventoryItem item, int number)
        {
            Debug.Log($"TransferToOtherInventory is called from {new System.Diagnostics.StackTrace().GetFrame(1).GetMethod().Name}");

            if (otherInventory.HasSpaceFor(item))
            {
                otherInventory.AddToFirstEmptySlot(inventory.GetItemInSlot(index), number, true);
                inventory.RemoveItem(item, number);
                SetSourceSlotIndex(this.index);
                SetTargetInventory(otherInventory); // you'll know what this does in step 3
                // Setup(inventory, index); // <-- if you keep this, the new Inventory-based Pickup System will complain (because these inventory slots vanish)
            }
        }
  1. Get and Set a Destination Slot. This will be done in ‘Inventory.AddToFirstEmptySlot()’, since this is the slot we are aiming to refresh:
        /// <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>Whether or not the item could be added.</returns>
        public bool AddToFirstEmptySlot(InventoryItem item, int number, bool refresh = true)
        {

            // If we picked up money, automatically send it to the pouch:
            foreach (var store in GetComponents<IItemStore>()) 
            {
                number -= store.AddItems(item, number);
            }

            if (number <= 0) return true;

            // ----------------------------------------------------------

            int i = FindSlot(item);

            if (i < 0)
            {
                return false;
            }

            slots[i].item = item;
            slots[i].number += number;
            SetDestinationSlotIndex(i);
            if (refresh) 
            {
                inventoryUpdated?.Invoke();
            }
            return true;
        }

        private int destinationSlotIndex;

        public int GetDestinationSlotIndex() 
        {
            return destinationSlotIndex;
        }

        public void SetDestinationSlotIndex(int destinationSlotIndex) 
        {
            this.destinationSlotIndex = destinationSlotIndex;
        }

  1. back in ‘InventorySlotUI.cs’, we need to find a way to get the inventory that we are transferring the item to, and this will also be setup in ‘TransferToOtherInventory()’:
        private Inventory targetInventory;

        public Inventory GetTargetInventory() 
        {
            return targetInventory;
        }

        public void SetTargetInventory(Inventory targetInventory) 
        {
            this.targetInventory = targetInventory;
        }

        // ----------------------------------------------------------------------------------------------------------------------

        // Transferring stuff from the Inventory to our Bank:
        private void TransferToOtherInventory(Inventory otherInventory, InventoryItem item, int number)
        {
            Debug.Log($"TransferToOtherInventory is called from {new System.Diagnostics.StackTrace().GetFrame(1).GetMethod().Name}");

            if (otherInventory.HasSpaceFor(item))
            {
                SetTargetInventory(otherInventory);
                otherInventory.AddToFirstEmptySlot(inventory.GetItemInSlot(index), number, true);
                inventory.RemoveItem(item, number);
                SetSourceSlotIndex(this.index);
                // Setup(inventory, index); // <-- if you keep this, the new Inventory-based Pickup System will complain (because these inventory slots vanish)
            }
        }
  1. We need a way to get the last clicked UI, so we will create a variable for that in ‘InventoryUI.cs’ and set it in ‘InventorySlotUI.cs’:
// in 'InventoryUI.cs':

        private InventorySlotUI LastUIClicked;

        public InventorySlotUI GetLastUIClicked() 
        {
            return LastUIClicked;
        }

        public void SetLastUIClicked(InventorySlotUI LastUIClicked) 
        {
            this.LastUIClicked = LastUIClicked;
        }

// in 'InventorySlotUI.cs' (you'll need the 'IPointerClickInterface' Interface for this to work):

        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
                GetComponentInParent<InventoryUI>().SetLastUIClicked(this);
                Debug.Log($"{index} was clicked once");
            }

            else 
            {
                HandleDoubleClick();
            }
        }

  1. And now, I can safely write a ‘SlotBasedRedraw()’ function:
        // --------------------------------------------- Slot-based Redraw (Performance booster for Inventory Transfers) ----------------------------

        private InventorySlotUI LastUIClicked;

        public InventorySlotUI GetLastUIClicked() 
        {
            return LastUIClicked;
        }

        public void SetLastUIClicked(InventorySlotUI LastUIClicked) 
        {
            this.LastUIClicked = LastUIClicked;
        }

        private void SlotBasedRedraw()
        {
            // Check for null reference before proceeding
            if (selectedInventory == null)
            {
                Debug.LogError("SelectedInventory is null!");
                return;
            }

            var lastUIClicked = GetLastUIClicked();
            if (lastUIClicked == null)
            {
                Debug.LogWarning("No UI clicked yet.");
                return;
            }

            foreach (Transform child in transform)
            {
                var slotUI = child.GetComponent<InventorySlotUI>();

                if (slotUI == null)
                {
                    Debug.LogWarning("Child does not have InventorySlotUI component.");
                    continue;
                }

                if (slotUI.GetSlotIndex() == lastUIClicked.GetSlotIndex())
                {
                    Destroy(child.gameObject);
                }
            }

            var targetInventory = GetLastUIClicked().GetTargetInventory();
            var destinationSlotIndex = targetInventory.GetDestinationSlotIndex();

            for (int i = 0; i < targetInventory.GetSize(); i++)
            {
                if (i == destinationSlotIndex)
                {
                    var itemUI = Instantiate(InventoryItemPrefab, transform);
                    itemUI.Setup(selectedInventory, i);
                }
            }
        }

        // ------------------------------------------------------------------------------------------------------------------------------------------

and 6. Make sure ‘InventoryUI’ subscriptions of ‘inventoryUpdated’ are done with ‘SlotBasedRedraw’ instead of ‘Redraw’:

        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 += SlotBasedRedraw; // you need this inventory subscription
            }
        }

        public bool Setup(GameObject user)
        {
            Debug.Log($"InventoryUI Setup is called from {new System.Diagnostics.StackTrace().GetFrame(1).GetMethod().Name}");

            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 += SlotBasedRedraw;
                Title.text = selectedInventory.GetDisplayName();
                Redraw();
                return true;
            }
            return false;
        }

So, I have one question: Why is my inventory not updating correctly when picking stuff up (if I recall, slots can randomly instantiate new slots, not show icons or place icons that don’t properly update their images and data in random slots), or more importantly, why is my bank not opening up correctly after this Update? (Bank shows absolutely no Inventory Slots when it’s opened up) Do you see any pitfalls here?

You may want to copy-paste that in your version of my systems, if you have one, because I also got Pickup Inventories aside from the banks, and see if you can fix it. PLEASEEE

This approach is… both confusing and overengineered… Please don’t take offense, but it needs to go back to the drawing board… It’s an unsimple solution that is more likely to break than it is to work properly.

A few things:

  • You’re still destroying slots, but arbitrarily in the middle of the collection of slots, meaning that items will be (at best) out of order as you rearrange slots…
  • There is no need to keep track of a target and destination slot because, and I’ve said this before every transaction in Inventory is singular, either Add, or Remove. The only place that both things are happening is in the Drag scripts, in the event of a direct drop of one thing onto an existing thing, and then Drag Removes both items (each of these is an individual transaction) and puts them in their new homes (each of these is an individual transaction). The actual transactions themselves are either an add or a replace. This means that with each transaction, you only have to update ONE display

There’s a problem with Individual refresh when it comes to Pickups… because Pickups have a dynamic size… as you pull from the pickups, unless you pull the very last item, the indexes of the items after you pull them will change. This is because they are working off of a list of pickups in the vicinity, not an Array of pickups, and when you remove the item, it necessarily has to remove the item from the supported list. So… the pickups need to be approached more like the classic way.

Banks and Inventories, however, will have a fixed number of slots, unless the number of slots available changes. But… we don’t know the inventory size until we connect to the inventory itself, so at least initially, the inventory needs a complete redraw when first starting.

If we didn’t have to deal with pickups, and there was only one fixed bank in the scene, individual redraws would be fairly simple, quite literally subscribe, refresh the icon when needed. However, this is not the case, and it locks out the possibility of players buying extra inventory space, or multiple banks.

This is why an Object Pooling model works better. Yes, it’s changing the parent often, but that’s still vastly superior to Destroying and Instantiating every change.

Nah I’m offended, I’ll sue you for $50,000 in emotional damages :stuck_out_tongue: (I’m joking). Honestly speaking Brian, I am not taking any offence. I’m here to learn and design, and to make this work.

Fun fact, from the concept remaining that I want to program, I believe this is as hard as it will ever get. It’s like I’m dealing with the final boss of a major battle, and we need to win this battle :smiley:

@bixarrio suggested this as a concept to me once. I didn’t think much of it, but I might take it into consideration if I can find a spot for it on my game plan :smiley: - but that’s for another day, for now we just want to ensure that I don’t run into long term performance issues. That’s literally all I’ve been begging for from my code, but this is quite difficult for me alone

If it improves long-term performance, and doesn’t have crazy inventory slots instantiating out of nowhere and inventory slots being used here and there without an elaborate explanation, then I’m all in on it. I honestly don’t mind which approach, as long as long-term, I don’t get lag spikes because we’re refreshing a huge number of inventory slots

For now let me just go erase whatever I have done if you don’t recommend it, since I can’t figure out how to make it work either if I’m being honest

I’ll wait for any updates from your side if you have any recommendations of what to do next, because I’m not sure how things are working for you, but for me it’s just… not, and I’m quite adamant on not moving forward unless we fix this because I’m afraid that if I let this go now, I might never solve it down the line, and then I have a universal complaint from all players, and that’s not how I want things to go :smiley:

As I was typing out my thesis about the destruction of all you have wrought, I came up with an idea that might solve one of the problems… the Pickups… It’s still a two tiered system, but I have an idea that could allow both individual refresh and dynamic size, selecting between the two based on whether it’s physical or not.

I’ve got several meetings today, but I’ll work on it in between as free time permits.

Sounds amazing, I’ll be waiting to hear from you soon. All the best with your meetings today!

(Just a heads-up though, I deleted all my previous object pooling attempts just to keep my code clean… and recently deleted my own slot-based refresh attempts as well)

Step 1: Any place in Inventory.cs that calls InventoryUpdated should also call InventorySlotUpdated(slot) (or i, or whatever index of the slot that just changed is).

Step 2: Add the following property to IInventorySource

bool IsPhysical => true;

By including the autoimplementation of the property (true), we ensure that we don’t need to actually override this in Inventory, because when there is a default implementation in an Interface, there is no need to override it to to fulfill the contract.

However, in PickupFinder, we want to override this

public overrided bool IsPhysical => false;

Now the Bank and the Inventory will be Physical, the PickupCollector won’t be.

This is because we want two different behaviours to happen based on the location…

The rest of this is handled in InventoryUI and InventorySlotUI. I pulled the ObjectPooling out, because it really won’t be saving much clock for the additional complexity.

InventoryUI.cs

using UnityEngine;
using GameDevTV.Inventories;
using TMPro;

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;
        [SerializeField] bool isPlayerInventory = true;
        [SerializeField] TextMeshProUGUI Title;

        [SerializeField] private Transform physicalPool;
        // CACHE
        IInventorySource selectedInventory;
        
        
        
        // LIFECYCLE METHODS

        private void Awake() 
        {
            if (isPlayerInventory)
            {
                selectedInventory = Inventory.GetPlayerInventory();
            }
            
        }




        private void Start()
        {
            if(isPlayerInventory)
            {
                Redraw();
            }
        }

        // PRIVATE

        private void OnEnable()
        {
            Redraw();
        }

        private void Redraw()
        {
            if (selectedInventory==null) return;
            foreach (Transform child in transform)
            {
                Destroy(child.gameObject);
            }

            for (int i = 0; i < selectedInventory.GetSize(); i++)
            {

                var itemUI = Instantiate(InventoryItemPrefab, transform);
                itemUI.transform.SetParent(transform);
                itemUI.Setup(selectedInventory, i);
            }
        }
        
        
        public bool Setup(GameObject user)
        {
            if (selectedInventory is { IsPhysical: false })
            {
                selectedInventory.inventoryUpdated -= Redraw;
            }
            if (user.TryGetComponent(out selectedInventory))
            {
                if (!selectedInventory.IsPhysical)
                {
                    selectedInventory.inventoryUpdated += Redraw;
                }
                Title.text = selectedInventory.GetDisplayName(); 
                Redraw();
                return true;
            }
            return false;
        }
    }
}

In Awake(), we’re first checking to see if we’re the Player’s inventory and setting the selectedInventory appropriately. We’re NOT subscribing to InventoryUpdated.

In Start(), we Redraw if we’re the Player Inventory, to get everything set up.

In Setup, if we’re NOT a physical inventory, then we subscribe to the inventoryUpdated, if we are a physical Inventory (bank), we don’t. Then, since we’re just setting up, we force a full Redraw, because either way, when we first open the other window, we want to Redraw.

So we’ve now set up updating the PickupFinder inventory when changes are made. Because it is dynamic, simply refreshing slots is a bad idea and will cause problems, so updating every time something in PickupFinder changes is the way to go.

Next, we head into InventorySlotUI.cs. I’ll just include the changed and added methods, because my version of the script does not contain the Top Ramen code that is the click handling.

   public void Setup(IInventorySource inventory, int index)
        {
            this.inventory = inventory;
            this.index = index;
            Redraw();
            if (inventory.IsPhysical)
            {
                inventory.GetGameObject().GetComponent<Inventory>().OnInventorySlotUpdated +=
                    Inventory_OnInventorySlotUpdated;
            }
        }

        void OnDestroy()
        {
            if (inventory is { IsPhysical: true })
            {
                inventory.GetGameObject().GetComponent<Inventory>().OnInventorySlotUpdated -=
                    Inventory_OnInventorySlotUpdated;
            }
        }
        
        private void Inventory_OnInventorySlotUpdated(int slot)
        {
            if (slot == index) Redraw();
        }
        
        private void Redraw()
        {
            icon.SetItem(inventory.GetItemInSlot(index), inventory.GetNumberInSlot(index));
        }

So in this case, we’re checking to see if we ARE physical. In the event that we are, then we subscribe to the InventorySlotUpdated method. We factored out the Icon change to be a Redraw method, and our handler checks to see if the slot matches and Redraws if appropriate.

OnDestroy() is added, and the Inventory is checked to see if it is physical. If it is, we unsubscribe from the event.

Thank you so much Brian for the hard work you’ve put into this, it really means a lot to me! - There’s a lot going on here, so I’ll carefully go through it and update you with any questions that I have. Hopefully this time it works :smiley:

@Brian_Trotter OK so here comes the first problem. The InventorySlotUpdated(slot) will have an action of type integer event, right? I believe it’s as follows:

        /// <summary>
        /// Broadcasts when an inventory slot is updated
        /// </summary>
        public event Action<int> InventorySlotUpdated;


The problem I have is in these two functions being called:

// for the button that transfers everything (do we even need to update this one? Looks to me like the first line of the for loop took care of that)

public void TransferAllInventory(Inventory otherInventory) {

    if (otherInventory == null || otherInventory == this) return;

    for (int i = 0; i < inventorySize; i++) {

        if (slots[i].item == null) continue;    // the sole responsible line for fixing my long banking times, which ensures empty inventory slots are not refreshed
        for (int j = slots[i].number; j > 0; j--) {

            if (otherInventory.HasSpaceFor(slots[i].item)) {

                otherInventory.AddToFirstEmptySlot(slots[i].item, 1, false);
                slots[i].number--;
                if (slots[i].number <= 0) slots[i].item = null;

            }

            else break;

        }

    }

    inventoryUpdated?.Invoke();
    otherInventory.inventoryUpdated?.Invoke();
    // InventorySlotUpdated?.Invoke(i);
    // otherInventory.InventorySlotUpdated?.Invoke(i);

}

// and for 'TriggerUpdate()' (if I recall, this was for systems that got the player to drop all his equipment on death)

        public void TriggerUpdated()
        {
            inventoryUpdated?.Invoke();
            InventorySlotUpdated?.Invoke(slot);
        }

Where do we get the slot values for these two?

Oh, and I’m assuming the ‘InventorySlotUpdated’ does not happen when restoring the save file as well, correct?


Another MAJOR problem I’m dealing with, is that the slots are acting like I only get only one click with them and they’ll never work again beyond that, unless I close and reopen the inventory. It’s acting like Electronic Arts right now, as in “you only get one click, and pay $5.50 for each click beyond that”

Humor aside, the slots are no longer clickable after the first click, unless you close and re-open the inventory, so that’s a new problem :sweat_smile: - this is the last problem I have with this system. Can we please fix that?

Edit: I fixed the last problem. I forgot about ‘isHandlingRightClick’ in my personal ‘TryHandleRightClick()’ function, so I commented it out:

        // Equipping a Weapon by clicking on it, and banking it if the bank is open:
        public void TryHandleRightClick()
        {
            // if (isHandlingRightClick) return; // <-- this was the troublesome line
            isHandlingRightClick = true;

            InventoryItem item = inventory.GetItemInSlot(index);
            int number = inventory.GetNumberInSlot(index);
            

I’m sure there’s a good reason this was there. I just don’t remember what


and one last question, do we remove the ‘InventoryUpdated’ event now?

And just a small heads-up, in ‘PickupFinder.cs’, you will want to refer to ‘IsPhysical’ from ‘IInventorySource.cs’ as follows:

        bool IInventorySource.IsPhysical => false; // Pickup Inventory is dynamic, as the slots vanish the moment their content is consumed

if you forget ‘IInventorySource’, it won’t recognize it, and your pickup inventory won’t work (and there was a few traces of pooling in your ‘InventoryUI.cs’, which I ignored)

Other than that, this new system is blazingly fast, and I am incredibly happy, and I owe you a lot for just this one! :smiley:

Anyway, time to move on to the next concept (I have less than 10 to go, and one of my previous ones (the NPC Fight-back logic in ‘EnemyStateMachine.OnTakenHit’) needs a complete re-write because the logic is a bit of a complete mess right now, with a few unpredicted actions that I’m struggling to hunt down happening here and there)

I’ll be honest… I’m getting frustrated because your Inventory is so far off of the standard that everything keeps breaking.

public void TransferAllInventory(Inventory otherInventory) {

    if (otherInventory == null || otherInventory == this) return;

    for (int i = 0; i < inventorySize; i++) {

        if (slots[i].item == null) continue;    // the sole responsible line for fixing my long banking times, which ensures empty inventory slots are not refreshed
        for (int j = slots[i].number; j > 0; j--) {

            if (otherInventory.HasSpaceFor(slots[i].item)) {

                otherInventory.AddToFirstEmptySlot(slots[i].item, 1, false);
                slots[i].number--;
                if (slots[i].number <= 0) slots[i].item = null;
                InventorySlotUpdated(i); //<<<-----
            }

            else break;

        }

    }

    inventoryUpdated?.Invoke();


}

Just like above, you’ll do InventorySlotUpdated in the middle of the for loop.

I can’t blame you, I honestly did ask for a lot. I apologize for this, I truly understand that by now you’d be frustrated by me entirely, and I don’t blame you, but I genuinely don’t know who else to ask. Again, I’m really sorry for all of this mess :sweat_smile:

Well after this whole inventory show, I had quite the run. In less than 24 hours. I integrated gliding, made sure that it needs to find a glider in your inventory, with an option to make this consumable, and damage from falling from high points in the game world (I also made sure you don’t get damaged from jumping, by optimizing the value that determines the damage and making sure it’s not too low)

I’m yet to fix a few bugs from my farming system though :sweat_smile: - that’s what I’ll do next

And then I just noticed a major bug that I’ve never encountered before… DYNAMIC CRAFTING TABLES AREN’T SAVEABLE YET!

Privacy & Terms