Scripting Lua

Atributos e ordem de replicação: por que o WaitForChild não é paranoia

Atributo é propriedade que você inventa, e é a forma mais limpa de configurar objeto sem script dentro dele. O que quase ninguém sabe: o motor não garante a ordem em que as coisas chegam ao cliente.

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.

Existe uma categoria de bug no Roblox com um sintoma característico: funciona no Studio, falha no jogo publicado, e some quando você põe um print no meio para investigar.

Quase sempre é a mesma coisa, e ela está escrita na documentação de propriedades e atributos, numa seção que o título não anuncia: o motor não garante a ordem em que objetos e mudanças são replicados do servidor para o cliente.

Este guia cobre atributos — que são a forma mais limpa de configurar objeto sem enfiar script dentro dele — e a ordem de replicação, que é o que faz o código de cliente funcionar ou não.

O problema

Primeiro a distinção. Propriedade faz parte da classe do objeto: o exemplo oficial é o Anchored da BasePart, que controla a física da parte. Ela existe porque o Roblox a definiu, e o motor sabe o que ela significa.

Atributo é essencialmente uma propriedade personalizada que você define. O projeto de referência Plant, citado na documentação, usa atributo para o preço da semente e para o tamanho máximo que um vaso comporta.

A diferença prática é quem dá sentido ao valor. O motor faz alguma coisa com Anchored; com Preco, quem faz alguma coisa é o seu código. E é isso que torna o atributo tão útil: ele deixa o dado de configuração junto do objeto, visível no Studio, editável por quem não programa, sem precisar de um IntValue escondido dentro de uma pasta.

O exemplo do projeto Plant mostra bem o tipo de dado que pede atributo. Preço da semente e tamanho máximo do vaso não são comportamento — são números que variam de objeto para objeto e que alguém vai querer ajustar sem abrir código. Vinte vasos diferentes com vinte tamanhos máximos não são vinte scripts: são vinte valores no painel de propriedades.

E há um ganho que só aparece quando o jogo tem mais de uma pessoa trabalhando nele. Atributo é editável por quem faz cenário, por quem faz balanceamento, por quem nunca vai abrir um .lua. O valor mora onde essa pessoa já está olhando, e ajustar o jogo deixa de depender de quem programa estar disponível.

O segundo problema é o que acontece quando esse dado viaja. E aqui a documentação é direta: o motor não garante a ordem de replicação. O exemplo que ela dá é preciso e assusta na primeira leitura — se um script de servidor muda uma propriedade e depois chama FireAllClients(), a mudança da propriedade pode chegar ao cliente antes ou depois de o OnClientEvent disparar.

Ou seja: o código do servidor fez A e depois B, e o cliente pode receber B antes de A.

Há um alívio parcial, e vale conhecer o limite dele: mudanças do mesmo tipo, como duas mudanças de atributo, geralmente chegam em ordem. Tipos diferentes, não.

Passo a passo

1. Crie os atributos no Studio. Dá para criar por script, mas a documentação diz qual é a prática comum: criar com valor padrão pela interface do Studio, e depois modificar por script em resposta à ação do jogador.

A ordem importa mais do que parece. Atributo criado na interface tem valor padrão desde o instante em que o objeto existe — inclusive na cópia que vai para o cliente. Atributo criado por script no meio da partida existe a partir daquele momento, e todo código que o leu antes leu nil.

2. Modifique com SetAttribute. A chamada leva nome e valor. Se o atributo ainda não existir, ele é criado ali.

3. Pegue a referência do objeto. Há dois caminhos. O mais simples é pôr o script como filho do objeto e usar script.Parent. O mais universal é pegar o objeto de um serviço com FindFirstChild ou WaitForChild.

Um detalhe que economiza confusão: com modelo, pode ser preciso escolher uma parte individual em vez do modelo em si — propriedade como Size ou Color é da parte, não do modelo.

4. Respeite a ordem de carregamento no cliente. A documentação descreve a sequência, e conhecê-la diz onde o WaitForChild é necessário e onde não é:

  1. O cliente carrega o conteúdo do ReplicatedFirst — tela de carregamento, assets, scripts. Scripts ali podem pegar objetos do próprio ReplicatedFirst sem WaitForChild. Mas não podem pegar de outros serviços, porque eles podem não ter carregado ainda. Dá para usar WaitForChild mesmo assim — e isso anula o benefício de estar no ReplicatedFirst, que é rodar cedo.
  2. O cliente continua carregando o resto. Quando termina, o evento Loaded dispara e o IsLoaded() passa a devolver verdadeiro.
  3. Rodam os LocalScripts de PlayerScripts, copiados de StarterPlayerScripts. Esses podem pegar objetos da ReplicatedStorage sem WaitForChild.
  4. O personagem nasce, e rodam os LocalScripts copiados de StarterCharacterScripts.

E uma ressalva que muda tudo para jogo grande: com streaming ligado, parte ou a maioria dos objetos pode não ter carregado na workspace — o que torna o WaitForChild uma medida de segurança ainda mais importante.

Artefato

O módulo abaixo embrulha os três padrões que a ordem de replicação exige, e a razão de serem três é que o problema tem três formas.

caminho() é WaitForChild encadeado com prazo e mensagem que diz onde parou. O prazo não é detalhe: WaitForChild sem prazo e com o nome errado trava o script para sempre, sem erro e sem funcionamento — o pior desfecho possível para depurar.

atributo() existe porque esperar o objeto não garante o valor. O objeto pode ter chegado antes do atributo dele, e é exatamente aí que se perde uma tarde.

quandoFor() é o padrão que a documentação prefere: em vez de supor que a mudança já replicou, reagir quando ela acontecer. Roda agora, se já houver valor, e de novo a cada mudança.

Erro comum

O erro comum é ler no cliente e acreditar no que veio.

O código parece defensivo — tem WaitForChild, tem verificação de nil — e ainda assim falha. O motivo é que a espera foi pelo objeto, não pelo valor. O objeto chegou, o WaitForChild voltou, o código seguiu, e GetAttribute devolveu nil porque o atributo ainda estava a caminho. A verificação de nil pega isso e transforma num comportamento padrão silencioso: a placa fica sem texto, a porta fica sem cor, o item fica sem preço. Nada quebra. Só está errado.

A recomendação oficial é a saída: detectar mudanças pelos métodos e eventos disponíveis, em vez de supor que a mudança já replicou. Na prática, usar GetAttributeChangedSignal em vez de ler uma vez e seguir a vida.

O segundo erro é o WaitForChild sem prazo. Ele é tão comum que vale repetir: sem o segundo argumento, ele espera para sempre. Com um nome digitado errado, o script morre parado ali — e o restante dele nunca roda. Como não há exceção, o output fica limpo, e a investigação começa no lugar errado.

O terceiro é assumir que o Studio prova alguma coisa sobre replicação. No Studio, no modo de um jogador só, tudo está local e tudo chega instantaneamente. A ordem que falha em produção é a ordem que nunca falha no seu teste — e é por isso que esse bug chega até os jogadores.

O quarto é usar FindFirstChild onde cabia WaitForChild. O FindFirstChild devolve nil na hora se o objeto ainda não chegou, e com streaming ligado isso é o caso normal, não a exceção. Ele é a ferramenta certa para "existe agora?", e a errada para "pegue quando chegar".

Próximo passo

O exercício útil é uma varredura: procure no seu código de cliente todo GetAttribute cujo resultado é usado imediatamente depois. Cada um é um candidato a virar GetAttributeChangedSignal.

Depois, vale migrar os IntValue e StringValue que sobraram dentro dos objetos para atributos. O ganho não é só de arrumação: atributo aparece no painel de propriedades do Studio, ao lado das propriedades de verdade, o que torna a configuração visível para quem abre o objeto — inclusive para você daqui a seis meses.

Vale também padronizar os nomes de atributo entre objetos do mesmo tipo. Preco em um e Custo em outro parece inofensivo até alguém escrever um script que percorre a pasta inteira — e aí metade dos objetos responde e a outra metade devolve nada, sem erro nenhum.

E, se o seu jogo usa streaming, vale rever as esperas com esse olho. A recomendação de WaitForChild passa de boa prática a requisito, porque o objeto distante não está "demorando": ele legitimamente não está lá.

Fontes

Como corrigimos erros

Artefato pronto pra usar

Acesso seguro a objeto e atributo no cliente, respeitando a ordem de replicação (ReplicatedStorage)
lua
-- ReplicatedStorage > ModuleScript "Aguardar"
--
-- O motor NÃO garante a ordem em que objetos e mudanças chegam ao
-- cliente. Este módulo embrulha os três padrões que isso exige, e cada
-- um existe por um motivo diferente:
--
--   caminho()  -> o objeto pode não ter chegado ainda
--   atributo() -> o objeto chegou, mas o VALOR pode não ter
--   quandoFor() -> nem esperar nem supor: reagir quando mudar
--
-- Vale no cliente. No servidor, tudo já existe.

local Aguardar = {}

local PRAZO = 10 -- segundos antes de desistir e avisar

-- WaitForChild encadeado, com prazo e mensagem que diz ONDE parou.
-- Sem o prazo, um nome errado trava o script para sempre em silêncio,
-- que é o pior desfecho possível: nem erro, nem funcionamento.
function Aguardar.caminho(raiz, ...)
	local atual = raiz
	local andado = raiz:GetFullName()

	for _, nome in { ... } do
		local proximo = atual:WaitForChild(nome, PRAZO)
		if not proximo then
			warn("nao apareceu em " .. PRAZO .. "s: " .. andado .. "." .. nome)
			return nil
		end
		atual = proximo
		andado = andado .. "." .. nome
	end

	return atual
end

-- O objeto pode ter chegado ANTES do atributo. Esperar o objeto não
-- garante o valor, e é exatamente aqui que se perde uma tarde.
function Aguardar.atributo(objeto, nome)
	local valor = objeto:GetAttribute(nome)
	if valor ~= nil then
		return valor
	end

	local pronto = false
	local conexao
	conexao = objeto:GetAttributeChangedSignal(nome):Connect(function()
		pronto = true
	end)

	local fim = os.clock() + PRAZO
	while not pronto and os.clock() < fim do
		task.wait()
	end
	conexao:Disconnect()

	if not pronto then
		warn("atributo nao chegou: " .. objeto:GetFullName() .. "." .. nome)
		return nil
	end
	return objeto:GetAttribute(nome)
end

-- O padrão PREFERIDO pela documentação: em vez de supor que a mudança
-- já replicou, reagir quando ela acontecer. Roda uma vez agora, se já
-- houver valor, e de novo a cada mudança.
function Aguardar.quandoFor(objeto, nome, callback)
	local atual = objeto:GetAttribute(nome)
	if atual ~= nil then
		task.spawn(callback, atual)
	end
	return objeto:GetAttributeChangedSignal(nome):Connect(function()
		callback(objeto:GetAttribute(nome))
	end)
end

return Aguardar

--[[
USO, num LocalScript:

  local ReplicatedStorage = game:GetService("ReplicatedStorage")
  local Aguardar = require(ReplicatedStorage:WaitForChild("Aguardar"))

  local placa = Aguardar.caminho(workspace, "Praca", "Placa")
  if placa then
     Aguardar.quandoFor(placa, "Texto", function(texto)
        placa.SurfaceGui.TextLabel.Text = texto or ""
     end)
  end

Com streaming ligado, `Praca` pode nem existir perto do jogador -- o
prazo expira e o aviso diz qual nome faltou, em vez de travar calado.
]]
Neste artigo

Continue por aqui