Planta é uma experiência de referência onde os jogadores plantam e regam sementes, para que possam 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 tradeoffs, 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.
Obter o Arquivo
- Navegue até a página de experiência Planta.
- Clique no botão ⋯ e Editar no Studio .
Use Cases
Planta cobre os seguintes casos de uso:
- Dados da sessão e dados do jogador persistência
- Gerenciamento de visão da interface
- Rede cliente-servidor
- Experiência do Usuário pela Primeira Vez (FTUE)
- Compras de moedas duras e molares
Além disso, esse projeto soluciona conjuntos mais estreitos de problemas que se aplicam a muitas experiências, incluindo:
- Personalização de uma área no local que está associada a um jogador
- Gerenciando a velocidade de movimento do personagem do jogador
- Criar um objeto que segue os personagens ao redor
- Detectando em qual parte do mundo um personagem está
Nota 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 são abordados.
Estrutura do Projeto
A primeira decisão ao criar uma experiência é decidir como estruturar o projeto, o que inclui principalmente onde colocar instâncias específicas no modelo de dados e como organizar e estruturar pontos de entrada para ambos o código do cliente e o servidor.
Modelo de Dados
A tabela a seguir descreve quais serviços de contêiner no modelo de dados são colocados.
Serviço | Tipos de Instâncias |
---|---|
Workspace | Contém modelos estáticos representando o mundo 3D, especificamente peças do mundo que não pertencem a nenhum jogador. Você não precisa Criar, modificar ou destruir essas instâncias em tempo de execução, então é aceitável deixar essas instâncias aqui. Há também um modelo de Class.Folder vazio, para o qual os modelos de fazenda dos jogadores serão adicionados em tempo de exec |
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 estiverem em ReplicatedFirst, maior será a espera delas para se replicar antes que o código em ReplicatedFirst possa ser executar. No pasta Instâncias existe a Interface gráfica do usuáriode carregamento. No pasta Fonte existe o código de carregamento e o código necessário para esperar o resto do código do jogo carregar. O 0> iniciar0> 3> Class.LocalScript |
ReplicatedStorage | Serviço como um contêiner de armazenamento para todas as instâncias para as quais acesso é necessário tanto no cliente quanto no servidor.
|
ServerScriptService | Contém um Script servindo como ponto de entrada para todo o código do lado do servidor no projeto. |
ServerStorage | Serviço 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 no espaço 3D. |
Pontos de Entrada
A maioria dos projetos organiza código dentro de reusável ModuleScripts que pode ser importado em toda a base de código. ModuleScripts
Para o Plant microgame, 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 suas necessidades, mas um ponto de entrada único fornece maior controle
As seguintes listas descrevem as tradeoffs de ambas as abordagens:
- Um único Script e um único LocalScript servidor e código de cliente respectivamente.
- Maior controle sobre a ordem em que diferentes sistemas são iniciados, pois todo o código é inicializado de um único script.
- Pode passar objetos por referência entre sistemas.
Arquitetura de Sistemas de Nível Alto
Os sistemas de nível superior no projeto são detalhados abaixo. Alguns desses sistemas são substancialmente mais complexos que outros, e em muitos casos, sua funcionalidade é abstrata em uma hierarquia de outras Classes.
Cada um desses sistemas é um "singleton," na medida em que é uma classe não instantânea que é inicializada pelo script start relevante. Você pode ler mais sobre o padrão de singleton mais tarde neste guia.
Servidor
Os seguintes sistemas estão associados ao servidor.
Sistema | Descrição |
---|---|
Redeira / Redeira | Cria todas as instâncias Class.RemoteEvent e Class.RemoteFunction. 0> Exposição de métodos para enviar e ouvir mensagens do cliente. 0>
|
Servidor de Dados do Jogador |
|
Mercado | Manipula transações de moedas suaves do cliente. Exibe um método para vender plantas colhidas. Exibe um método para vender plantas colhidas. |
Gerenciador de Colisão |
|
FarmManagerServer | Re-cria o modelo de fazenda de um jogador a partir de seus dados de jogador quando eles se juntam ao jogo. Remova o modelo de fazenda quando um jogador sai. Atualiza o método de acesso à classe Farm associada a um jog |
PlayerObjectsContainer |
|
Jogadores de TagPlayers |
|
FtueManagerServer |
|
Gerador de Personagens |
|
Cliente
Os seguintes sistemas estão associados ao cliente.
Sistema | Descrição |
---|---|
Redeira / Redeira |
|
PlayerDataCliente |
|
Cliente de Mercado |
|
Gerenciador de Caminhada Local |
|
Cliente FarmManager | Ouve atentamente para marcas específicas de Class.CollectionService ', que estão sendo aplicadas a instâncias, e cria aplicativos " components " que aplicam comportamento a essas instâncias. Um " componente " se refere a uma classe que é criada quando uma etiqueta Class.CollectionService é adicionada a uma instância e destruída quando for removida; estes são usados para os |
Configuração da Interface |
|
FtueManagerCliente |
|
Corrida de Personagens |
|
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 que o servidor execute uma certa ação e o servidor replicando atualizações ao cliente.
Neste projeto, a comunicação cliente-servidor é mantida o mais genérica possível, limitando o uso de objetos RemoteEvent e RemoteFunction para diminuir a quantidade de regras especiais para rastrear. Este projeto usa os seguintes métodos, na ordem de preferência:
- Replicação via o sistema de dados do jogador.
- Replicação via atributos.
- Replicação via tags .
- Mensageria diretamente via o 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 do cliente ao servidor e um conjunto de APIs que podem ser usadas para extrair dados e se inscrever em alterações, tornando-o ideal para replicar as alterações para o estado do jogador do servidor para o cliente.
Por exemplo, em vez de disparar um 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 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
- inventáriodo 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 valores de atributo, para que você não precise manter nenhum caminho de código para replicar o estado associado a um Objeto. Outra vantagem é que isso ocorre ao lado do próprio instância.
Isso é particularmente útil para instâncias criadas em tempo de execução, pois atributos definidos em uma nova instância antes de serem vinculados ao modelo de dados replicarão atomisticamente com a instância em si. Isso contorna qualquer necessidade de escrever código para "esperar" que dados extras sejam replicados via 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 nas alterações com o método GetAttributeChangedSignal(). No projeto Planta, esse método é usado para, entre outras coisas, replicar o
Replicação via Tags
CollectionService permite aplicar uma etiqueta de string a um Instance. Isso é útil para categorizar instâncias e replicar essa categoria para o cliente.
Por exemplo, a etiqueta CanPlant é aplicada no servidor para significar ao cliente que um determinado pote é capaz de receber uma planta.
Mensageria Diretamente via Modulo de Rede
Para situações em que nenhuma das opções anteriores se aplica, você pode usar chamadas de rede personalizadas através do módulo Rede . Essa é a única opção no projeto que permite a comunicação cliente-servidor e, portanto, é mais útil para transmitir pedidos de cliente e receber uma resposta do servidor.
Planta usa chamadas de rede diretas para uma variedade de pedidos de cliente, incluindo:
- Regando uma planta
- Plantando uma semente
- Comprando um item
O desafio com esta abordagem é que cada mensagem individual requer alguma configuração de personalização, o que pode aumentar a complexidade do projeto, embora isso tenha sido evitado o máximo possível, especialmente para comunicação do servidor ao cliente.
Classe e Singletons
Classe no projeto Planta, como instâncias no Roblox, pode ser criada e destruída. Sua sintaxe de classe é inspirada pelo idiomático Lua approach para Programação de objetos com uma série de alterações para habilitar o Suporte检查严格 para o tipo.
Inicialização
Muitas classes no projeto estão associadas a um 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 é BeamBetween que cria um objeto
Instâncias Cor correspondentes
Como notado 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 é instantiado, o código geralmente opta por Clone() uma versão pré-fabricada do
Composição
Embora a herança seja possível em Lua usando metatables, o projeto opta por permitir que as classes se estendam umas às outros através de composição . Ao combinar classes através de composição, o objeto "filho" é instanciado no método new() da classe e incluído como membro sob 1> self1>.
Para um exemplo disso em ação, veja a classe CloseButton que envolve a classe Button.
Limpar
Semelhante a como um Instance pode ser destruído com o método Destroy(), as aulas que podem ser instenciadas também podem ser destruídas. O método de destruidor para as aulas de projeto é destroy() com um 2>d2> menor
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 Lua, mesmo que não haja referências à instância ou conexões à instância.
Singletons
Singletons, como o nome indica, são classes para as quais apenas um objeto pode sempre existir. Eles são a versão do projeto do Serviços do Roblox. Em vez de armazenar uma referência
Singletons são distinguidos de classe instantâneas pelo fato de que eles 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 os singletons não são instantiados, a sintaxe self não é usada e os métod
Inferência de Tipo Rígido
Luau suporta a digitação gradual, o que significa que você está livre para adicionar definições de tipo opcional a alguns ou todos os seus códigos. Neste projeto, strict tipo de verificação de script é usado para todos os scripts. Esta é a opção menos permissiva para a ferramenta de análise de script do Roblox e, portanto, a mais provável de detectar erros de tipo antes do tempo de execução.
Sintaxe de Classe Escrita
A abordagem estabelecida para criar classes em Lua é bem documentada, no entanto, não é bem adequada para digitação forte Luau. Em Lua, 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 linguagem de script Lua idiomática é que a declaração de um método em uma classe self sempre será uma instância dessa classe; isso não é uma suposição que o tipo inferido pode fazer.
Para suportar a inferência de tipo rigoroso, o projeto Planta usa uma solução que difere da linguagem de script Lua idiomática em uma série de 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 custo de manutenção, mas avisos serão mostrados se as duas definições não estiverem em sincronia umas com as outras.
- Métodos de classe são declarados com um ponto, então self ainda pode ser declarado explicitamente como sendo do tipo ClassType. Métodos ainda podem ser chamados com um ponto como esperado.
--!estrutto
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
Casting Types After Logical Guards
No momento de escrever, o tipo de um valor não é restringido depois de uma declaração de condição de guarda. Por exemplo, seguindo a guarda abaixo, o tipo de optionalParameter não é restringido para number .
--!estrutto
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.
--!estrutto
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
local parameter = optionalParameter :: number
print(parameter + 1)
end
Viajando Modelos de Dados
Em alguns casos, a base de código precisa trafegar pela hierarquia de modelos de dados de uma árvore de objetos que são criados em tempo de execução. Isso apresenta um desafio interessante para o tipo de verificação. No momento da escrita, não é possível definir uma hierarquia de modelos de dados genéricos como um digitar. Como resultado, existem casos em que a única informação de tipo disponível para uma estrutura de modelos de dados é a do tipo de instância de nível superior
Um abordagem para esse desafio é usar any e depois refinar. Por exemplo:
local function enableVendor(vendor: Model)
local zonePart: BasePart = (vendor :: any).ZonePart
end
O problema com esta abordagem é que ela afeta a leitura. Em vez disso, o projeto usa um módulo genérico chamado getInstance para atravessar hierarquias de modelos de dados que são projetadas para any internamente.
local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
end
À medida que o tipo de motor entende o modelo de dados, é 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. Essas incluem itens não interativos de cabeça (HUD) como o contador de moedas e menus interativos complexos como a comprar.
Aproximação da Interface
Você pode comparar aproximadamente a Roblox UI com o DOM HTML, porque é uma hierarquia de objetos que descreve o que o usuário deve estar vendo. Aplicações para criar e atualizar uma UI Roblox são amplamente divididas em práticas de declaração e práticas de declaração .
Aproximação | Vantagens e Desvantagens |
---|---|
Imperativo | No abordagem imperativa, a UI é tratada como qualquer outra hierarquia |
Declarativo | No abordagem declarativa, o estado desejado das instâncias de UI é declarado explicitamente, e a implementação eficiente deste estado é abstrata por bibliotecas, como Roact ou Fusion |
Planta usa uma abordagem imperativa sob a noção de que mostrar as transformações diretamente dá uma visão mais efetiva 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 abstratas em componentes reutilizáveis para evitar uma armadilha comum no design de UI declarativo.
Arquitetura de Nível Alto
Camadas e Com componentes
Em Planta, todas as estruturas de UI são uma Layer ou um Component.
- Layer é definido como um grupo de nível superior que envolve estruturas de UI pré-fabricadas em ReplicatedStorage . Um layer pode conter um número de componentes ou pode encapsular sua própria lógica inteiramente. Exemplos de层 são o menu de inventário ou o número de moedas indicador no cabeçalho de exibição.
- Component é um elemento de UI reutilizável. Quando um novo objeto de componente é instenciado, ele clona um modelo pré-definido de ReplicatedStorage. Os componentes podem em si mesmos contener outros componentes. Exemplos de componentes são um clique de botão genérico ou o conceito de uma lista de itens.
Ver Gerenciamento
Um problema comum de gerenciamento de UI é o gerenciamento de visão. Este projeto tem uma variedade de menus e itens HUD, alguns dos quais ouvem a entrada do usuário e gerenciamento cuidadoso de quando eles são visíveis ou ativados é necessário.
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 classificadas como HUD ou 0> Menu0> e sua visibilidade é gerenciada pelas seguintes regras:
- O estado ativado de Menu e HUD camadas pode ser alternado.
- Os camadas HUD são mostrados apenas se nenhuma camada Menu for ativada.
- Os Menu camadas são armazenados em uma pilha, e apenas uma Menu camada é visível em um momento. Quando uma Menu camada é ativada, ela é inserida na frente da pilha e mostrada. Quando uma 2> Menu2> camada é desativada, ela é removida do Stack e
Essa abordagem é intuitiva porque permite que os menus sejam navegados com histórico. Se um menu for aberto de outro menu, fechar o novo menu mostrará o menu antigo novamente.
Os singletons da camada da interface registram-se com o UIHandler e são fornecidos com um sinal que dispara quando sua visibilidade deve mudar.
Leitura Adicional
A partir desta visão geral abrangente 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 olhar sobre o modelo cliente-servidor no Roblox.
- Eventos Remotos e Chamadas — Tudo sobre eventos remotos e chamadas para comunicação entre cliente e servidor.
- UI — Detalhes sobre objetos da interface do usuário e design na Roblox.