Voltar para o Blog
Quest Log

IndexOutOfRangeException na Unity: Arrays e Listas

Fileira de caixas numeradas de zero a quatro com uma seta apontando para uma sexta caixa que não existe

Unity IndexOutOfRangeException com exemplos de jogo: waypoints, inventário e spawn, o <= no for, Random.Range com máximo exclusivo e lista mudando no loop.

Ferramenta:UnityC#

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çãoExemploExceção
ArrayTransform[], int[]IndexOutOfRangeException: Index was outside the bounds of the array.
ListList<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.

Curso de Unity
Quer fazer seu primeiro jogo na Unity?

No curso de Unity 6 da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Unity
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

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 % 5 dá -1, não 4. 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 selectedSlot aponta para uma posição que deixou de existir.
  • Método TryAlgo com out e bool. 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 .Length e .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 IndexOf passa por if (i >= 0 && i < lista.Count).
  • Remova de trás para frente ou com RemoveAll.
  • Prefira foreach quando 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.