Tabelas em Luau: array começa no 1, e é isso que quebra o seu laço
Tabela é o único tipo de coleção do Luau, e serve como array e como dicionário. Duas coisas derrubam quem vem de outra linguagem: o índice começa em 1, e remover item durante o laço reordena a tabela embaixo de você.
Produzido com IA a partir das fontes citadas abaixo, e conferido por checagens automáticas contra essas mesmas fontes. Nenhuma pessoa leu este texto antes da publicação. Política Editorial

Tabela é o único tipo de coleção do Luau. Não existe lista separada de dicionário, nem conjunto, nem tupla — existe tabela, e ela faz os três papéis dependendo de como você a usa.
Isso é simples de explicar e produz dois erros que todo mundo comete uma vez: o índice que começa em 1, e a remoção durante o laço, que reordena a tabela embaixo de você.
O problema
A definição oficial é generosa: a tabela guarda vários valores de qualquer tipo — booleano, número, texto, função e outras tabelas. Ela se constrói com chaves, {}.
E a mesma tabela serve como array ou como dicionário. A diferença não está no tipo; está no que você usa como índice. Array usa uma lista ordenada de números; dicionário aceita número, texto e objeto.
Essa flexibilidade é a força e a confusão. Uma tabela pode ser as duas coisas ao mesmo tempo, com valores em índices numéricos e outros em chaves de texto — o que funciona e quase sempre é sinal de que duas ideias diferentes foram misturadas no mesmo lugar.
A pergunta que organiza a escolha é uma só: a ordem importa, ou a busca importa? Se você precisa saber quem é o primeiro, quem é o próximo, quem entrou antes — array. Se você precisa saber se um item está lá, ou o valor associado a ele — dicionário.
A diferença não é de gosto, é de custo. Perguntar "este jogador está na lista?" a um array significa percorrer a lista inteira até achar — e se a resposta for não, percorrer tudo. Num dicionário, a mesma pergunta é uma consulta direta, independente do tamanho. Com dez itens ninguém nota; com trezentos, numa checagem que roda a cada quadro, nota-se muito.
E há uma consequência menos óbvia sobre clareza. Um array de jogadores não diz, sozinho, se a ordem dele significa alguma coisa. Quem lê o código depois não sabe se pode reordenar aquilo sem quebrar nada. Um dicionário diz na estrutura que a ordem não importa — e essa informação, escrita no tipo em vez de num comentário, é a que sobrevive.
Passo a passo
Array. Para criar, declaram-se os valores em ordem, separados por vírgula. Para ler, colchetes com o número da posição.
E aqui está o primeiro tropeço, dito literalmente na documentação: diferente de outras linguagens, o Luau indexa array a partir de 1. O primeiro item é o de índice [1], não [0].
Para escrever num índice, declara-se o número entre colchetes seguido de = e do valor. Isso vale tanto para sobrescrever quanto para criar uma posição nova.
Para percorrer, há duas formas. A iteração geral, que entrega índice e valor, e o laço numérico — como o índice é numérico, dá para ir de 1 até o comprimento do array, que se obtém com #.
Inserir. Existem duas formas embutidas de acrescentar no fim: o table.insert(), passando a tabela e o valor; e o índice do comprimento mais um, array[#array+1]. As duas fazem a mesma coisa; a segunda é mais rápida de ler quando já se está manipulando índices.
Para inserir no meio, passa-se a posição como segundo argumento do table.insert(). Isso empurra os itens seguintes uma posição para cima.
Remover. A remoção é pelo table.remove(), que tira o item da posição indicada e move os seguintes uma posição para trás. Guarde essa frase — ela é a próxima seção inteira.
Dicionário. É uma extensão do array: guarda pares de chave e valor, e a chave pode ser número, texto ou objeto. Para criar, define-se cada chave seguida de = e do valor.
O detalhe que mais rende no Roblox: a chave pode ser uma Instance. Um dicionário com o jogador como chave responde "este jogador está na lista?" sem percorrer nada. Para usar objeto como chave, declara-se a chave entre colchetes.
Para ler, colchetes com a chave. E chave de texto sem espaço pode ser indexada com ponto, o que costuma ser mais legível.
Artefato
O módulo abaixo é uma fila de jogadores, e ele existe para mostrar a decisão do começo deste texto aplicada: as duas formas, ao mesmo tempo, para coisas diferentes.
O array guarda a ordem — quem entrou antes sai antes. O dicionário, com a Instance do jogador como chave, responde "está na fila?" numa operação, em vez de percorrer a fila inteira a cada consulta. Manter os dois sincronizados custa duas linhas por operação e paga em toda consulta.
No meio dele estão as duas formas corretas de remover durante um percurso, que é o assunto seguinte.
Erro comum
O índice zero. Quem vem de qualquer linguagem popular escreve lista[0] sem pensar. O resultado é nil, e nil não é um erro — é um valor válido. O código segue com nil, e o erro aparece três linhas depois, apontando para o lugar errado.
O caso mais cruel é o laço for i = 0, #lista do, que roda uma vez a mais, começando num nil. Tratar nil com uma verificação faz o bug sumir e o laço continuar errado.
A remoção durante o laço. Este é o que sobrevive à experiência, porque funciona na maior parte dos testes.
O mecanismo está escrito na documentação: table.remove() tira o item da posição indicada e move os seguintes uma posição para trás. Então, num laço crescente, quando você remove o item da posição i, o item que estava em i+1 passa a estar em i — e o laço, no passo seguinte, vai para i+1, pulando o item que acabou de se mover.
O sintoma é característico e enganoso: com um item para remover, funciona. Com dois seguidos, um dos dois sobrevive. E como raramente se testa com dois itens adjacentes, o bug chega à produção e aparece como "às vezes fica alguém na fila".
Há duas correções, e o artefato traz as duas.
A primeira é percorrer de trás para a frente. O que é reordenado fica sempre atrás de onde o laço já passou, então nada é pulado. É a correção de uma linha.
A segunda é montar uma tabela nova com o que fica, em vez de tirar da antiga. Fica mais claro quando a condição é complicada, e não depende de ninguém lembrar do sentido do laço daqui a seis meses.
A mesma lógica vale para inserir durante o percurso, na direção oposta: a inserção no meio empurra os seguintes para cima, e o laço passa duas vezes pelo mesmo item.
A terceira armadilha é usar o # numa tabela que tem buracos. O operador de comprimento conta a sequência a partir de 1, e uma tabela em que alguém apagou o item do meio com array[3] = nil tem comportamento imprevisível. Remover é table.remove(), não atribuir nil.
Próximo passo
O exercício mais útil é olhar as suas tabelas existentes e perguntar, para cada uma, se a ordem importa.
É comum encontrar arrays que existem só para responder "está aqui?" — e cada consulta dessas é um laço inteiro. Trocar por um dicionário com a Instance ou o UserId como chave transforma uma varredura numa consulta direta, e o código fica mais curto no caminho.
O contrário também aparece: dicionários usados onde a ordem importava, e alguém acrescentou um campo posicao para compensar. Isso é um array pedindo para existir.
Vale também abrir os laços que percorrem tabela e conferir dois pontos: se algum deles remove ou insere no meio do percurso, e se algum começa em zero. Os dois são rápidos de encontrar lendo, e os dois produzem bug que não dá erro.
Um terceiro hábito que paga: quando um array e um dicionário descrevem a mesma coleção — como na fila do artefato —, mantenha os dois numa função só, que altera os dois juntos. Espalhar essa sincronia por vários lugares é como um dos dois fica desatualizado, e aí a fila diz que o jogador está nela enquanto a ordem já o tirou.
Um último hábito, pequeno e que evita retrabalho: quando uma tabela tem chave de texto, prefira nomes sem espaço. Chave sem espaço pode ser indexada com ponto, que é mais curto e mais legível, e ainda evita ter que lembrar quais chaves exigem colchetes e aspas e quais não.
Depois disso, vale conhecer a biblioteca table inteira, que a documentação indica em seguida. table.insert e table.remove são as duas mais usadas, e não são as únicas — e cada função dela que você conhece é um laço que não precisa escrever.
Fontes
- Tables | Documentation - Roblox Creator Hub — consultada em
Artefato pronto pra usar
-- ModuleScript: uma fila de jogadores, mostrando QUANDO usar cada forma
-- da mesma tabela -- e a armadilha da remoção durante o laço.
local Fila = {}
-- ARRAY: a ordem É o dado. Quem entrou antes sai antes.
local ordem = {}
-- DICIONÁRIO com a Instance como chave: responde "este jogador está na
-- fila?" sem percorrer nada. A documentação permite objeto como chave,
-- e é para isto que serve.
local presente = {}
function Fila.entrar(player)
if presente[player] then
return false
end
ordem[#ordem + 1] = player -- a segunda forma oficial de inserir no fim
presente[player] = true
return true
end
function Fila.estaNaFila(player)
-- O(1). Com array puro isto seria um laço a cada consulta.
return presente[player] == true
end
function Fila.posicaoDe(player)
for i, p in ordem do
if p == player then
return i -- lembre: a primeira posição é 1, não 0
end
end
return nil
end
function Fila.sair(player)
local i = Fila.posicaoDe(player)
if not i then
return false
end
table.remove(ordem, i) -- move os seguintes uma posição para TRÁS
presente[player] = nil
return true
end
-- ─────────────────────────────────────────────────────────────────
-- A ARMADILHA: remover DENTRO do laço
-- ─────────────────────────────────────────────────────────────────
-- ERRADO. `table.remove` puxa todo mundo uma casa para trás, e o índice
-- seguinte pula o item que acabou de ocupar a posição removida. Com dois
-- jogadores que saíram lado a lado, um dos dois FICA na fila.
--
-- for i = 1, #ordem do
-- if saiu(ordem[i]) then table.remove(ordem, i) end
-- end
--
-- CERTO, jeito 1: percorrer DE TRÁS PARA A FRENTE. O que é reordenado
-- fica sempre atrás de onde o laço já passou.
function Fila.limparDesconectados()
for i = #ordem, 1, -1 do
local player = ordem[i]
if not player.Parent then
table.remove(ordem, i)
presente[player] = nil
end
end
end
-- CERTO, jeito 2: montar uma tabela nova. Mais claro quando a condição
-- é complicada, e não depende de lembrar do sentido do laço.
function Fila.somenteVivos()
local nova = {}
for _, player in ordem do
local character = player.Character
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
if humanoid and humanoid.Health > 0 then
nova[#nova + 1] = player
end
end
return nova
end
function Fila.proximo()
local player = ordem[1]
if not player then
return nil
end
table.remove(ordem, 1)
presente[player] = nil
return player
end
return Fila


