Scripting Lua

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.

por Gustavo Cantino9 min de leituraIniciante

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

Placa tipográfica com a categoria Scripting Lua em destaque, o nível iniciante abaixo, e um campo de barras verticais verde-limão sobre fundo escuro.

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

Como corrigimos erros

Artefato pronto pra usar

Módulo de configuração e módulo com estado, mostrando o que o require compartilha (ReplicatedStorage)
lua
-- 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.
]]
Neste artigo

Continue por aqui