ModuleScript: o require roda uma vez, e é isso que quase ninguém sabe
ModuleScript é como se reaproveita código no Roblox. A regra que explica metade dos bugs difíceis: o require roda o módulo uma vez só e devolve sempre a mesma referência — e no cliente e no servidor, uma para cada lado.
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

Depois de escrever alguns scripts, sempre chega o momento em que você quer usar o mesmo código em dois lugares. A resposta do Roblox para isso é o ModuleScript, e ele é simples de usar e fácil de usar errado — porque tem um comportamento que não é óbvio e que explica metade dos bugs difíceis de módulo.
O comportamento é este: o require roda o módulo uma vez só. Este guia começa pelo básico e termina nas consequências disso.
O problema
Sem módulo, código repetido é copiado. Copiar funciona até a primeira correção: você conserta num lugar e esquece do outro, e a partir daí o jogo tem duas versões da mesma regra discordando entre si.
O ModuleScript resolve isso de um jeito que atravessa até a fronteira cliente-servidor. Na definição oficial, ele permite reaproveitar código entre scripts dos dois lados da fronteira ou do mesmo lado — dependendo de onde o módulo estiver.
E é exatamente aí que mora a primeira decisão. Módulo pode ficar em qualquer lugar onde cabe script, mas a ReplicatedStorage é o local popular, porque guardar o módulo ali é o que permite reaproveitar código entre servidor e clientes.
A palavra que importa nessa frase é código, não dado. Guardar regras de cálculo na ReplicatedStorage é ótimo. Guardar ali o estado do jogo é entregar ao cliente uma cópia que ele pode ler — e, pior, achar que ele está escrevendo na sua.
Passo a passo
1. Crie o módulo. Um ModuleScript novo já nasce com o esqueleto: uma tabela vazia e um return. Esse código cria uma tabela Luau vazia e a devolve a qualquer script que faça require.
O valor devolvido pode ser qualquer tipo menos nil, mas a maioria dos módulos devolve função, tabela ou tabela de funções.
2. Prefira devolver uma tabela. Não é obrigatório — dá para devolver a função direto. A documentação recomenda a tabela mesmo assim, e dá dois motivos concretos: ela dá uma sintaxe fácil de entender na hora da chamada, e facilita acrescentar funções depois sem mudar como os outros scripts chamam.
Na prática: PickupManager.getPickupBonus("lendaria") se lê sozinho. PickupManager("lendaria") exige lembrar o que aquele módulo era.
3. Carregue com require. O carregamento é pela função require, passando o objeto do módulo.
No cliente, existe um detalhe que a documentação marca como medida de segurança importante: o padrão WaitForChild. Um script de cliente pode rodar antes de o objeto ter replicado, e sem a espera ele falha de forma intermitente — o pior tipo, porque funciona na sua máquina.
4. Use o mesmo módulo do outro lado. Guardado na ReplicatedStorage, o mesmo require funciona a partir do ServerScriptService. É esse o ganho que justifica a pasta.
5. Guarde dado no módulo, não em objeto de valor. Para associar dado a objeto, as alternativas são atributos ou pastas Configuration com objetos de valor. Elas funcionam para pouca coisa, mas a documentação é direta: as duas ficam problemáticas quando são dezenas de objetos ou de valores.
O argumento decisivo é de capacidade: atributo e objeto de valor não guardam tabela nem função. Módulo guarda. Uma configuração de arma com dano por parte do corpo é uma tabela aninhada — e isso não cabe num IntValue de jeito nenhum.
A regra que a documentação dá para decidir é sobre repetição: se você quer modificar o mesmo dado em várias cópias do mesmo objeto, ou reaproveitar o mesmo dado em objetos diferentes, guarde no módulo. Atributo é bom para o que é daquele objeto específico e só dele — a cor daquela porta, o número daquela sala. No instante em que o mesmo valor aparece em dois objetos, ele virou configuração, e configuração mora em módulo.
Há um ganho de operação que só aparece depois, e é o que convence de vez. Com o dado espalhado em atributos, mudar o dano de uma arma significa encontrar todos os objetos daquela arma no jogo e editar um por um — inclusive os que estão dentro de modelos, dentro de pastas, dentro de outros modelos. Com o dado no módulo, é uma linha. E, mais importante, é uma linha que aparece no histórico do repositório, com data e autor, em vez de uma mudança invisível na propriedade de um objeto que ninguém sabe dizer quando mudou.
Artefato
O módulo abaixo mostra os dois usos lado a lado, porque a diferença entre eles é a coisa que mais confunde no começo.
A primeira metade é configuração: constantes e uma função de cálculo. Todo mundo lê o mesmo, ninguém escreve, e não há o que dar errado.
A segunda metade é estado: uma tabela que os scripts modificam. Aí a regra do require deixa de ser curiosidade e passa a ser o comportamento central — e é o assunto da próxima seção.
Erro comum
Três erros, e o primeiro é o que dá nome a este texto.
"Cada script tem a sua cópia." Não tem. Quando você chama require num ModuleScript, ele roda uma vez e devolve um único item como referência. Chamar require de novo devolve exatamente a mesma referência — então uma tabela que você modificou continua modificada nas chamadas seguintes. O módulo em si não roda de novo.
Isso é ótimo quando se quer: dois scripts de servidor compartilham um contador sem encanamento nenhum. E é armadilha quando não se sabe: um módulo que guarda "o inimigo atual" acaba com um inimigo atual global, e dois sistemas que deveriam ser independentes começam a interferir um no outro por um caminho que não aparece em lugar nenhum do código.
A pergunta que resolve caso a caso: se dois lugares diferentes usarem este módulo ao mesmo tempo, eles deveriam ver o mesmo valor? Se sim, guarde no módulo. Se não, o módulo precisa devolver uma função que cria a coisa, não a coisa pronta.
"O cliente escreveu e o servidor não viu." Esta é a segunda metade da mesma regra, e pega quem já entendeu a primeira. Se você faz require de um ModuleScript dos dois lados da fronteira, ele devolve uma referência única para cada lado.
Ou seja: existem duas cópias vivas, uma no servidor e uma em cada cliente, e elas não se falam. Um valor escrito no cliente não aparece no servidor, e o contrário também não. Nada de errado acontece, nenhum erro é levantado — o número simplesmente não sobe do outro lado, e a pessoa passa horas procurando o bug no lugar errado.
A regra prática: módulo com estado que é do servidor mora em ServerStorage ou ServerScriptService. Aí o cliente nem consegue fazer require, e a ambiguidade deixa de existir. Na ReplicatedStorage ficam regras e constantes — coisas que não importa duplicar.
O require circular. A documentação avisa para não ter módulos fazendo require um do outro em círculo, o que produz o erro de módulo requerido recursivamente.
O erro é honesto, mas a causa é chata de achar, porque o círculo raramente é de dois. É A requer B, B requer C, C requer A — e nenhum dos três parece errado sozinho. O sinal de alerta é um módulo precisar de outro só para chamar uma função pequena: quase sempre essa função pertence a um terceiro módulo que ainda não existe.
A saída costuma ser extrair, não remendar. Se A e B precisam um do outro, existe uma coisa em comum entre os dois que quer ser um módulo C — e depois de extraída, A e B passam a depender de C sem depender um do outro. Mover o require para dentro da função, que é a gambiarra que resolve na hora, faz o erro sumir e deixa o ciclo de pé para a próxima pessoa achar.
Próximo passo
Com módulos no lugar, o padrão seguinte que a documentação apresenta resolve um incômodo real: evento próprio deixa scripts conversarem, mas rastrear referência a cada BindableEvent polui o código. Guardar os eventos dentro de um módulo, e expor funções para assinar e disparar, tira essa bagunça do caminho.
Antes disso, vale um exercício de arrumação. Abra o seu jogo e procure o mesmo número escrito em dois scripts diferentes — dano, preço, velocidade, tempo de recarga. Cada ocorrência repetida é uma constante que devia estar num módulo, e cada uma é um bug futuro esperando alguém consertar um lado só.
E ao mover, mova junto a lógica que usa o número. Um módulo que só tem constantes vira uma lista que ninguém lembra de atualizar; um módulo que tem a constante e a função que a aplica é o lugar óbvio para mexer quando a regra mudar.
Fontes
- Reuse code | Documentation - Roblox Creator Hub — consultada em
Artefato pronto pra usar
-- ReplicatedStorage > ModuleScript chamado "Economia"
--
-- Este arquivo mostra os DOIS usos de módulo e a diferença entre eles,
-- que é a coisa que mais confunde no começo:
--
-- CONFIGURAÇÃO -> só leitura. Todo mundo lê o mesmo, e tudo bem.
-- ESTADO -> escrita. Todo mundo escreve no MESMO lugar, e aí
-- é preciso saber exatamente quem tem acesso.
local Economia = {}
-- ─────────────────────────────────────────────────────────────────
-- 1. CONFIGURAÇÃO — o uso seguro
-- ─────────────────────────────────────────────────────────────────
-- Isto substitui atributos e pastas Configuration: guarda TABELA, que
-- objeto de valor não guarda, e fica tudo numa tela só.
Economia.MULTIPLICADOR_PADRAO = 1.25
Economia.RARIDADE = {
comum = 10,
incomum = 20,
rara = 50,
lendaria = 100,
}
function Economia.bonusDe(raridade)
local base = Economia.RARIDADE[raridade]
if not base then
warn("raridade desconhecida: " .. tostring(raridade))
return 0
end
return base * Economia.MULTIPLICADOR_PADRAO
end
-- ─────────────────────────────────────────────────────────────────
-- 2. ESTADO — o uso que exige atenção
-- ─────────────────────────────────────────────────────────────────
-- `total` é criado UMA VEZ, quando o módulo roda pela primeira vez.
-- Todo script do MESMO lado que fizer require recebe a mesma tabela,
-- então quem somar aqui soma para todos.
--
-- É por isso que isto é útil: dois scripts de servidor compartilham
-- contador sem inventar encanamento.
--
-- E é por isso que isto é perigoso: se este módulo estiver na
-- ReplicatedStorage, o CLIENTE também consegue fazer require dele. O
-- cliente ganha a PRÓPRIA cópia -- não a do servidor --, então nada
-- que ele escreva aqui chega ao servidor. Quem esperar o contrário vai
-- depurar um número que não sobe.
local total = {}
function Economia.creditar(userId, quanto)
total[userId] = (total[userId] or 0) + quanto
return total[userId]
end
function Economia.saldoDe(userId)
return total[userId] or 0
end
return Economia
--[[
COMO USAR, dos dois lados:
-- Script em ServerScriptService
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Economia = require(ReplicatedStorage:WaitForChild("Economia"))
print(Economia.bonusDe("lendaria"))
-- LocalScript
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Economia = require(ReplicatedStorage:WaitForChild("Economia"))
print(Economia.bonusDe("lendaria")) -- mesmo resultado: é só leitura
Economia.creditar(1, 100) -- NÃO chega ao servidor
O WaitForChild não é enfeite: no cliente, o objeto pode ainda não ter
replicado quando o script roda.
SE O ESTADO É SÓ DO SERVIDOR, o módulo deve morar em ServerStorage ou
ServerScriptService. Aí o cliente nem consegue fazer require, e a
ambiguidade acima deixa de existir.
]]


