Plant é uma experiência de referência onde os jogadores plantam e regam sementes, para que eles possam mais tarde colher e vender as plantas resultantes.

O projeto foca em casos de uso comuns que você pode encontrar ao desenvolver uma experiência no Roblox. Onde aplicável, você encontrará notas sobre trade-offs, compromissos e a racionalidade de várias escolhas de implementação, para que você possa tomar a melhor decisão para suas próprias experiências.
Obter o arquivo
- Navegue até a página da experiência Plant.
- Clique no botão ⋯ e Editar no Studio.
Casos de uso
Plant abrange os seguintes casos de uso:
- Persistência de dados de sessão e dados de jogador
- Gerenciamento de visualização da IU
- Rede cliente-servidor
- Experiência de Primeiro Usuário (FTUE)
- Compras de moeda dura e suave
Além disso, este projeto resolve conjuntos de problemas mais restritos que são aplicáveis a muitas experiências, incluindo:
- Personalização de uma área no lugar que está associada a um jogador
- Gerenciamento da velocidade de movimento do personagem do jogador
- Criação de um objeto que segue os personagens
- Detectar em que parte do mundo um personagem está
Note que há vários casos de uso nesta experiência que são muito pequenos, muito nichados ou não demonstram uma solução para um desafio de design interessante; estes não estão cobertos.
Estrutura do projeto
A primeira decisão ao criar uma experiência é decidir como estruturar o projeto, que inclui principalmente onde colocar instâncias específicas no modelo de dados e como organizar e estruturar pontos de entrada para o código tanto do cliente quanto do servidor.
Modelo de dados
A seguinte tabela descreve em quais serviços de contêiner no modelo de dados as instâncias são colocadas.
| Serviço | Tipos de instâncias |
|---|---|
| Workspace | Contém modelos estáticos representando o mundo 3D, especificamente partes do mundo que não pertencem a nenhum jogador. Você não precisa criar, modificar ou destruir dinamicamente essas instâncias em tempo de execução, então é aceitável deixá-las aqui. Há também uma Folder vazia, na qual os modelos de fazenda dos jogadores serão adicionados em tempo de execução. |
| Lighting | Efeitos atmosféricos e de iluminação. |
| ReplicatedFirst | Contém o menor subconjunto possível de instâncias necessárias para exibir a tela de carregamento e inicializar a experiência. Quanto mais instâncias estão colocadas em ReplicatedFirst, mais longa será a espera para que elas se replicarem antes que o código em ReplicatedFirst possa ser executado.
|
| ReplicatedStorage | Serve como um contêiner de armazenamento para todas as instâncias para as quais o acesso é necessário tanto no cliente quanto no servidor.
|
| ServerScriptService | Contém um Script servindo como o ponto de entrada para todo o código do lado do servidor no projeto. |
| ServerStorage | Serve como um contêiner de armazenamento para todas as instâncias que não precisam ser replicadas para o cliente.
|
| SoundService | Contém os objetos Sound usados para efeitos sonoros na experiência. Sob SoundService, esses objetos Sound não têm posição e não são simulados no espaço 3D. |
Pontos de entrada
A maioria dos projetos organiza o código dentro de ModuleScripts reutilizáveis que podem ser importados em toda a base de código. ModuleScripts são reutilizáveis, mas não são executados por conta própria; eles precisam ser importados por um Script ou LocalScript. Muitos projetos Roblox terão um grande número de objetos Script e LocalScript, cada um relacionado a um comportamento ou sistema particular na experiência, criando múltiplos pontos de entrada.
Para o microjogo Plant, uma abordagem diferente é implementada através de um único LocalScript que é o ponto de entrada para todo o código do cliente, e um único Script que é o ponto de entrada para todo o código do servidor. A abordagem correta para seu projeto depende de seus requisitos, mas um único ponto de entrada proporciona maior controle sobre a ordem em que os sistemas são executados.
As seguintes listas descrevem os trade-offs de ambas as abordagens:
- Um único Script e um único LocalScript cobrem o código do servidor e do cliente, respectivamente.
- Maior controle sobre a ordem em que diferentes sistemas são iniciados porque todo o código é inicializado a partir de um único script.
- Pode passar objetos por referência entre sistemas.
Arquitetura de sistemas de alto nível
Os sistemas de nível superior no projeto estão detalhados abaixo. Alguns desses sistemas são substancialmente mais complexos do que outros, e em muitos casos sua funcionalidade é abstraída em uma hierarquia de outras classes.

Cada um desses sistemas é um "singleton", na medida em que é uma classe não instanciável que é inicializada pelo script start do cliente ou servidor relevante. Você pode ler mais sobre o padrão singleton mais adiante neste guia.
Servidor
Os seguintes sistemas estão associados ao servidor.
| Sistema | Descrição |
|---|---|
| Rede |
|
| PlayerDataServer |
|
| Mercado |
|
| CollisionGroupManager |
|
| FarmManagerServer |
|
| PlayerObjectsContainer |
|
| TagPlayers |
|
| FtueManagerServer |
|
| CharacterSpawner |
|
Cliente
Os seguintes sistemas estão associados ao cliente.
| Sistema | Descrição |
|---|---|
| Rede |
|
| PlayerDataClient |
|
| MarketClient |
|
| LocalWalkJumpManager |
|
| FarmManagerClient |
|
| UISetup |
|
| FtueManagerClient |
|
| CharacterSprint |
|
Comunicação cliente-servidor
A maioria das experiências Roblox envolve algum elemento de comunicação entre o cliente e o servidor. Isso pode incluir o cliente solicitando que o servidor realize uma determinada ação e o servidor replicando atualizações para o cliente.
Neste projeto, a comunicação cliente-servidor é mantida o mais genérica possível, limitando o uso de objetos RemoteEvent e RemoteFunction a fim de diminuir a quantidade de regras especiais a serem acompanhadas. Este projeto usa os seguintes métodos, em ordem de preferência:
- Replicação via o sistema de dados do jogador.
- Replicação via atributos.
- Replicação via tags.
- Mensagem diretamente através do módulo Rede.
Replicação via sistema de dados do jogador
O sistema de dados do jogador permite que dados sejam associados ao jogador que persistem entre sessões de salvamento. Este sistema fornece replicação do cliente para o servidor e um conjunto de APIs que podem ser usadas para consultar dados e assinar mudanças, tornando-o ideal para replicar alterações ao estado do jogador do servidor para o cliente.
Por exemplo, em vez de disparar um RemoteEvent customizado UpdateCoins para informar ao cliente quantas moedas ele tem, você pode chamar o seguinte e deixar o cliente assinar isso via o evento PlayerDataClient.updated.
PlayerDataServer.setValue(player, "coins", 5)
Claro, isso é útil apenas para replicação do servidor para o cliente e para valores que você deseja persistir entre sessões, mas isso se aplica a um número surpreendente de casos no projeto, incluindo:
- O estágio atual do FTUE
- O inventário do jogador
- A quantidade de moedas que o jogador tem
- O estado da fazenda do jogador
Replicação via atributos
Em situações onde o servidor precisa replicar um valor customizado para o cliente que é específico para uma determinada Instance, você pode usar atributos. O Roblox replica automaticamente os valores dos atributos, então você não precisa manter caminhos de código para replicar estados associados a um objeto. Outra vantagem é que essa replicação ocorre junto com a própria instância.
Isso é particularmente útil para instâncias criadas em tempo de execução, pois os atributos definidos em uma nova instância antes de ser parentada ao modelo de dados serão replicados atômicos com a própria instância. Isso evita a necessidade de escrever código para "aguardar" que dados extras sejam replicados via um RemoteEvent ou StringValue.
Você também pode ler diretamente atributos do modelo de dados, tanto do cliente quanto do servidor, com o método GetAttribute() e se inscrever em mudanças com o método GetAttributeChangedSignal(). No projeto Plant, essa abordagem é usada para, entre outras coisas, replicar o status atual das plantas para os clientes.
Replicação via tags
CollectionService permite que você aplique uma tag de string a um Instance. Isso é útil para categorizar instâncias e replicar essa categorização para o cliente.
Por exemplo, a tag CanPlant é aplicada no servidor para sinalizar ao cliente que um determinado vaso pode receber uma planta.
Mensagem diretamente via módulo de rede
Para situações onde nenhuma das opções anteriores se aplica, você pode usar chamadas de rede personalizadas através do módulo Rede. Esta é a única opção no projeto que permite comunicação cliente-servidor e, portanto, é mais útil para transmitir solicitações do cliente e receber uma resposta do servidor.
Plant usa chamadas de rede diretas para uma variedade de solicitações do cliente, incluindo:
- Regar uma planta
- Plantar uma semente
- Comprar um item
A desvantagem desta abordagem é que cada mensagem individual requer alguma configuração personalizada, o que pode aumentar a complexidade do projeto, embora isso tenha sido evitado sempre que possível, particularmente para comunicação do servidor para o cliente.
Classes e singletons
Classes no projeto Plant, assim como instâncias no Roblox, podem ser criadas e destruídas. Sua sintaxe de classe é inspirada na abordagem idiomática do Lua ao programação orientada a objetos com várias mudanças para habilitar suporte a verificação de tipos estrita.
Instanciação
Muitas classes no projeto estão associadas a uma ou mais Instances. Objetos de uma determinada classe são criados utilizando um método new(), consistente com a forma como as instâncias são criadas no Roblox usando Instance.new().
Esse padrão é geralmente usado para objetos onde a classe tem uma representação física no modelo de dados, e a classe estende sua funcionalidade. Um bom exemplo é BeamBetween, que cria um objeto Beam entre dois dados objetos do Attachment e mantém essas conexões orientadas de modo que o feixe esteja sempre voltado para cima. Essas instâncias poderiam ser clonadas de uma versão pré-fabricada em ReplicatedStorage ou passadas para new() como um argumento e armazenadas dentro do objeto sobself.
Instâncias correspondentes
Como mencionado acima, muitas classes neste projeto têm uma representação do modelo de dados, uma instância que corresponde à classe e é manipulada por ela.
Em vez de criar essas instâncias quando um objeto de classe é instanciado, o código geralmente opta por Clone() uma versão pré-fabricada da Instance armazenada no ReplicatedStorage ou ServerStorage. Embora fosse possível serializar as propriedades dessas instâncias e criá-las do zero nas funções new() da classe, fazê-lo tornaria a edição dos objetos muito trabalhosa e tornaria mais difícil para um leitor entendê-los. Além disso, clonar uma instância é geralmente uma operação mais rápida do que criar uma nova instância e personalizar suas propriedades em tempo de execução.
Composição
Embora a herança seja possível no Luau usando metatables, o projeto opta por permitir que classes se estendam através da composição. Ao combinar classes através de composição, o objeto "filho" é instanciado no método new() da classe e incluído como um membro sobself.
Para um exemplo disso em ação, veja a classe CloseButton, que envolve a classe Button.
Limpeza
Semelhante a como uma Instance pode ser destruída com o método Destroy(), classes que podem ser instanciadas também podem ser destruídas. O método de destruição para as classes do projeto é destroy(), com um d minúsculo para consistência de camelCase entre os métodos da base de código, além de distinguir entre as classes do projeto e as instâncias do Roblox.
O papel do método destroy() é destruir quaisquer instâncias criadas pelo objeto, desconectar quaisquer conexões e chamar destroy() em quaisquer objetos filhos. Isso é particularmente importante para conexões, pois instâncias com conexões ativas não são limpas pelo coletor de lixo do Luau, mesmo que não existam mais referências à instância ou conexões com a instância.
Singletons
Singletons, como o nome sugere, são classes para as quais apenas um objeto pode existir. Eles são o equivalente do projeto aos Serviços do Roblox. Em vez de armazenar uma referência ao objeto singleton e passar isso adiante no código Luau, Plant aproveita o fato de que exigir um ModuleScript armazena em cache seu valor retornado. Isso significa que exigir o mesmo ModuleScript singleton de lugares diferentes fornece consistentemente o mesmo objeto retornado. A única exceção a essa regra seria se ambientes diferentes (cliente ou servidor) acessassem o ModuleScript.
Singletons são distinguidos de classes instanciáveis pelo fato de não terem um método new(). Em vez disso, o objeto junto com seus métodos e estado é retornado diretamente pelo ModuleScript. Como os singletons não são instanciados, a sintaxe self não é usada e os métodos são chamados com um ponto (.) em vez de dois-pontos (:).
Verificação de tipo estrita
Luau suporta tipagem gradual, o que significa que você tem liberdade para adicionar definições de tipos opcionais a alguns ou todos os seus códigos. Neste projeto, a verificação de tipos strict é usada para todos os scripts. Esta é a opção menos permissiva para a ferramenta Análise de Script do Roblox e, portanto, a mais propensa a capturar erros de tipo antes do tempo de execução.
Sintaxe de classe tipada
A abordagem estabelecida para a criação de classes em Lua é bem documentada, no entanto, não é bem adequada para a tipagem forte do Luau. No Luau, a abordagem mais simples para obter o tipo de uma classe é o método typeof():
type ClassType = typeof(Class.new())
Isso funciona, mas não é muito útil quando sua classe é iniciada com valores que existem apenas em tempo de execução, por exemplo, objetos Player. Além disso, a suposição feita na sintaxe de classes idiomática do Lua é que declarar um método em uma classe self sempre será uma instância daquela classe; esta não é uma suposição que o mecanismo de inferência de tipos pode fazer.
Para suportar a inferência de tipos estrita, o projeto Plant usa uma solução que difere da sintaxe de classe idiomática do Lua em várias maneiras, algumas das quais podem parecer não intuitivas:
- A definição de self é duplicada, tanto na declaração de tipo quanto no construtor. Isso introduz um ônus de manutenção, mas os avisos serão sinalizados se as duas definições ficarem fora de sincronia.
- Métodos de classe são declarados com um ponto, para que self possa ser explicitamente declarado como do tipo ClassType. Os métodos ainda podem ser chamados com dois-pontos como esperado.
--!strict
local MyClass = {}
MyClass.__index = MyClass
export type ClassType = typeof(setmetatable(
{} :: {
property: number,
},
MyClass
))
function MyClass.new(property: number): ClassType
local self = {
property = property,
}
setmetatable(self, MyClass)
return self
end
function MyClass.addOne(self: ClassType)
self.property += 1
end
return MyClass
Tipos de lançamento após guardas lógicos
No momento da redação, o tipo de um valor não é reduzido após uma declaração condicional de guarda. Por exemplo, após a guarda abaixo, o tipo de optionalParameter não é reduzido para number.
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
print(optionalParameter + 1)
end
Para mitigar isso, novas variáveis são criadas após essas guardas com seu tipo explicitamente lançado.
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
local parameter = optionalParameter :: number
print(parameter + 1)
end
Percorrer hierarquias do DataModel
Em alguns casos, a base de código precisa percorrer a hierarquia do modelo de dados de uma árvore de objetos que são criados em tempo de execução. Isso apresenta um desafio interessante para a verificação de tipos. No momento da redação, não é possível definir uma hierarquia de modelo de dados genérica como um tipo. Como resultado, há casos onde as únicas informações de tipo disponíveis para uma estrutura de modelo de dados é o tipo da instância de nível superior.
Uma abordagem para esse desafio é lançar para any e depois refinar. Por exemplo:
local function enableVendor(vendor: Model)
local zonePart: BasePart = (vendor :: any).ZonePart
end
O problema com essa abordagem é que isso impacta a legibilidade. Em vez disso, o projeto usa um módulo genérico chamado getInstance para percorrer hierarquias de modelo de dados que lança internamente para any.
local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
end
À medida que a compreensão do mecanismo de tipos evolui em relação ao modelo de dados, é possível que padrões como esse não sejam mais necessários.
Interface do usuário
Plant inclui uma variedade de interfaces de usuário 2D complexas e simples. Isso inclui itens de exibição não interativos (HUD) como o contador de moedas e menus interativos complexos como a loja.
Abordagem de IU
Você pode comparar vagamente a IU do Roblox ao DOM do HTML, porque é uma hierarquia de objetos que descreve o que o usuário deve estar vendo. As abordagens para criar e atualizar uma IU do Roblox são amplamente divididas em práticas imperativas e declarativas.
| Abordagem | Vantagens e Desvantagens |
|---|---|
| Imperativa | Na abordagem imperativa, a IU é tratada como qualquer outra hierarquia de instâncias no Roblox. A estrutura da IU é criada antes do tempo de execução no Studio e adicionada ao modelo de dados, tipicamente diretamente no StarterGui. Então, em tempo de execução, o código manipula partes específicas da IU para refletir o estado que o criador requer. Essa abordagem vem com algumas vantagens. Você pode criar a IU do zero no Studio e armazená-la no modelo de dados. Essa é uma experiência de edição simples e visual que pode acelerar a criação da IU. Como o código da IU imperativa se preocupa apenas com o que precisa ser alterado, também torna simples implementar alterações na IU. Uma desvantagem notável é que, uma vez que as abordagens de IU imperativas requerem que o estado seja implementado manualmente na forma de transformações, representações complexas de estado podem se tornar muito difíceis de encontrar e depurar. É comum que erros surjam ao desenvolver código de IU imperativa, especialmente quando o estado e a IU se tornam desincronizados devido a várias atualizações interagindo em uma ordem inesperada. Outro desafio com abordagens imperativas é que é mais difícil dividir a IU em componentes significativos que podem ser declarados uma vez e reutilizados. Como a árvore inteira da IU é declarada no tempo de edição, padrões comuns podem ser repetidos em várias partes do modelo de dados. |
| Declarativa | Na abordagem declarativa, o estado desejado das instâncias da IU é declarado explicitamente, e a implementação eficiente desse estado é abstraída por bibliotecas como Roact ou Fusion. A vantagem dessa abordagem é que a implementação do estado se torna trivial e você só precisa descrever como deseja que sua IU pareça. Isso torna a identificação e a resolução de bugs significativamente mais fáceis. A principal desvantagem é ter que declarar a árvore inteira da IU no código. Bibliotecas como Roact e Fusion têm sintaxe para facilitar isso, mas ainda é um processo demorado e uma experiência de edição menos intuitiva ao compor a IU. |
Plant utiliza uma abordagem imperativa com a noção de que mostrar as transformações diretamente oferece uma visão mais eficaz de como a IU é criada e manipulada no Roblox. Isso não seria possível com uma abordagem declarativa. Algumas estruturas e lógicas de IU repetidas também são abstraídas em componentes reutilizáveis para evitar uma armadilha comum no design de IU imperativa.
Arquitetura de alto nível

Camada e componentes
No Plant, todas as estruturas da IU são uma Layer ou um Component.
- Layer é definida como um singleton de agrupamento de nível superior que envolve estruturas de IU pré-fabricadas em ReplicatedStorage. Uma camada pode conter um número de componentes ou pode encapsular sua própria lógica inteiramente. Exemplos de camadas são o menu de inventário ou o indicador de número de moedas na tela de exibição.
- Component é um elemento de IU reutilizável. Quando um novo objeto de componente é instanciado, ele clona um template pré-fabricado da ReplicatedStorage. Os componentes podem, por si mesmos, conter outros componentes. Exemplos de componentes são uma classe de botão genérico ou o conceito de uma lista de itens.
Gerenciamento de visualizações
Um problema comum de gerenciamento de IU é o gerenciamento de visualizações. Este projeto possui uma variedade de menus e itens de HUD, alguns dos quais ouvem a entrada do usuário, e um gerenciamento cuidadoso de quando eles estão visíveis ou habilitados é necessário.
Plant aborda esse problema com seu sistema UIHandler, que gerencia quando uma camada de IU deve ser visível ou não. Todas as camadas de IU na experiência são categorizadas como HUD ou Menu e sua visibilidade é gerenciada pelas seguintes regras:
- O estado habilitado das camadas Menu e HUD pode ser alternado.
- Camadas HUD habilitadas são mostradas apenas se nenhuma camada Menu estiver habilitada.
- Camadas Menu habilitadas são armazenadas em uma pilha, e apenas uma camada Menu é visível por vez. Quando uma camada Menu é habilitada, ela é inserida no topo da pilha e exibida. Quando uma camada Menu é desabilitada, ela é removida da pilha e a próxima camada Menu habilitada na fila é exibida.
Essa abordagem é intuitiva, pois permite que os menus sejam navegados com histórico. Se um menu for aberto a partir de outro menu, fechar o novo menu mostrará o menu antigo novamente.
Singletons de camadas de IU se registram com o UIHandler e são fornecidos com um sinal que dispara quando sua visibilidade deve mudar.
Leitura adicional
A partir desta visão abrangente do projeto Plant, você pode querer explorar os seguintes guias que aprofundam mais em conceitos e tópicos relacionados.
- Modelo Cliente-Servidor — Uma visão geral do modelo cliente-servidor no Roblox.
- Eventos Remotos e Callbacks — Tudo sobre eventos de rede remotos e callbacks para comunicação através da fronteira cliente-servidor.
- IU — Detalhes sobre objetos e design da interface do usuário no Roblox.