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
- This topic has 9 replies, 3 voices, and was last updated 1 week ago by
Grim Rubbish.
-
AuthorPosts
-
August 20, 2025 at 6:45 am #18721::
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?
August 20, 2025 at 12:56 pm #18723::Will add this to the list of things to test for on stream tomorrow 🙏
- 1 anonymous person
August 20, 2025 at 1:05 pm #18724::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:-
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 anif
statement:if (index >= 0 && index < yourArray.Length) { // Access yourArray[index] } else { Debug.LogError("Index out of bounds: " + index); }
-
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. -
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);
-
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.
-
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.
-
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.
August 20, 2025 at 2:35 pm #18725August 20, 2025 at 3:35 pm #18726August 20, 2025 at 4:20 pm #18727::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
August 21, 2025 at 5:02 pm #18751::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
August 21, 2025 at 5:47 pm #18752::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).
August 21, 2025 at 5:50 pm #18753::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
andTreasureChest.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:
-
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.
-
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.
-
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.
-
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!
August 21, 2025 at 6:31 pm #18754::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?
August 21, 2025 at 9:56 pm #18757::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
August 21, 2025 at 11:27 pm #18758 -
-
AuthorPosts
- You must be logged in to reply to this topic.
Advertisement below: