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

O projeto se concentra em casos de uso comuns que você pode encontrar ao desenvolver uma experiência no Roblox.Onde Aplicável, você encontrará notas sobre trocas, compromissos e a razão de várias escolhas de implementação, para que você possa tomar a melhor decisão para suas próprias experiências.
Obtenha o arquivo
- Navegue até a página de experiência Planta.
- Clique no botão ⋯ e Editar no Studio .
Usar casos
Planta abrange os seguintes casos de uso:
- Persistência de dados de sessão e dados do jogador
- Gerenciamento de visão da UI
- Rede cliente-servidor
- Experiência do Usuário pela Primeira Vez (FTUE)
- Compras de moeda dura e macia
Além disso, este projeto resolve conjuntos mais estreitos de problemas que são aplicáveis a muitas experiências, incluindo:
- Personalização de uma área no local que está associado a um jogador
- Gerenciando a velocidade de movimento do personagem do jogador
- Criando um objeto que segue os personagens ao redor
- Detectar em qual parte do mundo um personagem está
Observe que existem 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 são abrangidos.
Estrutura do projeto
A primeira decisão ao criar uma experiência é decidir como estruturar o projeto, que principalmente inclui onde colocar instâncias específicas no modelo de dados e como organizar e estruturar pontos de entrada para tanto o código do cliente quanto do servidor.
modelode dados
A tabela a seguir descreve em quais serviços de contêineres nas instâncias do modelo de dados são colocados.
Serviço | Tipos de instâncias |
---|---|
Workspace | Contém modelos estáticos que representam 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.: Também há um vazio Folder , ao 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 conjunto 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 tempo demorará para que elas se replicam antes que o código em ReplicatedFirst possa ser executar.:
|
ReplicatedStorage | Serve como um contêiner de armazenamento para todas as instâncias para as quais é necessário acesso tanto no cliente quanto no servidor.:
|
ServerScriptService | Contém um servindo como 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.Abaixo de 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 código dentro de reutilizável ModuleScripts que pode ser importado em toda a base de código.ModuleScripts são reutilizáveis, mas não executam por conta possuir; eles precisam ser importados por um Script ou LocalScript .Muitos projetos do Roblox terão um grande número de Script e LocalScript objetos, cada um pertencente a um comportamento ou sistema específico no jogo, criando vários pontos de entrada.
Para o microjogo Planta, 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 o 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 as compensações 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 alto nível de sistemas
Os sistemas de alto nível no projeto estão detalhados abaixo.Alguns desses sistemas são substancialmente mais complexos do que outros, e, em muitos casos, sua funcionalidade é abstrata através de uma hierarquia de outras Classes.

Cada um desses sistemas é um "único", pois é uma classe não instantânea que é inicializada em vez disso pelo script relevante do cliente ou do servidor start.Você pode ler mais sobre o padrão solteiro mais tarde neste guia.
Servidor
Os seguintes sistemas estão associados ao servidor.
Sistema | Descrição |
---|---|
Rede |
|
Servidor de Dados do Jogador |
|
Mercado |
|
Gerente de Grupo de Colisão |
|
Servidor de Gerenciamento de Fazenda |
|
Contêiner de Objetos de Jogador |
|
Jogadores de Etiqueta |
|
Servidor de Gerenciamento de Ftue |
|
Gerador de Personagens |
|
Cliente
Os seguintes sistemas estão associados ao cliente.
Sistema | Descrição |
---|---|
Rede |
|
Cliente de Dados de Jogador |
|
Cliente do Mercado |
|
Gerenciador de Pulo de Caminhada Local |
|
Cliente de Gerenciador de Fazenda |
|
Configuração de UI |
|
FtueManagerClient |
|
Corrida de Personagem |
|
Comunicação cliente-servidor
A maioria das experiências do Roblox envolve algum elemento de comunicação entre o cliente e o servidor.Isso pode incluir o cliente solicitando ao servidor 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 RemoteEvent e RemoteFunction objetos para diminuir a quantidade de regras especiais para acompanhar.Este projeto usa os seguintes métodos, em ordem de preferência:
- Replicação via atributos.
- Replicação via tags.
- Mensageria diretamente através do módulo Rede .
Replicação via sistema de dados do jogador
O sistema de dados do jogador permite que os dados sejam associados ao jogador que persiste entre as sessões de salvamento .Este sistema fornece replicação de cliente para servidor e um conjunto de APIs que podem ser usadas para consultar dados e se inscrever em alterações, tornando-o ideal para replicar alterações no estado do jogador do servidor para o cliente.
Por exemplo, em vez de disparar um personalizado UpdateCoins``Class.RemoteEvent para dizer ao cliente quantas moedas ele tem, você pode chamar o seguinte e deixar o cliente se inscrever nele através do evento PlayerDataClient.updated.
PlayerDataServer:setValue(player, "coins", 5)
Claro, isso só é útil para a replicação do servidor para o cliente e para valores que você deseja persistir entre as sessões, mas isso se aplica a um número surpreendente de casos no projeto, incluindo:
- A fase 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 em que o servidor precisa replicar um valor personalizado para o cliente que é específico para um determinado Instance, você pode usar atributos.O Roblox replica automaticamente os 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 ocorre ao lado da 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 serem parentados para o modelo de dados se replicarão atomicamente com a própria instância.Isso contorna qualquer necessidade de escrever código para "esperar" que dados adicionais sejam replicados via um RemoteEvent ou StringValue.
Você também pode ler diretamente os atributos do modelo de dados, do cliente ou do servidor, com o método GetAttribute(), e se inscrever em alterações com o método GetAttributeChangedSignal().No projeto Planta, esta 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 corda a um Instance . Isso é útil para categorizar instâncias e replicar essa categorização para o cliente.
Por exemplo, a etiqueta CanPlant é aplicada no servidor para sinalizar ao cliente que um determinado vaso é capaz de receber uma planta.
Mensagem diretamente via módulo de rede
Para situações em que nenhuma das opções anteriores se aplique, você pode usar chamadas de rede personalizadas através do módulo Rede .Esta é a única opção no projeto que permite a comunicação cliente-servidor e, portanto, é mais útil para transmitir solicitações do cliente e receber uma resposta do servidor.
Planta usa chamadas diretas de rede para uma variedade de solicitações de clientes, incluindo:
- Regando uma planta
- Plantando uma semente
- Comprar um item
A desvantagem com essa abordagem é que cada mensagem individual requer alguma configuração personalizada que pode aumentar a complexidade do projeto, embora isso tenha sido evitado sempre que possível, especialmente para a comunicação do servidor para o cliente.
Classe e singletons
Classes no projeto Planta podem ser criadas e destruídas.Sua síntese de classe é inspirada pela abordagem idiomática do Lua à programação orientada por objetos com uma série de alterações para habilitar o Suportea verificação de tipo rigorosa.
Instantância
Muitas classes no projeto estão associadas a uma ou mais Instances .Objetos de uma classe dada são criados usando um método new(), consistente com como instâncias são criadas no Roblox usando Instance.new().
Este 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 é que cria um objeto entre dois objetos dados e mantém esses anexos orientados para que o feixe sempre esteja 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 observado acima, muitas classes neste projeto têm uma representação de 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 do Instance armazenada sob ReplicatedStorage ou ServerStorage.Embora fosse possível serializar as propriedades dessas instâncias e criá-las do zero nas funções da classe new(), fazer isso tornaria a edição dos objetos muito complicada e os tornaria mais difíceis de serem analisados por um leitor.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 as classes se estendam umas às outras através de composição .Ao combinar classes através da composição, o objeto "filho" é instanciado no método new() da classe e é incluído como membro sob self .
Para um exemplo disso em ação, veja a classe CloseButton que envolve a classe Button.
Limpar
Semelhante ao modo como um Instance pode ser destruído com o método Destroy(), as classes que podem ser instanciadas também podem ser destruídas.O método destrutor para classes de projeto é com uma minúscula para consistência entre os métodos do código-base, 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 qualquer objeto filho.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 restem referências à instância ou conexões para 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 em volta no código Luau, Planta aproveita o fato de que exigir um ModuleScript cache seu valor retornado.Isso significa que exigir o mesmo singleton ModuleScript de diferentes locais consistentemente fornece o mesmo Objetoretornado.A única exceção a essa regra seria se diferentes ambientes (cliente ou servidor) acessassem o ModuleScript.
Singletons são distintos de classes instá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 via o ModuleScript .Como singletons não são instanciados, a síntese self não é usada e os métodos são chamados em vez disso com um ponto ( . ) em vez de um ponto ( : ).
Inferência de tipo rigorosa
Luau suporta digitação gradual, o que significa que você está livre para adicionar definições de tipo opcional a algum ou a todos os seus códigos.Neste projeto, strict verificação de tipo é usada para cada script.Esta é a opção menos permissiva para a ferramenta de análise de scripts do Roblox e, portanto, a mais provável de detectar erros de tipo antes da tempo de execução.
Síntese de classe tipada
A abordagem estabelecida para criar classes em Lua é bem documentada, no entanto, não é bem adequada para tipagem forte de 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 Player.Além disso, a suposição feita na síntax de classe idiomática em Lua é que declarar um método em uma classe self sempre será uma instância dessa classe; essa não é uma suposição que o mecanismo de inferência de tipo pode fazer.
A fim de suportar a inferência de tipo rigoroso, o projeto Planta usa uma solução que difere da síntese de classe Lua idiomática 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 fardo de manutenibilidade, mas avisos serão marcados se as duas definições saírem de sincronia umas com as outras.
- Métodos de classe são declarados com um ponto, então self pode ser declarado explicitamente de tipo ClassType.Métodos ainda podem ser chamados com um colon, como esperado.
--!stringente
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ógicas
No momento da escrita, o tipo de um valor não é restringido após uma declaração condicional de guarda.Por exemplo, após o guarda abaixo, o tipo de optionalParameter não é restringido a number.
--!stringente
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 esses guardas com seu tipo explicitamente cast.
--!stringente
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
local parameter = optionalParameter :: number
print(parameter + 1)
end
Transpor as hierarquias do Modelo de Dados
Em alguns casos, o 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 escrita, não é possível definir uma hierarquia de modelo de dados genérica como um digitar.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 é 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 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 lançam para any internamente.
local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
end
À medida que a compreensão do tipo do motor de dados evolui, é possível que padrões como este não sejam mais necessários.
Interface de usuário
Planta inclui uma variedade de interfaces de usuário 2D complexas e simples.Estes incluem itens de exibição não interativa (HUD) como o contador de moedas e menus interativos complexos como a comprar.
Aproximação de UI
Você pode comparar de forma desconectada a interface Roblox UI com o DOM HTML, porque é uma hierarquia de objetos que descreve o que o usuário deve estar vendo.As abordagens para criar e atualizar uma interface de usuário do Roblox estão amplamente divididas em imperativas e declarativas práticas.
Aproximação | Vantagens e desvantagens |
---|---|
Imperativo | Na abordagem imperativa, a UI é tratada como qualquer outra hierarquia de instâncias no Roblox.A estrutura da interface de usuário é criada antes do tempo de execução no Studio e adicionada ao modelo de dados, geralmente diretamente em StarterGui.Então, na hora de executar, o código manipula peças específicas da interface para refletir o estado que o criador requer.: Essa abordagem vem com algumas vantagens.Você pode criar a interface de usuário do zero no Studio e armazená-la no modelo de dados.Esta é uma experiência de edição simples e visual que pode acelerar a criaçõesde UI.Porque o código de UI imperativo só se preocupa com o que precisa ser alterado, também torna as alterações simples na interface fáceis de implementar.: Uma desvantagem notável é que, uma vez que abordagens imperativas de UI 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 imperativo da interface, especialmente quando o estado e a interface se tornam des sincronizados devido a várias atualizações interagindo em uma ordem inesperada.: Outro desafio com abordagens imperativas é que é mais difícil dividir a interface em componentes significativos que podem ser declarados uma vez e reutilizados.Como toda a árvore de UI é declarada no momento de edição, padrões comuns podem ser repetidos em várias partes do modelo de dados. |
Declarativo | Na abordagem declarativa, o estado desejado de instâncias de UI são declarados explicitamente e a implementação eficiente desse estado é abstrata por bibliotecas como Roact ou Fusion.: A vantagem dessa abordagem é a implementação do estado se tornar trivial e você só precisa descrever o que você quer que sua interface tenha.Isso torna a identificação e resolução de bugs significativamente mais fácil.: A principal desvantagem é ter que declarar toda a árvore de UI em código.Bibliotecas como Roact e Fusion têm síntese para tornar isso mais fácil, mas ainda é um processo demorado e uma experiência de edição menos intuitiva ao compor a interface de usuário. |
Planta usa uma abordagem imperativa sob a noção de que mostrar as transformações diretamente dá 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ógica de UI repetidas também são abstratas em componentes reutilizáveis para evitar um problema comum no design de UI imperativo.
Arquitetura de alto nível

Camada e componentes
Em Planta, todas as estruturas de UI são um Layer ou um Component.
- Layer é definido como um grupo de nível superior que envolve estruturas de UI pré-fabricadas em ReplicatedStorage .Uma camada pode conter um número de componentes ou pode encapsular toda a sua própria lógica.Exemplos de camadas são o menu de inventário ou o indicador de número de moedas na exibição de cabeçalho.
- Component é um elemento de UI reutilizável.Quando um novo objeto de componente é instanciado, ele clona um modelo pré-fabricado de ReplicatedStorage .Os componentes podem, em si, conter outros componentes.Exemplos de componentes são uma classe de botão genérica ou o conceito de uma lista de itens.
Visualizar manipulação
Um problema comum de gerenciamento de UI é o manuseio de visualização.Este projeto tem uma variedade de menus e itens do HUD, alguns dos quais ouvem a entrada do usuário e é necessária uma gestão cuidadosa de quando eles são visíveis ou ativados.
Planta aborda esse problema com seu sistema UIHandler que gerencia quando uma camada de UI deve ou não deve 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 de Menu e HUD camadas pode ser alternado.
- Camadas habilitadas HUD são mostradas somente se nenhuma camada Menu estiver habilitada.
- Camadas habilitadas Menu são armazenadas em uma pilha e apenas uma camada Menu é visível de cada vez.Quando uma camada Menu é habilitada, ela é inserida na frente da pilha e mostrada.Quando uma camada Menu é desativada, ela é removida da pilha e a próxima camada habilitada Menu 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.
Singletons de camada de UI se registram com o UIHandler e são fornecidos com um sinal que dispara quando sua visibilidade deve mudar.
Leitura adicional
A partir desta abrangente visão geral do projeto Planta, você pode querer explorar os seguintes guias que vão mais a fundo em conceitos e tópicos relacionados.
- Modelo Cliente-Servidor — Um resumo do modelo cliente-servidor no Roblox.
- Eventos e chamadas remotas — Tudo sobre eventos e chamadas de rede remotas para comunicação através da fronteira cliente-servidor.
- UI — Detalhes sobre objetos de interface do usuário e design no Roblox.