Plant é um jogo de referência onde os jogadores plantam e regam sementes, para que possam posteriormente colher e vender as plantas resultantes.

O projeto foca em casos de uso comuns que você pode encontrar ao desenvolver um jogo no Roblox. Quando aplicável, você encontrará notas sobre trade-offs, compromissos e a lógica por trás de várias escolhas de implementação, para que você possa tomar a melhor decisão para seus próprios jogos.
Obter o arquivo
- Navegue até a página do jogo Plant.
- Clique no botão ⋯ e Editar no Estúdio.
Casos de uso
Plant cobre os seguintes casos de uso:
- Persistência de dados de sessão e dados do jogador
- Gerenciamento de visualização da interface do usuário
- Rede cliente-servidor
- Experiência do Primeiro Usuário (FTUE)
- Compras de moeda dura e suave
Além disso, este projeto resolve conjuntos mais restritos de problemas que são aplicáveis a muitos jogos, incluindo:
- Personalização de uma área no local associada a um jogador
- Gerenciamento da velocidade de movimento do personagem do jogador
- Criação de um objeto que segue os personagens
- Detecção de em qual parte do mundo um personagem está
Observe que existem vários casos de uso neste jogo que são muito pequenos, muito nichados ou não demonstram uma solução para um desafio de design interessante; estes não são cobertos.
Estrutura do projeto
A primeira decisão ao criar um jogo é decidir como estruturar o projeto, que inclui principalmente onde colocar instâncias específicas no modelo de dados e como organizar e estruturar os pontos de entrada para o código do cliente e do servidor.
Modelo de dados
A tabela a seguir descreve em quais serviços de contêiner as instâncias do modelo de dados 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, à 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 o jogo. Quanto mais instâncias forem colocadas em ReplicatedFirst, mais longa será a espera para que elas se repitam antes que o código em ReplicatedFirst possa ser executado.
|
| ReplicatedStorage | Serve como um contêiner de armazenamento para todas as instâncias que precisam ser acessadas tanto pelo cliente quanto pelo servidor.
|
| ServerScriptService | Contém um Script que serve 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 no jogo. Sob SoundService, esses objetos Sound não têm posição e não são simulados em 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 no jogo, 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 fornece maior controle sobre a ordem em que os sistemas são executados.
As listas a seguir 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, pois 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 relevante do cliente ou do servidor. 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 dos jogos Roblox envolve algum elemento de comunicação entre o cliente e o servidor. Isso pode incluir o cliente solicitando que o servidor execute uma certa 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.
- Mensagens 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 se inscrever em mudanças, tornando-o ideal para replicar mudanças no estado do jogador do servidor para o cliente.
Por exemplo, em vez de disparar um UpdateCoins RemoteEvent para informar ao cliente quantas moedas ele tem, você pode chamar o seguinte e deixar o cliente se inscrever 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 personalizado para o cliente que é específico para uma determinada Instance, você pode usar atributos. O Roblox replica automaticamente valores de atributos, então você não precisa manter nenhum caminho de código para replicar o estado associado a um objeto. Outra vantagem é que essa replicação acontece juntamente com a própria instância.
Isso é particularmente útil para instâncias criadas em tempo de execução, pois atributos definidos em uma nova instância antes de ser parentada ao modelo de dados serão replicados atomicamente com a própria instância. Isso contorna qualquer necessidade de escrever código para "esperar" 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 GetAttributeChangedSignal() método. 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 uma 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 do cliente para o 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 dessa 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, especialmente para comunicação do servidor para o cliente.
Classes e singletons
Classes no projeto Plant, como instâncias no Roblox, podem ser criadas e destruídas. Sua sintaxe de classe é inspirada pela abordagem idiomática de Lua para programação orientada a objetos com várias mudanças para permitir suporte a verificação de tipo estrita.
Instanciação
Muitas classes no projeto estão associadas a uma ou mais Instances. Objetos de uma determinada classe são criados usando um método new(), consistente com a forma como 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 Attachment dados e mantém essas conexões orientadas para que o feixe esteja sempre voltado para cima. Essas instâncias podem ser clonadas a partir de uma versão pré-fabricada em ReplicatedStorage ou passadas para new() como um argumento e armazenadas dentro do objeto sob self.
Instâncias correspondentes
Como mencionado acima, muitas classes neste projeto têm uma representação no 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 em ReplicatedStorage ou ServerStorage. Embora seja 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 complicada e tornaria mais difícil para um leitor entender. 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 em Luau usando metatables, o projeto opta por permitir que classes se estendam umas às outras através da composição. Ao combinar classes através da composição, o objeto "filho" é instanciado no método new() da classe e é incluído como um membro sob self.
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 destrutor para classes do projeto é destroy() com um d minúsculo para consistência de camelCase em todos os métodos da base de código, bem como para 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 porque instâncias com conexões ativas não são limpas pelo coletor de lixo Luau, mesmo que não haja 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 passá-la ao redor no código Luau, Plant aproveita o fato de que requerer um ModuleScript armazena em cache seu valor retornado. Isso significa que requerer o mesmo ModuleScript singleton de diferentes lugares 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 que não têm um método new(). Em vez disso, o objeto junto com seus métodos e estado é retornado diretamente via o ModuleScript. Como 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ê é livre para adicionar definições de tipo opcionais a algum ou todo o seu código. Neste projeto, a verificação de tipo strict é usada para cada script. Esta é a opção menos permissiva para a ferramenta de Análise de Script do Roblox e, portanto, a mais propensa a capturar erros de tipo antes da execução.
Sintaxe de classe tipada
A abordagem estabelecida para criar classes em Lua é bem documentada, no entanto, não é bem adequada para tipagem forte em Luau. Em 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 só existem em tempo de execução, por exemplo, objetos Player. Além disso, a suposição feita na sintaxe de classe idiomática de Lua é que declarar um método em uma classe self será sempre uma instância dessa classe; essa não é uma suposição que o mecanismo de inferência de tipo pode fazer.
Para suportar a inferência de tipo estrita, o projeto Plant usa uma solução que difere da sintaxe de classe idiomática de Lua de 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 avisos serão sinalizados se as duas definições ficarem fora de sincronia uma com a outra.
- 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 MyClassTipos de cast 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)
endPara mitigar isso, novas variáveis são criadas após essas guardas com seu tipo explicitamente convertido.
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
local parameter = optionalParameter :: number
print(parameter + 1)
endPercorrer 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 tipo. No momento da redação, não é possível definir uma hierarquia de modelo de dados genérica como um tipo. Como resultado, existem casos em que a única informação de tipo disponível para uma estrutura de modelo de dados é o tipo da instância de nível superior.
Uma abordagem para esse desafio é converter para any e depois refinar. Por exemplo:
local function enableVendor(vendor: Model)
local zonePart: BasePart = (vendor :: any).ZonePart
endO problema com essa abordagem é que ela impacta a legibilidade. Em vez disso, o projeto usa um módulo genérico chamado getInstance para percorrer hierarquias de modelo de dados que converte para any internamente.
local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
endÀ medida que a compreensão do mecanismo de tipos do modelo de dados evolui, é possível que padrões como este não sejam mais necessários.
Interface do usuário
Plant inclui uma variedade de interfaces de usuário 2D complexas e simples. Estas incluem itens de exibição de heads up (HUD) não interativos, como o contador de moedas, e menus interativos complexos, como a loja.
Abordagem da UI
Você pode comparar vagamente a UI do Roblox ao DOM HTML, porque é uma hierarquia de objetos que descrevem o que o usuário deve estar vendo. As abordagens para criar e atualizar uma UI do Roblox são amplamente divididas em práticas imperativas e declarativas.
| Abordagem | Vantagens e desvantagens |
|---|---|
| Imperativa | Na abordagem imperativa, a UI é tratada como qualquer outra hierarquia de instâncias no Roblox. A estrutura da UI é criada antes da execução no Estúdio e adicionada ao modelo de dados, tipicamente diretamente em StarterGui. Então, em tempo de execução, o código manipula peças específicas da UI para refletir o estado que o criador requer. Essa abordagem vem com algumas vantagens. Você pode criar a UI do zero no Estúdio e armazená-la no modelo de dados. Esta é uma experiência de edição simples e visual que pode acelerar a criação da UI. Como o código da UI imperativa se preocupa apenas com o que precisa ser alterado, também torna mudanças simples na UI fáceis de implementar. Uma desvantagem notável é que, uma vez que as abordagens de UI imperativas exigem 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 UI imperativa, especialmente quando o estado e a UI se tornam desincronizados devido a múltiplas atualizações interagindo em uma ordem inesperada. Outro desafio com abordagens imperativas é que é mais difícil dividir a UI em componentes significativos que podem ser declarados uma vez e reutilizados. Como toda a árvore da UI é 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 UI é 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 UI pareça. Isso torna a identificação e resolução de bugs significativamente mais fáceis. A principal desvantagem é ter que declarar toda a árvore da UI em 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 UI. |
Plant usa uma abordagem imperativa sob a noção de que mostrar as transformações diretamente oferece uma visão mais eficaz de como a UI é criada e manipulada no Roblox. Isso não seria possível com uma abordagem declarativa. Algumas estruturas e lógicas de UI repetidas também são abstraídas em componentes reutilizáveis para evitar uma armadilha comum no design de UI imperativa.
Arquitetura de alto nível

Camada e componentes
No Plant, todas as estruturas da UI são ou uma Layer ou um Component.
- Layer é definida como um singleton de agrupamento de nível superior que envolve estruturas de UI pré-fabricadas em ReplicatedStorage. Uma camada pode conter vários componentes, ou pode encapsular sua própria lógica completamente. Exemplos de camadas são o menu de inventário ou o indicador de número de moedas na exibição de heads up.
- Component é um elemento de UI reutilizável. Quando um novo objeto de componente é instanciado, ele clona um template pré-fabricado de ReplicatedStorage. Componentes podem, por sua vez, conter outros componentes. Exemplos de componentes são uma classe de botão genérica ou o conceito de uma lista de itens.
Gerenciamento de visualização
Um problema comum de gerenciamento de UI é o gerenciamento de visualização. Este projeto tem uma variedade de menus e itens HUD, alguns dos quais escutam 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 UI deve ou não ser visível. Todas as camadas de UI no jogo 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ó são mostradas 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 na frente da pilha e mostrada. Quando uma camada Menu é desabilitada, ela é removida da pilha e a próxima camada Menu habilitada na fila é mostrada.
Essa abordagem é intuitiva porque 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.
Os singletons da camada de UI se registram com o UIHandler e recebem um sinal que é acionado quando sua visibilidade deve mudar.
Leitura adicional
A partir desta visão geral completa do projeto Plant, você pode querer explorar os seguintes guias que aprofundam 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.
- UI — Detalhes sobre objetos e design da interface do usuário no Roblox.