Merging the Third Person Controller Course with the RPG Course

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!

One week later, and we fixed the dynamic crafting tables problem. Anyway, the next thing I want to do, is to make sure that in ‘WindowController.cs’, only when you open the pause menu do you freeze the time scale. In other words, if you open the inventory, quest list, etc… don’t freeze the time. Only freeze it when the pause menu is opened

I’ll work on this, but I figured I should let you know @Brian_Trotter (in case you got any ideas for that one, xD).

Anyway, here’s the ‘WindowController.cs’ script:

using System.Collections.Generic;
using RPG.InputReading;
using UnityEngine;

namespace RPG.UI 
{
    // Generic script, responsible for common behaviour between User Interface Systems

    public abstract class WindowController: MonoBehaviour
    {
        protected static HashSet<WindowController> activeWindows = new();

        public static event System.Action OnAnyWindowOpened;
        public static event System.Action OnAllWindowsClosed;

        protected InputReader InputReader;

        // Protected and Virtual, therefore this method can be overridden (because it's virtual) by any inheriting scripts (because it's protected),
        // and tuned:
        protected virtual void Awake() 
        {
            InputReader = GameObject.FindWithTag("Player").GetComponent<InputReader>();
            Subscribe();
        }

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

        // when any window is open:
        void OnEnable() 
        {
            activeWindows.Add(this);
            Time.timeScale = 0.0f;
            OnAnyWindowOpened?.Invoke();
            Debug.Log($"Window enabled");
        }

        // when any window is closed:
        protected virtual void OnDisable() 
        {
            activeWindows.Remove(this);
            if (activeWindows.Count == 0) {
                Time.timeScale = 1.0f;
                OnAllWindowsClosed?.Invoke();
            }
            Debug.Log($"Window disabled");
        }

        /// <summary>
        /// Override this method to subscribe to any events that will call this implementation of 'WindowController'
        /// </summary>
        protected abstract void Subscribe();

        /// <summary>
        /// Override this method to ubsubscribe to any events that will call this implementation of 'WindowController'
        /// </summary>
        protected abstract void Unsubscribe();

        protected void CloseWindow()
        {
            gameObject.SetActive(false);
            Debug.Log($"Window closed");
        }

        protected void ToggleWindow()
        {
            gameObject.SetActive(!gameObject.activeSelf);
            Debug.Log($"Window toggled to {(gameObject.activeSelf ? "active" : "inactive")}");
        }
    }
}

Anyway, here’s what I came up with:

  1. In ‘InputReader.cs’, I introduced a circular dependency (I realized that one was there from the days of me creating the parrying system… I will need to find a way around this). Apart from that, I swapped out ‘EnableControls’ and ‘DisableControls’ in ‘InputReader.cs’ for new ‘EnableFreeLookCamera’ and ‘DisableFreeLookCamera’. That way, you don’t lose your controls, you just freeze the Free Look camera when you got an inventory of some sort open in your game:
    private PlayerStateMachine playerStateMachine;

    private void Start() 
    {
        controls = new Controls();
        controls.Player.SetCallbacks(this);
        controls.Player.Enable();
        controls.UI.SetCallbacks(this);
        controls.UI.Enable();

        // WindowController.OnAnyWindowOpened += DisableControls;
        // WindowController.OnAllWindowsClosed += EnableControls;

        // TEST - DELETE IF FAILED
        WindowController.OnAnyWindowOpened += DisableFreeLookCamera;
        WindowController.OnAllWindowsClosed += EnableFreeLookCamera;

        playerStateMachine = GetComponent<PlayerStateMachine>();
        animationEventRelay = GetComponent<AnimationEventRelay>();
    }

    /* void EnableControls() 
    {
        controls.Player.Enable();
    }

    void DisableControls() 
    {
        controls.Player.Disable();
    } */

    // TEST FUNCTION - DELETE IF FAILED
    void EnableFreeLookCamera() 
    {
        // (HARD-CODED VALUES. IF YOU CHANGE THEM IN CINEMACHINE FREE LOOK CAMERA, CHANGE THEM HERE TOO!)
        playerStateMachine.PlayerFreeLookCamera.m_XAxis.m_MaxSpeed = 200;
        playerStateMachine.PlayerFreeLookCamera.m_YAxis.m_MaxSpeed = 0;

        // Do the same for the Boat driving camera as well
    }

    void DisableFreeLookCamera() 
    {
        playerStateMachine.PlayerFreeLookCamera.m_XAxis.m_MaxSpeed = 0f;
        playerStateMachine.PlayerFreeLookCamera.m_YAxis.m_MaxSpeed = 0f;

        // Do the same for the Boat driving camera as well
    }

        private void OnDestroy() 
    {
        // WindowController.OnAnyWindowOpened -= DisableControls;
        // WindowController.OnAllWindowsClosed -= EnableControls;

        // TEST - DELETE IF FAILED
        WindowController.OnAnyWindowOpened -= DisableFreeLookCamera;
        WindowController.OnAllWindowsClosed -= EnableFreeLookCamera;

        controls.Player.Disable();
        controls.UI.Disable();
    }
  1. in ‘WindowController.cs’, I removed the time scale freeezing, and strictly placed it in ‘PauseMenuUI.cs’:
        // when any window is open:
        void OnEnable()
        {
            activeWindows.Add(this);
            // Time.timeScale = 0.0f;
            OnAnyWindowOpened?.Invoke();
            Debug.Log($"Window enabled");
        }

        // when any window is closed:
        protected virtual void OnDisable()
        {
            activeWindows.Remove(this);
            if (activeWindows.Count == 0)
            {
                // Time.timeScale = 1.0f;
                OnAllWindowsClosed?.Invoke();
            }
            Debug.Log($"Window disabled");
        }
  1. in ‘PauseMenuUI.cs’, I modified ‘HandleCancelEvent()’, so that we can freeze the time on pause only for pausing, and not anything else:
        private void HandleCancelEvent() 
        {
            if (activeWindows.Count > 0) 
            {
                var windows = activeWindows.ToList();

                for (int i = 0; i < activeWindows.Count; i++) 
                {
                    windows[i].gameObject.SetActive(false);
                }

                // TEST LINE - DELETE IF FAILED
                Time.timeScale = 1.0f;
            }

            else 
            {
                // TEST LINE - DELETE IF FAILED
                Time.timeScale = 0.0f;

                gameObject.SetActive(true);
            }

        }

Now I gotta find which other cameras need to freeze as well (like the boat-driving camera for example), and work on that

But I also noticed something, the Quest UI is not part of the WindowController yet, so it doesn’t play by it’s rules just yet. I’ll need to work on that later

Is this the best approach? I truly don’t know, but time will tell


I also have to mention that the Resume button from the pause menu, along with the quit button, will suffer because of this setup. Here’s how I fixed it:

  1. Place this simple function in ‘PauseMenuUI.cs’:
        public void RestoreTimeSpeedToNormal() 
        {
            Time.timeScale = 1.0f;
        }
  1. Apply this to both the quit button, and the resume button, to restore the game speed:

and I also did the same for my own custom Pickup Inventory UI, where I restore the speed when it’s closed:

(I’ll work on the code of freezing it to begin with)

The one major drawback I believe I will face with my variant, is you’ll be strangled quite a lot by NPCs and your camera will be frozen. I’ll probably just need to make a button for that or something, instead of this setup

That looks like you’re on the right track.

I didn’t do the Quest panel as a WindowController because you might want that quest window up while you walk around looking for Baddy McBadface to get his McGuffin of Ultimate Power.

[IGNORE. I REVERSED EVERYTHING. I ADMIT, THIS WAS A TERRIBLE IDEA FROM MY SIDE!]

I want the Quest Panel in this as well, though, so I can freeze the camera for that as well. I think it would be important not to drive the player to insanity through camera rotations when he’s playing his/her game, and he has an open UI of any sort :slight_smile:

I’m literally debating on either this approach, or get an input reader button to universally block the current camera from rotating, but if their rotation speeds are different this will be very challenging (unless I set them all up to the exact same value, that is)

Something else I noticed, is if you hit the attack button whilst in the Inventory UI, or dragging and dropping stuff around, you do attack, and that’s… not 100% convenient

And after a little bit of thinking, because you can always be attacked by a hostile NPC whilst collecting stuff or something, when you’re attacked, why don’t we automatically set the player to enter his targeting state and aim for whoever just hit him, under the condition that he’s not aiming at someone else? (i.e: ‘stateMachine.Targeter.HasTargets’ is false) That way the player can focus on who just hit him

The one window I’d make an exception to freeze time for, because it would be quite annoying otherwise, would be the pickup window


Edit: Now I see why you chose to freeze the entire game when a window is open…

Edit 2: Ahh nevermind, forget this issue… I reversed everything. This was a terrible idea! (I wanted to do this only because I thought that this would make the game look more polished, but frankly speaking, it made gameplay insanely difficult, and I can see a million bugs coming out of just this one. I’d rather have great gameplay than a stylish-looking game)

Tomorrow I’ll go full insanity mode, and start working on making Animals enemies as well. I recently fixed the last few bugs that I had as of right now. This will take a while to get it right

That could only be happening if you had a state that was not unsubscribing from the attack button…

We? No… You? Probably. :slight_smile:

In the list of high end AAA games that I’m playing off and on over the last six months, four out of four of them (Marvel Midnight Suns, Final Fantasy VII Remake Intergrade, Borderlands, Dragon’s Age: Inqusition) freeze the game when you open a window. Witcher did too, if I recall. MMORPGS (multiplayer) like World of Warcraft do not freeze the game, but that’s sort of the nature of multi-player games.

I chose to freeze the entire game when the window is open because it’s what most gamers expect in single player mode.

I just wanted something a little more lively, but I’m not going down this path again. Last time I did, I had a sense that I’ll be wasting so much time fixing unwanted problems that could’ve easily been avoided, and the last thing I want right now is unwanted drama

Ehh, you learn and you grow

Neither will I, I learned my lesson lah!

HOWEVER… THINKING A LITTLE BIT ABOUT IT, I have a new problem for later to work on. If the inventory pauses the game, this makes eating whilst in battle very easy, and this beats the entire idea of making it a challenge, so I can go in one of two ways:

  1. Unfreeze the time when the inventory is open
  2. Use the Abilities bar as a place as well to hold the food, that way we don’t have to pause anything

I’ll probably go with the second approach, and then I’ll debate on whether I want my food to be stackable or not

I’ll do this later, I’m currently working with the animal death state, to make sure you can’t mount dead animals. This is one troublesome state… :slight_smile:

Hey everyone! I’m working on merging the two courses ‘backwards’ in a way. Using the Third Person Combat as the base, and adding pieces of the RPG course on (and using Brian’s amazing guide as needed). I’ve just done adding Patrolling and Dwell states to the enemies, and saw an interesting bug (that had been present for a long time in the EnemyChasingState, I just never saw it).

In any state where we set the FreeLookSpeed hash or any animator float (I’m guessing here), and the game is paused in any way (inventory screen, pause menu etc), the animation gets ‘lost’ until the enemy changes state. See in the gif below, it begins to ‘slide’ instead of walking / running. This DOESN’T happen in the attacking state, and this is what makes me think it is to do with setting animator floats and then pausing the game with Time.timeScale. The logic of the states works fine, so I don’t think there is an issue there (and the animations work, as long as the state isn’t interrupted by a Time.timeScale = 0 call.

Here is what it looks like:
AnimationBug (2)(1)

Attempted fixes:

Use Time.unscaledDeltaTime - The animation keeps playing during the pause menu, and the enemy will still move toward player or waypoint

Caching animator float value - Fire events for OnPause and OnResume. OnPause caches the animator float value when the game is paused, and OnResume sets in when resumed. Didn’t work.

Any thoughts? Has anyone else seen this bug? Thanks to anyone who can take the time to help out with this :slight_smile:

Disregard, found the fix further up in this thread (not sure why it didn’t show up when I searched for key words earlier)!

Also just wanted to say @Brian_Trotter you have done amazing work here, going well and truly above and beyond!

1 Like

Privacy & Terms