Forum begins after the advertisement:


[Part 27] Possible Bug with function “SkipToRewards()” in UITreasureChest.cs

Home Forums Video Game Tutorial Series Creating a Rogue-like Shoot-em Up in Unity [Part 27] Possible Bug with function “SkipToRewards()” in UITreasureChest.cs

Viewing 12 posts - 1 through 12 (of 12 total)
  • Author
    Posts
  • #18721
    Grim Rubbish
    Level 24
    Silver Supporter (Patron)
    Helpful?
    Up
    0
    ::

    Hey folks, sometimes if you hit “ESC” to skip animation of the Treasure Chest too early, an error occurs:

    IndexOutOfRangeException: Index was outside the bounds of the array.
    UITreasureChest.SetupBeam (System.Int32 index) (at Assets/Scripts/TreasureChest/UITreasureChest.cs:195)
    UITreasureChest.SkipToRewards () (at Assets/Scripts/TreasureChest/UITreasureChest.cs:342)
    UITreasureChest.Update () (at Assets/Scripts/TreasureChest/UITreasureChest.cs:65)

    The animation is then locked in a loop, and the “DONE” button never appears. Does anyone has the same issue?

    #18723
    Terence
    Level 31
    Keymaster
    Helpful?
    Up
    1
    ::

    Will add this to the list of things to test for on stream tomorrow 🙏

    Bug Reporter
      1 anonymous person
    has upvoted this post.
    #18724
    Alp Apustaja
    Level 10
    AI Assistant
    (BETA)
    Helpful?
    Up
    0
    ::

    It looks like you’re encountering an IndexOutOfRangeException when you skip the animation of the Treasure Chest too early. This typically happens when your code tries to access an index in an array that doesn’t exist. Here are a few steps you can take to troubleshoot and potentially fix the issue:

    1. Check Array Length: Before accessing the array in the SetupBeam method, make sure to check that the index is within the bounds of the array. You can do this with an if statement:

      if (index >= 0 && index < yourArray.Length) {
          // Access yourArray[index]
      } else {
          Debug.LogError("Index out of bounds: " + index);
      }
    2. Handle Skip Logic: In your SkipToRewards method, make sure that you handle the case where the animation has already completed or is not in a state where it can be skipped. You might want to set a flag that indicates whether the animation is currently playing.

    3. Debugging: Add some debugging logs to track the flow of your code and the values of important variables. This can help you identify why the index is going out of range:

      Debug.Log("Current index: " + index);
    4. Initialization: Ensure that the array or list you are accessing is properly initialized before you attempt to access its elements. If the array is being populated dynamically, make sure that it has been filled with the expected number of elements before the animation is skipped.

    5. Review Animation States: Make sure that when you skip the animation, it properly transitions to the rewards state. If there's a state management system, ensure that it reflects the current status correctly.

    6. Testing: After making changes, thoroughly test the skipping feature to ensure that the error no longer occurs and that the "DONE" button appears as expected.

    If you continue to have issues, you might want to share the relevant parts of your code so that we can take a closer look and provide more specific advice.

    #18725
    Grim Rubbish
    Level 24
    Silver Supporter (Patron)
    Helpful?
    Up
    1
    ::

    Thanks Terence, I hope I’m not starting to annoy you with my constant bug reports… xD

    has upvoted this post.
    #18726
    Terence
    Level 31
    Keymaster
    Helpful?
    Up
    0
    ::

    No, the bug reports are great :) These are things I would like to address.

    #18727
    Grim Rubbish
    Level 24
    Silver Supporter (Patron)
    Helpful?
    Up
    1
    ::

    Great, there is another small bug in the “UITreasureChest.cs”:

    If you klick the “DONE” button while the VFX-Animations is still playing, the next time you open a chest the VFX will already play before you hit the “OPEN” button. But the fix is easy, just put “openingVFX.SetActive(false);” into the “public void CloseUI()” function and the problem is gone! :)

      1 anonymous person
    has upvoted this post.
    #18751
    Terence
    Level 31
    Keymaster
    Helpful?
    Up
    1
    ::

    Grim, can I trouble you to share your UITreasureChest Items array setup?

    View post on imgur.com

    And your code for the below issue:

    Great, there is another small bug in the “UITreasureChest.cs”:

    If you klick the “DONE” button while the VFX-Animations is still playing, the next time you open a chest the VFX will already play before you hit the “OPEN” button. But the fix is easy, just put “openingVFX.SetActive(false);” into the “public void CloseUI()” function and the problem is gone! :)

    If you have the time to record the video for any of the bugs, I will highly appreciate it too, as I’m unable to recreate the bugs here.

      1 anonymous person
    has upvoted this post.
    #18752
    Grim Rubbish
    Level 24
    Silver Supporter (Patron)
    Helpful?
    Up
    0
    ::

    Hey Terence, I checked eveything and it’s fine. Can you please post the latest “UITreasureChest.cs” and “TreasureChest.cs” for me, so I can compare them with my data? Thank you in advance! (I can post a short Video of the problem later).

    #18753
    Alp Apustaja
    Level 10
    AI Assistant
    (BETA)
    Helpful?
    Up
    0
    ::

    I’m sorry, but I can’t provide the specific files you’re asking for as I don’t have access to external databases or the latest versions of specific scripts. However, I can help you troubleshoot or suggest how you might structure your UITreasureChest.cs and TreasureChest.cs files based on common practices.

    If you’re looking for a way to compare your code with what might be in the latest versions, here are a few suggestions:

    1. Version Control: If you haven’t already, consider using a version control system like Git. This will allow you to track changes in your files and compare different versions easily.

    2. Code Review: If you have access to a community or a forum related to your project, you could ask fellow developers to share their versions or snippets for comparison.

    3. Common Structures: If you’d like, I can help you outline what typical methods and properties might look like in those scripts. For example, common elements in a treasure chest script could include methods for opening the chest, handling rewards, and managing animations.

    4. Debugging Help: If you have specific sections of your code that you’re concerned about, feel free to share them here, and I can help you analyze or improve them.

    Let me know how you’d like to proceed!

    #18754
    Grim Rubbish
    Level 24
    Silver Supporter (Patron)
    Helpful?
    Up
    0
    ::

    Terence, can you put this into the SkipToRewards() and check what it prints out?

            // Debugging: Check the counts of icons and items before the loop
            Debug.Log($"SkipToRewards called. Icons Count: {icons.Count}, Items Count: {items.Count}");
    
            // Ensure we're within bounds
            int count = Mathf.Min(icons.Count, items.Count);
            Debug.Log($"Number of items to process: {count}");

    This is what I get when I hit “ESC” (1 Item version):

    - SkipToRewards called. Icons Count: 2, Items Count: 5
    - Number of items to process: 2

    Is this the way it should be?

    #18757
    Terence
    Level 31
    Keymaster
    Helpful?
    Up
    1
    ::

    This is the print I have for the 5-item chest:

    SkipToRewards called. Icons Count: 5, Items Count: 5
    Number of items to process: 5

    Here’s my full UITreasureChest + TreasureChest script:

    using System.Collections;
    using System.Collections.Generic;
    using TMPro;
    using UnityEngine;
    using UnityEngine.UI;
    using UnityEngine.EventSystems;
    
    public class UITreasureChest : MonoBehaviour
    {
    
        public static UITreasureChest instance;
        PlayerCollector collector;
        TreasureChest currentChest;
        TreasureChestDropProfile dropProfile;
    
        [Header("Visual Elements")]
        public GameObject openingVFX;
        public GameObject beamVFX;
        public GameObject fireworks;
        public GameObject doneButton;
        public GameObject curvedBeams;
        public List<ItemDisplays> items;
        Color originalColor = new Color32(0x42, 0x41, 0x87, 255);
    
        [Header("UI Elements")]
        public GameObject chestCover;
        public GameObject chestButton;
    
        [Header("UI Components")]
        public Image chestPanel;
        public TextMeshProUGUI coinText;
        private float coins;
    
        // Internal states
        private List<Sprite> icons = new List<Sprite>();
        private bool isAnimating = false;
        private Coroutine chestSequenceCoroutine;
    
        //audio
        private AudioSource audiosource;
        public AudioClip pickUpSound;
    
        [System.Serializable]
        public struct ItemDisplays
        {
            public GameObject beam;
            public Image spriteImage;
            public GameObject sprite;
            public GameObject weaponBeam;
        }
    
        private void Update()
        {
            //only allow skipping of animation when animation is playing adn esc is pressed
            if (isAnimating && Input.GetButtonDown("Cancel"))
            {
                SkipToRewards();
            }
    
            if (Input.GetKeyDown(KeyCode.Return))
            {
                TryPressButton(chestButton);
                TryPressButton(doneButton);
            }
        }
    
        private void TryPressButton(GameObject buttonObj)
        {
            if (buttonObj.activeInHierarchy)
            {
                Button btn = buttonObj.GetComponent<Button>();
                if (btn != null && btn.interactable)
                {
                    btn.onClick.Invoke();
                }
            }
        }
    
        private void Awake()
        {
            audiosource = GetComponent<AudioSource>();
            gameObject.SetActive(false);
    
            // Ensure only 1 instance can exist in the scene
            if (instance != null && instance != this)
            {
                Debug.LogWarning("More than 1 UI Treasure Chest is found. It has been deleted.");
                Destroy(gameObject);
                return;
            }
    
            instance = this;
        }
    
        public static void Activate(PlayerCollector collector, TreasureChest chest) {
            if(!instance) Debug.LogWarning("No treasure chest UI GameObject found.");
    
            // Save the important variables.
            instance.collector = collector;
            instance.currentChest = chest;
            instance.dropProfile = chest.GetCurrentDropProfile();
            Debug.Log(instance.dropProfile);
    
            // Activate the GameObject.
            GameManager.instance.ChangeState(GameManager.GameState.TreasureChest);
            instance.gameObject.SetActive(true);
        }
    
        // VFX logic
        public IEnumerator Open()
        {
            //Trigger if hasFireworks beam is true
            if (dropProfile.hasFireworks)
            {
                isAnimating = false; //if there are fireworks ensure it can't be skipped
                StartCoroutine(FlashWhite(chestPanel, 5)); // or whatever UI element you want to flash
                fireworks.SetActive(true);
                yield return new WaitForSecondsRealtime(dropProfile.fireworksDelay);
            }
    
            isAnimating = true; //allow skipping of animations
    
            //Trigger if hasCurved beam is true
            if (dropProfile.hasCurvedBeams)
            {
                StartCoroutine(ActivateCurvedBeams(dropProfile.curveBeamsSpawnTime));
            }
    
            // Set the coins to be received.
            StartCoroutine(HandleCoinDisplay(Random.Range(dropProfile.minCoins, dropProfile.maxCoins)));
    
            DisplayerBeam(dropProfile.noOfItems);
            openingVFX.SetActive(true);
            beamVFX.SetActive(true);
    
            yield return new WaitForSecondsRealtime(dropProfile.animDuration); //time VFX will be active
            openingVFX.SetActive(false);
    
        }
    
        IEnumerator ActivateCurvedBeams(float spawnTime)
        {
            yield return new WaitForSecondsRealtime(spawnTime);
            curvedBeams.SetActive(true);
        }
    
        // logic for chest to flash
        private IEnumerator FlashWhite(Image image, int times, float flashDuration = 0.2f)
        {
            originalColor = image.color;
    
            //flashes the chest panel for x amount of times
            for (int i = 0; i < times; i++)
            {
                image.color = Color.white;
                yield return new WaitForSecondsRealtime(flashDuration);
    
                image.color = originalColor;
                yield return new WaitForSecondsRealtime(0.2f);
            }
        }
    
        public void DisplayerBeam(float noOfBeams)
        {
            int delayedStartIndex = Mathf.Max(0, (int)noOfBeams - dropProfile.delayedBeams); //ensure beams do not go out of index
    
            // Show immediate beams
            for (int i = 0; i < delayedStartIndex; i++)
            {
                SetupBeam(i);
            }
    
            // Delay the rest
            if (dropProfile.delayedBeams > 0)
                StartCoroutine(ShowDelayedBeams(delayedStartIndex, (int)noOfBeams));
    
            StartCoroutine(DisplayItems(noOfBeams));
        }
    
        // Display beams
        private void SetupBeam(int index)
        {
            items[index].weaponBeam.SetActive(true);
            items[index].beam.SetActive(true);
            items[index].spriteImage.sprite = icons[index];
            items[index].beam.GetComponent<Image>().color = dropProfile.beamColors[index];
        }
    
        // Display delayed beams
        private IEnumerator ShowDelayedBeams(int startIndex, int endIndex)
        {
            yield return new WaitForSecondsRealtime(dropProfile.delayTime);
    
            for (int i = startIndex; i < endIndex; i++)
            {
                SetupBeam(i);
            }
        }
    
        private IEnumerator DisplayItems(float noOfBeams)
        {
            yield return new WaitForSecondsRealtime(dropProfile.animDuration);
    
            if (noOfBeams == 5)
            {
                // Show first item
                items[0].weaponBeam.SetActive(false);
                items[0].sprite.SetActive(true);
                yield return new WaitForSecondsRealtime(0.3f);
    
                // Show second and third at the same time
                for (int i = 1; i <= 2; i++)
                {
                    items[i].weaponBeam.SetActive(false);
                    items[i].sprite.SetActive(true);
                }
                yield return new WaitForSecondsRealtime(0.3f);
    
                // Show fourth and fifth at the same time
                for (int i = 3; i <= 4; i++)
                {
                    items[i].weaponBeam.SetActive(false);
                    items[i].sprite.SetActive(true);
                }
                yield return new WaitForSecondsRealtime(0.3f);
            }
            else
            {
                // Fallback for other item counts — show normally one by one
                for (int i = 0; i < noOfBeams; i++)
                {
                    items[i].weaponBeam.SetActive(false);
                    items[i].sprite.SetActive(true);
                    yield return new WaitForSecondsRealtime(0.3f);
                }
            }
        }
    
        // Activates animations.
        public void Begin()
        {
            chestCover.SetActive(false);
            chestButton.SetActive(false);
            chestSequenceCoroutine = StartCoroutine(Open());
            audiosource.clip = dropProfile.openingSound;
            audiosource.Play();
    
        }
    
        //Give coins to player
        IEnumerator HandleCoinDisplay(float maxCoins)
        {
            coinText.gameObject.SetActive(true);
            float elapsedTime = 0;
            coins = maxCoins;
    
            //coin rolling up animation and will stop when it has reached maxcoins
            while (elapsedTime < maxCoins) 
            {
                elapsedTime += Time.unscaledDeltaTime * 20f;
                coinText.text = string.Format("{0:F2}", elapsedTime);
                yield return null;
            }
    
            //only activate the done button when coins reach max
            yield return new WaitForSecondsRealtime(2f);
            doneButton.SetActive(true);
        }
    
        public void CloseUI()
        {
            //Display Coins earned
            collector.AddCoins(coins);
    
            // Reset UI & VFX to initial state
            chestCover.SetActive(true);
            chestButton.SetActive(true);
            icons.Clear();
            beamVFX.SetActive(false);
            coinText.gameObject.SetActive(false);
            gameObject.SetActive(false);
            doneButton.SetActive(false);
            fireworks.SetActive(false);
            curvedBeams.SetActive(false);
            ResetDisplay();
    
            //reset audio
            audiosource.clip = pickUpSound;
            audiosource.time = 0f;
            audiosource.Play();
    
            isAnimating = false;
    
            GameManager.instance.ChangeState(GameManager.GameState.Gameplay);
        }
    
        // Display the icons of all the items received from the treasure chest.
        public static void NotifyItemReceived(Sprite icon)
        {
            // Includes a warning message informing the user of what the issue is if
            // we are unable to update this class with the icon.
            if(instance) instance.icons.Add(icon);
            else Debug.LogWarning("No instance of UITreasureChest exists. Unable to update treasure chest UI.");
        }
    
        // Reset the items display
        private void ResetDisplay()
        {
            foreach (var item in items)
            {
                item.beam.SetActive(false);
                item.sprite.SetActive(false);
                item.spriteImage.sprite = null;
    
            }
            dropProfile = null;
            icons.Clear();
        }
    
        private void SkipToRewards()
        {
            if (chestSequenceCoroutine != null)
                StopCoroutine(chestSequenceCoroutine);
    
            StopAllCoroutines(); // Halt all coroutines
    
            // Immediately show all beams and icons
            for (int i = 0; i < icons.Count; i++)
            {
                SetupBeam(i);
                items[i].weaponBeam.SetActive(false);
                items[i].sprite.SetActive(true);
            }
    
            // Immediately show coin value
            coinText.gameObject.SetActive(true);
            coinText.text = coins.ToString("F2");
            doneButton.SetActive(true);
            openingVFX.SetActive(false);
            isAnimating = false;
            chestPanel.color = originalColor;
    
            // Skip to the last 1 second of the audio
            if (audiosource != null && dropProfile.openingSound != null)
            {
                audiosource.clip = dropProfile.openingSound;
    
                float skipToTime = Mathf.Max(0, audiosource.clip.length - 3.55f); // Ensure it doesn't go below 0
                audiosource.time = skipToTime;
                audiosource.Play();
    
            }
    
            // Debugging: Check the counts of icons and items before the loop
            Debug.Log($"SkipToRewards called. Icons Count: {icons.Count}, Items Count: {items.Count}");
    
            // Ensure we're within bounds
            int count = Mathf.Min(icons.Count, items.Count);
            Debug.Log($"Number of items to process: {count}");
        }
    
    
    }
    using System.Collections;
    using System.Collections.Generic;
    using TMPro;
    using Unity.PlasticSCM.Editor.WebApi;
    using UnityEngine;
    
    public class TreasureChest : MonoBehaviour
    {
        [System.Flags]
        public enum DropType
        {
            NewPassive = 1, NewWeapon = 2, UpgradePassive = 4,
            UpgradeWeapon = 8, Evolution = 16
        }
        public DropType possibleDrops = (DropType)~0;
    
        public enum DropCountType { sequential, random }
        public DropCountType dropCountType = DropCountType.sequential;
        public TreasureChestDropProfile[] dropProfiles;
        public static int totalPickups = 0;
        int currentDropProfileIndex = 0;
    
        PlayerInventory recipient;
    
        // Get the number of rewards the treasure chest provides, retrieved
        // from the assigned drop profiles.
        private int GetRewardCount()
        {
            TreasureChestDropProfile dp = GetNextDropProfile();
            if(dp) return dp.noOfItems;
            return 1;
        }
    
        public TreasureChestDropProfile GetCurrentDropProfile() 
        {
            return dropProfiles[currentDropProfileIndex];
        }
    
        // Get a drop profile from a list of drop profiles assigned to the treasure chest.
        public TreasureChestDropProfile GetNextDropProfile()
        {
            if (dropProfiles == null || dropProfiles.Length == 0)
            {
                Debug.LogWarning("Drop profiles not set.");
                return null;
            }
    
            switch (dropCountType)
            {
                case DropCountType.sequential:
                    currentDropProfileIndex = Mathf.Clamp(
                        totalPickups, 0,
                        dropProfiles.Length - 1
                    );
                    break;
    
                case DropCountType.random:
    
                    float playerLuck = recipient.GetComponentInChildren<PlayerStats>().Actual.luck;
    
                    // Build list of profiles with computed weight
                    List<(int index, TreasureChestDropProfile profile, float weight)> weightedProfiles = new List<(int, TreasureChestDropProfile, float)>();
                    for (int i = 0; i < dropProfiles.Length; i++)
                    {
                        float weight = dropProfiles[i].baseDropChance * (1 + dropProfiles[i].luckScaling * (playerLuck - 1));
                        weightedProfiles.Add((i, dropProfiles[i], weight));
                    }
    
                    // Sort by weight ascending (smallest first)
                    weightedProfiles.Sort((a, b) => a.weight.CompareTo(b.weight));
    
                    // Compute total weight
                    float totalWeight = 0f;
                    foreach (var entry in weightedProfiles)
                        totalWeight += entry.weight;
    
                    // Random roll and cumulative selection
                    float r = Random.Range(0, totalWeight);
                    float cumulative = 0f;
                    foreach (var entry in weightedProfiles)
                    {
                        cumulative += entry.weight;
                        if (r <= cumulative)
                        {
                            currentDropProfileIndex = entry.index;
                            return entry.profile;
                        }
                    }
                    break;
            }
    
            return GetCurrentDropProfile();
        }
    
    
        private void OnTriggerEnter2D(Collider2D col)
        {
            if (col.TryGetComponent(out PlayerInventory p))
            {
                // Save the recipient and start up the UI.
                recipient = p;
    
                // Rewards will be given first.
                int rewardCount = GetRewardCount();
                for (int i = 0; i < rewardCount; i++)
                {
                    Open(p);
                }
                gameObject.SetActive(false);
    
                UITreasureChest.Activate(p.GetComponentInChildren<PlayerCollector>(), this);
    
                // Increment first, then wrap around if necessary
                totalPickups = (totalPickups + 1) % (dropProfiles.Length + 1);
            }
        }
    
        // Continue down the list until one returns.
        void Open(PlayerInventory inventory)
        {
            if (inventory == null) return;
    
            if (possibleDrops.HasFlag(DropType.Evolution) && TryEvolve<Weapon>(inventory)) return;
            if (possibleDrops.HasFlag(DropType.UpgradeWeapon) && TryUpgrade<Weapon>(inventory)) return;
            if (possibleDrops.HasFlag(DropType.UpgradePassive) && TryUpgrade<Passive>(inventory)) return;
            if (possibleDrops.HasFlag(DropType.NewWeapon) && TryGive<WeaponData>(inventory)) return;
            if (possibleDrops.HasFlag(DropType.NewPassive)) TryGive<PassiveData>(inventory);
        }
    
        // Try to evolve a random item in the inventory.
        T TryEvolve<T>(PlayerInventory inventory) where T : Item
        {
            // Loop through every evolvable item.
            T[] evolvables = inventory.GetEvolvables<T>();
            foreach (Item i in evolvables)
            {
                // Get all the evolutions that are possible.
                ItemData.Evolution[] possibleEvolutions = i.CanEvolve(0);
                foreach (ItemData.Evolution e in possibleEvolutions)
                {
                    // Attempt the evolution and notify the UI if successful.
                    if (i.AttemptEvolution(e, 0))
                    {
                        UITreasureChest.NotifyItemReceived(e.outcome.itemType.icon);
                        return i as T;
                    }
                }
            }
            return null;
        }
    
        // Try to upgrade a random item in the inventory.
        T TryUpgrade<T>(PlayerInventory inventory) where T : Item
        {
            // Gets all weapons in the inventory that can still level up.
            T[] upgradables = inventory.GetUpgradables<T>();
            if (upgradables.Length == 0) return null; // Terminate if no weapons.
    
            // Do the level up, and tell the treasure chest which item is levelled.
            T t = upgradables[Random.Range(0, upgradables.Length)];
            inventory.LevelUp(t);
            UITreasureChest.NotifyItemReceived(t.data.icon);
            return t;
        }
    
        // Try to give a new item to the inventory.
        T TryGive<T>(PlayerInventory inventory) where T : ItemData
        {
            // Get all new item possibilities.
            T[] possibilities = inventory.GetUnowned<T>();
            if (possibilities.Length == 0) return null;
    
    
            // Add a random possibility.
            T t = possibilities[Random.Range(0, possibilities.Length)];
            inventory.Add(t);
            UITreasureChest.NotifyItemReceived(t.icon);
            return t;
        }
    }
      1 anonymous person
    has upvoted this post.
    #18758
    Grim Rubbish
    Level 24
    Silver Supporter (Patron)
    Helpful?
    Up
    1
    ::

    Thank you Terence, i found the problem!

    for (int i = 0; i <= icons.Count; i++) has to be for (int i = 0; i < icons.Count; i++)

    There was a “=” causing this bug. Now it runs fine, thank you so much Terence! :)

    has upvoted this post.
Viewing 12 posts - 1 through 12 (of 12 total)
  • You must be logged in to reply to this topic.

Go to Login Page →


Advertisement below: