Hello hope you're doing well
I have a GameObject that adds and removes other valid GameObjects to a List as they drift through a collider trigger. Under certain conditions it will select an object in the List at random and replace it with another GameObject. It works fine for the most part but occasionally it will give an ArgumentOutOfRangeException error. Here is the code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BubbleScriptNecroGrab : MonoBehaviour
{
[SerializeField] public List<GameObject> bubbleOnes = new List<GameObject>();
[SerializeField] public List<GameObject> necroAcolytes = new List<GameObject>();
[SerializeField] GameObject activeBubble;
[SerializeField] GameObject necroAcolyte;
bool necroReady = true;
void Update()
{
if (bubbleOnes.Count > 0 && necroReady == true)
{
StartCoroutine(bubbleSignal());
}
}
// These two functions add and remove eligible bubbles from its associated list
// as they pass through the collider.
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.tag == "Bubble1")
{
GameObject other = collision.gameObject;
bubbleOnes.Add(other);
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if(collision.gameObject.tag == "Bubble1")
{
GameObject other = collision.gameObject;
bubbleOnes.Remove(other);
}
}
// When this is called, select a random bubble from the list, delete it and
// replace it with a prefab.
IEnumerator bubbleSignal()
{
necroReady = false;
yield return new WaitForSeconds(Random.Range(0.1f, 1.5f));
int randomList = Random.Range(0, bubbleOnes.Count -1); // adding -1 reduced amount of errors
Vector3 targetBubblePos = bubbleOnes[randomList].transform.position; // Code seems to break on this line
Destroy(bubbleOnes[randomList]);
GameObject necroAcolyteClone = Instantiate(necroAcolyte, targetBubblePos, Quaternion.identity);
necroAcolyteClone.GetComponent<AcolyteScript>().activeTarget = transform.parent.gameObject;
necroReady = true;
}
}
What I suspect is happening is as the bubbleSignal function operates, it selects a large or largest value just as it gets removed from the list by drifting out of the collider. How do I fix this?
CodePudding user response:
In general the -1
in
int randomList = Random.Range(0, bubbleOnes.Count -1);
does not do what you think.
The upper bound already is exclusive so this will never ever return the last index.
Beyond that I see nothing obviously wrong in your code (at this point) which would lead me to the assumption that bubbleOnes
is empty and therefore randomList = 0
but since there is no element at all bubbleOnes[randomList]
already throws the exception.
This could e.g. happen if OnTriggerExit2D
is called while there is only a single element in the list before the bubbleSignal
routine or more exactly the WaitForSeconds
within it finished. In that case you would remove the last element from the list, leave an empty list and then WaitForSeconds
would eventually finish and you reach your exception line with an empty list.
You should handle this case and do e.g.
IEnumerator bubbleSignal()
{
necroReady = false;
yield return new WaitForSeconds(Random.Range(0.1f, 1.5f));
if(bubbleOnes.Count > 0)
{
var randomList = Random.Range(0, bubbleOnes.Count);
var targetBubblePos = bubbleOnes[randomList].transform.position;
Destroy(bubbleOnes[randomList]);
var necroAcolyteClone = Instantiate(necroAcolyte, targetBubblePos, Quaternion.identity);
necroAcolyteClone.GetComponent<AcolyteScript>().activeTarget = transform.parent.gameObject;
}
necroReady = true;
}
In general in your use-case I would not use a Coroutine and poll-check in Update
whether to run it or not. Here it would be way more flexible to directly stay in Update
and do e.g.
[SerializeField] private float minTime = 0.1f;
[SerializeField] private float maxTime = 1.5f;
private float timer;
void Start()
{
timer = Random.Range(minTime, maxTime);
}
void Update()
{
if (bubbleOnes.Count > 0)
{
timer -= Time.deltaTime;
if(timer <= 0)
{
var randomList = Random.Range(0, bubbleOnes.Count);
var targetBubblePos = bubbleOnes[randomList].transform.position;
Destroy(bubbleOnes[randomList]);
var necroAcolyteClone = Instantiate(necroAcolyte, targetBubblePos, Quaternion.identity);
necroAcolyteClone.GetComponent<AcolyteScript>().activeTarget = transform.parent.gameObject;
timer = Random.Range(minTime, maxTime);
}
}
// [optionally]
// Instead of just continuing the counter from where it stopped last time
// you could also reset it while the list is empty
else
{
timer = Random.Range(minTime, maxTime);
}
}
This way you can be sure that there is no timing issue as all the code is only actually executed if the list is not empty in that moment.