Projeto de referência de Plant

*Este conteúdo é traduzido por IA (Beta) e pode conter erros. Para ver a página em inglês, clique aqui.

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.

Banner do projeto Plant

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

  1. Navegue até a página da experiência Plant.
  2. 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çoTipos 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.

  • Na pasta Instances existe a GUI da tela de carregamento.
  • Na pasta Source existe o código da tela de carregamento e o código necessário para aguardar o restante da experiência ser carregado. O start LocalScript é o ponto de entrada para todo o código do lado do cliente no projeto.
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.

  • Na pasta Dependencies existem algumas bibliotecas de terceiros usadas pelo projeto.
  • Na pasta Instances existe uma ampla gama de instâncias pré-fabricadas.
  • Na pasta Source existe todo o código não necessário para o processo de carregamento que precisa ser acessível tanto do cliente quanto do 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.

  • Na pasta Instances existe um modelo de template Farm. Uma cópia deste é colocada em Workspace quando o jogador entra na experiência, onde será replicada para todos os jogadores.
  • Na pasta Source existe todo o código que é exclusivo para o servidor.
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.

Diagrama da arquitetura de sistemas do projeto Plant

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.

SistemaDescrição
Rede
  • Cria todas as instâncias RemoteEvent e RemoteFunction.
  • Expõe métodos para enviar e ouvir mensagens do cliente.
  • Validação de tipo para os argumentos recebidos do cliente em tempo de execução.
PlayerDataServer
  • Salva e carrega dados persistentes do jogador usando DataStoreService.
  • Armazena dados do jogador na memória e replica alterações para o cliente.
  • Expõe sinais e métodos para assinar, consultar e atualizar dados do jogador.
Mercado
  • Lida com transações de moeda suave do cliente.
  • Expõe um método para vender plantas colhidas.
CollisionGroupManager
  • Atribui modelos de personagem dos jogadores a grupos de colisão.
  • Configura grupos de colisão para que os personagens dos jogadores não colidam com vagões de plantas.
FarmManagerServer
  • Recria o modelo da fazenda de um jogador a partir dos dados do jogador quando ele entra na experiência.
  • Remove o modelo da fazenda quando um jogador sai.
  • Atualiza os dados do jogador quando a fazenda de um jogador é alterada.
  • Expõe um método para acessar a classe Farm associada a um determinado jogador.
PlayerObjectsContainer
  • Cria vários objetos associados ao tempo de vida de um jogador e fornece um método para recuperá-los.
TagPlayers
FtueManagerServer
  • Durante o FTUE, executa cada estágio e aguarda sua conclusão.
CharacterSpawner
  • Reaparece personagens quando eles morrem. Note que Players.CharacterAutoLoads foi desativado para que o spawn seja pausado até que os dados do jogador tenham sido carregados.

Cliente

Os seguintes sistemas estão associados ao cliente.

SistemaDescrição
Rede
  • Aguarda o servidor criar todas as instâncias RemoteEvent e RemoteFunction.
  • Expõe métodos para enviar e ouvir mensagens para e do servidor.
  • Impõe validação de parâmetros em tempo de execução.
  • Executa pcall() em funções remotas.
PlayerDataClient
  • Armazena os dados do jogador local na memória.
  • Expõe métodos e sinais para consultar e assinar mudanças nos dados do jogador.
MarketClient
  • Expõe um método para solicitar ao servidor a compra de um item por moeda suave.
LocalWalkJumpManager
  • Expõe métodos para modificar a WalkSpeed ou JumpHeight de um personagem por meio de multiplicadores para evitar conflitos ao modificar esses valores de vários lugares.
FarmManagerClient
  • Escuta por tags específicas do CollectionService sendo aplicadas às instâncias e cria "componentes" que adicionam comportamento a essas instâncias. Um "componente" refere-se a uma classe que é criada quando uma tag do CollectionService é adicionada a uma instância e destruída quando é removida; esses são usados para os prompts de CTA na fazenda e várias classes que comunicam o estado da fazenda ao jogador.
UISetup
  • Inicializa todas as camadas da IU.
  • Configura certas camadas para serem visíveis apenas em seções físicas do mundo.
  • Conecta um efeito especial de câmera para quando os menus estão habilitados.
FtueManagerClient
  • Configura estágios do FTUE no cliente.
CharacterSprint
  • Usa o LocalWalkJumpManager para aumentar a WalkSpeed quando um personagem jogador está fora de sua fazenda.

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 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.

AbordagemVantagens 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

Diagrama da arquitetura de IU do projeto Plant

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.
  • Luau — Detalhes sobre Luau, a linguagem de script criada pelo Roblox descendente do Lua 5.1.
  • 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.
©2026 Roblox Corporation, Roblox, o logotipo Roblox e Powering Imagination estão entre nossas marcas registradas e não registradas nos EUA e em outros países.