IndexOutOfRangeException na Unity: Arrays e Listas
A Unity IndexOutOfRangeException acontece quando o código pede uma posição que o array não tem: o índice 5 num array de 5 itens (que vai de 0 a 4), um índice negativo, ou qualquer posição num array vazio. Com List<T> o mesmo erro aparece com outro nome, ArgumentOutOfRangeException. A correção quase sempre está numa destas quatro linhas: o <= no for, o Random.Range com o máximo errado, uma lista vazia que ninguém checou, ou um item removido no meio do loop.
Abaixo, cada caso num exemplo de jogo real (patrulha por waypoints, inventário e spawn aleatório), com código C# para o Unity 6.3 LTS.
O que significa IndexOutOfRangeException
Array e lista guardam itens em posições numeradas a partir do zero. Um array com 5 waypoints tem as posições 0, 1, 2, 3 e 4. O tamanho é 5, mas o último índice é 4. Essa diferença de um, o "erro por um" (off-by-one), é a origem de quase todos os casos.
No Console, o erro aparece assim:
IndexOutOfRangeException: Index was outside the bounds of the array.
EnemyPatrol.Update () (at Assets/Scripts/EnemyPatrol.cs:21)
A segunda linha é a que importa: o script e a linha onde o acesso falhou. Dois cliques na mensagem abrem o arquivo nesse ponto. Nessa linha existe um algo[indice], e a pergunta é sempre a mesma: quanto valia indice e qual o tamanho de algo naquele momento?
Para descobrir, coloque um log logo antes da linha:
Debug.Log($"index={currentIndex} length={waypoints.Length}", this);
Em geral o log mostra index=5 length=5 ou index=0 length=0, e o problema fica óbvio.
Se o erro só acontece de vez em quando, o log vira uma enxurrada. Nesse caso use o depurador: com a IDE conectada à Unity (no Visual Studio, Attach to Unity; no Rider e no VS Code, a opção equivalente de anexar ao editor), coloque um breakpoint na linha do erro com a condição currentIndex >= waypoints.Length. O jogo para só no momento exato do problema, e você vê o valor de todas as variáveis e a pilha de chamadas que levou até ali.
Array vs List: as duas mensagens
O mesmo erro de lógica gera mensagens diferentes conforme a coleção:
| Coleção | Exemplo | Exceção |
|---|---|---|
| Array | Transform[], int[] | IndexOutOfRangeException: Index was outside the bounds of the array. |
| List | List<Item> | ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. |
A escolha entre os dois também evita erro. Array tem tamanho fixo: criou com 5, fica com 5, e é ótimo para coisas configuradas no Inspector que não mudam durante o jogo (pontos de spawn, waypoints, sons de passo). List<T> cresce e encolhe com Add e Remove, e serve para o que muda em tempo de jogo (inimigos vivos, itens do inventário, projéteis). Usar array para algo que muda leva a gambiarras com índice, e é aí que o erro aparece.
Também muda o nome do tamanho: array usa .Length, lista usa .Count. Trocar um pelo outro não compila, então pelo menos esse erro o compilador pega antes de você.
Um detalhe que engana: um array público ou [SerializeField] é preenchido pelo Inspector. Se o código diz que o array tem 4 elementos e o Inspector mostra 3, vale o Inspector. Confira o campo Size do componente na cena, e não só o prefab, porque a cena pode ter sobrescrito o valor.
O erro do <= no for
O caso clássico:
for (int i = 0; i <= waypoints.Length; i++)
{
Debug.Log(waypoints[i].position);
}
Com 5 waypoints, o loop roda com i de 0 a 5. No i = 5 o array não tem a posição e o erro estoura. A condição certa é <: i < waypoints.Length.
O mesmo erro aparece fora do for, na patrulha. O inimigo avança para o próximo ponto com currentIndex++ e, quando passa do último, quebra. A correção é voltar ao começo com o operador de resto (%).
Arquivo EnemyPatrol.cs:
using UnityEngine;
public class EnemyPatrol : MonoBehaviour
{
[SerializeField] private Transform[] waypoints;
[SerializeField] private float speed = 3f;
[SerializeField] private float arriveDistance = 0.1f;
private int currentIndex;
private void Update()
{
if (waypoints == null || waypoints.Length == 0)
{
return;
}
Vector3 target = waypoints[currentIndex].position;
transform.position = Vector3.MoveTowards(transform.position, target, speed * Time.deltaTime);
if (Vector3.Distance(transform.position, target) <= arriveDistance)
{
currentIndex = (currentIndex + 1) % waypoints.Length;
}
}
}
O % waypoints.Length faz o índice dar a volta: com 5 pontos, depois do 4 vem o 0. E o return no começo protege o caso de array vazio, que é a outra metade dos erros.
Random.Range com int: o máximo é exclusivo
O Random.Range tem duas versões, e elas se comportam diferente no máximo. A documentação do Unity 6.3 diz que, na versão com int, o máximo é exclusivo: Random.Range(0, 5) devolve 0, 1, 2, 3 ou 4, nunca 5. Na versão com float, o máximo é inclusivo.
Isso é uma vantagem para sortear índice: Random.Range(0, array.Length) já devolve sempre uma posição válida. O erro aparece quando alguém "corrige" para incluir o último:
// Errado: pode sortear spawnPoints.Length, que não existe.
int index = Random.Range(0, spawnPoints.Length + 1);
// Errado de outro jeito: nunca sorteia o último ponto.
int index2 = Random.Range(0, spawnPoints.Length - 1);
Arquivo RandomSpawner.cs:
using UnityEngine;
public class RandomSpawner : MonoBehaviour
{
[SerializeField] private Transform[] spawnPoints;
[SerializeField] private GameObject enemyPrefab;
public void SpawnEnemy()
{
if (spawnPoints.Length == 0)
{
Debug.LogWarning("RandomSpawner sem pontos de spawn configurados.", this);
return;
}
int index = Random.Range(0, spawnPoints.Length);
Transform point = spawnPoints[index];
Instantiate(enemyPrefab, point.position, point.rotation);
}
}
Outro detalhe da documentação: se o mínimo e o máximo forem iguais, o método devolve o mínimo. Random.Range(0, 0) devolve 0, e com array vazio esse 0 vira o índice inválido. Daí o if antes do sorteio.
Esses erros de índice são pequenos, mas param o jogo inteiro, e quando você junta inventário, spawn, IA e UI no mesmo projeto, eles se multiplicam. No curso de Unity você constrói um jogo completo com essas estruturas e aprende a pensar nos casos de borda antes de o Console reclamar.
Lista vazia e índice negativo
O inventário é o lugar onde lista vazia mais aparece. O jogador aperta a tecla de usar item com o inventário vazio, e o código faz items[selectedSlot].
Arquivo Inventory.cs:
using System.Collections.Generic;
using UnityEngine;
public class Inventory : MonoBehaviour
{
private readonly List<string> items = new List<string>();
private int selectedSlot;
public void Add(string itemId)
{
items.Add(itemId);
}
public void SelectNext()
{
if (items.Count == 0)
{
selectedSlot = 0;
return;
}
selectedSlot = (selectedSlot + 1) % items.Count;
}
public void SelectPrevious()
{
if (items.Count == 0)
{
selectedSlot = 0;
return;
}
selectedSlot = (selectedSlot - 1 + items.Count) % items.Count;
}
public bool TryUseSelected(out string itemId)
{
if (selectedSlot < 0 || selectedSlot >= items.Count)
{
itemId = null;
return false;
}
itemId = items[selectedSlot];
items.RemoveAt(selectedSlot);
if (selectedSlot >= items.Count)
{
selectedSlot = Mathf.Max(items.Count - 1, 0);
}
return true;
}
}
Três pontos de atenção:
- Voltar para trás com
%precisa do+ items.Count. Em C#,-1 % 5dá-1, não4. Somar o tamanho antes do resto garante um número positivo. - Depois de remover, ajuste a seleção. Se o jogador usou o último item da lista, o
selectedSlotaponta para uma posição que deixou de existir. - Método
TryAlgocomoutebool. Quem chama sabe se deu certo sem precisar de exceção.
Índice negativo também aparece em busca: List.IndexOf e Array.IndexOf devolvem -1 quando não acham o item. Usar esse resultado direto como índice gera o erro. Cheque if (index >= 0) antes.
Removendo itens durante o loop
Remover de uma lista enquanto percorre com foreach lança outra exceção:
InvalidOperationException: Collection was modified; enumeration operation may not execute.
Com for indo para frente, o erro é mais sutil: ao remover o item i, todos os seguintes descem uma posição, o loop pula um item e, no fim, pode ler fora do tamanho se você guardou o Count numa variável antes.
Duas formas certas, arquivo ProjectileManager.cs:
using System.Collections.Generic;
using UnityEngine;
public class ProjectileManager : MonoBehaviour
{
[SerializeField] private float maxDistance = 30f;
private readonly List<Transform> projectiles = new List<Transform>();
public void Register(Transform projectile)
{
projectiles.Add(projectile);
}
private void Update()
{
for (int i = projectiles.Count - 1; i >= 0; i--)
{
Transform projectile = projectiles[i];
if (projectile == null || projectile.position.magnitude > maxDistance)
{
if (projectile != null)
{
Destroy(projectile.gameObject);
}
projectiles.RemoveAt(i);
}
}
}
public void ClearDestroyed()
{
projectiles.RemoveAll(p => p == null);
}
}
Percorrer de trás para frente funciona porque remover o item i só mexe nas posições depois dele, que o loop já visitou. O RemoveAll é a versão de uma linha quando você só precisa filtrar. E repare no projectile == null: um projétil destruído por outro script continua na lista até alguém tirar, e acessar ele daria outro erro, o de objeto destruído.
Grid em array 2D: o erro que vem da posição no mundo
Jogo de grid (puzzle, tática, construção) costuma guardar o mapa num array de duas dimensões, int[,], e converter a posição do mundo em célula. É aqui que aparece o índice negativo sem ninguém ter escrito um -1: o jogador clica fora do tabuleiro, à esquerda da origem, e a conversão devolve x = -1.
Arquivo GridMap.cs:
using UnityEngine;
public class GridMap : MonoBehaviour
{
[SerializeField] private int width = 10;
[SerializeField] private int height = 8;
[SerializeField] private float cellSize = 1f;
private int[,] cells;
private void Awake()
{
cells = new int[width, height];
}
public bool TryGetCell(Vector3 worldPosition, out Vector2Int cell)
{
Vector3 local = worldPosition - transform.position;
cell = new Vector2Int(Mathf.FloorToInt(local.x / cellSize), Mathf.FloorToInt(local.y / cellSize));
return cell.x >= 0 && cell.x < cells.GetLength(0)
&& cell.y >= 0 && cell.y < cells.GetLength(1);
}
public void SetValue(Vector3 worldPosition, int value)
{
if (TryGetCell(worldPosition, out Vector2Int cell))
{
cells[cell.x, cell.y] = value;
}
}
}
Dois detalhes. Em array 2D, .Length é o total de células (80 aqui), não a largura: use GetLength(0) para a primeira dimensão e GetLength(1) para a segunda. E use Mathf.FloorToInt, não um cast (int), para converter: o cast arredonda em direção ao zero, então -0.5 vira 0 e o clique fora do tabuleiro cai na primeira coluna sem erro, que é um bug pior porque é silencioso.
Como prevenir com checagens simples
- Use
<com.Lengthe.Count, nunca<=. - Cheque vazio antes de acessar ou sortear:
if (array.Length == 0) return;. - Dê a volta com
%em patrulha, seleção de slot e ciclos de arma. - Não confie em índice vindo de fora. Valor do Inspector, de save ou de
IndexOfpassa porif (i >= 0 && i < lista.Count). - Remova de trás para frente ou com
RemoveAll. - Prefira
foreachquando só vai ler. Sem índice, não tem como errar o índice.
Se você precisa de uma base mais firme em loops e condições antes de voltar ao código de jogo, o post de lógica de programação para jogos cobre isso sem depender de engine.
Próximo passo
Pegue o script do seu projeto que mais mexe com array ou lista (spawner, inventário, patrulha) e passe as checagens acima nele, uma por uma. Depois, force os casos de borda de propósito: inventário vazio, um só waypoint, zero pontos de spawn. Se nada estoura, o código está pronto. Se você também usa Godot, compare com o post de arrays e listas em GDScript: o erro por um é igual nas duas engines, só muda a mensagem.
Perguntas frequentes
O que significa Index was outside the bounds of the array na Unity?
Significa que o código tentou ler ou escrever uma posição que o array não tem. Um array de tamanho 5 tem os índices de 0 a 4; pedir o índice 5, ou qualquer número negativo, gera a IndexOutOfRangeException.
Qual a diferença entre IndexOutOfRangeException e ArgumentOutOfRangeException?
A causa é a mesma, índice fora do tamanho, mas o tipo da coleção muda a mensagem. Array (int[], Transform[]) lança IndexOutOfRangeException. List<T> lança ArgumentOutOfRangeException, porque o índice é tratado como argumento do indexador da lista.
O Random.Range com int inclui o valor máximo?
Não. Na versão com int, a documentação da Unity diz que o máximo é exclusivo: Random.Range(0, 5) devolve de 0 a 4. Por isso Random.Range(0, array.Length) é o jeito certo de sortear um índice. Na versão com float, o máximo é inclusivo.
Posso remover itens de uma lista dentro de um foreach?
Não. Modificar a lista durante o foreach lança InvalidOperationException (Collection was modified). Percorra a lista de trás para frente com um for, ou use list.RemoveAll com uma condição.



